好的 - 我做了更多调查,发现了这个错误的根本问题(跳到最后寻找解决方法)。大多数/所有散布在 Internet 上并在此消息之前讨论的其他解决方法似乎只是掩盖了错误的症状,而没有真正找到根本原因 - 并且这些其他解决方法可能有其他不希望的副作用或限制(正如他们的一些作者所指出的那样)。
根本问题是当wParam参数为FALSE时,TFormStyleHook.WMNCCalcSize消息没有提供对WM_NCCALCSIZE消息的ANY处理。功能基本不完整。因此调用默认窗口处理程序 - Windows 提供的默认处理程序 - 当然返回 Windows 默认样式的客户端矩形,而不是用户指定的 VCL 样式。要修复此错误,Embarcadero 必须在 wParam 为 FALSE 时添加对 WM_NCCALCSIZE 的处理,以便仍返回 VCL 样式信息。这对他们来说是一个非常容易修复的问题,现在我已经调查并发现了他们的问题,我希望这个修复可以应用于 VCL 的下一个版本。
为了证明这是问题的原因,我记录了发送到表单的所有消息(通过覆盖 WndProc),并为每条消息记录 Win32 GetClientRect 提供的客户端 rect 对于 VCL 是否正确风格。我还注意到了WM_NCCALCSIZE 函数调用的类型(wParam 的值)。最后,我注意到WM_NCCALCSIZE 处理程序返回的新客户端矩形。
我发现,当应用程序运行时,几乎每条WM_NCCALCSIZE 消息都将wParam 设置为TRUE(它可以正常工作),因此该错误被隐藏并且不会发生。这就是为什么 Embarcadero 到目前为止已经摆脱了这个错误。但是,消息在wParam 设置为FALSE 的情况下发送一次,这发生在关键时刻:就在ClientWidth / ClientHeight 属性被TCustomForm.ReadState 设置为DFM 文件中的值之前. TControl.SetClientSize 函数通过从当前整体窗口宽度中减去当前客户端宽度(由 Windows GetClientRect 测量)来运行,然后添加新的客户端宽度。 换句话说,TControl.SetClientSize 要求当前窗口客户端 rect 准确,因为它使用它来计算新的客户端 rect。 因为不是,所以表单设置了错误的宽度,并且剩下的就是历史了。
哦,您想知道为什么会影响宽度而不是高度吗?这很容易证明——结果是在设置了ClientWidth 之后但在设置了ClientHeight 之前,发送了另一个WM_NCCALCSIZE——这次是TRUE 的wParam。 VCL Styles 正确处理它并将客户端大小设置回正确的值 - 因此ClientHeight 的计算结果正确。
请注意,Windows 的未来版本可能会出现更严重的问题:如果 Microsoft 决定更频繁地发送 WM_NCCALCSIZE 消息并将 wParam 设置为 FALSE,即使表单可见,VCL 也会出现严重问题。
通过手动将WM_NCCALCSIZE 发送到表单很容易证明该错误。重现步骤:
- 在 C++ Builder 中创建一个新的 VCL 表单应用程序。
- 从Project Options的Appearance部分将当前/默认VCL样式设置为Carbon VCL样式。
- 向表单添加一个新的
TButton 控件。
-
将以下代码添加到按钮的OnClick 事件中:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
// Compute the current cumulative width of the form borders:
int CurrentNonClientWidth = Width - ClientWidth;
// Get the current rectangle for the form:
TRect rect;
::GetWindowRect(Handle, &rect);
// Ask the window to calculate client area from the window rect:
SendMessage(Handle, WM_NCCALCSIZE, FALSE, (LPARAM)&rect);
// Calculate the new non-client area given by WM_NCCALCSIZE. It *should*
// match the value of CurrentNonClientWidth.
int NewNonClientWidth = Width - rect.Width();
if (CurrentNonClientWidth == NewNonClientWidth) {
ShowMessage("Test pass: WM_NCCALCSIZE with wParam FALSE gave "
"the right result.");
} else {
ShowMessage(UnicodeString::Format(L"Test fail: WM_NCCALCSIZE with "
"wParam FALSE gave a different result.\r\n\r\nCurrent NC width: %d"
"\r\n\r\nNew NC width: %d", ARRAYOFCONST((
CurrentNonClientWidth, NewNonClientWidth))));
}
}
运行项目并单击按钮。如果通过测试,则意味着 VCL 样式的 NC 宽度恰好与默认的 Windows NC 宽度一致。更改表单的边框样式或将 VCL 样式更改为其他样式,然后重试。
解决方法当然是找到一种方法来拦截WM_NCCALCSIZE 消息,其中wParam 为FALSE,然后将其转换为wParam 为TRUE 的消息。这实际上可以在全局基础上完成:我们可以从 TFormStyleHook 创建一个派生类来修复问题,然后全局使用钩子 - 这将修复所有表单的问题,包括 VCL 创建的表单(例如来自 Vcl .Dialogs 单元)。在上图示例工程中,修改主Project1.cpp如下:
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include <tchar.h>
#include <string.h>
#include <Vcl.Forms.hpp>
//---------------------------------------------------------------------------
#include <Vcl.Styles.hpp>
#include <Vcl.Themes.hpp>
USEFORM("Unit1.cpp", Form1);
//---------------------------------------------------------------------------
class TFixedFormStyleHook : public TFormStyleHook
{
public:
__fastcall virtual TFixedFormStyleHook(TWinControl* AControl)
: TFormStyleHook(AControl) {}
protected:
virtual void __fastcall WndProc(TMessage &Message)
{
if (Message.Msg == WM_NCCALCSIZE && !Message.WParam) {
// Convert message to format with WPARAM == TRUE due to VCL styles
// failure to handle it when WPARAM == FALSE. Note that currently,
// TFormStyleHook only ever makes use of rgrc[0] and the rest of the
// structure is ignored. (Which is a good thing, because that's all
// the information we have...)
NCCALCSIZE_PARAMS ncParams;
memset(&ncParams, 0, sizeof(ncParams));
ncParams.rgrc[0] = *reinterpret_cast<RECT*>(Message.LParam);
TMessage newMsg;
newMsg.Msg = WM_NCCALCSIZE;
newMsg.WParam = TRUE;
newMsg.LParam = reinterpret_cast<LPARAM>(&ncParams);
newMsg.Result = 0;
this->TFormStyleHook::WndProc(newMsg);
if (this->Handled) {
*reinterpret_cast<RECT*>(Message.LParam) = ncParams.rgrc[0];
Message.Result = 0;
}
} else {
this->TFormStyleHook::WndProc(Message);
}
}
};
//---------------------------------------------------------------------------
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
// Register our style hook. An audit of C++ Builder XE8 VCL source code
// for registration of the existing TFormStyleHook shows that these are
// the only two classes we need to register for.
TCustomStyleEngine::RegisterStyleHook(__classid(TForm),
__classid(TFixedFormStyleHook));
TCustomStyleEngine::RegisterStyleHook(__classid(TCustomForm),
__classid(TFixedFormStyleHook));
Application->Initialize();
Application->MainFormOnTaskBar = true;
TStyleManager::TrySetStyle("Carbon");
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
return 0;
}
//---------------------------------------------------------------------------
现在运行项目并点击按钮;您会看到 WM_NCCALCSIZE 现在已正确处理。您还会看到,如果您在DFM 文件中明确设置ClientWidth,它现在将被正确使用。