如何在不改变有效文本宽度的情况下绘制放大的文本?

8
我有一些定制绘图代码,基本上是一个WYSIWYG编辑器的表单填充程序。编辑器允许设置缩放级别。我在标签宽度方面存在问题,相对于表单中的其他所有内容,我的标签宽度会跳跃到不同的大小。
下面是我用于输出文本的代码示例。我很确定问题与字体大小的更改不匹配以及与其他所有内容的比例不对应有关。缩放级别必须更改足够才能将字体升级到下一个大小,即使表单上的所有其他内容都随着每次更改移动几个像素。
这导致两个不同的问题 - 文本可以看起来太小,有很多空白空间,或者文本将过大并重叠下一个控件。当我有一行完整的文本时,情况看起来非常糟糕。一个单词的标签不会改变到足以引起任何问题。
我考虑限制缩放级别 - 现在我有一个1%增量的滑块。但我无法看到任何一组级别比其他级别更好。我的表单具有不同字体大小的多个标签,在不同时间内跳转较短或较长的标签。
MultDiv函数对结果进行舍入。我可以截断此值以确保我始终比较小而不是更长,但在这些缩放级别下间隙看起来一样大,看起来同样糟糕。
代码说明:
当前使用的是Delphi 7。这是我们最后一个没有向前发展的项目,因此欢迎与新版本的Delphi相关的答案。
当我们查看时,我看到了ExtDrawText函数存在。然而,更改为该函数似乎没有什么差异。
由于用于构建表单定义的工具无法跟踪文本的右边界,因此边界框的右侧设置为0,并且未剪切文本。我们只是将其视觉上对齐到正确位置。

procedure OutputText(Canvas: TCanvas; LineNumber: integer; CurrentZoomLevel: integer; FontSize: integer; Text: string);
const
  FormatFlags = DT_BOTTOM + DT_SINGLELINE + DT_NOPREFIX + DT_LEFT + DT_NOCLIP;
var
  OutputBox: TRect;
  ZoomedLineHeight: integer;
begin
  ZoomedLineHeight := MulDiv(UnZoomedLineHeight, CurrentZoomLevel, 96);
  Canvas.Font.Height := -MulDiv(FontSize, CurrentZoomLevel, 96);

  OutputBox.Left := ZoomedLineHeight;
  OutputBox.Right := 0;
  OutputBox.Top := (LineNumber * ZoomedLineHeight);
  OutputBox.Bottom := OutputBox.Top + ZoomedLineHeight;

  DrawText(Canvas.Handle, PChar(Text), length(Text), OutputBox, FormatFlags);
end;

编辑:

使用mghie的答案,这是我修改后的测试应用程序。缩放代码已经消失了,只需设置MapMode即可。但是,TextOut函数似乎仍然选择完整的字体大小。文本似乎没有改变,除了我不需要自己计算字体的高度-地图模式可以为我完成。

我发现这个网页“GDI坐标系统”非常有用,但它没有涉及文本大小。

这是我的测试应用程序。它会随着您调整窗体大小而进行调整,并绘制出网格,以便您可以看到文本末尾如何跳动。

procedure DrawGrid(Canvas: TCanvas);
var
  StartPt: TPoint;
  EndPt: TPoint;
  LineCount: integer;
  HeaderString: string;
  OutputBox: TRect;
begin
  Canvas.Pen.Style := psSolid;
  Canvas.Pen.Width := 1;
  StartPt.X := 0;
  StartPt.Y := LineHeight;
  EndPt.X := Canvas.ClipRect.Right;
  EndPt.Y := LineHeight;

  LineCount := 0;
  while (StartPt.Y < Canvas.ClipRect.Bottom) do
  begin
    StartPt.Y := StartPt.Y + LineHeight;
    EndPt.Y := EndPt.Y + LineHeight;

    Inc(LineCount);
    if LineCount mod 5 = 0 then
      Canvas.Pen.Color := clRed
    else
      Canvas.Pen.Color := clBlack;

    Canvas.MoveTo(StartPt.X, StartPt.Y);
    Canvas.LineTo(EndPt.X, EndPt.Y);
  end;

  StartPt.X := 0;
  StartPt.Y := 2 * LineHeight;

  EndPt.X := 0;
  EndPt.Y := Canvas.ClipRect.Bottom;

  LineCount := 0;
  while StartPt.X < Canvas.ClipRect.Right do
  begin
    StartPt.X := StartPt.X + LineHeight;
    EndPt.X := EndPt.X + LineHeight;

    Inc(LineCount);
    if LineCount mod 5 = 0 then
      Canvas.Pen.Color := clRed
    else
      Canvas.Pen.Color := clBlack;

    Canvas.MoveTo(StartPt.X, StartPt.Y);
    Canvas.LineTo(EndPt.X, EndPt.Y);

    if Canvas.Pen.Color = clRed then
    begin
      HeaderString := IntToStr(LineCount);
      OutputBox.Left := StartPt.X - (4 * LineHeight);
      OutputBox.Right := StartPt.X + (4 * LineHeight);
      OutputBox.Top := 0;
      OutputBox.Bottom := OutputBox.Top + (LineHeight * 2);
      DrawText(Canvas.Handle, PChar(HeaderString), Length(HeaderString),
        OutputBox, DT_BOTTOM + DT_SINGLELINE + DT_NOPREFIX + DT_CENTER);
    end;
  end;

