PyQt 将鼠标按下的坐标映射到图像的坐标系

PyQt mapping the coordinates of a mouse press to the coordinate system of an image

在我的程序中,我试图将鼠标按下的坐标映射回图像的坐标尺寸。我在 Python 中使用 PyQt4。下面的程序演示了这个问题。我有一个可以进行一些图像转换的小部件。在这些图像转换之后,图像显示在小部件的中心,同时保持图像的原始纵横比。由于图像被缩放和平移,因此必须将 MouseEvent 的坐标重新映射到图像的坐标系。

下面的程序有一个 class "ScalingWidget",它应该能够进行这些转换,并且还应该能够将 mouseReleaseEvent 中的坐标重新映射回图像的坐标系。当我在 Layout 和 mainwindow 之外显示小部件时,这完全符合预期,但是当我将小部件嵌入更大的 gui 时,它变得很糟糕。然后映射回Image坐标后的坐标突然出现偏移

下面的最小程序可以通过在启动程序时指定标志-b来启动有无bug。选项-n可以将ScalingWidget的实例越来越深在 "gui" 中,它在布局中嵌入的越深,bug 就越明显。 愚蠢的是,虽然绘图表明变换是正确的,但映射坐标(打印在 window 标题和控制台中)表明当 -b 标志存在时将它们重新映射回图像坐标是搞砸了。

所以我的问题是:当我的 ScalingWidget 嵌入到布局中时,将鼠标坐标重新映射回图像尺寸我做错了什么?

我不希望重新映射像素完美,但与最终用户定位鼠标一样准确。有两个点 x=20、y=20 和 x=380 和 y=380 可以用作参考点。

欢迎任何帮助!

#!/usr/bin/env python

from PyQt4 import QtGui
from PyQt4 import QtCore
import sys
import argparse

class ScalingWidget (QtGui.QWidget):
    ''' Displays a pixmap optimally in the center of the widget, in such way
        the pixmap is shown in the middle
    '''
    white   = QtGui.QColor(255,255,255)
    black   = QtGui.QColor(  0,  0,  0)
    arcrect = QtCore.QRect(-10, -10, 20, 20)

    def __init__(self):
        super(ScalingWidget, self).__init__()
        self.pixmap = QtGui.QPixmap(400, 400)
        painter = QtGui.QPainter(self.pixmap)
        painter.fillRect(self.pixmap.rect(), self.white)
        self.point1 = QtCore.QPoint(20, 20)
        self.point2 = QtCore.QPoint(380, 380)
        painter.setPen(self.black)
        painter.drawRect(QtCore.QRect(self.point1, self.point2))
        painter.end()
        self.matrix = None

    def sizeHint(self):
        return QtCore.QSize(500,400)

    ##
    # Applies the default transformations
    #
    def _default_img_transform(self, painter):
        #size of widget
        winheight   = float(self.height())
        winwidth    = float(self.width())
        #size of pixmap
        scrwidth    = float(self.pixmap.width())
        scrheight   = float(self.pixmap.height())
        assert(painter.transform().isIdentity())

        if scrheight <= 0 or scrwidth <= 0:
            raise RuntimeError(repr(self) + "Unable to determine Screensize")

        widthr  = winwidth / scrwidth
        heightr = winheight / scrheight

        if widthr > heightr:
            translate = (winwidth - heightr * scrwidth) /2
            painter.translate(translate, 0)
            painter.scale(heightr, heightr)
        else:
            translate = (winheight - widthr * scrheight) / 2
            painter.translate(0, translate)
            painter.scale(widthr, widthr)

        # now store the matrix used to map the mouse coordinates back to the 
        # coordinates of the pixmap
        self.matrix = painter.deviceTransform()

    def paintEvent(self, e):
        painter = QtGui.QPainter(self)
        painter.setClipRegion(e.region())

        # fill the background of the entire widget.
        painter.fillRect(self.rect(), QtGui.QColor(0,0,0))

        # transform to place the image nicely in the center of the widget.
        self._default_img_transform(painter)
        painter.drawPixmap(self.pixmap.rect(), self.pixmap, self.pixmap.rect())
        pen = QtGui.QPen(QtGui.QColor(255,0,0))

        # Just draw on the points used to make the black rectangle of the pix map
        # drawing is not affected, be remapping those coordinates with the "same"
        # matrix is.
        pen.setWidth(4)
        painter.setPen(pen)
        painter.save()
        painter.translate(self.point1)
        painter.drawPoint(0,0)
        painter.restore()
        painter.save()
        painter.translate(self.point2)
        painter.drawPoint(0,0)
        painter.restore()

        painter.end()

    def mouseReleaseEvent(self, event):
        x, y = float(event.x()), float(event.y())
        inverted, invsucces = self.matrix.inverted()
        assert(invsucces)
        xmapped, ymapped = inverted.map(x,y)
        print x, y
        print xmapped, ymapped
        self.setWindowTitle("mouse x,y = {}, {}, mapped x, y = {},{} "
                                .format(x, y, xmapped, ymapped)
                            )


