13

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

djsoft
  • 1,051
  • 8
  • 19

8 Answers8

7

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.

RRUZ
  • 134,889
  • 20
  • 356
  • 483
  • 3
    Sure it's a bug. ClientWidth is streamed in with one value, but the control ends up with a different value. Borders differ from OS version to version too, but ClientWidth is always streamed correctly with the system style. – David Heffernan Feb 25 '12 at 21:43
  • Maybe it's not styles bug, but it's definitely a bug somewhere. Client width is calculated incorrectly. – djsoft Feb 25 '12 at 22:01
  • 2
    @DavidHeffernan, if the stored values of the form not match can be considered a bug. My answer just explains why the client height and client widht are modified when a vcl style is applied. – RRUZ Feb 25 '12 at 22:26
  • 1
    That also explains why changing `BorderStyle` results in client size changing, even in old pre-styles apps. But I believe the issue here is that the client size settings in the .dfm file are not honoured. – David Heffernan Feb 25 '12 at 22:48
  • 1
    I have to say I am surprised that this answer is still here in its original form. You accepted in a comment that it was a bug for ClientWidth to be mis-streamed. – David Heffernan Feb 26 '12 at 14:34
  • 1
    @DavidHeffernan, as says on my commnent my answer is about why the client area is modified by the vcl styles. So is a complement for your answer. – RRUZ Feb 26 '12 at 14:39
6

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.

James Johnston
  • 9,264
  • 9
  • 48
  • 76
  • 1
    It's amazing how your response was not even considered, nor by the other users who participated in this discussion and not even by the Embarcadero! Even today, in Delphi 25 (Delphi Tokyo) this problem persists and I have just solved it using its fix. My sincerest congratulations on your highly detailed and comprehensive response! I just needed to convert the C ++ language to Pascal. I'll put that as a response so that other people can take advantage of it. Thank you! – Carlos B. Feitoza Filho Aug 01 '18 at 20:39
  • 3
    Thanks a lot. This really fixes the problem. For anyone else looking, the Delphi translation for the code is here: https://stackoverflow.com/a/51642295/893241 – djsoft Aug 03 '18 at 14:41
  • 1
    And I don't know why I'm paying for the "Delphi subscription" as they failed to fix a relatively simple bug for 6 years already! – djsoft Aug 03 '18 at 14:42
  • Finally! Justice was done :) Thank you @djsoft for choose this answer as correct, because it IS! :) – Carlos B. Feitoza Filho Aug 03 '18 at 17:14
  • 2
    And yes, it is at least shameful, that this problem was never solved by EMBT team. Now they have a full roadmap to do it. Let's pray for a official fix :) – Carlos B. Feitoza Filho Aug 03 '18 at 17:19
  • 1
    IIRC I reported this bug to Embarcadero at the time I wrote this in their JIRA tracker.... and basically laid out this solution to them. They still haven't fixed it? (I'm not working with EMBT products these days... thankfully...) – James Johnston Aug 05 '18 at 23:25
  • @JamesJohnston No, the problem persists on the last version of Delphi (Delphi 25) and your solution was flawless. Sorry to hear that you do not work with Delphi anymore. Your knowledge and patience to solve this were impressive. Thank you – Carlos B. Feitoza Filho Aug 06 '18 at 16:35
6

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.

4

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • That didn't work for me. After I set the style the form stretches. I'll report it to QC... – djsoft Feb 25 '12 at 21:29
  • 1
    @RRUZ If the value of ClientWidth in the .dfm file doesn't get properly assigned when the form loads, then that's a bug in my view. – David Heffernan Feb 25 '12 at 21:57
  • Very weird. Are you using the same style as me? Perhaps it's related to the style. I find the ClientWidth property streamed incorrectly if the style is set in project options, but streamed correctly if set in .dpr file. – David Heffernan Feb 25 '12 at 22:23
1

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;
Xel Naga
  • 826
  • 11
  • 28
  • 1
    Sorry to hear about it. Unfortunately I do not own a High DPI Monitor to see the effect. I guess, however, the sollution proposed by James Johnston (and used on my answer approach) is effective on all the other cases :/ – Carlos B. Feitoza Filho Jun 16 '20 at 18:34
  • 1
    Yes, it works pretty good when display scaling is 100%. – Xel Naga Jun 17 '20 at 19:48
0

This problem is still present in Delphi XE8. A simple workaround is to use the following code, which just restores the design-time ClientWidth/ClientHeight, with a few caveats (most importantly, AutoScroll must be set to False):

type
  TFormHelper = class helper for Vcl.Forms.TCustomForm
  private
    procedure RestoreDesignClientSize;
  end;

procedure TfrmTestSize.FormCreate(Sender: TObject);
begin
  RestoreDesignClientSize;
end;

{ TFormHelper }

procedure TFormHelper.RestoreDesignClientSize;
begin
  if BorderStyle in [bsSingle, bsDialog] then
  begin
    if Self.FClientWidth > 0 then ClientWidth := Self.FClientWidth;
    if Self.FClientHeight > 0 then ClientHeight := Self.FClientHeight;
  end;
end;

Given the following design time form:

design time form

This corrects the runtime from:

enter image description here

To:

enter image description here

I have more detail and pictures on my blog: http://marc.durdin.net/2015/07/fixing-the-incorrect-client-size-for-delphi-vcl-forms-that-use-styles/

Marc Durdin
  • 1,675
  • 2
  • 20
  • 27
  • Thanks for letting us know. I'm glad I didn't pay for the XE8 update (as well as XE7). I've checked my QC report on this issue, it was posted in 2012 and still not fixed... – djsoft Aug 02 '15 at 17:15
  • I get error: E2361 Cannot access private symbol TCustomForm.FClientWidth – Xel Naga Jun 13 '20 at 08:31
0

I resolved this in my C++Builder VCL app by adding the following code to the form's FormResize function, which seems to work irrespective of the selected style & monitor the that program is being maximised in. I think this code should continue to work ok if Embarcadero finally get around to fixing the bug:

void __fastcall TForm1::FormResize(TObject *Sender)
{
 if(( WindowState == wsMaximized )  
  &&( UserPrefStyle != "Windows" ))
  {
   OnResize = NULL;
   int hAdj = Height - ClientHeight;
   hAdj -= ( Width - ClientWidth );
   ClientHeight = Monitor->WorkareaRect.Height() - hAdj;
   OnResize = FormResize;
   }
}
0

No XE2 here, but this sounds very familiar. Try setting AutoScroll to True (strangely, the opposite of this answer) to store the client form size rather then the border size.

Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200
  • `AutoScroll` default to `False` and the bug occurs in that state. In fact setting `AutoScroll` to `True` seems to make the program behave as expected! – David Heffernan Feb 25 '12 at 19:31
  • 1
    @David Hmm, strange. That seems to be changed then in XE2 (or earlier). Here in D7, `AutoScroll` defaults to `True`. – NGLN Feb 25 '12 at 19:52
  • Defaults to False in 2010, I don't have any versions in between D7 and D2010 – David Heffernan Feb 25 '12 at 19:54