PySide截图工具因显示器缩放与双屏导致坐标不匹配的解决咨询
问题描述
我开发了一款基于PySide6和PIL库的Python屏幕区域捕获应用,该应用在1920×1080分辨率显示器上运行正常,但在分辨率为2560×1600、缩放比例175%的主显示器上,捕获区域与实际截图存在明显偏差。推测该差异是由显示器缩放比例影响坐标计算导致的,请问如何调整坐标以适配显示器缩放比例?
此外,我希望扩展屏幕捕获功能,使其支持双显示器环境,能够同时从两台显示器捕获区域,请问应采用何种实现方案?目前正在研究screeninfo库以解决屏幕分辨率与DPI缩放相关问题。
附现有代码:
# source from https://github.com/harupy/snipping-tool import sys from PySide6 import QtCore, QtGui, QtWidgets import tkinter as tk from PIL import ImageGrab import numpy as np import cv2 import pygetwindow as gw class MyWidget(QtWidgets.QWidget): def __init__(self): super().__init__() root = tk.Tk() screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() root.withdraw() # Hide the root window screen_geometry = gw.getWindowsWithTitle(gw.getActiveWindow().title)[0] virtual_screen_width = screen_geometry.width virtual_screen_height = screen_geometry.height self.setGeometry(0, 0, virtual_screen_width, virtual_screen_height) self.setWindowTitle(' ') self.begin = QtCore.QPoint() self.end = QtCore.QPoint() self.setWindowOpacity(0.3) self.num_snip = 0 self.is_snipping = False QtWidgets.QApplication.setOverrideCursor( QtGui.QCursor(QtCore.Qt.CrossCursor) ) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) print('Capture the screen...') print('Press q if you want to quit...') self.show() def paintEvent(self, event): if self.is_snipping: brush_color = (0, 0, 0, 0) lw = 0 opacity = 0 else: brush_color = (128, 128, 255, 128) lw = 3 opacity = 0.3 self.setWindowOpacity(opacity) qp = QtGui.QPainter(self) qp.setPen(QtGui.QPen(QtGui.QColor('black'), lw)) qp.setBrush(QtGui.QColor(*brush_color)) qp.drawRect(QtCore.QRectF(self.begin, self.end)) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Q: print('Quit') self.close() event.accept() def mousePressEvent(self, event): self.begin = event.position() self.end = self.begin self.update() def mouseMoveEvent(self, event): self.end = event.position() self.update() def mouseReleaseEvent(self, event): self.num_snip += 1 x1 = min(self.begin.x(), self.end.x()) y1 = min(self.begin.y(), self.end.y()) x2 = max(self.begin.x(), self.end.x()) y2 = max(self.begin.y(), self.end.y()) self.is_snipping = True self.repaint() QtWidgets.QApplication.processEvents() img = ImageGrab.grab(bbox=(x1, y1, x2, y2)) self.is_snipping = False self.repaint() QtWidgets.QApplication.processEvents() img_name = 'snip{}.png'.format(self.num_snip) img.save(img_name) print(img_name, 'saved') img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) window = MyWidget() window.show() app.aboutToQuit.connect(app.deleteLater) sys.exit(app.exec())
解决方案
一、适配显示器缩放比例(修正坐标偏差)
问题核心是Qt界面使用逻辑坐标,而ImageGrab.grab()依赖物理像素坐标,DPI缩放会导致两者比例不匹配。修正步骤:
- 启用Qt高DPI缩放支持,确保界面元素适配系统缩放。
- 通过
QScreen获取当前屏幕的设备像素比(即缩放因子,如175%对应1.75)。 - 将Qt捕获的逻辑坐标乘以缩放因子,转换为物理像素坐标后再传递给
ImageGrab.grab()。
二、双显示器环境下的区域捕获方案
要覆盖多显示器,需先获取虚拟桌面的完整范围,再确保坐标转换对所有屏幕生效:
- 用
QApplication.screens()获取所有显示器的几何信息,计算虚拟桌面的总宽高与位置。 - 将捕获窗口设置为虚拟桌面尺寸,确保能覆盖所有屏幕。
- 捕获时基于虚拟桌面的逻辑坐标转换为物理坐标,
ImageGrab.grab()原生支持跨显示器区域捕获。
修改后的完整代码
# 适配DPI缩放与双显示器的截图工具 import sys from PySide6 import QtCore, QtGui, QtWidgets from PIL import ImageGrab import numpy as np import cv2 class MyWidget(QtWidgets.QWidget): def __init__(self): super().__init__() # 获取所有屏幕信息,计算虚拟桌面范围 self.screens = QtWidgets.QApplication.screens() virtual_rect = QtCore.QRect() for screen in self.screens: virtual_rect |= screen.geometry() # 设置窗口覆盖整个虚拟桌面 self.setGeometry(virtual_rect) self.setWindowTitle(' ') self.begin = QtCore.QPoint() self.end = QtCore.QPoint() self.setWindowOpacity(0.3) self.num_snip = 0 self.is_snipping = False QtWidgets.QApplication.setOverrideCursor( QtGui.QCursor(QtCore.Qt.CrossCursor) ) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) print('Capture the screen...') print('Press q if you want to quit...') self.show() def get_device_pixel_ratio(self): # 根据鼠标位置获取对应屏幕的缩放因子 mouse_pos = QtGui.QCursor.pos() for screen in self.screens: if screen.geometry().contains(mouse_pos): return screen.devicePixelRatio() # 默认返回主屏幕缩放比例 return self.screens[0].devicePixelRatio() def paintEvent(self, event): if self.is_snipping: brush_color = (0, 0, 0, 0) lw = 0 opacity = 0 else: brush_color = (128, 128, 255, 128) lw = 3 opacity = 0.3 self.setWindowOpacity(opacity) qp = QtGui.QPainter(self) qp.setPen(QtGui.QPen(QtGui.QColor('black'), lw)) qp.setBrush(QtGui.QColor(*brush_color)) qp.drawRect(QtCore.QRectF(self.begin, self.end)) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Q: print('Quit') self.close() event.accept() def mousePressEvent(self, event): self.begin = event.position() self.end = self.begin self.update() def mouseMoveEvent(self, event): self.end = event.position() self.update() def mouseReleaseEvent(self, event): self.num_snip += 1 # 获取逻辑坐标范围 x1 = min(self.begin.x(), self.end.x()) y1 = min(self.begin.y(), self.end.y()) x2 = max(self.begin.x(), self.end.x()) y2 = max(self.begin.y(), self.end.y()) # 转换为物理像素坐标 dpi_scale = self.get_device_pixel_ratio() px1, py1 = x1 * dpi_scale, y1 * dpi_scale px2, py2 = x2 * dpi_scale, y2 * dpi_scale self.is_snipping = True self.repaint() QtWidgets.QApplication.processEvents() # 用物理坐标捕获截图 img = ImageGrab.grab(bbox=(px1, py1, px2, py2)) self.is_snipping = False self.repaint() QtWidgets.QApplication.processEvents() img_name = 'snip{}.png'.format(self.num_snip) img.save(img_name) print(img_name, 'saved') img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB) if __name__ == '__main__': # 启用Qt高DPI缩放支持 QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) app = QtWidgets.QApplication(sys.argv) window = MyWidget() window.show() app.aboutToQuit.connect(app.deleteLater) sys.exit(app.exec())
关键修改说明
- 启动时配置Qt高DPI属性,确保界面适配系统缩放。
- 基于
QScreen获取多显示器信息,计算虚拟桌面范围,让捕获窗口覆盖所有屏幕。 - 添加
get_device_pixel_ratio()方法,根据鼠标所在屏幕动态获取缩放因子,适配多显示器不同缩放比例的场景。 - 将Qt逻辑坐标转换为物理像素坐标后再调用
ImageGrab.grab(),彻底解决捕获区域偏差问题。
内容的提问来源于stack exchange,提问作者Sungbin Lee
相关产品推荐
相关产品推荐

