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

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

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

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



ウィジェットの作成

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

def main():
    app = QtWidgets.QApplication(sys.argv)

    widget = QtWidgets.QWidget(None)

    leyout = QtWidgets.QVBoxLayout(widget)
    leyout.addWidget( QtWidgets.QLabel(widget) )
    leyout.addWidget( GraphicsView(widget) )

    widget.setLayout(leyout)
    widget.show()

    app.exec()
 

mousePress時の座標を表示

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

def mousePressEvent(self, event):
    p = self.mapToScene(event.pos())
    x, y = round(p.x(), 3), round(p.y(), 3)
    
    if event.button() == QtCore.Qt.MidButton:
        text = 'MidButton pressed!!   ' + str(x) + ',' + str(y)

    elif event.button() == QtCore.Qt.LeftButton:
        text = 'LeftButton pressed!!   ' + str(x) + ',' + str(y)

    self.label.setText(text)
    QtWidgets.QGraphicsView.mousePressEvent(self, event)

def mouseReleaseEvent(self, event):
    QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
    if event.button() == QtCore.Qt.LeftButton:
        p = self.mapToScene(event.pos())
        x, y = round(p.x(), 3), round(p.y(), 3)
        text = 'LeftButton released!!   ' + str(x) + ',' + str(y)
        self.label.setText(text)

実行結果

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

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

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

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

def mousePressEvent(self, event):
    gv = QtWidgets.QGraphicsView
    if event.button() == QtCore.Qt.MidButton:
        self.setDragMode(gv.ScrollHandDrag)
    elif event.button() == QtCore.Qt.LeftButton:
        self.setDragMode(gv.RubberBandDrag)
    gv.mousePressEvent(self, event)

def mouseReleaseEvent(self, event):
    gv = QtWidgets.QGraphicsView
    gv.mouseReleaseEvent(self, event)
    self.setDragMode(gv.NoDrag)

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

def mousePressEvent(self, event):
    gv = QtWidgets.QGraphicsView
    
    if event.button() == QtCore.Qt.MidButton:
        self.setDragMode(gv.ScrollHandDrag)

        event = QtGui.QMouseEvent(
            QtCore.QEvent.GraphicsSceneDragMove, 
            event.pos(), 
            QtCore.Qt.MouseButton.LeftButton, 
            QtCore.Qt.MouseButton.LeftButton, 
            QtCore.Qt.KeyboardModifier.NoModifier
        )

    elif event.button() == QtCore.Qt.LeftButton:
        self.setDragMode(gv.RubberBandDrag)

    gv.mousePressEvent(self, event)

実行結果

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



ソースコード全体

# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtWidgets, QtCore, QtGui

class GraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, *argv, **keywords):
        super(GraphicsView, self).__init__(*argv, **keywords)
        
        scene = QtWidgets.QGraphicsScene(self)
        image = QtGui.QImage('test.png')
        pixmap = QtGui.QPixmap.fromImage(image)
        scene.addPixmap(pixmap)
        scene.setBackgroundBrush(QtCore.Qt.gray)
        self.setScene(scene)

        self._numScheduledScalings = 0
        self.label = self.parent().findChild(QtWidgets.QLabel)

        self.coordinates = []

    def wheelEvent(self, event):
        numDegrees = event.angleDelta().y() / 8
        numSteps = numDegrees / 15
        self._numScheduledScalings += numSteps
        if self._numScheduledScalings * numSteps < 0:
            self._numScheduledScalings = numSteps
        anim = QtCore.QTimeLine(350, self)
        anim.setUpdateInterval(20)
        anim.valueChanged.connect(self.scalingTime)
        anim.finished.connect(self.animFinished)
        anim.start()

    def scalingTime(self, x):
        factor = 1.0 + float(self._numScheduledScalings) / 300.0
        self.scale(factor, factor)

    def animFinished(self):
        if self._numScheduledScalings > 0:
            self._numScheduledScalings -= 1
        else:
            self._numScheduledScalings += 1

    def mousePressEvent(self, event):
        p = self.mapToScene(event.pos())
        
        if event.button() == QtCore.Qt.MidButton:
            self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)

            event = QtGui.QMouseEvent(
                QtCore.QEvent.GraphicsSceneDragMove, 
                event.pos(), 
                QtCore.Qt.MouseButton.LeftButton, 
                QtCore.Qt.MouseButton.LeftButton, 
                QtCore.Qt.KeyboardModifier.NoModifier
            )

        elif event.button() == QtCore.Qt.LeftButton:
            self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)

        self.coordinates = [p.x(), p.y()]
        QtWidgets.QGraphicsView.mousePressEvent(self, event)
   
    def mouseReleaseEvent(self, event):
        QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
        self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
        if event.button() == QtCore.Qt.LeftButton:
            p = self.mapToScene(event.pos())
            self.coordinates.extend([p.x(), p.y()])
            text = 'select area : ' + ','.join([ str(int(c)) for c in self.coordinates])
            self.label.setText(text)

def main():
    app = QtWidgets.QApplication(sys.argv)

    widget = QtWidgets.QWidget(None)

    leyout = QtWidgets.QVBoxLayout(widget)
    leyout.addWidget( QtWidgets.QLabel(widget) )
    leyout.addWidget( GraphicsView(widget) )

    widget.setLayout(leyout)
    widget.show()

    app.exec()
 
if __name__ == '__main__':
    main()

コメント

タイトルとURLをコピーしました