dpi_scaling_round2: a bit of research on Qt6's hiDPI support #49
|
|
@ -184,22 +184,25 @@ class DpiAwareFont:
|
|||
self._font_inches = inches
|
||||
font_size = math.floor(inches * pdpi)
|
||||
|
||||
log.debug(
|
||||
f"screen:{screen.name()}\n"
|
||||
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
|
||||
f"\nOur best guess font size is {font_size}\n"
|
||||
ftype: str = f'{type(self)!r}'
|
||||
log.info(
|
||||
f'screen: {screen.name()}\n'
|
||||
f'pDPI: {pdpi!r}\n'
|
||||
f'lDPI: {ldpi!r}\n'
|
||||
f'scale: {scale!r}\n'
|
||||
f'{ftype}._font_inches={self._font_inches!r}\n'
|
||||
f'\n'
|
||||
f"Our best guess for an auto-font-size is,\n"
|
||||
f'font_size: {font_size!r}\n'
|
||||
)
|
||||
# apply the size
|
||||
self._set_qfont_px_size(font_size)
|
||||
|
||||
def boundingRect(self, value: str) -> QtCore.QRectF:
|
||||
|
||||
screen = self.screen
|
||||
if screen is None:
|
||||
if (screen := self.screen) is None:
|
||||
raise RuntimeError("You must call .configure_to_dpi() first!")
|
||||
|
||||
unscaled_br = self._qfm.boundingRect(value)
|
||||
|
||||
unscaled_br: QtCore.QRectF = self._qfm.boundingRect(value)
|
||||
return QtCore.QRectF(
|
||||
0,
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
#!env xonsh
|
||||
'''
|
||||
Compute the pxs-per-inch (PPI) naively for the local DE.
|
||||
|
||||
NOTE, currently this only supports the `sway`-TWM on wayland.
|
||||
|
||||
!TODO!
|
||||
- [ ] support Xorg (and possibly other OSs as well?
|
||||
- [ ] conver this to pure py code, dropping the `.xsh` specifics
|
||||
instead for `subprocess` API calls?
|
||||
- [ ] possibly unify all this with `./qt_screen_info.py` as part of
|
||||
a "PPI config wizard" or something, but more then likely we'll
|
||||
have lib-ified version inside modden/piker by then?
|
||||
|
||||
'''
|
||||
|
||||
import math
|
||||
import json
|
||||
|
||||
# XXX, xonsh part using "subprocess mode"
|
||||
disp_infos: list[dict] = json.loads($(wlr-randr --json))
|
||||
lappy: dict = disp_infos[0]
|
||||
|
||||
dims: dict[str, int] = lappy['physical_size']
|
||||
w_cm: int = dims['width']
|
||||
h_cm: int = dims['height']
|
||||
|
||||
# cm per inch
|
||||
cpi: float = 25.4
|
||||
|
||||
# compute "diagonal" size (aka hypot)
|
||||
diag_inches: float = math.sqrt((h_cm/cpi)**2 + (w_cm/cpi)**2)
|
||||
|
||||
# compute reso-hypot / inches-hypot
|
||||
hi_res: dict[str, float|bool] = lappy['modes'][0]
|
||||
w_px: int = hi_res['width']
|
||||
h_px: int = hi_res['height']
|
||||
|
||||
diag_pxs: float = math.sqrt(h_px**2 + w_px**2)
|
||||
unscaled_ppi: float = diag_pxs/diag_inches
|
||||
|
||||
# retrieve TWM info on the display (including scaling info)
|
||||
sway_disp_info: dict = json.loads($(swaymsg -r -t get_outputs))[0]
|
||||
scale: float = sway_disp_info['scale']
|
||||
|
||||
print(
|
||||
f'output: {sway_disp_info["name"]!r}\n'
|
||||
f'--- DIMENSIONS ---\n'
|
||||
f'w_cm: {w_cm!r}\n'
|
||||
f'h_cm: {h_cm!r}\n'
|
||||
f'w_px: {w_px!r}\n'
|
||||
f'h_cm: {h_px!r}\n'
|
||||
f'\n'
|
||||
f'--- DIAGONALS ---\n'
|
||||
f'diag_inches: {diag_inches!r}\n'
|
||||
f'diag_pxs: {diag_pxs!r}\n'
|
||||
f'\n'
|
||||
f'--- PPI-related-info ---\n'
|
||||
f'(DE reported) scale: {scale!r}\n'
|
||||
f'unscaled PPI: {unscaled_ppi!r}\n'
|
||||
f'|_ =sqrt(h_px**2 + w_px**2) / sqrt(h_in**2 + w_in**2)\n'
|
||||
f'scaled PPI: {unscaled_ppi/scale!r}\n'
|
||||
f'|_ =unscaled_ppi/scale\n'
|
||||
)
|
||||
|
|
@ -31,8 +31,8 @@ Resource list for mucking with DPIs on multiple screens:
|
|||
- https://doc.qt.io/qt-5/qguiapplication.html#screenAt
|
||||
|
||||
'''
|
||||
import os
|
||||
|
||||
from pyqtgraph import QtGui
|
||||
from PyQt6 import (
|
||||
QtCore,
|
||||
QtWidgets,
|
||||
|
|
@ -43,6 +43,11 @@ from PyQt6.QtCore import (
|
|||
QSize,
|
||||
QRect,
|
||||
)
|
||||
from pyqtgraph import QtGui
|
||||
|
||||
|
||||
# https://doc.qt.io/qt-6/highdpi.html#environment-variable-reference
|
||||
os.environ['QT_USE_PHYSICAL_DPI'] = '1'
|
||||
|
|
||||
|
||||
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
|
||||
# must be set before creating the application
|
||||
|
|
@ -58,13 +63,22 @@ if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
|
|||
True,
|
||||
)
|
||||
|
||||
# NOTE, inherits `QGuiApplication`
|
||||
# https://doc.qt.io/qt-6/qapplication.html
|
||||
# https://doc.qt.io/qt-6/qguiapplication.html
|
||||
app = QtWidgets.QApplication([])
|
||||
#
|
||||
# ^TODO? various global DPI settings?
|
||||
# [ ] DPI rounding policy,
|
||||
|
goodboy
commented
Review
Not sure if this helps us more as well but figured i’d include it for further investigation. Not sure if this helps us more as well but figured i'd include it for further investigation.
|
||||
# - https://doc.qt.io/qt-6/qt.html#HighDpiScaleFactorRoundingPolicy-enum
|
||||
# - https://doc.qt.io/qt-6/qguiapplication.html#setHighDpiScaleFactorRoundingPolicy
|
||||
|
||||
window = QtWidgets.QMainWindow()
|
||||
main_widget = QtWidgets.QWidget()
|
||||
window.setCentralWidget(main_widget)
|
||||
window.show()
|
||||
|
||||
pxr: float = main_widget.devicePixelRatioF()
|
||||
_main_pxr: float = main_widget.devicePixelRatioF()
|
||||
|
||||
# explicitly get main widget and primary displays
|
||||
current_screen: QtGui.QScreen = app.screenAt(
|
||||
|
|
@ -77,7 +91,13 @@ for screen in app.screens():
|
|||
name: str = screen.name()
|
||||
model: str = screen.model().rstrip()
|
||||
size: QSize = screen.size()
|
||||
geo: QRect = screen.availableGeometry()
|
||||
geo: QRect = screen.geometry()
|
||||
|
||||
# device-pixel-ratio
|
||||
# https://doc.qt.io/qt-6/highdpi.html
|
||||
pxr: float = screen.devicePixelRatio()
|
||||
|
||||
unscaled_size: QSize = pxr * size
|
||||
|
goodboy
commented
Review
Note this should actually be per-screen and the correct abs resolution dimensions (in pxs obvi). A further gotcha here on I’m not exactly sure how to guard against this yet but at the least we can document that it’s unsupported for now, possibly a warning in the main issue is that the a couple options on this front might be OH RIGHT XD i forgot but i don’t think we can get the “physical dimensions” (like in cm/inches) from this? needs some tinkering if you feel up for it @momo ;) Note this should actually be per-screen and the correct abs resolution dimensions (in pxs obvi).
A further gotcha here on `wayland` is that only `int` values of `pxr` are able to be read.. which means anyone doing fancy `float` reso-scaling (like i was in my `sway` config) will get a wrong calculation for this..
I'm not exactly sure how to guard against this yet but at the least we can document that it's unsupported for now, possibly a warning in the `.configure_to_dpi()` message; it would be best if we can actually detect that case but i found no immediately obvious cross-platform way other then something `gemini` recommended,
```python
from screeninfo import get_monitors
print("Monitor Information:")
for i, m in enumerate(get_monitors()):
print(f"Monitor {i}:")
print(f" Name: {m.name}")
print(f" Resolution (pixels): {m.width}x{m.height}")
# Physical dimensions are provided in millimeters (mm)
print(f" Physical Size (mm): {m.width_mm}mm x {m.height_mm}mm")
print(f" Is Primary: {m.is_primary}")
```
main issue is that the `screeninfo` lib doesn't natively support `wayland` except through `xwayland`.. so we need another wayland nodding approach as well.
a couple options on this front might be `pywayland` and `pywlroots` and i also was already sniffing at [python-wayland](https://python-wayland.org/) for use in `modden` as an eventual gesture daemon approach (for mobile / touch screens that is). Anyway, this is more of a *me only* problem i'd imagine since i seem be one of the few using `piker` on `wayland` Bp
---
OH RIGHT XD
i forgot `gemini` also reco-ed `tnkinter` which is in the stdlib,
```python
import tkinter as tk
def get_screen_resolution_tkinter():
"""
Gets the primary screen resolution using Tkinter.
Works on Windows, macOS, and Linux (including Wayland/X11).
"""
root = tk.Tk()
# Hide the main window
root.withdraw()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
# Destroy the Tkinter instance after getting the information
root.destroy()
return screen_width, screen_height
if __name__ == "__main__":
width, height = get_screen_resolution_tkinter()
print(f"Screen resolution: {width}x{height}")
```
but i don't think we can get the "physical dimensions" (like in cm/inches) from this? needs some tinkering if you feel up for it @momo ;)
|
||||
phydpi: float = screen.physicalDotsPerInch()
|
||||
logdpi: float = screen.logicalDotsPerInch()
|
||||
is_primary: bool = screen is primary_screen
|
||||
|
|
@ -88,11 +108,12 @@ for screen in app.screens():
|
|||
f'|_primary: {is_primary}\n'
|
||||
f' _current: {is_current}\n'
|
||||
f' _model: {model}\n'
|
||||
f' _screen size: {size}\n'
|
||||
f' _screen geometry: {geo}\n'
|
||||
f' _devicePixelRationF(): {pxr}\n'
|
||||
f' _physical dpi: {phydpi}\n'
|
||||
f' _logical dpi: {logdpi}\n'
|
||||
f' _size: {size}\n'
|
||||
f' _geometry: {geo}\n'
|
||||
f' _devicePixelRatio(): {pxr}\n'
|
||||
f' _unscaled-size: {unscaled_size!r}\n'
|
||||
f' _physical-dpi: {phydpi}\n'
|
||||
f' _logical-dpi: {logdpi}\n'
|
||||
)
|
||||
|
||||
# app-wide font info
|
||||
|
|
@ -110,8 +131,8 @@ str_w: int = str_br.width()
|
|||
|
||||
print(
|
||||
f'------ global font settings ------\n'
|
||||
f'font dpi: {fontdpi}\n'
|
||||
f'font height: {font_h}\n'
|
||||
f'string bounding rect: {str_br}\n'
|
||||
f'string width : {str_w}\n'
|
||||
f'font dpi: {fontdpi!r}\n'
|
||||
f'font height: {font_h!r}\n'
|
||||
f'string bounding rect: {str_br!r}\n'
|
||||
f'string width : {str_w!r}\n'
|
||||
)
|
||||
|
|
|
|||
FWIW, i think we might want to add this setting by default since it seems (at least on sway/wayland) the “logical DPI” has very little value and is often plain deceiving since almost all compositor’s are going to pre-scale yet always report a 96..
at least this way (and try it urself via the updated script to verify) the “logical DPI” will be a rounded version, at least it seems, of the physical value; this at least discards ever using the “always 96 and not correct” default XD