spacr.qt.hidpi

Pictures rendered for the panel they land on.

Why a module for two lines of arithmetic

QPixmap.scaled(w, h, ...) takes DEVICE pixels but every caller in a GUI means LOGICAL ones – the size the picture should occupy on screen. On a display with a device pixel ratio of 2, which is every retina Mac and plenty of Windows and Linux machines, Qt then stretches that logical bitmap across twice as many real pixels in each direction: a 200 px render blown up to 400 px, from a 3334 px source. The picture is not low-resolution; the scaling threw the resolution away.

The whole fix is to render at device pixels and then say so:

ratio = widget.devicePixelRatioF()
picture = source.scaled(round(px * ratio), round(px * ratio), ...)
picture.setDevicePixelRatio(ratio)

Missing the second line is worse than missing both. The picture comes out correct in pixels and twice the intended SIZE, because Qt has no way to know those pixels are dense ones.

A rule applied by hand at each site holds until the next site is written, so what ships is scaled_for() – source, widget, logical size – and every call site asking it. The next picture is then right without its author knowing this problem exists.

At a device pixel ratio of 1 scaled_for() is byte-for-byte what .scaled() did before: the same pixel dimensions, and a device pixel ratio of 1 is what a pixmap already carries. No ordinary display changes.

Reading a picture back

QPixmap.width() counts device pixels, so a picture rendered through this module reports twice its on-screen width on a 2x display. Anything that compares a pixmap against widget coordinates – centring it, mapping a mouse position onto it, fitting a label round it – wants logical_size(), which is the size it OCCUPIES whatever it was rendered at.

Moving a window between screens

The ratio belongs to the screen, not the application, and a window can be dragged from a retina laptop onto an ordinary external monitor. A picture rendered at 2x and moved to a 1x screen is merely wasteful; the reverse is blurry. follow_device_ratio() subscribes a widget to the change Qt already sends, for the pictures whose source is still in memory and whose re-render is a single call. Pictures scaled inside paintEvent need nothing: they ask for the ratio again on the next frame.

Functions

device_ratio(→ float)

How many real pixels target gets per logical pixel.

follow_device_ratio(→ Optional[_RatioWatcher])

Re-render widget's picture when it moves to a different screen.

logical_size(→ PySide6.QtCore.QSize)

The size picture OCCUPIES, whatever it was rendered at.

scaled_for(→ Any)

source at width x height LOGICAL pixels on target.

screen_for_widget([widget])

Find a display without making its Python wrapper a child of a window.

Module Contents

spacr.qt.hidpi.device_ratio(target: Any = None) → float[source]

How many real pixels target gets per logical pixel.

Asks the widget, then the screen the widget is on, then the primary screen, and answers 1.0 when nothing will say – which is a plain display and the behaviour every call site had before.

target may be any object with a devicePixelRatio accessor: a widget, a window, a screen, a paint device, or a stand-in in a test.

spacr.qt.hidpi.follow_device_ratio(widget: Any, redraw: Callable[[], None]) → _RatioWatcher | None[source]

Re-render widget’s picture when it moves to a different screen.

Qt sends DevicePixelRatioChange when a window is dragged from a retina laptop onto an ordinary monitor or back. redraw is the widget’s own “put the picture on again” call – it must scale from the source it kept, not from what is currently on the label, or each move re-scales an already-scaled picture.

Returns the watcher (for tests to drive) or None if the widget cannot take an event filter.

Parameters:
  • widget – the QObject (normally a widget) to watch; it becomes the watcher’s parent. Anything that is not a QObject gives None.

  • redraw – zero-argument callable run when the widget’s device pixel ratio actually changes; an exception it raises is logged at debug level and swallowed.

spacr.qt.hidpi.logical_size(picture: Any) → PySide6.QtCore.QSize[source]

The size picture OCCUPIES, whatever it was rendered at.

QPixmap.width() counts device pixels; this counts the widget coordinates the picture covers, which is what centring, hit-testing and layout are all measured in. A null or missing picture is 0 x 0.

Parameters:

picture – a QPixmap or QImage (or None); its device-independent size is used when it has one, otherwise its pixel size divided by its device pixel ratio.

spacr.qt.hidpi.scaled_for(source: Any, target: Any, width: SizeLike, height: SizeLike | None = None, *, aspect: PySide6.QtCore.Qt.AspectRatioMode = Qt.KeepAspectRatio, mode: PySide6.QtCore.Qt.TransformationMode = Qt.SmoothTransformation) → Any[source]

source at width x height LOGICAL pixels on target.

The one call every picture in the application should make. source is a QPixmap or a QImage; target is the widget the picture will be shown on (anything device_ratio() understands); the size is a side length, a (w, h) pair, a QSize, or two arguments.

The result is rendered at size * ratio real pixels and carries that ratio, so it lays out at size and draws at full panel resolution.

A null or missing source comes back untouched – a caller that already checks isNull() keeps its own answer, and one that does not is no worse off than it was with a bare .scaled().

Parameters:
  • source – the QPixmap or QImage to scale; None or a null picture is returned unchanged.

  • target – the widget (or window, screen or paint device) the picture will be shown on; its device pixel ratio is read through device_ratio().

  • width – logical width in widget pixels, or the whole size as a single side length, a (w, h) pair or a QSize when height is not given.

spacr.qt.hidpi.screen_for_widget(widget: Any = None)[source]

Find a display without making its Python wrapper a child of a window.

PySide can associate QWidget.screen()’s shared QScreen wrapper with that widget. Closing it then invalidates the wrapper; cyclic collection can even destroy the display object still used by QApplication. Static QGuiApplication lookups leave display lifetime with Qt.

Parameters:

widget – QWidget or QWindow whose center selects the display, or None for the primary display. An unshown child uses its parent. Non-Qt stand-ins may supply their own screen() accessor.

Returns:

QScreen at that position, the primary display as fallback, or None when there is no GUI application/display.