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.
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.
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.
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.
Downloads
GetTextMetricsDemo.zip (10.51 KB)
All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/retrieving-font-and-text-metrics-using-csharp?source=rss