I don't know if it's a bug... But when I set any other VCL style except for "Windows", the window width is reduced.
-
Is there any solution for this?
UPDATE I submitted this to QC: http://qc.embarcadero.com/wc/qcmain.aspx?d=103697 Hope they'll fix it...
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
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:
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.
For those looking for a really clever solution for this very strange behaviour, take a look on the James Johnston answer. I've applied it on my project and it is working flawlessly. Below is the Delphi translation from the James answer. Thank you James!
program Solve;
uses
Vcl.Forms,
Unit1 in 'Unit1.pas' {Form1},
Windows,
Messages,
Vcl.Themes,
Vcl.Styles;
type
TFixedFormStyleHook = class(TFormStyleHook)
protected
procedure WndProc(var AMessage: TMessage); override;
end;
{ TFixedFormStyleHook }
procedure TFixedFormStyleHook.WndProc(var AMessage: TMessage);
var
NewMessage: TMessage;
ncParams: NCCALCSIZE_PARAMS;
begin
if (AMessage.Msg = WM_NCCALCSIZE) and (AMessage.WParam = 0) then
begin
// 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...)
ZeroMemory(@ncParams,SizeOf(NCCALCSIZE_PARAMS));
ncParams.rgrc[0] := TRect(Pointer(AMessage.LParam)^);
NewMessage.Msg := WM_NCCALCSIZE;
NewMessage.WParam := 1;
NewMessage.LParam := Integer(@ncParams);
NewMessage.Result := 0;
inherited WndProc(NewMessage);
if Handled then
begin
TRect(Pointer(AMessage.LParam)^) := ncParams.rgrc[0];
AMessage.Result := 0;
end;
end
else
inherited;
end;
{$R *.res}
begin
// Register our style hook. An audit of Delphi 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(TForm,TFixedFormStyleHook);
TCustomStyleEngine.RegisterStyleHook(TCustomForm,TFixedFormStyleHook);
Application.Initialize;
Application.MainFormOnTaskbar := True;
TStyleManager.TrySetStyle('Glossy Light');
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
With this code, the ClientWidth / ClientHeight dimensions are respected and the inside contents are shown correctly. Of course the external size of Window will be bigger to accomodate the ClientWidth / ClientHeight dimensions, but this is not so bad because normally the window contents is more important.
You may want to put the code inside a separate unit to use it on any project. Here is only the direct raw solution.
It does indeed appear to be a VCL bug. The ClientWidth
property is not properly streamed from the .dfm file when the style is set in the project options to be other than the system style.
I suggest that you submit a report to QualityCentral. In the meantime you may be able to work around this by setting the style in the .dpr file after the forms have been created.
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainForm, MainForm);
TStyleManager.SetStyle('Amakrits');//after CreateForm, rather than before
Application.Run;
However, I don't imagine that will get you very far because you probably want to be able to create forms on the fly and not have to create the all upon startup.
This bug still exists in Delphi Rio 10.3.3. I thought I solved the problem by using Carlos Feitoza Filho's code. However it doesn't work when Windows Scaling is on (high-DPI monitor). Many users complained about it.
Here is my always working solution: Using FormResize event!
procedure TForm1.FormResize(Sender: TObject);
begin
ClientHeight := Button1.Top + Button1.Height + Button1.Top; // whatever you want
ClientWidth := Button1.Left + Button1.Width + Button1.Left; // whatever you want
end;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With