end;

procedure OutputText(Canvas: TCanvas; LineNumber: integer; Text: string);
const
  FormatFlags = DT_BOTTOM + DT_SINGLELINE + DT_NOPREFIX + DT_LEFT + DT_NOCLIP;
var
  OutputBox: TRect;
begin
  OutputBox.Left := LineHeight;
  OutputBox.Right := 0;
  OutputBox.Top := (LineNumber * LineHeight);
  OutputBox.Bottom := OutputBox.Top + LineHeight;
  Windows.TextOut(Canvas.Handle, OutputBox.Left, OutputBox.Top, PChar(Text), Length(Text));
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  DoubleBuffered := false;
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  Invalidate;
end;

procedure TForm1.FormPaint(Sender: TObject);
const
  ShortString = 'Short';
  MediumString = 'This is a little longer';
  LongString = 'Here is something that is really long here is where I see the problem with zooming.';

  PhysicalHeight = 500;
  PhysicalWidth = 400;
var
  DC: HDC;
  OldMode, i, xy: integer;
  LF: TLogFont;
  OldFont: HFONT;

begin

  Canvas.Brush.Style := bsClear;

  FillChar(LF, SizeOf(TLogFont), 0);
  LF.lfOutPrecision := OUT_TT_ONLY_PRECIS;
  LF.lfFaceName := 'Arial';
  LF.lfHeight := -12;

  DC := Self.Canvas.Handle;
  OldMode := SetMapMode(DC, MM_ISOTROPIC);
  // OldMode := SetMapMode(DC, MM_HIMETRIC);

  SetWindowExtEx(DC, PhysicalWidth, PhysicalHeight, nil);
  SetViewportExtEx(DC, Self.Width, Self.Height, nil);

  try
    OldFont := Windows.SelectObject(DC, CreateFontIndirect(LF));

    DrawGrid(Self.Canvas);
    OutputText(Self.Canvas, 3, ShortString);
    OutputText(Self.Canvas, 4, MediumString);
    OutputText(Self.Canvas, 5, LongString);

    DeleteObject(SelectObject(DC, OldFont));
  finally
    SetMapMode(DC, OldMode);
  end;

end;
5个回答

9
根本问题在于您试图通过更改文本的 Height 来缩放文本。鉴于 Windows API 使用整数坐标系统,只有某些离散字体高度是可能的。例如,如果您有一个字体在 100% 的缩放比例下高度为 20 像素,则基本上只能设置倍数为 5% 的缩放比例值。更糟糕的是,即使使用 TrueType 字体,也不是所有这些倍数都会产生令人满意的结果。
Windows 已经有多年处理此问题的工具,但 VCL 不幸地没有封装它(并且它在内部也没有真正使用),这就是映射模式。Windows NT 引入了 变换,但 SetMapMode() 在 16 位 Windows 中已经可用了(如果我没记错的话)。
通过设置像 MM_HIMETRICMM_HIENGLISH 这样的模式(取决于您是否以米或弗隆为单位测量),您可以计算字体高度和边界矩形,并且由于像素非常小,因此可以精细地缩放。
通过设置 MM_ISOTROPICMM_ANISOTROPIC 模式,您可以继续使用相同的字体高度和边界矩形值,并且在缩放值更改时调整页面空间和设备空间之间的变换矩阵。
SynEdit 组件套件曾经有一个打印预览控件(在 SynEditPrintPreview.pas 文件中),它使用了 MM_ANISOTROPIC 映射模式,以允许在不同的缩放级别下预览可打印文本。如果它仍然在 SynEdit 中或者您可以找到旧版本,则可能会对您有所帮助。 编辑: 为了方便起见,这里有一个小演示,已在 Delphi 4 和 Delphi 2009 中测试过:
procedure TForm1.FormCreate(Sender: TObject);
begin
  ClientWidth := 1000;
  ClientHeight := 1000;
  DoubleBuffered := False;
end;

