By Johnson Lau (laozho...@163.com)
As we know, applications made by Delphi have different appearances compared
to the other Win32 applications. Its title of application can differ from
the caption of the main form, and the pop-up menu of the taskbar button only
contains Restore, Minimize and Close.
The cause is that there is a special class called TApplication, and an
instance of the class, a global variable Application object, in Delphi,
which controls the base behaviors of the Delphi applications. It brings us
many conveniences to control the applications, but it sometimes confuses our
users, especially the beginners of computer – “Why this application cannot
be maximized (through the button on the taskbar)!” And this is not the most
important point. When we are building applications with special styles, it
stops us. Because an application can only have an instance of TApplication,
and the main form doesn’t have its own taskbar button, if we build a
application of Word 2002 style, we will get a very strange application in
operation. It behaves the same when making a DirectX game. So it is
necessary to make our Delphi applications behave the same with traditional
Win32 applications (what are using MFC or plain Win32 APIs). We call this
Delphi Application Normalization (DAN). And our normalized Delphi
applications are called Delphi Normalized Architecture applications (DNA
application). This article is to discuss how to DAN a DNA application.
1. Removing the taskbar button of Application object
For the most-likely appearance, firstly, we must remove the button of
Application object. To achieve this, we should change the extended style of
the invisible Application window, through adding WS_EX_TOOLWINDOW.
Now add an OnCreate message procedure to the main form, if the class name of
the main form is TMainForm, it will be TMainForm.FormCreate automatically.
Make it like what is shown below:
procedure TMainForm.FormCreate(Sender: TObject);
begin
SetWindowLong ( Application.Handle, GWL_EXSTYLE,
GetWindowLong (Application.Handle, GWL_EXSTYLE) or
WS_EX_TOOLWINDOW);
end;
Code 1.1 Codes to remove the button of Application from taskbar
Test the application. You can see that the taskbar button of Application
object is removed. But now there is no taskbar button owned by our
application. On the next step we will change this.
2. A common way to show the taskbar button of the form
If we want to show a taskbar button of a form (even a non-VCL window), just
simply adds WS_EX_APPWINDOW to the extended style. This is a very common
way. Now we will show how it works.
1) Override the CreateParams of TMainForm
procedure CreateParams(var Params: TCreateParams); override;
2) Add our codes
procedure TMainForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;
Code 2.1 Very common codes to show the taskbar button of the form
But there are many potential problems. The first, when showing a modal
dialog, if we use mouse buttons to click the taskbar button, we will lose
the focus of the whole application, and it cannot be found through Alt+Tab
or other simple ways. We need to use Ctrl+Alt+Del to kill it at last. This
is a very difficult problem to solve. We will use another way to overcome it
at the 5th part of this article.
The second, when the application is running, minimizes the application. The
application will disappear as what it is being in the first problem.
Fortunately it’s a simply problem. We will solve this problem at first. The
other problem is left to solve later.
3. Solving the problem of disappearing when minimized
The cause is that the message is not processed properly. In the default
style of Delphi applications, the minimize behavior is handled to
Application object, which is an invisible window. In our codes, we haven’t
done any modifications to it. When the main form is minimized, it still call
minimize handler of Application object, not the main form. So our main form
disappear after minimize. Now we must change the message handler of TForm,
to response the minimize message, the WM_SYSCOMMAND message.
1) Process WM_SYSCOMMAND message
procedure WMSysCommand(var Message: TWMSysCommand); message WM_SYSCOMMAND;
2) Add codes
procedure TMainForm.WMSysCommand(var Message: TWMSysCommand);
begin
if (Message.CmdType and $FFF0 = SC_MINIMIZE) and
(Application.MainForm = Self) then
begin
if not IsIconic ( Handle) then
begin
Application.NormalizeTopMosts;
SetActiveWindow(Handle);
if (Application.MainForm <> nil) and (Application.ShowMainForm
or Application.MainForm.Visible) and IsWindowEnabled( Handle) then
DefWindowProc( Handle, WM_SYSCOMMAND, SC_MINIMIZE, 0);
end;
end
else if (Message.CmdType and $FFF0 <> SC_MOVE) or (csDesigning in
ComponentState) or (Align = alNone) or (WindowState = wsMinimized) then
inherited;
if ((Message.CmdType and $FFF0 = SC_MINIMIZE) or
(Message.CmdType and $FFF0 = SC_RESTORE)) and
not (csDesigning in ComponentState) and (Align <> alNone) then
RequestAlign;
end;
Code 3.1 solving the problem of disappearing when minimized
The most important sentence is the one in bold strokes. We have passed the
message to the default handler of Windows, the DefWindowProc function. Test
the application. Now our application behaves more properly.
But we still have the problem of the showing of modal dialogs. The next part
contains some base Win32 API knowledge, which is needed to get into the
source codes of VCL.
4. Getting prepared
When we use plain Win32 API to program, we must register window class,
create window using CreateWindow or CreateWindowEx, show the window we
create, and step into message loop. Why plain Win32 API applications can
retrieve a taskbar button? Use WS_EX_APPWINDOW? No. If we use WinSight32 or
Spy++ to view the properties of the Win32 API applications main window,
there is no WS_EX_APPWINDOW extended style attached to the window.
Let’s create a plain Win32 API application with Delphi to see how a window
retrieves its taskbar button.
program Test;
uses
Windows;
{$R *.RES}
var
Msg : TMsg;
wcex : WNDCLASSEX;
HandleA, HandleB : HWND;
function WndProc(hWnd : HWND; msg : UINT; wParam : WPARAM; lParam : LPARAM):
LRESULT stdcall;
begin
if Msg = 2 then PostQuitMessage (0);
Result := DefWindowProc(hWnd, msg, wParam, lParam);
end;
const
ClassName : PChar = 'DelphiWin32API';
WndName : PChar = 'Delphi Win32 API Program';
begin
wcex.cbSize := sizeof(WNDCLASSEX);
wcex.style := CS_HREDRAW or CS_VREDRAW;
wcex.lpfnWndProc := @WndProc;
wcex.cbClsExtra := 0;
wcex.cbWndExtra := 0;
wcex.hInstance := hInstance;
wcex.hIcon := HICON (nil);
wcex.hCursor := HICON (nil);
wcex.hbrBackground := HBRUSH (COLOR_WINDOW + 1);
wcex.lpszMenuName := nil;
wcex.lpszClassName := ClassName;
wcex.hIconSm := HICON (nil);
RegisterClassEx( wcex);
HandleA := CreateWindowEx( 0, ClassName, WndName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
HWND(nil), HMENU(nil), hInstance, nil);
ShowWindow ( HandleA, SW_SHOW);
HandleB := CreateWindowEx( 0, ClassName, WndName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HandleA,
HMENU(nil), hInstance, nil);
ShowWindow ( HandleB, SW_SHOW);
while GetMessage ( Msg, 0, 0, 0) do
begin
TranslateMessage (Msg);
DispatchMessage (Msg);
end;
end.
Code 4.1 a plain Win32 application with Delphi
Run the application. There will be two windows, one with a taskbar button,
one without. Let’s modify our codes to do another test.
HandleB := CreateWindowEx( 0, ClassName, WndName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
HWND(nil), HMENU(nil), hInstance, nil);
ShowWindow ( HandleB, SW_SHOW);
Code 4.2 a plain Win32 application with Delphi (modification)
Build and run the application. There will be two windows, both with a
taskbar button. What’s the difference of the two applications? Focus on
CreateWindowEx. From Win32 API Help, we know the 9th parameter is the handle
of parent window or owner window. When we set it to HandleA, a valid window
handle, the HandleB will lose the taskbar button. When we set it to nil, the
handle value of the Windows Desktop, the HandleB will retrieve a taskbar
button.
OK. Maybe you understand now, there may be some similar errors in VCL source
codes.
5. Getting into VCL
TObject
|
TPersistent
|
TComponent
|
TControl
|
TWinControl
|
TScrollingWinControl
|
TCustomForm
|
TForm
|
TMainForm (our class)
Diagram 5.1 Deriving relations
The deriving relations are shown. From Delphi Help, we can know that, there
is a procedure called CreateWindowHandle.
We can get their descriptions (including overrode procedures) from Delphi
Help.
TCustomForm.CreateWindowHandle
procedure CreateWindowHandle(const Params: TCreateParams); override;
Description
The CreateWnd method calls CreateWindowHandle to create the form
window once it has been specified in the window-creation parameters.
CreateWindowHandle creates the window by calling the CreateWindowEx API
function, passing parameters from the record passed in the Params parameter.
CreateWindowHandle removes the CS_HREDRAW and CS_VREDRAW class styles from
the window class.
Params holds information needed when telling Windows to create a
window handle.
TWinControl.CreateWindowHandle
procedure CreateWindowHandle(const Params: TCreateParams); virtual;
Description
The CreateWnd method calls CreateWindowHandle to create the window for
a control. CreateWindowHandle creates the window by calling the
CreateWindowEx API function, passing parameters from the record passed in
the Params parameter. Once the window is created, its handle is available as
the Handle property.
Diagram 5.2 Descriptions of CreateWindowHandle in Delphi Help, Copyright by
Borland
It seems that the problem may be in TWinControl.CreateWindowHandle. Open
forms.pas and controls.pas located in the directories of VCL source codes,
and find TWinControl.CreateWindowHandle. Let’s have a look at the source
code.
procedure TCustomForm.CreateWindowHandle(const Params: TCreateParams);
var
CreateStruct: TMDICreateStruct;
NewParams: TCreateParams;
begin
.
.
.
end;
procedure TWinControl.CreateWindowHandle(const Params: TCreateParams);
begin
with Params do
FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style,
X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);
end;
Code 5.1 CreateWindowHandle procedure source code (from Delphi VCL source
codes), Copyright by Borland
TWinControl.CreateWindowHandle uses Params.WndParent to be its parent.
It seems be no error. Let’s find out who call this procedure.
procedure TWinControl.CreateWnd;
var
Params: TCreateParams;
TempClass: TWndClass;
ClassRegistered: Boolean;
begin
CreateParams(Params);
with Params do
begin
if (WndParent = 0) and (Style and WS_CHILD <> 0) then
if (Owner <> nil) and (csReading in Owner.ComponentState) and
(Owner is TWinControl) then
WndParent := TWinControl(Owner).Handle
else
raise EInvalidOperation.CreateFmt(SParentRequired, [Name]);
FDefWndProc := WindowClass.lpfnWndProc;
ClassRegistered := GetClassInfo(WindowClass.hInstance, WinClassName,
TempClass);
if not ClassRegistered or (TempClass.lpfnWndProc <> @InitWndProc) then
begin
if ClassRegistered then Windows.UnregisterClass(WinClassName,
WindowClass.hInstance);
WindowClass.lpfnWndProc := @InitWndProc;
WindowClass.lpszClassName := WinClassName;
if Windows.RegisterClass(WindowClass) = 0 then RaiseLastOSError;
end;
CreationControl := Self;
CreateWindowHandle(Params);
if FHandle = 0 then RaiseLastOSError;
end;
StrDispose(FText);
FText := nil;
UpdateBounds;
Perform(WM_SETFONT, FFont.Handle, 1);
if AutoSize then AdjustSize;
end;
Code 5.2 TWinControl.CreateWnd procedure source code (from Delphi VCL source
codes), Copyright by Borland
The sentences in bold are the causes! The Params.WndParent is always set
to the owner of the form, and the owner will be Application object
automatically when the application initializes (See the source code of the
project). This is why the main form cannot retrieve its taskbar button.
6. Solutions
Now, we’ve got the cause. And there are two ways to overcome this problem.
In one way, we can derive a new class from TFrom, and override the CreateWnd
or CreateWindowHandle procedure. Or, we can directly rewrite the VCL source
codes, to make it compatible with our codes. We recommend the second way.
Because there are many calling of private member functions in CreateWnd and
CreateWindowHandle in TWinControl or derived class, if we derive a new class
from TForm, we must copy these private functions to the new class. It will
take very long time. So we will directly rewrite the VCL source codes.
And there is a problem we must consider. Maybe all Delphi applications are
written in the default style, or called non-DNA style. If we rewrites the
VCL source codes, there will be incompatible with the current codes. So we
add a variable into the source codes, for easily switching between the two
styles (the DNA style and non-DNA style).
1) In Controls.pas
Description: In this portion of our modifications, we make two changes. The
first change is to ensure the main form will get an icon. The second change
is the solution we’ve mentioned above. We’ve added two new variables,
ShowOnTaskbar and OldApplicationStyle. For detail read the second portion of
the modifications.
procedure TWinControl.CreateWnd;
var
Params: TCreateParams;
TempClass: TWndClass;
ClassRegistered: Boolean;
begin
CreateParams(Params);
with Params do
begin
if (WndParent = 0) and (Style and WS_CHILD <> 0) then
if (Owner <> nil) and (csReading in Owner.ComponentState) and
(Owner is TWinControl) then
WndParent := TWinControl(Owner).Handle
else
raise EInvalidOperation.CreateFmt(SParentRequired, [Name]);
FDefWndProc := WindowClass.lpfnWndProc;
ClassRegistered := GetClassInfo(WindowClass.hInstance, WinClassName,
TempClass);
if not ClassRegistered or (TempClass.lpfnWndProc <> @InitWndProc) then
begin
if ClassRegistered then Windows.UnregisterClass(WinClassName,
WindowClass.hInstance);
WindowClass.lpfnWndProc := @InitWndProc;
WindowClass.lpszClassName := WinClassName;
// Change made by Johnson Lau
// --------- Begin ----------
WindowClass.hIcon := Application.Icon.Handle;
// ---------- End -----------
if Windows.RegisterClass(WindowClass) = 0 then RaiseLastOSError;
end;
CreationControl := Self;
// Change made by Johnson Lau
// --------- Begin ----------
if (Self is TCustomForm) and (OldApplicationStyle = False) then
if Application.MainForm = nil then
begin
if Params.WndParent = Application.Handle then
Params.WndParent := 0;
end
else
begin
if ShowOnTaskBar then
Params.WndParent := 0
else
Params.WndParent := Application.MainForm.Handle;
end;
// ---------- End -----------
CreateWindowHandle(Params);
if FHandle = 0 then RaiseLastOSError;
end;
StrDispose(FText);
FText := nil;
UpdateBounds;
Perform(WM_SETFONT, FFont.Handle, 1);
if AutoSize then AdjustSize;
end;
2) In Forms.pas
Description:
For the maximum compatibility with non-DNA applications (the default style
of Delphi applications), we add a variable of OldApplicationStyle. If this
variable is set to True, our application will behave as the non-DNA
application. If is set to False, the default value, our application will
behave as a DNA application. If you want to effect the application, the
variable must be set before the main form is created, so set it in the
project’s source before the “Application.CreateForm” sentence of the main
form.
Another variable ShowOnTaskBar can show the taskbar button of any form. The
default value is to hide. This will not affect the main form, or when
OldAppliationStyle is set to True. The variable must be set before a form is
created, and put it in the project’s source before the
“Application.CreateForm” sentences except the main form.
In the procedure of TCustomForm.WndProc, we avoid a bug (maybe in Delphi).
The bug is affecting our application when a modal dialog is showing, the
application not being activated from another active window by clicking left
mouse button in the area of the main form except the area of the modal
dialog, but the right mouse button actives properly. (So it is thought to be
a bug in Delphi?)
We will solve the problem of disappearing when minimized in the procedure of
TApplication.Minimize. And in TApplication.Restore the relevant change is
made.
The change in TApplication.MessageBox will ensure the application to
response properly when calling this function to display a message box.
A. Adding these two variable to the interface part
OldApplicationStyle : Boolean;
ShowOnTaskBar : Boolean;
B. Add this variable to the implementation part
RefCount : Integer;
C. Modify these procedures and functions
constructor TCustomForm.Create(AOwner: TComponent);
begin
GlobalNameSpace.BeginWrite;
try
.
.
.
end;
// Change made by Johnson Lau
// --------- Begin ----------
if ( OldApplicationStyle = false ) then
begin
if ( RefCount = 0) then
SetWindowLong ( Application.Handle, GWL_EXSTYLE,
GetWindowLong (Application.Handle, GWL_EXSTYLE) or
WS_EX_TOOLWINDOW);
Inc ( RefCount);
end;
// ---------- End -----------
end;
destructor TCustomForm.Destroy;
begin
// Change made by Johnson Lau
// --------- Begin ----------
if ( OldApplicationStyle = false ) then
begin
Dec ( RefCount);
if ( RefCount = 0) then
SetWindowLong ( Application.Handle, GWL_EXSTYLE,
GetWindowLong (Application.Handle, GWL_EXSTYLE) and
( not WS_EX_TOOLWINDOW));
end;
// ---------- End -----------
.
.
.
end;
procedure TCustomForm.WndProc(var Message: TMessage);
.
.
.
begin
with Message do
case Msg of
// Change made by Johnson Lau
// --------- Begin ----------
WM_SETCURSOR:
begin
if (OldApplicationStyle = False) and
(Message.LParamLo = Word (HTERROR)) then
DefWindowProc ( Handle, Message.Msg, Message.WParam,
Message.LParam);
end;
// ---------- End -----------
.
.
.
end;
procedure TApplication.Minimize;
begin
if not IsIconic(FHandle) then
begin
NormalizeTopMosts;
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then SetActiveWindow(FHandle);
if (MainForm <> nil) and (ShowMainForm or MainForm.Visible)
and IsWindowEnabled(MainForm.Handle) then
begin
if OldApplicationStyle then
begin
SetWindowPos(FHandle, MainForm.Handle, MainForm.Left,
MainForm.Top, MainForm.Width, 0, SWP_SHOWWINDOW);
DefWindowProc(FHandle, WM_SYSCOMMAND, SC_MINIMIZE, 0)
end else
DefWindowProc( MainForm.Handle, WM_SYSCOMMAND, SC_MINIMIZE, 0);
// ---------- End -----------
end else
ShowWinNoAnimate(FHandle, SW_MINIMIZE);
if Assigned(FOnMinimize) then FOnMinimize(Self);
end;
end;
procedure TApplication.Restore;
begin
if IsIconic(FHandle) then
begin
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then
begin
SetActiveWindow(FHandle);
if (MainForm <> nil) and (ShowMainForm or MainForm.Visible)
and IsWindowEnabled(MainForm.Handle) then
DefWindowProc(FHandle, WM_SYSCOMMAND, SC_RESTORE, 0)
else ShowWinNoAnimate(FHandle, SW_RESTORE);
SetWindowPos(FHandle, 0, GetSystemMetrics(SM_CXSCREEN) div 2,
GetSystemMetrics(SM_CYSCREEN) div 2, 0, 0, SWP_SHOWWINDOW);
end;
// ---------- End -----------
if (FMainForm <> nil) and (FMainForm.FWindowState = wsMinimized) and
not FMainForm.Visible then
begin
FMainForm.WindowState := wsNormal;
FMainForm.Show;
end;
RestoreTopMosts;
if Screen.ActiveControl <> nil then
Windows.SetFocus(Screen.ActiveControl.Handle);
if Assigned(FOnRestore) then FOnRestore(Self);
end;
end;
function TApplication.MessageBox(const Text, Caption: PChar; Flags:
Longint): Integer;
.
.
.
begin
.
.
.
try
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle or (MainForm = nil) then
Result := Windows.MessageBox(Handle, Text, Caption, Flags)
else
Result := Windows.MessageBox(MainForm.Handle, Text, Caption, Flags);
// ---------- End -----------
finally
.
.
.
end;
Code 6.1 Modifications to the VCL source codes (from Delphi VCL source
codes), Copyright by Borland, modified by Johnson Lau.
Build and run our application. Now our application behaves properly.
7. For the maximum compatible
Our application becomes DNA application, but not the non-DNA application by
standard. And there may be some components working strangely in the DNA
style, like the common dialog (the same problem with the modal dialog, and
we will give a solution below), or even more. You can change all references
of the handle of main form from Application.Handle to
Application.MainForm.Handle to solve this problem.
function TOpenDialog.DoExecute(Func: Pointer): Bool;
.
.
.
begin
.
.
.
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then
hWndOwner := Application.Handle
else
hWndOwner := Application.MainForm.Handle;
// ---------- End -----------
Result := TaskModalDialog(Func, OpenFileName);
.
.
.
end;
function TColorDialog.Execute: Boolean;
.
.
.
begin
.
.
.
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then
hWndOwner := Application.Handle
else
hWndOwner := Application.MainForm.Handle;
// ---------- End -----------
Result := TaskModalDialog(@ChooseColor, ChooseColorRec);
.
.
.
end;
function TFontDialog.Execute: Boolean;
.
.
.
begin
.
.
.
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then
hWndOwner := Application.Handle
else
hWndOwner := Application.MainForm.Handle;
// ---------- End -----------
SaveFontDialog := FontDialog;
.
.
.
end;
function TPrinterSetupDialog.Execute: Boolean;
.
.
.
begin
.
.
.
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then
hWndOwner := Application.Handle
else
hWndOwner := Application.MainForm.Handle;
// ---------- End -----------
Result := TaskModalDialog(@PrintDlg, PrintDlgRec);
.
.
.
end;
function TPrintDialog.Execute: Boolean;
.
.
.
begin
.
.
.
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then
hWndOwner := Application.Handle
else
hWndOwner := Application.MainForm.Handle;
// ---------- End -----------
Result := TaskModalDialog(@PrintDlg, PrintDlgRec);
.
.
.
end;
function TFindDialog.Execute: Boolean;
var
Option: TFindOption;
begin
.
.
.
// Change made by Johnson Lau
// --------- Begin ----------
if OldApplicationStyle then
FFindReplace.hWndOwner := FRedirector.Handle
else
FFindReplace.hWndOwner := Application.MainForm.Handle;
// ---------- End -----------
for Option := Low(Option) to High(Option) do
.
.
.
end;
Code 7.1 Modifications in dialogs.pas (from Delphi VCL source codes),
Copyright by Borland, modified by Johnson Lau.
8. Not reflecting our current applications?
If you don’t want to reflect the current applications, you can copy your
new controls.dcu, forms.dcu, and dialogs.dcu to a new directory, and add the
directory to your environment options.
And till now, all work has been done. And we can easily get a DNA
application. There may be some incompatible, but this is the default
appearance of Win32 application, so we strongly recommend you to use it. And
we strongly recommend Borland to strongly recommend this way.
All codes are tested in Windows XP Profession + Delphi 6 Update Pack 2.
The End
Caution: All rights of this article are reserved by Johnson Lau (Zhongcheng
Lao). Unauthorized copying prohibited.