This is still the header! Main site

Oddly simple GUI programs

2024/05/04

A true Unix programmer will never consider creating a GUI app for anything. They consume too many resources, are slow, require lots of clicking, are really hard to get right, and they have a lot of library dependencies.

The rest of the world disagrees. Thus, they typically create an Electron app, hereby doing a really good job proving all the points the Unix people are making.

In this post, we show that GUI programming doesn't necessarily have to be a dependency hellhole creating huge trees of views and hundreds of lines of event handlers. So let's do the exact opposite of using the newest & trendiest javascript frameworks and...

... let's use win32

(This is also not the first proposal that hardcore Unix people would come up with.)

But then... isn't win32 the ugliest API ever created on Earth, with 17 parameters per function call, requiring you to fill out structures with all kinds of arcane values due to backwards compatibility reasons?

Well, fortunately, we are in 2024, so we can always ask our friends the proto-shoggoths to help us out with specific invocation details. It also helps that said friends are much better at dealing with relatively low level, slow-changing APIs and function calls, versus many layers of OOP abstractions that are different every year.

Also, if you are me, there is something cool about creating an actual Windows GUI app that looks exactly like the other cool Windows GUI apps from the early 2000s. Opinions might vary on this one though.

As an example, we are going to create a little application to switch lights in our apartment on and off... just to give you a flavor of how this looks like.

How is this simple again?

Typically, Win32 tutorials start with a 20-line piece of code just to create an empty window. Then, if you think Visual C++ might make things simpler, just wait until it generates you a starter project which already has way more code than you ever thought would be needed for this purpose.

So we are going to use exactly none of that.

The nice thing about this era of computing, though, is that it happened before we tried to come up with general, scalable, customizable, tweakable, and universal UI markup languages that increasingly resemble HTML and CSS. Instead, we can use a UI editor that we click buttons into. For this purpose, just to avoid relying on too many tools supplied by Microsoft, we are going to use a tool called RisohEditor, by Katayama Hirofumi MZ, of ReactOS fame.

adding a dialog

As it happens, in Win32, a dialog box with all its buttons and text fields is a "resource", just like the icons in DLLs that you might be more familiar with. Just like with a lot of other things, they are looked up by their numerical IDs. First, let's create an ID for our dialog box.

adding a dialog

This will also prompt you to save a file called resource.h, which you can then include from your code so that you can use these resource IDs from it. After creating another ID for a button, it basically looks like this:

#define IDC_RANDOM_BUTTON                   100

#define IDD_TESTDIALOG                      100
          

We can then go on editing our dialog box...

by adding a button:

As a result now we have two files: the aforementioned header, and a resource file which is mostly just a list of what controls are in your dialog box. Its interesting portions are something like this:


IDD_TESTDIALOG DIALOG 0, 0, 215, 140
CAPTION "Sample Dialog"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS Shell Dlg"
{
    DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
    PUSHBUTTON "Cancel", IDCANCEL, 115, 115, 60, 14
    PUSHBUTTON "Here is a random button", IDC_RANDOM_BUTTON, 61, 46, 95, 21
}
          

(Quick reminder: we still haven't written a single line of code. All of this is fairly editable by hand, but it has been generated automatically so far.)

As for the code itself, our main function will contain a single line of code. We just show a dialog box. ("IDD_TESTDIALOG" is the ID we gave it earlier.)

#include <windows.h>

#include "resource.h"

// so that we link to the GUI library
#pragma comment(lib, "user32.lib")

INT WINAPI
WinMain(HINSTANCE   hInstance,
        HINSTANCE   hPrevInstance,
        LPSTR       lpCmdLine,
        INT         nCmdShow)
{
  DialogBox(hInstance, MAKEINTRESOURCE(IDD_TESTDIALOG), NULL, DialogProc);
}

The way we are passing an event handler is by giving it a function ("DialogProc" in our case) that will handle all the messages. Said messages will include ones that indicate the user clicking some of the buttons we created. (Note that, so far, no one is forcing us to subclass any silly framework class at all.)

The control IDs we are looking for are just the numbers in our resource.h header. This is why we gave an ID to each button: this way we know what button was clicked. (Look for "IDC_RANDOM_BUTTON".)

INT_PTR CALLBACK
DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
    {
    case WM_INITDIALOG:
      return TRUE;
    case WM_COMMAND:
      switch (LOWORD(wParam))
        {
        case IDC_RANDOM_BUTTON:
          // Do something interesting here instead of closing the dialog
          EndDialog(hwnd, IDCANCEL);
          break;
        case IDCANCEL:
          EndDialog(hwnd, IDCANCEL);
          break;
        }
    }
  return 0;
}

How do we build this?

All that we need is the Windows SDK. Or, you could use GCC to compile it instead. (Win64DevKit is a nice pre-packaged distribution of it.)

Assuming the former, we can accomplish this in a 3 line batch file; no actual build system needed. After some environment variable adjustments, we turn the resource file (which is still text), into a binary blob, then link it together with the program itself.

call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
rc /fo blog_example.res blog_example.rc
cl blog_example.cpp blog_example.res /I. /Fe:blog_example.exe /EHsc /Zi

Here is our end result:

the final result, with a button

... weren't we promised simplicity and light switches?

Admittedly, this is a bit of effort to create a completely useless program, even if it has a window.

On the other hand, here is something that is a lot more useful in practice: light switches as promised.

The nice thing is that we still don't have anything like a view tree or layout algorithms. Our dialog looks exactly as designed; we need no further knowledge of CSS selectors, markup languages, alignment, grids, floating divs, columns, flexbox, jquery, etc.

All we need to do for this to work is linking against libcurl and making a few requests.

void Fetch(const char* uri)
{
    CURL *curl;
    CURLcode res;
    curl = curl_easy_init();

    if (!curl) return;

    curl_easy_setopt(curl, CURLOPT_URL, uri);
    res = curl_easy_perform(curl);
    curl_easy_cleanup(curl);
}
          

Our dialog procedure now has parts like this:


(...)
    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDC_ALL_ON:
          Fetch("http://anarillis.ltn.simonsafar.com/lights/all_on.php");
          break;
        case IDC_ALL_OFF:
          Fetch("http://anarillis.ltn.simonsafar.com/lights/all_off.php");
          break;
(...)
          

(Obviously, we do need these endpoints to actually do something to the light switches; this is a topic for another post though.)

Platforms

So, um, we created yet another Windows-only program that will surely only run on this terrible, proprietary operating system, right?

the same program running on linux

As it turns out, our app runs perfectly on Linux under Wine, even with weird esoteric window managers that might get their own post eventually. In fact, it's probably more portable than binaries compiled for actual Linux, given all the library dependencies on GTK or Qt.

But then also.. here it is on ReactOS:

same app, running on reactos

(... minus libcurl, which wasn't happy about NT 5 era APIs, but... the GUI is there! On an open source OS, no less.)

... comments welcome, either in email or on the (eventual) Mastodon post on Fosstodon.