You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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缩放会导致两者比例不匹配。修正步骤:

  1. 启用Qt高DPI缩放支持,确保界面元素适配系统缩放。
  2. 通过QScreen获取当前屏幕的设备像素比(即缩放因子,如175%对应1.75)。
  3. 将Qt捕获的逻辑坐标乘以缩放因子,转换为物理像素坐标后再传递给ImageGrab.grab()。

二、双显示器环境下的区域捕获方案

要覆盖多显示器,需先获取虚拟桌面的完整范围,再确保坐标转换对所有屏幕生效:

  1. 用QApplication.screens()获取所有显示器的几何信息,计算虚拟桌面的总宽高与位置。
  2. 将捕获窗口设置为虚拟桌面尺寸,确保能覆盖所有屏幕。
  3. 捕获时基于虚拟桌面的逻辑坐标转换为物理坐标,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 19:32:05