如何正确地使无模式窗体显示在任务栏上

12
我希望您能在任务栏中显示无模式窗体,这是Delphi梦寐以求的目标。
有什么正确的方法可以使无模式窗体出现在任务栏中?

研究努力

这些是我解决问题的尝试。有很多事情需要做才能使它表现得正确 - 仅仅让一个按钮出现在任务栏上并不是一个解决方案。让Windows应用程序表现出正确的Windows应用程序应该具有的功能是我的目标。

对于那些了解我的人,以及了解我的"展示研究努力"有多深入的人,请耐心等待,因为这将是一次奇妙的探险之旅。

问题就在标题中,在上面的横线上方。以下所有内容只是为了说明一些经常重复的建议是不正确的原因。

Windows仅为未拥有的窗口创建任务栏按钮

最初我有我的"主窗体",从那里我显示这个其他的非模态窗体:

procedure TfrmMain.Button2Click(Sender: TObject);
begin
    if frmModeless = nil then
        Application.CreateForm(TfrmModeless, frmModeless);

    frmModeless.Show;
end;

这正确显示了新的表单,但任务栏上没有出现新的按钮:

enter image description here

没有创建任务栏按钮的原因是这是按设计来的。Windows只会为一个"未拥有"的窗口显示任务栏按钮。这个非模态的Delphi表单明显是已拥有的。在我的例子中,它是由所拥有的:

enter image description here

我的项目名是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;

当我们运行此代码时,新创建的非模态窗体确实会获得自己的任务栏按钮:

enter image description here

我们完成了吗?不,因为它的行为不正确。

如果用户点击frmMain任务栏按钮,该窗口不会被置于前台。相反,另一个窗体(frmModeless)会被置于前台:

enter image description here

这在你理解Windows的所有权概念后就很有道理了。按照设计,Windows会将任何子窗体拥有的窗体提前显示。这正是所有权的全部目的——将拥有的窗体保持在其所有者之上。

使窗体真正未被拥有

解决方法,正如你们中的一些人所知道的,不是与任务栏启发式和窗口对抗。如果我想让窗体未被拥有,那就让它未被拥有。

这(相当)简单。在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;

