计算屏幕 DPI

13

我正在使用Python构建一个图像渲染应用程序,将numpy数组渲染为图像。我需要一种自动计算屏幕DPI的方法,因为我在不同的监视器上生成相同大小的图像(例如2x2英寸)。我使用的操作系统是Windows 10,Python版本为3.7。

是在问如何获取监视器的屏幕大小 - 例如查找您的监视器是否为3000x2000像素(如已在以下链接中解决:如何在Python中获取监视器分辨率?)。

相反,我想要一些特别计算DPI的东西,并考虑到用户应用的任何比例设置可能会更改其系统的DPI。例如,我将比例设置为250%。

根据这个回答专家交流 (https://www.experts-exchange.com/questions/21794611/Detecting-system-DPI-settings-in-Python.html),我尝试使用以下代码:

from ctypes import windll

def get_ppi():
    LOGPIXELSX = 88
    LOGPIXELSY = 90
    user32 = windll.user32
    user32.SetProcessDPIAware()
    dc = user32.GetDC(0)   
    pix_per_inch = windll.gdi32.GetDeviceCaps(dc, LOGPIXELSX)
    print("Horizontal DPI is", windll.gdi32.GetDeviceCaps(dc, LOGPIXELSX))
    print("Vertical DPI is", windll.gdi32.GetDeviceCaps(dc, LOGPIXELSY))
    user32.ReleaseDC(0, dc)
    return pix_per_inch

这个数字大致是正确的,但是返回的数字过低。它返回240,而我的实际DPI更接近285。

我希望避免使用类似Qt这样的GUI框架,因为我想要尽量减少开销,但如果那是唯一的方法,那我就会使用!如果我必须使用框架,我更喜欢PyQt5。


也许您已经设置了每个显示器的 DPI?在这种情况下,请尝试使用 GetDpiForWindow()winDpi = user32.GetDpiForWindow(window) - mnistic
@mnistic 听起来很酷,但对我似乎不起作用:当我在我的代码中尝试将其设置为 window=0 时,我只会得到返回的 0。除非我设置错了? - eric
将实际窗口句柄传递给 GetDpiForWindow(),使用 GetDpiForSystem() 获取系统 DPI,即 sysDpi = user32.GetDpiForSystem() - mnistic
@mnistic,这听起来会是一个很好的答案 :) GetDpiForSystem返回240,这就是我的初始尝试返回的结果。显然,我对所有这些windll.user32的东西不是很擅长 :/ - eric
3个回答

12

虽然我说过想要避免它,但使用PyQt5有一种非常简单的方法可以实现。越想,我越认为这可能是最佳解决方案,因为它在很大程度上独立于平台:

import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
screen = app.screens()[0]
dpi = screen.physicalDotsPerInch()
app.quit()
注意,app.screens() 返回一个屏幕列表。在我的情况下,我只连接了一个屏幕,但是你可能有多个,所以确保知道需要从哪个屏幕获取dpi。如果你将所有代码都包含在一个函数中,它就不会在PyQt垃圾桶中混乱。
另外,有关QScreen的更多信息(sc是一个QScreen对象),请参见此文档页面:
https://doc.qt.io/qt-5/qscreen.html 你可以从中提取各种酷炫的东西。

2
可能QScreen中更好的DPI获取方法是logicalDotsPerInch()。我发现当我使用"physical"时,会得到像94.56这样的数字,而规格是96。"logical"实际上返回96,如果您想要进行自己的物理缩放,则更适合使用该数字。 - trumpetlicks

8
虽然@eric的答案有效,但我更愿意避免离开标准库(忽略不包括ctypes的Python发行版)。
这最初引导我使用ctypes,你可以看到我的初始(非常复杂的)答案,可以在下面找到。最近,我发现只需使用Tk就可以做到这一点(我记不得链接了,所以将链接到一个Stack Overflow答案,其内容相同)。
import tkinter
root = tkinter.Tk()
dpi = root.winfo_fpixels('1i')

译文

以下答案提供了两种解决方案:

  1. 第一种是由于用户的显示缩放而导致的 Windows 报告的 DPI。

  2. 第二种是通过找到显示器的物理尺寸和分辨率来计算出显示器的真实 DPI。

这些解决方案假定只有一个显示器,并设置处理 DPI 意识(这在某些情况下可能不适用)。关于 ctypes 使用的 Windows API 调用的详细信息,请参阅 SetProcessDPIAwarenessGetDPIForWindowGetDCGetDeviceCaps 的文档。

解决方案1

此解决方案不返回正确的 DPI,而是返回该显示器的 Windows“缩放因子”。要获得该因子,您应该把返回的 DPI 值除以 96(表示 96 是100% 的比例因子,而 120 表示125% 的比例因子,等等)。

此方法使用 tkinter 获取有效的 HWND(窗口标识符),然后使用 ctypes 获取新窗口的 DPI。

# 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()
解决方案2

这种方法应该返回监视器的真实DPI,但是需要注意两点:

  1. 有时会错误地报告显示器的物理大小。这将导致完全不正确的DPI值,尽管我不确定如何防止这种情况,除非添加检查以确保它在合理的范围内(无论什么意思!)。

  2. Windows使用的分辨率通常与显示器的实际分辨率不同。此方法计算监视器的真实DPI,但如果要计算每英寸物理像素的虚拟像素数量,则应将dwdh的赋值替换为root.winfo_screenwidth()root.winfo_screenheight()(分别)

此方法使用tkinter获取有效的HWND(窗口标识符),然后使用ctypes从中获取设备上下文(DC)和硬件详细信息。

# 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))

我只有一台显示器,使用的是Windows10操作系统。但是你提供的两种不同解决方案给出了两个非常不同的答案,都不是我期望的“100”。第一个解决方案给出了“96”,第二个解决方案给出了“141.8 141.4 141.7”。 - Pro Q
1
第一个解决方案不能提供正确的 DPI,而是仅仅是该监视器的 Windows“缩放因子”(96 表示你将其设置为 100% - 如果是 125% 则会报告为 120 DPI)。如果物理尺寸被错误地报告或者你所述的 100 是使用你所驱动的分辨率而不是你显示器的真实分辨率计算的话(你可以检查数字来帮助调试),那么第二个答案只有在这种情况下才会给出错误的读数。 - Minion Jim
注意:如果您想要每英寸物理像素的虚拟像素数,则应将dwdh的赋值替换为root.winfo_screenwidth()root.winfo_screenheight()(分别)。 - Minion Jim
这就是为什么我只使用Qt。当我不使用它时,事情变得复杂。而当我使用它时,它们变得简单/符合Python风格。 :) - eric
@ProQ 我不知道这里是否有任何关系或者这只是巧合,但是 100 x sqrt(2) == 141.4 - Guimoute
当我使用“root”窗口时,我最终得到了一个额外的窗口,因此我最终使用了plt.get_current_fig_manager().window.winfo_fpixels('1i') - Compholio

1
如果您不想使用Qt,可以使用上一种方法,因为在我的情况下,Qt解决方案和ctypes解决方案之间存在45.95 dpi的差异。我认为这对于所有笔记本电脑都是相同的。每次使用ctypes时,只需将(45.96 dpi)添加到结果中即可。

screen shot for representation


非常有趣,我还没有检查过,但如果它能够正常工作,那将会非常不错! - eric

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接