2

I'm trying to show an information message and an animated gif (an Hourglass) while my application is busy (loading a query).

I have defined a Form to show that Message (using the code shown in this post: How to use Animated Gif in a delphi form). This is the constructor.

constructor TfrmMessage.Show(DisplayMessage: string);
begin
  inherited Create(Application);
  lblMessage.Caption := DisplayMessage;
  // Set the Message Window on Top
  SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NoMove or SWP_NoSize);
  Visible := True;
  // Animate the HourGlass image
  (imgHourGlass.Picture.Graphic as TGIFImage).Animate := True;
  Update;
end;

The problem is that the animated Gif remains still while the main thread is busy (loading the query).

I have tried drawing manually the animation on a separate thread.

type
  TDrawHourGlass = class(TThread)
    private
      FfrmMessage: TForm;
    public
      constructor Create(AfrmMessage: TForm);
      procedure Execute; override;
      procedure ShowFrame1;
      procedure ShowFrame2;
      procedure ShowFrame3;
      procedure ShowFrame4;
      procedure ShowFrame5;
  end;

constructor TDrawHourGlass.Create(AfrmMessage: TForm);
begin
  inherited Create(False);
  FfrmMessage := AfrmMessage;
end;

procedure TDrawHourGlass.Execute;
var FrameActual: integer;
begin
  FrameActual := 1;
  while not Terminated do begin
    case FrameActual of
      1: Synchronize(ShowFrame1);
      2: Synchronize(ShowFrame2);
      3: Synchronize(ShowFrame3);
      4: Synchronize(ShowFrame4);
      5: Synchronize(ShowFrame5);
    end;
    FrameActual := FrameActual + 1;
    if FrameActual > 6 then FrameActual := 1;
    sleep(200);
  end;
end;

procedure TDrawHourGlass.ShowFrame1;
begin
  (FfrmMessage as TfrmMessage).imgHourGlass.Picture.Bitmap.Assign((FfrmMessage as TfrmMessage).Frame1.Picture.Graphic);
  (FfrmMessage as TfrmMessage).imgHourGlass.Update;
end;

implementation

procedure TDrawHourGlass.ShowFrame2;
begin
  (FfrmMessage as TfrmMessage).imgHourGlass.Picture.Bitmap.Assign((FfrmMessage as TfrmMessage).Frame2.Picture.Graphic);
  (FfrmMessage as TfrmMessage).imgHourGlass.Update;
end;

procedure TDrawHourGlass.ShowFrame3;
begin
  (FfrmMessage as TfrmMessage).imgHourGlass.Picture.Bitmap.Assign((FfrmMessage as TfrmMessage).Frame3.Picture.Graphic);
  (FfrmMessage as TfrmMessage).imgHourGlass.Update;
end;

procedure TDrawHourGlass.ShowFrame4;
begin
  (FfrmMessage as TfrmMessage).imgHourGlass.Picture.Bitmap.Assign((FfrmMessage as TfrmMessage).Frame4.Picture.Graphic);
  (FfrmMessage as TfrmMessage).imgHourGlass.Update;
end;

procedure TDrawHourGlass.ShowFrame5;
begin
  (FfrmMessage as TfrmMessage).imgHourGlass.Picture.Bitmap.Assign((FfrmMessage as TfrmMessage).Frame5.Picture.Graphic);
  (FfrmMessage as TfrmMessage).imgHourGlass.Update;
end;

But I get the same result, while the main thread is busy the animation remains still, because the calls (FfrmMessage as TfrmMessage).imgHourGlass.Update; to draw each frame, waits until the main thread has finished (even when not calling them within a Synchronize).

Do you have a suggestion what can I also try ?.

Thank you.

