The fundamental problem is that you try to zoom the text by changing its Height
. Given that the Windows API uses an integer coordinate system it follows that only certain discrete font heights are possible. If for example you have a font 20 pixels high at a scale value of 100%, then you can basically set only scale values that are multiples of 5%. Worse than that, even with TrueType fonts not all of those will give pleasing results.
Windows has had a facility to deal with this for years, which the VCL sadly doesn't wrap (and which it doesn't really make use of internally, either) - mapping modes. Windows NT introduced transformations, but SetMapMode()
has been available in 16 bit Windows already IIRC.
By setting a mode like MM_HIMETRIC
or MM_HIENGLISH
(depending on whether you measure in meters or furlongs) you can calculate the font height and the bounding rectangle, and because pixels are very small it will be possible to finely zoom in or out.
By setting the MM_ISOTROPIC
or MM_ANISOTROPIC
modes OTOH you can keep using the same values for font height and bounding rectangle, and you would instead adjust the transformation matrix between page-space and device-space whenever the zoom value changes.
The SynEdit component suite used to have a print preview control (in the SynEditPrintPreview.pas file) that used the MM_ANISOTROPIC
mapping mode to allow preview of the printable text at different zoom levels. This may be useful as an example if it's still in SynEdit or if you can locate the old versions.
Edit:
For your convenience a little demo, tested with Delphi 4 and 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;
Second Edit:
I thought a bit more about this, and I think that for your problem doing the scaling in user code may actually be the only way to implement this.
Let's look at it with an example. If you have a line of text that would be 500 pixels wide with a font height of 20 pixels at a zoom factor of 100%, then you would have to increase the zoom level to 105% to get a line of text with 525 by 21 pixels size. For all integer zoom levels in between you would have an integer width and a non-integer height of this text. But text output doesn't work this way, you can't set the width of a line of text and have the system compute the height of it. So the only way to do it is to force the font height to 20 pixels for 100% to 104% zoom, but set a font of 21 pixels height for 105% to 109% zoom, and so on. Then the text will be too narrow for most of the zoom values. Or set the font height to 21 pixels starting with 103% zoom, and live with the text being too wide then.
But with a little additional work you can achieve the text width incrementing by 5 pixels for every zoom step. The ExtTextOut()
API call takes an optional integer array of character origins as the last parameter. Most code samples I know don't use this, but you could use it to insert additional pixels between some characters to stretch the width of the line of text to the desired value, or to move characters closer together to shrink the width. It would go more or less like this:
- Calculate the font height for the zoom value. Select a font of this height into the device context.
- Call the
GetTextExtentExPoint()
API function to calculate an array of default character positions. The last valid value should be the width of the whole string. - Calculate a scale value for those character positions by dividing the intended width by the real text width.
- Multiply all character positions by this scale value, and round them to the nearest integer. Depending on the scale value being higher or lower than 1.0 this will either insert additional pixels at strategic positions, or move some characters closer together.
- Use the calculated array of character positions in the call to
ExtTextOut()
.
This is untested and may contain some errors or oversights, but hopefully this would allow you to smoothly scale the text width independently of the text height. Maybe it's worth the effort for your application?