Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

VCL Styles - client size of form reduced

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.

Windows styleAny other style looks like this... -

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...

like image 433
djsoft Avatar asked Feb 25 '12 18:02

djsoft


5 Answers

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

enter image description here

the carbon style has a border width and height of 5 pixels

enter image description here

the Amakrits style has a border width and height of 6 pixels

enter image description here

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.

like image 176
RRUZ Avatar answered Nov 19 '22 13:11

RRUZ


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:

  1. Create a new VCL Forms Application in C++ Builder.
  2. Set the current / default VCL style to the Carbon VCL style from the Appearance section in the Project Options.
  3. Add a new TButton control to the form.
  4. 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))));
        }
    }
    
  5. 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.

like image 6
James Johnston Avatar answered Nov 19 '22 12:11

James Johnston


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.

like image 6
Carlos B. Feitoza Filho Avatar answered Nov 19 '22 11:11

Carlos B. Feitoza Filho


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.

like image 4
David Heffernan Avatar answered Nov 19 '22 13:11

David Heffernan


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;
like image 1
Xel Naga Avatar answered Nov 19 '22 13:11

Xel Naga