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
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
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;
}
Changing font name
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.