Children Of Threadmare

by Nikolai Sklobovsky,
Senior System Analyst,
Retail Technologies International, Inc.

Table Of Contents

Introduction

In the previous article (Waking from Threadmare) we discussed different problems a novice programmer may encounter when dealing with multiple threads. We also outlined a few strategies for solving those problems. Our main weapon in a fight against the Lord Threadd was a powerful component named TActivityProcessLog and its light-weight sidekick TCustomActivityLogClient. Our sample application provided the diligent reader with answers to the natural how's and why's. But due to the obvious article space limitations, we could only scratch the surface of its possible uses. Now it is time to take a closer look at the client side and see what perks are immediately available for us. In the course of this article we'll discuss it in greater details and learn how to fulfill various tasks, from the simplest to the most sophisticated ones. Our examples will cover the following widely used areas

In contrast to the previous article that pursued mostly conceptual goals, this text takes a more practical approach. This means you will see a lot of "inline" code. And, of course, complete sources are available separately for download.

back to the top

Auxiliary tools

Before we can proceed with our main goal of catching thread/process feedback, we need to develop some simple visual tool for displaying it. What kind of tool do we need? Probably - something simple where we can display a line of text and eventually replace this text with a new line while necessarily disabling user's capability to manipulate the application itself.

To achieve this task we are going to create a special form, conveniently named TDisplayForm. It's interface portion is shown in Listing1.

Listing 1

  TDisplayForm = class(TRunTimeForm)
  private
    FPanel: TPanel;
    FTaskList: pointer;
    FCursor: TCursor;
    FPumpOnUpdate: boolean;
    FDoNotDecreaseAutoWidth: boolean;
    FCancelled: boolean;
    procedure FormKeyPress(Sender: TObject; var Key: Char);
  protected
    procedure DoShow; override;
    procedure DoHide; override;
  public
    constructor Create; reintroduce; overload;
    procedure SetMessage(const csMessage: string; bAutoSize: boolean = TRUE);
    property  PumpOnUpdate: boolean read FPumpOnUpdate write FPumpOnUpdate;
    property  DoNotDecreaseAutoWidth: boolean read FDoNotDecreaseAutoWidth 
                                              write FDoNotDecreaseAutoWidth 
                                              default TRUE;
    property  Cancelled: boolean read FCancelled write FCancelled;
  end; // TDisplayForm
  // helper function - wrapper for a constructor
  function CreateDisplayForm(const csMessage: string; 
                             iWidth: integer = -240; 
                             iHeight: integer = 40): TDisplayForm;

-- End of Listing 1 --

Before we proceed with the investigation of our Display Form features we may want to know the answer to one simple question: why on Net are we deriving this form from some mysterious TRunTimeForm rather than simply from TForm? The answer will become obvious if you are willing to experiment and dare to change the base class to TForm. The inherited TCustomForm.Create constructor will detect that our new from is NOT a TForm itself and then will attempt to locate and load its resource part (originally stored in a DFM file). And as far as there is no existing RunTimeForm.dfm file whatsoever, an exception will be raised, effectively preventing us from using anything that is not a TForm.

To overcome this, we create a heir to TForm and supply it with an overridden constructor Create. Its interface and implementation are very straightforward and shown in Listing 2. In essence it mimics the standard TCustomForm constructor, skipping only its resource locating segment.

Listing 2

interface
// this form allows its any descendant to be created w/o DFM file
  TRunTimeForm = class(TForm) // Run Time dfm-Indefendent form
  public
    constructor Create(AOwner: TComponent); override;
  end; // TRunTimeForm
implementation
constructor TRunTimeForm.Create(AOwner: TComponent);
begin
  GlobalNameSpace.BeginWrite;
  try
    inherited CreateNew(AOwner);
    // resource-locating-loading part skipped 
  finally
    GlobalNameSpace.EndWrite;
  end;
end;

-- End of Listing 2 --

Ok, now we can return to our display form. All of the methods are shown in Listing 3.

Listing 3

constructor TDisplayForm.Create;
begin
  inherited Create(Application);
  Position := poScreenCenter;
  BorderStyle := bsNone;
  FPanel := TPanel.Create(self);
  FPanel.Parent := self;
  FPanel.BevelWidth := 2;
  FPanel.Align := alClient;
  FDoNotDecreaseAutoWidth := TRUE;
  KeyPreview := TRUE;
  OnKeyPress := FormKeyPress;