作为旁注,我想调查一下是否有一种方法可以使用PopupModePopupParent属性使窗口不受控制。我发誓我在某个地方读到过你(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你想让表单没有所有者。但我现在无法在任何地方找到这个注释了。不幸的是,任何PopupParentPopupMode的组合都不能使表单真正没有所有者:

  • PopupMode: pmNone
    • Owner hwnd: Application.Handle/Application.MainForm.Handle
  • PopupMode: pmAuto
    • Owner hwnd: Screen.ActiveForm.Handle
  • PopupMode: pmExplicit
    • PopupParent: nil
      • Owner hwnd: Application.MainForm.Handle
    • PopupParent: AForm
      • Owner hwnd: AForm.Handle
    • PopupParent: Self
      • Owner hwnd: Application.MainForm.Handle

我无法做任何事情使得表格实际上没有所有者(每次都使用Spy ++检查)。

CreateParams期间手动设置WndParent

  • 确实会使窗体没有所有者
  • 确实有一个任务栏按钮
  • 并且两个任务栏按钮都会正常工作:

enter image description here

我们完成了,对吧?我也是这么想的。我改变了一切来使用这种新技术。

除了我的修复存在问题,似乎会引起其他问题 - Delphi不喜欢我改变窗体的所有权。

提示窗口

我的非模态窗口上的一个控件有一个工具提示:

enter image description here

问题在于当这个提示窗口出现时,它会导致其他表单(frmMain,模态的)前置。它并没有获得激活焦点;但是它现在遮挡了我所看的表单:

enter image description here

可能的原因是逻辑上的。Delphi的 HintWindow 可能归属于 Application.Handle Application.MainForm.Handle ,而不是应该拥有它的窗体:

enter image description here

我认为这是 Delphi 的一个错误,使用了错误的所有者。
转移以查看实际应用程序布局

现在重要的是我花一点时间展示我的应用程序不是一个主窗体和一个非模态窗体:

enter image description here

实际上是这样的:

  • 登录界面(一个被隐藏的牺牲主窗体)
  • 主界面
  • 模态控制面板
  • 显示无模式表单

enter image description here

即使应用程序布局的实际情况如此,除了提示窗口所有内容都有效。有两个任务栏按钮,单击它们会将正确的表单带到前台:

enter image description here

但我们仍然面临着HintWindow所有权的问题,会导致错误的窗体被置于前台:

enter image description here

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.

当我添加了这个选项后,工具提示的外观没有将错误的表单置于前台!

enter image description here

成功!但是,那些知道接下来会发生什么的人除外。我的"牺牲品"主登录表单显示了"真正的"主表单,隐藏了自己:
procedure TfrmSacrificialMain.Button1Click(Sender: TObject);
var
    frmMain: TfrmMain;
begin
    frmMain := TfrmMain.Create(Application);
    Self.Hide;
    try
        frmMain.ShowModal;
    finally
        Self.Show;
    end;
end;

当发生这种情况并且我“登录”时,我的任务栏图标完全消失:

enter image description here

这是因为:
  • 未拥有的牺牲主表单不是隐藏的:因此按钮与其一起出现
  • 真正的主表单是拥有的,因此它不会获得工具栏按钮

使用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;

然后我们开始尝试:

enter image description here

看起来不错!

  • 两个任务栏按钮
  • 工具提示不会弹出错误的窗体

但是,当我点击第一个工具栏按钮时,错误的窗体会出现。它显示模态窗体 frmMain,而不是当前模态窗体 frmControlPanel

enter image description here

这可能是因为新创建的frmControlPanel被弹出到了Application.MainForm而不是Screen.ActiveForm。请在Spy++中检查:

enter image description here

是的,父级是MainForm.Handle。这是由于VCL中的另一个错误导致的。如果窗体的PopupMode是:
  • pmAuto
  • pmNone(如果它是模态窗体)
VCL会尝试将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;

但是这也没有起作用。点击第一个任务栏按钮会激活错误的表单:

enter image description here

此时我十分困惑。我的模态表单的父级应该是frmMain,而它就是!:

enter image description here

那现在怎么办?

我有一些想法。

任务栏按钮是frmMain的一个表示,Windows正在将其前置。

MainFormOnTaskbar设置为false时,它的行为是正确的。

Delphi VCL中可能有一些魔法,在MainFormOnTaskbar := True时会导致正确性失效,但是是什么呢?

我不是第一个希望Delphi应用程序与Windows 95工具栏良好配合的人。 我以前问过这个问题,但那些答案总是针对Delphi 5及其旧的中央路由窗口。

我被告知在Delphi 2007时间范围内解决了所有问题。

那么正确的解决方案是什么呢?

额外阅读


4
@KenWhite 图像不是问题(我认为)。这个问题的主要问题在于,除非我花下一个小时阅读它,否则我不认为我能够知道真正的问题和期望的行为是什么。 - Dalija Prasnikar
4
@DalijaPrasnikar:是的,有人错过了[帮助]中关于“不要写书”的部分。你的问题应该简洁明了,而且不需要几十张插图。那些使用移动设备(尤其是使用Web浏览器而不是SO应用程序的用户)会感到非常恼火。我很想给Ian发一张数据费账单。 - Ken White
问题的标题不清楚。 - Jouan
2
@IanBoyd:在我看来,他们本不应该首先引入 "ShowMainFormOnTaskbar"。相反,他们应该引入单独的 "TApplication.ShowOnTaskbar" 和 "TForm.ShowOnTaskbar" 属性,甚至可能还有一个 "TForm.ApplicationWindowIsOwner" 属性。多少年来,有多少人一再要求从任务栏中隐藏 "TApplication" 窗口或将 "TForm" 窗口添加到任务栏中?太多了,数不胜数。这些应该作为单独的功能添加。也许他们仍然可以这样做,并废弃 "ShowMainFormOnTaskbar" 以设置适当的属性。 - Remy Lebeau
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Ian Boyd
显示剩余9条评论
2个回答

7
我觉得根本问题在于你的主窗体在VCL的眼中不是真正的主窗体。一旦你解决了这个问题,所有的问题都会消失。
你需要做以下几步:
  1. 仅调用一次 Application.CreateForm,创建真正的主窗体。这是一个很好的规则。考虑 Application.CreateForm 的工作是创建你应用程序的主窗体。
  2. 创建登录窗体并将其 WndParent 设置为 0。这样可以确保它显示在任务栏上。然后以模态方式显示它。
  3. 按照通常的方式通过调用 Application.CreateForm 创建主窗体。
  4. 将 MainFormOnTaskbar 设置为 True。
  5. 对于非模态窗体,将 WndParent 设置为 0。
就是这样。下面是一个完整的示例: 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

如果你想让无模式窗体归属于主窗体,你可以通过替换TModelessForm.CreateParams来实现:

procedure TModelessForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;

更改的问题是关闭“主”窗体不再返回“登录”窗体。在修订版中,主窗体关闭并终止应用程序。我确实遵循了您添加(另一个)修复程序到Vcl.Controls.pas以设置THintWindow的正确父级的建议,并且似乎起作用了。一般来说,我之所以提出这个问题,是因为我盲目地假设Delphi已经解决了它。多年来,人们一直告诉我“停止运行过时版本的Delphi”。我想这一切都解决了,我只是错过了像.ShowModless之类的东西。 - Ian Boyd
1
你可以通过更改主窗体的关闭操作并显示一个新的登录窗体或重新显示原始窗体来解决这个问题。从使主窗体成为VCL主窗体的前提开始,并围绕它进行适配。然后一切都会很好。 - David Heffernan
应该这样做,但我不想做。 :) 然后我想起 Raymond Chen 的话:没有人说编程会很容易 - Ian Boyd
嗯,你应该想要这样做。这会让一切变得更容易。 - David Heffernan
你能想到一种方法,在 MainForm 关闭后重新显示登录表单吗?在你的示例中,你在进入 MainFormApplication.Run 之前销毁了登录表单。我需要重新显示那个登录表单。 - Ian Boyd
创建新的问题在于它不是旧的。我发现了最好的方法。与其使用.ShowModal显示我的“主”表单,我使用Application.Run来显示它。这样,在调用.ShowModal后的所有代码都可以继续运行(在其所有现有的try..finally中),最重要的是用户不会失去他们已经拥有的现有登录表单。 - Ian Boyd

0

非常好的工作,非常感谢。 我使用了这个。

  Application.Initialize;
 // Application.MainFormOnTaskbar := True;///*
  Application.CreateForm(TAT_musteriler, AT_dATA);
  Application.CreateForm(TForm2, Form2);
  Application.CreateForm(TForm1, Form1);
  Application.CreateForm(TForm3, Form3);
  Application.CreateForm(TForm4, Form4);

哪种表单(活动的)会显示在Windows任务栏上,如果*行是活动的,则只有1个表单显示在任务栏上,当我隐藏主表单并显示其他表单时,我无法在Windows任务栏上看到它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接