有什么正确的方法可以使无模式窗体出现在任务栏中?
研究努力
这些是我解决问题的尝试。有很多事情需要做才能使它表现得正确 - 仅仅让一个按钮出现在任务栏上并不是一个解决方案。让Windows应用程序表现出正确的Windows应用程序应该具有的功能是我的目标。
对于那些了解我的人,以及了解我的"展示研究努力"有多深入的人,请耐心等待,因为这将是一次奇妙的探险之旅。
问题就在标题中,在上面的横线上方。以下所有内容只是为了说明一些经常重复的建议是不正确的原因。
Windows仅为未拥有的窗口创建任务栏按钮
最初我有我的"主窗体",从那里我显示这个其他的非模态窗体:
procedure TfrmMain.Button2Click(Sender: TObject);
begin
if frmModeless = nil then
Application.CreateForm(TfrmModeless, frmModeless);
frmModeless.Show;
end;
这正确显示了新的表单,但任务栏上没有出现新的按钮:
没有创建任务栏按钮的原因是这是按设计来的。Windows只会为一个"未拥有"的窗口显示任务栏按钮。这个非模态的Delphi表单明显是已拥有的。在我的例子中,它是由所拥有的:
我的项目名是ModelessFormFail.dpr
,这是与所有者相关联的Windows类名Modelessformfail
的来源。
幸运的是,有一种方法可以“强制”Windows为窗口创建一个任务栏按钮,即使该窗口是所有者所有:
只需使用WS_EX_APPWINDOW
WS_EX_APPWINDOW
的MSDN文档如下所示:
WS_EX_APPWINDOW
0x00040000L
当窗口可见时,强制将顶级窗口放置在任务栏上。
这也是一个众所周知的Delphi技巧,可以覆盖CreateParams
并手动添加WS_EX_APPWINDOW
样式:
procedure TfrmModeless.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned window to appear in taskbar
end;
当我们运行此代码时,新创建的非模态窗体确实会获得自己的任务栏按钮:
我们完成了吗?不,因为它的行为不正确。
如果用户点击frmMain任务栏按钮,该窗口不会被置于前台。相反,另一个窗体(frmModeless)会被置于前台:
使窗体真正未被拥有
解决方法,正如你们中的一些人所知道的,不是与任务栏启发式和窗口对抗。如果我想让窗体未被拥有,那就让它未被拥有。
这(相当)简单。在CreateParam
中强制所有者窗口为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;
作为旁注,我想调查一下是否有一种方法可以使用
PopupMode
和PopupParent
属性使窗口不受控制。我发誓我在某个地方读到过你(David)的评论,如果你将Self
作为PopupParent
传递,例如: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;
这原本应该是一种超级秘密的方式,用于指示Delphi你想让表单没有所有者。但我现在无法在任何地方找到这个注释了。不幸的是,任何PopupParent
和PopupMode
的组合都不能使表单真正没有所有者:
- 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
我无法做任何事情使得表格实际上没有所有者(每次都使用Spy ++检查)。
在CreateParams
期间手动设置WndParent
:
- 确实会使窗体没有所有者
- 它确实有一个任务栏按钮
- 并且两个任务栏按钮都会正常工作:
我们完成了,对吧?我也是这么想的。我改变了一切来使用这种新技术。
除了我的修复存在问题,似乎会引起其他问题 - Delphi不喜欢我改变窗体的所有权。
提示窗口
我的非模态窗口上的一个控件有一个工具提示:
Application.Handle
或 Application.MainForm.Handle
,而不是应该拥有它的窗体:
现在重要的是我花一点时间展示我的应用程序不是一个主窗体和一个非模态窗体:
实际上是这样的:
- 登录界面(一个被隐藏的牺牲主窗体)
- 主界面
- 模态控制面板
- 显示无模式表单
但我们仍然面临着HintWindow所有权的问题,会导致错误的窗体被置于前台:
ShowMainFormOnTaskbar
当我试图创建一个最小的应用程序来重现问题时,我发现我做不到。有些事情是不同的:
- 我的Delphi 5应用程序移植到XE6之间
- 在XE6中创建的新应用程序
经过比较,我最终追踪到了这样一个事实:XE6中的新应用程序默认添加 MainFormOnTaskbar := True
在任何新项目中(可能是为了不破坏现有的应用程序):
program ModelessFormFail;
//...
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TfrmSacrificialMain, frmSacrificialMain);
//Application.CreateForm(TfrmMain, frmMain);
Application.Run;
end.
当我添加了这个选项后,工具提示的外观没有将错误的表单置于前台!
procedure TfrmSacrificialMain.Button1Click(Sender: TObject);
var
frmMain: TfrmMain;
begin
frmMain := TfrmMain.Create(Application);
Self.Hide;
try
frmMain.ShowModal;
finally
Self.Show;
end;
end;
当发生这种情况并且我“登录”时,我的任务栏图标完全消失:
- 未拥有的牺牲主表单不是隐藏的:因此按钮与其一起出现
- 真正的主表单是拥有的,因此它不会获得工具栏按钮
使用WS_APP_APPWINDOW
现在我们有机会使用WS_EX_APPWINDOW
。我想强制我的已拥有的主窗体出现在任务栏上。所以我重写CreateParams
并强制它出现在任务栏上:
procedure TfrmMain.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned window to appear in taskbar
end;
然后我们开始尝试:
看起来不错!
- 两个任务栏按钮
- 工具提示不会弹出错误的窗体
但是,当我点击第一个工具栏按钮时,错误的窗体会出现。它显示模态窗体 frmMain,而不是当前模态窗体 frmControlPanel:
这可能是因为新创建的frmControlPanel被弹出到了Application.MainForm而不是Screen.ActiveForm。请在Spy++中检查:
MainForm.Handle
。这是由于VCL中的另一个错误导致的。如果窗体的PopupMode
是:
- pmAuto
- pmNone(如果它是模态窗体)
Application.ActiveFormHandle
用作hWndParent
。不幸的是,它随后检查模态窗体的父级是否已启用:if (WndParent <> 0) and (
IsIconic(WndParent) or
not IsWindowVisible(WndParent) or
not IsWindowEnabled(WndParent)) then
当然,模态窗口的父级不可用。如果可用,那么它就不是模态窗口了。因此,VCL会退而使用:
WndParent := Application.MainFormHandle;
手动设置父级
这意味着我可能需要确保手动设置弹出窗口的父级关系?
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;
但是这也没有起作用。点击第一个任务栏按钮会激活错误的表单:
那现在怎么办?
我有一些想法。
任务栏按钮是frmMain的一个表示,Windows正在将其前置。
当MainFormOnTaskbar设置为false时,它的行为是正确的。
Delphi VCL中可能有一些魔法,在MainFormOnTaskbar := True时会导致正确性失效,但是是什么呢?
我不是第一个希望Delphi应用程序与Windows 95工具栏良好配合的人。 我以前问过这个问题,但那些答案总是针对Delphi 5及其旧的中央路由窗口。
我被告知在Delphi 2007时间范围内解决了所有问题。
那么正确的解决方案是什么呢?