wsMaximized窗体未最大化显示

36

将一个窗体的WindowState属性设置为wsMaximized有时会导致该窗体不能最大化,而出现下面的情况:

enter image description here

长期存在的问题:这是我在2003年首次在Borland新闻组中提出的问题:

然后在2006年再次提出:

然后在2008年又一次提出:

有人在2012年在Embarcadero论坛上提出了这个问题:

现在是将这个18年老问题移植到Stackoverflow的时候了,也许终于有人想出了解决方法。

复现步骤

我的帖子包含了半打失败模式,但最简单的是:

  • 在表单上放置一个Label和一个Edit

    enter image description here

  • TEdit添加一个OnEnter事件:

    procedure TForm1.Edit1Enter(Sender: TObject);
    begin
       Label1.Font.Style := Label1.Font.Style + [fsBold];
    end;
    
  • 并设置表单:

    • WindowStatewsMaximized
    • AutoScrollFalse

然后,嘣,失败了。

2008年帖子中的另一组步骤之一:

  1. 创建新应用程序和一个表单。
  2. 在设计时间将表单设置为最大化(WindowState = wsMaximized)。
  3. 在表单上放置 ListView 控件。
  4. 在 OnShow 过程中向列表视图添加 20 个空项:

    procedure TForm1.FormShow(Sender: TObject);
    var
         i: Integer;
    begin
         for i := 1 to 20 do
              ListView1.Items.Add;
    
    end;
    
  5. 在设计时将表单的AutoScroll属性设置为false(AutoScroll = False)

当然,我不是想要“在RadStudio版本n中修复了此问题,请使用该版本”。 我正在寻找实际的解决方法(如果有),这可能包括引用CodeGear最终修复它时VCL源代码的相关更改。 (如果有修复的话)。

注意:PositionpoDesigned更改为任何其他值都无法解决该问题。

解决方法

我曾经使用过的一个可怕、丑陋、可耻的解决方法是在OnShow期间启动一个计时器,然后当计时器触发时,将窗体最大化:

procedure TForm1.tmrVclMaximizeHackTimer(Sender: TObject);
begin
   Self.WindowState := wsMaximized;
end;

后来我改进了这个技巧,在 OnShow 期间发布一条消息;这本质上与定时器消息相同,而不必使用定时器:

const
  WM_MaximizeWindow = WM_APP + $03;

procedure TForm1.FormShow(Sender: TObject);
begin
  if (Self.WindowState = wsMaximized) then
  begin
     Self.WindowState := wsNormal;
     PostMessage(Self.Handle, WM_MaximizeWindow , 0, 0);
  end;
end;

private
   procedure WMMaximizeWindow(var Message: TMessage); message WM_MaximizeWindow;

procedure TForm1.WMMaximizeWindow(var Message: TMessage);
begin
   Self.WindowState := wsMaximized;
end;
有时候我会发明一个Delphi从未有过的OnAfterShow事件:
const
  WM_AfterShow = WM_APP + $02;

procedure TForm1.FormShow(Sender: TObject);
begin
  PostMessage(Self.Handle, WM_AfterShow, 0, 0);
  if (Self.WindowState = wsMaximized) then
  begin
     Self.WindowState := wsNormal;
     FMaximizeNeeded := True;
  end;
end;

private
   procedure WMAfterShow(var Message: TMessage); message WM_AfterShow;

procedure TForm1.WMAfterShow(var Message: TMessage);
begin
   if FMaximizeNeeded then
   begin    
      FMaximizeNeeded := False;
      Self.WindowState := wsMaximized;
   end;
end;

但是没有黑客比黑客更好。


需要注意版本差异吗?标记为D5和D7?在两者中都失败了吗?我没有任何一个,只有D6。如果我在那里调试,那可能就足够了。 - David Heffernan
嗯,在D6中我无法重现这个问题。我需要使用Windows 2000或者其他什么系统吗?;-) - David Heffernan
它在Windows 7上的Delphi 7中失败了(我昨天拍摄的截图)。不确定它是否在Delphi 8的Windows 8或Delphi 8.1的Windows 8.1上失败。 - Ian Boyd
不是对我而言。虽然我正在使用D6。请使用@myname,这样我会收到通知。 - David Heffernan
无法在 Windows 7 上使用 Delphi XE4 复现。 - gordy
我正在Windows 7上使用XE5获取它。 - Jaydee
5个回答

13

我能在D7/Win7上重现此问题。

我根本不使用 wsMaximized(与您描述的类似的随机问题)。

解决方法: 使用 OnActivate -> ShowWindow(Handle, SW_MAXIMIZE) ,例如:

procedure TForm1.FormActivate(Sender: TObject);
begin
  // Maximize only once when the Form is first activated
  if not FMaxsimized then
  begin
    FMaxsimized := True;
    ShowWindow(Handle, SW_MAXIMIZE);
  end;
end;

这种方法在 OnShow 期间将不会起作用。

更好的解决方案:OnShowOnCreate 中使用ShowWindowAsync,例如:

