Skip to main content

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

License: LGPL v3 Platform: Windows x64 Python: 3.10+ Assembly: NASM

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

  • 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 #F3F3F3 background
  • 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 (WinGUI class) or flat module functions
  • Context managerwith 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_command callbacks 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) DestroyWindowPostQuitMessage(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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wingui-1.0.0.tar.gz (116.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wingui-1.0.0-py3-none-any.whl (64.9 kB view details)

Uploaded Python 3

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

Hashes for wingui-1.0.0.tar.gz
Algorithm Hash digest
SHA256 e8dd38ce95e6757c6e17870ee8980ba05a0fbadb705bb130224c8679af5ee4e6
MD5 755496429b837ac6b00795d0d21a841e
BLAKE2b-256 4aac0a2a02912ffee1a3ee591aff85cbd1697fe40632e2766c463d5bba133778

See more details on using hashes here.

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

Hashes for wingui-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 58f588d20ecaef2ef97c212daae59f597024cdda5152c4fc9537f9d8d3c5d229
MD5 6c7579dd6a349f88170dd6b8ecac1033
BLAKE2b-256 d069d0d1782e96730b461bfc274210dd3419023b32506644324533c135ef7bef

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page