PyQt5 QGraphicsViewでホイールでドラッグ、ドラッグで範囲選択する

Share on:

QGraphicsViewでスクロールやドラッグなどの動作を切り分けようと考えた場合、マウスボタンで動作の切り分けをする方法が考えられます。

そこで下記の仕様でズームやドラッグ、選択を出来るようなプログラムを作成しました。

  • ズームイン、ズームアウト:マウスホイールを回す
  • 画像の移動:ホイールクリックでドラッグ
  • 画像の範囲選択:左クリックでドラッグ

ウィジェットの作成

テスト用のウィジェットとしてQWidgetにQVBoxLayoutを追加して、QVBoxLayoutにQLabelと自作のGraphicsViewを追加します。そして、widget.show()でウィンドウが表示されます。そのmain関数は下記の通りです。

 1def main():
 2    app = QtWidgets.QApplication(sys.argv)
 3
 4    widget = QtWidgets.QWidget(None)
 5
 6    leyout = QtWidgets.QVBoxLayout(widget)
 7    leyout.addWidget( QtWidgets.QLabel(widget) )
 8    leyout.addWidget( GraphicsView(widget) )
 9
10    widget.setLayout(leyout)
11    widget.show()
12
13    app.exec()

mousePress時の座標を表示

mousePressEventとmouseReleaseEventをオーバーライドして、押されたボタンと座標を取得して、QLabelにクリックしたボタンとクリックした座標を表示してみます。座標はevent.pos()で取得できて、ボタンはevent.button()で取得できます。座標はself.mapToScene(event.pos())でシーン内の座標に変換しています。

 1def mousePressEvent(self, event):
 2    p = self.mapToScene(event.pos())
 3    x, y = round(p.x(), 3), round(p.y(), 3)
 4    
 5    if event.button() == QtCore.Qt.MidButton:
 6        text = 'MidButton pressed!!   ' + str(x) + ',' + str(y)
 7
 8    elif event.button() == QtCore.Qt.LeftButton:
 9        text = 'LeftButton pressed!!   ' + str(x) + ',' + str(y)
10
11    self.label.setText(text)
12    QtWidgets.QGraphicsView.mousePressEvent(self, event)
13
14def mouseReleaseEvent(self, event):
15    QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
16    if event.button() == QtCore.Qt.LeftButton:
17        p = self.mapToScene(event.pos())
18        x, y = round(p.x(), 3), round(p.y(), 3)
19        text = 'LeftButton released!!   ' + str(x) + ',' + str(y)
20        self.label.setText(text)

実行結果

mousePressEventでドラッグモードを指定

setDragModeでドラッグモードを指定できます。モードは下記の3つです。

  • QtWidgets.QGraphicsView.NoDrag : ドラッグなし
  • QtWidgets.QGraphicsView.RubberBandDrag : 範囲指定
  • QtWidgets.QGraphicsView.ScrollHandDrag : シーンを上下左右に移動

通常時はNoDragモードにしておいて、mousePressEvent時にクリックしたマウスボタンの種類ごとに、これらのモードを切り替えます。MidButtonの場合はScrollHandDrag、LeftButtonの場合はRubberBandDragとして、mouseReleaseEventでNoDragに戻します。

 1def mousePressEvent(self, event):
 2    gv = QtWidgets.QGraphicsView
 3    if event.button() == QtCore.Qt.MidButton:
 4        self.setDragMode(gv.ScrollHandDrag)
 5    elif event.button() == QtCore.Qt.LeftButton:
 6        self.setDragMode(gv.RubberBandDrag)
 7    gv.mousePressEvent(self, event)
 8
 9def mouseReleaseEvent(self, event):
10    gv = QtWidgets.QGraphicsView
11    gv.mouseReleaseEvent(self, event)
12    self.setDragMode(gv.NoDrag)

