Saturday, July 14, 2018

A peek at the old win32 APIs

I wanted to write a simple GUI for HKCU\Control Panel\Desktop\WindowMetrics key. Until now Windows had a dialog that allowed fonts/colours/etc tweaking for the current 'theme', but as soon as the name of w10 became synonymous with all that is terrible, the dialog was removed in r1703.

What's the easiest way to write a similar dialog? I don't mind the default colours but I don't like the default font sizes. What if we could draw fake controls using html & provide a simple form to modify the appearance of the elements in the RT? It feels like a trivial task.

Obviously we would need a way to do get/set operations on the registry. What options do we have?

I didn't want to employ Electron. That one time I've tried to write a commercial peace of software with it, it brought me nothing but irritation. It's also a great way to become a mockery on HN/reddit: every time some pour soul posts about their little app written in JS+Electron, they receive so much shit from the 'real' programmers for not writing it in QT or something native to the platform, that most readers immediately start feeling sorry for the OP.

If not Electron then what? The 'new' Microsoft way is to use UWP API, that strongly reminds me of the Electron concept, only done in a vapid style. I didn't want to touch it with a 3.048 m pole.

What about plain Node + a browser? We can write a server in node that communicates with the registry & binds to a localhost address, to which a user connects using their favourite browser. Except that perhaps it could be too tedious for the user to run 2 programs in a consecutive manner + Node itself isn't installed on Windows by default.

32bit node.exe binary is < 10MB zipped, so shipping it with the app isn't a big deal. To simplify the program startup we could write a C wrapper that starts the node server & opens the default browser, except that there's no need to, for w10 still comes with Windows Scripting Host! God almighty, WSH is awful: its JScript is a subset of ES3 with interesting constructs like env("foo") = "bar" (this is not a joke) & virtually every WSH resource describes it using VBScript for reasons unbeknown to the mankind. Nevertheless, for a tiny wrapper it's an acceptable choice, especially if you manage not to puke while reading the antediluvian VBScript examples.

Fonts

The values inside HKCU\Control Panel\Desktop\WindowMetrics key that correspond to font options, like MenuFont, are binary blobs:

$ reg query "HKCU\Control Panel\Desktop\WindowMetrics" /v MenuFont

HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics
MenuFont REG_BINARY F1FFFFFF0000000000000000000000009001000000000001000000005300650067006F006500200055004900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

People say the blobs are serialized Logfont structures. Apparently, MS guys have decided it's a safe bet, for the structure doesn't contain pointers.

Can we decode it properly in a browser using JS? I leave such a strive to another enterprising soul. But we can write a tiny C program that displays some 'standard' Windows font dialog. The idea is: a user clicks on a button in the browser, the browser sends a request to the node server.js that runs my-font-dialog.exe that shows the font dialog & prints to the stdout a deciphered Logfont structure + the hex encoded Logfont. Then the server sends the obtained data to the browser, that, in turn, updates the fake html controls.

It's not actually a logfont

If you don't know any winapi, how hard is to write my-font-dialog.exe? Can a non-console Windows program write to the stdout? Or should I use some kind of IPC if it cannot? The more I thought about downloading the Windows SDK the more I regarded the whole endeavour with aversion.

Then it dawned on me that I can take a shortcut & utilise Cygwin! It has w32api-runtime & w32api-headers packages, its C library has all the friendly helpers like readline(). Its 64bit build even has 32bit version of gcc (cygwin32-gcc-core, cygwin32-binutils, etc), thus we can create on a 64bit machine a 32bit executable. E.g., having a simple Makefile:

target := i686-pc-cygwin
CC := $(target)-gcc
LD := $(target)-ld
LDFLAGS := -mwindows -mconsole

& foo.c file, typing

$ make foo

produces such a foo, that can invoke Windows gui routines & simultaneously can access the stdin/stdout. The resulting exe depends on a 32bit cygwin1.dll but while we ship it with the app, nobody cares.

To display an ancient Windows font dialog box, we ought to call ChooseFont() fn that fills a special CHOOSEFONT structure. One field of that structure is the desired Logfont.

$ echo F4FFFFFF000000000000000000000000900100000000000100000500530065006700 6F006500200055004900000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000 | _out.i686-pc-cygwin/app/cgi-bin/choosefont.exe

If a user click OK, the choosefont.exe util prints:

F1FFFFFF00000000000000000000000090010000000000010302014249006E006B0020004600720065006500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,Ink Free,400,0,11.250000,15

When I converted the Logfont value into a hex string I noticed that although it looked decidedly similar to the values in the registry, it wasn't the same: the blob was shorter. It was either an obscure Unicode issue with the toolchain or I didn't know how to computer or both. Have I mondegreened about the Logfont? Turns out that not everything you read on the web is accurate: it's not Logfont, it's Logfontw! I don't see a point of describing the reason why or what macro you should define to get Anythingw automatically, for Windows programmers are already laughing at me. Evidently, this is what you get for avoiding Visual Studio.

System logger

Being naïve, I thought of sending errors from server.js to the Windows event log. Ain't logs often useful? What is their syslog(3) here? ReportEvent()? Alrighty, we'll write a small C util to aid server.js!

facepalm.jpg

I was going to painstakingly describe the steps one must take to obtain the permissions for the proper writing to the event log, but ultimately deleted all the corresponding code & decided to stay with console.error. To get a sense for a gallant defence Windows makes before permitting anyone to write to its hallowed log storage, read these 2 SO answers:

Your pixels are not my pixels

At some point I wanted to show the current DPI to the user. There are ways to do it in a browser without external help, but they are unreliable in edge cases. Winapi has GetDpiForMonitor().

You think it's a piece of cake, until you play with w10 'custom scaling' feature. For some reasons it allows only to specify a relative value.

In

p = (d/c) * 100

p is the %-value in the Windows custom scaling input field, c is the current dpi, d–the desired one.

Say your initial dpi is 96 & you want to upgrade to 120, then p = 125 = (120/96) * 100. For the desired 144 dpi, p = 150.

This all breaks if d fails to hit one of the 'standard' Windows DPIs (120, 144). In such cases GetDpiForMonitor() fn yields 96.

The result

Download the full, pre-compiled app from the github page.

No comments:

Post a Comment