by Nikolai Sklobovsky,
Senior System Analyst,
Retail Technologies International, Inc.
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.
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.
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.
| 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.
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.
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 topOne 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:
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
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.
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:
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.
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 topTo 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:
// 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.
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.
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.
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:
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:
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:
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.
|
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
|
You are Visitor No:
|