1

I'm developing a Windows Forms control as a plugin for a larger application. The control isn't localized and always displays English text using the Windows Forms default font, i.e. Control.DefaultFont, which resolves to Microsoft Sans Serif on my German Windows 10 (or whatever is configured as the default system font).

Now, on a Chinese Windows 10 the default font may be something that's optimized for Chinese script and the English text that my control is rendering looks rather weird (like in this question). However, the standard dialog that is shown by MessageBox.Show() looks okay despite it almost exclusively showing Latin characters. I assume that's because it is using SystemFonts.MessageBoxFont which resolves to Microsoft YaHei UI on that system.

screenshot of a standard message box with a sensible font on a Chinese Windows

I guess I could explicitly use SystemFonts.MessageBoxFont or hard code another font like Microsoft Sans Serif in my control's constructor (but then there are the Microsoft font guidelines). Is there any default font for Latin script in the .NET framework or Windows in general (FontFamily.GenericSansSerif maybe?) or at least something that tells me if a font can be used for Latin script?

PoByBolek
  • 3,775
  • 3
  • 21
  • 22
  • See the notes [here](https://stackoverflow.com/a/51612395/7444103) about Font fallback. Make a trip to `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink` to verify what font mappings are most probably used when a specific Font is selected as the default Font of a Control. You may notice that Segoe UI is linked to other Font families (e.g., `Microsoft YaHei` and `Microsoft JhengHei`, among the others) while `Microsoft Sans Serif` is not (this font should not be used). – Jimi May 14 '20 at 16:11
  • Consider finding a font that satisfies your needs and embed it in the application and then use the PrivateFontCollection class to load it. Depending on the control used to display the font, you may also have to use the API function AddFontMemResourceEx to load the font into the process space. – TnTinMn May 15 '20 at 02:47
  • For Universal Windows Platform (UWP) there is a [LanguageFontGroup](https://learn.microsoft.com/en-us/uwp/api/windows.globalization.fonts.languagefontgroup) – PoByBolek May 18 '20 at 12:13
  • @Jimi why shouldn't I be using Microsoft Sans Serif? I mean it has its own [overview page](https://learn.microsoft.com/en-us/typography/font-list/microsoft-sans-serif) and all... Are you talking about *MS* Sans Serif perhaps? (This is the predecessor of Microsoft Sans Serif.) – PoByBolek May 19 '20 at 15:33

1 Answers1

1

I haven't found a default font for a given script yet but at least a way to figure out whether a font supports a given script. Starting from Windows Vista, Uniscribe (linked from @Jimi's answer to a similar question) and in particular the ScriptGetFontScriptTags function can be used to query information about OpenType fonts.

Using a little P/Invoke we could do:

private const int MaximumTagCount = 32;

private static int[] GetScriptTags(IntPtr context, Font font)
{
    IntPtr oldFont = SelectObject(context, font.ToHfont());
    if (oldFont == IntPtr.Zero)
        throw new Win32Exception();

    IntPtr scriptCache = IntPtr.Zero;
    IntPtr tagsPointer = IntPtr.Zero;
    IntPtr tagCountPointer = IntPtr.Zero;
    try
    {
        // uniscribe expects a pointer to a SCRIPT_CACHE pointer
        scriptCache = Marshal.AllocHGlobal(IntPtr.Size);
        Marshal.WriteIntPtr(scriptCache, IntPtr.Zero);

        tagsPointer = Marshal.AllocHGlobal(4 * MaximumTagCount); // one tag is 4 bytes long
        tagCountPointer = Marshal.AllocHGlobal(4);

        int status = ScriptGetFontScriptTags(context, scriptCache, IntPtr.Zero, MaximumTagCount, tagsPointer, tagCountPointer);
        if (status != 0)
            throw new Win32Exception(status);

        int tagCount = Marshal.ReadInt32(tagCountPointer);
        if (tagCount > 0 && tagCount <= MaximumTagCount)
        {
            int[] tags = new int[tagCount];
            for (int i = 0; i < tagCount; i++)
                tags[i] = Marshal.ReadInt32(tagsPointer, 4 * i);
            return tags;
        }
        else
        {
            return new int[0];
        }
    }
    finally
    {
        SelectObject(context, oldFont);
        if (scriptCache != IntPtr.Zero)
        {
            ScriptFreeCache(scriptCache);
            Marshal.FreeHGlobal(scriptCache);
        }
        if (tagsPointer != IntPtr.Zero)
            Marshal.FreeHGlobal(tagsPointer);
        if (tagCountPointer != IntPtr.Zero)
            Marshal.FreeHGlobal(tagCountPointer);
    }
}

[DllImport("gdi32.dll")]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr value);

