В продолжение развитии темы базовой формы и фреймы. Хорошая новость: я решил добавить поддержку Delphi 7, которая оказалась довольно живучей. Ещё я сделал разделение пакета на два: DesignTime only (пакет с визардами) и RunTime only (пакет с модулем BaseForms). Плюс я хочу поделиться решением одной насущной проблемы…
Многие Delphi-программисты знают, что в VCL происходит некорректное масштабирование форм. Само масштабирование применяется в случае, когда текущее логическое разрешение экрана (значение Screen.PixelsPerInch) не совпадает с тем, при котором разрабатывалась форма в дизайнере (значение PixelsPerInch, сохранённое в DFM-файле). Некорректность заключается в том, что в некоторых случаях масштабирование не применяется к размеру самой формы, но применяется ко всем дочерним контролам. Также не масштабируются констрейнты формы, что приводит к ещё более некрасивым результатам – сначала форма масштабируется, а потом её размер ограничивается старыми констрейнтами.
А ещё VCL не масштабирует фреймы. Т.е. если фрейму создать в Run-Time вручную, а после этого встроить в форму, то фрейма останется неотмасштабированной.
Для наглядности приведу картинки.
Так форма выглядит без масштабирования (ppi = 96) в классической теме оформления (слева) и Aero (справа):
А так уже с масштабированием (ppi = 120):
Тут видно, что внешний размер формы не изменился, хотя сами контролы отмасштабировались. А ещё на картинке справа уменьшился размер клиентской области.
Теперь я укажу у формы свойство BorderStyle = bsSingle, это неявным образом заставит VCL сохранить свойства ClientWidth и ClientHeight в DFM-файл формы. Вот что получилось:
Похоже, что в этом случае масштабирование прошло корректно. А теперь у формы указываю констрейнты, получаем такую картинку:
Всё отмасштабировалось, но потом размер формы сбросился до констрейнтов, которые не отмасштабировались.
Ну и на последок я вернул форме стиль границы и сбросил якоря для всех контролов в значения по умолчанию ([akLeft, akTop]), получилось так:
На картинках показано приложение, собранное в Delphi 7. В Delphi 2010 проблемы масштабирования проявляются реже, но всё же проявляются. Например, достаточно выставить свойство AutoScroll = True, и получим поведение масштабирования как на второй картинке.
Есть ещё одна проблема: VCL не масштабирует фреймы, созданные в Run-Time. Вот простой пример:
procedure TForm1.Button1Click(Sender: TObject); var Form: TForm; Frame: TFrame2; begin Form := TForm.Create(Self); Frame := TFrame2.Create(Form); Form.ClientHeight := Frame.Height; Form.ClientWidth := Frame.Width; Frame.Align := alClient; Frame.Parent := Form; Form.Show; end;
По клику на кнопке получим форму с неотмасштабированной фреймой. Это воспроизводится и в Delphi XE2 (в более поздних пока не проверял).
Фиксим масштабирование форм
Я решил эту проблему в несколько этапов.
- Необходимо во время дизайна формы принудительно сохранять значения ClientWidth и ClientHeight.
- Необходимо отключить масштабирование на уровне VCL.
- Необходимо самостоятельно выполнить масштабирование.
Первое решается довольно просто:
.. private procedure WriteClientHeight(Writer: TWriter); procedure WriteClientWidth(Writer: TWriter); protected procedure DefineProperties(Filer: TFiler); override; .. procedure TBaseForm.WriteClientHeight(Writer: TWriter); begin Writer.WriteInteger(ClientHeight); end; procedure TBaseForm.WriteClientWidth(Writer: TWriter); begin Writer.WriteInteger(ClientWidth); end; procedure TBaseForm.DefineProperties(Filer: TFiler); function NeedWriteClientSize: Boolean; begin //Result := Scaled and not IsClientSizeStored // IsClientSizeStored = not IsFormSizeStored // IsFormSizeStored = AutoScroll or (HorzScrollBar.Range <> 0) or (VertScrollBar.Range <> 0) Result := Scaled and (AutoScroll or (HorzScrollBar.Range <> 0) or (VertScrollBar.Range <> 0)); end; begin inherited DefineProperties(Filer); // ClientHeight и ClientWidth сохраняются не всегда, а вместо этого сохраняются внешние размеры формы. // Это не совсем правильно, т.к. масштабировать необходимо именно клиентскую область. Функция NeedWriteClientSize // определяет, нужно ли принудительно сохранять размер клиентской области. Filer.DefineProperty('ClientHeight', nil, WriteClientHeight, NeedWriteClientSize); Filer.DefineProperty('ClientWidth', nil, WriteClientWidth, NeedWriteClientSize); end;
Второе решается сложнее: масштабирование VCL делает в методе TCustomForm.ReadState сразу после считывания DFM:
... inherited ReadState(Reader); if (FPixelsPerInch <> 0) and (FTextHeight > 0) then begin // проверка, изменилось ли значение PixelsPerInch и // само масштабирование end; ...
В общем я решил сбросить значение приватного поля FTextHeight в 0. Чтобы это можно было сделать, необходимо вклиниться в код между строками 2 и 3. Для этого я объявил фэйковое свойство под названием ScaleFix, которое считывается самым последним у формы, выглядит это примерно так:
... private FPixelsPerInch: Integer; procedure WriteScaleFix(Writer: TWriter); procedure ReadScaleFix(Reader: TReader); ... procedure TBaseForm.WriteScaleFix(Writer: TWriter); begin // просто сохраняем флаг в DFM-файл, чтобы при его чтении можно было вклиниться в процесс метода ReadState Writer.WriteBoolean(True); end; procedure TBaseForm.ReadScaleFix(Reader: TReader); begin if not Reader.ReadBoolean then Exit; // запоминаем прочитанное ранее свойство PixelsPerInch FPixelsPerInch := THackCustomForm(Self).FPixelsPerInch; // и устанавливаем текущее THackCustomForm(Self).FPixelsPerInch := Screen.PixelsPerInch; // сбрасываем свойство FTextHeight для отключения масштабирования на уровне VCL THackCustomForm(Self).FTextHeight := 0; end; procedure TBaseForm.DefineProperties(Filer: TFiler); ... Filer.DefineProperty('ScaleFix', ReadScaleFix, WriteScaleFix, Scaled); end;
Ну и наконец, третье: масштабирование своей процедурой. Делается в методе Loaded.
... protected procedure Loaded; override; ... procedure TBaseForm.Loaded; begin if (FPixelsPerInch > 0) and (FPixelsPerInch <> Screen.PixelsPerInch) then ScaleControl(Self, Screen.PixelsPerInch, FPixelsPerInch); inherited Loaded; end;
Самое интересное в процедуре ScaleControl (её код – ниже), к сожалению без хака там не обошлось, причём хак жёстко привязан к версии Delphi, т.к. необходимо получать доступ к приватным полям компонентов. Если Вы сравните мою процедуру с тем, как масштабирует VCL, вы увидите, что я не обрабатываю ScalingFlags. Дело в том, что у меня масштабирование вызывается всего один раз после полной загрузки формы в методе Loaded, и этого достаточно, т.к. масштабирование нужно применять для всех контролов. В VCL же масштабирование применяется в методе ReadState, который вызывается несколько раз – для каждого класса в иерархии, у которых есть DFM-ресурс. А такое встречается, если используется визуальное наследование. Поэтому VCL необходимо знать, масштабировался контрол, или нет, для чего и используется это свойство ScalingFlags. Этот “финт ушами” VCL использует для случая, когда разные формы в цепочке визуального наследования разрабатываются при разных значениях свойства PixelsPerInch.
Вот код самой процедуры (описание типов THackXXX можно найти в исходнике):
procedure ScaleControl(Control: TControl; MX, DX, MY, DY, MF, DF: Integer); procedure ScaleControlConstraints(Control: TControl); begin with THackSizeConstraints(Control.Constraints) do begin FMaxHeight := MulDiv(FMaxHeight, MY, DY); FMaxWidth := MulDiv(FMaxWidth, MX, DX); FMinHeight := MulDiv(FMinHeight, MY, DY); FMinWidth := MulDiv(FMinWidth, MX, DX); end; //TFriendlySizeConstraints(Control.Constraints).Change; end; {$ifdef Controls.TMargins} procedure ScaleControlMargins(Control: TControl); begin with THackMargins(Control.Margins) do begin FLeft := MulDiv(FLeft, MX, DX); FTop := MulDiv(FTop, MY, DY); FRight := MulDiv(FRight, MX, DX); FBottom := MulDiv(FBottom, MY, DY); end; //TFriendlyMargins(Control.Margins).Change; end; {$endif} procedure ScaleControl(Control: TControl); var L, T, W, H: Integer; begin with Control do begin // scale Left L := MulDiv(Left, MX, DX); // scale Top T := MulDiv(Top, MY, DY); // scale Width if not (csFixedWidth in ControlStyle) then W := MulDiv(Left + Width, MX, DX) - L else W := Width; // scale Hight if not (csFixedHeight in ControlStyle) then H := MulDiv(Top + Height, MY, DY) - T else H := Height; end; ScaleControlConstraints(Control); {$ifdef Controls.TMargins} ScaleControlMargins(Control); {$endif} {$ifdef bf_tb2k} // scale TTBToolWindow if Control is TTBToolWindow then with TTBToolWindow(Control) do begin MaxClientHeight := MulDiv(MaxClientHeight, MY, DY); MaxClientWidth := MulDiv(MaxClientWidth, MX, DX); MinClientHeight := MulDiv(MinClientHeight, MY, DY); MinClientWidth := MulDiv(MinClientWidth, MX, DX); end; {$endif} // apply new bounds (with check constraints and margins) Control.SetBounds(L, T, W, H); with THackControl(Control), TFriendlyControl(Control) do begin // scale OriginalParentSize FOriginalParentSize.X := MulDiv(FOriginalParentSize.X, MX, DX); FOriginalParentSize.Y := MulDiv(FOriginalParentSize.Y, MY, DY); // scale Font.Size if not ParentFont and (MF <> DF) then Font.Size := MulDiv(Font.Size, MF, DF); end; end; procedure ScaleWinControlDesignSize(WinControl: TWinControl); begin with TFriendlyWinControl(WinControl) do begin FDesignSize.X := MulDiv(FDesignSize.X, MX, DX); FDesignSize.Y := MulDiv(FDesignSize.Y, MY, DY); end; end; {$ifdef Controls.TPadding} procedure ScaleWinControlPadding(WinControl: TWinControl); begin with THackPadding(WinControl.Padding) do begin FLeft := MulDiv(FLeft, MX, DX); FTop := MulDiv(FTop, MY, DY); FRight := MulDiv(FRight, MX, DX); FBottom := MulDiv(FBottom, MY, DY); end; TFriendlyPadding(WinControl.Padding).Change; end; {$endif} procedure ScaleWinControl(WinControl: TWinControl); begin ScaleControl(WinControl); ScaleWinControlDesignSize(WinControl); {$ifdef Controls.TPadding} ScaleWinControlPadding(WinControl); {$endif} end; procedure ScaleScrollBars(Control: TScrollingWinControl); begin with TFriendlyScrollingWinControl(Control) do begin if not AutoScroll then begin with HorzScrollBar do begin Position := 0; Range := MulDiv(Range, MX, DX); end; with VertScrollBar do begin Position := 0; Range := MulDiv(Range, MY, DY); end; end; end; end; procedure ScaleScrollingWinControl(ScrollingWinControl: TScrollingWinControl); begin ScaleScrollBars(ScrollingWinControl); ScaleWinControl(ScrollingWinControl); end; procedure ScaleCustomFormConstraints(CustomForm: TCustomForm; cdx, cdy: Integer); procedure ScaleValue(var Value: TConstraintSize; M, D, s: Integer); var tmp: Integer; begin if Value > 0 then begin tmp := MulDiv(Value - s, M, D) + s; if tmp < 0 then Value := 0 else Value := tmp; end; end; begin // при масштабировании констрейнтов формы, надо учитывать // разницу между внешними размерами и размерами клиентской области with THackSizeConstraints(CustomForm.Constraints) do begin ScaleValue(FMaxWidth, MX, DX, cdx); ScaleValue(FMinWidth, MX, DX, cdx); ScaleValue(FMaxHeight, MY, DY, cdy); ScaleValue(FMinHeight, MY, DY, cdy); end; //TFriendlySizeConstraints(Constraints).Change; end; procedure ScaleCustomForm(CustomForm: TCustomForm); var W, H: Integer; cdx, cdy: Integer; begin with CustomForm do begin cdx := Width - ClientWidth; cdy := Height - ClientHeight; if MF <> DF then Font.Height := MulDiv(Font.Height, MF, DF); W := MulDiv(ClientWidth, MX, DX) + cdx; H := MulDiv(ClientHeight, MY, DY) + cdy; end; ScaleWinControlDesignSize(CustomForm); ScaleScrollBars(CustomForm); ScaleCustomFormConstraints(CustomForm, cdx, cdy); // При уменьшении размера иногда (пока не разбирался почему) новые размеры не применяются // Наращивание ширины и высоты на 1 пиксель помогает обойти такую проблему if DX > MX then inc(W); if DY > MY then inc(H); // apply new bounds (with check constraints and margins) with CustomForm do SetBounds(Left, Top, W, H); end; procedure ScaleAndAlignWinControl(WinControl: TWinControl); var SavedAnchors: array of TAnchors; i: Integer; begin with WinControl do begin // disable anchors of child controls: SetLength(SavedAnchors, ControlCount); for i := 0 to ControlCount - 1 do begin SavedAnchors[i] := Controls[i].Anchors; Controls[i].Anchors := [akLeft, akTop]; end; DisableAlign; try // scale itself: if WinControl is TCustomForm then ScaleCustomForm(TCustomForm(WinControl)) else if WinControl is TScrollingWinControl then ScaleScrollingWinControl(TScrollingWinControl(WinControl)) else ScaleWinControl(WinControl); // scale child controls: for i := 0 to ControlCount - 1 do BaseForms.ScaleControl(Controls[i], MX, DX, MY, DY, MF, DF); finally EnableAlign; // enable anchors of child controls: for i := 0 to ControlCount - 1 do Controls[i].Anchors := SavedAnchors[i]; end; end; end; begin if Control is TWinControl then ScaleAndAlignWinControl(TWinControl(Control)) else ScaleControl(Control); end;
Приведённое мною решение проблемы масштабирования вполне работоспособное. Единственное требование, которое я к нему предъявляю – разработка всех форм в одном разрешении, и то, если у Вас используется визуальное наследование.
Фиксим масштабирование фрейм
С фреймой проще: нам достаточно сохранять/считывать свойство PixelsPerInch и выполнять масштабирование в том случае, если фрейма создана без родителя.
... private FPixelsPerInch: Integer; procedure WritePixelsPerInch(Writer: TWriter); procedure ReadPixelsPerInch(Reader: TReader); protected procedure DefineProperties(Filer: TFiler); override; procedure Loaded; override; ... procedure TBaseFrame.WritePixelsPerInch(Writer: TWriter); begin Writer.WriteInteger(Screen.PixelsPerInch); end; procedure TBaseFrame.ReadPixelsPerInch(Reader: TReader); begin FPixelsPerInch := Reader.ReadInteger; end; procedure TBaseFrame.DefineProperties(Filer: TFiler); begin inherited DefineProperties(Filer); // сохранять свойство PixelsPerInch нужно только при дизайне самой фреймы. Если фрейма встроена во что-то, то // тогда свойство сохранять не нужно Filer.DefineProperty('PixelsPerInch', ReadPixelsPerInch, WritePixelsPerInch, not Assigned(Filer.Ancestor)); end; procedure TBaseFrame.Loaded; begin // масштабируем только в том случае, если фрейма создаётся в Run-Time вручную (т.е. Parent = nil), // либо в дизайнере if (FPixelsPerInch > 0) and (FPixelsPerInch <> Screen.PixelsPerInch) and (not Assigned(Parent) or (csDesigning in ComponentState)) then ScaleControl(Self, Screen.PixelsPerInch, FPixelsPerInch); inherited Loaded; end;
Скачать
Исходники доступны на этой странице. Ссылка для быстрого скачивания.
UPD 16.05.2013 – небольшой рефакторинг + поддержка Delphi XE2
13 коммент.:
Маленькая просьба: добавить пакет для DelphiXE *150.*, чтобы не ставить пакет *140.* от Delphi2010.
IL, я могу сделать необходимые переименования (в файлах и именах файлов). Но Delphi XE я устанавливать пока не хочу..
Можно сделать так - Вы мне напишите на eamil, я Вам отошлю пакеты, вы их потестируете (в плане устанавливаются они, или нет) и отпишитесь.. после этого я их выложу в Git
Как вариант обхода проблемы масштабирования: можно ли запретить масштабирование форм своего приложения так, чтобы при 120dpi они выглядели также, как и при 96dpi?
IL, могу добавить глобальную переменную, по которой масштабирование будет отключаться.
Вероятно, это помогло бы если не решить проблемы масштабирования в Delphi или BaseForms, то хотя бы задвинуть проблему подальше. Особенно в D7.
Я совсем забыл, что в D7 еще проблема с уездом компонет за правый и нижний край формы в режиме Aero или в режиме рабочего стола, отличном от классического в Виста+. Возможно ли в BaseForms сделать такой фикс?
Вот BaseForms как раз эту проблему решает, пока не совсем идеально (надо покрутить с масштабированием шрифта), но решает. Здесь важно, чтобы в DFM-формы сохранилось свойство ScaleFix, если его в DFM-нет, то сработает масштабирование на уровне VCL с указанной проблемой.
ОК, что-то меня переклинило. Почему-то я подумал, что в XE одни проблемы с масштабированием, а в 7 другие. Вообще, интересно, удастся ли решить их простым исправлением положения и размеров компонент. А еще, почему в EMBT не сделали поддержку high DPI для VCL? Понятно, что поезд уже ушел в сторону FireMonkey. Но все же VCL по-прежнему native-библиотека для Windows. В документе http://msdn.microsoft.com/en-us/library/windows/desktop/dd464660(v=vs.85).aspx вроде бы описано, что нужно сделать, чтобы приложение стало high DPI aware.
Обсуждения high-DPI конечно были https://forums.embarcadero.com/message.jspa?messageID=442224 и https://forums.embarcadero.com/message.jspa?messageID=471064
Интересная статья про DPI-aware приложения http://www.rw-designer.com/DPI-aware
Получается, у нас есть возможность поиграть свойством формы Scaled и уведомить ОС, что наше приложение DPI-aware, чтобы она не врала про DPI.
Судя по всему в Win8.1 или win8.2 будут разные PPI на разных экранах одновременно. И просто таская форму между экранами будешь вызывать ее многокартное перемасштабирование.
Похоже всё-таки тупик - попиксельно уже не получится, будут ошибки округления лезть.
а) можно ссылку?
б) в принципе, это не криминально. По идее, сама ОС сможет масштабировать окна. Т.е. для приложения будет использоваться виртуальное PPI. С таким мы уже сталкивались в Vista/7 - флаг "Масштабировать в стиле Windows XP". Правда о качестве картинки придётся забыть.
в) чтобы минимизировать ошибки округления в VCL - можно сохранять (или перечитывать из dfm) оригинальные значения размеров при каждом изменении масштаба. Ну т.е. мне думается, костыль изобрести будет вполне реально
google "WM_DPICHANGED"
> можно сохранять (или перечитывать из dfm)
не в общем виде.
Формы могут изменять расположение - или ползователь что-тo подвинет, или программист в OnRResize....
Да, далеко не во всех формах, но тем не менее, это будет
Разработчики Help And Manual выложили документ "A Delphi Developers Guide for 4K Displays"
http://www.helpandmanual.com/downloads_delphi.html
Отправить комментарий