end;
procedure TDisplayForm.SetMessage(const csMessage: string;
  bAutoSize: boolean);
const
  csDelta = 'WWWWWWWW';
var
  w, wDelta: integer;
begin
  FPanel.Caption := csMessage;
  if bAutoSize then
  begin
    w := Canvas.TextWidth(csMessage);
    wDelta := Canvas.TextWidth(csDelta);
    w := ((w + wDelta + pred(wDelta)) div wDelta) * wDelta;
    if (w > Width) or not FDoNotDecreaseAutoWidth then
    SetBounds(0 + ((Screen.Width - w) div 2),
              0 + ((Screen.Height - Height) div 2),
              w,
              Height);
  end; // autosize
  if FPumpOnUpdate then
    Application.ProcessMessages
  else
    Repaint;
end;
procedure TDisplayForm.DoShow;
begin
  inherited DoShow;
  FTaskList := DisableTaskWindows(Handle);
  FCursor := Screen.Cursor;
  Screen.Cursor := crHourGlass;
end;
procedure TDisplayForm.DoHide;
begin
  EnableTaskWindows(FTaskList); // works with nil OK
  Screen.Cursor := FCursor;
  inherited DoHide;
end;
procedure TDisplayForm.FormKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #27 then // user hit Esc key 
   Cancelled := TRUE;
end;
function CreateDisplayForm(const csMessage: string; 
                           iWidth, iHeight: integer): TDisplayForm;
begin
  Result := TDisplayForm.Create;
  Result.Height := iHeight;
  if iWidth > 0 then
    Result.Width := iWidth;
  Result.SetMessage(csMessage, iWidth <= 0);
  Result.Show;
  Result.Update;
end; // CreateShowDisplayForm

-- End of Listing 3 --

As you can see, constructor creates and initiates the necessary visible controls. Procedures SetMessage and FormKeyPress are also pretty obvious, while DoShow and DoHide may require some comments. They utilize one of the very powerful Delphi commodities: the set of task list routines. This "sandwich" consists of two methods, namely DisableTaskWindows and EnableTaskWindows. The first call disables all of the application's high level windows (except the one passed as a parameter) exactly as the ShowModal method does. The other one performs the reverse operation.

Although not immediately obvious, the advantage of using this technique rather than traditional ShowModal call is indisputable. ShowModal is a synchronous call, which forces you to move all of your business logic to the form being displayed. Our CreateDisplayForm call is asynchronous, allowing us to show the form and immediately proceed with our errands. Furthermore, the business code is not required to be linked to the UI element and thus may reside wherever we choose. We'll see more real-life examples of its usage down the road. In the meantime, a simple, but helpful, example is shown in Listing 4.

Listing 4

procedure ScanDataSet(Table: TDataSet; bSingleThreaded: boolean);
var
  frm: TDisplayForm;
begin
  frm := CreateDisplayForm('Scanning the table...');
  with Table do
  try // for finally
    frm.PumpOnUpdate := bSingleThreaded;
    First;
    while not Eof do
    begin
      if frm.Cancelled then break; // user hit Esc
      if RecNo mod 1000 = 0 then // we don't need to react on every single record
        frm.SetMessage(Format('Records processed: %d out of %d', 
                              [RecNo, RecordCount]));
      // do something with table here
      Next;
    end; // for all records
  finally
    frm.Free;
  end; // try-finally
end; // ScanDataSet

-- End of Listing 4 --

Thus, Display Form provides us with an adequate means for displaying one line of variable text in a modal way and for intercepting the user's possible cancellation instructions. You can easily expand its capability by adding picture or animations or by allowing for more text.

Even without multiple threads, this object is already a valuable asset, but wait till you see how it works in conjunction with our activity log component...

back to the top

Displayng loading info

One of the most typical tasks every programmer encounters when dealing with a new middle-size project is a splash screen display. Traditionally this task is handled in manner that shown in Listing 5:

Listing 5