procedure TForm1.FormCreate(Sender: TObject);
begin
  ShowWindowAsync(Handle, SW_MAXIMIZE);
end;

这将设置窗口的显示状态,而无需等待操作完成。


你不会想在 OnActivate 期间最大化窗体,否则窗体将在激活时尝试最大化(即每次激活时都会尝试)。 - Ian Boyd
5
@Ian Boyd,使用ShowWindowAsync的解决方法相当优雅,不是吗?肯定比那个hack要好。 - Tim Ebenezer
1
@IanBoyd,实际上你可以使用一个布尔标志来指示表单在第一次最大化/激活时已经被使用过了。我会进行编辑。 - kobik
不是说这不是一个好的解决方法,但基本上ShowWindowAsync就是Ian答案中系统自己实现的PostMessage hack。使用这两种解决方法,如果你在正确的位置设置断点,你会发现窗口在最大化之前是正常显示的。 - Sertac Akyuz

10

我只测试了第一个复制的案例(使用D7,D2007,XE2),并且能够在D7和D2007中复制问题,但在XE2中不行。

问题在于标签更改字体后,请求其父级重新对齐。这最终导致在表单上调用TWinControl.AdjustSize中的SetWindowPos,其中包括恢复宽度/高度,即使表单已经最大化 - 这会导致奇怪的行为上最大化但视觉上未最大化的表单停留在屏幕上。


我追踪了D2007和XE2中的代码,并确定了其中的区别。 TWinControl.AlignControls中的代码在这两个版本之间是不同的。具体重要的是最后一条语句。

D2007:

procedure TWinControl.AlignControls(AControl: TControl; var Rect: TRect);

  ..
  { Apply any constraints }
  if Showing then AdjustSize;
end;

XE2:

procedure TWinControl.AlignControls(AControl: TControl; var Rect: TRect);

  ..
    // Apply any constraints
    if FAutoSize and Showing then
      DoAdjustSize;
end;

我希望这能帮助您想出/决定使用哪种解决方法。



我可以建议的解决方案(虽然我没有彻底测试过它)是尽早强制显示表单的最大化:

procedure TForm1.FormCreate(Sender: TObject);
var
  wplc: TWindowPlacement;
begin
  if not AutoScroll and (WindowState = wsMaximized) then begin
    wplc.length := SizeOf(wplc);
    GetWindowPlacement(Handle, @wplc);
    wplc.rcNormalPosition.Right := wplc.rcNormalPosition.Left + Width;
    wplc.rcNormalPosition.Bottom := wplc.rcNormalPosition.Top + Height;
    wplc.showCmd := SW_MAXIMIZE;
    SetWindowPlacement(Handle, @wplc);
  end;
end;

上面的方法有效是因为它在VCL设置窗体可见标志之前(OnEnter事件)强制将焦点设置到编辑控件,从而标签的对齐请求不会导致窗体大小调整。此外,由于VCL调用ShowWindow时,窗体窗口已经可见,因此不会在任何阶段导致窗体以还原状态显示。

但是,我不知道它是否有助于不同的重现场景。


最后,尽管我可以看到在更新的Delphi版本中行为已经得到了“纠正”,但我认为这不是VCL中的错误。在我看来,用户代码应负责在窗口显示状态正在更改时不要导致窗口调整。对于特定情况下的操作,我会推迟修改标签字体,直到VCL完成显示窗体的操作。


2
+1пјҒжҲ‘жӣҙе–ңж¬ўдҪ дҪҝз”ЁSetWindowPlacementзҡ„и§ЈеҶіж–№жЎҲпјҢеӣ дёәзӘ—дҪ“дјҡз«ӢеҚід»ҘжңҖеӨ§еҢ–зҠ¶жҖҒжҳҫзӨәпјҲдёҺжҲ‘зҡ„и§ЈеҶіж–№жЎҲдёҚеҗҢпјҢеңЁжңҖеӨ§еҢ–д№ӢеүҚзӘ—еҸЈдјҡд»ҘжӯЈеёёзҠ¶жҖҒжҳҫзӨәпјүгҖӮжӯӨеӨ–пјҢжӮЁзҡ„ж–№жі•ж— и®әwsMaximizedжҳҜеҗҰйў„и®ҫйғҪеҸҜд»Ҙе·ҘдҪңгҖӮ пјҲдёӘдәәиҖҢиЁҖпјҢжҲ‘дёҚдјҡдҪҝз”ЁwsMaximizedпјҢзңҒз•Ҙif not AutoScrollжЈҖжҹҘпјҢ并дҪҝз”ЁSetWindowPlacementиҝӣиЎҢжңҖеӨ§еҢ–пјүгҖӮеҸҰеӨ–пјҢеҲҶжһҗеҫ—еҫҲеҘҪпјҡпјүгҖӮ - kobik
@kobik - 谢谢!- "..省略 if not AutoScroll .." - 我只想尽可能地少干扰 VCL。 - Sertac Akyuz
我的重现步骤在XE6中不再能够重现。因此,看起来CodeGear(或者他们当时还是Imprise,或者已经改名为Embargadero)在XE2时期解决了这个问题。 - Ian Boyd

