Skip to content

Instantly share code, notes, and snippets.

@mieki256
Last active April 16, 2022 06:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mieki256/02f07f15fce2483e0dd59200c5ba4364 to your computer and use it in GitHub Desktop.
Save mieki256/02f07f15fce2483e0dd59200c5ba4364 to your computer and use it in GitHub Desktop.
PySideでCGツール用ラバーバンドを試しに書いてみる
#!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