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_()
Это то, что вы хотели? ")