问题
I am trying to achieve the age-old Delphi dream of having a modeless form appear in the taskbar.
What is the correct way to have a modeless form appear in the taskbar?
Research Effort
These are my attempts to solve the problem. There are a lot of things needed to make it behave correctly - simply having a button appear on the taskbar is not a solution. Having a Windows application behave correctly as a Windows application should is my goal.
For those who know me, and how deep my "shows research effort" goes, hang on because it will be wild ride down a rabbit hole.
The question is in the title, as well above the horizontal line above. Everything below only serves to show why some on the oft-repeated suggestions are incorrect.
Windows only creates as taskbar button for unowned windows
Initially i have my "Main Form", from that i show this other modeless form:
procedure TfrmMain.Button2Click(Sender: TObject);
begin
if frmModeless = nil then
Application.CreateForm(TfrmModeless, frmModeless);
frmModeless.Show;
end;
This correctly shows the new form, but no new button appears on the taskbar:
The reason no taskbar button is created is because that is by design. Windows will only show a taskbar button for a window that "unowned". This modeless Delphi form is most definitely owned. In my case it is owned by the Application.Handle
:
My project's name is ModelessFormFail.dpr
, which is the origin of the Windows class name Modelessformfail
associated with the owner.
Fortunately there is a way to force Windows to create a taskbar button for a window, even though the window is owned:
Just use WS_EX_APPWINDOW
The MSDN documentation of WS_EX_APPWINDOW says it:
WS_EX_APPWINDOW
0x00040000L
Forces a top-level window onto the taskbar when the window is visible.
It also a well-known Delphi trick to override CreateParams
and manually add the WS_EX_APPWINDOW
style:
procedure TfrmModeless.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned window to appear in taskbar
end;
When we run this, the newly created modeless form does indeed get its own taskbar button:
And we're done? No, because it doesn't behave correctly.
If the user clicks on the frmMain taskbar button, that window is not brought forward. Instead the other form (frmModeless) is brought forward:
This makes sense once you understand the Windows concept of ownership. Windows will, by design, bring any child owned forms forward. It was the entire purpose of ownership - to keep owned forms on top of their owners.
Make the form actually unowned
The solution, as some of you know is not to fight against the taskbar heuristics and windows. If i want the form to be unowned, make it unowned.
This is (fairly) simple. In CreateParam
force the owner windows to be null
:
procedure TfrmModeless.CreateParams(var Params: TCreateParams);
begin
inherited;
//Doesn't work, because the form is still owned
// Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned windows to appear in taskbar
//Make the form actually unonwed; it's what we want
Params.WndParent := 0; //unowned. Unowned windows naturally appear on the taskbar.
//There may be a way to simulate this with PopupParent and PopupMode.
end;
As an aside, i wanted to investigate is there was a way to use the PopupMode and PopupParent properties to make a window unowned. I swear i read a comment (from you David) somewhere on SO saying that if you passed Self
as the PopupParent
, e.g.:
procedure TfrmMain.Button1Click(Sender: TObject);
begin
if frmModeless = nil then
begin
Application.CreateForm(TfrmModeless, frmModeless);
frmModeless.PopupParent := frmModeless; //The super-secret way to say "unowned"? I swear David Heffernan mentioned it somewhere on SO, but be damned if i can find it now.
frmModeless.PopupMode := pmExplicit; //happens automatically when you set a PopupParent, but you get the idea
end;
frmModeless.Show;
end;
it was supposed to be the super-secret way to indicate to Delphi that you want to form to have "no owner". But i cannot find the comment anywhere on now. Unfortunately, no combination of PopupParent
and PopupMode
cause a form to actually be un-owned:
- PopupMode: pmNone
- Owner hwnd:
Application.Handle/Application.MainForm.Handle
- Owner hwnd:
- PopupMode: pmAuto
- Owner hwnd:
Screen.ActiveForm.Handle
- Owner hwnd:
- PopupMode: pmExplicit
- PopupParent: nil
- Owner hwnd:
Application.MainForm.Handle
- Owner hwnd:
- PopupParent:
AForm
- Owner hwnd:
AForm.Handle
- Owner hwnd:
- PopupParent: Self
- Owner hwnd:
Application.MainForm.Handle
- Owner hwnd:
- PopupParent: nil
Nothing i could do could cause the form to actually have no owner (each time checking with Spy++).
Setting the WndParent
manually during CreateParams:
- does make the form unowned
- it does have a taskbar button
- and both taskbar buttons dobehave correctly:
And we're done, right? I thought so. I changed everything to use this new technique.
Except there are problems with my fix that seem to cause other problems - Delphi didn't like me changing to ownership of a form.
Hint Windows
One of the controls on my modeless window has a tooltop:
The problem is that when this tooltip window appears, it causes the other form (frmMain, the modal one) to come forward. It doesn't gain activation focus; but it does now obscure the form i was look at:
The reason is probably logical. The Delphi HintWindow is probably owned either by Application.Handle
or Application.MainForm.Handle
, rather than being owned by the form that it should be owned by:
I would have considered this a bug on Delphi's part; using the wrong owner.
Diversion to see the actual app layout
Now it's important that i take a moment to show that my application isn't a main form and a modeless form:
It's actually:
- a login screen (a sacrificial main form that gets hidden)
- a main screen
- a modal control panel
- that shows the modeless form
Even with the reality of the application layout, everything except for hint window ownership works. There are two taskbar buttons, and clicking them brings the proper form forward:
But we still have the problem of the HintWindow ownership bringing the wrong form forward:
ShowMainFormOnTaskbar
It was when i was attempting to create a minimal application to reproduce the problem when i realize i couldn't. There was something different:
- between my Delphi 5 application ported to XE6
- a new application created in XE6
After comparing everything, i finally traced it down to the fact that new applications in XE6 add the MainFormOnTaskbar := True
by default in any new project (presumably to not break existing applications):
program ModelessFormFail;
//...
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TfrmSacrificialMain, frmSacrificialMain);
//Application.CreateForm(TfrmMain, frmMain);
Application.Run;
end.
When i added this option, then the appearance of the tooltip didn't bring the wrong form forward!:
Success! Except, people who know what's coming know what's coming. My "sacrificial" main login form shows the "real" main form, hiding itself:
procedure TfrmSacrificialMain.Button1Click(Sender: TObject);
var
frmMain: TfrmMain;
begin
frmMain := TfrmMain.Create(Application);
Self.Hide;
try
frmMain.ShowModal;
finally
Self.Show;
end;
end;
When that happens, and i "login", my taskbar icon disappers entirely:
This happens because:
- the un-owned sacrificial main form is not invisible: so the button goes with it
- the real main form is owned so it does not get a toolbar button
Use WS_APP_APPWINDOW
Now we have the opportunity to use WS_EX_APPWINDOW
. I want to force my main form, which is owned, to appear on the taskbar. So i override CreateParams
and force it to appear on the taskbar:
procedure TfrmMain.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned window to appear in taskbar
end;
and we give it a whirl:
Looking pretty good!
- two taskbar buttons
- the tooltip doesn't pop the wrong owner form forward
except, when i click on the first toolbar button, the wrong form comes up. It shows the modal frmMain, rather than the currently modal frmControlPanel:
Presumably because the newly created frmControlPanel was PopupParented to Application.MainForm rather than Screen.ActiveForm. Check in Spy++:
Yes, the parent is MainForm.Handle
. This turns out to be because of another bug in the VCL. If the form's PopupMode
is:
- pmAuto
- pmNone (if it's a modal form)
the VCL attempts to use Application.ActiveFormHandle
as the hWndParent
. Unfortunately it then checks if the modal form's parent is enabled:
if (WndParent <> 0) and (
IsIconic(WndParent) or
not IsWindowVisible(WndParent) or
not IsWindowEnabled(WndParent)) then
Of course the modal form's parent is not enabled. If it was, it would not be a modal form. So the VCL falls back to using:
WndParent := Application.MainFormHandle;
Manual parenting
This means i probably have to be sure to manually(?) set the popup parenting?
procedure TfrmMain.Button2Click(Sender: TObject);
var
frmControlPanel: TfrmControlPanel;
begin
frmControlPanel := TfrmControlPanel.Create(Application);
try
frmControlPanel.PopupParent := Self;
frmControlPanel.PopupMode := pmExplicit; //Automatically set to pmExplicit when you set PopupParent. But you get the idea.
frmControlPanel.ShowModal;
finally
frmControlPanel.Free;
end;
end;
Except that didn't work either. Clicking the first taskbar button causes the wrong form to activate:
At this point i'm thoroughly confused. The parent of my modal form should be frmMain, and it is!:
So what now?
I have a sense of what might be going on.
That taskbar button is a representation of frmMain. Windows is bringing that for forward.
Except it behaved correctly when MainFormOnTaskbar was set to false.
There must be some magic in Delphi VCL that caused correctness before, but gets disabled with MainFormOnTaskbar := True, but what is it?
I am not the first person to want a Delphi application to behave nicely with the Windows 95 toolbar. And i've asked this question in the past, but those answers were always geared towards Delphi 5 and it's old central routing window.
I've been told that everything was fixed around Delphi 2007 timeframe.
So what is the correct solution?
Bonus Reading
- http://blogs.msdn.com/b/oldnewthing/archive/2003/12/29/46371.aspx
- What does WS_EX_APPWINDOW do?
- My detail form is hidden behind main form when calling the TsaveDialog
- The Oracle at Delphi Blog: PopupMode and PopupParent
- DocWiki: Vcl.Forms.TForm.PopupMode
- DocWiki: Vcl.Forms.TCustomForm.PopupParent
- How can I start Delphi application with the hidden main form?
回答1:
It seems to me that the fundamental problem is that your main form is, in the eyes of the VCL, not your main form. Once you fix that, all the problems go away.
You should:
- Call
Application.CreateForm
exactly once, for the real main form. That is a good rule to follow. Consider the job ofApplication.CreateForm
to be to create the main form of your application. - Create the login form and set its
WndParent
to0
. That makes sure it appears on the taskbar. Then show it modally. - Create the main form in the usual way by calling
Application.CreateForm
. - Set
MainFormOnTaskbar
to beTrue
. - Set
WndParent
to0
for the modeless form.
And that's it. Here's a complete example:
Project1.dpr
program Project1;
uses
Vcl.Forms,
uMain in 'uMain.pas' {MainForm},
uLogin in 'uLogin.pas' {LoginForm},
uModeless in 'uModeless.pas' {ModelessForm};
{$R *.res}
begin
Application.Initialize;
Application.ShowHint := True;
Application.MainFormOnTaskbar := True;
with TLoginForm.Create(Application) do begin
ShowModal;
Free;
end;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end.
uLogin.pas
unit uLogin;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs;
type
TLoginForm = class(TForm)
protected
procedure CreateParams(var Params: TCreateParams); override;
end;
implementation
{$R *.dfm}
procedure TLoginForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.WndParent := 0;
end;
end.
uLogin.dfm
object LoginForm: TLoginForm
Left = 0
Top = 0
Caption = 'LoginForm'
ClientHeight = 300
ClientWidth = 635
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
end
uMain.pas
unit uMain;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, uModeless;
type
TMainForm = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
procedure TMainForm.Button1Click(Sender: TObject);
begin
with TModelessForm.Create(Self) do begin
Show;
end;
end;
end.
uMain.dfm
object MainForm: TMainForm
Left = 0
Top = 0
Caption = 'MainForm'
ClientHeight = 300
ClientWidth = 635
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 288
Top = 160
Width = 75
Height = 23
Caption = 'Button1'
TabOrder = 0
OnClick = Button1Click
end
end
uModeless.pas
unit uModeless;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TModelessForm = class(TForm)
Label1: TLabel;
protected
procedure CreateParams(var Params: TCreateParams); override;
end;
implementation
{$R *.dfm}
procedure TModelessForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.WndParent := 0;
end;
end.
uModeless.dfm
object ModelessForm: TModelessForm
Left = 0
Top = 0
Caption = 'ModelessForm'
ClientHeight = 300
ClientWidth = 635
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
ShowHint = True
PixelsPerInch = 96
TextHeight = 13
object Label1: TLabel
Left = 312
Top = 160
Width = 98
Height = 13
Hint = 'This is a hint'
Caption = 'I'#39'm a label with a hint'
end
end
If you'd rather the modeless form was owned by the main form, you can achieve that by replacing TModelessForm.CreateParams
with:
procedure TModelessForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;
来源:https://stackoverflow.com/questions/30809532/how-to-correctly-have-modeless-form-appear-in-taskbar