0

I've been struggling with this for a day or two and cannot find an answer anywhere. I thought this answer might help but it didn't.

In my sample code below I have two Timage Components each containing a "Start Image". When clicking on the "Start Button" two anonymous threads are created, one animates Image1 between the Start Image and End Image, the other does the same for Image2.

My problem is that when the KillAnimation boolean is set to True both animations should stop (which they do) but only one of the threads exit, the other one stops animating but leaves the image mid-animation.

I have the same problem if I use a predefined thread as well.

The sample application has only two images, the real world app can have anywhere from 15 to 24. Anonymous threads seem to suit because I can create them and no have to worry about defining up to 24 TThreads. I hope that makes sense.

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics,  FMX.Objects,
  FMX.Controls.Presentation, FMX.StdCtrls, FMX.Ani, FMX.Effects,
  FMX.Filter.Effects;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Image2: TImage;
    BtnStop: TButton;
    BtnStart: TButton;
    BtnReset: TButton;
    procedure BtnStopClick(Sender: TObject);
    procedure BtnStartClick(Sender: TObject);
    procedure BtnResetClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure createthread(TheImage: TImage; TargetBMP: String);
  end;

var
  Form1: TForm1;
  KillAnimation: Boolean;

implementation

{$R *.fmx}

procedure TForm1.BtnStopClick(Sender: TObject);
begin
  KillAnimation := true;
end;

procedure TForm1.createthread(TheImage: TImage; TargetBMP: String);
begin
  { The thread works right up until it is stopped. Even though two
    threads are started only one finishes. In addition, the images
    do not end up as the target image, and neither image can be
    reset even if I reload them from files. }

  TThread.CreateAnonymousThread(
    procedure()
    var
      Wiggle: TWiggleTransitionEffect;
      TheFloat: TFloatAnimation;
    begin
      TThread.NameThreadForDebugging('Animate ' + TheImage.Name);
      Wiggle := TWiggleTransitionEffect.Create(Nil);
      Wiggle.RandomSeed := 0.3;
      Wiggle.Progress := 0;
      Wiggle.Parent := TheImage;

      TThread.Synchronize(TThread.CurrentThread,
        procedure()
        Begin
          Wiggle.Target.LoadFromFile(TargetBMP)
        end);

      TheFloat := TFloatAnimation.Create(Nil);
      TheFloat.Parent := Wiggle;
      TheFloat.PropertyName := 'Progress';
      TheFloat.Duration := 2;
      TheFloat.AutoReverse := true;
      TheFloat.Loop := true;
      TheFloat.StartValue := 0;
      TheFloat.StopValue := 100;
      TheFloat.StartFromCurrent := false;
      TheFloat.start;

      while not KillAnimation do
        application.handlemessage;

      TheFloat.stop;
    end).start;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin

  Image1.Bitmap.LoadFromFile('c:\sample\startimage.png');
  Image2.Bitmap.LoadFromFile('c:\sample\startimage.png');

end;

procedure TForm1.BtnStartClick(Sender: TObject);
begin
  KillAnimation := false;
  createthread(Image1, 'c:\sample\endimage.png');
  createthread(Image2, 'c:\sample\endimage.png');

end;

procedure TForm1.BtnResetClick(Sender: TObject);
begin
  KillAnimation := false;
  Image1.Bitmap.LoadFromFile('c:\sample\startimage.png');
  Image2.Bitmap.LoadFromFile('c:\sample\startimage.png');
end;

end.

I would have thought that creating the Transition and FloatAnimation within the thread meant they would be destroyed when done because FreeOnTerminate is True.

When I try to reset the Images using "image1.bitmap.loadfromfile" it doesn't change.

The images are 170 x 170 png files.

What have I done wrong here?

My goal is to pass TImage and an Image File to the thread, let it animate until told to stop. It is unlikely that I'll need all 24 images to animate, but you never know.

Apologies if I shouldn't have posted all the code from my sample. At least you can see everything going on.

