当在Windows控制面板中选择较大的字体大小(如125%或150%)时,VCL应用程序就会出现问题,每次设置像素时都会出现问题。
以TStatusBar.Panel
为例。我已经设置了它的宽度,使它只包含一个标签,现在使用大字体的标签"overflows“。其他组件也存在同样的问题。
戴尔的一些新笔记本电脑已经将125%作为默认设置,因此,尽管过去这个问题很少见,但现在它真的很重要。
可以做些什么来克服这个问题?
发布于 2011-11-29 00:10:03
注意:请查看其他答案,因为它们包含非常有价值的技术。我在这里的回答只提供了警告和警告,不要假设DPI感知很容易。
我通常避免使用TForm.Scaled = True
进行感知DPI的缩放。只有当DPI意识对那些打电话给我并愿意为此付费的客户变得重要时,DPI意识才对我很重要。这种观点背后的技术原因是,无论是否意识到DPI,你都是在打开一扇通往伤害世界的窗户。许多标准和第三方VCL控件在High DPI中不能很好地工作。值得注意的例外是,包装Windows Common控件的VCL部件在高DPI下工作得非常好。大量的第三方和内置的Delphi VCL自定义控件不能很好地工作,或者根本不能在高DPI下工作。如果您计划启用TForm.Scaled,请确保在96、125和150DPI下对项目中的每个窗体以及您使用的每个第三方和内置控件进行测试。
Delphi本身是用Delphi编写的。它打开了高DPI意识标志,对于大多数表单,尽管最近在Delphi中,集成开发环境作者自己决定不打开高DPI意识清单标志。请注意,在Delphi和更高版本中,高XE4感知标志被打开,并且集成开发环境看起来很好。
我建议您不要将TForm.Scaled=true (这是Delphi中的默认设置,所以除非您修改过它,否则大多数表单都有Scaled=true)与使用内置delphi表单设计器构建的VCL应用程序一起使用。
我在过去曾尝试过,当TForm.Scaled为真时,以及当Delphi form scaling出现故障时,您可以期望看到的那种破坏的最小样本。这些毛刺并不总是且仅由非96的DPI值触发。我无法确定其他内容的完整列表,包括Windows XP字体大小更改。但由于大多数这些小故障只出现在我自己的应用程序中,在相当复杂的情况下,我决定向您展示一些您可以验证自己的证据。
当你在Windows7中将DPI缩放设置为"Fonts @200%时,Delphi看起来就像这样,而在Windows7和8上,Delphi XE2也出现了类似的问题,但这些小故障似乎在Delphi XE4中得到了修复:
这些大多是在高DPI时表现不佳的标准VCL控件。请注意,大多数东西根本没有缩放,因此Delphi IDE开发人员决定忽略DPI感知,并关闭DPI虚拟化。真是个有趣的选择。
仅当需要这种新的痛苦来源和困难选择时,才关闭DPI虚拟化。我建议你别管它。请注意,Windows公共控件似乎大多工作得很好。请注意,Delphi data-explorer控件是标准Windows Tree公共控件的C# WinForms包装器。这是一个纯粹的微软故障,修复它可能需要Embarcadero为他们的数据浏览器重写一个纯原生.Net树控件,或者编写一些DPI-check- and -modify-properties代码来更改控件中的项高度。即使是微软的WinForms也不能干净利落地、自动地处理高DPI,而且没有自定义的杂乱代码。
更新:有趣的事实:虽然delphi IDE似乎没有“虚拟化”,但它并没有使用David所显示的清单内容来实现“非DPI虚拟化”。也许它在运行时使用了一些API函数。
更新2:为了回应我如何支持100%/125%DPI,我会提出一个分两个阶段的计划。第一阶段是盘点我的自定义控件代码,这些控件需要为高DPI进行修复,然后制定一个计划来修复它们或逐步淘汰它们。阶段2是将我的代码的某些部分设计为没有布局管理的表单,并将它们更改为使用某种布局管理的表单,以便DPI或字体高度的更改可以在没有裁剪的情况下生效。我怀疑,在大多数应用程序中,这种“内部控件”布局工作要比“内部控件”工作复杂得多。
更新:在2016年,最新的Delphi10.1柏林版在我的150dpi工作站上运行良好。
发布于 2011-11-28 21:55:20
只要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
时表单持久性框架所做的事情。
事实上,您可以提出一个有说服力的论点,将此函数替换为硬编码为分母的值96的版本。这使您可以使用绝对尺寸值,而不必担心在开发计算机上更改字体缩放并重新保存.dfm文件时含义会发生变化。之所以重要,是因为存储在.dfm文件中的PixelsPerInch
属性是上次保存.dfm文件的计算机的值。
const
SmallFontsPixelsPerInch = 96;
function ScaleFromSmallFontsDimension(const X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, SmallFontsPixelsPerInch);
end;
因此,继续这个主题,另一件需要注意的事情是,如果您的项目是在具有不同DPI值的多台计算机上开发的,您会发现Delphi在保存.dfm文件时使用的缩放会导致控件在一系列编辑过程中游荡。在我工作的地方,为了避免这种情况,我们有一个严格的政策,只有在96dpi (100%缩放)时才能编辑表单。
事实上,我的ScaleFromSmallFontsDimension
版本还允许表单字体在运行时与设计时设置的字体不同。在XP机器上,我的应用程序的表单使用8pt Tahoma。在Vista及以上版本中,使用的是9pt Segoe UI。这提供了另一种自由度。缩放必须考虑到这一点,因为源代码中使用的绝对尺寸值被假定为相对于96dpi处的8pt Tahoma的基线。
如果您在UI中使用任何图像或字形,则这些图像或字形也需要缩放。一个常见的例子是工具栏和菜单上使用的字形。您需要提供这些字形作为链接到可执行文件的图标资源。每个图标都应该包含一个大小范围,然后在运行时选择最合适的大小并将其加载到图像列表中。有关该主题的一些详细信息可在此处找到:How do I load icons from a resource without suffering from aliasing?
另一个有用的技巧是使用相对于TextWidth
或TextHeight
的相对单位来定义尺寸。所以,如果你想要10条左右的垂直线,你可以使用10*Canvas.TextHeight('Ag')
。这是一个非常粗糙和现成的指标,因为它不考虑行间距等。但是,您通常需要做的就是能够安排图形用户界面使用PixelsPerInch
进行正确的缩放。
您还应该将您的应用程序标记为high DPI aware。执行此操作的最佳方法是通过应用程序清单。由于Delphi的构建工具不允许您自定义您使用的清单,这迫使您链接您自己的清单资源。
<?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%的字体缩放。这看起来很可怕。尽量避免掉进那个陷阱。
Windows8.1每显示器DPI更新
从Windows8.1开始,操作系统现在支持每个显示器的DPI设置(http://msdn.microsoft.com/en-ca/magazine/dn574798.aspx)。对于现代设备来说,这是一个很大的问题,因为现代设备可能具有不同的显示器,具有非常不同的功能。您可能有一个非常高的DPI笔记本电脑屏幕,和一个低DPI的外部投影仪。支持这样的场景需要比上面描述的更多的工作。
发布于 2012-03-26 21:58:44
同样重要的是要注意,尊重用户的DPI只是你实际工作的一个子集:
支持用户字体大小的
几十年来,Windows已经通过使用对话框单位而不是像素来执行布局的概念解决了这个问题。定义了一个“对话单元”,以便字体的平均字符是
Delphi确实附带了一个(有缺陷的) Scaled
概念,其中一个表单试图根据
的开发人员计算机上的
当用户使用与您设计的表单不同的字体时,这并不能解决问题,例如:
6.21px x 13.00px
,96dpi)设计表单;5.94px x 13.00px
,96dpi)运行任何为Windows2000或Windows XP开发应用程序的人都是如此。
或
5.94px x 13.00px
,96dpi)6.67px x 15px
,96dpi)的用户作为一名优秀的开发人员,您应该尊重用户的字体偏好。这意味着您还需要缩放窗体上的所有控件以匹配新的字体大小:
Scaled
不会为您处理此问题。
在以下情况下,情况会变得更糟:
10.52px x 25px
现在,您必须缩放所有内容
Scaled
不会为您处理此问题。
如果你够聪明,你会发现尊重DPI是多么的无关紧要:
使用Segoe UI 9pt @ 96dpi (6.67px x 15px)设计的
你不应该看用户的DPI设置,你应该看他们的字体大小。两个用户运行
都运行着相同的字体。只是一种影响字体大小的东西;用户的偏好是另一种。
StandardizeFormFont
克洛维斯注意到,我引用了一个函数StandardizeFormFont
来固定表单上的字体,并将其缩放为新的字体大小。它不是一个标准函数,而是完成Borland从未处理过的简单任务的一整套函数。
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;
Windows有6种不同的字体;在Windows中没有单一的“字体设置”。
但是我们从经验中知道,我们的表单应该遵循图标标题字体设置
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;
这比你想象的要多得多,我知道。可悲的是,地球上没有Delphi开发人员,除了我,他们的应用程序实际上是正确的。
亲爱的Delphi Developer:将你的Windows字体设置为Segoe UI 14pt,然后修复你有buggy的应用程序
注释:任何代码都会发布到公共领域。不需要属性。
https://stackoverflow.com/questions/8296784
复制相似问题