Native Win32 GUI for Python — powered by NASM x86-64 Assembly
Project description
WinGUI
Native Win32 GUI for Python — powered by NASM x86-64 Assembly
WinGUI is a zero-dependency Python GUI framework that drives the Win32 API directly from hand-written NASM x86-64 Assembly. There is no Tkinter, no Qt, no Electron — just a thin ctypes shim over a compiled DLL.
Table of Contents
- Features
- Architecture
- Requirements
- Installation
- Quick Start
- API Reference
- Examples
- 01 — Minimal window
- 02 — Single button
- 03 — Multiple buttons
- 04 — Labels
- 05 — Text input
- 06 — Multi-field form
- 07 — Dynamic title
- 08 — Counter app
- 09 — Calculator
- 10 — Toggle button
- 11 — Live label update
- 12 — Note-taking app
- 13 — Unicode
- 14 — Multiple callbacks
- 15 — Notification filtering
- 16 — Shared state
- 17 — Programmatic close
- 18 — Raw set_callback
- Running the Example Gallery
- Diagnostics
- Project Structure
- Build Reference
- Design Notes
- License
Features
- Pure Win32 — no third-party GUI runtime required
- NASM x86-64 Assembly core — all GUI logic is in
wingui32.asm; Python is only a caller - Modern UI out of the box — ComCtl32 v6 Visual Styles, Segoe UI 10 pt ClearType, DPI-aware, themed
#F3F3F3background - Full Unicode — UTF-8 in, UTF-16LE in the DLL via
MultiByteToWideChar; CJK, Arabic, emoji all work - Zero dependencies — only Python's standard library (
ctypes) and the compiled DLL - Two API styles — OOP (
WinGUIclass) or flat module functions - Context manager —
with WinGUI() as gui:frees GDI handles automatically - Decorator-based events —
@gui.on_command(control_id=1)with optional notification-code filtering - Multiple handlers per control — register as many
@on_commandcallbacks as you like on the same ID
Architecture
Python script
│
│ ctypes (zero-copy, ABI-safe)
▼
wingui.py ───────────────── pure shim, no GUI logic
│
│ LoadLibrary / function pointers
▼
wingui32.dll (NASM x86-64 Assembly)
│
│ Win32 API calls (W-variants, UTF-16LE)
▼
user32.dll · kernel32.dll · gdi32.dll · comctl32.dll
The DLL exposes ten C-callable functions:
| Export | Description |
|---|---|
create_window |
Register class, init modern UI, CreateWindowExW |
show_window |
ShowWindow + UpdateWindow |
run_message_loop |
GetMessageW / TranslateMessage / DispatchMessageW |
create_button |
CreateWindowExW("BUTTON") + WM_SETFONT |
create_label |
CreateWindowExW("STATIC") + WM_SETFONT |
create_textbox |
CreateWindowExW("EDIT") + WM_SETFONT |
set_window_title |
SetWindowTextW on the main window |
show_message_box |
MessageBoxW |
close_window |
DestroyWindow + PostQuitMessage(0) + GDI cleanup |
set_callback |
Install a WINFUNCTYPE pointer for WM_COMMAND dispatch |
Requirements
| Component | Minimum version |
|---|---|
| Windows | 10 (1703+) or Windows 11 |
| Python | 3.10 (64-bit) |
| NASM | 2.15+ (to rebuild the DLL) |
| GCC (MinGW-w64) | 12+ or MSVC 2019+ (to link) |
Important: Python and the DLL must both be 64-bit. A 32-bit Python will fail to load
wingui32.dll.
Installation
Build the DLL
The pre-built binary lives in bin/wingui32.dll. To rebuild from source:
MSYS2 / MinGW-w64
cd asm
nasm -f win64 wingui32.asm -o wingui32.obj
gcc -shared -o ../bin/wingui32.dll wingui32.obj \
-luser32 -lkernel32 -lgdi32 -lcomctl32
Or use the included batch file from the project root:
build.bat
MSVC (Developer Command Prompt)
cd asm
nasm -f win64 wingui32.asm -o wingui32.obj
link /DLL /OUT:..\bin\wingui32.dll ^
/EXPORT:create_window /EXPORT:show_window ^
/EXPORT:run_message_loop /EXPORT:create_button ^
/EXPORT:create_label /EXPORT:create_textbox ^
/EXPORT:set_window_title /EXPORT:show_message_box ^
/EXPORT:close_window /EXPORT:set_callback ^
wingui32.obj user32.lib kernel32.lib gdi32.lib comctl32.lib
Install the package
Install in editable mode from the project root:
pip install -e .
Or copy the wingui/ directory and bin/wingui32.dll next to your script and use it directly.
Verify the installation
python -m wingui --check
Expected output:
✓ wingui32.dll loaded successfully
Path: D:\...\bin\wingui32.dll
Quick Start
OOP Style
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd):
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
with WinGUI() as gui:
hwnd = gui.create_window(640, 360, "Hello — 你好 🌍")
gui.create_label (hwnd, 20, 20, 300, 22, "Enter your name:")
txt = gui.create_textbox(hwnd, 20, 50, 300, 28)
gui.create_button (hwnd, 20, 96, 130, 36, "Say Hello", control_id=1)
gui.create_button (hwnd, 165, 96, 100, 36, "Quit", control_id=99)
@gui.on_command(control_id=1)
def on_hello(hwnd, ctrl_id, notif, ctrl_hwnd):
name = read(txt) or "stranger"
gui.show_message_box(f"Hello, {name}! 👋", "Greeting")
@gui.on_command(control_id=99)
def on_quit(hwnd, ctrl_id, notif, ctrl_hwnd):
gui.close_window(hwnd)
gui.run_message_loop()
Flat Module API
import wingui
hwnd = wingui.create_window(480, 200, "Flat API Demo")
wingui.create_button(hwnd, 20, 60, 120, 35, "Click Me", control_id=1)
@wingui.on_command(control_id=1)
def on_click(hwnd, ctrl_id, notif, ctrl_hwnd):
wingui.show_message_box("It works!", "Info")
wingui.run_message_loop()
Context Manager
from wingui import WinGUI
with WinGUI() as gui:
hwnd = gui.create_window(400, 200, "Context Manager")
gui.create_button(hwnd, 150, 80, 100, 35, "Close", control_id=1)
@gui.on_command(control_id=1)
def on_close(hwnd, ctrl_id, notif, ctrl_hwnd):
gui.close_window(hwnd)
gui.run_message_loop()
# __exit__ is called here — font and brush handles are freed automatically
API Reference
WinGUI class
Constructor
WinGUI(dll_path: str | None = None)
Loads wingui32.dll. Pass an explicit path or let the loader search next to wingui.py and in ../bin/.
Window lifecycle
| Method | Signature | Description |
|---|---|---|
create_window |
(width, height, title) -> int |
Create and show the main window. Returns HWND. |
show_window |
(hwnd=None) |
ShowWindow + UpdateWindow. Usually not needed — create_window already shows the window. |
run_message_loop |
(threaded=False) |
Block on the Win32 message pump until the window closes. |
close_window |
(hwnd=None) |
DestroyWindow → PostQuitMessage(0) + GDI cleanup. |
set_window_title |
(hwnd, title) |
Update the title bar at runtime. |
Control creation
All control methods return the child HWND as a plain int. Pass this value to user32.GetWindowTextW / SetWindowTextW to read/write content at runtime.
| Method | Signature | Description |
|---|---|---|
create_button |
(parent, x, y, width, height, text, control_id=0) -> int |
Push-button. control_id appears as ctrl_id in callbacks. |
create_label |
(parent, x, y, width, height, text) -> int |
Read-only STATIC control. Text colour #1A1A1A, background #F3F3F3. |
create_textbox |
(parent, x, y, width, height) -> int |
Single-line EDIT control with ES_AUTOHSCROLL. Read/write via Win32 directly. |
Dialogs
| Method | Signature | Description |
|---|---|---|
show_message_box |
(text, caption="Info") -> int |
Modal MessageBoxW. Returns IDOK=1, IDCANCEL=2, etc. |
Event system
@gui.on_command(control_id=None, notif=None)
def handler(hwnd, ctrl_id, notif, ctrl_hwnd):
...
| Parameter | Type | Description |
|---|---|---|
control_id |
int | None |
Filter to one control. None catches all controls. |
notif |
int | None |
Filter to one notification code. BN_CLICKED = 0 for buttons. None passes all codes. |
Multiple decorators on the same control_id are allowed and called in registration order. A handler that raises an exception does not block the others — the traceback is printed and execution continues.
gui.set_callback(fn)
Lower-level alternative: installs a single raw WM_COMMAND handler that receives every event. Using @on_command is preferred.
Context manager
with WinGUI() as gui:
...
__exit__ calls close_window and frees the Segoe UI font and background brush GDI handles. Exceptions inside the with block are never suppressed.
Flat API
Every method on WinGUI has a module-level twin that operates on an implicit singleton instance:
import wingui
wingui.create_window(width, height, title)
wingui.show_window(hwnd=None)
wingui.run_message_loop(threaded=False)
wingui.create_button(parent, x, y, w, h, text, control_id=0)
wingui.create_label(parent, x, y, w, h, text)
wingui.create_textbox(parent, x, y, w, h)
wingui.set_window_title(hwnd, title)
wingui.show_message_box(text, caption="Info")
wingui.close_window(hwnd=None)
wingui.set_callback(fn)
wingui.on_command(control_id=None, notif=None)
Reset the singleton between independent uses:
import wingui as _w
_w._instance = None
Callback signature
def handler(hwnd: int, ctrl_id: int, notif: int, ctrl_hwnd: int) -> None:
...
| Parameter | Description |
|---|---|
hwnd |
Parent window HWND |
ctrl_id |
Control identifier (the control_id you passed to create_button) |
notif |
Notification code — BN_CLICKED = 0 for button clicks |
ctrl_hwnd |
Child control HWND |
The type alias CommandCallbackType is exported for annotations:
from wingui import CommandCallbackType
def my_handler(hwnd, ctrl_id, notif, ctrl_hwnd) -> None: ...
cb: CommandCallbackType = CommandCallbackType(my_handler)
Examples
All 22 examples are runnable via the gallery:
python -m wingui --examples
# or
python example.py
Reading and writing control text
Most examples use these two small helpers:
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd: int) -> str:
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
def write(hwnd: int, text: str) -> None:
_user32.SetWindowTextW(hwnd, text)
01 — Minimal window
Open a blank window. The simplest possible WinGUI program.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(400, 300, "Minimal Window")
gui.run_message_loop()
02 — Single button
Click the button to show a message box.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(400, 200, "Single Button")
gui.create_button(hwnd, x=150, y=80, width=100, height=35,
text="Click Me", control_id=1)
@gui.on_command(control_id=1)
def on_click(hwnd, ctrl_id, notif, ctrl_hwnd):
gui.show_message_box("You clicked the button!", "Hello")
gui.run_message_loop()
03 — Multiple buttons
Three buttons with a single catch-all handler dispatching by ctrl_id.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(420, 200, "Multiple Buttons")
gui.create_button(hwnd, 20, 80, 110, 35, "Red", control_id=1)
gui.create_button(hwnd, 155, 80, 110, 35, "Green", control_id=2)
gui.create_button(hwnd, 290, 80, 110, 35, "Blue", control_id=3)
colours = {1: "Red 🔴", 2: "Green 🟢", 3: "Blue 🔵"}
@gui.on_command() # no control_id → catches ALL buttons
def on_any(hwnd, ctrl_id, notif, ctrl_hwnd):
name = colours.get(ctrl_id, f"Unknown (id={ctrl_id})")
gui.show_message_box(f"You chose: {name}", "Colour Picker")
gui.run_message_loop()
04 — Labels
Six STATIC text labels at various positions.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(440, 320, "Labels")
gui.create_label(hwnd, 20, 20, 400, 22, "This is a label at the top.")
gui.create_label(hwnd, 20, 60, 400, 22, "Labels use the Win32 STATIC class.")
gui.create_label(hwnd, 20, 100, 400, 22, "Rendered with Segoe UI and modern colours.")
gui.create_label(hwnd, 20, 140, 200, 22, "Left column")
gui.create_label(hwnd, 230, 140, 190, 22, "Right column")
gui.create_label(hwnd, 20, 240, 400, 22, "Unicode: 你好 • مرحبا • こんにちは • 🌍")
gui.run_message_loop()
05 — Text input
Read from an EDIT control when Submit is clicked.
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd):
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
gui = WinGUI()
hwnd = gui.create_window(440, 220, "Text Input")
gui.create_label (hwnd, 20, 20, 400, 22, "Type something and press Submit:")
txt = gui.create_textbox(hwnd, 20, 50, 400, 28)
gui.create_button (hwnd, 160, 96, 110, 35, "Submit", control_id=1)
@gui.on_command(control_id=1)
def on_submit(hwnd, ctrl_id, notif, ctrl_hwnd):
text = read(txt)
if text:
gui.show_message_box(f'You typed:\n\n"{text}"', "Input Received")
else:
gui.show_message_box("The text box is empty!", "Notice")
gui.run_message_loop()
06 — Multi-field form
Name / Last Name / Email form — Submit prints all values.
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd):
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
gui = WinGUI()
hwnd = gui.create_window(480, 320, "Simple Form")
fields = [("First Name:", 30), ("Last Name:", 90), ("Email:", 150)]
textboxes = []
for label_text, y in fields:
gui.create_label (hwnd, 20, y, 130, 22, label_text)
tb = gui.create_textbox(hwnd, 160, y+2, 300, 26)
textboxes.append(tb)
gui.create_button(hwnd, 180, 230, 120, 35, "Submit", control_id=1)
@gui.on_command(control_id=1)
def on_submit(hwnd, ctrl_id, notif, ctrl_hwnd):
first, last, email = [read(tb) for tb in textboxes]
msg = (
f"First Name : {first or '(empty)'}\n"
f"Last Name : {last or '(empty)'}\n"
f"Email : {email or '(empty)'}"
)
gui.show_message_box(msg, "Form Data")
gui.run_message_loop()
07 — Dynamic title
Each click appends the click count to the title bar.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(420, 200, "Click to update title")
count = [0]
gui.create_label (hwnd, 20, 20, 380, 22, "Each click updates the title bar.")
gui.create_button(hwnd, 155, 80, 110, 35, "Click", control_id=1)
@gui.on_command(control_id=1)
def on_click(hwnd, ctrl_id, notif, ctrl_hwnd):
count[0] += 1
gui.set_window_title(hwnd, f"Clicked {count[0]} time(s)")
gui.run_message_loop()
08 — Counter app
Increment, decrement, and reset — result shown in the title bar.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(420, 200, "Counter: 0")
counter = [0]
gui.create_label (hwnd, 20, 20, 380, 22, "Use the buttons to change the counter.")
gui.create_button(hwnd, 20, 80, 110, 35, "− Decrement", control_id=1)
gui.create_button(hwnd, 155, 80, 110, 35, "Reset", control_id=2)
gui.create_button(hwnd, 290, 80, 110, 35, "+ Increment", control_id=3)
def refresh():
gui.set_window_title(hwnd, f"Counter: {counter[0]}")
@gui.on_command(control_id=1)
def on_dec(hwnd, ctrl_id, notif, ctrl_hwnd):
counter[0] -= 1; refresh()
@gui.on_command(control_id=2)
def on_reset(hwnd, ctrl_id, notif, ctrl_hwnd):
counter[0] = 0; refresh()
@gui.on_command(control_id=3)
def on_inc(hwnd, ctrl_id, notif, ctrl_hwnd):
counter[0] += 1; refresh()
gui.run_message_loop()
09 — Calculator
Two number inputs, four operator buttons, result in a textbox.
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd):
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
def write(hwnd, text):
_user32.SetWindowTextW(hwnd, text)
gui = WinGUI()
hwnd = gui.create_window(480, 260, "Calculator")
gui.create_label(hwnd, 20, 20, 70, 22, "Number A:")
tb_a = gui.create_textbox(hwnd, 95, 20, 120, 26)
gui.create_label(hwnd, 250, 20, 70, 22, "Number B:")
tb_b = gui.create_textbox(hwnd, 325, 20, 120, 26)
for i, (sym, cid) in enumerate([("+", 1), ("−", 2), ("×", 3), ("÷", 4)]):
gui.create_button(hwnd, 20 + i * 110, 68, 95, 35, sym, control_id=cid)
gui.create_label(hwnd, 20, 130, 70, 22, "Result:")
tb_result = gui.create_textbox(hwnd, 95, 130, 350, 26)
@gui.on_command()
def on_op(hwnd, ctrl_id, notif, ctrl_hwnd):
if ctrl_id not in (1, 2, 3, 4):
return
try:
a, b = float(read(tb_a)), float(read(tb_b))
except ValueError:
write(tb_result, "Error: enter valid numbers"); return
if ctrl_id == 1: result = a + b
elif ctrl_id == 2: result = a - b
elif ctrl_id == 3: result = a * b
elif ctrl_id == 4:
if b == 0: write(tb_result, "Error: division by zero"); return
else: result = a / b
write(tb_result, str(int(result)) if result == int(result) else f"{result:.6g}")
gui.run_message_loop()
10 — Toggle button
Button label flips between OFF and ON ✓ on each click.
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
gui = WinGUI()
hwnd = gui.create_window(320, 180, "Toggle")
state = [False]
gui.create_label(hwnd, 20, 20, 280, 22, "Toggle the button on and off:")
btn = gui.create_button(hwnd, 95, 80, 130, 35, "OFF", control_id=1)
@gui.on_command(control_id=1)
def on_toggle(hwnd, ctrl_id, notif, ctrl_hwnd):
state[0] = not state[0]
label = "ON ✓" if state[0] else "OFF"
_user32.SetWindowTextW(btn, label)
gui.set_window_title(hwnd, f"Toggle ({label.strip()})")
gui.run_message_loop()
11 — Live label update
Update a label's text at runtime from a textbox.
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd):
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
gui = WinGUI()
hwnd = gui.create_window(480, 240, "Live Label Update")
gui.create_label(hwnd, 20, 20, 440, 22, "Type text and press Update:")
txt = gui.create_textbox(hwnd, 20, 50, 440, 28)
lbl = gui.create_label(hwnd, 20, 100, 440, 22, "(nothing yet)")
gui.create_button(hwnd, 175, 148, 130, 35, "Update Label", control_id=1)
@gui.on_command(control_id=1)
def on_update(hwnd, ctrl_id, notif, ctrl_hwnd):
text = read(txt) or "(empty)"
_user32.SetWindowTextW(lbl, text)
gui.run_message_loop()
12 — Note-taking app
Add, view, clear, and count notes — status label updates after each action.
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd):
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
gui = WinGUI()
hwnd = gui.create_window(500, 300, "Note Taker")
notes = []
gui.create_label(hwnd, 20, 20, 460, 22, "Enter a note:")
txt = gui.create_textbox(hwnd, 20, 48, 460, 28)
status = gui.create_label (hwnd, 20, 174, 460, 22, "No notes yet.")
gui.create_button(hwnd, 20, 92, 110, 32, "Add Note", control_id=1)
gui.create_button(hwnd, 145, 92, 110, 32, "Show Notes", control_id=2)
gui.create_button(hwnd, 270, 92, 110, 32, "Clear All", control_id=3)
gui.create_button(hwnd, 395, 92, 85, 32, "Count", control_id=4)
@gui.on_command(control_id=1)
def on_add(hwnd, ctrl_id, notif, ctrl_hwnd):
note = read(txt).strip()
if note:
notes.append(note)
_user32.SetWindowTextW(txt, "")
_user32.SetWindowTextW(status, f"{len(notes)} note(s) saved.")
@gui.on_command(control_id=2)
def on_show(hwnd, ctrl_id, notif, ctrl_hwnd):
body = "\n".join(f"{i+1}. {n}" for i, n in enumerate(notes)) if notes else "No notes added yet."
gui.show_message_box(body, f"Notes ({len(notes)})")
@gui.on_command(control_id=3)
def on_clear(hwnd, ctrl_id, notif, ctrl_hwnd):
notes.clear()
_user32.SetWindowTextW(status, "All notes cleared.")
@gui.on_command(control_id=4)
def on_count(hwnd, ctrl_id, notif, ctrl_hwnd):
gui.show_message_box(f"You have {len(notes)} note(s).", "Count")
gui.run_message_loop()
13 — Unicode
Full Unicode in labels, textboxes, and message boxes.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(520, 320, "Unicode 🌍")
gui.create_label(hwnd, 20, 20, 480, 22, "Chinese: 你好,世界!")
gui.create_label(hwnd, 20, 52, 480, 22, "Arabic: مرحبا بالعالم")
gui.create_label(hwnd, 20, 84, 480, 22, "Japanese: こんにちは世界")
gui.create_label(hwnd, 20, 116, 480, 22, "Emoji: 🎉 🚀 🌟 🎨 🏆")
gui.create_label(hwnd, 20, 160, 480, 22, "Type any Unicode text below:")
txt = gui.create_textbox(hwnd, 20, 188, 480, 28)
gui.create_button(hwnd, 195, 232, 130, 35, "Show Text", control_id=1)
@gui.on_command(control_id=1)
def on_show(hwnd, ctrl_id, notif, ctrl_hwnd):
import ctypes
buf = ctypes.create_unicode_buffer(512)
ctypes.WinDLL("user32").GetWindowTextW(txt, buf, 512)
gui.show_message_box(buf.value or "(empty)", "Your Input")
gui.run_message_loop()
14 — Multiple callbacks
Two @on_command handlers registered for the same control_id — both fire in order.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(440, 200, "Multiple Callbacks")
log = []
gui.create_label (hwnd, 20, 20, 400, 22, "Two handlers are registered for button 1.")
gui.create_button(hwnd, 165, 80, 110, 35, "Click", control_id=1)
@gui.on_command(control_id=1)
def handler_a(hwnd, ctrl_id, notif, ctrl_hwnd):
log.append("Handler A fired")
@gui.on_command(control_id=1)
def handler_b(hwnd, ctrl_id, notif, ctrl_hwnd):
log.append("Handler B fired")
gui.show_message_box("\n".join(log), "Event Log")
gui.run_message_loop()
15 — Notification filtering
Handler fires only for BN_CLICKED (notif=0) — focus events are silently ignored.
from wingui import WinGUI
BN_CLICKED = 0
gui = WinGUI()
hwnd = gui.create_window(440, 200, "Notification Filtering")
gui.create_label (hwnd, 20, 20, 400, 44,
"Handler fires ONLY for BN_CLICKED (notif=0).\n"
"Other notification codes are silently ignored.")
gui.create_button(hwnd, 160, 100, 120, 35, "Click Me", control_id=1)
@gui.on_command(control_id=1, notif=BN_CLICKED)
def on_clicked(hwnd, ctrl_id, notif, ctrl_hwnd):
gui.show_message_box(
f"Button clicked!\nctrl_id={ctrl_id} notif={notif}",
"Filtered Handler"
)
gui.run_message_loop()
16 — Shared state
Accumulate words from a textbox into a Python list across multiple button clicks.
from wingui import WinGUI
import ctypes
_user32 = ctypes.WinDLL("user32")
def read(hwnd):
buf = ctypes.create_unicode_buffer(512)
_user32.GetWindowTextW(hwnd, buf, 512)
return buf.value
gui = WinGUI()
hwnd = gui.create_window(480, 220, "Shared State")
history = []
gui.create_label (hwnd, 20, 20, 440, 22, "Type a word and press Add.")
txt = gui.create_textbox(hwnd, 20, 50, 320, 28)
gui.create_button (hwnd, 350, 48, 110, 30, "Add", control_id=1)
gui.create_button (hwnd, 165, 110, 150, 35, "Show History", control_id=2)
@gui.on_command(control_id=1)
def on_add(hwnd, ctrl_id, notif, ctrl_hwnd):
word = read(txt).strip()
if word:
history.append(word)
_user32.SetWindowTextW(txt, "")
@gui.on_command(control_id=2)
def on_show(hwnd, ctrl_id, notif, ctrl_hwnd):
if history:
gui.show_message_box(
"\n".join(f"{i+1}. {w}" for i, w in enumerate(history)),
f"History ({len(history)} item(s))"
)
else:
gui.show_message_box("No items added yet.", "History")
gui.run_message_loop()
17 — Programmatic close
A Quit button calls gui.close_window() to exit the message loop cleanly.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(400, 180, "Programmatic Close")
gui.create_label (hwnd, 20, 20, 360, 22, "Press Quit to close from code.")
gui.create_button(hwnd, 150, 80, 100, 35, "Quit", control_id=1)
@gui.on_command(control_id=1)
def on_quit(hwnd, ctrl_id, notif, ctrl_hwnd):
gui.close_window(hwnd)
gui.run_message_loop()
18 — Raw set_callback
Uses gui.set_callback() directly instead of @on_command. Every WM_COMMAND event routes to one function.
from wingui import WinGUI
gui = WinGUI()
hwnd = gui.create_window(440, 200, "Raw set_callback")
gui.create_label (hwnd, 20, 20, 400, 22, "Uses gui.set_callback() directly.")
gui.create_button(hwnd, 20, 80, 110, 35, "Button A", control_id=1)
gui.create_button(hwnd, 155, 80, 110, 35, "Button B", control_id=2)
gui.create_button(hwnd, 290, 80, 110, 35, "Button C", control_id=3)
names = {1: "A", 2: "B", 3: "C"}
def raw_handler(hwnd, ctrl_id, notif, ctrl_hwnd):
name = names.get(ctrl_id)
if name:
gui.show_message_box(f"Button {name} pressed\nctrl_id={ctrl_id} notif={notif}",
"Raw Callback")
gui.set_callback(raw_handler)
gui.run_message_loop()
Running the Example Gallery
The example.py file contains all 22 examples with an interactive console menu.
# Via the package entry point
python -m wingui --examples
# Or directly
python example.py
The launcher also provides:
python -m wingui # Interactive quick-start menu
python -m wingui --demo # Run the Hello World demo directly
python -m wingui --check # Verify DLL is present and loadable
python -m wingui --help # Show help text
Diagnostics
If controls fail to appear, run the diagnostic script before filing a bug:
python wingui/diag.py
diag.py patches all control-creation calls to print GetLastError() codes before Python raises OSError, prints hInstance values, and independently tests the STATIC window class from Python ctypes. Typical error codes:
| Code | Hex | Meaning |
|---|---|---|
| 1400 | 0x00000578 |
Invalid parent HWND — DLL and EXE handle mismatch. Rebuild the DLL. |
| 1407 | 0x0000057F |
Cannot find window class — wrong hInstance in RegisterClassExW. Rebuild. |
| 87 | 0x00000057 |
Invalid parameter — stack layout or argument type mismatch. |
Project Structure
WinGUI/
├── asm/
│ ├── wingui32.asm # NASM x86-64 source — all GUI logic
│ └── wingui32.def # DLL export list (for MSVC link)
├── bin/
│ ├── wingui32.dll # Pre-built 64-bit DLL
│ └── wingui32.obj # Object file
├── wingui/
│ ├── wingui.py # ctypes shim — WinGUI class + flat API
│ ├── __init__.py # Package init, re-exports public API
│ ├── __main__.py # python -m wingui entry point
│ ├── diag.py # Win32 diagnostic tool
│ └── py.typed # PEP 561 marker
├── example.py # 22-example interactive gallery
├── build.bat # One-command DLL rebuild (MSYS2)
├── pyproject.toml
├── LICENSE.txt # GNU LGPL v3.0
└── README.md
Build Reference
build.bat
@echo off
cd asm
nasm -f win64 wingui32.asm -o wingui32.obj
gcc -shared -o ..\bin\wingui32.dll wingui32.obj ^
-luser32 -lkernel32 -lgdi32 -lcomctl32
echo Done.
Key assembly design decisions
hInstance via GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, WindowProc)
rather than GetModuleHandleW(NULL). The NULL form returns the host EXE handle; RegisterClassExW must use the DLL's own handle so CreateWindowExW can locate the class.
hInstance = NULL for system controls (BUTTON, EDIT, STATIC).
Passing the DLL handle causes ERROR_CANNOT_FIND_WND_CLASS (1407) because these classes are registered against the null/system module.
SetProcessDpiAwarenessContext loaded via GetProcAddress at runtime rather than a static import. Static imports of this symbol fail to load on Windows < 1703 where the function may not exist in user32.dll.
Controls created with NULL text, then text set via SetWindowTextW after CreateWindowExW returns. This avoids the bug where calling utf8_to_wchar before CreateWindowExW corrupted the stack frame and caused ERROR_INVALID_WINDOW_HANDLE (1400).
RSP alignment strictly maintained throughout. On entry to any function, RSP mod 16 = 8 (return address just pushed). Shadow space of 32 bytes is always reserved. Stack arguments start at [rsp+32].
License
Copyright © 2026 Divyanshu Sinha
Licensed under the GNU Lesser General Public License v3.0. See LICENSE.txt for the full text.
In brief: you may use WinGUI in your own applications (commercial or open-source) without restriction. If you modify wingui32.asm or wingui.py themselves, you must release those modifications under LGPL v3+.
Made with NASM, Python, and the Win32 API — no frameworks were harmed.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wingui-1.0.0.tar.gz.
File metadata
- Download URL: wingui-1.0.0.tar.gz
- Upload date:
- Size: 116.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e8dd38ce95e6757c6e17870ee8980ba05a0fbadb705bb130224c8679af5ee4e6
|
|
| MD5 |
755496429b837ac6b00795d0d21a841e
|
|
| BLAKE2b-256 |
4aac0a2a02912ffee1a3ee591aff85cbd1697fe40632e2766c463d5bba133778
|
File details
Details for the file wingui-1.0.0-py3-none-any.whl.
File metadata
- Download URL: wingui-1.0.0-py3-none-any.whl
- Upload date:
- Size: 64.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58f588d20ecaef2ef97c212daae59f597024cdda5152c4fc9537f9d8d3c5d229
|
|
| MD5 |
6c7579dd6a349f88170dd6b8ecac1033
|
|
| BLAKE2b-256 |
d069d0d1782e96730b461bfc274210dd3419023b32506644324533c135ef7bef
|