Calculate screen DPI
While @eric's answer works, I would prefer to avoid leaving the standard library (ignoring the distributions of Python that don't include ctypes
).
This initially led me to ctypes and you can see my initial (very over-complicated) answer which can be found below. More recently I found a way to do it with just Tk (I can't remember the link to it so will link to a Stack Overflow answer which has the same content)
import tkinter
root = tkinter.Tk()
dpi = root.winfo_fpixels('1i')
Original answer
The following answer offers two solutions:
The first is Windows' reported DPI due to the user's display scaling
The second is the monitor's true DPI calculated by finding the monitor's physical size and resolution
These solutions assume there is only one monitor and also sets process DPI awareness (which won't be suitable for some contexts). For details on the Windows API calls made by ctypes
see the documentation for SetProcessDPIAwareness
, GetDPIForWindow
, GetDC
and GetDeviceCaps
.
Solution 1
This solution doesn't return a proper DPI, and is instead just Window's "zoom factor" for that monitor. To get the factor, you should divide the returned DPI by 96
(meaning 96
is a scale factor of 100%
while 120
means a scale factor of 125%
, etc.).
The method uses tkinter
to get a valid HWND
(an identifier for the window) and then ctypes
to get the DPI for the new window.
# Import the libraries
import ctypes
import tkinter
# Set process DPI awareness
ctypes.windll.shcore.SetProcessDpiAwareness(1)
# Create a tkinter window
root = tkinter.Tk()
# Get the reported DPI from the window's HWND
dpi = ctypes.windll.user32.GetDpiForWindow(root.winfo_id())
# Print the DPI
print(dpi)
# Destroy the window
root.destroy()
Solution 2
This method should return the monitor's true DPI, however, two things should be noted:
The physical size of the monitor is (occasionally) reported incorrectly. This will therefore result in a completely incorrect DPI value, though I am not sure how to prevent this except add a check to ensure it is between sensible values (whatever that means!).
The resolution Windows is using is often different to the monitor's actual resolution. This method calculates the monitor's true DPI, but if you want to calculate the number of virtual pixels per physical inch, you would replace the assignment of
dw
anddh
withroot.winfo_screenwidth()
androot.winfo_screenheight()
(respectively)
This method uses tkinter
to get a valid HWND
(an identifier for the window) and then ctypes
to get a Device Context (DC) & hardware details from this.
# Our convertion from millimeters to inches
MM_TO_IN = 0.0393700787
# Import the libraries
import ctypes
import math
import tkinter
# Set process DPI awareness
ctypes.windll.shcore.SetProcessDpiAwareness(1)
# Create a tkinter window
root = tkinter.Tk()
# Get a DC from the window's HWND
dc = ctypes.windll.user32.GetDC(root.winfo_id())
# The the monitor phyical width
# (returned in millimeters then converted to inches)
mw = ctypes.windll.gdi32.GetDeviceCaps(dc, 4) * MM_TO_IN
# The the monitor phyical height
mh = ctypes.windll.gdi32.GetDeviceCaps(dc, 6) * MM_TO_IN
# Get the monitor horizontal resolution
dw = ctypes.windll.gdi32.GetDeviceCaps(dc, 8)
# Get the monitor vertical resolution
dh = ctypes.windll.gdi32.GetDeviceCaps(dc, 10)
# Destroy the window
root.destroy()
# Horizontal and vertical DPIs calculated
hdpi, vdpi = dw / mw, dh / mh
# Diagonal DPI calculated using Pythagoras
ddpi = math.hypot(dw, dh) / math.hypot(mw, mh)
# Print the DPIs
print(round(hdpi, 1), round(vdpi, 1), round(ddpi, 1))
While I said I wanted to avoid it, there is one very simple way to pull this off using PyQt5. The more I think about it, the more I think this could be the best solution, as it is largely platform independent:
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
screen = app.screens()[0]
dpi = screen.physicalDotsPerInch()
app.quit()
Note that app.screens()
returns a list of screens. In my case I only have one attached but you may have multiple, so be sure to be aware of which screen you need to get dpi from. And if you keep all this contained in a function, it won't clutter your namespace with PyQt junk.
Also, for more on QScreen (sc
is a QScreen object) see this doc page:
https://doc.qt.io/qt-5/qscreen.html
There's all sorts of cool stuff you can pull from it.
If you don't want to use Qt, you can use your last approach because in my case, there is a 45.95 dpi difference between a Qt solution and ctypes solution. I think it's always the same for all laptops. Just add (45.96 dpi) on your result every time you use ctypes.