def start_bug():
    ''' Displays the mouse press mapping bug.
        This is a bit contrived, but in the real world
        a widget is embedded in deeper in a gui
        than a single widget, besides the problem
        grows with the depth of embedding.
    '''
    app = QtGui.QApplication(sys.argv)
    win     = QtGui.QWidget()
    layout  = QtGui.QVBoxLayout()
    win.setLayout(layout)
    widget = None
    for i in range(0, args.increase_bug):
        if i < args.increase_bug-1:
            widget = QtGui.QWidget()
            layout.addWidget(widget)
            layout= QtGui.QVBoxLayout()
            widget.setLayout(layout)
        else:
            layout.addWidget(ScalingWidget())
    win.show()
    sys.exit(app.exec_())

def start_no_bug():
    ''' Does not show the mapping bug, the mouse event.x() and .y() map nicely back to
        the coordinate system of the pixmap
    '''
    app = QtGui.QApplication(sys.argv)
    win = ScalingWidget()
    win.show()
    sys.exit(app.exec_())

# parsing arguments
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-b', '--display-bug', action='store_true',
                    help="Toggle this option to get the bugged version"
                    )
parser.add_argument('-n', '--increase-bug', type=int, default=1,
                    help="Increase the bug by n times."
                    )

if __name__ == "__main__":
    args = parser.parse_args()
    if args.display_bug:
        start_bug()
    else:
        start_no_bug()

_default_image_transform的基本思路是正确的。错误在函数的末尾。

def _default_img_transform(self, painter):
    #size of widget
    winheight   = float(self.height())
    winwidth    = float(self.width())
    #size of pixmap
    scrwidth    = float(self.pixmap.width())
    scrheight   = float(self.pixmap.height())
    assert(painter.transform().isIdentity())

    if scrheight <= 0 or scrwidth <= 0:
        raise RuntimeError(repr(self) + "Unable to determine Screensize")

    widthr  = winwidth / scrwidth
    heightr = winheight / scrheight

    if widthr > heightr:
        translate = (winwidth - heightr * scrwidth) /2
        painter.translate(translate, 0)
        painter.scale(heightr, heightr)
    else:
        translate = (winheight - widthr * scrheight) / 2
        painter.translate(0, translate)
        painter.scale(widthr, widthr)

    # now store the matrix used to map the mouse coordinates back to the 
    # coordinates of the pixmap
    self.matrix = painter.deviceTransform() ## <-- error is here

函数_default_image_transform的最后一行应该是:

self.matrix = painter.transform()

根据文档,只有在使用 QT::HANDLE 时才应调用 QPainter.deviceTransform(),这是一个依赖于平台的句柄。因为我没有使用依赖于平台的句柄,所以我不应该调用它。它在我显示小部件时有效,但在它嵌入布局中时无效。然后 deviceTransform 矩阵不同于普通的 QPainter.transform() 矩阵。另见 http://doc.qt.io/qt-4.8/qpainter.html#deviceTransform