[DllImport("usp10.dll")]
private static extern int ScriptGetFontScriptTags(IntPtr hdc, IntPtr scriptCache, IntPtr scriptAnalysis, int maxTags, IntPtr tags, IntPtr tagCount);

[DllImport("usp10.dll")]
private static extern int ScriptFreeCache(IntPtr scriptCache);

which we could then use once we have a device context from a graphics object:

class Form1 : Form
{
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);

        using (var g = Graphics.FromHwnd(Handle))
        {
            IntPtr context = g.GetHdc();
            try
            {
                foreach (FontFamily family in FontFamily.Families)
                {
                    using (var font = new Font(family, 12))
                    {
                        Console.WriteLine(font);
                        Console.Write("   Scripts:");
                        foreach (int script in GetScriptTags(context, font))
                        {
                            Console.Write(" {0} ({1:x})", TagToString(script), script);
                        }
                        Console.WriteLine();
                    }
                }
            }
            finally
            {
                g.ReleaseHdc(context);
            }
        }
    }

    private static string TagToString(int script)
    {
        var bytes = BitConverter.GetBytes(script);
        var builder = new StringBuilder(bytes.Length);
        foreach (var b in bytes)
            builder.Append((char)b);
        return builder.ToString();
    }
}

Now all we have to do is check whether a font has the latn (or 0x6e74616c as a little endian integer) script tag. Some examples:

[Font: Name=楷体, Size=12, Units=3, GdiCharSet=1, GdiVerticalFont=False]
   Scripts: hani (696e6168)
[Font: Name=Microsoft Himalaya, Size=12, Units=3, GdiCharSet=1, GdiVerticalFont=False]
   Scripts: tibt (74626974)
[Font: Name=Microsoft JhengHei Light, Size=12, Units=3, GdiCharSet=1, GdiVerticalFont=False]
   Scripts: hani (696e6168) kana (616e616b) latn (6e74616c)
[Font: Name=Microsoft Sans Serif, Size=12, Units=3, GdiCharSet=1, GdiVerticalFont=False]
   Scripts: arab (62617261) hebr (72626568) latn (6e74616c) thai (69616874) cyrl (6c727963)
[Font: Name=Segoe UI, Size=12, Units=3, GdiCharSet=1, GdiVerticalFont=False]
   Scripts: arab (62617261) bng2 (32676e62) cyrl (6c727963) dev2 (32766564) gjr2 (32726a67) grek (6b657267) gur2 (32727567) hebr (72626568) khmr (726d686b) knd2 (32646e6b) lao  (206f616c) latn (6e74616c) mlm2 (326d6c6d) mong (676e6f6d) mymr (726d796d) ory2 (3279726f) talu (756c6174) tel2 (326c6574) thai (69616874) tibt (74626974) tml2 (326c6d74) beng (676e6562) deva (61766564) gujr (726a7567) guru (75727567) knda (61646e6b) mlym (6d796c6d) orya (6179726f) taml (6c6d6174) telu (756c6574)
PoByBolek
  • 3,775
  • 3
  • 21
  • 22
  • You're making it more complex that it has to be. I suggested Segoe UI because it's very similar to other Fonts used as fallback Fonts all over the planet. Microsoft Yahei UI, for example (a very well engineered and complete Font), is the direct fallback Font when Segoe UI is used. As you can clearly see, the two Fonts are almost identical, except Yahei UI is 2.25 points larger and its kerning is a little wider then Segoe UI. The support for different scripts is also quite wide. Setting the Form's Font to `SystemFonts.MessageBoxFont` is still a decent choice, along with Form Templates [...] – Jimi May 19 '20 at 16:59
  • Not so great when your application is DpiAware and you need to scale the UI when required. My choice (well, the choice of the Team that handles UI Tests) is to use just one type of Font that falls back nicely to something very similar (by design, not by chance). So I only use templates that use that specific Font and also handle the fallback Font when scaling is needed. Since WinForms doesn't have the support that WPF or UWP have in this department, you need to know what is going to happen when the Font and/or the Font scale changes. -- Anyway, the code is interesting :) – Jimi May 19 '20 at 16:59