ОК - я провел еще несколько расследований и обнаружил основную проблему этой ошибки (пропустите до конца для обходного пути). Большинство / все другие обходные пути, рассредоточенные в Интернете и обсуждавшиеся до этого сообщения, по-видимому, просто маскируют симптомы ошибки, в действительности не найдя основную причину - и эти другие обходные пути могут иметь другие нежелательные побочные эффекты или ограничения ( как отметили некоторые их авторы).
Основная проблема заключается в том, что сообщение TFormStyleHook.WMNCCalcSize
не обеспечивает ЛЮБОЙ обработки сообщений WM_NCCALCSIZE
, когда параметр wParam
равен FALSE
. Функция в основном неполный. И поэтому обработчик окна по умолчанию называется - обработчик по умолчанию, предоставляемый Windows - который, конечно, возвращает клиентский прямоугольник для стиля Windows по умолчанию, а не пользовательский стиль VCL . Чтобы исправить эту ошибку, Embarcadero должен добавить обработку WM_NCCALCSIZE
, когда wParam
равен FALSE
, чтобы информация о стиле VCL все еще возвращалась. Это было бы очень легко исправить для них, и теперь, когда я исследовал и нашел для них проблему, я надеюсь, что исправление может быть применено к следующему выпуску VCL.
Чтобы доказать, что это было причиной проблемы, я регистрировал все сообщения, отправленные в форму (путем переопределения WndProc
), и для каждого сообщения отмечал, является ли клиент прямоугольным, как предусмотрено Win32 GetClientRect
было правильно для стиля VCL. Я также отметил тип вызова функции WM_NCCALCSIZE
(значение wParam
). Наконец, я заметил новый клиентский прямоугольник, возвращаемый обработчиком WM_NCCALCSIZE
.
Я обнаружил, что во время работы приложения почти каждое сообщение WM_NCCALCSIZE
имело wParam
, установленное на TRUE
(что работает правильно), поэтому ошибка скрыта и не возникает. Вот почему Embarcadero до сих пор справился с этой ошибкой. Однако сообщение отправляется ОДИН РАЗ с wParam
, установленным на FALSE
, и это происходит в ключевой момент: непосредственно перед тем, как свойства ClientWidth
/ ClientHeight
будут установлены на значения из файла DFM
с помощью TCustomForm.ReadState
, А функция TControl.SetClientSize
работает, вычитая текущую ширину клиента (измеренную в Windows GetClientRect
) из текущей общей ширины окна, а затем добавляет новую ширину клиента. Другими словами, TControl.SetClientSize
требует, чтобы текущий прямоугольный клиент окна был точным, потому что он использует его для вычисления нового клиентского прямоугольника. А поскольку это не так, форма получает неправильный набор ширины, и отдых - это история.
О, вы удивляетесь, почему была затронута ширина, а не высота? Это было легко доказать - получается после установки ClientWidth
, но до установки ClientHeight
отправляется еще один WM_NCCALCSIZE
- на этот раз с wParam
из TRUE
. VCL Styles правильно обрабатывает его и возвращает клиенту правильное значение - поэтому вычисления для ClientHeight
, следовательно, оказываются правильными.
Обратите внимание, что будущие версии Windows могут сломаться хуже: если Microsoft решит более регулярно отправлять WM_NCCALCSIZE
сообщения с wParam
, установленным на FALSE
, даже когда форма видна, для VCL все будет очень плохо.
Ошибка легко доказать, вручную отправив WM_NCCALCSIZE
в форму. Шаги для воспроизведения:
- Создайте новое Приложение VCL Forms в C ++ Builder.
- Установите текущий / стандартный стиль VCL на Углеродный Стиль VCL из раздела Внешний вид в Параметры проекта .
- Добавить новый
TButton
элемент управления в форму.
Добавьте следующий код к событию 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))));
}
}
Запустите проект и нажмите кнопку. Если вы проходите тестирование, это означает, что ширина ЧПУ в стиле VCL совпадает с шириной ЧПУ в Windows по умолчанию. Измените стиль границы формы или измените стиль VCL на другой и повторите попытку.
Обходной путь, конечно, заключается в том, чтобы найти способ перехватить WM_NCCALCSIZE
сообщения, где wParam
равен FALSE
, а затем преобразовать его в сообщение, где wParam
равно TRUE
. На самом деле это можно сделать глобально: мы можем создать производный класс из TFormStyleHook
, который решит проблему, а затем использовать хук глобально - это решит проблему во всех формах, включая формы, созданные в VCL (например, из Vcl .Диалог блок). В примере проекта, показанном выше, измените основной 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 теперь правильно обрабатывается. Также вы увидите, что если вы явно задали ClientWidth
в файле DFM
, теперь он будет использоваться правильно.