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 | Who does the scaling | What the app sees |
|---|---|---|
| macOS | the OS composites points → pixels (Retina too) | logical points, never physical pixels — e.g. 2048×1152, or 1408×881 on Retina — at a flat 96 DPI |
| Windows | the app, guided by the reported DPI | physical pixels + the real DPI (96 at 100 %, 144 at 150 %) — Tk 9 is per-monitor-DPI-aware |
| Linux / X11 | the app | the DPI your desktop set via Xft.dpi (≈107 here), or a fabricated 96 on a bare window manager |
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 byapp.scale— or just wrap them inapp.dp(n)(= Math.round(n * app.scale)); leave fractions (relwidth,relx, …) alone:
// a 200×120 logical-pixel panel, 16 logical px in from the top-left:
panel.place.configure({
x: app.dp(16),
y: app.dp(16),
width: app.dp(200),
height: app.dp(120),
});
// stretch-with-margins that survives both resize and DPI:
field.place.configure({ x: app.dp(8), relwidth: 1, width: app.dp(-16), height: app.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.
app.scale and app.dp(n)
app.scale is the device-pixel multiplier for absolute geometry. It folds in
both the DPI scaling (S above) and the sw.app.uiScale density that
scales fonts and padding — so it tracks --ui-scale, and absolute place()
coords stay aligned with the (density-scaled) content. app.dp(n) is
Math.round(n * app.scale). Prefer them over reading tk scaling by hand.