In a post earlier this year, I investigated how to retrieve information about theme fonts in Windows. Briefly, the Visual Styles APIs can be used when visual styles are enabled, but values need to be hard-coded (to some extent) otherwise.
Andrew Powell commented on my previous post noting difficulties in implementing the GetThemeFont function in managed code. In this post, I’ll demonstrate how to implement the relevant functions in a simple WPF project. In particular, I’ll focus on displaying information about the ‘main instruction’ text style as seen in Task Dialogs.
Read on for details.
Part 1: Fallback Values
As noted last time, the Visual Styles APIs don’t work when visual styles are disabled.
The first step is to call the OpenThemeData function, which takes a window handle and a class name and returns a handle to theme data. If no match to the specified class name is found, NULL is returned. The class we will reference is TEXTSTYLE, which includes the TEXT_MAININSTRUCTION part. The parts and states for standard controls can be found here.
We need to use PInvoke to call OpenThemeData (and the associated CloseThemeData). As usual, PInvoke.net is very useful for getting PInvoke signatures.
1 2 |
[DllImport("uxtheme.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] public static extern IntPtr OpenThemeData(IntPtr hWnd, String classList); |
1 2 |
[DllImport("uxtheme.dll", ExactSpelling = true)] public extern static Int32 CloseThemeData(IntPtr hTheme); |
1 |
IntPtr hTheme = OpenThemeData(IntPtr.Zero, "TEXTSTYLE"); |
If hTheme is NULL (that is, IntPtr.Zero), we can assume that visual styles are disabled (and in the event that this is not the case, we still won’t be able to use GetThemeFont and GetThemeColor). The fallback values that we should use are defined in AeroStyle.xml, which can be found in the Windows SDK.
1 2 3 4 5 6 7 8 9 10 11 |
<class name="TextStyle"> <part name="MainInstruction"> <caption> <TextColor>0 51 153</TextColor> <Font>Segoe UI, 12, Quality:ClearType</Font> <ClassicValue type="TextColor">WindowText</ClassicValue> <ClassicValue type="Font">CaptionFont</ClassicValue> </caption> </part> ... </class> |
Fortunately, the SystemFonts and SystemColors classes in the System.Windows namespace allow easy access to these values:
1 2 3 4 5 6 7 8 9 10 11 12 |
FontFamily fontfamily; double fontsize; FontWeight fontweight; Brush foreground; if (hTheme == IntPtr.Zero) { fontfamily = SystemFonts.CaptionFontFamily; fontsize = SystemFonts.CaptionFontSize; fontweight = SystemFonts.CaptionFontWeight; foreground = SystemColors.WindowTextBrush; } else { ... } |
Part 2: GetThemeFont and GetThemeColor
In the case where visual styles are enabled, hTheme will hold a handle to theme data that we will pass to the GetThemeFont and GetThemeColor functions.
Firstly, the PInvoke signatures:
1 2 |
[DllImport("uxtheme", ExactSpelling = true, CharSet = CharSet.Unicode)] public extern static Int32 GetThemeFont(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, int iPropId, out LOGFONT pFont); |
1 2 |
[DllImport("uxtheme", ExactSpelling = true)] public extern static Int32 GetThemeColor(IntPtr hTheme, int iPartId, int iStateId, int iPropId, out COLORREF pColor); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct LOGFONT { public const int LF_FACESIZE = 32; public int lfHeight; public int lfWidth; public int lfEscapement; public int lfOrientation; public int lfWeight; public byte lfItalic; public byte lfUnderline; public byte lfStrikeOut; public byte lfCharSet; public byte lfOutPrecision; public byte lfClipPrecision; public byte lfQuality; public byte lfPitchAndFamily; [MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst = LF_FACESIZE)] public string lfFaceName; } |
1 2 3 4 5 6 7 |
[StructLayout(LayoutKind.Sequential)] public struct COLORREF { public byte R; public byte G; public byte B; } |
Let’s start with GetThemeColor:
1 2 3 4 5 6 7 |
// from vsstyle.h int TEXT_MAININSTRUCTION = 1; // from vssym32.h int TMT_TEXTCOLOR = 3803; COLORREF pColor; GetThemeColor(hTheme, TEXT_MAININSTRUCTION, 0, TMT_TEXTCOLOR, out pColor); foreground = new SolidColorBrush(Color.FromRgb(pColor.R, pColor.G, pColor.B)); |
Now for GetThemeFont:
1 2 |
// from vssym32.h int TMT_FONT = 210; |
1 2 3 4 5 |
LOGFONT pFont; GetThemeFont(hTheme, IntPtr.Zero, TEXT_MAININSTRUCTION, 0, TMT_FONT, out pFont); fontfamily = new FontFamily(pFont.lfFaceName); fontsize = Math.Abs(pFont.lfHeight); fontweight = FontWeight.FromOpenTypeWeight(pFont.lfWeight); |
At this point, we’re finished, though we need to call CloseThemeData, preferably in a finally block.
Update (2011-10-25): Something I missed in the original implementation was that the lfHeight value represents the font height in pixels, which is a problem as WPF uses device-independent units (DIUs) for its measurements (something I’ve written about before). To properly support non-default DPI settings, the value in pixels should be converted to DIUs. I’ve added code to the sample project below that does this.
Download Sample
You can download a working sample here (updated 2011-10-25):
14,489 bytes; SHA-1: B01722E889780D94CDB26E31F115F4B2DF2E7111
Hi,
first of all, thank you for this post. It was very helpful for me.
However I ran into an weird issue on all Vista machines. Maybe you have heard of it or know a solution.
I need to get the fonts for “TEXT_BODYTEXT” and “TEXT_BODYTITLE”.
On Win7 I the method GetThemeFont() returns the font family “Segoe UI” and a font weight of 400 (text) or 700 (title).
On Vista however the font family for the title is “Segoe UI Bold”, which cannot be used to create a FontFamily object.
I cannot remove the “Bold” from the string either, because the font name is different in every language.
I have a made a workaround where I just use the font of the text and make it bold, but that’s not a very elegant solution.
Is this the fault of Microsoft? I just want to avoid ending up making a workaround for every system.
I haven’t even tested it on Win8 yet.
Best regards,
Tobias
Hi Tobias, thanks for the comment.
FontFamily x = new FontFamily("Segoe UI Bold");
Works for me as expected in Windows Vista, Windows 7 and Windows 8. Are you getting an error when you try to create the object, or is the font just not appearing as you’d expect?
You shouldn’t need any workarounds (that’s the whole point of this code :)).
Hi, your demostration doesn;t show how to change the font size, do you have this demo?
Only get font() cannot do anything.
GetThemeFont returns the font size in lfHeight.
Can we use the same code to change font and size in WinForms (and not WPF)?
The P/Invoke calls will work just the same, but you’ll find Windows Forms has different classes and methods for dealing with fonts. It’s actually more straightforward: just use Font.FromLogFont for the font and Color.FromArgb for the colour.