2016/12/04(日) [n年前の日記]
#1 [python] PySideでCGツールっぽい選択範囲を作れるかテスト
PySideを使って、CGツールでよく見かける蟻の行進(Marching ants)っぽい選択範囲だかラバーバンドだかを表示できるかテスト。
_graphicsrubberband.py
テスト用画像。 _tmp_bg.png 。
見た目はそれっぽくなった気がする。なんとかなりそう。
_graphicsrubberband.py
u"""
CGツール用RubberBandを実装.
蟻の行進(Marching ant)が表示できるのか実験。
動作確認環境 : Windows10 x64 + Python 2.7.12 + PySide 1.2.4
"""
import sys
from PySide.QtCore import * # NOQA
from PySide.QtGui import * # NOQA
class CGRuberBand(QGraphicsPolygonItem, QObject):
u"""CGツール用のラバーバンド. 2つのクラスを継承してる."""
def __init__(self, rect, parent=None, scene=None):
"""init."""
QGraphicsPolygonItem.__init__(self, parent)
QObject.__init__(self, parent) # タイマーを使うために多重継承
self.start_pos = QPoint()
self.end_pos = QPoint()
# 背景用のQGraphics*Itemを用意する
# 白黒の点線を描画するため、背景は白、自分自身は黒で線を描画する
self.bg = QGraphicsPolygonItem(self)
# 背景用のpen設定。白一色の線。
bg_pen = QPen(QBrush(Qt.white), 1, s=Qt.SolidLine, c=Qt.SquareCap,
j=Qt.MiterJoin)
self.bg.setPen(bg_pen)
self.bg_pen = bg_pen
scene.addItem(self.bg)
# 点線用のpen設定。黒の点線。
pen = QPen(QBrush(Qt.black), 1, s=Qt.DashLine, c=Qt.SquareCap,
j=Qt.MiterJoin)
pen.setDashPattern([3, 3]) # Dash line のパターンを設定
self.setPen(pen)
self.pen = pen
# 内部を半透明で塗り潰すなら以下のコメントアウトを外す
# self.brush = QBrush(QColor(48, 160, 255, 64))
# self.setBrush(self.brush)
self.startTimer(25) # 一定時間毎に処理を呼んで点線をアニメさせる
self.hide() # 発生時は非表示にしておく
scene.addItem(self) # 自身をsceneに追加登録
def timerEvent(self, event):
u"""一定時間毎に呼ばれる処理."""
if self.isVisible():
# 点線の描画開始位置を変化させて蟻の行進に見せる
do = (self.pen.dashOffset() + 1) % 6
self.pen.setDashOffset(do)
self.setPen(self.pen)
def set_geometry(self, p0, p1):
u"""範囲を設定."""
self.start_pos = QPoint(p0)
self.end_pos = QPoint(p1)
# ドット単位で正確に表示するためにwidthとheightを調整
rect = QRect(p0, p1)
w = p1.x() - p0.x()
h = p1.y() - p0.y()
rect.setWidth(w)
rect.setHeight(h)
self.poly = QPolygonF(rect)
self.bg.setPolygon(self.poly)
self.setPolygon(self.poly)
def show(self):
u"""表示."""
self.bg.show()
super(CGRuberBand, self).show()
def hide(self):
u"""非表示."""
self.bg.hide()
super(CGRuberBand, self).hide()
class GView(QGraphicsView):
"""Graphics View."""
def __init__(self, *argv, **keywords):
"""init."""
super(GView, self).__init__(*argv, **keywords)
scene = QGraphicsScene(self)
self.setScene(scene)
# 背景画像
pm = QPixmap("./tmp_bg.png")
pm_item = QGraphicsPixmapItem(pm)
scene.addItem(pm_item)
# ラバーバンドを生成
# sceneへの追加登録は、sceneを渡してラバーバンド側で行う
self.rband = CGRuberBand(QRect(), parent=None, scene=self.scene())
self.start_pos = QPoint()
self.end_pos = QPoint()
self.selecting = False
def mousePressEvent(self, event):
u"""マウスボタンが押された."""
if event.button() == Qt.LeftButton:
# 左ボタンが押された
if not self.selecting:
self.start_pos = event.pos() # クリックした座標を記憶
self.end_pos = self.start_pos
self.set_rubberband_geometry(self.start_pos, self.end_pos)
self.rband.show()
self.selecting = True
elif event.button() == Qt.RightButton:
# 右ボタンが押された
self.clear_rubberband_area()
def mouseMoveEvent(self, event):
u"""マウスカーソルが動いた."""
if self.selecting:
self.end_pos = event.pos()
self.set_rubberband_geometry(self.start_pos, self.end_pos)
def mouseReleaseEvent(self, event):
u"""マウスボタンが離された."""
if event.button() == Qt.LeftButton:
self.selecting = False
def set_rubberband_geometry(self, p0, p1):
u"""ラバーバンドの範囲を設定."""
if p0.isNull():
return
if p0 == p1:
p0 = self.mapToScene(p0).toPoint()
p1 = p0
self.rband.set_geometry(p0, p1)
self.set_status("(%d, %d)" % (p0.x(), p0.y()))
return
# scene上の座標値に変換
p0 = self.mapToScene(p0).toPoint()
p1 = self.mapToScene(p1).toPoint()
self.rband.set_geometry(p0, p1)
x0, y0 = p0.x(), p0.y()
x1, y1 = p1.x(), p1.y()
dx, dy = x1 - x0, y1 - y0
if dx >= 0:
dx += 1
elif dx < 0:
dx = -dx + 1
if dy >= 0:
dy += 1
if dy < 0:
dy = -dy + 1
s = "(%d, %d) - (%d, %d) : (%d x %d)" % (x0, y0, x1, y1, dx, dy)
self.set_status(s)
def clear_rubberband_area(self):
u"""ラバーバンドの範囲をクリア."""
self.rband.hide()
self.start_pos = QPoint()
self.end_pos = QPoint()
def set_status(self, str):
u"""ステータスバー相当のテキストを設定."""
self.parent().set_status(str)
class MyWidget(QWidget):
u"""メインウインドウ相当."""
def __init__(self, *argv, **keywords):
"""init."""
super(MyWidget, self).__init__(*argv, **keywords)
self.gview = GView(self)
self.lbl = QLabel("Ready", self)
l = QVBoxLayout()
l.addWidget(self.gview)
l.addWidget(self.lbl)
self.setLayout(l)
def set_status(self, str):
u"""ステータスバー相当にテキストを設定."""
self.lbl.setText(str)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MyWidget()
w.show()
sys.exit(app.exec_())
テスト用画像。 _tmp_bg.png 。
見た目はそれっぽくなった気がする。なんとかなりそう。
◎ 実装についてメモ。 :
PySide には、線の設定時に使う
_QPen
てのがあるのだけど、その QPen の style に
_Qt.DashLine
を指定すれば点線が引けるし、dashOffset() を使うと点線の描画開始位置をずらせるので、それらを使えば蟻の行進ができそうだなと。
ただ、実際に黒い点線を描いてみたところ、背景が黒かった場合にほとんど点線が見えなくて。まあ、黒い背景に黒い点線を描いてるから見えなくて当然だけど。
改めて GIMP の選択範囲の境界線を確認してみたところ、必ず白と黒の点線を描いていた。そうすることで、どんな背景でも境界線がちゃんと分かる上に、境界線の色を背景に応じて変更、等の処理をしなくて済む。
ということは、PySide上でも白と黒の点線を描けばいい…のだけど、どうやら QPen は背景色と前景色の2つを指定できない模様。なので、QGraphicsPolygonItem の中でもう一つ QGraphicsPolygonItem を確保して、白一色の線の上に、黒い点線を乗せることで白黒の点線に見せかけてみたり。
点線の描画開始位置を一定時間毎にずらすあたりは、QObject も継承(多重継承?)して _QObject.startTimer() を使った。コレを使うと、指定したミリ秒毎に、timerEvent(self, event) という関数が呼ばれるようになる。その中で dashOffset() を変化させてやれば、見た目、蟻の行進っぽくなる。
ちょっとハマったのは、境界線の描画位置。ドットエディタ用として使いたいからドット単位でキッチリと表示したかったのだけど、何故か範囲の大きさが1ドット大きくなって。
矩形情報を持てる _QRect のドキュメントを眺めて理由が分かった。右、および下については、内部で持ってる座標値の1ドット外側に境界線を描く仕様になっているのだな…。ということで、そのあたりはちょっと調整。もっとも、今回はドットエディタ用を意識しつつ書いてるからそういう調整処理が必要になるわけで、一般的なCGツールのように選択範囲境界線の内側に対して処理が行われるのだ、という取り決めにしておくなら問題にはならないような気もする。
ただ、実際に黒い点線を描いてみたところ、背景が黒かった場合にほとんど点線が見えなくて。まあ、黒い背景に黒い点線を描いてるから見えなくて当然だけど。
改めて GIMP の選択範囲の境界線を確認してみたところ、必ず白と黒の点線を描いていた。そうすることで、どんな背景でも境界線がちゃんと分かる上に、境界線の色を背景に応じて変更、等の処理をしなくて済む。
ということは、PySide上でも白と黒の点線を描けばいい…のだけど、どうやら QPen は背景色と前景色の2つを指定できない模様。なので、QGraphicsPolygonItem の中でもう一つ QGraphicsPolygonItem を確保して、白一色の線の上に、黒い点線を乗せることで白黒の点線に見せかけてみたり。
点線の描画開始位置を一定時間毎にずらすあたりは、QObject も継承(多重継承?)して _QObject.startTimer() を使った。コレを使うと、指定したミリ秒毎に、timerEvent(self, event) という関数が呼ばれるようになる。その中で dashOffset() を変化させてやれば、見た目、蟻の行進っぽくなる。
ちょっとハマったのは、境界線の描画位置。ドットエディタ用として使いたいからドット単位でキッチリと表示したかったのだけど、何故か範囲の大きさが1ドット大きくなって。
矩形情報を持てる _QRect のドキュメントを眺めて理由が分かった。右、および下については、内部で持ってる座標値の1ドット外側に境界線を描く仕様になっているのだな…。ということで、そのあたりはちょっと調整。もっとも、今回はドットエディタ用を意識しつつ書いてるからそういう調整処理が必要になるわけで、一般的なCGツールのように選択範囲境界線の内側に対して処理が行われるのだ、という取り決めにしておくなら問題にはならないような気もする。
◎ 課題。 :
後は境界線をドラッグして範囲をリサイズできれば、なのだけど、この先は結構面倒臭そうで。以下のような仕様になるだろうけど…。
また、マウスカーソルを移動しただけでも常時それらの判別処理をしないといけないので、PySideの場合はマウストラッキング(常時マウスカーソル座標を取得できる状態)が必要になる。
これだけ面倒臭い処理をするのだから、何かのクラス、例えば QGraphics*Item を継承したクラスの中に関連処理をまとめてしまってあちこちで流用したいところだけど…。QGraphicsView側でマウス座標の取得等をやってる関係で、そのあたりの整理が悩ましいというか。
巷のグラフィックツールは、よくまああれだけ上手に作ってあるものだなあ、などと再認識。
- 境界線の上にマウスカーソルが乗ったらマウスカーソルが変化する。
- 境界線の左上、右下、で「\」のマウスカーソルになる。
- 境界線の右上、左下、で「/」のマウスカーソルになる。
- 境界線の上、下、で「|」のマウスカーソルになる。
- 境界線の左、右、で「―」のマウスカーソルになる。
- マウスカーソルが変わっている時にドラッグすると下にある境界線の位置を変更できる。通常のマウスカーソルの時は新規に選択範囲を指定する。
また、マウスカーソルを移動しただけでも常時それらの判別処理をしないといけないので、PySideの場合はマウストラッキング(常時マウスカーソル座標を取得できる状態)が必要になる。
これだけ面倒臭い処理をするのだから、何かのクラス、例えば QGraphics*Item を継承したクラスの中に関連処理をまとめてしまってあちこちで流用したいところだけど…。QGraphicsView側でマウス座標の取得等をやってる関係で、そのあたりの整理が悩ましいというか。
巷のグラフィックツールは、よくまああれだけ上手に作ってあるものだなあ、などと再認識。
[ ツッコむ ]
以上です。