2023-08-12

QRubberBand を使いこなしたい 〜 PySide6 〜

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。配布ライセンスは LGPL で公開されています(商用ライセンスも有り)。最新のバージョンは Qt6 に対応した PySide6(記事執筆時点で 6.5.2)です。

ちゃちゃっとプロットする GUI アプリを PySide6 で作りたい場合、使い慣れている Matplotlib を利用することができます。しかし、PySide6 でも QtCharts モジュールを利用すれば一通りのチャートを扱えるので、簡単なチャートであれば Qt だけでアプリケーションを完結させたいと考えてしまいます。

ただ、Matplotlib でできて PySide6 でできないことがいくつかあります。いや、できないこと、と言うよりも、やり方を知らない、と表現するべきですね。

今回は、そんな「やり方を知らなかった」ことの一つ、チャート上でマウスをドラックして(矩形)領域を指定し、そのデータを選択できるようにすることができるようになりましたので、サンプルを紹介します。

下記の OS 環境で動作確認をしました。

Fedora Linux 38 (Workstation Edition) x86_64
python3.11 python3-3.11.4-1.fc38.x86_64
PySide6 6.5.2

チャートのズームに利用していた QRubberBand を [3]、今回は矩形領域の選択に利用しています。rubber band とは輪ゴムのことですが、マニュアルを読んでもピンとこなかったので、ウィジェットとして使いこなせていませんでした。今回いろいろ調べたので、少しは使えるようになりました。👍

機能を紹介するサンプルとしては、冗長になっていて判りにくいかもしれません。

