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

How to correctly have modeless form appear in taskbar

我正在努力实现 age-old Delphi 在任务栏中出现无模式窗体的梦想。

在任务栏中显示无模式窗体的正确方法是什么?


研究工作

这些是我解决问题的尝试。要使它 正确 需要做很多事情 - 简单地在任务栏上显示一个按钮不是解决方案。让 Windows 应用程序像 Windows 应用程序一样正确运行应该是我的目标。

对于那些了解我,以及我的 "shows research effort" 有多深的人,坚持下去,因为它会像兔子洞一样疯狂。

问题在标题中,也在上面的横线上。以下所有内容仅用于说明为什么 oft-repeated 中的某些建议不正确。

Windows 只为无主创建任务栏按钮 windows

最初我有我的"Main Form",从那我展示了另一个无模式形式:

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

    frmModeless.Show;
end;

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

没有创建任务栏按钮的原因是设计使然。 Windows will only show a taskbar button for a window that "unowned"。这种无模式 Delphi 形式绝对是 拥有的 。在我的例子中,它属于 Application.Handle:

我的项目名称是 ModelessFormFail.dpr,这是与所有者关联的 Windows class 名称 Modelessformfail 的来源。

幸运的是,有一种方法可以 强制 Windows 为 window 创建任务栏按钮,即使 window 已被拥有:

只需使用WS_EX_APPWINDOW

WS_EX_APPWINDOW 的 MSDN 文档说:

WS_EX_APPWINDOW 0x00040000L Forces a top-level window onto the taskbar when the window is visible.

它也是一个 well-known 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 任务栏按钮,则不会显示 window。而是提出了另一种形式(frmModeless):

一旦您理解了 所有权 的 Windows 概念,这就很有意义了。 Windows 将按设计将任何 child 拥有的 表格向前移动。这是所有权的全部目的 - 将拥有的表单置于其所有者之上。

使表格真正无主

解决方案,as some of you know 不对抗任务栏启发式和windows。如果我希望表单无主,请将其设置为无主。

这(相当)简单。在 CreateParam 中强制所有者 windows 为 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 and PopupParent 属性使 window 成为无主。我 发誓 我在 SO 的某个地方读到一条评论(来自你大卫)说如果你通过 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;

它应该是 super-secret 向 Delphi 表明你想要形成 "no owner" 的方式。但我现在无法在任何地方找到评论。不幸的是,PopupParentPopupMode 的组合不会导致表单 实际上 成为 un-owned:

我无能为力导致表单实际上 没有 所有者(每次使用 Spy++ 检查)。

CreateParams 期间手动设置 WndParent

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

除了我的修复有问题似乎会导致其他问题 - Delphi 不喜欢我更改为表单的所有权。

提示Windows

我的无模式 window 上的一个控件有一个工具栏:

问题是,当这个工具提示 window 出现时,它会导致另一种形式(frmMain,模态形式)出现。它不会获得激活焦点;但它现在确实掩盖了我正在查看的表格:

原因可能是合乎逻辑的。 Delphi HintWindo 可能属于 Application.HandleApplication.MainForm.Handle,而不是属于它应该属于的形式:

我会认为这是 Delphi 方面的错误;使用错误的所有者。

查看实际应用布局的转移

现在重要的是我要花点时间证明我的应用程序不是主窗体和无模式窗体:

实际上是:

即使在应用程序布局的现实情况下,除了提示 window 所有权之外的所有内容都有效。有两个任务栏按钮,单击它们会向前移动正确的表单:

但我们仍然遇到 HintWindow 所有权带来错误形式的问题:

ShowMainFormOnTaskbar

当我试图创建一个最小的应用程序来重现问题时,我意识到我做不到。有一些不同:

comparing 一切之后,我终于追查到 XE6 中的新应用程序在任何新项目中默认添加 MainFormOnTaskbar := True 的事实(大概是为了不破坏现有的应用程序):

program ModelessFormFail;
//...
begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TfrmSacrificialMain, frmSacrificialMain);
  //Application.CreateForm(TfrmMain, frmMain);
  Application.Run;
end.

当我添加这个选项时,工具提示的出现并没有将错误的表格向前推进!:

成功!除了,知道会发生什么的人 know what's coming。我的 "sacrificial" 主登录表单显示 "real" 主表单,隐藏自身:

procedure TfrmSacrificialMain.Button1Click(Sender: TObject);
var
    frmMain: TfrmMain;
begin
    frmMain := TfrmMain.Create(Application);
    Self.Hide;
    try
        frmMain.ShowModal;
    finally
        Self.Show;
    end;
end;

发生这种情况时,我 "login",我的任务栏图标完全消失了:

发生这种情况是因为:

使用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是PopupParented Application.MainForm而不是Screen.ActiveForm。签入 Spy++:

是的,parent 是 MainForm.Handle。事实证明这是因为 VCL 中的另一个错误。如果表单的 PopupMode 是:

VCL 尝试使用 Application.ActiveFormHandle 作为 hWndParent。不幸的是,它随后检查模态形式的 parent 是否已启用:

if (WndParent <> 0) and (
      IsIconic(WndParent) or 
      not IsWindowVisible(WndParent) or
      not IsWindowEnabled(WndParent)) then

当然模态表单的parent是没有启用的。如果是,它就不是模态形式。所以 VCL 退回到使用:

WndParent := Application.MainFormHandle;

手动parenting

这意味着我可能必须确保手动(?)设置弹出窗口 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;

除此之外也没有用。单击第一个任务栏按钮会导致激活错误的表单:

此时我彻底糊涂了。我的模态形式的parent应该是frmMain,结果是!:

那现在呢?

我知道可能会发生什么。

该任务栏按钮代表 frmMain。 Windows 正在推进这一点。

除了当 MainFormOnTaskbar 设置为 false 时它的行为正确。

Delphi VCL 中一定有一些魔法导致了之前的正确性,但是被 MainFormOnTaskbar := True 禁用了,但它是什么?

我不是第一个希望 Delphi 应用程序与 Windows 95 工具栏配合良好的人。我过去问过这个问题,但这些答案总是针对 Delphi 5 并且它是旧的中央路由 window.

我被告知一切都在 Delphi 2007 年左右修复。

那么正确的解决方法是什么?

红利阅读

在我看来,根本问题在于,在 VCL 看来,您的主窗体不是您的主窗体。一旦你解决了这个问题,所有的问题都会消失。

你应该:

  1. 只调用一次Application.CreateForm,用于真正的主窗体。这是一个很好的规则。考虑 Application.CreateForm 的工作是创建应用程序的主窗体。
  2. 创建登录表单并将其 WndParent 设置为 0。这确保它出现在任务栏上。然后模态显示。
  3. 通过调用 Application.CreateForm.
  4. 以通常的方式创建主窗体
  5. MainFormOnTaskbar 设置为 True
  6. 将无模式窗体的 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;