ScrollHandDragは左クリックでしか機能しないようです。ホイールクリックで動作させるためには、ホイールクリック時にeventを左クリックのeventとして作り変える必要があります。QtGui.QMouseEvent()でイベントを新規作成して、座標はevent.pos()で取得したもの、ボタンはLeftButtonとします。

 1def mousePressEvent(self, event):
 2    gv = QtWidgets.QGraphicsView
 3    
 4    if event.button() == QtCore.Qt.MidButton:
 5        self.setDragMode(gv.ScrollHandDrag)
 6
 7        event = QtGui.QMouseEvent(
 8            QtCore.QEvent.GraphicsSceneDragMove, 
 9            event.pos(), 
10            QtCore.Qt.MouseButton.LeftButton, 
11            QtCore.Qt.MouseButton.LeftButton, 
12            QtCore.Qt.KeyboardModifier.NoModifier
13        )
14
15    elif event.button() == QtCore.Qt.LeftButton:
16        self.setDragMode(gv.RubberBandDrag)
17
18    gv.mousePressEvent(self, event)

実行結果

クリック時の座標をラベルに表示させた結果が下記の通りです。

ソースコード

 1# -*- coding: utf-8 -*-
 2import sys
 3from PyQt5 import QtWidgets, QtCore, QtGui
 4
 5class GraphicsView(QtWidgets.QGraphicsView):
 6    def __init__(self, *argv, **keywords):
 7        super(GraphicsView, self).__init__(*argv, **keywords)
 8        
 9        scene = QtWidgets.QGraphicsScene(self)
10        image = QtGui.QImage('test.png')
11        pixmap = QtGui.QPixmap.fromImage(image)
12        scene.addPixmap(pixmap)
13        scene.setBackgroundBrush(QtCore.Qt.gray)
14        self.setScene(scene)
15
16        self._numScheduledScalings = 0
17        self.label = self.parent().findChild(QtWidgets.QLabel)
18
19        self.coordinates = []
20
21    def wheelEvent(self, event):
22        numDegrees = event.angleDelta().y() / 8
23        numSteps = numDegrees / 15
24        self._numScheduledScalings += numSteps
25        if self._numScheduledScalings * numSteps < 0:
26            self._numScheduledScalings = numSteps
27        anim = QtCore.QTimeLine(350, self)
28        anim.setUpdateInterval(20)
29        anim.valueChanged.connect(self.scalingTime)
30        anim.finished.connect(self.animFinished)
31        anim.start()
32
33    def scalingTime(self, x):
34        factor = 1.0 + float(self._numScheduledScalings) / 300.0
35        self.scale(factor, factor)
36
37    def animFinished(self):
38        if self._numScheduledScalings > 0:
39            self._numScheduledScalings -= 1
40        else:
41            self._numScheduledScalings += 1
42
43    def mousePressEvent(self, event):
44        p = self.mapToScene(event.pos())
45        
46        if event.button() == QtCore.Qt.MidButton:
47            self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
48
49            event = QtGui.QMouseEvent(
50                QtCore.QEvent.GraphicsSceneDragMove, 
51                event.pos(), 
52                QtCore.Qt.MouseButton.LeftButton, 
53                QtCore.Qt.MouseButton.LeftButton, 
54                QtCore.Qt.KeyboardModifier.NoModifier
55            )
56
57        elif event.button() == QtCore.Qt.LeftButton:
58            self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
59
60        self.coordinates = [p.x(), p.y()]
61        QtWidgets.QGraphicsView.mousePressEvent(self, event)
62   
63    def mouseReleaseEvent(self, event):
64        QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
65        self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
66        if event.button() == QtCore.Qt.LeftButton:
67            p = self.mapToScene(event.pos())
68            self.coordinates.extend([p.x(), p.y()])
69            text = 'select area : ' + ','.join([ str(int(c)) for c in self.coordinates])
70            self.label.setText(text)
71
72def main():
73    app = QtWidgets.QApplication(sys.argv)
74
75    widget = QtWidgets.QWidget(None)
76
77    leyout = QtWidgets.QVBoxLayout(widget)
78    leyout.addWidget( QtWidgets.QLabel(widget) )
79    leyout.addWidget( GraphicsView(widget) )
80
81    widget.setLayout(leyout)
82    widget.show()
83
84    app.exec()
85 
86if __name__ == '__main__':
87    main()

関連記事