如何在Delphi中不使用主窗体?

5

我一直在尝试让我的应用程序中的一些非模态窗体出现在任务栏上 - 利用Windows 7中新的有用任务栏。

在VCL中有许多问题需要解决,才能使窗体存在于任务栏上。

但最终的问题是,将VCL指定为主窗体的窗体最小化会导致应用程序中的所有窗口消失。

十年前,Peter Below(TeamB)记录了这些问题,并尝试解决它们。但有一些问题无法解决。这些问题深深地植根于VCL本身,因此几乎不可能使Delphi应用程序正常运行。

这一切源于工具栏上看到的按钮并不代表应用程序窗口,而是代表隐藏且从未见过的TApplications窗口。然后还有应用程序的MainForm,它被赋予了特殊的能力,如果被最小化,则指示应用程序隐藏自身。

我觉得如果我能做到

Application.MainForm := nil;

然后所有这些错误都会消失。应用程序可以拥有它的隐藏窗口,与此同时,我将覆盖应用程序中的每个其他表单,包括我的主表单,使用以下内容:
procedure TForm2.CreateParams(var params: TCreateParams ); 
begin 
   inherited CreateParams(params); 
   params.ExStyle := params.ExStyle or WS_EX_APPWINDOW; 
end; 

但在Delphi中,Application.MainForm属性是只读的。

我如何在Delphi中没有MainForm

另请参阅


如果只有只读属性是阻止您实现目标的唯一问题,您也可以尝试修改Forms.pas以使其成为可读写属性。 - Ondrej Kelle
6个回答

10

如果没有指定 MainForm,你将无法运行 GUI 项目。在没有 MainForm 的情况下,主消息循环将立即退出。但这并不意味着 MainForm 必须运行你的 UI。你可以使用一个空白的隐藏 TForm 作为指定的 MainForm,然后让它实例化你的真正 MainForm 作为第二个 TForm。例如:

HiddenMainFormApp.dpr:

project HiddenMainFormApp;

uses
  ..., Forms, HiddenMainForm;

begin
  Application.Initialize;
  Application.CreateForm(THiddenMainForm, MainForm);
  Application.ShowMainForm := False;
  Application.Run;
end.

HiddenMainForm.cpp:

uses
  ..., RealMainForm;

procedure THiddenMainForm.FormCreate(Sender: TObject);
begin
  RealMainForm := TRealMainForm.Create(Self);
  RealMainForm.Show;
end;

RealMainForm.cpp:

procedure TRealMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
  Application.Terminate;
end;

或者:

HiddenMainFormApp.dpr:

project HiddenMainFormApp;

uses
  ..., Forms, HiddenMainForm, RealMainForm;

begin
  Application.Initialize;
  Application.CreateForm(THiddenMainForm, MainForm);
  Application.ShowMainForm := False;

  RealMainForm := TRealMainForm.Create(Application);
  RealMainForm.Show;
  RealMainForm.Update;

  Application.Run;
end.

RealMainForm.cpp:

procedure TRealMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
  Application.Terminate;
end;

何时以及如何触发创建真正的 MainForm?您将谁设置为窗体的所有者?ApplicationBlankHiddenFormnil?您如何显示新的 MainForm,以及何时显示?您何时调用 BlankHiddenForm.Hide?我无法在 OnShow 期间隐藏我的虚拟窗体(这是不允许的)。我如何将焦点放在我的次要 MainForm 上? - Ian Boyd
随时创建它。将任何所有者分配给它。以任何你想要的方式展示它。这些都不重要,因为你的代码正在管理该表单,所以你可以按照自己的舒适程度进行操作。最简单的方法是自动创建它(这将设置应用程序作为所有者),然后在准备好时显示它。至于隐藏分配的主窗体,只需在调用TApplication.Run()之前将TApplication.ShowMainForm属性设置为False即可,不要尝试手动Hide()它。 - Remy Lebeau
一个人在这里回答了这个问题 https://dev59.com/JFfUa4cB1Zd3GeqPMOEc。这实际上非常棘手,一点也不明显。我应该在 OnCreate 期间创建我的表单吗?OnShowOnActivate?我应该启动0ms计时器吗?我应该在 OnCreate 期间启动它吗?OnShowOnActivate?真正的表单应该被创建为 Application.CreateForm?**.Create(Application).Create(Self).Create(nil)**?我要调用 .Show 吗?什么时候释放它?我要调用 .ShowModal; .Free 吗?这些都不是新奇的问题。 - Ian Boyd
最重要的是,现在我的主窗体是不可见的:用户如何关闭应用程序? - Ian Boyd
叹气 显然我得为你写一些代码。我已经编辑了我的回复,包括代码示例。 - Remy Lebeau

5
你无法在Delphi 5中实现这一点,特别是。关于TApplication窗口是任务栏上可见的窗口的引用已经不再适用于几个Delphi版本了(我相信D2007进行了更改)。因为你正在使用过时的Delphi副本(D5非常老),所以你写的大部分内容当前版本都已经没有了。我建议你升级到更高版本的Delphi(如果你需要避免Unicode,则选择Delphi 2007;如果可以使用或不介意VCL和RTL中的Unicode支持,则选择Delphi XE)。顺便说一下,你描述的事情并不是“错误”。它们是在设计Delphi 1时故意做出的设计决策,并且在可用的Windows版本中通过Delphi 7正常工作。后来的Windows版本(XP/Vista/Win7和相应的Server版本)对该架构进行了更改,随着Delphi与Windows一起发展,这些更改也被实现了。因为你选择不升级Delphi版本而保持其更新,所以你写的东西并不会神奇地变成错误。 :-)

我很想每一两年花几千美元,但那是不可能的。我们大部分已经转而使用免费的Visual Studio了。我希望Borland能发布一个免费版本的Delphi。但接受的答案是“你不能”,因为那是一个合理(且我认为是正确的)的答案。 - Ian Boyd
1
Ian:如果你在D5时期升级了,每年的费用不会超过几千美元;当时Pro版本的升级费用大约为300美元。目前的Pro版本已经涨到了约400美元,但是软件保障(价格大约相同)可以让您每次免费升级12个月。如果Embarcadero有来自其他产品线的数十亿美元的收入来支持它,那么一个免费版本将是很棒的。但由于他们不是微软,所以没有这样的条件。 - Ken White
1
将代码转换和测试所需的非零成本远远小于将整个开发从一种语言/IDE(Delphi)切换到全新的语言/IDE(VS/任何你选择的语言)所需的非零成本。 :-) - Ken White
既然你在谈论D5,那么D5发布时的VS版本是什么?可能是5或6。由于VS.NET改变了所有的事情,这意味着在MS领域进行等效的转换将会更加昂贵,因为它将(或者几乎)需要完全重写应用程序。从这个角度来看,Delphi版本之间的转换在成本方面将会轻松得多。 - Fabricio Araujo
必须同意Ken和Fabricia的观点。我拿了一个有大约800,000行代码的Delphi 7项目,使用了来自Report Builder、Woll2Woll和其他第三方组件,并将其迁移到了Delphi 2007。这花费了我一天时间,我不得不修改一些uses子句和实际上少于40行的代码。几乎没有我所担心的成本和努力。 :) - GolezTrol
显示剩余4条评论

4

在最小化主窗体时,在任务栏上显示另一个非模态窗体,似乎不会因为已分配Application.MainForm而出现问题。

Project1.dpr:

program Project1;

uses
  Forms,
  Windows,
  Unit1 in 'Unit1.pas' {MainForm},
  Unit2 in 'Unit2.pas' {Form2};

{$R *.res}

var
  MainForm: TMainForm;

begin
  Application.Initialize;
  Application.CreateForm(TMainForm, MainForm);
  ShowWindow(Application.Handle, SW_HIDE);
  Application.Run;
end.

Unit1.pas:

unit Unit1;

interface

uses
  Windows, Messages, Classes, Controls, Forms, StdCtrls, Unit2;

type
  TMainForm = class(TForm)
    ShowForm2Button: TButton;
    ShowForm2ModalButton: TButton;
    procedure ShowForm2ButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure ShowForm2ModalButtonClick(Sender: TObject);
  private
    FForm2: TForm2;
    procedure ApplicationActivate(Sender: TObject);
    procedure Form2Close(Sender: TObject; var Action: TCloseAction);
    procedure WMSysCommand(var Msg: TWMSysCommand); message WM_SYSCOMMAND;
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  end;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Visible := True; //Required only for MainForm, can be set designtime
  Application.OnActivate := ApplicationActivate;
end;

procedure TMainForm.ApplicationActivate(Sender: TObject);
{ Necessary in case of any modal windows dialog or modal Form active }
var
  TopWindow: HWND;
  I: Integer;
begin
  TopWindow := 0;
  for I := 0 to Screen.FormCount - 1 do
  begin
    Screen.Forms[I].BringToFront;
    if fsModal in Screen.Forms[I].FormState then
      TopWindow := Screen.Forms[I].Handle;
  end;
  Application.RestoreTopMosts;
  if TopWindow = 0 then
    Application.BringToFront
  else
    SetForegroundWindow(TopWindow);
end;

procedure TMainForm.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params do
  begin
    ExStyle := ExStyle or WS_EX_APPWINDOW;
    WndParent := GetDesktopWindow;
  end;
end;

procedure TMainForm.WMSysCommand(var Msg: TWMSysCommand);
begin
  if Msg.CmdType = SC_MINIMIZE then
    ShowWindow(Handle, SW_MINIMIZE)
  else
    inherited;
end;

{ Testing code from here }

