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.

Obtaining metrics using GetTextMetrics
Obtaining metrics using GetTextMetrics

The GetTextMetrics metrics function is used to obtain metrics based on a font and a device context by populating a TEXTMETRICW structure.

csharp
[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.

csharp
[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 avoiding CreateGraphics and trying pure Win32 API calls.

csharp
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

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


Comments