Last active
April 16, 2022 06:19
-
-
Save mieki256/02f07f15fce2483e0dd59200c5ba4364 to your computer and use it in GitHub Desktop.
PySideでCGツール用ラバーバンドを試しに書いてみる
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!python | |
# -*- mode: python; Encoding: utf-8; coding: utf-8 -*- | |
# Last updated: <2016/12/07 20:37:05 +0900> | |
u""" | |
PySideを使って、CGツール用を前提としたRubberBandを実装. | |
蟻の行進(Marching ant)を表示、 | |
かつ、境界線のドラッグでリサイズできる仕様を入れてみた。 | |
動作確認環境 : Windows10 x64 + Python 2.7.12 + PySide 1.2.4 | |
Author : mieki256 | |
License : CC0 / Public Domain | |
""" | |
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.s_pos = QPoint() | |
self.e_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_start_pos(self, p0): | |
u"""開始座標を指定.""" | |
self.s_pos = QPoint(p0) | |
self.e_pos = QPoint(p0) | |
self.update_rect(p0, p0) | |
def set_end_pos(self, p0): | |
u"""終了座標を指定.""" | |
self.e_pos = QPoint(p0) | |
self.update_rect(self.s_pos, p0) | |
def get_points(self): | |
u"""開始座標と終了座標を返す.""" | |
return (self.s_pos, self.e_pos) | |
def update_rect(self, p0, p1): | |
u"""選択範囲領域を更新.""" | |
# ドット単位で正確に表示するために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() | |
def normlize(self): | |
u"""保持してる2点の座標の大小関係を調整.""" | |
p0, p1 = self.normalize_points(self.s_pos, self.e_pos) | |
self.s_pos = p0 | |
self.e_pos = p1 | |
self.update_rect(p0, p1) | |
return (self.s_pos, self.e_pos) | |
def normalize_points(self, p0, p1): | |
u"""2点の座標が、左上 -> 右下になるように調整して返す.""" | |
x0, y0 = p0.toTuple() | |
x1, y1 = p1.toTuple() | |
if x0 > x1: | |
x0, x1 = x1, x0 | |
if y0 > y1: | |
y0, y1 = y1, y0 | |
return (QPoint(x0, y0), QPoint(x1, y1)) | |
def in_rubberband(self, mpos): | |
u"""境界線とアタリ判定して結果を None or 0-8で返す.""" | |
return self.in_check(self.s_pos, self.e_pos, mpos) | |
def in_check(self, p0, p1, mpos): | |
u"""境界線とアタリ判定して結果を None or 0-8で返す.""" | |
# どことも重ならないなら None | |
# 左上、上、右上 -> 0,1,2 | |
# 左、真ん中、右 -> 3,4,5 | |
# 左下、下、右下 -> 6,7,8 | |
x0, y0 = p0.toTuple() | |
x1, y1 = p1.toTuple() | |
mx, my = mpos.toTuple() | |
d0 = 2 | |
d1 = 2 | |
ret = -1 | |
if y0 - d0 <= my and my <= y0 + d1: | |
ret = 0 | |
elif y0 + d1 + 1 <= my and my <= y1 - d1 - 1: | |
ret = 3 | |
elif y1 - d1 <= my and my <= y1 + d0: | |
ret = 6 | |
else: | |
return None | |
if x0 - d0 <= mx and mx <= x0 + d1: | |
ret += 0 | |
elif x0 + d1 + 1 <= mx and mx <= x1 - d1 - 1: | |
ret += 1 | |
elif x1 - d1 <= mx and mx <= x1 + d0: | |
ret += 2 | |
else: | |
return None | |
return ret | |
def get_mouse_cursor(self, kind): | |
u"""境界線上の位置に応じて表示するマウスカーソルの種類を返す.""" | |
ret = Qt.ArrowCursor | |
if kind == 0 or kind == 8: | |
# 左上、右下 | |
ret = Qt.SizeFDiagCursor | |
elif kind == 2 or kind == 6: | |
# 右上、左下 | |
ret = Qt.SizeBDiagCursor | |
elif kind == 1 or kind == 7: | |
# 上、下 | |
ret = Qt.SizeVerCursor | |
elif kind == 3 or kind == 5: | |
# 左、右 | |
ret = Qt.SizeHorCursor | |
return ret | |
def adjust_rubberband(self, kind): | |
u"""境界線上の位置に応じて開始・終点座標を変更.""" | |
x0, y0 = self.s_pos.toTuple() | |
x1, y1 = self.e_pos.toTuple() | |
hv_fix = 0 | |
if kind == 1 or kind == 7: | |
hv_fix = 1 | |
elif kind == 3 or kind == 5: | |
hv_fix = 2 | |
else: | |
hv_fix = 0 | |
if kind == 0 or kind == 1: | |
self.s_pos = QPoint(x1, y1) | |
self.e_pos = QPoint(x0, y0) | |
elif kind == 2 or kind == 5: | |
self.s_pos = QPoint(x0, y1) | |
self.e_pos = QPoint(x1, y0) | |
elif kind == 8 or kind == 7: | |
self.s_pos = QPoint(x0, y0) | |
self.e_pos = QPoint(x1, y1) | |
elif kind == 6 or kind == 3: | |
self.s_pos = QPoint(x1, y0) | |
self.e_pos = QPoint(x0, y1) | |
else: | |
# リサイズできる場所にマウスカーソルは無い | |
return (False, hv_fix, self.s_pos, self.e_pos) | |
return (True, hv_fix, self.s_pos, self.e_pos) | |
class GView(QGraphicsView): | |
"""Graphics View.""" | |
def __init__(self, *argv, **keywords): | |
"""init.""" | |
super(GView, self).__init__(*argv, **keywords) | |
self.setMouseTracking(True) | |
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.clear_rubberband_area() | |
def clear_rubberband_area(self): | |
u"""ラバーバンドの範囲をクリア(初期化).""" | |
self.rband.hide() | |
self.s_pos = QPoint() | |
self.e_pos = QPoint() | |
self.selecting = False | |
self.hitchk = None | |
self.hv_fix = 0 | |
def mousePressEvent(self, event): | |
u"""マウスボタンが押された.""" | |
if event.button() == Qt.LeftButton: | |
# 左ボタンが押された | |
if not self.rubberband_resize_enable(): | |
# ラバーバンドの上にマウスカーソルが乗ってない | |
if not self.selecting: | |
# 選択範囲新規作成開始 | |
self.s_pos = event.pos() # クリックした座標を記憶 | |
self.e_pos = self.s_pos | |
p0 = self.mapToScene(self.s_pos).toPoint() | |
self.rband.set_start_pos(p0) | |
self.set_status("(%d, %d)" % (p0.x(), p0.y())) | |
self.rband.show() | |
self.selecting = True | |
else: | |
# ラバーバンド上にマウスカーソルが乗ってる | |
# 境界線移動モードに移行 | |
ret = self.rband.adjust_rubberband(self.hitchk) | |
fg, hv_fix, p0, p1 = ret | |
if fg: | |
self.hv_fix = hv_fix | |
self.s_pos = self.mapFromScene(p0) | |
self.e_pos = self.mapFromScene(p1) | |
self.rband.show() | |
self.selecting = True | |
elif event.button() == Qt.RightButton: | |
# 右ボタンが押された | |
self.clear_rubberband_area() | |
self.set_status("Selection Clear.") | |
def mouseMoveEvent(self, event): | |
u"""マウスカーソルが動いた.""" | |
if not self.rband.isVisible(): | |
return | |
if self.selecting: | |
# 選択範囲作成中 | |
x0, y0 = event.pos().toTuple() | |
if self.hv_fix == 1: | |
# y方向のみ移動可能 | |
self.e_pos.setY(y0) | |
elif self.hv_fix == 2: | |
# x方向のみ移動可能 | |
self.e_pos.setX(x0) | |
else: | |
# xy方向に移動可能 | |
self.e_pos = event.pos() | |
# ラバーバンドの範囲(終了座標)を更新 | |
p0 = self.mapToScene(self.e_pos).toPoint() | |
self.rband.set_end_pos(p0) | |
self.update_status() | |
else: | |
# 選択範囲が存在するのでマウスカーソルとアタリ判定する | |
p0 = self.mapToScene(event.pos()).toPoint() | |
self.hitchk = self.rband.in_rubberband(p0) | |
if self.hitchk is None or self.hitchk == 4: | |
# どことも当たってない | |
self.reset_mouse_cursor() | |
else: | |
# どこかに当たってるのでマウスカーソル形状を変更 | |
self.cur = self.rband.get_mouse_cursor(self.hitchk) | |
self.setCursor(self.cur) | |
def mouseReleaseEvent(self, event): | |
u"""マウスボタンが離された.""" | |
if event.button() == Qt.LeftButton: | |
self.selecting = False | |
p0, p1 = self.rband.normlize() | |
self.s_pos = self.mapFromScene(p0) | |
self.e_pos = self.mapFromScene(p1) | |
self.hv_fix = 0 | |
self.update_status() | |
def reset_mouse_cursor(self): | |
u"""マウスカーソルをリセット.""" | |
if self.cursor() != Qt.ArrowCursor: | |
self.setCursor(Qt.ArrowCursor) | |
def rubberband_resize_enable(self): | |
u"""ラバーバンドがリサイズ可能かどうかを返す.""" | |
if not self.rband.isVisible(): | |
return False | |
if self.hitchk is None or self.hitchk == 4: | |
return False | |
return True | |
def update_status(self): | |
u"""ステータスバー相当の情報を更新.""" | |
p0, p1 = self.rband.get_points() | |
x0, y0 = p0.toTuple() | |
x1, y1 = p1.toTuple() | |
dx = x1 - x0 | |
if dx >= 0: | |
dx += 1 | |
elif dx < 0: | |
dx = -dx + 1 | |
dy = y1 - y0 | |
if dy >= 0: | |
dy += 1 | |
elif dy < 0: | |
dy = -dy + 1 | |
s = "(%d, %d) - (%d, %d) : (%d x %d)" % (x0, y0, x1, y1, dx, dy) | |
self.set_status(s) | |
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_()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment