2016/10/25(火) [n年前の日記]
#1 [python] PySideでマウス座標を常に取得
PySideを使って、QGraphicsScene の中でマウス座標を常に取得することができるか実験。要は、マウスカーソルを動かすとその位置に追従して画像も表示される、みたいなことをしたい。お絵かきソフトではブラシ枠がマウスカーソル位置に表示されてたりするけど、おおよそあんな感じで。
_gview_mousecursor.py
一応できたっぽい。
ブラシ画像は、 _brush.png を使った。
_gview_mousecursor.py
""" PySide + QGraphicsView上でマウスカーソルに何かを追従させる 動作確認環境 : Windows10 x64 + Python 2.7.11 + PySide 1.2.4 """ import sys from PySide.QtCore import * from PySide.QtGui import * brushFile = "brush.png" canvasSize = (640, 480) padding = 48 status = None class DrawAreaScene(QGraphicsScene): """ 描画ウインドウ用Scene """ def __init__(self, *argv, **keywords): super(DrawAreaScene, self).__init__(*argv, **keywords) global brushFile global canvasSize global padding # Scene に 空QPixmap を追加 w, h = canvasSize self.pixmap = QPixmap(w, h) self.pixmap.fill(QColor(192, 192, 192, 255)) self.imgItem = QGraphicsPixmapItem(self.pixmap) self.addItem(self.imgItem) self.imgItem.setX(padding) self.imgItem.setY(padding) # Scene にブラシ画像を追加 self.brushPixmap = QPixmap(brushFile) self.brushImgItem = QGraphicsPixmapItem(self.brushPixmap) self.addItem(self.brushImgItem) def mousePressEvent(self, event): """ マウスボタンを押した """ x, y = self.getMousePos(event, "Click") def mouseReleaseEvent(self, event): """ マウスボタンを離した """ x, y = self.getMousePos(event, "Release") def mouseMoveEvent(self, event): """ マウスを動かしてる時に呼ばれる処理。 デフォルトではマウスボタンを押してる間(ドラッグ中)しか呼ばれないが、 親のQGraphicsView で viewport() の mouseTracking を True にすれば マウスを動かした際に常時呼ばれるようになる。 """ x, y = self.getMousePos(event, "Move") # ブラシが非表示なら表示を有効化 if not self.brushImgItem.isVisible(): self.setVisibleBrush(True) # ブラシの表示位置を変更 pm = self.brushImgItem.pixmap() xd = pm.width() / 2 yd = pm.height() / 2 self.brushImgItem.setOffset(int(x - xd), int(y - yd)) def getMousePos(self, event, msg): """ マウス座標を取得 """ x = event.scenePos().x() y = event.scenePos().y() global status status.showMessage("(%d , %d) %s" % (x, y, msg)) return (x, y) def setVisibleBrush(self, flag): """ ブラシ表示の有効無効切り替え """ self.brushImgItem.setVisible(flag) class DrawAreaView(QGraphicsView): """ メインになるQGraphicsView """ def __init__(self, *argv, **keywords): super(DrawAreaView, self).__init__(*argv, **keywords) self.setBackgroundBrush(QColor(64, 64, 64, 255)) # 背景色を設定 self.setCacheMode(QGraphicsView.CacheBackground) # self.setRenderHints(QPainter.Antialiasing | # QPainter.SmoothPixmapTransform | # QPainter.TextAntialiasing) # Sceneを登録 scene = DrawAreaScene(self) self.setScene(scene) self.setSceneNewRect() # 子のSceneに対してマウストラッキングを有効に vp = self.viewport().setMouseTracking(True) def resizeEvent(self, event): """ リサイズ時に呼ばれる処理 """ super(DrawAreaView, self).resizeEvent(event) self.setSceneNewRect() def scrollContentsBy(self, dx, dy): """ スクロールバー操作時に呼ばれる処理 """ # スクロール中、Scene内にブラシがあると # 何故かゴミが残るので、ブラシを非表示にしている self.scene().setVisibleBrush(False) super(DrawAreaView, self).scrollContentsBy(dx, dy) def setSceneNewRect(self): """ Sceneの矩形を更新 """ # 以下は、Sceneのアイテム群境界ボックスを取得する例 # rect = self.scene().itemsBoundingRect() # 以下は、viewport のサイズを指定する例 # rect = QRectf(self.viewport().rect()) global canvasSize global padding w, h = canvasSize # キャンバス周辺に余白を設けたサイズを求める w += padding * 2 h += padding * 2 rect = QRectF(0, 0, w, h) # Sceneの矩形を更新。自動でスクロールバーの長さも変わってくれる self.scene().setSceneRect(rect) class MyVWidget(QWidget): """ メインウインドウ周辺に配置するWidget """ def __init__(self, parent=None): super(MyVWidget, self).__init__(parent) l = QVBoxLayout() # 縦に並べる l.addWidget(QLabel("Dummy")) self.setLayout(l) class MyMainWindow(QMainWindow): """ メインウインドウ """ def __init__(self, *argv, **keywords): super(MyMainWindow, self).__init__(*argv, **keywords) self.setWindowTitle("Mouse Tracking Test") self.resize(640, 480) # メニューバー mb = QMenuBar() file_menu = QMenu("&File", self) exit_action = file_menu.addAction("&Close") exit_action.setShortcut('Ctrl+Q') exit_action.triggered.connect(qApp.quit) mb.addMenu(file_menu) self.setMenuBar(mb) # ステータスバー global status status = QStatusBar(self) self.setStatusBar(status) status.showMessage("Status Bar") # 左ドック self.leftDock = QDockWidget("Left Dock", self) self.leftDock.setWidget(MyVWidget(self)) self.addDockWidget(Qt.LeftDockWidgetArea, self.leftDock) # 中央Widget self.gview_image = DrawAreaView(self) self.setCentralWidget(self.gview_image) def main(): """ メイン処理 """ # このあたりを指定すると描画が速くなるという話を見かけたが、 # "native"、"raster"、"opengl" を指定しても結果は変わらなかった… QApplication.setGraphicsSystem("raster") app = QApplication(sys.argv) w = MyMainWindow() w.show() sys.exit(app.exec_()) if __name__ == '__main__': main()
一応できたっぽい。
ブラシ画像は、 _brush.png を使った。
◎ mouseTracking が肝らしい。 :
QGraphicsScene には、mouseMoveEvent() という、マウスカーソルを動かした時に呼ばれるメソッドがあらかじめ用意されているのだけど。この mouseMoveEvent() は、デフォルトでは「マウスボタンを押してる間」しか呼ばれない。要は、ドラッグ操作を前提としているわけで。
しかし今回は、マウスボタンの状態に関係なく、マウスカーソルが動いたらとにかく座標を取得したい。
その場合は、QGraphicsScene の親に相当する、QGraphicsView の viewport() に対して、setMouseTracking(True) をしてやるといいらしい。
_シーンでマウストラッキング - TB-code
試してみたところ、常にマウスカーソル座標が取得できるようになった。
他にも、全てのイベントを一旦受け付けて、イベント種類でフィルタリングして、マウスカーソルが動いたかどうかを検出する、というやり方もあるらしい。
しかし今回は、マウスボタンの状態に関係なく、マウスカーソルが動いたらとにかく座標を取得したい。
その場合は、QGraphicsScene の親に相当する、QGraphicsView の viewport() に対して、setMouseTracking(True) をしてやるといいらしい。
_シーンでマウストラッキング - TB-code
試してみたところ、常にマウスカーソル座標が取得できるようになった。
他にも、全てのイベントを一旦受け付けて、イベント種類でフィルタリングして、マウスカーソルが動いたかどうかを検出する、というやり方もあるらしい。
◎ スクロールバーの長さ。 :
今まで QGraphicsView を使った際に、スクロールバーの長さが妙な感じになっていて悩んでたけど。self.scene().setSceneRect() で、ちゃんとした QRectf を渡してやればそれっぽいスクロールバーの長さになってくれることが分かった。
例えば、Sceneのアイテム群境界ボックスを取得してやれば、それらしくなるし。
あるいは、QGraphicsView の viewport のサイズを指定してやれば、QGraphicsView のスクロール領域サイズ = Sceneのサイズになるのでスクロールバーが消えてくれるし。
今回は、Scene が持ってる空のQPixmap(キャンバス相当)サイズ+余白を指定して、キャンバスの端の部分もある程度表示できるようにしてみたり。
例えば、Sceneのアイテム群境界ボックスを取得してやれば、それらしくなるし。
rect = self.scene().itemsBoundingRect() self.scene().setSceneRect(rect)
あるいは、QGraphicsView の viewport のサイズを指定してやれば、QGraphicsView のスクロール領域サイズ = Sceneのサイズになるのでスクロールバーが消えてくれるし。
rect = QRectf(self.viewport().rect()) self.scene().setSceneRect(rect)
今回は、Scene が持ってる空のQPixmap(キャンバス相当)サイズ+余白を指定して、キャンバスの端の部分もある程度表示できるようにしてみたり。
w += padding * 2 h += padding * 2 rect = QRectF(0, 0, w, h) self.scene().setSceneRect(rect)ただし、この場合は、空のQPixmap の offset を余白分ずらしておく必要有。
◎ 描画速度が遅い。 :
一応処理はできたけど、別の問題が出てきた。マウスを素早く動かすと、追従してる画像がちらついてしまう。どうも描画が遅いというか、パフォーマンスが出ないというか。
アプリの開始時に QApplication.setGraphicsSystem("opengl") を呼ぶことで描画速度が変わってくるという話も見かけたけど、自分の手元の環境では特に変化は見られなかった。まあ、仮に変化があったとしても、環境によって、opengl を指定するとむしろ遅くなるとか、native と raster の速度が変わらんとかあるようで。改善してくれることを期待しちゃダメ、ってことだろうなと。
また、「QGraphics* は複数のアイテムを登録してアレコレできる分、色々と処理が遅い。処理速度を求めるなら使うべきではない」という主張も見かけたし、あるいは、「最低限の再描画領域をその都度求めてクリッピングしてやれば速度が稼げる」という話も見かけた。
もっとも、考えてみれば…。マウスを素早く動かしてる時に正確な画像描画を目にしないと作業ができないというわけでもないだろうし。このくらいは目を瞑るのもアリかもしれないなと。
あるいは、お絵かきソフトの類は大量のアイテムを登録して描画したいわけでもないだろうから、別のWidgetを使ってどうにかできないか検討するのもアリかもしれず。レイヤーに相当する QPixmap を何枚か上書きしていって、最後にブラシ枠画像を描画すればいいのだろうし。であれば、QGraphics* を持ち出すほどの処理ではない、かもしれない。
アプリの開始時に QApplication.setGraphicsSystem("opengl") を呼ぶことで描画速度が変わってくるという話も見かけたけど、自分の手元の環境では特に変化は見られなかった。まあ、仮に変化があったとしても、環境によって、opengl を指定するとむしろ遅くなるとか、native と raster の速度が変わらんとかあるようで。改善してくれることを期待しちゃダメ、ってことだろうなと。
また、「QGraphics* は複数のアイテムを登録してアレコレできる分、色々と処理が遅い。処理速度を求めるなら使うべきではない」という主張も見かけたし、あるいは、「最低限の再描画領域をその都度求めてクリッピングしてやれば速度が稼げる」という話も見かけた。
もっとも、考えてみれば…。マウスを素早く動かしてる時に正確な画像描画を目にしないと作業ができないというわけでもないだろうし。このくらいは目を瞑るのもアリかもしれないなと。
あるいは、お絵かきソフトの類は大量のアイテムを登録して描画したいわけでもないだろうから、別のWidgetを使ってどうにかできないか検討するのもアリかもしれず。レイヤーに相当する QPixmap を何枚か上書きしていって、最後にブラシ枠画像を描画すればいいのだろうし。であれば、QGraphics* を持ち出すほどの処理ではない、かもしれない。
[ ツッコむ ]
以上です。