begin // my project
  Application.Initialize;
  Application.Title := 'Children of Threadmare';
  with TfrmAbout.Create(Application) do
  try // for finally
    btnOK.Hide; // get rid of visual reminder of the form's "closeability"
    BorderIcons := []; // same purpose
    Caption := 'Loading, please wait..'; // the only place we can say something
    Enabled := FALSE; // and actually prevent user from doing anything to it
    Show; 
    Update; // inevitable evil - otherwise it won't be properly drawn
    Application.CreateForm(TfrmMain, frmMain);
    // some additional initialization code goes here..
    // you can use sleep(3*1000) to imitate it
  finally
    Free;
  end; // with about form do
  Application.Run;
end.

-- End of Listing 5 --

While there is nothing wrong with that approach, sometimes only a few seconds are actually required for loading the application. In this scenario displaying an About screen with its usually sizable graphical content would be a total waist of time and resources. Its own loading may consume 50% or even 100% of the otherwise relatively short loading time. Remember - your primary goal in this case is to make the loading time a little bit more comfortable, not 100% longer.

Another reason for not displaying an About screen hides behind the following simple fact. Quite often you are dealing with some kind of document processing and thus, you may need to display something "splashy" many times during the session, i.e. each time you change the document. During these changes you definitely don't want your user to hit various buttons. Due to the data dependency, you also have no idea how long each load process might take. However, it would be unfair to leave the user face-to-face with only the hourglass cursor alone.

And here is where our display form and activity log may come in handy. Let's consider the following real-life example.

Our application will be processing a file of records with variants. Apart from the other important business information, each record holds a "type" field. Let's assume that we're asked to provide a simple navigator which would display all the record types in a narrow list. When user clicks on particular row you must display the record details in the main part of the window. (We're not going to actually do this in the sample code, but we'll do everything else.)