procedure TForm1.FormPaint(Sender: TObject);
var
  DC: HDC;
  OldMode, i, xy: integer;
  LF: TLogFont;
  OldFont: HFONT;
begin
  Canvas.Brush.Style := bsClear;

  FillChar(LF, SizeOf(TLogFont), 0);
  LF.lfOutPrecision := OUT_TT_ONLY_PRECIS;
  LF.lfFaceName := 'Arial';

  DC := Canvas.Handle;
  OldMode := SetMapMode(DC, MM_HIMETRIC);
  try
    SetViewportOrgEx(DC, ClientWidth div 2, ClientHeight div 2, nil);
    Canvas.Ellipse(-8000, -8000, 8000, 8000);

    for i := 42 to 200 do begin
      LF.lfHeight := -5 * i;
      LF.lfEscapement := 100 * i;
      OldFont := Windows.SelectObject(DC, CreateFontIndirect(LF));
      xy := 2000 - 100 * (i - 100);
      Windows.TextOut(DC, -xy, xy, 'foo bar baz', 11);
      DeleteObject(SelectObject(DC, OldFont));
    end;
  finally
    SetMapMode(DC, OldMode);
  end;
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  Invalidate;
end;

第二次编辑:

我再想了一下,我认为对于你的问题,在用户代码中进行缩放可能是实现这一目标的唯一方法。

让我们以一个例子来看看。如果您有一行文本,它在100%缩放系数下具有20像素的字体高度,那么该行文本的宽度将为500像素。那么,要想获得525 x 21像素大小的文本行,则必须将缩放级别增加到105%。 对于介于所有整数缩放级别之间的文本,您将拥有这段文本的整数宽度和非整数高度。但是,文本输出并不是这样工作的,您不能设置一行文本的宽度并让系统计算其高度。因此,唯一的方法是将字体高度强制设为100%至104%缩放的20像素,但对于105%至109%缩放,则设置21像素高的字体,依此类推。然后,大多数缩放值下的文本将太窄。或者从103%缩放开始将字体高度设置为21像素,并接受文本过宽的情况。

但是通过一些额外的工作,您可以使文本宽度在每个缩放步骤中递增5像素。 ExtTextOut() API调用将最后一个参数作为可选的字符起点整数数组。我所知道的大多数代码示例都不使用此功能,但是您可以使用它在某些字符之间插入其他像素以拉伸文本行的宽度,或者将字符靠得更近以缩小宽度。具体操作如下:

  • 计算缩放值的字体高度。将此高度的字体选择到设备上下文中。
  • 调用GetTextExtentExPoint() API函数来计算默认字符位置的数组。最后一个有效值应该是整个字符串的宽度。
  • 通过将预期宽度除以真实文本宽度来计算这些字符位置的比例值。
  • 将所有字符位置乘以此比例值,并将其四舍五入为最接近的整数。根据比例值高于或低于1.0,这将在战略位置插入其他像素,或将一些字符靠得更近。
  • 在调用ExtTextOut()时使用计算出的字符位置数组。

这还未经过测试,可能包含某些错误或疏漏,但希望这可以让您在文本高度独立于文本宽度的情况下平稳地缩放文本宽度。也许对于您的应用程序来说,这值得一试?


现在你只是在炫耀 :-). 代码真是太棒了。然而,我仍然遇到同样的问题。如果我知道MapModes,我本来可以真正简化我的缩放代码。但是当我尝试时,我仍然看到我的文本大小跳来跳去。我正在使用您的代码和SynEdit中看到的内容编辑我的帖子,并附上我的示例应用程序。 - Mark Elder
一旦达到Windows文本渲染的极限,你可能没有太多可以做的。为了平滑地缩放一行文本的宽度而不出现“跳跃”,您需要在子像素范围内缩放高度。我只在DVI查看器中看到过这种情况,它们会自己呈现字形。我今晚稍后会查看您的代码。 - mghie
你的代码通过一些微小的改变,实际上比我的更好地展示了这个问题。如果你去掉lfEscapement的变化,使用一个更长的字符串,并保持写入文本的左侧不变,你就可以看到阶梯状的效果。 OutString := 'foo bar bazfoo bar bazfoo bar bazfoo bar bazfoo bar bazfoo bar bazfoo bar bazabcdefghijklmnopqrstuvwxyaz'; Windows.TextOut(DC, -15000, xy, PChar(OutString), Length(OutString));我不认为我真的需要子像素缩放,但似乎Windows没有字体的每个像素的缩放。至少现在我知道我没有错过什么简单的东西。 - Mark Elder
现在我明白你所说的亚像素了。我用一个字母运行了一下,然后截屏并放大查看。我可以看到它每次只调整一个像素。问题是所有字符串中的字母都会这样调整。感谢你的帮助。 - Mark Elder

