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.