VCL Styles - client size of form reduced
This is not a vcl styles bug, This is how the vcl styles works, each style(skin) has a own border width and height, which sometimes doesn't match with the native windows border size.
check the next images
the carbon style has a border width and height of 5 pixels
the Amakrits style has a border width and height of 6 pixels
You can check the border style size of each style using the VCL Styles Designer
- Objects -> Form- > Image -> LeftBorder -> Width
- Objects -> Form- > Image -> RigthBorder -> Width
- Objects -> Form- > Image -> BottomBorder -> Height
So, depending of the above properties the Style hook of the form recalculates the bounds of the Client area.
OK - I did some more investigating and found the root problem of this bug (skip to the end for the workaround). Most/all of the other workarounds scattered on the Internet and discussed prior to this message seem to just be masking the symptoms of the bug, without having really found the root cause - and those other workarounds could have other undesired side-effects or limitations (as some of their authors have noted).
The root problem is that the TFormStyleHook.WMNCCalcSize
message does not provide ANY handling of WM_NCCALCSIZE
messages when the wParam
parameter is FALSE
. The function is basically incomplete. And so the default window handler is called - the Windows-provided default handler - which of course returns a client rect for the Windows-default style, not the user-specified VCL style. To fix this bug Embarcadero must add handling of WM_NCCALCSIZE
when wParam
is FALSE
so that VCL style information is still returned. This would be a very easy fix for them to do, and now that I have investigated and found the problem for them, I hope the fix can be applied to the next release of the VCL.
To prove this was the cause of the problem, I logged all messages sent to the form (by overriding WndProc
) and for each message, noted whether the client rect as provided by Win32 GetClientRect
was correct for the VCL style. I also noted the type of WM_NCCALCSIZE
function call made (value of wParam
). Finally, I noted the new client rect returned by the WM_NCCALCSIZE
handler.
I found that while the application was running, almost every single WM_NCCALCSIZE
message had wParam
set to TRUE
(which does work correctly), so the bug is therefore hidden and does not occur. That is why Embarcadero has gotten away with this bug so far. However, the message is sent ONCE with wParam
set to FALSE
and this happens at a key moment: just before the ClientWidth
/ ClientHeight
properties are set to the values from the DFM
file by TCustomForm.ReadState
. And the TControl.SetClientSize
function operates by subtracting the current client width (as measured by Windows GetClientRect
) from the current overall window width, and then it adds the new client width. In other words, TControl.SetClientSize
requires that the current window client rect be accurate, because it uses it to calculate the new client rect. And since it is not, the form gets a wrong width set, and the rest is history.
Oh, you wonder why the width was affected and not the height? That was easy to prove - it turns out after the ClientWidth
is set but before the ClientHeight
is set, another WM_NCCALCSIZE
is sent - this time with wParam
of TRUE
. VCL Styles correctly handles it and sets the client size back to the proper value - and so the calculations for ClientHeight
therefore turn out correct.
Note that future versions of Windows might break more badly: if Microsoft decides to more regularly send WM_NCCALCSIZE
messages with wParam
set to FALSE
even while the form is visible, things will break very badly for VCL.
The bug is easy to prove by manually sending WM_NCCALCSIZE
to the form. Steps to reproduce:
- Create a new VCL Forms Application in C++ Builder.
- Set the current / default VCL style to the Carbon VCL style from the Appearance section in the Project Options.
- Add a new
TButton
control to the form. Add the following code to the button's
OnClick
event: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)))); } }
Run the project and click the button. If you get a passing test, then it means that the VCL style NC width happens to coincide with the default Windows NC width. Change the form's border style or change the VCL style to a different one, and try again.
The workaround, of course, is to find a way to intercept WM_NCCALCSIZE
messages where wParam
is FALSE
and then convert it to a message where wParam
is TRUE
. This can actually be done on a global basis: we can make a derived class from TFormStyleHook
that fixes the problem, and then use the hook globally - this will fix the problem on all forms, including VCL-created forms (e.g. from Vcl.Dialogs unit). In the sample project shown above, modify the main Project1.cpp
as follows:
//---------------------------------------------------------------------------
#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;
}
//---------------------------------------------------------------------------
Now run the project and click the button; you'll see that the WM_NCCALCSIZE is now correctly handled. Also you'll see that if you explicitly set a ClientWidth
in the DFM
file, it will now be correctly used.