在Windows控制面板中选择大字号(如125%或150%)时,VCL应用程序存在问题,每次设置像素精细度时都会出现问题。
以 TStatusBar.Panel
为例。我已经设置了它的宽度,使其恰好包含一个标签,但是使用大字体时,标签会“溢出”。其他组件也存在相同的问题。
戴尔的一些新款笔记本电脑已将125%设置为默认值,因此尽管过去这个问题非常罕见,现在变得非常重要。
如何解决这个问题?
在Windows控制面板中选择大字号(如125%或150%)时,VCL应用程序存在问题,每次设置像素精细度时都会出现问题。
以 TStatusBar.Panel
为例。我已经设置了它的宽度,使其恰好包含一个标签,但是使用大字体时,标签会“溢出”。其他组件也存在相同的问题。
戴尔的一些新款笔记本电脑已将125%设置为默认值,因此尽管过去这个问题非常罕见,现在变得非常重要。
如何解决这个问题?
只要Scaled
为True
,您在.dfm文件中的设置将被正确缩放。
如果您正在代码中设置尺寸,则需要通过Screen.PixelsPerInch
除以Form.PixelsPerInch
来缩放它们。使用MulDiv
来完成此操作。
function TMyForm.ScaleDimension(const X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, PixelsPerInch);
end;
Scaled
为 True
时,表单持久性框架所做的事情。PixelsPerInch
属性是最后保存 .dfm 文件的计算机的值。const
SmallFontsPixelsPerInch = 96;
function ScaleFromSmallFontsDimension(const X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, SmallFontsPixelsPerInch);
end;
ScaleFromSmallFontsDimension
还考虑到了在运行时表单字体可能与设计时设置的字体不同的可能性。在XP机器上,我的应用程序的表单使用8pt Tahoma。在Vista及更高版本上,使用9pt Segoe UI。这提供了另一种自由度。缩放必须考虑到这一点,因为源代码中使用的绝对尺寸值被假定相对于96dpi下8pt Tahoma的基线。TextWidth
或TextHeight
。因此,如果您想让某个东西的垂直大小大约为10行,则可以使用10*Canvas.TextHeight('Ag')
。这是一种非常粗略和简单的度量方法,因为它不考虑行间距等因素。然而,通常您只需要能够安排GUI与PixelsPerInch
正确缩放即可。<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
1 24 "Manifest.txt"
其中Manifest.txt
包含实际清单。您还需要包括comctl32 v6部分,并将requestedExecutionLevel
设置为asInvoker
。然后将此编译的资源链接到您的应用程序,并确保Delphi不会尝试使用其清单进行操作。在现代Delphi中,通过将运行时主题项目选项设置为None来实现。
清单是声明您的应用程序支持高DPI的正确方式。如果您只想快速尝试而不破坏清单,请调用SetProcessDPIAware
。在应用程序运行时的第一件事情就这样做。最好在早期单元初始化部分之一或在.dpr文件的第一件事情。
如果您未声明应用程序支持高DPI,则Vista及更高版本将在任何字体缩放超过125%的情况下以传统模式呈现它。这看起来非常可怕。尽量避免陷入这种困境。
Windows 8.1每个监视器DPI更新
从Windows 8.1开始,现在有操作系统支持每个监视器的DPI设置(http://msdn.microsoft.com/en-ca/magazine/dn574798.aspx)。这对于可能连接具有非常不同功能的不同显示器的现代设备来说是一个大问题。您可能有一个非常高的DPI笔记本电脑屏幕和低DPI外部投影仪。支持这种情况需要比上述更多的工作。
TForm.Scaled = True
实现 DPI 感知缩放。对我而言,只有在顾客要求并愿意为此付费时,DPI 感知才变得重要。技术上讲,这样做的原因是,无论是否具备 DPI 感知,您都将打开一个充满风险的窗口。许多标准和第三方 VCL 控件在高 DPI 下表现不佳。需要特别指出的是,VCL 封装 Windows 常规控件的 VCL 部分在高 DPI 下表现良好。大量第三方和内置的 Delphi VCL 自定义控件在高 DPI 下表现不佳或根本不工作。如果您计划启用 TForm.Scaled,请确保针对项目中的每个窗体以及您使用的每个第三方和内置控件在 96、125 和 150 DPI 下进行测试。当您在Windows 7中将DPI缩放设置为“字体@ 200%”时,Delphi XE看起来像这样,而Delphi XE2在Windows 7和8上也存在类似的问题,但是自Delphi XE4以来,这些故障似乎已经得到修复:
这些大多数是标准VCL控件,在高DPI下表现不佳。请注意,大多数东西都没有被缩放,因此Delphi IDE开发人员决定忽略DPI感知,以及关闭DPI虚拟化。这是一个有趣的选择。
只有想要新的痛苦和困难选择时才关闭DPI虚拟化。我建议您不要管它。请注意,Windows常用控件似乎大多正常工作。请注意,Delphi数据浏览器控件是一个C# WinForms包装器,围绕标准的Windows Tree常规控件。这是一个纯微软的故障,并且修复它可能需要Embarcadero重写一个纯本地的.Net树控件,或编写一些DPI检查和修改属性代码来更改控件中的项目高度。即使是Microsoft WinForms也无法自动,清晰地处理高DPI,而不需要自定义修补代码。
更新:有趣的事实:虽然Delphi IDE似乎没有“虚拟化”,但它没有使用David显示的清单内容来实现“非DPI虚拟化”。也许它在运行时使用一些API函数。
更新2:针对如何支持100%/125% DPI的问题,我的方案分为两个阶段。第一阶段是对自定义控件进行清单编制,确定需要修复或逐步淘汰的控件,并制定计划进行修复。第二阶段是将一些以表单形式设计但没有布局管理的代码转换成使用某种布局管理的表单,以便DPI或字体高度的变化可以避免裁剪。我怀疑,在大多数应用程序中,“控件间”布局工作要比“控件内”工作复杂得多。
更新:2016年最新版的Delphi 10.1 Berlin在我的150 dpi工作站上运行良好。
Delphi自带一个(有缺陷的)概念TCustomForm.Scaled
,其中一个窗体会尝试根据用户的Windows DPI设置自动调整,而不是开发者上次保存窗体时机器的DPI设置。
但当用户使用与您设计窗体不同的字体时,这并不能解决问题,例如:
开发人员使用MS Sans Serif 8pt设计窗体(其中平均字符为6.21px x 13.00px
,在96dpi下)
用户使用Tahoma 8pt(其中平均字符为5.94px x 13.00px
,在96dpi下)
这就像任何为Windows 2000或Windows XP开发应用程序的人一样。
或者
5.94px x 13.00px
)6.67px x 15px
)使用 TCustomForm.Scaled
是不好的。这是一个不好的想法,也是一个不好的选择和设计。你需要在设计时将所有表单的 .Scaled
设置为 False。
Scaled
无法为您处理此操作。10.52px x 25px
现在您需要按比例缩放所有内容:Scaled
无法为您处理此操作。
function StandardizeFormFont(AForm: TForm): Real;
var
preferredFontName: string;
preferredFontHeight: Integer;
begin
GetUserFontPreference({out}preferredFontName, {out}preferredFontHeight);
//e.g. "Segoe UI",
Result := Toolkit.StandardizeFormFont(AForm, PreferredFontName, PreferredFontHeight);
end;
字体 | 如何检索 |
---|---|
图标标题 | SystemParametersInfo(SPI_GETICONTITLELOGFONT) |
标题 | SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfCaptionFont |
小标题 | SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfSmCaptionFont |
菜单 | SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfMenuFont |
状态 | SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfStatusFont |
消息 | SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfMessageFont |
但是根据我们的经验,表单应该遵循图标标题字体设置。
procedure GetUserFontPreference(out FaceName: string; out PixelHeight: Integer);
var
font: TFont;
begin
font := Toolkit.GetIconTitleFont;
try
FaceName := font.Name; //e.g. "Segoe UI"
//Dogfood testing: use a larger font than we're used to; to force us to actually test it
if IsDebuggerPresent then
font.Size := font.Size+1;
PixelHeight := font.Height; //e.g. -16
finally
font.Free;
end;
end;
-16
,而表单当前为-11
,则我们需要按以下方式缩放整个表单:-16 / -11 = 1.45454%
function StandardizeFormFont(AForm: TForm; FontName: string; FontHeight: Integer): Real;
var
oldHeight: Integer;
begin
Assert(Assigned(AForm));
if (AForm.Scaled) then
begin
OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to Scaled. Proper form scaling requires VCL scaling to be disabled, unless you implement scaling by overriding the protected ChangeScale() method of the form.'));
end;
if (AForm.AutoScroll) then
begin
if AForm.WindowState = wsNormal then
begin
OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to AutoScroll. Form designed size will be suseptable to changes in Windows form caption height (e.g. 2000 vs XP).'));
if IsDebuggerPresent then
Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
end;
end;
if (not AForm.ShowHint) then
begin
AForm.ShowHint := True;
OutputDebugString(PChar('INFORMATION: StandardizeFormFont: Turning on form "'+GetControlName(AForm)+'" hints. (ShowHint := True)'));
if IsDebuggerPresent then
Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
end;
oldHeight := AForm.Font.Height;
//Scale the form to the new font size
// if (FontHeight <> oldHeight) then For compatibility, it's safer to trigger a call to ChangeScale, since a lot of people will be assuming it always is called
begin
ScaleForm(AForm, FontHeight, oldHeight);
end;
//Now change all controls to actually use the new font
Toolkit.StandardizeFont_ControlCore(AForm, g_ForceClearType, FontName, FontHeight,
AForm.Font.Name, AForm.Font.Size);
//Return the scaling ratio, so any hard-coded values can be multiplied
Result := FontHeight / oldHeight;
end;
这里是实际缩放表单的工作。它解决了Borland自己Form.ScaleBy
方法中的错误。首先,它必须禁用表单上的所有锚点,然后执行缩放,最后重新启用锚点:
TAnchorsArray = array of TAnchors;
procedure ScaleForm(const AForm: TForm; const M, D: Integer);
var
aAnchorStorage: TAnchorsArray;
RectBefore, RectAfter: TRect;
x, y: Integer;
monitorInfo: TMonitorInfo;
workArea: TRect;
begin
if (M = 0) and (D = 0) then
Exit;
RectBefore := AForm.BoundsRect;
SetLength(aAnchorStorage, 0);
aAnchorStorage := DisableAnchors(AForm);
try
AForm.ScaleBy(M, D);
finally
EnableAnchors(AForm, aAnchorStorage);
end;
RectAfter := AForm.BoundsRect;
case AForm.Position of
poScreenCenter, poDesktopCenter, poMainFormCenter, poOwnerFormCenter,
poDesigned: //i think i really want everything else to also follow the nudging rules...why did i exclude poDesigned
begin
//This was only nudging by one quarter the difference, rather than one half the difference
// x := RectAfter.Left - ((RectAfter.Right-RectBefore.Right) div 2);
// y := RectAfter.Top - ((RectAfter.Bottom-RectBefore.Bottom) div 2);
x := RectAfter.Left - ((RectAfter.Right-RectAfter.Left) - (RectBefore.Right-RectBefore.Left)) div 2;
y := RectAfter.Top - ((RectAfter.Bottom-RectAfter.Top)-(RectBefore.Bottom-RectBefore.Top)) div 2;
end;
else
//poDesigned, poDefault, poDefaultPosOnly, poDefaultSizeOnly:
x := RectAfter.Left;
y := RectAfter.Top;
end;
if AForm.Monitor <> nil then
begin
monitorInfo.cbSize := SizeOf(monitorInfo);
if GetMonitorInfo(AForm.Monitor.Handle, @monitorInfo) then
workArea := monitorInfo.rcWork
else
begin
OutputDebugString(PChar(SysErrorMessage(GetLastError)));
workArea := Rect(AForm.Monitor.Left, AForm.Monitor.Top, AForm.Monitor.Left+AForm.Monitor.Width, AForm.Monitor.Top+AForm.Monitor.Height);
end;
// If the form is off the right or bottom of the screen then we need to pull it back
if RectAfter.Right > workArea.Right then
x := workArea.Right - (RectAfter.Right-RectAfter.Left); //rightEdge - widthOfForm
if RectAfter.Bottom > workArea.Bottom then
y := workArea.Bottom - (RectAfter.Bottom-RectAfter.Top); //bottomEdge - heightOfForm
x := Max(x, workArea.Left); //don't go beyond left edge
y := Max(y, workArea.Top); //don't go above top edge
end
else
begin
x := Max(x, 0); //don't go beyond left edge
y := Max(y, 0); //don't go above top edge
end;
AForm.SetBounds(x, y,
RectAfter.Right-RectAfter.Left, //Width
RectAfter.Bottom-RectAfter.Top); //Height
end;
然后我们必须递归地实际使用新字体:
procedure StandardizeFont_ControlCore(AControl: TControl; ForceClearType: Boolean;
FontName: string; FontSize: Integer;
ForceFontIfName: string; ForceFontIfSize: Integer);
const
CLEARTYPE_QUALITY = 5;
var
i: Integer;
RunComponent: TComponent;
AControlFont: TFont;
begin
if not Assigned(AControl) then
Exit;
if (AControl is TStatusBar) then
begin
TStatusBar(AControl).UseSystemFont := False; //force...
TStatusBar(AControl).UseSystemFont := True; //...it
end
else
begin
AControlFont := Toolkit.GetControlFont(AControl);
if not Assigned(AControlFont) then
Exit;
StandardizeFont_ControlFontCore(AControlFont, ForceClearType,
FontName, FontSize,
ForceFontIfName, ForceFontIfSize);
end;
{ If a panel has a toolbar on it, the toolbar won't paint properly. So this idea won't work.
if (not Toolkit.IsRemoteSession) and (AControl is TWinControl) and (not (AControl is TToolBar)) then
TWinControl(AControl).DoubleBuffered := True;
}
//Iterate children
for i := 0 to AControl.ComponentCount-1 do
begin
RunComponent := AControl.Components[i];
if RunComponent is TControl then
StandardizeFont_ControlCore(
TControl(RunComponent), ForceClearType,
FontName, FontSize,
ForceFontIfName, ForceFontIfSize);
end;
end;
当递归禁用锚点时:
function DisableAnchors(ParentControl: TWinControl): TAnchorsArray;
var
StartingIndex: Integer;
begin
StartingIndex := 0;
DisableAnchors_Core(ParentControl, Result, StartingIndex);
end;
procedure DisableAnchors_Core(ParentControl: TWinControl; var aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
iCounter: integer;
ChildControl: TControl;
begin
if (StartingIndex+ParentControl.ControlCount+1) > (Length(aAnchorStorage)) then
SetLength(aAnchorStorage, StartingIndex+ParentControl.ControlCount+1);
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
aAnchorStorage[StartingIndex] := ChildControl.Anchors;
//doesn't work for set of stacked top-aligned panels
// if ([akRight, akBottom ] * ChildControl.Anchors) <> [] then
// ChildControl.Anchors := [akLeft, akTop];
if (ChildControl.Anchors) <> [akTop, akLeft] then
ChildControl.Anchors := [akLeft, akTop];
// if ([akTop, akBottom] * ChildControl.Anchors) = [akTop, akBottom] then
// ChildControl.Anchors := ChildControl.Anchors - [akBottom];
Inc(StartingIndex);
end;
//Add children
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
if ChildControl is TWinControl then
DisableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
end;
end;
并且锚点被递归地重新启用:
procedure EnableAnchors(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray);
var
StartingIndex: Integer;
begin
StartingIndex := 0;
EnableAnchors_Core(ParentControl, aAnchorStorage, StartingIndex);
end;
procedure EnableAnchors_Core(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
iCounter: integer;
ChildControl: TControl;
begin
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
ChildControl.Anchors := aAnchorStorage[StartingIndex];
Inc(StartingIndex);
end;
//Restore children
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
if ChildControl is TWinControl then
EnableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
end;
end;
我需要实际更改控件字体的工作留给:
procedure StandardizeFont_ControlFontCore(AControlFont: TFont; ForceClearType: Boolean;
FontName: string; FontSize: Integer;
ForceFontIfName: string; ForceFontIfSize: Integer);
const
CLEARTYPE_QUALITY = 5;
var
CanChangeName: Boolean;
CanChangeSize: Boolean;
lf: TLogFont;
begin
if not Assigned(AControlFont) then
Exit;
{$IFDEF ForceClearType}
ForceClearType := True;
{$ELSE}
if g_ForceClearType then
ForceClearType := True;
{$ENDIF}
//Standardize the font if it's currently
// "MS Shell Dlg 2" (meaning whoever it was opted into the 'change me' system
// "MS Sans Serif" (the Delphi default)
// "Tahoma" (when they wanted to match the OS, but "MS Shell Dlg 2" should have been used)
// "MS Shell Dlg" (the 9x name)
CanChangeName :=
(FontName <> '')
and
(AControlFont.Name <> FontName)
and
(
(
(ForceFontIfName <> '')
and
(AControlFont.Name = ForceFontIfName)
)
or
(
(ForceFontIfName = '')
and
(
(AControlFont.Name = 'MS Sans Serif') or
(AControlFont.Name = 'Tahoma') or
(AControlFont.Name = 'MS Shell Dlg 2') or
(AControlFont.Name = 'MS Shell Dlg')
)
)
);
CanChangeSize :=
(
//there is a font size
(FontSize <> 0)
and
(
//the font is at it's default size, or we're specifying what it's default size is
(AControlFont.Size = 8)
or
((ForceFontIfSize <> 0) and (AControlFont.Size = ForceFontIfSize))
)
and
//the font size (or height) is not equal
(
//negative for height (px)
((FontSize < 0) and (AControlFont.Height <> FontSize))
or
//positive for size (pt)
((FontSize > 0) and (AControlFont.Size <> FontSize))
)
and
//no point in using default font's size if they're not using the face
(
(AControlFont.Name = FontName)
or
CanChangeName
)
);
if CanChangeName or CanChangeSize or ForceClearType then
begin
if GetObject(AControlFont.Handle, SizeOf(TLogFont), @lf) <> 0 then
begin
//Change the font attributes and put it back
if CanChangeName then
StrPLCopy(Addr(lf.lfFaceName[0]), FontName, LF_FACESIZE);
if CanChangeSize then
lf.lfHeight := FontSize;
if ForceClearType then
lf.lfQuality := CLEARTYPE_QUALITY;
AControlFont.Handle := CreateFontIndirect(lf);
end
else
begin
if CanChangeName then
AControlFont.Name := FontName;
if CanChangeSize then
begin
if FontSize > 0 then
AControlFont.Size := FontSize
else if FontSize < 0 then
AControlFont.Height := FontSize;
end;
end;
end;
end;
这里是我的礼物。一个能够帮助你在GUI布局中进行水平定位元素的函数。免费供所有人使用。
function CenterInParent(Place,NumberOfPlaces,ObjectWidth,ParentWidth,CropPercent: Integer): Integer;
{returns formated centered position of an object relative to parent.
Place - P order number of an object beeing centered
NumberOfPlaces - NOP total number of places available for object beeing centered
ObjectWidth - OW width of an object beeing centered
ParentWidth - PW width of an parent
CropPercent - CP percentage of safe margin on both sides which we want to omit from calculation
+-----------------------------------------------------+
| |
| +--------+ +---+ +--------+ |
| | | | | | | |
| +--------+ +---+ +--------+ |
| | | | | |
+-----------------------------------------------------+
| |<---------------------A----------------->| |
|<-C->|<------B----->|<-----B----->|<-----B---->|<-C->|
| |<-D>|
|<----------E------------>|
A = PW-C B = A/NOP C=(CP*PW)/100 D = (B-OW)/2
E = C+(P-1)*B+D }
var
A, B, C, D: Integer;
begin
C := Trunc((CropPercent*ParentWidth)/100);
A := ParentWidth - C;
B := Trunc(A/NumberOfPlaces);
D := Trunc((B-ObjectWidth)/2);
Result := C+(Place-1)*B+D;
end;