A modern, event-driven take on Win32 GUIs

About PureForms


What is PureForms?

PureForms is a “wrapper” around the Win32 GUI APIs for creating windows and responding to window messages. Whereas in Win32, many GUI elements are though of as windows and window messages are often overloaded and opaque, PureForms aims to have clear separation between forms, controls, and other UI elements and uses an event driven architecture for callbacks. If you have worked with WinForms in the past, PureForms should seem fairly familiar. If you have not, I am confident you will still be able to catch on quite quickly.

The below code is all it takes to get a window on the screen with PureForms:

#include "PureForms.h"
int WINAPI wWinMain(
	_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ PWSTR args,
	_In_ int showCommand
)
{
	Form* frmMain = createForm(
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		300,
		170,
		L"My Form"
	);
	assert(frmMain != NULL);
	showForm(frmMain, showCommand);
}

How is PureForms Implemented?

PureForms is implemented as a single C source file with a single header. The header contains structs, functions, and other data structures that are meant to be used by an application implemented using PureForms. The backing functions that facilitate PureForms’ functionality are defined within the C file and prefixed with “private” to indicate that they should not be used externally.

// An example of a “private” function to
// subclass a particular control for hover events
void private_registerSubclass(Control* control)
{
	BOOL result = SetWindowSubclass(
		control->hWnd,
		(SUBCLASSPROC) private_subclassProc,
		PRIVATE_SUBCLASS_ID,
		(DWORD_PTR) control
	);
	assert(result);
}

PureForms attempts to mimic the useful concept of classes from object-oriented languages like c#. It is very important to note that this is a surface-level imitation and is not intended to imply other object-oriented functionality. This is done using structs. The “form” is a struct with no “parent class”, therefore all fields are implemented inside that struct. Controls such as button have a “base class” of control, that being another struct, that is their first member and holds common fields such as position, size, and their HWNDs. This allows us to reuse the same event handler struct and function pointers for all controls, and helps avoid copying/pasting the same fields for each control type. There is a helpful macro, getControl(), that retrieves the underlying Control* from a control.

// The getControl() macro
#define getControl(_a) &((_a)→control)

// The control “parent” struct
typedef struct structControl
{
    HWND hWnd;
    int x;
    int y;
    int width;
    int height;
    ControlEventHandlers eventHandlers;
    Tooltip tooltip;
} Control;

// A struct that “inherits” from control
typedef struct structButton
{
    Control control;
    int id;
    wchar* text;
} Button;

Event handlers for forms and controls are implemented in their respective eventHandler structs. This is to again allow reuse between different control types. Since there are many events that only make sense in the context of a form, such as onClose, the form “class” has its own separate event handlers defined. The eventHandler structs contained named function pointers intended to be used for the event they are named after, for example, “onHover.” They share a common function pointer definition, which includes a “this” argument containing the affected form or control, and a void* intended to hold an eventData struct containing pertinent information, or simply NULL.

// The control event handler function prototype
typedef void (*ControlEventHandler) (Control* this, void* eventData);

// Struct containing control event handlers
typedef struct structControlEventHandlers
{
    ControlEventHandler OnClick;
    ControlEventHandler OnHover;
} ControlEventHandlers;

// Function to register an event handler callback to a particular control
void addControlEventHandler(
    Control* control, 
    ControlEvent event,
    ControlEventHandler eventHandler
);

// An example of an eventData structure that would be passed to an event handler
typedef struct structEventDataOnClick
{
    wchar* text;
} EventData_OnClick;

Like Win32 windows being carved out into more helpful and clear “objects”, window messages are also abstracted into events. This does away with the user of PureForms needing to worry about a window procedure, and all of the complex window procedure minutiae. Instead, the user need only “register” for those events that they care about by adding an event handler (callback function). PureForms will check if an event is “registered” by whether or not the function pointer for that form or control’s event is null. If so, the event is simply ignored. If not, the appropriate eventData struct is created and the event handler is called. This mimics the WinForms notion of only needing to write code for the events that you want to respond to.

// PureForms user code to register an event handler
addControlEventHandler(
	getControl(btnFirst),
	ControlEvent_OnClick,
	button_OnClick
);

// Example of an event handler implementation
void button_OnClick(Form* this, void* eventData)
{
	EventData_OnClick* data = (EventData_OnClick*) eventData;
	OutputDebugStringW(data->text);
}

How do I use PureForms?

A PureForms application can be thought of as having two “phases”. In the first “phase”, we set up the form and any controls that we would like the form to have. Note that form is analogous to what we typically think of as a window. We can also add any bitmaps we would like, and associate tooltips with controls. In the second “phase”, we show the form and listen for events. Events are actions taken by Windows, the user, or even the program itself that we would like to respond to via a callback function. Common events include clicking a button, hovering over something, or closing the form. All we have to worry about is hooking up our “event handlers” or callback functions for the events that we care about, and implementing those functions. There is no need to worry about particular window messages, or even the message loop as a whole.

