High-DPI & scaling
Short version: you usually don't have to do anything. Size text in points and
lay out with pack / grid / Flex / Grid, and your app renders at a sensible
physical size on a 1080p laptop, a 4K monitor, and a Retina MacBook alike. This
page explains why, and what to do in the one case that needs care — absolute
pixel positioning.
How each platform handles density
Screen density ("DPI") is resolved in surprisingly different places on each OS. Measured on one 27″ 2560×1440 panel (a true ~109 DPI) plus a Retina MacBook:
| Platform | What the app sees | Who does the scaling |
|---|---|---|
| macOS | logical points, never physical pixels — e.g. 2048×1152, or 1408×881 on Retina — at a flat 96 DPI | the OS composites points → pixels (Retina too) |
| Windows | physical pixels + the real DPI (96 at 100 %, 144 at 150 %) — Tk 9 is per-monitor-DPI-aware | the app, guided by the reported DPI |
| Linux / X11 | the DPI your desktop set via Xft.dpi (≈107 here), or a fabricated 96 on a bare window manager |
the app |
Two takeaways:
- macOS and Windows fold scaling into the coordinate space (logical points, or a real per-monitor DPI). Tk already reflects it, so a points-based UI is correct with no work from you.
- Only Linux/X11 may need the app to scale explicitly — and even there Tk picks up
the desktop's
Xft.dpiautomatically if one is set. On a bare window manager with no DPI configured, X reports a fabricated 96 DPI (the physical-size figure is the fiction), so never compute DPI fromwinfo screenmmwidth.
tk scaling is the one number that's right everywhere: device pixels per point, and
Tk uses it to size fonts and any geometry you express in points.
The convention: think in logical pixels
ScriptWeaver's unit of choice is the logical pixel — a CSS-style reference pixel at 96 DPI. To convert logical pixels to device pixels, multiply by
S = tk scaling × 0.75
(0.75 because a point is 1/72″ while a logical pixel is 1/96″.) Measured S on the
hardware above:
| Platform / scale | tk scaling |
S |
|---|---|---|
Linux, Xft.dpi 107 |
1.485 | 1.11 |
| macOS (external + Retina) | 1.333 | 1.00 |
| Windows @ 100 % | 1.333 | 1.00 |
| Windows @ 150 % | 1.998 | 1.50 |
S is 1.0 exactly where the OS already owns the scaling (macOS, Windows at 100 %)
and rises only where the app must do the work — so you can multiply by S
unconditionally and never double-scale.
When you actually need it
- Fonts — give a point size (
{ font: 'Helvetica 11' }) and Tk scales it for you. Nothing to do. pack/grid/Flex/Grid— content- and constraint-driven, so they adapt to the scaled fonts and window automatically. Nothing to do.- Absolute
placecoordinates / fixed pixel sizes — this is the case that needs scaling. Multiply the absolute parts byS; leave fractions (relwidth,relx, …) alone:
const S = parseFloat(__native_tcl_eval('tk scaling')) * 0.75;
const dp = (n) => Math.round(n * S);
// a 200×120 logical-pixel panel, 16 logical px in from the top-left:
panel.place.configure({ x: dp(16), y: dp(16), width: dp(200), height: dp(120) });
// stretch-with-margins that survives both resize and DPI:
field.place.configure({ x: dp(8), relwidth: 1, width: dp(-16), height: dp(28) });
Gotchas
- Don't derive DPI from physical size.
winfo screenmmwidthis fabricated on bare X11 (to force 96 DPI) and meaningless on macOS (which reports logical points). Usetk scaling. winfo screenwidthis logical, not physical, on macOS — it reads smaller than the panel's native resolution under any scaled or Retina mode. Expected, not a bug.
Planned
A Player-level convenience (app.scale and app.dp(n)) is planned so you won't read
tk scaling by hand. Until it lands, the two-line helper above is the whole story.