Retrieving font and text metrics using C#
In several of my applications, I need to be able to line up text, be it blocks of text using different fonts, or text containers of differing heights. As far as I'm aware, there isn't a way of doing this natively in .NET, however with a little platform invoke we can get the information we need to do it ourselves.
The GetTextMetrics
metrics function is used to obtain
metrics based on a font and a device context by populating a
TEXTMETRICW
structure.
[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern bool GetTextMetrics(IntPtr hdc, out TEXTMETRICW lptm);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct TEXTMETRICW
{
public int tmHeight;
public int tmAscent;
public int tmDescent;
public int tmInternalLeading;
public int tmExternalLeading;
public int tmAveCharWidth;
public int tmMaxCharWidth;
public int tmWeight;
public int tmOverhang;
public int tmDigitizedAspectX;
public int tmDigitizedAspectY;
public ushort tmFirstChar;
public ushort tmLastChar;
public ushort tmDefaultChar;
public ushort tmBreakChar;
public byte tmItalic;
public byte tmUnderlined;
public byte tmStruckOut;
public byte tmPitchAndFamily;
public byte tmCharSet;
}
Although there's a lot of information available (as you can see
in the demonstration program), for the most part I tend to use
just the tmAscent
value which returns the pixels above the
base line of characters.
A quick note on leaks
I don't know how relevant clean up is in modern versions of Windows, but in older versions of Windows it used to be very important to clean up behind you. If you get a handle to something, release it when you're done. If you create a GDI object, delete it when you're done. If you select GDI objects into a DC, store and restore the original objects when you're done. Not doing these actions used to be a good source of leaks. I don't use GDI anywhere near as much as I used to years ago as a VB6 developer, but I assume the principles still apply even in the latest versions of Windows.
Calling GetTextMetrics
As GetTextMetrics
is a Win32 GDI API call, it requires a
device context, which is basically a bunch of graphical objects
such as pens, brushes - and fonts. Generally you would use the
GetDC
or CreateDC
API calls, but fortunately the .NET
Graphics
object is essentially a wrapper around a device
context, so we can use this.
A DC can only have one object of a specific type activate at a
time. For example, in order to draw a line, you need to tell the
DC the handle of the pen to draw with. When you do this, Windows
will tell you the handle of the pen that was originally in the
DC. After you have finished drawing your line, it is up to you
to both restore the state of the DC, and to destroy your pen.
The GDI calls SelectObject
and DeleteObject
can do
this.
[DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiObj);
The following helper functions can be used to get the font ascent, either for the specified Control
or for a IDeviceContext
and Font
combination.
I haven't tested the performance of using
Control.CreateGraphics
versus directly creating a DC. If you are calling this functionality a lot it may be worth caching the values or avoidingCreateGraphics
and trying pure Win32 API calls.
private int GetFontAscent(Control control)
{
using (Graphics graphics = control.CreateGraphics())
{
return this.GetFontAscent(graphics, control.Font);
}
}
private int GetFontAscent(IDeviceContext dc, Font font)
{
int result;
IntPtr hDC;
IntPtr hFont;
IntPtr hFontDefault;
hDC = IntPtr.Zero;
hFont = IntPtr.Zero;
hFontDefault = IntPtr.Zero;
try
{
NativeMethods.TEXTMETRICW textMetric;
hDC = dc.GetHdc();
hFont = font.ToHfont();
hFontDefault = NativeMethods.SelectObject(hDC, hFont);
NativeMethods.GetTextMetrics(hDC, out textMetric);
result = textMetric.tmAscent;
}
finally
{
if (hFontDefault != IntPtr.Zero)
{
NativeMethods.SelectObject(hDC, hFontDefault);
}
if (hFont != IntPtr.Zero)
{
NativeMethods.DeleteObject(hFont);
}
dc.ReleaseHdc();
}
return result;
}
In the above code you can see how we first get the handle of the
underlying device context by calling GetDC
. This essentially
locks the device context, as in the same way that only a single
GDI object of each type can be associated with a GDI, only one
thread can use the DC at a time. (It's little more complicated
than that, but this will suffice for this post).
Next, we convert the managed .NET Font
into an unmanaged
HFONT
.
You are responsible for deleting the handle returned by
Font.ToHfont
Once we have our font handle, we set that to be the current font
of the device context using SelectObject
, which returns the
existing font handle - we store this for later.
Now we can call GetTextMetrics
passing in the handle of the
DC, and a TEXTMETRIC
instance to populate. Note that the
GetTextMetrics
call could fail, and if so the function call
will return false. In this demonstration code, I'm not checking
for success or failure and assuming the call will always
succeed.
Once we've called GetTextMetrics
, it's time to reverse some of
the steps we did earlier.
Note the use of a finally block, so even if a crash occurs during processing, our clean up operations will still get called
First we restore the original font handle that we obtained from
the first call to SelectObject
.
Now it's safe to delete our HFONT
- so we do that with
DeleteObject
.
It's important to do these steps in order - deleting the handle to a GDI object that is currently associated with a device context isn't a great idea!
Finally, we release the DC handle we created earlier via
ReleaseDC
.
And that's pretty much all there is to it - we've got our font ascent, cleaned up everything behind us and can now get on with the whatever purpose we needed that value for!
What about the other information?
The example code above focuses on the tmAscent
value as this
is mostly what I use. However, you could adapt the function to
return the TEXTMETRICW
structure directly, or to populate a
more .NET friendly object using .NET naming conventions and
converting things like tmPitchAndFamily
to friendly enums etc.
Update History
- 2016-07-09 - First published
- 2020-11-21 - Updated formatting
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
GetTextMetricsDemo.zip
|
Sample project for the retrieving font and text metrics using C# blog post. |
09/07/2016 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?