Search code examples
c#windowspowershellconsolepinvoke

SetCurrentConsoleFontEx isn't working for long font names


I can't get this to work for font names that are 16 characters or longer, but the console itself obviously doesn't have this limitation. Does anyone know a programmatic way to set the font that will work with the built-in "Lucida Sans Typewriter" or the open source "Fira Code Retina"?

This following code works:

I have copied PInvoke code from various places, particularly the PowerShell console host, and the Microsoft Docs

Note that the relevant docs for CONSOLE_FONT_INFOEX and SetCurrentConsoleFontEx do not talk about this, and the struct defines the font face as a WCHAR field of size 32...

Also note what's not a limitation, but is a restriction from the console dialog, is that the font has to have True Type outlines, and must be genuinely fixed width. Using this API you can chose variable width fonts like "Times New Roman" ...

However, in the API, it has to have less than 16 characters in the name -- which is a restriction the console itself doesn't have, and may be a bug in the API and not my code below 🤔

using System;
using System.Runtime.InteropServices;

public static class ConsoleHelper
{
    private const int FixedWidthTrueType = 54;
    private const int StandardOutputHandle = -11;

    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern IntPtr GetStdHandle(int nStdHandle);

    [return: MarshalAs(UnmanagedType.Bool)]
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern bool SetCurrentConsoleFontEx(IntPtr hConsoleOutput, bool MaximumWindow, ref FontInfo ConsoleCurrentFontEx);

    [return: MarshalAs(UnmanagedType.Bool)]
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern bool GetCurrentConsoleFontEx(IntPtr hConsoleOutput, bool MaximumWindow, ref FontInfo ConsoleCurrentFontEx);


    private static readonly IntPtr ConsoleOutputHandle = GetStdHandle(StandardOutputHandle);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct FontInfo
    {
        internal int cbSize;
        internal int FontIndex;
        internal short FontWidth;
        public short FontSize;
        public int FontFamily;
        public int FontWeight;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
        //[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.wc, SizeConst = 32)]
        public string FontName;
    }

    public static FontInfo[] SetCurrentFont(string font, short fontSize = 0)
    {
        Console.WriteLine("Set Current Font: " + font);

        FontInfo before = new FontInfo
        {
            cbSize = Marshal.SizeOf<FontInfo>()
        };

        if (GetCurrentConsoleFontEx(ConsoleOutputHandle, false, ref before))
        {

            FontInfo set = new FontInfo
            {
                cbSize = Marshal.SizeOf<FontInfo>(),
                FontIndex = 0,
                FontFamily = FixedWidthTrueType,
                FontName = font,
                FontWeight = 400,
                FontSize = fontSize > 0 ? fontSize : before.FontSize
            };

            // Get some settings from current font.
            if (!SetCurrentConsoleFontEx(ConsoleOutputHandle, false, ref set))
            {
                var ex = Marshal.GetLastWin32Error();
                Console.WriteLine("Set error " + ex);
                throw new System.ComponentModel.Win32Exception(ex);
            }

            FontInfo after = new FontInfo
            {
                cbSize = Marshal.SizeOf<FontInfo>()
            };
            GetCurrentConsoleFontEx(ConsoleOutputHandle, false, ref after);

            return new[] { before, set, after };
        }
        else
        {
            var er = Marshal.GetLastWin32Error();
            Console.WriteLine("Get error " + er);
            throw new System.ComponentModel.Win32Exception(er);
        }
    }
}

You can play with it in a PowerShell window by using Add-Type with that code, and then doing something like this:

[ConsoleHelper]::SetCurrentFont("Consolas", 16)
[ConsoleHelper]::SetCurrentFont("Lucida Console", 12)

Then, use your console "Properties" dialog and manually switch to Lucida Sans Typewriter ... and try just changing the font size, specifying the same font name:

[ConsoleHelper]::SetCurrentFont("Lucida Sans Typewriter", 12)

And you'll get output like this (showing the three settings: before, what we tried, and what we got):

Set Current Font: Lucida Sans Typewriter

FontSize FontFamily FontWeight FontName
-------- ---------- ---------- --------
      14         54        400 Lucida Sans Typeʈ
      12         54        400 Lucida Sans Typewriter
      12         54        400 Courier New

You see that weird character on the end of the "before" value? That's happens whenever the font is longer than 16 characters (I'm getting garbage data because of a problem in the API or the marshalling).

The actual console font name is obviously not length limited, but perhaps it's not possible to use a font with a name that's 16 characters or longer?

For what it's worth, I discovered this problem with Fira Code Retina, a font that has exactly 16 characters in the name -- and I have a little bit more code than what's above in a gist here if you care to experiment...


Solution

  • I had found a bug in the Console API. It's fixed as of Windows 10 (insider) build 18267.

    Prior to that release, there wasn't a way around it -- except to use fonts with shorter names, or use the actual window properties panel to set it.

    The original post code works now ...