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

Posted

技术标签:

【中文标题】Delphi 在应用程序运行时更改主窗体【英文标题】:Delphi Change main form while application is running 【发布时间】:2014-10-29 06:30:00 【问题描述】:

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

是否可以在应用程序已经运行时更改主窗体?

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

我使用的是 Delphi XE6,它是一个 VCL Forms 应用程序。

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

【问题讨论】:

您可以使用Pointer((@Application.MainForm)^) := Form2;更改主窗体。 @linluk:这是个坏建议。 MainForm 是一个属性,而不是一个变量。您无法获取属性的地址,因此您实际上获取的是属性返回的指针的地址。然后你修改那个指针指向一个不同的对象,你没有修改 TApplication 本身内部的指针。 这听起来真的很危险:S,它到目前为止工作,我知道这是一个肮脏的黑客,但我会为我未来使用它的应用程序寻找更好的解决方案(也许设置通过 rtti 或其他方式进行变量)或尽量避免更改主表单的需要:) rtti 解决方案可能看起来像TRttiContext.Create.GetType(TApplication).GetField('FMainForm').SetValue(Application,ANewMainForm);,但我不确定它是“更安全”还是“更好”。优点是我设置了变量的值而不是属性的值,但它仍然依赖于 tapplication 实现。 @Remy - 属性返回的指针地址是TApplication内的指针,因为MainForm直接读取FMainForm。该语句成功写入FMainForm 【参考方案1】:

是否可以在应用程序已经运行时更改主窗体?

无法在程序运行时更改 VCL 主窗体。该属性在程序启动时一劳永逸地确定。

一种可行的方法是安排次要表单,即不是主要表单的表单,在任务栏上有一个按钮。通过将其设为无主或使用WS_EX_APPWINDOW 扩展窗口样式来做到这一点。

更新

嗯,你可以更改Application.MainForm,但这需要你破坏当前的主窗体,然后创建一个新的。

【讨论】:

好的,谢谢。我已经考虑过了。为什么我隐藏主窗体时会隐藏任务栏图标?,因为我的应用程序仍在运行,只有窗体被隐藏。 隐藏的窗口不会显示在任务栏上,按设计:msdn.microsoft.com/en-gb/library/windows/desktop/cc144179.aspx 最小化窗口是为无法看到的窗口设置任务栏按钮的简单方法 @DavidHeffernan 我认为可以更改主窗体。我知道,因为我在我的一个应用程序中使用它。我喜欢这样做:Pointer((@Application.MainForm)^) := Form2;,它工作正常。我可以在不关闭应用程序的情况下关闭旧的主窗体,当我关闭新的主窗体时,应用程序会按预期终止。 @linluk 这显然是一个严重的黑客攻击。有什么后果? 我没有遇到任何问题(到目前为止)。所以我认为/希望没有后果。【参考方案2】:

如果您在启动例程中将Application.MainFormOnTaskbar 设置为false(在.dpr 文件中),则VCL 将创建一个隐藏表单,其唯一目的是提供任务栏图标。这是一种较旧的方法,通常不推荐使用,但只要其他窗口可见,它就可以让您隐藏主窗体,而应用程序不会从任务栏中消失。

您还可以提供Application.OnGetMainFormHandle 事件处理程序来在运行时更改您的Application.MainFormHandle(但不是 Application.MainForm)。 MainFormHandle 会影响模态弹出对话框的所有者等内容。

有关Application.MainFormOnTaskbar 的更多信息以及禁用它的缺点:这很快就会变得复杂。简短的版本在VCL docs 中,它解释了在Vista 中引入的几个Windows 的新功能(如实时任务栏缩略图)需要MainFormOnTaskbar := True。 this SO discussion 有更多背景阅读。

【讨论】:

这种方法有什么负面影响? (将 Application.MainFormOnTaskbar 更改为 false) @DavidHeffernan - 当我在 XE4 中测试它时它可以工作。在创建任何表单之前,必须在 .dpr 中完成分配。 哈哈不,我不知道该怎么做。你是怎么做到的? @JoshKelley 好的,您需要显示另一个***窗口。我的实验来自当天早些时候没有显示窗口时需要任务栏按钮的问题。您的回答是一个不错的解决方法。【参考方案3】:

正如 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 方法。 如果您希望能够切换回真正的主窗体调用主窗体的 Show 方法而不是 Close 方法。

这种方法让我们可以非常快速地交换表单,因此只有最热衷的用户才会注意到任务栏按钮的短动画。 注意:如果您的第二个 for 是一个复杂的,并且因此需要一些时间来创建,您可能希望隐藏它,然后在其创建过程完成后显示它并进行交换。否则,您最终可能会同时显示两个任务栏按钮,我相信您想避免这样做。

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

登录表单代码:

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.

【讨论】:

永远不要将窗口所有者设置为桌面。 blogs.msdn.com/b/oldnewthing/archive/2004/02/24/79212.aspx - “这是windows本身的限制。” - Windows不在乎你有没有主窗体。它甚至没有主窗体的概念。【参考方案4】:

Application.MainForm 分配后无法更改。但是,您也不需要这样做。解决这个问题最简单的方法是创建一个空白的隐藏TForm 作为real Application.MainForm 并让它正常管理任务栏,然后您可以显示/隐藏任何辅助@987654324 @ 对象在需要时,您想要的“MainForm”是次要形式,而不是真正的MainForm

【讨论】:

【参考方案5】:

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

【讨论】:

漂亮优雅的解决方案!【参考方案6】:

我在使用也是 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。

我最终能够使用代码每 10 毫秒轮询一次 Application.MainForm,最多 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;

【讨论】:

【参考方案7】:

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

如果您查看 TApplication 类,您会发现 FMainForm 仅用于检查应用程序是否具有至少一个表单,并在 FMainForm 存在时迭代 TApplication.Run 方法内的主循环。如果你不想用指针来破解这个属性,你可以实现自己的TApplication类,比如TMyApplication,复制里面所有的例程,定义MainForm属性读写

【讨论】:

【参考方案8】:

我以 @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;

希望这会有所帮助:-)

【讨论】:

【参考方案9】:

删除 *.dproj 文件。删除 *.dpr 文件中的以下行。你摆脱了所有的烦恼:) Application.MainFormOnTaskbar := True; 对不起我的英语不好。

【讨论】:

以上是关于Delphi 在应用程序运行时更改主窗体的主要内容,如果未能解决你的问题,请参考以下文章

Delphi 应用主窗体暂时弹到最前面

delphi 窗体的创建和释放

delphi 窗体的创建和释放

Delphi CloseHaltterminateExitProcess的区别

在delphi中,在运行时如何让拖动的控件显示在最上方?

delphi中,怎样设置新窗体打开时,就运行指定的SQL语句