やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

マウスでドットを描く

 マウスイベントに応じて。

成果物

demo demo2

 マウス左ボタンで描画。マウス右ボタンで消す。

コード

main.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, numpy
from PySide2 import QtCore, QtGui, QtWidgets

class Window(QtWidgets.QWidget):
    def __init__(self):
        super(self.__class__, self).__init__()
        view = GraphicView()
        scroller = QtWidgets.QScrollArea()
        scroller.setWidget(view)

        layout = QtWidgets.QGridLayout()
        layout.addWidget(scroller, 0, 0)

        self.setLayout(layout)
        self.resize(view.width(), view.height())
        self.setWindowTitle("QGraphics View Scene Item + QScrollArea")
        self.show()
     
class GraphicView(QtWidgets.QGraphicsView):
    def __init__(self):
        QtWidgets.QGraphicsView.__init__(self)
        self.setWindowTitle("QGraphicsScene draw Grid")
        self.setScene(EditorScene(self))

    def mousePressEvent(self, event):
        super(self.__class__, self).mousePressEvent(event)
        if self.scene(): self.scene().update()

    def mouseMoveEvent(self, event):
        super(self.__class__, self).mouseMoveEvent(event)
        if self.scene(): self.scene().update()

class EditorScene(QtWidgets.QGraphicsScene):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.size = 16
        self.scale = 32
        self.setSceneRect(0, 0, self.size*self.scale, self.size*self.scale)

        grid = GridItem()
        self.addItem(grid)

        bg = BackgroundItem()
        self.addItem(bg)

        drawable = DrawableItem()
        self.addItem(drawable)

        bg.setZValue(0)
        drawable.setZValue(1)
        grid.setZValue(9999)

    def mousePressEvent(self, event):
        for item in self.items():
            item.mousePressEvent(event)
        super(self.__class__, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        for item in self.items():
            item.setAcceptHoverEvents(True)
            item.mouseMoveEvent(event)
        super(self.__class__, self).mouseMoveEvent(event)

class DrawableItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.setAcceptHoverEvents(True)
        self.size = 16
        self.scale = 32
        self.pixels = numpy.zeros(self.size*self.size).reshape(self.size, self.size)

    def paint(self, painter, option, widget):
        painter.fillRect(widget.rect(), QtGui.QBrush( QtGui.QColor(0,0,0,0), QtCore.Qt.SolidPattern))
        for y in range(self.size):
            for x in range(self.size):
                if 1 == self.pixels[y][x]:
                    painter.fillRect(x*self.scale, y*self.scale, self.scale, self.scale, QtGui.QBrush( QtGui.QColor(255,0,0,128), QtCore.Qt.SolidPattern))

    def mouseMoveEvent(self, event):
        pos = event.scenePos()
        x = int(pos.x()//self.scale)
        y = int(pos.y()//self.scale)
        print('Move', str(pos.x()//self.scale), str(pos.y()//self.scale))
        if event.buttons() & QtCore.Qt.LeftButton:
            self.pixels[y][x] = 1
            print('L DRAG!!', x, y)
        if event.buttons() & QtCore.Qt.RightButton:
            self.pixels[y][x] = 0
            print('R DRAG!!', x, y)

    def mousePressEvent(self, event):
        pos = event.scenePos()
        x = int(pos.x()//self.scale)
        y = int(pos.y()//self.scale)
        print(type(pos.x()))
        print('Press', str(pos.x()//self.scale), str(pos.y()//self.scale))
        if event.buttons() & QtCore.Qt.LeftButton:
            self.pixels[y][x] = 1
            print('L PRESS!!', x, y)
        if event.buttons() & QtCore.Qt.RightButton:
            self.pixels[y][x] = 0
            print('R PRESS!!', x, y)

    def mouseReleaseEvent(self, event):
        pass
    def mouseDoubleClickEvent(self, event):
        pass

class BackgroundItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.size = 16
        self.scale = 32
        self.colors = [QtGui.QColor(196,196,196,255), QtGui.QColor(232,232,232,255)]
    def paint(self, painter, option, widget):
        for i in range(self.size*self.size):
            x = (i % self.size)
            y = (i // self.size)
            color = QtGui.QColor(128,128,128,255) if 0 == (i % 2) and 0 == (x % 2) else QtGui.QColor(196,196,196,255)
            painter.fillRect(x * (self.scale),               y * (self.scale),               self.scale//2, self.scale//2, self.colors[0])
            painter.fillRect(x * (self.scale)+self.scale//2, y * (self.scale)+self.scale//2, self.scale//2, self.scale//2, self.colors[0])
            painter.fillRect(x * (self.scale)+self.scale//2, y * (self.scale),               self.scale//2, self.scale//2, self.colors[1])
            painter.fillRect(x * (self.scale),               y * (self.scale)+self.scale//2, self.scale//2, self.scale//2, self.colors[1])

class GridItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.size = 16
        self.scale = 32
    def paint(self, painter, option, widget):
        painter.fillRect(widget.rect(), QtGui.QBrush(QtGui.QColor(0,0,0,0), QtCore.Qt.SolidPattern))
        lines = []
        for y in range(self.size+1):
            lines.append(QtCore.QLine(0, y*self.scale, self.size*self.scale, y*self.scale))
        for x in range(self.size+1):
            lines.append(QtCore.QLine(x*self.scale, 0, x*self.scale, self.size*self.scale))
        painter.drawLines(lines)

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    sys.exit(app.exec_())
python3 main.py

demo

  • マウス左ボタンで描く
  • マウス右ボタンで消す

苦労した所

マウスのMoveイベント

 マウスを移動するたびに座標を取得したいとき、どう実装すればいいかわからなかった。要点は以下2つのAPI

 コード例は以下。

class DrawableItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.setAcceptHoverEvents(True)
    def mouseMoveEvent(self, event):
        print(event.scenePos())

 惑わされたのがhover系イベント。普通にmouseMoveEventだけを使うと、クリックしたときだけしか反応しなかった。そこでGraphicsItem.setAcceptHoverEvents()Trueを指定する必要がある。ならばQGraphicsItem.hoverMoveEvent()を使うのだろうと思う所だが違う。使うのはQGraphicsItem.mouseMoveEvent()だった。わけわからん。ググってもわからんかったので苦労した。

イベント・バブリング

 QGraphicsItemで描画イベント処理を実装した。クリックされたらドットが描かれるように。しかし描画されなかった。なぜかクリック後にウインドウを拡大・縮小すると表示された。つまりクリック後に再描画されていなかったのである。

 改善方法としては、親クラスでイベントハンドラをオーバーライドして、子ウィジェットのイベント呼出を実装してやる必要があった。超面倒くさい……。

class GraphicView(QtWidgets.QGraphicsView):
    def __init__(self):
        QtWidgets.QGraphicsView.__init__(self)
        self.setScene(EditorScene(self))
    def mousePressEvent(self, event):
        super(self.__class__, self).mousePressEvent(event)
        if self.scene(): self.scene().update()
    def mouseMoveEvent(self, event):
        super(self.__class__, self).mouseMoveEvent(event)
        if self.scene(): self.scene().update()

class EditorScene(QtWidgets.QGraphicsScene):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        grid     = GridItem();       self.addItem(grid);
        bg       = BackgroundItem(); self.addItem(bg);
        drawable = DrawableItem();   self.addItem(drawable)
        bg.setZValue(0); drawable.setZValue(1); grid.setZValue(9999);
    def mousePressEvent(self, event):
        for item in self.items():
            item.mousePressEvent(event)
        super(self.__class__, self).mousePressEvent(event)
    def mouseMoveEvent(self, event):
        for item in self.items():
            item.setAcceptHoverEvents(True)
            item.mouseMoveEvent(event)
        super(self.__class__, self).mouseMoveEvent(event)

class DrawableItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.setAcceptHoverEvents(True)
    def mousePressEvent(self, event): pass
    def mouseMoveEvent(self, event): pass

 イベントのコード書いてんだから察してくれよ。

所感

 やっとそれっぽくできた!

 しかしイベント処理のコードを書いたら一気に汚くなってきた……。

対象環境

$ uname -a
Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux