为什么 TForm.SetBounds 只有在设计时将 TForm.Position 设置为 poDefault 时才能正常工作

Why does TForm.SetBounds only work correctly when TForm.Position is set to poDefault at design time

我注意到一些非常奇怪的事情。我在窗体关闭时保留窗体的顶部、左侧、宽度和高度属性,并使用此信息在窗体再次打开时通过使用先前存储的信息调用 SetBounds 来恢复窗体的最后位置。这很有效,但前提是表单的 Position 属性 在设计时设置为 poDefault。如果设置为其他值,例如 poDesigned、poScreenCenter 或 poMainFormCenter,SetBounds 不会恢复窗体之前的位置和大小。

这是奇怪的部分。重要的是 Position 属性 在设计时的设置。我可以在运行时将此 属性 的值更改为 poDefault 并且对 SetBounds 的调用仍然无法正常工作。我试过类似下面的东西

if Self.Position <> poDefault then
  Self.Position := poDefault;

在表单的 OnCreate 事件处理程序中,以及来自重写的构造函数(并在构造函数中将 Position 设置为 poDefault,并在 OnCreate 事件处理程序中调用 SetBounds)。在所有情况下,在运行时将表单的 Position 属性 更改为 poDefault 并不能解决我在 SetBounds 中观察到的问题。我发现的唯一一致的模式是 SetBounds 仅当表单的 Position 属性 在设计时为 poDefault 时才能正常工作。

当窗体的 Position 属性 在设计时未设置为 poDefault 时,关于 SetBounds 如何工作,我还注意到了其他一些事情。例如,如果您调用 SetBounds,在设计时将 Position 属性 设置为 poScreenCenter 的窗体不一定会显示在屏幕中央。但是,它不会出现在 SetBounds 定义的左上角位置,也不会遵守调用 SetBounds 时指定的宽度和高度。但是,让我重复一遍,我在调用 SetBounds 之前将窗体的 Position 属性 设置为 poDefault。我什至在两次操作之间插入了对 Application.ProcessMessages 的调用,但这并不能解决问题。

我已经在 Delphi 10.1 Berlin 运行 上对 Windows 10 进行了广泛的测试。我还在 Windows 7 上使用 Delphi XE6 对其进行了测试. 结果相同。

如果您有疑问,请创建一个具有四个窗体的 VCL 应用程序。在第一个窗体上放置三个按钮,并为每个按钮添加类似于以下 OnClick 的内容:

 with TForm2.Create(nil) do
 try
   ShowModal;
 finally
   Release;
 end;

其中构造函数创建 TForm2,然后是 TForm3 和 TForm4。

在窗体 2 到 4 的 OnCreate 上,添加以下代码:

if Self.Position <> poDefault then
  Self.Position := poDefault;
Self.SetBounds(500,500,500,500);

在 form2 上,将 Position 设置为 poDefault,在 form3 上将 Position 设置为 poScreenCenter,在 form4 上将 Position 设置为默认值 poDefaultPosOnly。只有form2会出现在500、500,宽500,高500。

有人对这个结果有合理的解释吗?

poDefault 和朋友的意思是 "let Microsoft Windows position this form's window when the form would create and show it"。

您刚刚创建了 Delphi 对象 - 但我想知道它是否也有 created/shown Windows 对象(HWND 句柄和所有相应的 Windows 内部结构).特别是主题应用程序,而不是那些使用标准 XP 之前外观和感觉的应用程序 - 它们在显示时往往 ReCreateHWND,因为预加载那些花哨的 Windows 主题是相对昂贵的操作,只应在需要时进行.

我认为当您(或 TApplication - 这对主题没什么影响)最后做 FormXXX.Show.

在 "make me a window and display it" 序列期间,当您的表单查看其属性并告诉 MS Windows 类似 "now I want to create your internal HWND-object and position it at default coordinates/size at your discretion".

的内容时

这是绝对正确的行为 - 否则 TForm 何时以及如何应用 Position 属性???向 Windows 询问 window 的坐标是没有意义的,它在屏幕上还不存在,也许永远不会存在。 Windows 在被询问的这一秒提供默认值 coords/sizes,查看那里有多少其他 windows 以及它们的位置(并且 AMD/NVidia 视频驱动程序也可能应用它们对其进行更正)。

现在获取默认值并在两小时后应用它们是没有意义的,届时一切可能都会不同 - 其他 windows 的数量不同,位置不同,连接的显示器组不同,具有不同的分辨率等

只考虑 "desktop replacement" 类型的笔记本。它是在连接到大型固定外部监视器的 table 上设置的。然后 - 让我们想象一下 - 我 运行 你的应用程序,它创建了 tform Delphi 对象,在构造函数中它向 MS Windows 询问位置 - Windows 正确地提供了位置在那个非常次要的大显示器上。但一个小时后,我拔掉了笔记本的电源,然后带着它离开了。一个小时后,我告诉您的应用程序显示表单 - 它会做什么?用属于现在分离的外部显示器的坐标显示它?在我目前只有的笔记本内部显示器的视口之外?这个表格应该显示在现在 "invisible" 的位置只是因为当我启动应用程序时那个地方仍然在那里可见吗???我认为是无利可图地迷惑用户。

因此,唯一正确的行为是在表单从隐藏变为可见时立即询问 Windows 默认坐标,而不是提前一秒钟。

这意味着如果你想移动你的表单 - 你应该在它显示之后再做。将您的 Self.SetBounds(500,500,500,500); 放入 OnShow 事件处理程序。因此,让 MS Windows 将您的表单具体化到默认位置,如 poDefaultPosition 属性 中所要求的那样 - 然后移动您的 Window。尝试移动尚不存在的 window 对我来说是徒劳的。

要么预设你的表格(在构造顺序中)以明确忽略 MS Windows 默认值并使用预设线(通过 poDesigned 值),要么让表格询问 Windows坐标,但在 之后用 SetBounds 移动它,它通过 OnShow 处理程序可见。