如何正确地使非模式形式出现在任务栏中 [英] How to correctly have modeless form appear in taskbar

查看:102
本文介绍了如何正确地使非模式形式出现在任务栏中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试实现古老的Delphi梦想,即在任务栏中显示无模式形式。



什么是正确






研究工作



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



对于那些了解我的人以及我的显示出研究成果的深度继续,请稍等,因为它会沿着兔子洞狂奔。



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



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



最初我有我的主表单 ,从中我显示了另一种无模式的表单:

 过程TfrmMain.Button2Click(Sender:TObject); 

开始,如果frmModeless = nil,则
Application.CreateForm(TfrmModeless,frmModeless);

frmModeless.Show;
结尾;

这可以正确显示新表格,但任务栏上没有新按钮:





之所以没有创建任务栏按钮是因为这是设计使然。 技巧可以覆盖 CreateParams 并手动添加 WS_EX_APPWINDOW 样式:

 过程TfrmModeless。 CreateParams(var Params:TCreateParams); 
开始
继承;

Params.ExStyle:= Params.ExStyle或WS_EX_APPWINDOW; //强制拥有的窗口出现在任务栏
end中;

运行此命令时,新创建的无模态确实自己的任务栏按钮:





我们完成了吗?不,因为它无法正常运行。



如果用户单击 frmMain 任务栏按钮,则不会弹出该窗口。而是提出另一种形式( frmModeless ):





一旦您了解Windows的所有权概念,就可以理解。 Windows会设计使所有所有子窗体提前。所有权的全部目的是-将拥有的表单保持在其所有者之上。



使表单实际上不拥有



解决方案一切,我最终将其归结为XE6中的新应用程序默认在任何新版本中添加 MainFormOnTaskbar:= True 项目(大概不会破坏现有应用程序):

 程序ModelessFormFail; 
// ...
开始
Application.Initialize;
Application.MainFormOnTaskbar:=真;
Application.CreateForm(TfrmSacrificialMain,frmSacrificialMain);
//Application.CreateForm(TfrmMain,frmMain);
Application.Run;
结尾。

当我添加此选项时,工具提示的外观并没有带来错误的提示! :





