Changing font size in Windows dialog in C++
Changing font size in Windows dialog in C++

How to dynamically change font size in a Windows dialog

	
	




Windows’s win32 API is old and crufty. Many things that are trivial to do in HTML are difficult in win32.

One of those things is changing size of font used by your native, desktop app.

I encountered this in SumatraPDF. A user asked for a way to increase the font size.

I introduced UIFontSize option but implementing that was difficult and time consuming.

One of the issues was changing the font size used in dialogs.



How dialogs work

	
	




SumatraPDF defines a bunch of dialogs in SumatraPDF.rc.

Here’s a find dialog:
IDD_DIALOG_FIND DIALOGEX 0, 0, 247, 52
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Find"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    LTEXT           "&Find what:",IDC_STATIC,6,8,60,9
    EDITTEXT        IDC_FIND_EDIT,66,6,120,13,ES_AUTOHSCROLL
    CONTROL         "&Match case",IDC_MATCH_CASE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,24,180,9
    LTEXT           "Hint: Use the F3 key for finding again",IDC_FIND_NEXT_HINT,6,37,180,9,WS_DISABLED
    DEFPUSHBUTTON   "Find",IDOK,191,6,50,14
    PUSHBUTTON      "Cancel",IDCANCEL,191,24,50,14
END

.rc is compiled by a resource compiler rc.exe and embedded in resources section of a PE .exe file.

Compiled version is a binary blob that has a stable format.

At runtime we can get that binary blob from resources and pass it to DialogBoxIndirectParam() function to create a dialog.

How to change font size of a dialog at runtime

	
	




DIALOGEX tell us it’s an extended dialog, which has different binary layout than non-extended DIALOG.

As you can see part of dialog definition is a font definition:
FONT 8, "MS Shell Dlg", 400, 0, 0x1

To provide a FONT you also need to specify DS_SETFONT or DS_FIXEDSYS flag.

We’re asking for MS Shell Dlg font with size of 8 points (12 pixels). 400 specifies standard weight (800 would be bold font).

Unfortunately the binary blob is generated at compilation time and we want to change font size when application runs.

The simplest way to achieve that is to patch the binary blob in memory.

The code for changing dialog font size at runtime

	
	




Full code is here



In C++ this is represented by the following struct:
#pragma pack(push, 1)
struct DLGTEMPLATEEX {
    WORD dlgVer;    // 0x0001
    WORD signature; // 0xFFFF
    DWORD helpID;
    DWORD exStyle;
    DWORD style;
    WORD cDlgItems;
    short x, y, cx, cy;
    /*
    sz_Or_Ord menu;
    sz_Or_Ord windowClass;
    WCHAR     title[titleLen];
    WORD      fontPointSize;
    WORD      fontWWeight;
    BYTE      fontIsItalic;
    BYTE      fontCharset;
    WCHAR     typeface[stringLen];
    */
};
#pragma pack(pop)

#pragma pack(push, 1) tells C++ compiler to not do padding between struct members.

That part after x, y, cx, cy is commented out because sz_or_Ord and WCHAR [] are variable length, which can’t be represented in C++ struct.

fontPointSize is the value we need to patch.

But first we need to get a copy binary blob.
DLGTEMPLATE* DupTemplate(int dlgId) {
    HRSRC dialogRC = FindResourceW(nullptr, MAKEINTRESOURCE(dlgId), RT_DIALOG);
    CrashIf(!dialogRC);
    HGLOBAL dlgTemplate = LoadResource(nullptr, dialogRC);
    CrashIf(!dlgTemplate);
    void* orig = LockResource(dlgTemplate);
    size_t size = SizeofResource(nullptr, dialogRC);
    CrashIf(size == 0);
    DLGTEMPLATE* ret = (DLGTEMPLATE*)memdup(orig, size);
    UnlockResource(orig);
    return ret;
}

dlgId is from .rc file (e.g. IDD_DIALOG_FIND for our find dialog). Most of it is win32 APIs, memdup() makes a copy of memory block.

Here’s the code to patch the font size:
static void SetDlgTemplateExFont(DLGTEMPLATE* tmp, int fontSize) {
    CrashIf(!IsDlgTemplateEx(tmp));
    DLGTEMPLATEEX* tpl = (DLGTEMPLATEEX*)tmp;
    CrashIf(!HasDlgTemplateExFont(tpl));
    u8* d = (u8*)tpl;
    d += sizeof(DLGTEMPLATEEX);
    // sz_Or_Ord menu
    d = SkipSzOrOrd(d);
    // sz_Or_Ord windowClass;
    d = SkipSzOrOrd(d);
    // WCHAR[] title
    d = SkipSz(d);
    // WCHAR pointSize;
    WORD* wd = (WORD*)d;
    fontSize = ToFontPointSize(fontSize);
    *wd = fontSize;
}

We start at the end of fixed-size portion of the blob () d += sizeof(DLGTEMPLATEEX).

We then skip variable-length fields menu, windowClass and title and patch the font size in points.

SumatraPDF code operates in pixels so has to convert that to Windows points:
static int ToFontPointSize(int fontSize) {
    int res = (fontSize * 72) / 96;
    return res;
}

Here’s how we skip past sz_or_Ord fields:
/*
Type: sz_Or_Ord

Variable-length array of 16-bit representing an id of resource (e.g. menu resource).
First value:
- 0 : no resource
- 0xffff : next 16-bit value is id of resource in .exe
- any other value : this is a utf-16, zero-terminated string that is a name
                    of resource in .exe
*/
static u8* SkipSzOrOrd(u8* d) {
    WORD* pw = (WORD*)d;
    WORD w = *pw++;
    if (w == 0x0000) {
        // no menu
    } else if (w == 0xffff) {
        // menu id followed by another WORD item
        pw++;
    } else {
        // anything else: zero-terminated WCHAR*
        WCHAR* s = (WCHAR*)pw;
        while (*s) {
            s++;
        }
        s++;
        pw = (WORD*)s;
    }
    return (u8*)pw;
}

Strings are zero-terminated utf-16:
static u8* SkipSz(u8* d) {
    WCHAR* s = (WCHAR*)d;
    while (*s) {
        s++;
    }
    s++; // skip terminating zero
    return (u8*)s;
}

To make the code more robust, we check the dialog is extended and has font information to patch:
static bool IsDlgTemplateEx(DLGTEMPLATE* tpl) {
    return tpl->style == MAKELONG(0x0001, 0xFFFF);
}

static bool HasDlgTemplateExFont(DLGTEMPLATEEX* tpl) {
    DWORD style = tpl->style & (DS_SETFONT | DS_FIXEDSYS);
    return style != 0;
}



It’s also possible to change font name but it’s slightly harder (which is why I didn’t implement it).

WCHAR typeface[] is inline null-terminated string that is name of the font.

To change it we would also have to move the data that follows it.

The roads not taken

	
	




There are other ways to achieve that.

Dialog is just a HWND. In WM_INITDIALOG message we could iterate over all controls, change their font with WM_SETFONT message and then resize the controls and the window.

That’s much more work than our solution. We just patch the font size and let Windows do the font setting and resizing.

Another option would be to generate binary blog representing dialogs at runtime. It would require writing more code but then we could define new dialogs in C++ code that wouldn’t be that much different than .rc syntax.

I want to explore that solution because this would also allow adding simple layout system to simplify definition the dialogs.

In .rc files everything must be absolutely positioned. The visual dialog editor helps a bit but is unreliable and I need resizing logic anyway because after translating strings absolute positioning doesn’t work.