2
另一种处理字体缩放的方法是将其绘制到内存位图中,然后使用StretchBlt()按所需大小拉伸。
与上一个答案的想法相同,但实现更加清晰。
基本步骤如下:
  1. 使用SetMapMode()设置MM_ISOTROPIC映射模式
  2. 使用SetWindowExtEx()SetViewPortExtEx()定义坐标映射
  3. 绘制线条和图形
  4. 恢复映射模式
  5. 创建原始大小的位图
  6. 在位图上绘制文本
  7. 创建所需大小的透明位图
  8. 使用HALFTONE模式的StretchBlt()将带有文本的位图内容复制到透明位图中
  9. 在窗体画布上绘制包含文本的透明位图
  10. 销毁两个位图
下面是页面顶部示例的代码。
首先,在OnPaint处理程序中创建一个新函数以清除代码:
procedure DrawTestText(drawCanvas : TCanvas);
    const
      ShortString = 'Short';
      MediumString = 'This is a little longer';
      LongString = 'Here is something that is really long here is where I see the problem with zooming.';
    var
      LF             : TLogFont;
      OldFont        : HFONT;
      NewFont        : HFONT;
    begin

      FillChar(LF, SizeOf(TLogFont), 0);
      LF.lfOutPrecision := OUT_TT_ONLY_PRECIS;
      LF.lfFaceName := 'Arial';
      LF.lfHeight := -12;
      LF.lfQuality := PROOF_QUALITY;

      NewFont := CreateFontIndirect(LF);
      try
        OldFont := Windows.SelectObject(drawCanvas.Handle, NewFont);
        try
          OutputText(drawCanvas, 3, ShortString);
          OutputText(drawCanvas, 4, MediumString);
          OutputText(drawCanvas, 5, LongString);
        finally
          Windows.SelectObject(drawCanvas.Handle, OldFont);
        end;
      finally
        Windows.DeleteObject(NewFont);
      end;

    end;

下面是 OnPaint 事件的代码:

procedure TForm1.FormPaint(Sender: TObject);
const
  PhysicalHeight = 500;
  PhysicalWidth = 400;
var
  bmp            : TBitmap;
  bufferBitmap   : TBitmap;
  drawCanvas     : TCanvas;
  OldMapMode     : integer;
  OldStretchMode : integer;
  outHeight      : extended;
begin

  // compute desired height
  outHeight := PhysicalHeight * (ClientWidth / PhysicalWidth) ;

  // 1. Set MM_ISOTROPIC mapping mode with SetMapMode()
  OldMapMode := SetMapMode(Self.Canvas.Handle, MM_ISOTROPIC);
  try
    // 2. Define coordinate mappings with SetWindowExtEx() and SetViewPortExtEx()
    SetWindowExtEx(Self.Canvas.Handle, PhysicalWidth, PhysicalHeight, nil);
    SetViewportExtEx(Self.Canvas.Handle, Self.Width, round(outHeight), nil);
    SelectClipRgn(Self.Canvas.Handle, CreateRectRgn(0,0, Width, round(outHeight)));

    // 3. Draw lines and graphics
    DrawGrid(Self.Canvas);

  finally
    // 4. Restore mapping mode
    SetMapMode(Self.Canvas.Handle, OldMapMode);
  end;

  // 5. Create bitmap with original size
  bmp := TBitmap.Create;
  try
    bmp.Transparent := false;
    bmp.Width := PhysicalWidth;
    bmp.Height := PhysicalHeight;

    drawCanvas := bmp.Canvas;
    drawCanvas.Font.Assign(Self.Canvas.Font);
    drawCanvas.Brush.Assign(Self.Canvas.Brush);
    drawCanvas.Pen.Assign(Self.Canvas.Pen);

    drawCanvas.Brush.Style := bsSolid;
    drawCanvas.Brush.Color := Color;
    drawCanvas.FillRect(Rect(0,0,PhysicalWidth, PhysicalHeight));

    // 6. Draw text on bitmap
    DrawTestText(drawCanvas);

    // 7. Create transparent bitmap with desired size
    bufferBitmap := TBitmap.Create;
    try
      bufferBitmap.PixelFormat := pfDevice;
      bufferBitmap.TransparentColor := Color;
      bufferBitmap.Transparent := true;
      bufferBitmap.Width := ClientWidth;
      bufferBitmap.Height := round(outHeight);
      bufferBitmap.Canvas.Brush.Style := bsSolid;
      bufferBitmap.Canvas.Brush.Color := Color;
      bufferBitmap.Canvas.FillRect(Rect(0,0,bufferBitmap.Width, bufferBitmap.Height));

      // 8. Copy content of bitmap with text to transparent bitmap with StretchBlt() in HALFTONE mode
      OldStretchMode := SetStretchBltMode(bufferBitmap.Canvas.Handle, HALFTONE);
      try
        SetBrushOrgEx(bufferBitmap.Canvas.Handle, 0, 0, nil);
        StretchBlt(
          bufferBitmap.Canvas.Handle, 0, 0, bufferBitmap.Width, bufferBitmap.Height,
          drawCanvas.Handle,          0, 0, PhysicalWidth,      PhysicalHeight,
          SRCCOPY
        );

      finally
        SetStretchBltMode(bufferBitmap.Canvas.Handle, oldStretchMode);
      end;

      // 9. Draw transparent bitmap, which contains text now, on form's canvas
      Self.Canvas.Draw(0,0,bufferBitmap);

      // 10. Destroy both bitmaps
    finally
      bufferBitmap.Free;
    end;

  finally
    bmp.Free;
  end;