qtcharts_scatterchart_rubberband.py
#!/usr/bin/env python
# coding: utf-8
# Reference:
# https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QRubberBand.html
import random
import sys
from PySide6.QtCharts import (
QChart,
QChartView,
QScatterSeries,
QValueAxis,
)
from PySide6.QtCore import (
QPointF,
QRect,
Qt,
Signal, QEvent,
)
from PySide6.QtGui import (
QPainter,
QPen,
)
from PySide6.QtWidgets import (
QApplication,
QDockWidget,
QMainWindow,
QPushButton,
QRubberBand,
QSizePolicy,
QVBoxLayout,
QWidget,
)
class ScatterPlot(QChart):
def __init__(self, list_data: list):
super().__init__()
self.setDropShadowEnabled(False)
self.legend().hide()
self.axis_x = QValueAxis()
self.addAxis(
self.axis_x,
Qt.AlignmentFlag.AlignBottom
)
self.axis_y = QValueAxis()
self.addAxis(
self.axis_y,
Qt.AlignmentFlag.AlignLeft
)
# all plot data
series = QScatterSeries()
series.setMarkerShape(
QScatterSeries.MarkerShape.MarkerShapeCircle
)
series.setMarkerSize(10)
series.setPen(QPen(Qt.PenStyle.NoPen))
for xy_pair in list_data:
series.append(*xy_pair)
self.addSeries(series)
series.attachAxis(self.axis_x)
series.attachAxis(self.axis_y)
self.axis_x.setRange(0, 1)
self.axis_y.setRange(0, 1)
# for selected data
self.series_selected = QScatterSeries()
self.series_selected.setMarkerShape(
QScatterSeries.MarkerShape.MarkerShapeCircle
)
self.series_selected.setBrush(Qt.GlobalColor.red)
self.series_selected.setMarkerSize(10)
self.series_selected.setPen(QPen(Qt.PenStyle.NoPen))
self.addSeries(self.series_selected)
self.series_selected.attachAxis(self.axis_x)
self.series_selected.attachAxis(self.axis_y)
def highlightSelectedPoints(self, list_selected):
for xy_pair in list_selected:
self.series_selected.append(*xy_pair)
def clearSelected(self):
self.series_selected.clear()
class ChartView(QChartView):
def __init__(self, list_data: list):
super().__init__()
self.rect = None
self.origin = None
self.mouseReleased = False
self.rubberBand = QRubberBand(QRubberBand.Shape.Rectangle, self)
self.chart = ScatterPlot(list_data)
self.setChart(self.chart)
self.setRenderHint(QPainter.Antialiasing)
self.setMaximumSize(500, 500)
def mousePressEvent(self, event):
self.rubberBand.hide()
self.origin = event.position()
self.mouseReleased = False
def mouseMoveEvent(self, event):
if self.origin is None:
return
if self.mouseReleased:
return
self.rubberBand.show()
self.rubberBand.setGeometry(
QRect(
self.origin.toPoint(),
event.position().toPoint()
).normalized()
)
def mouseReleaseEvent(self, event):
self.mouseReleased = True
self.rect = QRect(
self.origin.toPoint(),
event.position().toPoint()
).normalized()
def clearSelected(self):
self.chart.clearSelected()
def getSelectedArea(self) -> list:
if not self.mouseReleased:
return list()
p1 = self.chart.mapToValue(
QPointF(
self.rect.x(),
self.rect.y()
)
)
p2 = self.chart.mapToValue(
QPointF(
self.rect.x() + self.rect.width(),
self.rect.y() + self.rect.height()
)
)
return [p1.x(), p1.y(), p2.x(), p2.y()]
def addSelectedPoins(self, list_selected):
self.chart.highlightSelectedPoints(list_selected)
self.rubberBand.hide()
class DockControl(QDockWidget):
selected = Signal()
clear = Signal()
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
base = QWidget()
self.setWidget(base)
layout = QVBoxLayout()
base.setLayout(layout)
but_sel = QPushButton('Select')
but_sel.clicked.connect(self.on_click_selected)
layout.addWidget(but_sel)
but_clr = QPushButton('Clear')
but_clr.clicked.connect(self.on_click_clear)
layout.addWidget(but_clr)
vpad = QWidget()
vpad.setSizePolicy(
QSizePolicy.Policy.Fixed,
QSizePolicy.Policy.Expanding
)
layout.addWidget(vpad)
def on_click_selected(self):
self.selected.emit()
def on_click_clear(self):
self.clear.emit()
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.cview = None
self.list_data = None
self.init_ui()
# self.resize(600, 500)
self.setWindowTitle('Scatter Plot')
def init_ui(self):
# ChartView widget
self.list_data = self.data_prep()
self.cview = ChartView(self.list_data)
self.setCentralWidget(self.cview)
# right dock
dockWidget = DockControl()
dockWidget.setAllowedAreas(
Qt.DockWidgetArea.LeftDockWidgetArea |
Qt.DockWidgetArea.RightDockWidgetArea
)
dockWidget.selected.connect(self.on_click_selected)
dockWidget.clear.connect(self.on_click_clear)
self.addDockWidget(
Qt.DockWidgetArea.RightDockWidgetArea,
dockWidget
)
@staticmethod
def data_prep() -> list:
list_data = list()
for r in range(100):
xy_pair = [random.random(), random.random()]
list_data.append(xy_pair)
return list_data
def on_click_selected(self):
area_selected = self.cview.getSelectedArea()
if len(area_selected) > 0:
self.points_in_area(area_selected)
else:
print('area not selected!')
def points_in_area(self, area):
x1, y1, x2, y2 = area
list_selected = list()
for x, y in self.list_data:
if x1 <= x and x2 >= x and y1 >= y and y2 <= y:
xypair = [x, y]
list_selected.append(xypair)
self.cview.addSelectedPoins(list_selected)
def on_click_clear(self):
self.cview.clearSelected()
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()

今回やりたかったことは、マウスのクリックやドラッグの操作で、チャート上で矩形領域を指定することです。

実行例 (1) : マウスでドラッグして矩形領域を指定

現実的な用途としては、矩形領域を選択したあとに、下記のように色を変えるなどの視覚化をしたりします。この例では赤色の点を重ね書きしているだけです。

実行例 (2) : Select ボタンをクリックして選択を確定

今回紹介したサンプルコードは、はっきり言って長すぎます。もっとすっきりと簡潔なサンプルになるように gist のサンプルを改良していきます。

参考サイト

  1. PySide6.QtCharts - Qt for Python
  2. QRubberBand - Qt for Python
  3. bitWalk's: Qt for Python によるチャート (7) [2021-07-25]

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



0 件のコメント: