【问题标题】:VCL Styles - client size of form reducedVCL Styles - 客户端尺寸减小
【发布时间】:2012-02-25 18:33:58
【问题描述】:

不知道是不是bug...但是当我设置除“Windows”之外的任何其他VCL样式时,窗口宽度都会减小。

-

有什么解决办法吗?

更新 我将此提交给 QC:http://qc.embarcadero.com/wc/qcmain.aspx?d=103697 希望他们能解决它...

【问题讨论】:

  • 我无法用普通的应用程序重现这一点。您能否尝试从表单/应用程序中删除内容,直到行为消失。那么你可能会有一个指向罪魁祸首的指针。
  • @Cosmin 样式是在 XE2 中引入的。
  • @David Heffernan 为实现这一目标,将表单的 BorderStyle 设置为 bsSingle,使用单个按钮处理空表单。
  • @djsoft OK,我们可以重现了。
  • 伙计,这个问题我有好几年了。

标签: delphi delphi-xe2 vcl-styles


【解决方案1】:

好的 - 我做了更多调查,发现了这个错误的根本问题(跳到最后寻找解决方法)。大多数/所有散布在 Internet 上并在此消息之前讨论的其他解决方法似乎只是掩盖了错误的症状,而没有真正找到根本原因 - 并且这些其他解决方法可能有其他不希望的副作用或限制(正如他们的一些作者所指出的那样)。

根本问题是当wParam参数为FALSE时,TFormStyleHook.WMNCCalcSize消息没有提供对WM_NCCALCSIZE消息的ANY处理。功能基本不完整。因此调用默认窗口处理程序 - Windows 提供的默认处理程序 - 当然返回 Windows 默认样式的客户端矩形,而不是用户指定的 VCL 样式。要修复此错误,Embarcadero 必须在 wParamFALSE 时添加对 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——这次是TRUEwParam。 VCL Styles 正确处理它并将客户端大小设置回正确的值 - 因此ClientHeight 的计算结果正确。

请注意,Windows 的未来版本可能会出现更严重的问题:如果 Microsoft 决定更频繁地发送 WM_NCCALCSIZE 消息并将 wParam 设置为 FALSE,即使表单可见,VCL 也会出现严重问题。

通过手动将WM_NCCALCSIZE 发送到表单很容易证明该错误。重现步骤:

  1. 在 C++ Builder 中创建一个新的 VCL 表单应用程序
  2. Project OptionsAppearance部分将当前/默认VCL样式设置为Carbon VCL样式。
  3. 向表单添加一个新的TButton 控件。
  4. 将以下代码添加到按钮的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))));
        }
    }
    
  5. 运行项目并单击按钮。如果通过测试,则意味着 VCL 样式的 NC 宽度恰好与默认的 Windows NC 宽度一致。更改表单的边框样式或将 VCL 样式更改为其他样式,然后重试。

解决方法当然是找到一种方法来拦截WM_NCCALCSIZE 消息,其中wParamFALSE,然后将其转换为wParamTRUE 的消息。这实际上可以在全局基础上完成:我们可以从 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,它现在将被正确使用。

【讨论】:

  • 令人惊讶的是,您的回复甚至没有被参与讨论的其他用户甚至 Embarcadero 考虑过!即使在今天,在 Delphi 25(Delphi Tokyo)中,这个问题仍然存在,我刚刚使用它的修复程序解决了它。我最诚挚地祝贺您的高度详细和全面的答复!我只需要将 C++ 语言转换为 Pascal。我会把它作为回应,以便其他人可以利用它。谢谢!
  • 非常感谢。这确实解决了问题。对于其他人来说,代码的 Delphi 翻译在这里:stackoverflow.com/a/51642295/893241
  • 我不知道为什么我要为“Delphi 订阅”付费,因为他们已经 6 年没有修复一个相对简单的错误了!
  • 是的,这至少是可耻的,EMBT 团队从未解决过这个问题。现在他们有一个完整的路线图来做到这一点。让我们祈祷官方修复:)
  • IIRC 我在 Embarcadero 的 JIRA 跟踪器中写了这个错误时,我向 Embarcadero 报告了这个错误......并且基本上向他们提出了这个解决方案。他们还没修好? (这些天我没有使用 EMBT 产品......谢天谢地......)
【解决方案2】:

这不是 vcl 样式的错误, 这是 vcl 样式的工作原理,每个样式(皮肤)都有自己的边框宽度和高度,有时与原生 windows 边框不匹配尺寸。

查看下一张图片

碳素样式的边框宽度和高度为 5 像素

Amakrits 样式的边框宽度和高度为 6 像素

您可以使用VCL Styles Designer检查每个样式的边框样式大小

  • 对象->表格->图像->左边框->宽度
  • 对象 -> 表单-> 图像 -> RigthBorder -> 宽度
  • 对象->表格->图像->底部边框->高度

因此,根据上述属性,表单的样式挂钩会重新计算客户区域的边界。

