Putting your VCL application in the tray
Some applications are designed to be always in the Windows tray bar. For almost all their running time, the user knows where that particular application is in the tray. Think about antivirus, custom audio processors, and video management tools provided by hardware vendors and many other things. Instead, some other applications need to go in the tray only when a long operation is running and the user should otherwise attend in front of a boring please wait animation. In these cases, users will be very happy if our application is not blocked and lets them do some other things. Then, a not intrusive notification will bring up an alert if some thing interesting happens. Think about heavy queries, statistics, heavy report generation, file upload or download, or huge data import or export. Think for a second: what if Google Chrome showed one modal dialog with a message Please wait, while this 2 GB file is downloading… stopping you to navigate to other pages? Crazy! Many applications could potentially behave like this.
In such cases, the users knows that they have to wait, but the application should be so "polite" as to let them do other things. Usually, programmers think that their software is the only reason the user bought a computer. Very often, this is not the case. So, let's find a way to do the right thing at the right moment.
Getting ready
This recipe is about creating a good Windows citizen application. Let's say our application allows us to execute a huge search in a database. When the user starts this long operation, the application UI remains usable. During the request execution, the user can decide to wait in front of the form or minimize it to the taskbar. If the user minimizes the application window, it also goes on the tray bar and when the operation finishes and it will alert the user with a nonintrusive message.
How to do it…
- Create a new VCL application and drop on it a TButton, a TLabel, a TTrayIcon, a TApplicationEvents, a TImagelist, a TDataSource, and a TDBGrid component. Connect the TDBGrid to the TDataSource. Leave the default component names (I'll refer to the components using their default names). Use the disposition and the captions to make the form similar to the following screenshot:
- In the
implementation
section of the unit, add the following units:AnonThread
: Add this unit to the project (this is located underC:\Users\Public\Documents\Embarcadero\Studio\14.0\Samples\Object Pascal\RTL\CrossPlatform Utils
on my machine). You can avoid adding this unit in the project and add the path to the IDE library path by navigating to Tools | Options and then clicking on Delphi Options | Library.RandomUtilsU
: Add this unit to the project (this is located under theCommons
folder of the recipes).FireDAC.Comp.Client
: Add this unit in theimplementation uses
section of the form.
- We'll start with the code that will actually do the heavy work. In the
Button1.OnClick
method, put this code:procedure TMainForm.Button1Click(Sender: TObject); var I: Integer; ds: TDataSet; begin Button1.Enabled := False; if Assigned(DataSource1.DataSet) then begin ds := DataSource1.DataSet; DataSource1.DataSet := nil; RemoveComponent(ds); FreeAndNil(ds); end; Label1.Caption := 'Data retrieving... may take a while'; TAnonymousThread<TFDMemTable>.Create( function: TFDMemTable var MemTable: TFDMemTable; I: Integer; begin Result := nil; MemTable := TFDMemTable.Create(nil); try MemTable.FieldDefs.Add('EmpNo', ftInteger); MemTable.FieldDefs.Add('FirstName', ftString, 30); MemTable.FieldDefs.Add('LastName', ftString, 30); MemTable.FieldDefs.Add('DOB', ftDate); MemTable.CreateDataSet; for I := 1 to 400 do begin MemTable.AppendRecord([ 1000 + Random(9000), GetRndFirstName, GetRndLastName, EncodeDate(1970, 1, 1) + Random(10000) ]); end; MemTable.First; //just mimic a slow operation TThread.Sleep(2*60*1000); Result := MemTable; except FreeAndNil(MemTable); raise; end; end, procedure(MemTable: TFDMemTable) begin InsertComponent(MemTable); DataSource1.DataSet := MemTable; Button1.Enabled := True; Label1.Caption := Format('Retrieved %d employee', [MemTable.RecordCount]); ShowSuccessBalloon(Label1.Caption); end, procedure(Ex: Exception) begin Button1.Enabled := True; Label1.Caption := Format('%s (%s)', [Ex.Message, Ex.ClassName]); ShowErrorBalloon(Label1.Caption); end); end;
- Now, create the following event handler for the
Tray1
.OnBalloonClick
method and connect it to theTra1.OnDoubleClick
event handler:procedure TMainForm.TrayIcon1BalloonClick(Sender: TObject); begin TrayIcon1.Visible := False; WindowState := wsNormal; SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE or SWP_NOMOVE); SetWindowPos(Handle, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE or SWP_NOMOVE); end;
- In the next step, the two raw
SetWindowPos
calls will be less obscure, believe me. - Now, to keep things clear, we need the following two procedures. Create them as private methods of the form:
procedure TMainForm.ShowErrorBalloon(const Mess: String); begin if TrayIcon1.Visible then begin TrayIcon1.IconIndex := 2; TrayIcon1.BalloonFlags := bfError; TrayIcon1.BalloonTitle := 'Errors occurred'; TrayIcon1.BalloonHint := Label1.Caption; TrayIcon1.ShowBalloonHint; end; end; procedure TMainForm.ShowSuccessBalloon(const Mess: String); begin if TrayIcon1.Visible then begin TrayIcon1.IconIndex := 0; TrayIcon1.BalloonFlags := bfInfo; TrayIcon1.BalloonTitle := 'Request terminated'; TrayIcon1.BalloonHint := Label1.Caption; TrayIcon1.ShowBalloonHint; end; end;
- Create one last event handler for the
ApplicationEvents1.OnMinimize
method:procedure TMainForm.ApplicationEvents1Minimize( Sender: TObject); begin TrayIcon1.Visible := True; TrayIcon1.BalloonTitle := 'Employee Manager'; TrayIcon1.BalloonHint := 'Employee Manager is still running in the tray.' + sLineBreak + 'Reactivate it with a double click on the tray icon'; TrayIcon1.BalloonFlags := bfInfo; TrayIcon1.ShowBalloonHint; TrayIcon1.IconIndex := 0; end;
- Run the application by hitting F9 (or navigate to Run | Run).
- Click on the Get Employee button and then minimize the application (note that as the GUI is responsive, you can resize, minimize, and maximize the form).
- An icon is shown in the tray and shows a message about what the application is doing.
- As soon as the data has been retrieved, a Request terminated message will pop up. Click on the balloon. The application will come to the front and you will see the data in the TDBGrid.
- Try to repeat the procedure without minimizing the window. All is working as usual (this time without the tray messages) and the GUI is responsive.
How it works…
This recipe is a bit articulated. Let's start from the beginning.
The actual code that executes the request uses a nice helper class provided by Embarcadero in the Samples
folder of RADStudio (not officially supported, it is just an official sample). The TAnonymousThread<T>
constructor is a class that simplifies the process of starting a thread and when the thread ends, this class updates the UI with data retrieved by the thread.
The TAnonymousThread<T>
constructor (there are other overloads, but this the most used) expects three anonymous methods:
function: T
: This function is executed in the background thread context created internally (so you should avoid accessing the UI). ItsResult
value will be used after the thread execution.procedure (Value: T)
: This procedure is called after the thread is executed. Itsinput
parameter is the result value of the first function. This procedure is executed in the context of the main thread, so it can update the UI safely. It is not called in the case of an exception raised by the first function.procedure (E: Exception)
: This procedure is called in the case of an exception during the thread execution and is executed in the context of the main thread, so it can update the UI safely. It is not called if there isn't an exception during thread execution.
The background thread (the first function passed to the TAnonymousThread<T>
constructor) creates a memory table using the TFDMemTable
component (we will talk about this component in the FireDAC-related section) and then that object is passed to the second anonymous method that adds it to the form's components using the InsertComponent()
method and binds it to the DBGrid causing the data visualization.
When the data is ready in the grid, a call to the ShowSuccessBalloon()
function shows a balloon message in the tray area, informing users that their data is finally available. If the user clicks on the balloon (or double-clicks on the tray icon), the application is restored. The balloon message is shown in the following screenshot:
If the user clicks on the balloon, the form is restored. However, since Windows XP (with some variation in subsequent versions), the system restricts which processes can set the foreground window. An application cannot force a window to the foreground while the user is working with another window. The calls to SetWindowPos
are needed to bring the form to the front.
In the included code, there is also another version of the recipe (20_VCLAppFlashNotification
) that uses the most recent flash on the taskbar to alert the user. Consider this approach when you want to implement an application that, when minimized, has to alert the user in some way. The tray area may become rapidly crowded with icons. So consider to flash your icons in the taskbar instead.
The other code is required to correctly handle the memory ownership of the TFDMemTable
instance.
There's more...
The use of a tray icon is a well-known pattern in Windows development. However, the concept of I'll go into the background for a while, if you want, and I'll show you the notification as soon something happens is used very often on Android, iOS, and Mac OS X. In fact, some part of this recipe code is reusable also on Mac OS X, iOS, and Android. Obviously, using the right system to alert the user when the background thread finishes (for example, on a mobile platform) execution should use the notification bar. The thread handling of this recipe works on every platform supported by Delphi.