Win32 for Common Lisp: window classes and procedures, windows, message loops, DPI, and keyboard and pointer state.
  • Common Lisp 100%
Find a file
dxtr 5be85a16f7 Move the pins on the workspace's libraries
To the latest commit of cl-ffi, whose allocate boxes no pointer, of
Pyssel's step 25.
2026-10-19 21:30:55 +02:00
ffi Define the keywords as cl-ffi enums and types, and pass wide strings 2026-10-18 15:34:33 +02:00
src Declare the arguments' full domain types 2026-10-18 16:25:37 +02:00
tests Declare the arguments' full domain types 2026-10-18 16:25:37 +02:00
.gitattributes Keep LF line endings in every checkout 2026-10-16 15:49:11 +02:00
.gitignore Pin the dependencies with stift in place of qlot 2026-10-16 11:19:52 +02:00
cl-win32.asd Call C through cl-ffi in place of cffi 2026-10-15 11:16:08 +02:00
CLAUDE.md Declare the arguments' full domain types 2026-10-18 16:25:37 +02:00
LICENSE Add cl-win32: windows, messages and input through user32 2026-10-11 17:52:26 +02:00
README.org Declare the arguments' full domain types 2026-10-18 16:25:37 +02:00
stiftfile Move the pins on the workspace's libraries 2026-10-19 21:30:55 +02:00

cl-win32

Win32 for Common Lisp: window classes and window procedures, windows, message loops, DPI, and keyboard and pointer state. Handles are integers, strings are Lisp strings, and failures are conditions.

(asdf:load-system :cl-win32)

(defpackage #:example
  (:use #:cl)
  (:local-nicknames (#:win32 #:cl-win32)))
(in-package #:example)

;; Windows calls this with each message for the windows of its classes.
(win32:define-window-procedure example-procedure (window message wparam lparam)
  (declare (ignore wparam lparam))
  (cond ((= message win32:+wm-close+) (win32:destroy-window window) 0)
        ((= message win32:+wm-destroy+) (win32:post-quit-message 7) 0)
        ;; NIL: the default handling.
        (t nil)))

(win32:set-thread-dpi-awareness :per-monitor-aware-v2)
(win32:register-window-class "example" 'example-procedure)
(let ((window (win32:create-window "example" :title "Hello" :width 640 :height 400)))
  (win32:show-window window)
  (prog1 (list (win32:window-text window)
               (win32:window-dpi window)
               ;; As if the user closed the window.
               (progn (win32:post-message window win32:+wm-close+)
                      (win32:run-message-loop)))
    (win32:unregister-window-class "example")))
;; => ("Hello" 96 7)   on a display at 100%

Windows and messages

A window class names a window procedure. Define one with define-window-procedure: Windows calls it with the window's handle, the message, such as +wm-size+, and the message's two parameters, all integers. It returns the message's result, or nil for the default handling. register-window-class registers a class, and create-window makes a window of it: :styles such as :overlapped-window (a title bar and borders) and :visible, keywords of the type window-style, or :parent :message for a message-only window, which only receives messages. A :popup window, without a frame, made with another as its :parent, is owned by it: it stays above it and goes with it, as a menu does, and with the :extended-styles :no-activate and :tool-window it is not activated when clicked and has no button on the taskbar. destroy-window and unregister-window-class release them.

A window belongs to the thread that created it. Only that thread receives its messages, and only that thread may destroy it. run-message-loop hands the thread's messages to their window procedures until post-quit-message; process-messages hands over those waiting and returns. post-message puts a message on a window's queue, from any thread; send-message calls its window procedure and waits for the result, on the window's thread. peek-message looks at the next message without taking it. message-time and message-position say when the message being handled was sent, and where the pointer was then.

window-text, client-size, window-bounds, set-window-bounds, show-window and validate-window do what their names say. change-window-styles adds and removes styles, keeping the rest, and has Windows draw the frame anew. window-placement says where a window goes when neither minimised nor maximised and whether it is either, and set-window-placement puts it back so. window-thread-id names a window's thread, as current-thread-id names the caller's. (setf window-enabled-p) disables a window's input from the mouse and keyboard, as a modal dialog disables its owner while it is open, and enables it again; window-enabled-p tells.

low-word, high-word, signed-low-word and signed-high-word take parameters apart, as LOWORD and GET_X_LPARAM do; make-lparam puts one together. read-rect reads the RECT a parameter points to, as +wm-dpichanged+'s does.

Displays and DPI

display-work-area returns the part of a display that windows may cover, without the taskbar, for the display a rectangle on the screen is on, to keep a window such as a menu on the screen. display-bounds returns all of the display a window is on, which a fullscreen window covers:

(win32:change-window-styles window :remove '(:overlapped-window :maximize))
(multiple-value-bind (x y width height) (win32:display-bounds window)
  (win32:set-window-bounds window :x x :y y :width width :height height))

Keep window-placement before, to give the window its frame and place back after.

set-thread-dpi-awareness with :per-monitor-aware-v2 has Windows report sizes and positions in pixels for the windows the thread creates from then on, and send +wm-dpichanged+ when one moves to a display of another DPI. window-dpi is the DPI of a window's display, 96 (+default-dpi+) at 100%. window-size-for-client-size adds a title bar and borders to a size.

Keyboard and pointer

Virtual keys are keywords: the names of their VK_ constants without the prefix, such as :shift, :lcontrol, :return, :f1, and :key-a and :key-1 for letters and digits. virtual-key-name and virtual-key-code convert. key-state says whether a key is down and toggled, as of the message being handled; map-virtual-key converts between virtual keys, scancodes and characters for the keyboard layout.

track-pointer-leave asks for +wm-mouseleave+. set-capture and release-capture keep the pointer's messages for a window during a drag. system-cursor returns a system cursor, such as :ibeam or :size-we, and set-cursor shows it; a window procedure keeps its own cursor by calling set-cursor for +wm-setcursor+ when LPARAM's low word is +htclient+, and returning 1. screen-to-client and client-to-screen convert positions. cursor-position and set-cursor-position read and move the pointer on the screen. clip-cursor keeps the pointer within a rectangle, and cursor-clip says which; the pointer is the desktop's, so let it go when the window loses the focus. register-raw-mouse has the mice send their own motion to a window as +wm-input+, which raw-mouse-motion reads in counts, without the pointer's acceleration and whether or not the pointer can move: with the pointer clipped to one point and hidden, a game's captured pointer. A window procedure answers +wm-mouseactivate+ with +ma-noactivate+ to stay inactive when clicked, and +wm-nchittest+ with +httransparent+ to let the pointer through to the window below. system-metric answers questions such as :mouse-present.

Input methods

An input method composes text from what is typed, such as Chinese or Japanese, and the user chooses the result from its candidate list. A window's input method is enabled when it is made; disable-input-method and enable-input-method turn it off while no text field has the focus and on again. To show the text being composed in the window yourself:

  • In +wm-ime-setcontext+, clear +isc-showuicompositionwindow+ from LPARAM before the default handling, and answer +wm-ime-startcomposition+ with 0, so the input method draws no composition window of its own.
  • In +wm-ime-composition+, read input-method-composition, the text and its caret, when LPARAM holds +gcs-compstr+, and input-method-result, the text chosen, when it holds +gcs-resultstr+; answer with 0, or Windows sends the result again as +wm-char+.
  • set-input-method-caret places the candidate list beside the caret.

cancel-input-method-composition drops the text being composed. Call these from the window's thread.

The clipboard

clipboard-text returns the text on the clipboard, or nil if it holds none, and setf puts text there. Both take a window, which Windows makes the clipboard's owner when text is put there, and wait up to :timeout seconds while another program has the clipboard open:

(setf (win32:clipboard-text window) "Copied")
(win32:clipboard-text window)          ; => "Copied"

Windows' text has CR LF between lines; cl-win32 neither adds nor removes them. While the session is locked, Windows gives no program the clipboard, and both signal win32-error.

Events

make-event makes an event object, a flag one thread sets with set-event and others wait for with wait-for-event, which takes a timeout in seconds. with-event closes it on leaving.

Errors

A failed Win32 call signals win32-error, with the function's name, the error code and Windows' description of it:

DestroyWindow failed with Windows error 1400: Invalid window handle.

An argument of the wrong type signals a type-error before Windows is called: each function declaims the full domain of its arguments, so a window, an event or a cursor is never 0, which names nothing, and a width or a height is never negative, and SBCL warns of a constant outside it when it compiles the call. The keywords the functions take are types, such as window-style, show-command and system-cursor-name, so an unknown one is refused with the ones there are:

:SIDEWAYS, among the styles (:OVERLAPPED-WINDOW :SIDEWAYS), is not a window style: :OVERLAPPED, :POPUP, ...

An error in a window procedure cannot unwind through the Windows code that called it. It is caught there, the message gets the default handling, and the error is signalled again from the cl-win32 function during which Windows called the procedure: run-message-loop, process-messages, send-message, create-window, destroy-window and the like. run-message-loop may be called again to go on.

The raw layer

cl-win32/ffi binds the C functions one to one, with handles as integers and strings as Lisp strings, of the cl-ffi type wide-string, which cl-ffi copies to UTF-16 for each call. Use it for what cl-win32 does not cover.

Development

Dependencies are pinned in the stiftfile with stift, and tests use parachute. On Windows:

stift test

The tests need Windows 10 version 1607 or later and a desktop session: they create windows, without showing them.

License

MIT. See LICENSE.