【讨论】:

  • 确实是一个错误。 ClientWidth 以一个值流入,但控件以不同的值结束。边框也因操作系统版本而异,但 ClientWidth 始终与系统样式正确流式传输。
  • @DavidHeffernan,如果表单的存储值不匹配可以认为是一个错误。我的回答只是解释了为什么在应用 vcl 样式时会修改客户端高度和客户端宽度。
  • 这也解释了为什么更改 BorderStyle 会导致客户端大小发生变化,即使在旧的 pre-styles 应用程序中也是如此。但我认为这里的问题是 .dfm 文件中的客户端大小设置没有得到尊重。
  • 我不得不说我很惊讶这个答案仍然以其原始形式出现。您在评论中接受了 ClientWidth 错误流式传输的错误。
  • @DavidHeffernan,正如我在评论中所说,我的回答是关于为什么客户区被 vcl 样式修改。你的答案也是如此。
【解决方案3】:

对于那些为这种非常奇怪的行为寻找真正聪明的解决方案的人,请查看 James Johnston 的答案。我已将它应用到我的项目中,并且运行良好。以下是詹姆斯答案的德尔福翻译。谢谢詹姆斯!

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.

使用此代码,ClientWidth / ClientHeight 尺寸得到尊重,内部内容正确显示。 当然 Window 的外部尺寸会更大以适应 ClientWidth / ClientHeight 尺寸,但这并没有那么糟糕,因为通常窗口内容更重要。

您可能希望将代码放在一个单独的单元中,以便在任何项目中使用它。这里只是直接的原始解决方案。

【讨论】:

  • 很遗憾,此解决方案在使用高 DPI 显示器缩放时失败。
【解决方案4】:

这确实似乎是一个 VCL 错误。当项目选项中的样式设置为非系统样式时,ClientWidth 属性未正确从 .dfm 文件流式传输。

建议您向QualityCentral 提交报告。与此同时,您可以通过在创建表单后在 .dpr 文件中设置样式来解决此问题。

Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainForm, MainForm);
TStyleManager.SetStyle('Amakrits');//after CreateForm, rather than before
Application.Run;

但是,我认为这不会让您走得太远,因为您可能希望能够即时创建表单,而不必在启动时创建所有内容。

【讨论】:

  • 这对我不起作用。在我设置样式后,表格会伸展。我会报告给质检...
  • @RRUZ 如果表单加载时.dfm 文件中的ClientWidth 值没有正确分配,那么在我看来这是一个错误。
  • 很奇怪。你用的是和我一样的风格吗?可能跟风格有关。如果在项目选项中设置样式,我发现 ClientWidth 属性流式传输不正确,但如果在 .dpr 文件中设置,则流式传输正确。
【解决方案5】:

此错误在 Delphi Rio 10.3.3 中仍然存在。我以为我通过使用 Carlos Feitoza Filho 的代码解决了这个问题。但是,当 Windows Scaling 打开时(高 DPI 显示器),它不起作用。许多用户抱怨它。

这是我一直有效的解决方案:使用 FormResize 事件!

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;

【讨论】:

  • 很抱歉听到这个消息。不幸的是,我没有高 DPI 显示器来查看效果。但是,我想詹姆斯约翰斯顿提出的解决方案(并在我的回答方法中使用)对所有其他情况都有效:/
  • 是的,当显示缩放为 100% 时效果很好。
【解决方案6】:

这里没有 XE2,但这听起来很熟悉。尝试将 AutoScroll 设置为 True (奇怪的是,与 this answer 相反) 以存储客户端表单大小而不是边框​​大小。

【讨论】:

  • AutoScroll 默认为False 并且在该状态下会发生错误。事实上,将AutoScroll 设置为True 似乎可以让程序按预期运行!
  • @David 嗯,奇怪。这似乎在 XE2(或更早版本)中发生了变化。在 D7 中,AutoScroll 默认为 True
  • 在 2010 年默认为 False,我在 D7 和 D2010 之间没有任何版本
【解决方案7】:

这个问题在 Delphi XE8 中仍然存在。一个简单的解决方法是使用以下代码,它只是恢复设计时 ClientWidth/ClientHeight,但有一些注意事项(最重要的是,AutoScroll 必须设置为 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;

给定以下设计时间形式:

这更正了运行时:

收件人:

我的博客上有更多细节和图片:http://marc.durdin.net/2015/07/fixing-the-incorrect-client-size-for-delphi-vcl-forms-that-use-styles/

【讨论】:

  • 感谢您告知我们。我很高兴我没有为 XE8 更新(以及 XE7)付费。我已经检查了我关于这个问题的 QC 报告,它是在 2012 年发布的,但仍未修复......
  • 我收到错误:E2361 无法访问私有符号 TCustomForm.FClientWidth
【解决方案8】:

我在我的 C++Builder VCL 应用程序中通过将以下代码添加到表单的 FormResize 函数中解决了这个问题,无论选择的样式如何,该函数似乎都可以工作并监视该程序被最大化的情况。我认为这段代码应该继续如果 Embarcadero 最终能够修复该错误,则可以正常工作:

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;
   }
}

【讨论】:

    猜你喜欢
    • 2021-11-25
    • 2012-10-05
    • 2018-01-30
    • 2012-04-22
    • 2015-01-27
    • 2020-01-24
    • 2011-12-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多