简体   繁体   English

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

[英]How to correctly have modeless form appear in taskbar

I am trying to achieve the age-old Delphi dream of having a modeless form appear in the taskbar.我正在努力实现古老的 Delphi 梦想,即在任务栏中出现无模式窗体。

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.让 Windows 应用程序像 Windows 应用程序一样正确运行应该是我的目标。

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 Windows 只为无主创建任务栏按钮 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" . Windows 只会显示“无主”的 window 的任务栏按钮 This modeless Delphi form is most definitely owned .这个无模式的 Delphi 表格绝对是拥有的。 In my case it is owned by the Application.Handle :在我的例子中,它归Application.Handle所有:

在此处输入图像描述

My project's name is ModelessFormFail.dpr , which is the origin of the Windows class name Modelessformfail associated with the owner.我的项目名称是ModelessFormFail.dpr ,这是与所有者关联的 Windows class 名称Modelessformfail的来源。

Fortunately there is a way to force Windows to create a taskbar button for a window, even though the window is owned:幸运的是,有一种方法可以强制Windows 为 window 创建任务栏按钮,即使 window 已被拥有:

Just use WS_EX_APPWINDOW只需使用WS_EX_APPWINDOW

The MSDN documentation of WS_EX_APPWINDOW says it: WS_EX_APPWINDOW的 MSDN 文档说:

WS_EX_APPWINDOW 0x00040000L Forces a top-level window onto the taskbar when the window is visible. WS_EX_APPWINDOW 0x00040000L当 window 可见时,强制将顶级 window 放到任务栏上。

It also a well-known Delphi trick to override CreateParams and manually add the WS_EX_APPWINDOW style:它也是一个著名的 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;

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.如果用户单击frmMain任务栏按钮,则不会显示 window。 Instead the other form ( frmModeless ) is brought forward:相反,提出了另一种形式( frmModeless ):

在此处输入图像描述

This makes sense once you understand the Windows concept of ownership .一旦您理解了所有权的 Windows 概念,这是有道理的。 Windows will, by design, bring any child owned forms forward. Windows 将按照设计将 forms拥有的任何子级向前移动。 It was the entire purpose of ownership - to keep owned forms on top of their owners.这是所有权的全部目的 - 将拥有的 forms 置于其所有者之上。

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. 正如你们中的一些人所知,解决方案不是对抗任务栏启发式算法和 windows。如果我希望表单无主,请将其设置为无主。

This is (fairly) simple.这(相当)简单。 In CreateParam force the owner windows to be null :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;

As an aside, i wanted to investigate is there was a way to use the PopupMode and PopupParent properties to make a window unowned.顺便说一句,我想调查是否有一种方法可以使用PopupModePopupParent属性来制作 window unowned。 I swear i read a comment (from you David) somewhere on SO saying that if you passed Self as the PopupParent , eg:发誓我在 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;

it was supposed to be the super-secret way to indicate to Delphi that you want to form to have "no owner" .它应该是向 Delphi 表明您想形成“无所有者”的超级秘密方式。 But i cannot find the comment anywhere on now.但我现在无法在任何地方找到评论。 Unfortunately, no combination of PopupParent and PopupMode cause a form to actually be un-owned:不幸的是,没有PopupParentPopupMode的组合会导致表单实际上被取消拥有:

  • PopupMode: pmNone弹出模式: pmNone
    • Owner hwnd: Application.Handle/Application.MainForm.Handle所有者 hwnd: Application.Handle/Application.MainForm.Handle
  • PopupMode: pmAuto弹出模式: pmAuto
    • Owner hwnd: Screen.ActiveForm.Handle所有者 hwnd: Screen.ActiveForm.Handle
  • PopupMode: pmExplicit弹出模式: pmExplicit
    • PopupParent: nil PopupParent:
      • Owner hwnd: Application.MainForm.Handle所有者 hwnd: Application.MainForm.Handle
    • PopupParent: AForm PopupParent: AForm
      • Owner hwnd: AForm.Handle所有者 hwnd: AForm.Handle
    • PopupParent: Self PopupParent:自己
      • Owner hwnd: Application.MainForm.Handle所有者 hwnd: Application.MainForm.Handle

Nothing i could do could cause the form to actually have no owner (each time checking with Spy++).我无能为力导致表单实际上没有所有者(每次使用 Spy++ 检查)。

Setting the WndParent manually during CreateParams :CreateParams期间手动设置WndParent

  • does make the form unowned确实使表格无主
  • it does have a taskbar button确实有一个任务栏按钮
  • and both taskbar buttons do behave 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.除了我的修复有问题似乎会导致其他问题 - Delphi 不喜欢我更改为表单的所有权。

Hint Windows提示 Windows

One of the controls on my modeless window has a tooltop:我的无模式 window 上的控件之一有一个工具面板:

在此处输入图像描述

