0

I have a program which is currently converting some large UTF16 font files (on the order of megabytes), which come 3 to a set. This program takes these font files, reduces it down to only the characters used, and saves off the needed characters. The program itself works fine, but the loading process takes around 25 seconds threaded and around 40-45 seconds unthreaded. I was thinking it'd be nice to have a progress bar in there to show the progress of the loading threads. I found a guide here for how to handle this with a message (haven't been able to test whether my solution works yet or not).

I made a new form with three progress bars, and was going to wiring it up so that they'd report their progress while loading. The problem is, when I call the ShowModal method on the form, the form loads up without the controls (just a blank white window), does the loading, and when it's finished, the controls appear and I can exit the window and the process is finished.

The question I have is, how do I get all the controls to appear first before it starts loading? This is something I'm not quite sure how to ask (hence the explanation and question) and my research hasn't given anything (I'm guessing it's one of those "you need to know the right words" scenarios).

Things I've tried:

  • Put the code-to-be-ran in the OnShow and OnCreate events. No luck.
  • People are saying the OnActivate is the event that I should use, but using that event gives the same issue.
  • Tried doing away with an event. Called Show(), then my Load() function, and finally Close(). No luck.
  • Just Show() the form, manually do the logic, and then Close().
  • Looking on this site thoroughly
  • Checked to see if Embarcadero offers something like this already that I was unaware of (if there is I'm still unaware).

I suppose this feature isn't important, but a 30 second block is a long time without a notice that something is happening.

Community
  • 1
  • 1
Zulukas
  • 1,180
  • 1
  • 15
  • 35
  • 1
    Don't do anything that would block in the main thread before the form is displayed in full and you won't have a problem. – Sertac Akyuz Apr 06 '17 at 00:02
  • 1
    The usual solution to this is to post a custom message to yourself at the end of OnShow, and start your processing in a handler for that message. This allows all other messages to be processed first. – Ken White Apr 06 '17 at 02:28

2 Answers2

0

What you should do is to separate the worker thread from the UI form. Do your work in a TThread descendant.

All the communication between the two should be done by posting asynchronous messages. The worker thread should post messages to the UI when it starts, to report progress, and when it ends. The UI can use this to visualize the form, update the progress bars and eventually close the form. In case you need to have a user cancel button on the UI, this should post a message to the worker thread that it uses to abort the execution.

By "posting asynchronous messages" I mean you should either use a messaging framework other that the RTL.Messaging (it only handles synchronous messages) or at least wrap the calls in TThread.Queue. I do all the messaging with my messaging classes explained here.

To handle the work I myself use this base class:

  TMEBatchOp = Class(TMELocalizableComponent)
  Private
    FMessageHandler:   TMEMessageHandler;
    FInfo:             TMEBatchOpInfo;
    FLastUpdateStatus: Cardinal;
    FRequestedCancel:  Boolean;
    Procedure CheckCancelled;
    Procedure Run;
    Procedure SetResult(Const Value: TMEBatchOpResult);
    Procedure SetStatus(Const Value: TMEBatchOpStatus);
    Procedure UpdateStatus(Const DelaySecs: Integer = 0);
  Protected
    Function CalcWorkMax: Integer; Virtual;
    Procedure DoExecute; Virtual; Abstract;
    Procedure DoSetup; Virtual;
    Procedure DoTeardown; Virtual;
    Function GetAllowCancel: Boolean; Virtual;
    Function GetDescription: String; Virtual;
    Procedure ReceiveEnvelope(Const Envelope: TMEMessageEnvelope);
    Property MessageHandler: TMEMessageHandler Read FMessageHandler;
    Property Info: TMEBatchOpInfo Read FInfo;
    Property RequestedCancel: Boolean Read FRequestedCancel;
  Public
    Class Procedure CaptureLocalizable(Localizer: TMELocalizer); Override;
    Class Function GetModuleName: String;
    Constructor Create(AOwner: TComponent); Override;
    Procedure Worked(Qty: Integer = 1);
  End;

Implementation of a specific worker thread requires defining a subclass of TMEBatchOp and implementing the DoExecute method. Optionally CalcWorkMax, DoSetup, and DoTeardown can also be redefined if needed. For Example:

Type
  TBatchOpSaveTexts = Class(TMEBatchOp)
  Protected
    Function CalcWorkMax: Integer; Override;
    Procedure DoExecute; Override;
    Function GetDescription: String; Override;
  Public
    Class Procedure CaptureLocalizable(Localizer: TMELocalizer); Override;
  End;

Executing this just requires a call like this:

RunAndFree(TBatchOpSaveTexts);

Where RunAndFree is implemented like this:

Function RunAndFree(BatchOpClass: TMEBatchOpClass): TMEBatchOpResult; Overload;
Begin
  Result := RunAndFree(BatchOpClass.Create(Nil));
End;

Function RunAndFree(BatchOp: TMEBatchOp): TMEBatchOpResult; Overload;
Begin
  Try
    BatchOp.Run;
    Result := BatchOp.Info.Result;
  Finally
    BatchOp.Free;
  End;
End;

TMEBatchOpInfo contains info on work result and status, times and work done and to do and progress. It also contains info on whether the operation can be cancelled. All messages contain a copy of this info, so the UI can update itself accordingly. Here is its interface:

  TMEBatchOpInfo = Class(TMEPersistent)
  Private
    FID:          String;
    FParentID:    String;
    FDescription: String;
    FTimeBegin:   TDateTime;
    FTimeEnd:     TDateTime;
    FResult:      TMEBatchOpResult;
    FWorkMax:     Integer;
    FWorkDone:    Integer;
    FAllowCancel: Boolean;
    FStatus:      TMEBatchOpStatus;
    Function GetDone: Boolean;
  Public
    Constructor Create; Override;
    Procedure Assign(Source: TPersistent); Override;
    Property ID: String Read FID;
    Property ParentID: String Read FID;
    Property Description: String Read FDescription;
    Property TimeBegin: TDateTime Read FTimeBegin;
    Property TimeEnd: TDateTime Read FTimeEnd;
    Property Result: TMEBatchOpResult Read FResult;
    Property WorkMax: Integer Read FWorkMax;
    Property WorkDone: Integer Read FWorkDone;
    Property Done: Boolean Read GetDone;
    Property AllowCancel: Boolean Read FAllowCancel;
    Property Status: TMEBatchOpStatus Read FStatus;
  End;
Community
  • 1
  • 1
Frazz
  • 2,995
  • 2
  • 19
  • 33
  • Just to see if I understand exactly, I open my form, the form spawns a thread which will block until it receives a message from the form to start. When the thread receives the green light, it starts doing its thing and sends updating messages to the form for progress updating, and then ultimately lets the form know that it's done? – Zulukas Apr 06 '17 at 15:46
  • It could work, but I do it slightly differently. I spawn the thread that sends a message that it is starting. My main form receives the message and pops up a form with a progress bar, description of the task and work stats. The thread sends messages with progress and eventually end of job. The progress popup consumes those messages to update the UI and eventually to close itself when the task is done. – Frazz Apr 07 '17 at 07:25
-1

What I do, that doesn't require a thread, is to create the window, DisableTaskWindows(), force a repaint with TForm.Repaint(), then do the work. Something like this:

var
  LWindowList: pointer;
  LForm: TProgressForm;
begin
  LForm := TProgressForm.Create(nil);
  try
    LWindowList := DisableTaskWindows(LForm.Handle);
    try
      LForm.Show;

      LForm.Repaint;

      // Do your work here.
      // When updating your UI, be sure to call LForm.Repaint()
      // or Application.ProcessMessages();

    finally
      EnableTaskWindows(LWindowList);
    end;
  finally
    LForm.Free;
  end;
end;

Note: this was typed directly into the answer, and may not compile.

Nat
  • 5,414
  • 26
  • 38