P3: Somewhat important
5.15.2, 6.2.0 Beta3
Windows 10 x64, usually 125% DPI scaling
QWindow::screenChanged() is documented to fire when a QWindow moves to a different QScreen. However, lots of code within Qt itself, examples, and third-party apps only pick up changes to QScreen properties when QWindow::screenChanged() is emitted. As a result, they fail to pick up changes to DPI and resolution that occur when you change the screen resolution or DPI in Windows, or plug in an external monitor into a laptop (which changes physical screens, but retains the same \\.\DISPLAY1 Windows display and QScreen object).
Apps where this causes issues:
All Qt5 Widgets apps only relayout widgets when dragging a window between screens of different DPI, not when changing screen DPI or plugging in external monitors. Qt6 Widgets mostly dodges this issue, but the logic error remains.
- QGuiApplicationPrivate::processScreenLogicalDotsPerInchChange() fails to tell widgets they need to relayout.
- This is because widgets only relayout upon QEvent::ScreenChangeInternal, which only fires when switching between QScreen, not when a QScreen changes DPI.
- Widgets also relayout when "_q_customDpi" is set, but this is only used in unit tests, and I think this doesn't cascade from parents to children.
- This is the root cause of bugs like
QTBUG-77144(closed but not fixed) and QTBUG-83649 (never investigated).
- On Qt 6.2.0 Beta 3, it seems that when you launch an app when any connected display has a DPI above 100%, then text uses non-integer glyph advance, and sizeHint() is DPI-independent when you switch DPIs. However, an app launched at 100% will start out with integer glyph widths and a different sizeHint().
- Since sizeHint() is now in virtual pixels and never changes when dragging between displays, then QWidget calling d->updateFont(d->data.fnt); may be unnecessary.
- A fixed-size dialog (setSizeConstraint(QLayout::SetFixedSize)) launched at 125% scaling and then switched to 150% scaling is 1 or 2 pixels bigger/smaller than the same fixed-size dialog launched at 150% scaling. And a dialog launched on a 125% display and dragged onto a 100% display is 1 pixel bigger/smaller than the same dialog launched on a 100% display (both with fractional glyph advance enabled).
- Launching an app on a 100% display with no other displays, then enabling DPI scaling on that display or plugging in and dragging to a HiDPI display, results in integer glyph positioning but uneven spacing (painfully awkward kerning).
- Launching an app with a HiDPI display connected, but on a LoDPI display (or dragging to a LoDPI display), results in fractional glyph advance, and "IIIIIIIIIIIIIIIIII" alternates between sharp and fuzzy lines. I'd be happier if Qt 6 copied WPF apps and Chrome (unsure about WinUI) and used DirectWrite rendering without emulating GDI rendering. This results in fractional advance which looks more uniform than Qt 6 HiDPI does now.
Most other usages of QEvent::ScreenChangeInternal within Qt itself are questionable or buggy:
- QOpenGLWidget binds QEvent::ScreenChangeInternal to recreating the Fbo with a different devicePixelRatio, failing to pick up the devicePixelRatio of a given QScreen changing. However it does call recreateFbo() upon resizing, if a resolution change triggers a resize.
- QDockWidgetTitleButton only invalidates icon size (m_iconSize = -1) upon QEvent::ScreenChangeInternal (recomputed using QStyle::pixelMetric() which is probably DPI-dependent, and on Windows clamped to 10 * logicalDpiX()), and doesn't recompute icon size when a given screen changes DPI.
- I didn't investigate the Qt Widgets tests referencing ScreenChangeInternal (tests/manual/highdpi/kitchensink/main.cpp and tests/manual/qscreen/main.cpp).
QWindow::screenChanged() is universally misused within Qt's own examples (and some tests):
- Even in Qt6 dev, the widget gallery example connects QWindow::screenChanged() to reloading the resolution and DPI, ensuring it isn't picked up when the screen changes resolution or logical DPI, or an external monitor is plugged in.
- The icons example also only recomputes screen size when switching between QScreen, not when its resolution changes.
- I didn't thoroughly investigate Qt's tests, but the drpgadget examples queries DPI on every resize/repaint, so only picking up QScreen changes isn't a deal-breaker. I'm not sure about the kitchensink test either. The qcursorhighdpi, touch, and styles tests are probably broken (only recompute cached information upon screenChanged()).
It's often misused among third-party apps:
- OBS Studio's OBSBasic (main window) updates preview resolution upon QWindow::screenChanged(). (I'm not sure why it connects to screenChanged at all, since ovi contains the video recording's base/output resolution, not the screen resolution, and the function called doesn't query the current screen or resolution.) It responds to preview widget resizing by connecting to OBSQTDisplay::DisplayResized (emitted when the "display preview widget" is resized) to the same callback as well. I'm not sure why it even connects to QWindow::screenChanged(), which is emitted when dragging the window across screens, but not when plugging in external monitors with a different resolution, or changing the resolution of an existing screen.
- OBS Studio's OBSQTDisplay (parent class of OBSBasicPreview, a video preview widget) calls obs_display_resize() with the widget size multiplied by devicePixelRatioF(), upon QWindow::screenChanged. This is only emitted when dragging the window across screens, but not when plugging in external monitors with a different resolution, or changing the resolution of an existing screen. However, it might not be a bug in practice because resizeEvent() also calls obs_display_resize() with the widget size multiplied by devicePixelRatioF(), and it gets emitted when I change display DPI.
- The obscure third-party library libzeug makes the same mistake.
- The obscure third-party app "Maliit Keyboard 2" actually manages to avoid this mistake and properly handle screens changing DPI. &QWindow::screenChanged is connected to through the added complexity of tracking the active QScreen as m_screen, and connecting to &QScreen::physicalDotsPerInchChanged (though Device::updateScreen() itself has a bug where it doesn't call updateValues() directly).
In fact I cannot find a single correct usage of QWindow::screenChanged() (and only debatably correct uses of QEvent::ScreenChangeInternal) within Qt itself, and most third-party usages misinterpret QWindow::screenChanged() and only update cached screen metadata when it gets emitted (though OBS Studio happens to pick up changes properly despite it not being emitted).
It may be justified to emit QWindow::screenChanged() (and likely QEvent::ScreenChangeInternal too) when any QScreen metadata changes (any QScreen signal gets emitted), and update the documentation of QWindow::screenChanged() to match.