mapToScene是否需要显示视图才能进行正确的转换?

时间:2017-04-19 23:16:15

标签: python pyqt

主要问题: QGraphicsView.mapToScene方法会返回不同的答案,具体取决于是否显示GUI。 为什么,我可以解决它吗?

上下文是我正在尝试编写单元测试,但我不想实际显示测试工具。

下面的小例子说明了这种行为。我使用一个子类视图,通过调用mapToScene在场景坐标中打印鼠标点击事件位置,左下角的原点(垂直方向为-1)。但是,mapToScene在显示对话框之前不会返回我期望的内容。如果我在底部运行主要部分,我会得到以下输出:

Size is (150, 200)
Putting in (50, 125) - This point should return (50.0, 75.0)
Before show(): PyQt5.QtCore.QPointF(84.0, -20.0)
After show() : PyQt5.QtCore.QPointF(50.0, 75.0)

show()之前,x中有34个像素和y中有105个一致的偏移(并且在y中,偏移量反向移动,就像没有应用比例一样)。那些偏移似乎是随机的,我不知道它们来自哪里。

以下是示例代码:

import numpy as np
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPointF, QPoint
from PyQt5.QtWidgets import (QDialog, QGraphicsView, QGraphicsScene,
                             QVBoxLayout, QPushButton, QApplication,
                             QSizePolicy)
from PyQt5.QtGui import QPixmap, QImage

class MyView(QGraphicsView):
    """View subclass that emits mouse events in the scene coordinates."""

    mousedown = pyqtSignal(QPointF)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setSizePolicy(QSizePolicy.Fixed,
                           QSizePolicy.Fixed)

        # This is the key thing I need
        self.scale(1, -1)

    def mousePressEvent(self, event):
        return self.mousedown.emit(self.mapToScene(event.pos()))

class SimplePicker(QDialog):

    def __init__(self, data, parent=None):
        super().__init__(parent=parent)

        # Get a grayscale image
        bdata = ((data - data.min()) / (data.max() - data.min()) * 255).astype(np.uint8)
        wid, hgt = bdata.shape
        img = QImage(bdata.T.copy(), wid, hgt, wid,
                     QImage.Format_Indexed8)

        # Construct a scene with pixmap
        self.scene = QGraphicsScene(0, 0, wid, hgt, self)
        self.scene.setSceneRect(0, 0, wid, hgt)
        self.px = self.scene.addPixmap(QPixmap.fromImage(img))

        # Construct the view and connect mouse clicks
        self.view = MyView(self.scene, self)
        self.view.mousedown.connect(self.mouse_click)

        # End button
        self.doneb = QPushButton('Done', self)
        self.doneb.clicked.connect(self.accept)

        # Layout
        layout = QVBoxLayout(self)
        layout.addWidget(self.view)
        layout.addWidget(self.doneb)

    @pyqtSlot(QPointF)
    def mouse_click(self, xy):
        print((xy.x(), xy.y()))


if __name__ == "__main__":

    # Fake data
    x, y = np.mgrid[0:4*np.pi:150j, 0:4*np.pi:200j]
    z = np.sin(x) * np.sin(y)

    qapp = QApplication.instance()
    if qapp is None:
        qapp = QApplication(['python'])

    pick = SimplePicker(z)

    print("Size is (150, 200)")
    print("Putting in (50, 125) - This point should return (50.0, 75.0)")
    p0 = QPoint(50, 125)
    print("Before show():", pick.view.mapToScene(p0))

    pick.show()
    print("After show() :", pick.view.mapToScene(p0))

    qapp.exec_()

这个例子在Windows上的PyQt5中,但是Linux上的PyQt4做同样的事情。

2 个答案:

答案 0 :(得分:2)

在深入研究C ++ Qt源代码时,这是QPoint的mapToScene的Qt定义:

QPointF QGraphicsView::mapToScene(const QPoint &point) const
{
    Q_D(const QGraphicsView);
    QPointF p = point;
    p.rx() += d->horizontalScroll();
    p.ry() += d->verticalScroll();
    return d->identityMatrix ? p : d->matrix.inverted().map(p);
}

关键的事情是p.rx() += d->horizontalScroll();和同样的垂直滚动。 QGraphicsView始终包含滚动条,即使它们总是关闭或未显示。在显示小部件之前观察到的偏移来自初始化时水平和垂直滚动条的值,当显示小部件和计算布局时,必须修改该偏移以匹配视图/视口。为了使mapToScene正常运行,必须设置滚动条以匹配场景/视图。

如果我在示例中调用mapToScene之前放置了以下行,那么我将获得适当的转换结果,而无需显示小部件。

pick.view.horizontalScrollBar().setRange(0, 150)
pick.view.verticalScrollBar().setRange(-200, 0)
pick.view.horizontalScrollBar().setValue(0)
pick.view.verticalScrollBar().setValue(-200)

为了更一般地执行此操作,您可以从视图中提取一些相关的转换。

# Use the size hint to get shape info
wid, hgt = (pick.view.sizeHint().width()-2,
            pick.view.sizeHint().height()-2) # -2 removes padding ... maybe?

# Get the opposing corners through the view transformation
px = pick.view.transform().map(QPoint(wid, 0))
py = pick.view.transform().map(QPoint(0, hgt))

# Set the scroll bars accordingly
pick.view.horizontalScrollBar().setRange(px.y(), px.x())
pick.view.verticalScrollBar().setRange(py.y(), py.x())
pick.view.horizontalScrollBar().setValue(px.y())
pick.view.verticalScrollBar().setValue(py.y())

这是一个黑客和丑陋的解决方案,所以虽然它确实有效,但可能有一种更优雅的方式来处理这个问题。

答案 1 :(得分:1)

您是否尝试过实施自己的qgraphicsview并覆盖resizeEvent?当你乱搞mapTo"某事"你需要照顾你的resizeEvents,看一下我从你的代码中修改过的一段代码><

from PyQt5.QtCore import QRectF
from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QVBoxLayout,
                             QApplication, QFrame, QSizePolicy)
from PyQt5.QtCore import QPoint


class GraphicsView(QGraphicsView):

    def __init__(self):
        super(GraphicsView, self).__init__()


        # Scene and view
        scene = QGraphicsScene(0, 0, 150, 200,)
        scene.setSceneRect(0, 0, 150, 200)


    def resizeEvent(self, QResizeEvent):
        self.setSceneRect(QRectF(self.viewport().rect()))

qapp = QApplication(['python'])

# Just something to be a parent

view = GraphicsView()


# Short layout


# Make a test point
p0 = QPoint(50, 125)

# Pass in the test point before and after
print("Passing in point: ", p0)
print("Received point before show:", view.mapToScene(p0))
view.show()
print("Received point after show:", view.mapToScene(p0))

qapp.exec_()

这是你想要的行为吗? &#34)