Marc Guillot
  • 5,367
  • 11
  • 30
  • Are you using FireDAC or another library? – Victoria Jul 19 '17 at 14:18
  • Yes, FireDAC (calling a Datasnap remote method that returns a Dataset). – Marc Guillot Jul 19 '17 at 15:18
  • Well, then it's not a data component that needs to work in the background but a REST call (why would you need to display such dialog on server?). FireDAC has support for asynchronous mode, but for REST server it's unwanted. It has some threading model, as far as I remember. – Victoria Jul 19 '17 at 15:34
  • I want to put it on the client. When I call the remote method (using a FDStoredProcedure component) I show a message of "loading data ...", and I wanted to also display an animated hourglass. – Marc Guillot Jul 19 '17 at 15:39
  • 1
    I see. Then create a thread in which you'll be waiting for an execution event, do the REST call and post the received dataset as a parameter of a message posted to an invisible window created by `AllocateHwnd`, or use `Synchronize` to pass the dataset to the main thread. The animation keep running in the main thread. Create that form when the thread starts, destroy it when thread is terminated or starts waiting for another execution. – Victoria Jul 19 '17 at 15:46
  • Thanks Victoria, tomorrow I will try it. – Marc Guillot Jul 19 '17 at 15:49
  • If you need to help with a code example, let me know. I can craft a short example. – Victoria Jul 19 '17 at 15:56
  • Thank you very much, I will open a new question if that's the case, but I'm certain I can manage. I just hoped to find a solution that didn't involve having to change my current code (I will add those messages to every remote call I have in my program). – Marc Guillot Jul 19 '17 at 16:14
  • 1
    No, so long you keep the main thread message loop blocked by a long running synchronous call, any worker thread won't help you (`Synchronize` will execute after such call finishes). Rule of thumb for a responsible UI is, keep the main thread without any long running blocking calls (which such REST call is, so move that call to a worker thread). – Victoria Jul 19 '17 at 16:36

2 Answers2

4

It's very unfortunate that the many components in Delphi basically encourage poor application design (blocking the main thread). In situations like this, you should seriously consider swapping around the purpose of your thread, so that all lengthy processing is done inside of a thread (or multiple), and leave all the drawing up to the main UI thread. There aren't many clean ways to make the main thread responsive while it's processing any amount of data.

Jerry Dodge
  • 25,720
  • 28
  • 139
  • 301
  • I would not say that data components do that. If you're speaking about binded data aware controls, then simply unbind them, do the work in the worker thread and bind them back. Or if the component supports asynchronous execution, you can give that a try. – Victoria Jul 19 '17 at 14:05
  • @Victoria Indeed, certainly not all components, and not just a few either. Many of them, by nature, lock the main thread. And I don't mean anything with binding or data-aware components. Just executing a simple query on a table with millions of records is enough to lock the main thread - of course, depending on which components are being used. – Jerry Dodge Jul 19 '17 at 14:09
  • I've never met what you say. If you execute query in a worker thread, it doesn't block the main one in general. We need more information about used technology to be more specific. – Victoria Jul 19 '17 at 14:12
  • 1
    @Victoria Exactly, that's why I'm recommending it as a solution. In the original question: "The problem is that the animated Gif remains still while the main thread is busy (loading the query)" I don't understand your comment. The solution is the same, even for other components which have nothing to do with a database. – Jerry Dodge Jul 19 '17 at 14:15
  • It's a REST call that needs to work in the background (see the answer to my question comment). Besides, FireDAC offers asynchronous execution modes, but not for this case. Here you are right in swapping the logic. – Victoria Jul 19 '17 at 15:38
  • Does someone disagree? Or just wanted to see my rep at 13,333? :-) – Jerry Dodge Jul 20 '17 at 00:49
3

If it's only for a query and you're using FireDAC, then check out http://docwiki.embarcadero.com/RADStudio/Berlin/en/Asynchronous_Execution_(FireDAC) it seems to be possible.

To handle any kind of lengthy processing, you can use the Threading unit. You don't do the work in the main thread so the UI can be displayed correctly.

This example is not perfect (you should probably use some kind of callback), but the gif is spinning.

procedure TForm3.ButtonProcessClick(Sender: TObject);
begin
  // Block UI to avoid executing the work twice
  ButtonProcess.Enabled := false;
  TTask.Create(
    procedure
    begin
      Sleep(10000);
      // Enable UI again
      ButtonProcess.Enabled := true;
    end).Start();
end;

To make the gif spin in the first place I use :

procedure TForm3.FormCreate(Sender: TObject);
begin
  (GifLoading.Picture.Graphic as TGIFImage).Animate := true;
end;

I haven't tried, but this link seems to provide something very close from what you want.

Hope this helps.

Etsitpab Nioliv
  • 224
  • 1
  • 2
  • 13
  • Why would you use asynchronous execution on REST server? It's the REST client call that needs to run (and display that dialog) in background. – Victoria Jul 19 '17 at 15:47
  • @Victoria I can think of plenty reasons. One of my own projects is an emulator for a credit card machine, which has a REST server inside of it, receiving commands. – Jerry Dodge Jul 19 '17 at 15:50
  • @Victoria I haven't seen OP mentioning REST. The TForm3 class of my example suggests we're in a classic VCL heavy client application. – Etsitpab Nioliv Jul 19 '17 at 15:51
  • I see. Here is [this comment](https://stackoverflow.com/questions/45190841/show-a-form-with-a-message-and-an-animated-image-while-the-application-is-busy#comment77356861_45190841). – Victoria Jul 19 '17 at 15:59