end;

感谢您的输入。我明白那个方法可以行得通,但我会选择调整每个字母之间的间距来解决问题。这样我仍然可以使用 ClearType 字体。 - Mark Elder
好的,谢谢回复。但是字母间距解决方案存在一些问题:
  1. 字体高度的跳跃没有被消除
  2. 像素插入算法必须非常智能才能产生良好的外观。
  3. 假设您达到了“缩小”和“扩展”之间的界限...
请注意,在页面顶部的示例中,您始终选择相同高度的字体并使用映射进行缩放。实际上,您必须选择具有缩放高度的字体,并以正常(未映射)模式输出文本。我会发布另一个带有OnClick处理程序的“答案”,它说明了方法之间的差异。
- ThinkJet

1

好的,基于mghie的建议,我修改了字符之间的空格,这是我想出来的方法。我没有使用字符间距的数组,而是使用了SetTextCharacterExtraSetTextJustification

SetTExtCharacterExtra函数有以下注意事项:

该函数主要用于与现有应用程序兼容。新应用程序通常应避免调用此函数,因为它与复杂脚本(需要文本整形的脚本;阿拉伯语脚本就是一个例子)不兼容。

推荐的方法是,应用程序应调用ExtTextOut而不是调用此函数,然后使用其lpDx参数提供宽度。

我可能会改变我的代码以使用这种方法,但目前这种方法已经非常有效。下面是我修改后的函数。

const
   LineHeight = 20;

procedure DrawGrid(Output: TCanvas; ZoomLevel: integer);
var
  StartPt: TPoint;
  EndPt: TPoint;

  ZoomedStartPt: TPoint;
  ZoomedEndPt: TPoint;

  ZoomedIncrement: integer;
  LineCount: integer;
  HeaderString: string;
  OutputBox: TRect;
begin
  ZoomedIncrement := MulDiv(LineHeight, ZoomLevel, 100);

  if (ZoomedIncrement = 0) then
    exit;

  Output.Pen.Style := psSolid;
  Output.Pen.Width := 1;


  StartPt.X := 0;
  StartPt.Y := LineHeight;

  EndPt.X := 1000;
  EndPt.Y := LineHeight;

  LineCount := 0;
  while StartPt.Y < 1000 do
  begin
    StartPt.Y := StartPt.Y + LineHeight;
    EndPt.Y := EndPt.Y + LineHeight;

    Inc(LineCount);
    if LineCount mod 5 = 0 then
      Output.Pen.Color := clRed
    else
      Output.Pen.Color := clBlack;

    ZoomedStartPt.X :=  MulDiv(StartPt.X, ZoomLevel, 100);
    ZoomedStartPt.Y :=  MulDiv(StartPt.Y, ZoomLevel, 100);
    ZoomedEndPt.X :=  MulDiv(EndPt.X, ZoomLevel, 100);
    ZoomedEndPt.Y :=  MulDiv(EndPt.Y, ZoomLevel, 100);

    Output.MoveTo(ZoomedStartPt.X, ZoomedStartPt.Y);
    Output.LineTo(ZoomedEndPt.X, ZoomedEndPt.Y);
  end;


  StartPt.X := 0;
  StartPt.Y := 2 * LineHeight;

  EndPt.X := 0;
  EndPt.Y := 1000;



  LineCount := 0;
  while StartPt.X < 1000 do
  begin
    StartPt.X := StartPt.X + LineHeight;
    EndPt.X := EndPt.X + LineHeight;

    Inc(LineCount);
    if LineCount mod 5 = 0 then
      Output.Pen.Color := clRed
    else
      Output.Pen.Color := clBlack;

    ZoomedStartPt.X :=  MulDiv(StartPt.X, ZoomLevel, 100);
    ZoomedStartPt.Y :=  MulDiv(StartPt.Y, ZoomLevel, 100);
    ZoomedEndPt.X :=  MulDiv(EndPt.X, ZoomLevel, 100);
    ZoomedEndPt.Y :=  MulDiv(EndPt.Y, ZoomLevel, 100);

    Output.MoveTo(ZoomedStartPt.X, ZoomedStartPt.Y);
    Output.LineTo(ZoomedEndPt.X, ZoomedEndPt.Y);

    if Output.Pen.Color = clRed then
    begin
      HeaderString := IntToStr(LineCount);
      OutputBox.Left := StartPt.X - (4 * LineHeight);
      OutputBox.Right := StartPt.X + (4 * LineHeight);
      OutputBox.Top := 0;
      OutputBox.Bottom := OutputBox.Top + (LineHeight * 2);


      OutputBox.Left := MulDiv(OutputBox.Left, ZoomLevel, 100);
      OutputBox.Right := MulDiv(OutputBox.Right, ZoomLevel, 100);
      OutputBox.Top := MulDiv(OutputBox.Top, ZoomLevel, 100);
      OutputBox.Bottom := MulDiv(OutputBox.Bottom, ZoomLevel, 100);


      DrawText(Output.Handle, PChar(HeaderString), Length(HeaderString),
        OutputBox, DT_BOTTOM + DT_SINGLELINE + DT_NOPREFIX + DT_CENTER);
    end;
  end;