To achieve this, we may need to read the entire file immediately upon opening it in order to collect all the record types. (Let's assume, for the sake of the overall task, that we have to do it for some important reason.) Since this step will take a relatively short but unpredictable length of time, it would be nice to provide some progress information during this process.

Comment: We purposely use a highly simplified and somewhat old-fashioned approach to the data processing here. This article is not devoted to I/O optimization or data structures design. The real code may utilize asynchronous I/O routines, caching, streaming, reading-ahead and all the other usual optimization techniques. Here we are only concentrating on the visualization aspects.

Our file reading code looks relatively simple and is shown in listing 6

Listing 6

  TDataRecordType = (drtName, drtAddr1, drtAddr2, drtCityStateZip, drtPhoneFax);
  TDataRecordTypeArray = array of TDataRecordType;

  TDataRecord = record
  case RecType: TDataRecordType of
  drtName:(First, Last: string[16]);
  drtAddr1:(Street: string[30];
            Apt: integer);
  drtAddr2:(Addr2: string[30]);
  drtCityStateZip:(City: string[20];
                   State: string[2];
                   ZIP: string[5]);
  drtPhoneFax:(Area: string[3];
               Phone: string[7];
               Ext: string[4];
               Fax: string[7]);
  end;
  PTDataRecord = ^TDataRecord;

  TDataFile = file of TDataRecord;
  private // part of the form
    FForm: TDisplayForm;
    FMessage: string;
    FTypes: TDataRecordTypeArray;
    FAllowCancel: boolean;
    procedure OpenDataFile(const csFileName: string);
resourcestring
  rsRecords_II = 'Record processed: %d out of %d';
procedure TfrmMain.Open1Click(Sender: TObject);
begin
  if OpenDiaFLog2Execute then
    OpenDataFile(OpenDiaFLog2FileName)
end;
procedure TfrmMain.OpenDataFile(const csFileName: string);
begin
  FLog1Initialize;
  try // for finally
    FAllowCancel := FALSE;
    FTypes := ReadDataTypes(csFileName, FLog1Client);
  finally
    FLog1Finalize;
  end; // try-finally
end;
function ReadDataTypes(const csFileName: string;
  LogClient: TCustomActivityLogClient = nil): TDataRecordTypeArray;
var
  i, iCount: integer;
  data: TDataRecord;
  datafile: TDataFile;
  x: TDataRecordTypeArray;
begin // GenerateDataFile
  Assign(datafile, csFileName);
  Reset(datafile);
  iCount := FileSize(datafile);
  LogClient.Start(csFileName, cbProgress);
  SetLength(x, iCount);
  try // for finally
    for i := 0 to pred(iCount) do
    begin
      if i mod 100 = 0 then
        LogClient.Report(i, iCount, Format(rsRecords_II, [i, iCount]));
      Read(datafile, data);
      x[i] := data.RecType;
    end; // for all items
    Close(datafile);
    Result := x;
  finally
    LogClient.Finish;
  end; // try-finally
end; // ReadDataTypes

-- End of Listing 6 --

So far we have only a few log-related lines, namely its initialization and finalization in the OpenDataFile procedure. As long as we don't use multiple threads in this case it is wise to take complete control and set the Log's AutoFinalize property to FALSE. And, of course, UsePumping property should be set to TRUE, otherwise everything would seem frozen until the end of the process.

To fulfill our assignment we only need to setup a few log's event handlers. At the initialization point we will create Display Form, which will be eventually released in OnFinalize. Also, we'll collect some information during each OnProcessUpdate event and display it at OnDataUpdateFinish. The complete code is shown in Listing 7.

Listing 7

procedure TfrmMain.Log1Initialize(Sender: TActivityProcessLog);
begin
  FForm := CreateDisplayForm('Working, please wait...');
end;
procedure TfrmMain.Log1Finalize(Sender: TActivityProcessLog);
begin
  FreeAndNil(FForm);
end;
procedure TfrmMain.Log1DataUpdateStart(Sender: TActivityProcessLog);
begin
  FMessage := '';
end;
procedure TfrmMain.Log1ProcessUpdate(Sender: TActivityProcess);
begin
  Sender.GetMessage(FMessage);
end;
procedure TfrmMain.Log1DataUpdateFinish(Sender: TActivityProcessLog);
begin
  if FAllowCancel and FForm.Cancelled then
    FLog1Cancel
  else
    if FMessage <> '' then
      FForm.SetMessage(FMessage);
end;

-- End of Listing 7 --

As the result of our efforts, we finally get something like this:

back to the top

Generating data

That was pretty good for a few lines of code. But can we get more from it? Sure thing!

Now, we need to test our application somehow. Let's allow the user (i.e. ourselves!:-) to generate a file to play with. And rather than displaying an additional entry dialog for a projected file size, let's simply allow our user to cancel the generation process at any moment. All we would need to do then is to provide a generation code and bind it to a menu item. The result is shown in Listing 8.

Listing 8

procedure TfrmMain.Generate1Click(Sender: TObject);
var
  i: integer;
begin
  if SaveDiaFLog2Execute then
  begin
    FLog1Initialize;
    try // for finally
      FAllowCancel := TRUE;
      i := GenerateDataFile(SaveDiaFLog2FileName, 1000000, FLog1Client);
    finally
      FLog1Finalize;
    end; // try-finally
    ShowMessage(Format('Records generated: %d', [i]));
  end; // ok
end;
function GenerateDataFile(const csFileName: string;
  MaxRecCount: integer;
  LogClient: TCustomActivityLogClient = nil): integer;
var
  i, rectype: integer;
  data: TDataRecord;
  datafile: TDataFile;
begin // GenerateDataFile
  Result := 0;
  Assign(datafile, csFileName);
  Rewrite(datafile);
  try // for finally
    try // for except
      LogClient.Start(csFileName, cbProgress);
      try // for finally
        for i := 0 to pred(MaxRecCount) do
        begin
          if i mod 100 = 0 then
            LogClient.Report(i, MaxRecCount, Format(rsRecords_II, [i, MaxRecCount]));
          fillchar(data, sizeof(data), #0);
          rectype := random(ord(high(TDataRecordType)));
          data.RecType := TDataRecordType(rectype);
          // you can fill additional fields here  
          Write(datafile, data);
          inc(Result);
        end; // for all items
      finally
        LogClient.Finish;
      end; // try-finally
    except // some error occurred
      on e: EActivityProcessLogAbort do ; // noting serious
      else raise; // something else
    end; // try-except
  finally
    Close(datafile);
  end; // try-finally
end; // GenerateDataFile

-- End of Listing 8 --

Now, after we start the generating process, the user may hit Esc any time he/she decides the generated file size is big enough. Notice that no other changes are necessary. Our log will automatically create display form and release it when it's no longer needed. We can add as many business processes as we need, and each time all we have to do is to initialize the log and finalize it when the work is done.

back to the top

Finding text/data

To make our application a little bit more useful, let's provide some simple search capabilities. First, let's add some randomly generated phone data to our file as shown in Listing 9:

Listing 9

// added to GenerateDataFile code 
case data.RecType of
drtPhoneFax:
  begin
    data.Area := Format('%.3d', [Random(999)]);
    data.Phone := IntToStr(1000000 + Random(9999999)); 
  end; // phone/fax
end;

--End of Listing 9 --

Of course, dropping and setting up a FindDialog is not a problem. The problem is that sometimes we can find what we're looking for in no time at all, while in a worst case scenario we would need to scan the entire file and still not find the match. We certainly don't want to display our progress form if all we have to do is move a couple of records ahead, but otherwise some visual feedback is definitely welcome.

Although this may sound like a relatively tricky problem, it's actually not. Let's use the same technique we used before, but let's postpone our Display Form creation for a second or so. Only if we don't find a match during that time would it be worthwhile to display the form. The modified version of our event handlers is shown in Listing 10.

Listing 10

procedure TfrmMain.Log1Initialize(Sender: TActivityProcessLog);
begin
  // nothing here
end;
procedure TfrmMain.Log1DataUpdateStart(Sender: TActivityProcessLog);
const
  OneSecond = 1.0 / (24 * 60 * 60);
begin
  if (FForm = nil) and ((Now - Sender.TimeStart) > OneSecond) then
    FForm := CreateDisplayForm('Working, please wait...');
  FMessage := '';
end;
procedure TfrmMain.Log1DataUpdateFinish(Sender: TActivityProcessLog);
begin
  if FForm = nil then EXIT;
  if FAllowCancel and FForm.Cancelled then
    FLog1.Cancel
  else
    if FMessage <> '' then
      FForm.SetMessage(FMessage);
end;

-- End Of Listing 10 --

As you can easily see, now the display form is only created if more than one second passed since the log initialization moment. Also, form presence is checked before its SetMessage method is called.

Well, now it's time to add some threads to our application and to also change its visual representation.

back to the top

Background processing

Let's use our old friend from Waking from Nightmare: TTestThread, which runs two nested loops in its Execute method. Since we are now dealing with multiple threads, let's add one more log component and this time leave its AutoFinalize and UsePumping properties intact. We will also use a progress bar as our progress indicator and a status bar as its container. We would have to add a few lines of code to ensure the fact that the progress bar resides on the status bar: by default you cannot drop a progress bar onto a status bar, so we are going to achieve it at run time as shown in Listing 11.

Listing 11

procedure TfrmMain.FormCreate(Sender: TObject);

  procedure SetupProgressBar;
  begin
    ProgressBar1.Hide;
    ProgressBar1.Parent := StatusBar1;
    ProgressBar1.Left := StatusBar1.Panels[0].Width + 3;
    ProgressBar1.Top := 3;
    ProgressBar1.Height := StatusBar1.ClientHeight - 3;
    ProgressBar1.Width := StatusBar1.ClientWidth - ProgressBar1.Left - 20;
  end; // SetupProgressBar

begin
  // other initialization...
  SetupProgressBar;
end;
procedure TfrmMain.StatusBar1Resize(Sender: TObject);
begin
  ProgressBar1.Width := StatusBar1.ClientWidth - ProgressBar1.Left - 20;
end;

-- End of Listing 11 --

Now the only thing left is to attach some code to our new FLog2 component event handlers as shown in Listing 12:

Listing 12

procedure TfrmMain.Log2Initialize(Sender: TActivityProcessLog);
begin
  StatusBar1.Panels[1].Bevel := pbNone;
  ProgressBar1.Position := ProgressBar1.Min;
  ProgressBar1.Show;
end;
procedure TfrmMain.Log2Finalize(Sender: TActivityProcessLog);
begin
  ProgressBar1.Hide;
  StatusBar1.Panels[1].Bevel := pbLowered;
  StatusBar1.Panels[0].Text := '';
end;
procedure TfrmMain.Log2ProcessUpdate(Sender: TActivityProcess);
begin
  with Sender do
  begin
    if Level = 1 then
    begin
      StatusBar1.Panels[0].Text := 
        Format('Steps completed: %d out of %d', [Position, Max])
    end; // total progress
    if Level = 2 then
    begin
      ProgressBar1.Max := Max;
      ProgressBar1.Position := Position;
    end; // local progress
  end; // with Sender do
end;
procedure TfrmMain.Start1Click(Sender: TObject);
begin
  FLog2.Initialize;
  TTestThread.Create(FLog2Client,
                     100,
                     100000,
                     100,
                     0
  );
end;

-- End of Listing 12 --

As you can see, in this case we don't call Finalize manually. Instead, we rely upon Log's AutoFinalize feature which guarantees that it will be called in due time.

Now launch the program, select Background | Start - and watch it run while you're doing something else:

back to the top

Progress Dialog

Having had so much time invested in our Log, it would be completely unwise not to spend a little bit more to create a component which can be used in all applications which require similar feedback from a business process. It's not even a hard task now. All we need to do is to provide some properties and handle them properly. The main form of our sample application provides an access to almost all of these properties:

Hit the Start button and you'll get what you want: a fully customizable progress dialog in action!

Let's take a closer look at the properties that we can use to make our Progress Dialog suit a wide spectrum of different tasks. The public/published part of its interface is shown in Listing 13:

Listing 13

TActivityProgressDialog = class(TComponent)
// private and protected parts are omitted for briefness
public
  constructor Create(AOwner: TComponent); override;
  destructor Destroy; override;
  procedure Start;
  procedure Journal(const csText: string); overload;
  procedure Journal(strs: TStrings); overload;
  procedure Finish;
  // in case you don't use CloseOnFinish you may need this one:
  procedure CloseDialog;
  // run-time properties
  property Client: TCustomActivityLogClient read FClient;
  // WARNING: under no circumstances change form/log event handlers!
  // the main purpose of those properties is to provide easy read-only access
  // to their sub-properties w/o wrapping each one
  property Form: TfrmActivityProgress read FForm;
  property Log: TActivityProcessLog read FLog;
published
  // these properties should set in inactive mode only
  property FileName: string read GetFileName write SetFileName;
  property FileMode: TActivityDialogFiling read FFileMode write SetFileMode;
  property DetailsMode: TActivityDialogDetails read FDetailsMode 
                                               write SetDetailsMode;
  property CancelMode: TActivityDialogCancel read FCancelMode 
                                             write SetCancelMode;
  property TimeMode: TTimeReportMode read GetTimeMode write SetTimeMode;
  property UsePumping: boolean read GetUsePumping write SetUsePumping;
  property Modal: boolean read FModal write SetModal default TRUE;
  property OneLevel: boolean read FOneLevel write SetOneLevel; 
  // these properties may be set in both active and inactive modes
  property CloseOnFinish: boolean read FCloseOnFinish write SetCloseOnFinish;
  property Caption: string read GetCaption write SetCaption;
  property AutoScrollDetails: boolean read FAutoScrollDetails 
                                      write FAutoScrollDetails default TRUE;
  // event-handlers
  property OnStart: TActivityDialogEvent read FOnStart write FOnStart;
  property OnFinish: TActivityDialogEvent read FOnFinish write FOnFinish;
  property OnClose: TActivityDialogEvent read FOnClose write FOnClose;
  property OnCancel: TActivityDialogQueryEvent read FOnCancel write FOnCancel;
  property OnTimer: TActivityDialogEvent read FOnTimer write FOnTimer;
end; // TActivityProgressDialog

-- End of Listing 13 --

While some of the features are pretty obvious and self-explanatory, some of the others might definitely benefit from a few extra words. Let's then consider them one by one.

Feature

Explanation

procedure Start
One of the most important methods. You should ALWAYS call it when you want the dialog to be shown. It initializes the internal log component and shows the dialog.
procedure Finish
As opposite to its companion Start, you may never find yourself calling this method. All it does it calls internal Log's Finalize method. As long as Dialog always keeps AutoFinalize equal TRUE (unless you change it to FALSE for some better be important reason), the need for Finalize and, therefore, Finish is negligible and the very method exists mostly for symmetrical/aesthetical purposes.
procedure Journal
These two overloaded methods allow you to "log" a bunch of lines of text to Dialog's journal in a single call. They are only wrappers to relevant internal Log' smethods.
procedure CloseDialog
As it is said in the inline comment, you would only need to call this manually when you have CloseOnFinish property set to FALSE.
property Client
This important property is a shortcut to dialog's internal log's client. This actually gives you an entry point to all goodies we learned to use so far.
property Form
property Log
While these properties basically provide full access to their targets, it is important that you never modify these objects' event handlers or other important properties. The rule of thumb is: it is safe to use them in read-only mode and you should think thrice before change anything you're not sure of.
property FileName
property FileMode
These two properties basically serve as safe wrappers to log's filing properties and help you to deal with the log file
property DetailsMode
addUser,
addNone,
addAlways
This property allows you to specify the desired details level. The first (default) option allows the end-user to show or hide the journal with the help of the Details button. The other two hide this controlling button and freeze the dialog in either detail-less or detailed mode.
property CancelMode
adcAsk,
adcDontAsk,
adcNone,
adcCustom
Similar to DetailsMode, this property provides you with a one-touch way of configuring dialog's behavior versus possible canceling action. First two options are pretty obvious. The third one eliminates the very possibility of closing this dialog for the end-user. The fourth option provides you as a programmer with the ultimate control, but in this case you would have to handle OnCancel event yourself.
property TimeMode
property UsePumping
These two properties should be already familiar to Log's user.They are, in essence, write-safe wrappers for the internal log's similar properties.
property Modal
This important property provides you with modal-like dialog behavior (Task List routines are used again).
property OneLevel
By default the dialog would display two progress bars. If you have or need only one level of nesting you may want to set this property to TRUE.
property CloseOnFinish
This property controls auto closing. If you don't care about letting the end user to study the journal details before closing the dialog simply go ahead and set this property to TRUE, and you'll save your user one click.
property Caption
This is basically a Form.Caption. It's just a little bit smarter than that and displays some default text even if you set it to an empty string.
property AutoScrollDetails 
Whether you want the dialog's journal to automatically scroll down on acquiring new data or you want it stand still - this property gives you a fair chance to create the proper effect. And if you study the code you will see that its scrolling part is smart enough to NOT scroll if user decided to investigate some part of the journal. If it detects any user activity it suspends scrolling for 10 second and resumes it only if user didn't do anything to the journal during this time.
property OnStart
property OnFinish
property OnClose 
property OnCancel
property OnTimer
However important these event handlers are, you will find that it's fairly possible keep them all unassigned and still have the dialog component very useful. While their usage is highly application-specific, it's altogether possible to give some rule of thumb for each one.
  • OnStart is fired after the dialog is shown.
  • OnFinish is fired when the dialog's log has been finalized and its internal timer has been stopped. It means no more new data is expected as far as all of the business processes cease to run.
  • OnClose is very similar to OnFinish, but it is fired a little bit later, namely when dialog has been closed. If you have CloseOnFinish set to TRUE, you would probably use only one of these two events.
  • OnCancel event handler may be only fired if you set CancelMode property to adcCustom. In this case you'll be given a chance to consult with the other parts of your application or with your end-user and then modify Continue parameter according to your decision.
  • OnTimer evet simply provides you with a decent way to do something else. Normally you would leave this event handler unassigned, but in case you do some system tray programming it may come in handy.

back to the top

Finalization

In this article we considered in fine details some most typical ways of TActivityProcessLog usage. As we hoped, it turns out to be very useful in both conventional single-threaded and, of course, multi-threaded environment. We also came out with a new multi-purpose Progress Dialog component and some other useful tools and techniques. Sample application code is provided here (42.6Kb zip file for Delphi 5, including sample projects for both Waking from Threadmare and Children of Threadmare).

Happy multi-threading!

Nikolai Sklobovsky,
threadmare@sklobovsky.com

back to the top


Home

Escati Free Counter
You are Visitor No:

View Counter Stats