MapToScene требует отображения вида для правильных преобразований?

Основная проблема: метод QGraphicsView.mapToScene возвращает разные ответы в зависимости от того, отображается ли графический интерфейс. Почему, и могу ли я обойти это?

В контексте я пытаюсь написать модульные тесты, но я не хочу показывать инструменты для тестов.

Маленький пример ниже иллюстрирует поведение. Я использую подклассифицированное представление, которое печатает позиции события щелчка мыши в координатах сцены с началом в левом нижнем углу (он имеет шкалу -1 по вертикали), вызывая mapToScene. Тем не менее, 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() существует согласованное смещение 34 пикселей в x и 105 в y (и в 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_()

Этот пример находится в PyQt5 в Windows, но PyQt4 в Linux делает то же самое.

Ответы

Ответ 1

При погружении в исходный код С++ Qt это определение Qt mapToScene для QPoint:

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

Это хакерское и уродливое решение, поэтому, хотя он работает, может быть более элегантный способ справиться с этим.

Ответ 2

Вы пытались реализовать свой собственный 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_()

Это то, что вы хотели? ")