The problem is that when this tooltip window appears, it causes the other form ( frmMain , the modal one) to come forward.问题是,当此工具提示 window 出现时,它会导致另一种形式( frmMain ,模态形式)出现。 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: Delphi HintWindow可能属于Application.HandleApplication.MainForm.Handle ,而不是属于它应该属于的表单:

在此处输入图像描述

I would have considered this a bug on Delphi's part;我会认为这是 Delphi 的一个错误; 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.即使在应用程序布局的现实情况下,除了提示 window 所有权之外的所有内容都有效。 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:但是我们仍然有 HintWindow 所有权带来错误形式的问题:

在此处输入图像描述

ShowMainFormOnTaskbar 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我的 Delphi 5 应用移植到 XE6 之间
  • a new application created in XE6在 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):比较了所有内容之后,我最终将其追溯到 XE6 中的新应用程序在任何新项目中默认添加MainFormOnTaskbar:= True的事实(大概是为了不破坏现有应用程序):

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 . Success, Except, 知道未来的人知道未来会发生什么 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使用 WS_APP_APPWINDOW

Now we have the opportunity to use WS_EX_APPWINDOW .现在我们有机会使用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:所以我覆盖了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;

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 :它显示模态frmMain ,而不是当前模态frmControlPanel

在此处输入图像描述

Presumably because the newly created frmControlPanel was PopupParented to Application.MainForm rather than Screen.ActiveForm .大概是因为新创建的frmControlPanel是 PopupParented 到Application.MainForm而不是Screen.ActiveForm Check in Spy++:检查间谍++:

在此处输入图像描述

Yes, the parent is MainForm.Handle .是的,父项是MainForm.Handle This turns out to be because of another bug in the VCL.事实证明这是因为 VCL 中的另一个错误。 If the form's PopupMode is:如果表单的PopupMode是:

  • pmAuto汽车
  • pmNone (if it's a modal form) pmNone (如果是模态形式)

the VCL attempts to use Application.ActiveFormHandle as the hWndParent . VCL 尝试使用Application.ActiveFormHandle作为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:所以 VCL 回退到使用:

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::我的模态形式的级应该是frmMain ,它是:

在此处输入图像描述

So what now?所以现在怎么办?

I have a sense of what might be going on.我知道可能会发生什么。

That taskbar button is a representation of frmMain .该任务栏按钮是frmMain的表示。 Windows is bringing that for forward. Windows 将其提前。

Except it behaved correctly when MainFormOnTaskbar was set to false.除了当MainFormOnTaskbar设置为 false 时它表现正确。

There must be some magic in Delphi VCL that caused correctness before, but gets disabled with MainFormOnTaskbar:= True , but what is it? Delphi VCL 中一定有一些魔法导致之前正确,但被MainFormOnTaskbar:= True禁用,但它是什么?

I am not the first person to want a Delphi application to behave nicely with the Windows 95 toolbar.我不是第一个希望 Delphi 应用程序能够与 Windows 95 工具栏良好配合的人。 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.我过去问过这个问题,但这些答案总是针对 Delphi 5,它是旧的中央路由 window。

I've been told that everything was fixed around Delphi 2007 timeframe.有人告诉我,一切都在 Delphi 2007 时间范围内修复。

So what is the correct solution?那么正确的解决方法是什么?

Bonus Reading红利阅读

It seems to me that the fundamental problem is that your main form is, in the eyes of the VCL, not your main form. 在我看来,根本的问题是,在VCL看来,您的主要形式不是您的主要形式。 Once you fix that, all the problems go away. 解决此问题后,所有问题都会消失。

You should: 你应该:

  1. Call Application.CreateForm exactly once, for the real main form. 对于真正的主表单,只需调用一次Application.CreateForm That is a good rule to follow. 这是遵循的好规则。 Consider the job of Application.CreateForm to be to create the main form of your application. 考虑Application.CreateForm的工作是创建应用程序的主要形式。
  2. Create the login form and set its WndParent to 0 . 创建登录表单并将其WndParent设置为0 That makes sure it appears on the taskbar. 这样可以确保它出现在任务栏上。 Then show it modally. 然后模态显示。
  3. Create the main form in the usual way by calling Application.CreateForm . 通过调用Application.CreateForm以通常的方式创建主表单。
  4. Set MainFormOnTaskbar to be True . MainFormOnTaskbar设置为True
  5. Set WndParent to 0 for the modeless form. 对于无模式形式,将WndParent设置为0

And that's it. 就是这样。 Here's a complete example: 这是一个完整的示例:

Project1.dpr 项目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 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 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 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 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 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 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: 如果您希望无模式表单由主表单拥有,则可以通过将TModelessForm.CreateParams替换为:

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

Great working thanks so much I use this干得好,非常感谢,我用这个

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

.... which form is showing (active) that show on windows task bar if * line active only 1 form showing on taskbar when i hide main form and show other form i cant see at windows task bar ....当我隐藏主窗体并显示我在 windows 任务栏上看不到的其他窗体时,在 windows 任务栏上显示(活动)哪个窗体如果 * 行仅在任务栏上显示一个窗体

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM