Delphi在应用程序运行时更改主窗体

12

我有这个问题。当我隐藏我的主窗体时,应用程序的任务栏图标也会被隐藏。我看到了一个关于这个问题的新问题,但是答案并没有真正帮助我解决问题。他们建议将其最小化,但是我不想最小化应用程序。

是否可能在应用程序已经运行的情况下更改主窗体?

例如,我有两个窗体。当我想隐藏其中一个窗体并显示另一个窗体时,任务栏图标应该停留在任务栏上,而主窗体应该切换到另一个窗体。

我正在使用 Delphi XE6,这是一个 VCL 窗体应用程序。

我还看到了一个关于在运行时更改主窗体的不同老问题,但它非常古老,仍然基于 Delphi 6。


你可以使用 Pointer((@Application.MainForm)^) := Form2; 来更改主窗体。 - linluk
2
@linluk:这是错误的建议。MainForm是一个属性,而不是变量。你不能获取属性的地址,所以实际上你获取的是属性返回的指针的地址。然后你修改了那个指针指向不同的对象,你并没有修改TApplication内部的指针。 - Remy Lebeau
1
听起来真的很危险:S,到目前为止它一直在工作,我知道这是一个不好的hack,但是我会寻找更好的解决方案,用于我未来的应用程序中(也许通过RTTI设置变量,或者尝试避免改变主窗体的需要 :))。 - linluk
1
@Remy - 属性返回的指针地址是 TApplication 内部的指针,因为 MainForm 直接读取了 FMainForm。该语句成功地写入了 FMainForm - Sertac Akyuz
@SertacAkyuk: Pointer((@Application.MainForm)^) := Form2; 应该与 Tmp := Application.MainForm; Pointer((@Tmp)^) := Form2; 相同 - 修改临时变量,而不是 TApplication.FMainForm 字段。你所说的唯一可能是编译器优化了属性读取,因此代码变成了 Pointer((@Application.FMainForm)^) := Form2; - Remy Lebeau
显示剩余2条评论
9个回答

8
当应用程序正在运行时更改主窗体是不可能的。该属性在程序启动时就确定了。对于您来说,一种可能的方法是安排次要窗体(不是主窗体)在任务栏上有一个按钮。通过使其无主,或使用WS_EX_APPWINDOW扩展窗口样式来实现。更新:您可以更改Application.MainForm,但这需要您销毁当前的主窗体,然后创建一个新的窗体。

好的,谢谢。我已经考虑过了。为什么当我隐藏主窗体时它会隐藏任务栏图标呢?因为我的应用程序仍在运行,只是窗体被隐藏了。 - Shaun Roselt
设计上,隐藏的窗口不会显示在任务栏上:http://msdn.microsoft.com/en-gb/library/windows/desktop/cc144179.aspx 最简单的方法是将窗口最小化,这样就可以为无法看到的窗口拥有一个任务栏按钮。 - David Heffernan
2
@DavidHeffernan 我认为可以更改主窗体。我知道这一点,因为我在我的一个应用程序中使用它。我像这样做:Pointer((@Application.MainForm)^) := Form2;,它可以正常工作。我可以关闭旧的主窗体而不关闭应用程序,当我关闭新的主窗体时,应用程序会按预期终止。 - linluk
3
@linluk 明显这是一种粗糙的黑客行为,会有什么后果? - David Heffernan
我目前还没有遇到任何问题,所以我认为/希望没有后果。 - linluk

6

正如David Heffernan所说,不可能更改已运行应用程序的主窗体。这是Windows本身的限制。

你可以欺骗系统,实际上并没有更改主窗体,只是让它看起来像是更改了。
如何实现呢?

步骤1:在第二个窗体中添加代码以创建自己的任务栏按钮。

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

步骤2:在切换到第二个表单之前动态创建第二个表单。在其创建时,先前添加的代码将为您的第二个表单创建一个新的任务栏按钮。

步骤3:现在隐藏您实际的主表单。隐藏它也会隐藏属于它的任务栏按钮。因此,您最终仍然只有一个任务栏按钮显示出来,即属于您的第二个表单的任务栏按钮。

步骤4:为了允许您的第二个表单在关闭时终止应用程序,请从第二个表单的OnClose或OnFormCloseQuery事件中调用真正的主表单的Close方法。
如果您想能够切换回真正的主表单,请使用Main Form的Show方法而不是Close方法。

这种方法允许我们快速交换表单,因此只有最敏锐的用户会注意到任务栏按钮的短动画。
注意:如果您的第二个表单比较复杂,因此需要一些时间来创建,则可能希望将其隐藏并在创建过程完成后显示它并进行交换。否则,您可能会同时出现两个任务栏按钮,我相信您希望避免这种情况。

这里是一个简短的例子:
- LoginForm是一个真正的主表单,它在应用程序启动时创建 - WorkForm是用户在登录过程中花费大部分时间的表单,这个表单是在登录过程中创建的

Login Form代码:

unit ULoginForm;

interface

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

type
  TLoginForm = class(TForm)
    BLogIn: TButton;
    procedure BLogInClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  LoginForm: TLoginForm;

  //Global variable to tell us if we are only logging out or closing our program
  LoggingOut: Boolean;

implementation

uses Unit2;

{$R *.dfm}

procedure TLoginForm.BLogInClick(Sender: TObject);
begin
  //Create second Form
  Application.CreateForm(TWorkForm, WorkForm);
  //Hide Main Form
  Self.Hide;
  //Don't forget to clear login fields
end;

end.

从代码中工作:

unit UWorkForm;

interface

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

type
  TWorkForm = class(TForm)
    BLogOut: TButton;
    //Used in overriding forms creating parameters so we can add its own Taskbar button
    procedure CreateParams(var Params: TCreateParams); override;
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure BLogOutClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  WorkForm: TWorkForm;

implementation

uses Unit1;

{$R *.dfm}

procedure TWorkForm.BLogOutClick(Sender: TObject);
begin
  //Set to true so we know we are in the process of simply logging out
  LoggingOut := True;
  //Call close method to begin closing the current Form
  Close;
end;

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

procedure TWorkForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  //Check to see if we are in the process of simply logging out
  if not LoggingOut then
  begin
    //If we are not in the process of logging out close the Main Form
    LoginForm.Close;
    //and then allow closing of current form
    CanClose := True;
  end
  else
  begin
    //But if we are in the process of simply logging out show the Main Form
    LoginForm.Show;
    //Reset the LoggingOut to false
    LoggingOut := False;
    //and then alow closing of current form
    CanClose := True;
  end;
end;

end.

2
永远不要将窗口所有者设置为桌面。http://blogs.msdn.com/b/oldnewthing/archive/2004/02/24/79212.aspx - David Heffernan
3
  • "这是Windows本身的限制。"
  • Windows并不关心你是否有一个主窗体,它甚至没有主窗体的概念。
- Sertac Akyuz

5
一旦Application.MainForm被分配,就无法更改。但是,您也不需要更改它。解决此问题的最简单方法是创建一个空白隐藏的TForm作为真正的Application.MainForm,让它正常管理任务栏,然后在需要时显示/隐藏任何次要的TForm对象,其中您想要的“MainForm”是次要窗体而不是真正的MainForm

4
我用与@DavidHeffernan建议的方法相同的方式实现了这个功能,目前为止还没有遇到任何问题。可能这不是最好的方法,但对于我想要实现的东西来说已经足够好了,我的目标是在用户最小化其MainWork窗体时具有“正常”感觉的行为。

代码类似于这样:

procedure TfrmLogin.btnLoginClick(Sender: TObject);
begin
    frmMainWork := TfrmMain.Create(Application);
    try
        Pointer((@Application.MainForm)^) := frmMainWork;
        frmLogin.Hide;
        frmMainWork.ShowModal;
    finally
        Pointer((@Application.MainForm)^) := frmLogin;
        frmLogin.Show;
        FreeAndNil(frmMainWork);
    end;

end;

希望这可以帮到你 :-)

3

您可以更改主窗体。 创建一个变量F: ^TForm,然后将其设置为@Application.MainForm。之后,您可以通过F^ := YourAnotherForm将主窗体设置为另一个窗体。


2
优美而简洁的解决方案! - Zam

2
如果在启动程序(在 .dpr 文件中)中将 Application.MainFormOnTaskbar 设置为 false,则 VCL 将创建一个隐藏的窗体,其唯一目的是提供任务栏图标。这是一种较旧的方法,通常不建议使用,但只要其他窗口可见,它就可以让您隐藏主窗体而应用程序不会从任务栏消失。
您还可以提供一个 Application.OnGetMainFormHandle 事件处理程序,在运行时更改 Application.MainFormHandle(但不是 Application.MainForm)。MainFormHandle 影响诸如模态弹出对话框所有者之类的东西。
关于Application.MainFormOnTaskbar的更多信息以及禁用它的缺点:这很快就会变得复杂。简短的版本在VCL文档中,其中解释了Windows的几个新功能(例如Vista中引入的实时任务栏缩略图)需要MainFormOnTaskbar := True。在this SO讨论中还有更多背景阅读资料。

这种方法有什么不好的地方吗?(将Application.MainFormOnTaskbar更改为false) - Shaun Roselt
@DavidHeffernan - 我在XE4中测试时它可以工作。赋值必须在创建任何窗体之前在.dpr中完成。 - Josh Kelley
哈哈,我不确定怎么做。你是怎么做的? - Shaun Roselt
@JoshKelley 好的,你需要另一个顶级窗口显示。我的实验是从当天早些时候的问题中得出的,该问题希望在没有窗口显示时有任务栏按钮。你的答案是一个不错的解决方法。 - David Heffernan

0

我在使用同时也是COM服务器的Delphi XE2 MDI应用程序时遇到了一个额外的问题。我的主窗体Main调用一个包含整个应用程序共享图标和弹出菜单的二级窗体MenuForm。

在我的示例中,当独立运行应用程序时,它可以完美地工作。

Main被创建后,在FormCreate的末尾创建了一个MenuForm。一切正常。

但是,当从COM服务器调用时,Main.FormCreate首先被调用,但不知何故,MenuForm被分配给Application.MainForm。底层RTL中存在竞争条件。这在尝试创建第一个SDI子项时会造成混乱,因为Application.MainForm不是MDI。

我尝试通过Main.FormCreate向自身发送消息来解决这个问题,以延迟MenuForm的创建,直到Main.FormCreate返回后再进行创建。

但是这里仍然存在竞争条件-MenuForm仍然被分配给Application.MainForm。

最终,我能够使用代码轮询Application.MainForm每10毫秒一次,并最多持续10秒钟来解决这个问题。我还必须在明确创建MenuForm之前从Main中删除对MenuForm图标列表(在.dfm中)的任何引用-否则MenuForm将在MainForm.create的末尾隐式创建。

希望这能帮助到某些人!

const
  CM_INITIAL_EVENT = WM_APP + 400;


TmainForm = class(TForm)
  ...
  procedure afterCreate(var Message: TMessage); message CM_INITIAL_EVENT;
  ...
end;


procedure TmainForm.FormCreate(Sender : TObject);
begin
  ...
  ...standard init code
  ...

  postmessage( handle, CM_INITIAL_EVENT, 0, 0 );
End;


procedure TmainForm.AfterCreate(var Message: TMessage);
var
  i: Integer;
begin

  //must assign these AFTER menuform has been created
  if menuForm = nil then
  begin
    //wait for mainform to get assigned
    //wait up to 10*1000ms = 10 seconds
    for i := 0 to 1000 do
    begin
      if Application.Mainform = self then break;
      sleep(10);
    end;

    Application.CreateForm(TmenuForm, menuForm);
    menuForm.parent := self;
  end;

  //NOW we can assign the icons
  Linktothisfilterfile1.SubMenuImages := menuForm.treeIconList;
  ActionManager.Images := menuForm.treeIconList;
  allFilters.Images := menuForm.treeIconList;
  MainMenu.Images := menuForm.treeIconList;
  ...
end;

0

在当前的Delphi实现中,我确信使用指针更改Application.MainForm没有任何后果。

如果您查看TApplication类,您会发现FMainForm仅用于检查应用程序是否至少有一个窗体,并在TApplication.Run方法中迭代主循环,同时FMainForm存在。如果您不想使用指针来破解此属性,则可以实现自己的TApplication类,例如TMyApplication,将其中所有例程复制并定义MainForm属性以进行读写。


0
删除 *.dproj 文件。在 *.dpr 文件中删除以下行。这样你就可以摆脱所有的麻烦了 :) Application.MainFormOnTaskbar := True; 对不起,我的英语不好。

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