12

我正在努力实现古老的 Delphi 梦想,即在任务栏中出现无模式表单。

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


研究工作

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

对于那些了解我,以及我的“展示研究工作”有多深入的人,请坚持住,因为这将是疯狂的兔子洞。

问题在标题中,以及上面的水平线上方。以下所有内容仅用于说明为什么一些经常重复的建议是不正确的。

Windows 仅创建为无主窗口的任务栏按钮

最初我有我的“主要形式”,从中我展示了另一种无模式形式:

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

    frmModeless.Show;
end;

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

在此处输入图像描述

没有创建任务栏按钮的原因是因为这是设计使然。Windows 只会为“无主”的窗口显示任务栏按钮。这种无模式的 Delphi 形式是绝对拥有的。在我的情况下,它归Application.Handle

在此处输入图像描述

我的项目名称是,它是与所有者关联ModelessFormFail.dpr的 Windows 类名的由来。Modelessformfail

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

只需使用WS_EX_APPWINDOW

的 MSDN 文档WS_EX_APPWINDOW说:

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):

在此处输入图像描述

一旦您了解了 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;

顺便说一句,我想调查是否有一种方法可以使用PopupModeandPopupParent属性使窗口无主。我发誓我在某处读到了一条评论(来自大卫),说如果你通过SelfPopupParent,例如:

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导致表单实际上是无主的:

  • 弹出模式:pmNone
    • 业主 hwnd:Application.Handle/Application.MainForm.Handle
  • 弹出模式:pmAuto
    • 业主 hwnd:Screen.ActiveForm.Handle
  • 弹出模式:pmExplicit
    • PopupParent:
      • 业主 hwnd:Application.MainForm.Handle
    • 弹出父:AForm
      • 业主 hwnd:AForm.Handle
    • PopupParent:自我
      • 业主 hwnd:Application.MainForm.Handle

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

WndParent期间手动设置CreateParams

  • 确实使表单无主
  • 确实有一个任务栏按钮
  • 并且两个任务栏按钮的行为正确:

在此处输入图像描述

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

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

提示窗口

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

在此处输入图像描述

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

在此处输入图像描述

原因可能是合乎逻辑的。Delphi HintWindow可能由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是 PopupParented 到Application.MainForm而不是Screen.ActiveForm。签入间谍++:

在此处输入图像描述

是的,父母是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;

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

在此处输入图像描述

在这一点上,我彻底糊涂了。我的模态表单的级应该是frmMain,它是!:

在此处输入图像描述

所以现在怎么办?

我对可能发生的事情有预感。

该任务栏按钮是frmMain的表示。Windows 正在推动这一点。

除非它在​​MainFormOnTaskbar设置为 false 时表现正确。

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

我不是第一个希望 Delphi 应用程序与 Windows 95 工具栏配合得很好的人。我过去曾问过这个问题,但这些答案总是面向 Delphi 5,它是旧的中央路由窗口。

有人告诉我,一切都在 Delphi 2007 时间范围内得到修复。

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

奖金阅读

4

1 回答 1

7

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

你应该:

  1. 只调用Application.CreateForm一次,用于真正的主窗体。这是一个很好的规则。考虑Application.CreateForm创建应用程序的主要表单的工作。
  2. 创建登录表单并将其设置WndParent0. 这确保它出现在任务栏上。然后模态显示。
  3. 通过调用以通常的方式创建主窗体Application.CreateForm
  4. 设置MainFormOnTaskbarTrue
  5. 设置WndParent0无模式形式。

就是这样。这是一个完整的例子:

项目1.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;
于 2015-06-13T07:06:57.623 回答