procedure TMainForm.ShowForm2ButtonClick(Sender: TObject);
begin
  if FForm2 = nil then
  begin
    FForm2 := TForm2.Create(Application); //Or: AOwner = nil, or Self
    FForm2.OnClose := Form2Close;
  end;
  ShowWindow(FForm2.Handle, SW_RESTORE);
  FForm2.BringToFront;
end;

procedure TMainForm.Form2Close(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
  FForm2 := nil;
end;

procedure TMainForm.ShowForm2ModalButtonClick(Sender: TObject);
begin
  with TForm2.Create(nil) do
    try
      ShowModal;
    finally
      Free;
    end;
end;

end.

Unit2.pas:

unit Unit2;

interface

uses
  Windows, Messages, Classes, Controls, Forms;

type
  TForm2 = class(TForm)
  private
    procedure WMSysCommand(var Msg: TWMSysCommand); message WM_SYSCOMMAND;
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  end;

implementation

{$R *.dfm}

procedure TForm2.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params do
  begin
    ExStyle := ExStyle or WS_EX_APPWINDOW;
    WndParent := GetDesktopWindow;
  end;
end;

procedure TForm2.WMSysCommand(var Msg: TWMSysCommand);
begin
  if Msg.CmdType = SC_MINIMIZE then
    ShowWindow(Handle, SW_MINIMIZE)
  else
    inherited;
end;

end.

(在XP和Win7上使用D5和D7进行测试。)

(是的,你可以将其标记为不是答案,因为它不是:仍然有一个MainForm。但我认为这回答了问题背后的问题...)


2

我不能代表Delphi 5,但在Delphi 7中,如果你愿意动手,你绝对可以不使用主窗体运行。我在另一个答案这里详细介绍了很多细节。

由于Delphi 5没有MainFormOnTaskbar属性,你需要在你的dpr文件中进行以下操作:

// Hide application's taskbar entry
WasVisible := IsWindowVisible(Application.Handle);
if WasVisible then
  ShowWindow(Application.Handle, SW_HIDE);
SetWindowLong(Application.Handle, GWL_EXSTYLE,
  GetWindowLong(Application.Handle, GWL_EXSTYLE) or WS_EX_TOOLWINDOW);
if WasVisible then
  ShowWindow(Application.Handle, SW_SHOW);
// Hide the hidden app window window from the Task Manager's
// "Applications" tab.  Don't change Application.Title since
// it might get read elsewhere.
SetWindowText(Application.Handle, '');

这将隐藏应用程序窗口,只要您覆盖窗体的CreateParams以设置Params.WndParent := 0,每个窗口都会有自己的任务栏条目。Application.MainForm未分配,因此诸如最小化覆盖等问题并不是问题,但您必须谨慎处理任何假定MainForm有效的代码。


0

实际上,你所抱怨的大部分问题实际上是Windows设计的问题,而不是VCL。请参阅Windows Features以获取所有详细信息。

问题的关键在于owner属性,我指的是窗口所有者而不是VCL所有者。

当其所有者最小化时,拥有窗口将被隐藏。

如果您希望能够最小化主窗体而不会隐藏其他窗口,则需要了解拥有窗口的工作原理。


0
你可以把非模态窗体放在一个dll中,然后它们基本上会自己运作。(如果在创建它们时没有使用dll的Application实例(Application.CreateForm),那么dll中的Application.Mainform将为nil)。
当然,这取决于窗体可能需要做什么,这可能不可行。

将类从dll中公开会带来可怕的风险 - 您无法添加或删除任何私有成员或方法。 - Ian Boyd
我不太明白您所说的添加私有成员是什么意思,但这种设计当然会带来一些限制。正如我在上一段中所说,这可能并不可行。顺便说一下,我有一些旧项目(D3),可执行文件中只有一行代码:ShowFormInDll;。然后,对于该项目,“Application.MainForm”实际上为nil。 - Sertac Akyuz
这是在 DLL 中拥有类的风险。任何调用从 DLL 中公开的对象上的方法的人都必须知道 DLL 中的表单声明。这样,当它调用一个方法时,它就会调用正确的虚方法表 (VMT) 中的偏移量。如果您的表单添加了一个私有成员 (例如一个整数,它有四个字节),那么调用该表单的任何人现在都将崩溃,因为他们将在观察他们认为是 VMT 时缺少四个字节。应用程序和 dll 必须始终一起编译。如果不这样做,您将遇到崩溃。这些问题正是 COM 被发明的原因。 - Ian Boyd
@Ian - 好的,那就把你的表单放在一个COM DLL里吧。<g> 不过说真的,我从来没有想过直接在应用程序中操作DLL对象,感谢您的解释。 - Sertac Akyuz
呃,现在每次想要发布一个新版本都必须注册COM类吗?Delphi的单一可执行文件是一个非常重要的功能 - 不幸的是微软没有这个功能。 - Ian Boyd

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