Craig
  • 68
  • 5
  • 2
    First, using a thread to handle calls directly to a GUI component is not recommended. You are also calling `Application.HandleMessage` from the thread, which is not correct. `Wiggle` and `TheFloat` has to be finalized by code. I suggest you try to make all this to work in the main thread instead. – LU RD Nov 06 '17 at 09:48
  • @LURD I have tried the animation within the main UI Thread but buttons and others become unresponsive, particularly when there are 4 or 5 animations repeating until a button is pressed. I thought the FMX components, including Transitions & FloatAnimate were thread safe. Interestingly application.processmessages didn't work and the animation would not play. Thanks for you comment and thoughts :) – Craig Nov 06 '17 at 10:36
  • @LURD I'm also puzzled by the fact that 2 threads start but only 1 ends. It's a bit weird because the code is identical. Both animations stop, but a thread is left dangling in the wind. – Craig Nov 06 '17 at 11:01
  • It is hard to comment on why the code working in the main thread is unresponsive, without seeing code. Always assume FMX and VCL components to be working in the main thread only. Screw up that convention and expect bad things happening. – LU RD Nov 06 '17 at 12:40
  • I'm not familiar with FMX, but in VCL, calling Application.Handle causes the thread to create itself a message queue, and then wait on it. I'd expect an anonymous thread to receive next to no messages beside broadcasts, so it's normal to see the thread freezes on `handlemessage` and not return. I'm actually surprised that any of your thread actually exit at all. Whether or not this is valid for FMX, that's your homework to find out! ;) – Ken Bourassa Nov 06 '17 at 14:51
  • @kenbourassa My intention with handlemessage was simply to give it something to do until KillAnimation was set to True. I did remove it after LU-RD's comments and the thread did die, but the animation would not run. More homework for me I think :) – Craig Nov 07 '17 at 00:11

1 Answers1

3

You should NOT be using a thread for this at all. UI elements, including visual effects, should be used only in the main UI thread. And unless you are developing your app for mobile platforms, objects are not destroyed automatically when they go out of scope, you have to destroy them yourself when you are done using them, or else assign them an Owner that will destroy them for you.

Try this instead:

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Objects, FMX.Controls.Presentation,
  FMX.StdCtrls, FMX.Ani, FMX.Effects, FMX.Filter.Effects;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Image2: TImage;
    BtnStop: TButton;
    BtnStart: TButton;
    BtnReset: TButton;
    procedure BtnStopClick(Sender: TObject);
    procedure BtnStartClick(Sender: TObject);
    procedure BtnResetClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    Float1: TFloatAnimation;
    Float2: TFloatAnimation;
    function PrepareEffect(TheImage: TImage; const TargetBMP: String): TFloatAnimation;
    procedure ResetImages;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

procedure TForm1.BtnResetClick(Sender: TObject);
begin
  ResetImages;
end;

procedure TForm1.BtnStartClick(Sender: TObject);
begin
  Float1.start;
  Float2.start;
end;

procedure TForm1.BtnStopClick(Sender: TObject);
begin
  Float1.stop;
  Float2.stop;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  ResetImages;
  Float1 := PrepareEffect(Image1, 'c:\sample\endimage.png');
  Float2 := PrepareEffect(Image2, 'c:\sample\endimage.png');
end;

function TForm1.PrepareEffect(TheImage: TImage; const TargetBMP: String): TFloatAnimation;
var
  Wiggle: TWiggleTransitionEffect;
  TheFloat: TFloatAnimation;
begin
  Wiggle := TWiggleTransitionEffect.Create(Self);
  Wiggle.RandomSeed := 0.3;
  Wiggle.Progress := 0;
  Wiggle.Parent := TheImage;
Wiggle.Target.LoadFromFile(TargetBMP);

  TheFloat := TFloatAnimation.Create(Self);
  TheFloat.Parent := Wiggle;
  TheFloat.PropertyName := 'Progress';
  TheFloat.Duration := 2;
  TheFloat.AutoReverse := true;
  TheFloat.Loop := true;
  TheFloat.StartValue := 0;
  TheFloat.StopValue := 100;
  TheFloat.StartFromCurrent := false;

  Result := TheFloat;
end;

procedure TForm1.ResetImages;
begin   
  Image1.Bitmap.LoadFromFile('c:\sample\startimage.png');
  Image2.Bitmap.LoadFromFile('c:\sample\startimage.png');
end;

end.
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thanks @Remy. It will indeed be a Mobile app eventually. I had originally done it within the main UI but other buttons would not respond fast enough given that there might be up to 15 or more animations playing. It's a slot machine (for my wife). All the reels rotate within threads (5 of them) and all work and exit correctly. I'll work on it some more and do the tile animations in the main UI as best I can. Thanks for you help :) – Craig Nov 06 '17 at 23:29
  • Fun Fact: ResetImages procedure works only when first run. After the animation the "start image" is not shown. At first run the Timages have no children, after running they do so I'll look into that. – Craig Nov 06 '17 at 23:38
  • 2
    You can't run an animated visual effect in a worker thread and have it manipulate a UI control without syncing with the main UI thread. The effect has to perform its manipulations in the main UI thread only, in which case the worker thread becomes useless. If you have a bunch of animated effects running and your UI is too slow to respond to user interactions, that suggests that either the effects are inefficient, or you are doing something else to cause blockage in the main UI thread when you shouldn't be. – Remy Lebeau Nov 07 '17 at 01:52