Last time I introduced a bit of skullduggery with my Deltics.Forms unit as a way to easily “inject” a new TForm class into my projects. We used this new class to add public property that we could use to add a size grip control to any form.
This time we shall introduce some more code the Deltics.Forms unit in order to add some frequently useful capabilities to the Application object.
What Is Application?
As you are no doubt aware, the Application object is a single object declared in the VCL Forms unit (but instantiated in the Controls unit), and is the heart of most Delphi applications. It’s a well named object in that respect.
Unfortunately, the Application object is of a fixed class – TApplication. This class cannot be extended or replaced with a class that might, for example, contain extensions that you might wish to have included in all of your applications.
It was a surprise to me to find some years ago, for example, that the Application object in a Windows Application project has nothing in common with the Application object in a Windows Service Application project, for example – one is a TApplication the other a TServiceApplication and both derive directly from TComponent.
It is also something of a surprise that the basic capabilities of the TApplication object have not evolved over the years to provide better access to version information or command line parameters, for example. But despite the rather closed nature of TApplication, the Deltics.Forms unit provides the platform for adding these things ourselves.
Helpers For The Rest Of Us
A post on MelanderBlog shows how to do this with a class helper, but these things are a) a bad idea (imho) and b) not available to anyone using Delphi 2006 (I think) or earlier.
Class helpers are intrinsically a “bad idea” because only one can be in scope at a time, so if you had some other code that introduced another helper for the same class (perhaps without you even knowing it) you have a problem – one or other of those helpers will “disappear” and code that compiled perfectly well before suddenly stops compiling.
But we can achieve the same result more safely without class helpers. They are merely syntactic sugar for a time-tested technique that is actually safer (and more opaque) than the formalised “helper” mechanism. “Safer” because code that uses the non-helper technique cannot cause problems with other code using the same technique in the way that formal class helpers suffer from.
As we saw, the Deltics.Forms unit introduces a new TForm class that is compatible with the VCL TForm class. We can also introduce a new TApplication class that is similarly compatible with the VCL TApplication:
type TApplication = class(Forms.TApplication);
A problem here however is that we cannot alter the fact that the Controls unit instantiates a Forms.TApplication, and knows nothing of this new TApplication class. Our new TApplication class must therefore observe the same limitations of a class helper – we cannot introduce new member data and we cannot override any virtual methods.
Another problem is that the Application variable in Forms is also declared using the Forms.TApplication class, so if we are to refer to Application as our new class we’ll need just a little more smoke and mirrors in Deltics.Forms:
interface function Application: TApplication; implementation function Application: TApplication; begin result := TApplication(Forms.Application); end;
We still need to do a little work in our actual projects to make use of this new Application object – the Forms unit reference in our project source needs to be replaced by a reference to Deltics.Forms. It is far less likely that we will need to have both Forms and Deltics.Forms referenced in the project source, but if that is necessary then instead of replacing the Forms reference, add the Deltics.Forms unit after it in the uses list.
With these fundamentals in place, we can now – with a little care – add some useful new capabilities to the Application object. For starters let’s add a VersionInfo property.
Adding Properties Without Adding Member Data
The “care” that we need to exercise is as previously mentioned – we cannot add member data, and we cannot override any virtual methods. So how can we add any new properties?
In this case, things are made a great deal simpler by the fact that the Application object is a singleton – there is only ever one of them in any project. There is also only ever one Deltics.Forms unit in an application, so we can declare variables in the unit to hold the data we would otherwise introduce as member data in the Application object.
These unit variables (sometimes mistakenly referred to as global variables – Delphi actually has no such thing) need only be declared in the implementation section as they are exposed only via our extensions to the TApplication class:
interface uses Forms, Deltics.VersionInfo; type TApplication = class(Forms.TApplication) private function get_VersionInfo: TVersionInfo; public property VersionInfo: TVersionInfo read get_VersionInfo; end; implementation var _VersionInfo: TVersionInfo = NIL; function TApplication.get_VersionInfo: TVersionInfo; begin if NOT Assigned(_VersionInfo) then _VersionInfo := TVersionInfo.Create; result := _VersionInfo; end; initialization // NO-OP finalization FreeAndNIL(_VersionInfo); end.
Obviously the above code is not entirely complete, only showing the changes and additions needed to add the VersionInfo property. The details of the TVersionInfo implementation aren’t particularly important – there are lots of implementations of such a class freely available (the one I use will be available shortly – more on that in my next post).
So we can add some useful new properties, but I also mentioned previously that we could introduce some behavioural changes inspired by an article about Creating Vista Ready Applications.
How do we do that without overriding any virtuals?
Changing Behaviour Without Overriding
The original article about creating Vista ready applications mentioned adding some code to a FormCreate event (presumably the FormCreate of the main form) to modify the characteristics of the hidden application window (Application.Handle). Not only is this a little inelegant (using an event in one form to make changes to some other form), it is also going to be cumbersome having to do this in every application.
Now that we have our own TApplication class we can introduce these changes once and have that change apply in our applications simply by using the Deltics.Forms unit. The key method in this case is the TApplication.CreateForm method. This is not virtual and typically isn’t even referenced outside of our project source file anyway, so replacing this method with our own implementation is quite safe:
interface type TApplication = class(Forms.TApplication) public procedure CreateForm(InstanceClass: TComponentClass; var Reference); end; implementation procedure TApplication.CreateForm(InstanceClass: TComponentClass; var Reference); var bMainForm: Boolean; begin bMainForm := (Forms.Application.MainForm = NIL); inherited; if NOT bMainForm or NOT MainFormOnTaskBar or NOT (Application.MainForm.InheritsFrom(TForm)) then EXIT; ShowWindow(Application.Handle, SW_HIDE); SetWindowLong(Application.Handle, GWL_EXSTYLE, GetWindowLong(Application.Handle, GWL_EXSTYLE) and NOT WS_EX_APPWINDOW or WS_EX_TOOLWINDOW); ShowWindow(Application.Handle, SW_SHOW); TForm(Application.MainForm).RecreateWnd; end;
Again the above code showns only the changes and additions and is not complete.
The MainFormOnTaskBar property is another property that I introduce to TApplication in a similar way that VersionInfo is introduced. It is deliberately named for a property that is introduced in Delphi 2007, and is used to determine whether the form shown on the Task Bar will be the hidden application hidden window (normal Delphi behaviour) or the MainForm window (the new behaviour introduced here).
Our new CreateForm takes care of modifying the characteristics of the hidden application window when creating the Application MainForm (by design in the VCL, the MainForm is the first form created via a call to the Application.CreateForm method).
We also only apply these changes if the MainForm is derived from our Deltics.Forms.TForm (otherwise it won’t be aware of our Vista readiness changes).
Finally, if the changes are applied, the MainForm window itself needs to be re-created to ensure that the necessary window parameter changes are applied that will present that form on the task bar. These parameter changes are applied in an override of CreateParams in the Deltics.Forms.TForm class:
procedure TForm.CreateParams(var Params: TCreateParams); begin inherited; Params.Style := Params.Style and (not WS_CHILD) or WS_GROUP or WS_TABSTOP; if ShowOnTaskBar then Params.ExStyle := Params.ExStyle and (NOT WS_EX_TOOLWINDOW) or WS_EX_APPWINDOW; end;
Time for a quick confession – I originally did all this stuff quite a while ago now and rather foolishly neglected to document a lot of the finer details. The changes to Params.Style being one such undocumented detail.
It solves a puzzling problem – changing the caption of an AutoSize TLabel on a Deltics.Form.TForm results in that form changing size (a result of AlignControls). Setting these Style params fixes that behaviour. I must have reasoned this out myself at the time or come across some information that gave me the insight, but I’ve since lost that information. I hate code that “just works”, so if anyone can explain why this is necessary I would be very grateful.
One last thing we can do is introduce the WM_SYSCOMMAND message handling described in the Vista readiness article:
procedure TForm.WMSysCommand(var aMessage: TWMSysCommand); begin if Application.MainFormOnTaskBar then begin case (aMessage.CmdType and $FFF0) of SC_MINIMIZE : begin ShowWindow(Handle, SW_MINIMIZE); aMessage.Result := 0; end; SC_RESTORE : begin ShowWindow(Handle, SW_RESTORE); aMessage.Result := 0; end; else inherited; end; end else inherited; end;
Code In Preparation
There is no code to download this time – for the time being – other than that provided in the article above.
I am currently in the process of setting up a CodePlex hosted project where I shall release all code that I cover in my blog – and more. I have decided to release under the X11 (a.k.a “MIT”) License. I find the Team Foundation version control model as unpalatable as SVN (as I understand it CodePlex projects can be accessed using Team Explorer or SVN) – the tight coupling between repository and local workspace is particularly uncomfortable to me.
But this, and other aspects of the CodePlex experience, is something I shall blog about separately.
Once the project is available I shall post the details in my blog.