// Start “phase one” by building our form and registering event handlers
Form* frmMain = createForm(
	CW_USEDEFAULT,
	CW_USEDEFAULT,
	300,
	170,
	L"My Form"
);
assert(frmMain != NULL);
addFormEventHandler(frmMain, FormEvent_OnClick, form_OnClick);
addFormEventHandler(frmMain, FormEvent_OnClose, form_OnClose);
Button* btnFirst = createButton(
	90,
	10,
	150,
	30,
	L"Reinvent wheel",
	true
);
addControlEventHandler(
	getControl(btnFirst),
	ControlEvent_OnClick,
	button_OnClick
);

// End “phase one” by showing our form, begin “phase two”
showForm(frmMain, showCommand);

When an event that we have registered an event handler for occurs, the event handler will be called with two arguments. The first is a pointer to the control or form that generated the event, and the second is a struct that corresponds to that event if such a struct exists, or NULL if not. This is referred to as the “event data”, and is passed in a void* to allow easier function pointer reuse. We can use this structure both to receive relevant info from PureForms, and to send information back to PureForms. For example, in the eventData_OnClose struct, there is a shouldClose boolean that the user can set to tell PureForms whether the form and all associated resources should be destroyed.

// A “form_onClose” event handler that verifies if the user really wants to quit
void form_OnClose(Form* this, void* eventData)
{
	EventData_OnClose* data = (EventData_OnClose*) eventData;
	int option = MessageBoxW(
		this->hWnd,
		L"Are you sure?",
		L"Closing",
		MB_YESNO | MB_ICONQUESTION
	);

	// Communicate back to PureForms whether or not the form should close
	data->shouldClose = (option == IDYES) ? true : false;
}

As alluded to above, the application’s lifetime is tied explicitly to the form’s lifetime. When the form opens, we begin listening for window messages, and when the form closes, we likewise deallocate all resources and end the program. This avoids the confusing notion of the “message loop”, although it does mimic it to some extent.

What are the current limitations of PureForms?

  • At this time, all layout needs to be done manually by the user. In the future, I would like to implement some kind of automatic layout functionality, but in the time frame of the jam this was not possible.

  • PureForms is not DPI-aware and does not guarantee that things will look proper on different DPI.

  • The only controls currently available are buttons and bitmaps.

  • There are a lot of global variables and hardcoded data. This was a jam project, after all.

Read more
Filters

Recent Activity

&pureforms
With the jam project coming to a close, we created the first PureForms app and what may very well be the worst reinvention of a wheel ever: A color picker that only works in increments of 16 and doesn't even give you the RGB value of the color you've created. But it does show you pretty tool tips, badger you about closing it, and take up only ~150 LOC. Be sure to check out PureForms in its entirety at https://github.com/errorsuccessdev/PureForms. I've also written up an overview of what this project is and what it's for over at https://handmade.network/p/592/pureforms/. I plan to continue working on this library post-jam, so if easy Win32 GUI dev is appealing to you, keep an eye out!
https://i.imgur.com/uNtPskz.gif

View original message on Discord

&pureforms
This morning we knocked out event handlers and added my correctly positioned™️ tooltips! PureForms allows you to create this GUI and receive click/hover/close events in ~100 LOC:
https://i.imgur.com/SXTpOSy.gif
The full code is available on my GitHub at https://github.com/errorsuccessdev/PureForms.

View original message on Discord

I do get the DM. My post is tagged with &pureforms at the top...

View original message on Discord

&pureforms
Not much of an update today as I spent the morning prepping for and doing a job interview (which I think went well 🤞). I did, however, rough in a way for us to keep track of all the buttons. What could possibly go wrong?

typedef struct structPrivateButtonList 
{
    Button* button;
    struct structPrivateButtonList* next;
} private_ButtonList;

// ...

void private_addButtonToList(Button* button)
{
    private_ButtonList* newListEnd = (private_ButtonList*) HeapAlloc(
        GetProcessHeap(),
        HEAP_ZERO_MEMORY,
        sizeof(private_ButtonList)
    );
    assert(newListEnd);
    newListEnd->button = button;
    newListEnd->next = NULL;
    if (global_firstButton == NULL)
    {
        global_firstButton = newListEnd;
    }
    else
    {
        private_ButtonList* listEnd = global_firstButton;
        while (listEnd->next != NULL)
        {
            listEnd = listEnd->next;
        }
        listEnd->next = newListEnd;
    }
}
View original message on Discord

&pureforms
We are off to a great start! This code + PureForms magic is all it takes to create the window (or "form") shown in the screenshot. The repo is already on github here if you'd like to take a peek behind the scenes: https://github.com/errorsuccessdev/PureForms

#include "PureForms.h"

int WINAPI wWinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ PWSTR args,
    _In_ int showCommand
)
{
    Form* frmMain = createForm(
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        L"My Form"
    );
    assert(frmMain != NULL);

    Button* btnFirst = createButton(
        10,
        10,
        200,
        30,
        L"Click me!"
    );

    Button* btnSecond = createButton(
        10,
        50,
        200,
        30,
        L"No, click me instead!"
    );

    bool result = showForm(frmMain, showCommand);
    assert(result);
}```
View original message on Discord