成功!除此之外,知道即将发生什么的人

  • WS_EX_APPWINDOW是做什么的?

  • 调用TsaveDialog

  • Delphi上的Oracle博客:PopupMode和Popu pParent

  • DocWiki:Vcl.Forms.TForm.PopupMode

  • DocWiki:Vcl.Forms.TCustomForm.PopupParent

  • 如何使用隐藏的主表单启动Delphi应用程序?


  • 解决方案

    在我看来,根本的问题是,在VCL看来,您的主要形式不是主要形式形成。解决此问题后,所有问题都会消失。



    您应该:


    1. 对于真正的主表单,只需调用一次 Application.CreateForm 。这是遵循的好规则。考虑 Application.CreateForm 的工作是创建应用程序的主表单。

    2. 创建登录表单并设置其 WndParent 0 。这样可以确保它出现在任务栏上。然后以模态显示。

    3. 通过调用 Application.CreateForm 的常规方法创建主窗体。

    4. MainFormOnTaskbar 设置为 True

    5. 设置 WndParent 0 为无模格式。

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



    Project1.dpr

     计划Project1; 

    使用
    Vcl.Forms,在'uMain.pas'{MainForm}中使用
    uMain,在'uLogin.pas'{LoginForm}中使用
    uLogin,
    uModeless.pas {ModelessForm}中的uModeless;

    {$ R * .res}

    开始
    Application.Initialize;
    Application.ShowHint:= True;
    Application.MainFormOnTaskbar:=真;
    与TLoginForm.Create(Application)一起开始
    ShowModal;
    免费;
    结尾;
    Application.CreateForm(TMainForm,MainForm);
    Application.Run;
    结尾。

    uLogin.pas

     个单位uLogin; 

    界面

    使用
    Winapi.Windows,Winapi.Messages,System.SysUtils,System.Variants,System.Classes,Vcl.Graphics,
    Vcl.Controls,Vcl.Forms,Vcl.Dialogs;

    类型
    TLoginForm = class(TForm)
    受保护的
    过程CreateParams(var Params:TCreateParams);覆盖
    结尾;

    实现

    {$ R * .dfm}

    过程TLoginForm.CreateParams(var Params:TCreateParams);
    开始
    继承;
    Params.WndParent:= 0;
    结尾;

    结尾。

    uLogin.dfm

     对象LoginForm:TLoginForm 
    左侧= 0
    顶部= 0
    标题='LoginForm'
    ClientHeight = 300
    ClientWidth = 635
    颜色= clBtnFace
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -11
    Font.Name ='MS Sans衬线
    Font.Style = []
    OldCreateOrder = False
    PixelsPerInch = 96
    TextHeight = 13
    end

    uMain.pas

     单位uMain; 

    界面

    使用
    Winapi.Windows,Winapi.Messages,System.SysUtils,System.Variants,System.Classes,Vcl.Graphics,
    Vcl.Controls,Vcl.Forms,Vcl.Dialogs,Vcl.StdCtrls,uModeless;

    类型
    TMainForm = class(TForm)
    Button1:TButton;
    过程Button1Click(Sender:TObject);
    结尾;

    var
    MainForm:TMainForm;

    实现

    {$ R * .dfm}

    过程TMainForm.Button1Click(Sender:TObject);
    以TModelessForm开始$ b $b。Create(Self)开始
    显示;
    结尾;
    结尾;

    结尾。

    uMain.dfm

     对象MainForm:TMainForm 
    Left = 0
    Top = 0
    标题='MainForm'
    ClientHeight = 300
    ClientWidth = 635
    颜色= clBtnFace
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -11
    Font.Name ='MS Sans衬线
    Font.Style = []
    OldCreateOrder = False
    PixelsPerInch = 96
    TextHeight = 13
    object Button1:TButton
    Left = 288
    顶部= 160
    宽度= 75
    高度= 23
    标题='Button1'
    TabOrder = 0
    OnClick = Button1Click
    end
    结束

    uModeless.pas

     单位uModeless; 

    界面

    使用
    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;
    受保护的
    过程CreateParams(var Params:TCreateParams);覆盖
    结尾;

    实现

    {$ R * .dfm}

    过程TModelessForm.CreateParams(var Params:TCreateParams);
    开始
    继承;
    Params.WndParent:= 0;
    结尾;

    结尾。

    uModeless.dfm

     对象ModelessForm:TModelessForm 
    左= 0
    顶部= 0
    标题='ModelessForm'
    ClientHeight = 300
    ClientWidth = 635
    颜色= clBtnFace
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -11
    Font.Name ='MS Sans衬线
    Font.Style = []
    OldCreateOrder = False
    ShowHint = True
    PixelsPerInch = 96
    TextHeight = 13
    object Label1:TLabel
    左侧= 312
    顶部= 160
    宽度= 98
    高度= 13
    提示='这是一个提示'
    标题='I'#39'带有提示的ma标签'
    结束
    结束

    无模式表单归主表单所有,您可以通过将 TModelessForm.CreateParams 替换为:

     程序TModelessForm.CreateParams(var Params:TCreateParams); 
    开始
    继承;
    Params.ExStyle:= Params.ExStyle或WS_EX_APPWINDOW;
    结尾;


    I am trying to achieve the age-old Delphi dream of having a modeless form appear in the taskbar.

    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.

    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

    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". This modeless Delphi form is most definitely owned. In my case it is owned by the Application.Handle:

    My project's name is ModelessFormFail.dpr, which is the origin of the Windows class name Modelessformfail associated with the owner.

    Fortunately there is a way to force Windows to create a taskbar button for a window, even though the window is owned:

    Just use WS_EX_APPWINDOW

    The MSDN documentation of WS_EX_APPWINDOW says it:

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

    It also a well-known Delphi trick to override CreateParams and manually add the WS_EX_APPWINDOW style:

    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. Instead the other form (frmModeless) is brought forward:

    This makes sense once you understand the Windows concept of ownership. Windows will, by design, bring any child owned forms forward. It was the entire purpose of ownership - to keep owned forms on top of their owners.

    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.

    This is (fairly) simple. In CreateParam force the owner windows to be 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. I swear i read a comment (from you David) somewhere on SO saying that if you passed Self as the PopupParent, e.g.:

    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". But i cannot find the comment anywhere on now. Unfortunately, no combination of PopupParent and PopupMode cause a form to actually be un-owned:

    • 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

    Nothing i could do could cause the form to actually have no owner (each time checking with Spy++).

    Setting the WndParent manually during CreateParams:

    • does make the form unowned
    • it does have a taskbar button
    • and both taskbar buttons dobehave 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.

    Hint Windows

    One of the controls on my modeless window has a tooltop:

    The problem is that when this tooltip window appears, it causes the other form (frmMain, the modal one) to come forward. 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:

    I would have considered this a bug on Delphi's part; 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. 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:

    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
    • a new application created in 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):

    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. 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

    Now we have the opportunity to use 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:

    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:

    Presumably because the newly created frmControlPanel was PopupParented to Application.MainForm rather than Screen.ActiveForm. Check in Spy++:

    Yes, the parent is MainForm.Handle. This turns out to be because of another bug in the VCL. If the form's PopupMode is:

    • pmAuto
    • pmNone (if it's a modal form)

    the VCL attempts to use Application.ActiveFormHandle as the 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:

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

    So what now?

    I have a sense of what might be going on.

    That taskbar button is a representation of frmMain. Windows is bringing that for forward.

    Except it behaved correctly when MainFormOnTaskbar was set to false.

    There must be some magic in Delphi VCL that caused correctness before, but gets disabled with MainFormOnTaskbar := True, but what is it?

    I am not the first person to want a Delphi application to behave nicely with the Windows 95 toolbar. 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.

    I've been told that everything was fixed around Delphi 2007 timeframe.

    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. Once you fix that, all the problems go away.

    You should:

    1. Call Application.CreateForm exactly once, for the real main form. That is a good rule to follow. Consider the job of Application.CreateForm to be to create the main form of your application.
    2. Create the login form and set its WndParent to 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.
    4. Set MainFormOnTaskbar to be True.
    5. Set WndParent to 0 for the modeless form.

    And that's it. Here's a complete example:

    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
    

    If you'd rather the modeless form was owned by the main form, you can achieve that by replacing TModelessForm.CreateParams with:

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

    这篇关于如何正确地使非模式形式出现在任务栏中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