3 Recipes
dxtr edited this page 2026-10-15 11:31:02 +02:00

Recipes

The examples use SBCL's sb-thread where they need threads; any thread library works the same.

Windows on a thread of their own

A window belongs to the thread that created it, and that thread must keep taking its messages. A program whose other threads come and go, such as one driven from a REPL, can give its windows a thread of their own and hand that thread the work. The thread owns a message-only window, the messenger; work is sent to it as a message of the program's own. send-message waits until the thread has run it, and the thread takes it even while Windows runs its own loop, as when the user drags a window's border.

(defconstant +run-work+ (+ win32:+wm-app+ 1) "The message that runs *WORK*.")

(defvar *messenger* 0 "The window thread's message-only window.")
(defvar *work* nil "The function the window thread runs next.")
(defvar *result* nil "What it returned, or the error it signalled.")
(defvar *work-lock* (sb-thread:make-mutex) "Lets one thread at a time hand over work.")

(win32:define-window-procedure app-procedure (window message wparam lparam)
  "Run work sent to the messenger; leave everything else to Windows."
  (declare (ignore wparam lparam))
  (when (and (= window *messenger*) (= message +run-work+))
    (setf *result* (handler-case (funcall *work*)
                     (error (condition) condition)))
    0))

(defun call-in-window-thread (function)
  "Call FUNCTION in the window thread, and return what it returns."
  (sb-thread:with-mutex (*work-lock*)
    (setf *work* function)
    (win32:send-message *messenger* +run-work+)
    (if (typep *result* 'error)
        (error *result*)
        *result*)))

(defun start-window-thread ()
  "Start the window thread, and return once it takes work."
  (win32:register-window-class "app" 'app-procedure)
  (win32:with-event (started)
    (sb-thread:make-thread
     (lambda ()
       (win32:set-thread-dpi-awareness :per-monitor-aware-v2)
       (setf *messenger* (win32:create-window "app" :parent :message :styles '()))
       (win32:set-event started)
       (win32:run-message-loop))
     :name "windows")
    (win32:wait-for-event started)))

(defun stop-window-thread ()
  "End the window thread, whose windows have been destroyed."
  (call-in-window-thread (lambda ()
                           (win32:destroy-window *messenger*)
                           (win32:post-quit-message)))
  (win32:unregister-window-class "app"))

Then, from any thread:

(start-window-thread)
(let ((window (call-in-window-thread
               (lambda () (win32:create-window "app" :title "From the REPL"
                                                     :styles '(:overlapped-window :visible))))))
  (setf (win32:window-text window) "Renamed")   ; any thread may do this
  (call-in-window-thread (lambda () (win32:destroy-window window))))
(stop-window-thread)

An error in the work comes back to the caller of call-in-window-thread. Never call it holding a lock the window procedure takes: the window thread would wait for the lock, and the caller for the window thread. This is how cl-windowing's Win32 backend runs its windows.

A game loop

A game renders frames as fast as it can and handles messages between them, without waiting:

(loop
  (multiple-value-bind (quit code) (win32:process-messages)
    (when quit
      (return code)))
  (update-world)
  (render-frame))

The window procedure posts the quit message when the window is destroyed, as in Getting Started.

Drawing with Vulkan

A window drawn through a Vulkan swapchain needs Windows to stay out of the way:

((= message win32:+wm-paint+) (win32:validate-window window) 0)  ; frames come from the swapchain
((= message win32:+wm-erasebkgnd+) 1)                            ; nothing to erase
((= message win32:+wm-size+)
 (unless (= wparam win32:+size-minimized+)
   (setf *swapchain-stale* t))                                   ; recreate at the new size
 0)

The swapchain's size is the client area, client-size; Vulkan on Windows insists on it. vkCreateWin32SurfaceKHR takes the program's module and the window, as pointers: (cl-ffi:make-pointer (win32:module-handle)) and (cl-ffi:make-pointer window). cl-windowing/vulkan does all this for cl-windowing's windows.

Typing text

Join surrogate pairs, and drop control characters:

(defvar *high-surrogate* 0 "The first half of a character typed in two +WM-CHAR+s, or 0.")

(defun typed (code)
  "Return the character a +WM-CHAR+ with CODE completes, or NIL."
  (cond ((<= #xD800 code #xDBFF) (setf *high-surrogate* code) nil)
        ((<= #xDC00 code #xDFFF)
         (prog1 (and (plusp *high-surrogate*)
                     (code-char (+ #x10000 (ash (- *high-surrogate* #xD800) 10) (- code #xDC00))))
           (setf *high-surrogate* 0)))
        ((or (< code 32) (= code 127)) nil)
        (t (code-char code))))

;; In the window procedure:
((= message win32:+wm-char+)
 (let ((character (typed wparam)))
   (when character
     (insert-character character)))
 0)
((= message win32:+wm-syschar+) 0)   ; typed with Alt: no beep

Dragging

Capture the pointer from the press to the release, and give up the drag if the capture is lost:

((= message win32:+wm-lbuttondown+)
 (win32:set-capture window)
 (start-drag (win32:signed-low-word lparam) (win32:signed-high-word lparam))
 0)
((= message win32:+wm-mousemove+)
 (when *dragging*
   (drag-to (win32:signed-low-word lparam) (win32:signed-high-word lparam)))
 0)
((= message win32:+wm-lbuttonup+)
 (end-drag)
 (win32:release-capture)
 0)
((= message win32:+wm-capturechanged+)
 (cancel-drag)        ; another window took the pointer: no release will come
 0)

release-capture sends +wm-capturechanged+ before it returns, so end the drag first, and cancel-drag finds nothing to cancel.

Telling AltGr from Control

(defun altgr-control-p (window wparam lparam)
  "In the handling of a key message for WINDOW: true if this left Control
is the one Windows sends with AltGr."
  (and (eq (win32:virtual-key-name wparam) :control)
       (not (logbitp 24 lparam))
       (multiple-value-bind (next next-wparam next-lparam next-time)
           (win32:peek-message :window window)
         (and next
              (member next (list win32:+wm-keydown+ win32:+wm-syskeydown+
                                 win32:+wm-keyup+ win32:+wm-syskeyup+))
              (eq (win32:virtual-key-name next-wparam) :menu)
              (logbitp 24 next-lparam)
              (= next-time (win32:message-time))))))

Call it from the window procedure for +wm-keydown+ and +wm-keyup+ with the procedure's own arguments.