1
我认为这不是Delphi的Bug,而是Windows CreateWindow函数中的一个Bug(或者仅仅是奇怪的行为)。如果你搜索CreateWindow和WS_MAXIMIZE not working,你会发现类似的非常古老的线程和讨论,这些人在调用CreateWindow或CreateWindowEx时将WS_MAXIMIZE传递给样式参数,但在运行应用程序时没有看到最大化的窗口。 来自旧gamedev.net线程的摘录 问题在于当使用WS_OVERLAPPEDWINDOW时,WS_MAXIMIZE显然不起作用。如果你用WS_POPUP替换WS_OVERLAPPEDWINDOW,你将得到一个最大化的窗口。当然,这可能并不适用于所有版本的Windows,甚至也不一定适用于Windows shell UI的所有版本。
WS_OVERLAPPEDWINDOW是微软的旧默认窗口“类型”,他们显然编写了CreateWindow/Ex来忽略某些样式,认为ShowWindow将被调用以SW_SHOWDEFAULT,这将根据CreateProcess启动信息参数显示窗口。这最终使用户通过使用shell的快捷方式设置来控制应用程序的主窗口如何显示。

解决方法只是调用ShowWindow。在Delphi中也应该有效:

procedure TForm1.FormShow(Sender: TObject);
begin
   ShowWindow(Handle, SW_MAXIMIZE);
end;

我不这么认为。切换“自动滚动”来解决问题表明操作系统本身并没有问题。如果你追踪VCL代码,你会发现问题中的情况导致在一个最大化窗口上调用SetWindowsPos函数,并恢复宽度/高度,这很可能是问题的一部分。 - Sertac Akyuz
还要注意的是,窗口认为它已经最大化了;边框图标显示它处于最大化状态。但实际上并不是这样。我甚至可以在第三方应用程序中看到这个错误;这是一个明显的Delphi应用程序特征(还有不正确绘制的工具提示窗口)。 - Ian Boyd

0

哇!我在帖子中没有看到:

ShowWindowAsync(Handle,SW_MAXIMIZE);

谢谢你,有了它,我的问题得到了更好的解决,比使用计时器要好得多,但并不完美,它仍然会引起一些闪烁(窗口在不完整的渲染状态下显示,然后进入最大化状态),但比计时器好得多,在非最大化状态下显示的时间更短。

而且它与 OnShow 方法中的先前 SetBounds() 兼容。

我希望:在显示窗体之前设置窗体的初始大小(宽度、高度)和可能的初始位置(左、上),但该窗体必须以最大化方式显示;这些位置和大小是为用户取消最大化窗口时准备的。

那个 ShowWindowAsync 对于这样的目标完美地工作,无需添加一个丑陋的计时器(其代码中的间隔=1和 .Enabled=False 作为第一句话)。

当我阅读帖子时,怎么会错过它呢!

所以,从现在开始,我将使用它(例如相对于监视器的初始大小):

procedure TtheForm.FormShow(Sender: TObject);
var
   theInitialDefaultWidth,theInitialDefaultHeight:Integer;
begin
     theInitialDefaultWidth:=Round(Screen.Width*3/5);
     theInitialDefaultHeight:=Round(Screen.Height*3/5);
     WindowState:=wsNormal; // So it can still have at design time wsMaximized, this is for the SetBounds to work on a non maximized state
     SetBounds((Screen.Width-theInitialDefaultWidth)div 2,(Screen.Height-theInitialDefaultHeight)div 2,theInitialDefaultWidth,theInitialDefaultHeight); // Set default position and default size as i wish
     ShowWindowAsync(Handle,SW_MAXIMIZE); // Make the window to be shown maximized when it will be visible
     // ... // Rest of actions for the FormShow method
end;

非常完美!我不需要触碰设计时属性,可以让它们保持原样(WindowState=wsMaximized, Position=poScreenCenter等)... 百分百的代码解决方案,解决了我的问题。

非常感谢!

P.S.:这个程序在 Linux 上能运行吗?也就是说,如果将代码编译为 Linux 版本(在 Lazarus 中),我必须测试一下,看看它是否能正常运行。如果能运行,那将会对我目前使用的工具进行极大的改进。


0

希望我使用的解决方案能帮助其他人(我知道窗口首次显示时是设计时大小):

  • 添加一个定时器,间隔尽可能小,只需1(不要放0)。
  • 为其编写代码:theTimer.Enabled:=False;WindowState:=wsMaximized;

这对我来说从未失败过。

一旦窗体显示并完成了所有挂起的任务,定时器就会触发并将窗口最大化。

以前,我使用的是在最大化按钮所在位置发送鼠标单击的技巧,但我发现检查Windows操作系统版本、插件(在Linux上)等使得事情变得非常困难。那个方法完全像用户请求最大化窗口一样工作。

我现在使用的定时器完全相同,但避免了操作系统检查等问题。

更不用说:在设计时将WindowState设置为wsNormal(不要将其设置为wsMinimized或wsMaximized)。


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