end;



function CountSpaces(S: string): integer;
var
  i: integer;
begin
  result := 0;
  for i := 1 to Length(S) do
  begin
    if (S[i] = ' ') then
      result := result + 1;
  end;
end;


procedure OutputText(Canvas: TCanvas; LineNumber: integer; CurrentZoomLevel: integer; FontSize: integer; Text: string;
  AdjustChars: boolean = true; AdjustSpaces: boolean = true);
var
  DC: HDC;

  UnzoomedStringWidth: integer;
  UnzoomedFontHeight: integer;

  ZoomedLineHeight: integer;
  ZoomedStringWidth: integer;
  ZoomedFontHeight: integer;
  OutputBox: TRect;

  ExtraPixels: integer;
  StringWidth: integer;
  TextOutSize: TSize;
  TextLength: integer;

  SpacesCount: integer;

  PixelsPerChar: Integer;

  Report: string;

begin
  DC := Canvas.Handle;

  // First find the box where the string would be for unzoomed text
  UnzoomedFontHeight := -MulDiv(FontSize, GetDeviceCaps(Canvas.Handle, LOGPIXELSY), 72);
  Canvas.Font.Height := UnzoomedFontHeight;
  UnzoomedStringWidth := Canvas.TextWidth(Text);

  // Now figure out the zoomed sizes for the font and the box where
  // the string will be drawn
  ZoomedLineHeight := MulDiv(LineHeight, CurrentZoomLevel, 96);
  ZoomedFontHeight := -MulDiv(-UnzoomedFontHeight, CurrentZoomLevel, 96);
  ZoomedStringWidth := MulDiv(UnzoomedStringWidth, CurrentZoomLevel, 96);

  OutputBox.Left := ZoomedLineHeight;
  OutputBox.Right := OutputBox.Left + ZoomedStringWidth;
  OutputBox.Top := (LineNumber * ZoomedLineHeight);
  OutputBox.Bottom := OutputBox.Top + ZoomedLineHeight;

  Canvas.Font.Height := ZoomedFontHeight;

  TextLength := Length(Text);

  Windows.GetTextExtentPoint32(Canvas.Handle, PChar(Text), TextLength, TextOutSize);
  ExtraPixels := ZoomedStringWidth - TextOutSize.cx;

  PixelsPerChar := Round(ExtraPixels / TextLength);

  // If we let extra push past two pixels in our out we will end up with either
  // letters overlapping or really wide text.  A maximum of 1 pixel adjustment
  // outside the spaces seem to help keep the text looking normal and
  // removes some of the pressure on the spaces adjustment.  Also is needed
  // for short 1 word labels.

  if PixelsPerChar > 1 then
    PixelsPerChar := 1;

  if PixelsPerChar < -1 then
    PixelsPerChar := -1;

  if (PixelsPerChar <> 0) and (AdjustChars = true) then
  begin
    Windows.SetTextCharacterExtra(Canvas.Handle, PixelsPerChar);
    ExtraPixels := ExtraPixels - (PixelsPerChar * TextLength);
  end;

  // What ever is left over do with spaces
  if (ExtraPixels <> 0) and (AdjustSpaces = true) then
  begin
    SpacesCount := CountSpaces(Text);
    Windows.SetTextJustification(Canvas.Handle, ExtraPixels, SpacesCount);
  end;

  Windows.SetTextAlign(Canvas.Handle, TA_LEFT + TA_BASELINE);
  Windows.ExtTextOut(Canvas.Handle, OutputBox.Left, OutputBox.Top, 0, @OutputBox, PChar(Text), TextLength, nil);

  Windows.GetTextExtentPoint32(Canvas.Handle, PChar(Text), TextLength, TextOutSize);


  // Reset these values to 0
  Windows.SetTextCharacterExtra(Canvas.Handle, 0);
  Windows.SetTextJustification(Canvas.Handle, 0, 0);


  Report := 'T=' + IntToStr(ZoomedStringWidth); // Target
  Report := Report + ': A=' + IntToStr(TextOutSize.cx); // Actual
  Windows.TextOut(Canvas.Handle, OutputBox.Right + 30, OutputBox.Top, PChar(Report), Length(Report));
end;


procedure TForm1.FormCreate(Sender: TObject);
begin
  DoubleBuffered := false;
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  Invalidate;
end;

procedure TForm1.FormPaint(Sender: TObject);
const
  ShortString = 'Short';
  MediumString = 'This is a little longer';
  LongString = 'Here is something that is really long here is where I see the problem with zooming.';

  PhysicalWidth = 700;

var
  ZoomLevel: integer;
begin
  Canvas.Font.Name := 'Arial';
  ZoomLevel := Round((Self.Width / PhysicalWidth) * 100);
  DrawGrid(Self.Canvas, ZoomLevel);

  OutputText(Self.Canvas, 3, ZoomLevel, 12, ShortString);
  OutputText(Self.Canvas, 4, ZoomLevel, 12, MediumString);
  OutputText(Self.Canvas, 5, ZoomLevel, 12, LongString);
end;

SetTextCharacterExtra 得到了大力支持,这对我正在遇到的类似问题非常有帮助 - 谢谢! - RichieHindle

1

有测试代码来比较不同的解决方案。

代码将长缩放线的实际宽度输出到font_cmp.csv文件中。

链接到比较的图片

示例代码:

procedure TForm1.Button1Click(Sender: TObject);
const
  LongString = 'Here is something that is really long here is where I see the problem with zooming.';
  PhysicalHeight = 500;
  PhysicalWidth = 400;
var
  bmp             : TBitmap;
  drawCanvas      : TCanvas;
  OldMapMode      : integer;
  OldStretchMode  : integer;
  outHeight       : extended;
  originalStrSize : TSize;
  scaledStrSize   : TSize;
  proposedStrSize : TSize;
  desiredWidth    : integer;
  LF              : TLogFont;
  OldFont         : HFONT;
  NewFont         : HFONT;
  cmpList         : TStringList;
  ratio           : extended;
begin

  FillChar(LF, SizeOf(TLogFont), 0);
  LF.lfOutPrecision := OUT_TT_ONLY_PRECIS;
  LF.lfFaceName := 'Arial';
  LF.lfHeight := -12;
  LF.lfQuality := PROOF_QUALITY;

  NewFont := CreateFontIndirect(LF);
  try
    OldFont := Windows.SelectObject(Self.Canvas.Handle, NewFont);
    try
      GetTextExtentPoint32(Self.Canvas.Handle, PChar(LongString), Length(LongString), originalStrSize);
    finally
      Windows.SelectObject(Self.Canvas.Handle, OldFont);
    end;
  finally
    Windows.DeleteObject(NewFont);
  end;

  cmpList := TStringList.Create;
  try

    cmpList.Add(
      'OriginalLength' + ';' +
      'ProperLength'  + ';' +
      'ScaledLength'  + ';' +
      'MappedLength'  + ';' +
      'ScaledLengthDiff' + ';' +
      'MappedLengthDiff'
    );

    for desiredWidth := 1 to 3000 do begin
      // compute desired height
      ratio := desiredWidth / PhysicalWidth;
      outHeight := PhysicalHeight * ratio ;
      if(outHeight < 1) then outHeight := 1;

      LF.lfHeight := round(12*ratio) * (-1);
      NewFont := CreateFontIndirect(LF);
      try
        OldFont := Windows.SelectObject(Self.Canvas.Handle, NewFont);
        try
          GetTextExtentPoint32(Canvas.Handle, PChar(LongString), Length(LongString), scaledStrSize);
        finally
          Windows.SelectObject(Self.Canvas.Handle, OldFont);
        end;
      finally
        Windows.DeleteObject(NewFont);
      end;

      OldMapMode := SetMapMode(Self.Canvas.Handle, MM_ISOTROPIC);
      try
        SetWindowExtEx(Self.Canvas.Handle, PhysicalWidth, PhysicalHeight, nil);
        SetViewportExtEx(Self.Canvas.Handle, desiredWidth, round(outHeight), nil);

        LF.lfHeight := -12;
        NewFont := CreateFontIndirect(LF);
        try
          OldFont := Windows.SelectObject(Self.Canvas.Handle, NewFont);
          try
            GetTextExtentPoint32(Canvas.Handle, PChar(LongString), Length(LongString), proposedStrSize);
          finally
            Windows.SelectObject(Self.Canvas.Handle, OldFont);
          end;
        finally
          Windows.DeleteObject(NewFont);
        end;

      finally
        SetMapMode(Self.Canvas.Handle, OldMapMode);
      end;

      cmpList.Add(
        IntToStr(originalStrSize.cx) + ';' +
        IntToStr(round(ratio * originalStrSize.cx))  + ';' +
        IntToStr(scaledStrSize.cx)  + ';' +
        IntToStr(proposedStrSize.cx)  + ';' +
        IntToStr(round(ratio * originalStrSize.cx - scaledStrSize.cx)) + ';' +
        IntToStr(round(ratio * originalStrSize.cx - proposedStrSize.cx))
      );

    end;
    cmpList.SaveToFile('font_cmp.csv');

  finally
    cmpList.Free;
  end;

