mieki256's diary



2016/10/25(火) [n年前の日記]

#1 [python] PySideでマウス座標を常に取得

PySideを使って、QGraphicsScene の中でマウス座標を常に取得することができるか実験。要は、マウスカーソルを動かすとその位置に追従して画像も表示される、みたいなことをしたい。お絵かきソフトではブラシ枠がマウスカーソル位置に表示されてたりするけど、おおよそあんな感じで。

_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()

gview_mousecursor_ss01.gif

一応できたっぽい。

ブラシ画像は、 _brush.png を使った。

mouseTracking が肝らしい。 :

QGraphicsScene には、mouseMoveEvent() という、マウスカーソルを動かした時に呼ばれるメソッドがあらかじめ用意されているのだけど。この mouseMoveEvent() は、デフォルトでは「マウスボタンを押してる間」しか呼ばれない。要は、ドラッグ操作を前提としているわけで。

しかし今回は、マウスボタンの状態に関係なく、マウスカーソルが動いたらとにかく座標を取得したい。

その場合は、QGraphicsScene の親に相当する、QGraphicsView の viewport() に対して、setMouseTracking(True) をしてやるといいらしい。

_シーンでマウストラッキング - TB-code

試してみたところ、常にマウスカーソル座標が取得できるようになった。

他にも、全てのイベントを一旦受け付けて、イベント種類でフィルタリングして、マウスカーソルが動いたかどうかを検出する、というやり方もあるらしい。

スクロールバーの長さ。 :

今まで QGraphicsView を使った際に、スクロールバーの長さが妙な感じになっていて悩んでたけど。self.scene().setSceneRect() で、ちゃんとした QRectf を渡してやればそれっぽいスクロールバーの長さになってくれることが分かった。

例えば、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* を持ち出すほどの処理ではない、かもしれない。

#2 [prog][windows] AtomのScriptがやっとWindowsに対応してくれた

Atomエディタの拡張として、 _Script という、PythonスクリプトだのRubyスクリプトだのをその場で実行できる拡張があるのだけど。

ふと気付いたら、ようやく Windows上でShebangを無視してくれるようになったようで。

_Ignore firstline check on Windows - rgbkrk/atom-script@4374623

修正箇所は、たったの1行っぽいけど。

長かった…。今まで、アップデートでファイルが差し変わるたびに、 _毎回自分で修正 してましたよ。実にアホらしかった…。

ちょっと解説。 :

Python や Ruby等のスクリプトは、えてして、ソースの1行目に、 _Shebang(シバン行) と呼ばれる、以下のような記述があって。
#!/usr/bin/env python

これは *NIX用の記述で…。

*NIX は Windows と違って、ファイルの拡張子と、そのファイルを利用するプログラムを関連付けてない。だから、「オイ、*NIXよ。このファイルを実行しろや」と指示しても、何を使って実行すればいいのか分からない。なので、ファイルの1行目を読んで、「ふむ。要は、この /usr/bin/env python なる者に、このファイルをそのまま渡してやればよろしいのですな」と判断して実行するわけで。

ただ、*NIX の世界は、環境によって、python だの ruby だのをどの場所に置いているか違っていたりする。だから、python等の場所(ファイルパス)を直接書くと問題が発生する。他所からスクリプトを持ってくるたびに、1行目を自分の環境に合わせて修正してやる羽目になる。

それはマズいよね、面倒臭いよね、ってことで、*NIX 世界の人達は、/usr/bin/ に _env ってツールを入れといて、env を経由してやれば python だの ruby だのは探して呼んでくれるよ、○○がどこに置いてあるかなんて気にしなくて済むよ、ということにした。これで、どの環境に持っていっても動いてくれるスクリプトが書けるようになったわけで。

ところが、Windows には /usr/bin/ なんてフォルダ自体が無いし、もちろん env も入ってない。そもそも Windows は、ファイルの拡張子で何のプログラムを呼び出すか覚えているから、Shebang を見る必要が無い。

ということで、Windows用の Python や Ruby は、1行目の Shebang を無視するようになっていて。せいぜい見るとしても、そこに書かれてるオプション文字列ぐらいしかチェックしない。「#!/usr/bin/env」の部分は無視しちゃう。

こうすることで、*NIX でも Windows でも、同じスクリプトソースを動かせるようにしていたわけですよ。

ところが、Atom拡張の Script は、1行目の Shebang をキッチリ見てしまう。*NIX でも Mac でも Windows でも、「/usr/bin/env python を呼べばええのやな」と処理していて。結果、Windows上で動かした時だけ、「/usr/bin/env なんて知らねえよボケ」とエラーが出ていたという。

更に、「コレ、エラー出るんだけど」とバグ報告があっても、「は? Linux や Mac では動くんですけど?」「Shebang に #!C:\〜\python って書けば動くだろ? 1行目には正確なパスを書くもんだぞ」とか返されちゃって。コイツラ、「#!/usr/bin/env 〜」の意味が分かってねえ…。

せっかく Pythonその他が、どのOS上でも動くようにと気配りして実装されてるのに、Atom拡張の Script はその努力を台無しにしていたわけですよ。…どうやら作者様が Windows を持ってないらしいので仕方ないところもありますが。

てなわけで、そのあたりの問題が解決したのはありがたいことだなと。こうして長々と誰も読まないであろう解説をテンション上がって書いてしまうぐらいに、個人的には大変喜んでおります。ありがたや。修正してくれてマジthx。

以上、1 日分です。

過去ログ表示

Prev - 2016/10 - Next
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project