3 DPI
dxtr edited this page 2026-10-18 15:34:41 +02:00

DPI

Awareness

A program that says nothing about DPI gets windows Windows scales up itself on displays above 100%: they look blurred, and every size Windows reports is in made-up units. Turn on per-monitor awareness for the threads that create windows:

(win32:set-thread-dpi-awareness :per-monitor-aware-v2)

From then on, the windows the thread creates get:

  • sizes and positions in pixels, from every function and message;
  • title bars and borders drawn at the display's DPI;
  • +wm-dpichanged+ when they move to a display of another DPI.

Awareness belongs to the thread, and counts for the windows it creates afterwards. A thread that creates no windows does not need it. The other awarenesses are :per-monitor-aware (the first version, without scaled title bars), :system-aware, :unaware and :unaware-gdi-scaled; there is rarely a reason to use them. The type dpi-awareness is the five.

set-thread-dpi-awareness, window-dpi and window-size-for-client-size need Windows 10 version 1607 or later. On an older Windows, user32 lacks the functions they call, and calling them signals an error from the Lisp implementation that the foreign function is undefined.

A window's DPI

(win32:window-dpi window)   ; => 96 at 100%, 120 at 125%, 144 at 150%, 192 at 200%
win32:+default-dpi+         ; => 96

window-dpi is the DPI of the display a window is on. Its scale is that over +default-dpi+: (/ (win32:window-dpi window) 96) is 3/2 at 150%. A program draws at the same physical size on every display by scaling what it draws by that.

To size a window by its content, scale the content's size and add the frame at the same DPI:

(let* ((dpi (win32:window-dpi window))
       (scale (/ dpi win32:+default-dpi+)))
  (multiple-value-bind (width height)
      (win32:window-size-for-client-size (round (* 640 scale)) (round (* 400 scale)) :dpi dpi)
    (win32:set-window-bounds window :width width :height height)))

Make the window first, then size it: its DPI is known only once it exists, on the display Windows put it on.

Moving between displays

When a window moves to a display of another DPI, it gets +wm-dpichanged+. wparam's low word is the new DPI, and lparam points to the bounds Windows suggests for the window, which keep it the same size at the new scale. Take them:

((= message win32:+wm-dpichanged+)
 (setf *scale* (/ (win32:low-word wparam) win32:+default-dpi+))
 (multiple-value-bind (left top right bottom) (win32:read-rect lparam)
   (win32:set-window-bounds window :x left :y top
                                   :width (- right left) :height (- bottom top)))
 0)

The window then gets +wm-size+ with its new client size, before set-window-bounds returns.