end;

0

解决方案,由mghie介绍,对于图形效果很好,但在缩放字体时失败。
还有另一种具有相反属性的缩放方法:SetWorldTransform。这种方法在缩放TrueType字体时效果很好,但在使用GDI绘制图形时失败。

因此,我的建议是使用mghie的方法切换DC模式以绘制线条,并在绘制文本时使用SetWorldTransform。 结果不是很清晰,但看起来更好...

这里是问题文本示例的OnPaint事件处理程序代码,使用了两种方法:

procedure TForm1.FormPaint(Sender: TObject);
const
  ShortString = 'Short';
  MediumString = 'This is a little longer';
  LongString = 'Here is something that is really long here is where I see the problem with zooming.';

  PhysicalHeight = 500;
  PhysicalWidth = 400;
var
  DC: HDC;
  OldMode, i, xy: integer;
  LF: TLogFont;
  OldFont: HFONT;
  NewFont: HFONT;
  oldGraphicMode : integer;
  transform : TXForm;
begin

  Canvas.Brush.Style := bsClear;

  SetMapperFlags(DC, 1);

  FillChar(LF, SizeOf(TLogFont), 0);
  LF.lfOutPrecision := OUT_TT_ONLY_PRECIS;
  LF.lfFaceName := 'Arial';
  LF.lfHeight := -12;
  LF.lfQuality := DRAFT_QUALITY;

  DC := Self.Canvas.Handle;

  // Mode switch for drawing graphics
  OldMode := SetMapMode(DC, MM_ISOTROPIC);
  try
    SetWindowExtEx(DC, PhysicalWidth, PhysicalHeight, nil);
    SetViewportExtEx(DC, Self.Width, Self.Height, nil);
    DrawGrid(Self.Canvas);
  finally
    SetMapMode(DC, OldMode);
  end;

  // Mode switch for text output
  oldGraphicMode := Windows.SetGraphicsMode(DC, GM_ADVANCED);
  try
    //x' = x * eM11 + y * eM21 + eDx,
    transform.eM11 := Width / PhysicalWidth;
    transform.eM21 := 0;
    transform.eDx := 0;
    //y' = x * eM12 + y * eM22 + eDy,
    transform.eM12 := 0;
    transform.eM22 := Height / PhysicalHeight;
    transform.eDy := 0;

    Windows.SetWorldTransform(DC, transform);
    try
      NewFont := CreateFontIndirect(LF);
      try
        OldFont := Windows.SelectObject(DC, NewFont);
        try
          OutputText(Self.Canvas, 3, ShortString);
          OutputText(Self.Canvas, 4, MediumString);
          OutputText(Self.Canvas, 5, LongString);
        finally
          Windows.SelectObject(DC, OldFont);
        end;
      finally
        Windows.DeleteObject(NewFont);
      end;
    finally
      transform.eM11 := 1;
      transform.eM22 := 1;
      Windows.SetWorldTransform(DC, transform);
    end;

  finally
    Windows.SetGraphicsMode(DC, oldGraphicMode);
  end;

end;

也许我做错了什么,但是用这段代码实际上看到字符串宽度的跳跃更大了。 - Mark Elder
是的,在编辑帖子时出现了我的错误。必须删除DeleteObject(SelectObject(DC,OldFont))中的奇怪行。我已经更正了示例并删除了该行。但是现在我找到了一个更干净但需要更多资源的解决方案。测试后,我会在这里发布示例。 - ThinkJet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接