しばらく複数のブログを運営しようと考えていましたが、note の使い勝手が良いので、note に集約することにしました。
現在、少しずつ引っ越しを始めています。💁♂️
参考サイト
- noteの特徴、使い方、機能紹介|note公式 [2014-04-18]
にほんブログ村
#オープンソース
すぐりふひとのプログラミングメモ
しばらく複数のブログを運営しようと考えていましたが、note の使い勝手が良いので、note に集約することにしました。
現在、少しずつ引っ越しを始めています。💁♂️
PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。
QChart を含む QtCharts モジュールは、Qt 6.10 / PySide6 6.10 以降、公式に非推奨となりました[1] [2]。
従来 QChart で作成していたプロットを(Matplotlib や PyQtGraph を利用せずに)新しいやり方で作成できたので、前回に引き続き、備忘録的にサンプルを紹介します。
今回は、折れ線グラフです。時間の経過に伴うデータの変化や傾向(トレンド)を表すことを意識して、トレンドチャートと呼んでいます。
下記の環境で動作確認をしています。
|
Fedora Linux 44 KDE Plasma Desktop x86_64 |
|
| Python | 3.14.7 | |
| PySide6 | 6.11.2 | |
| NumPy | 2.5.2 | |
まずは、実行例を示します。乱数を生成してつなげてプロットしただけのサンプルです。
ひとつのファイルにまとめたコードを示しました。
import sys
import numpy as np
from PySide6.QtCore import Qt
from PySide6.QtGraphs import (
QGraphsTheme,
QLineSeries,
QValueAxis,
)
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QStyle,
)
class MyTrend(QQuickWidget):
def __init__(self):
super().__init__()
self.setResizeMode(
QQuickWidget.ResizeMode.SizeRootObjectToView
)
self.setFixedSize(600, 300)
# --------------------------------
# Theme
# --------------------------------
self.theme = theme = QGraphsTheme()
grid = theme.grid()
grid.setMainWidth(1.0)
grid.setSubWidth(0.5)
theme.setGrid(grid)
# --------------------------------
# Axes
# --------------------------------
self.axis_x = axis_x = QValueAxis()
axis_x.setRange(0, 100)
self.axis_y = axis_y = QValueAxis()
axis_y.setRange(0, 1)
axis_y.setTitleText("Value")
# --------------------------------
# Line series
# --------------------------------
self.series = series = QLineSeries()
series.setAxisX(axis_x)
series.setAxisY(axis_y)
# 線の太さと色
series.setWidth(1.0)
series.setColor(Qt.GlobalColor.green)
# GraphsView に渡すプロパティ
self.setInitialProperties({
"theme": theme,
"axisX": axis_x,
"axisY": axis_y,
"seriesList": [series],
})
# QtGraphs の GraphsView をロード
self.loadFromModule(
"QtGraphs",
"GraphsView",
)
def append_data(self, x: float, y: float):
self.series.append(x, y)
class Example(QMainWindow):
def __init__(self):
super().__init__()
icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMenuButton
)
self.setWindowIcon(icon)
self.setWindowTitle("Trend Chart")
# --------------------------------
# QQuickWidget
# --------------------------------
trend = MyTrend()
self.setCentralWidget(trend)
arr_y = np.random.random(100)
for i, y in enumerate(arr_y):
trend.append_data(i, y)
def main():
app = QApplication(sys.argv)
win = Example()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
このプログラムでは、QQuickWidget を継承した MyTrend クラスを作成し、Qt Graphs の QLineSeries を使って折れ線グラフを表示しています。MyTrend の初期化時に表示サイズを 600×300 に固定し、グラフのテーマ、グリッド線、X軸とY軸、そして折れ線グラフ用の系列を順番に設定します。
テーマには QGraphsTheme を使用し、グリッド線についてはメインの線を 1 ピクセル、サブの線を 0.5 ピクセルに設定しています。X軸とY軸には QValueAxis を使用し、X軸を 0 ~ 100、Y軸を 0 ~ 1 の範囲に設定しています。また、Y軸には Valueというタイトルを設定しています。
グラフのデータを保持する QLineSeries を作成し、先ほど作成した X軸 と Y軸 を割り当てます。折れ線の太さを 1 ピクセル、色を緑色に設定した後、テーマ、軸、系列を GraphsView に渡すための初期プロパティとして設定します。その後、QtGraphs モジュールの GraphsView をロードすることで、グラフが表示される状態になります。
MyTrend には append_data() メソッドを用意しており、X座標 と Y座標を受け取って QLineSeries に追加します。これによって、グラフそのものを管理する MyTrend と、そこへデータを供給する側を分離しています。
Example クラスでは MyTrend をメインウィンドウの中央ウィジェットとして配置し、NumPy の random.random() を使って 100個 のランダムな値を生成します。生成した値を enumerate() で順番に取り出し、インデックスを X座標、ランダムな値を Y座標 として append_data() に渡します。
その結果、0 ~ 99 の X座標 に対して 0 ~ 1 の範囲でランダムに生成された 100個 のデータが QLineSeries に追加され、Qt Graphs によって折れ線グラフとして描画されます。
最後に main() で QApplication を生成し、メインウィンドウを表示してイベントループを開始します。
PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。
QChart を含む QtCharts モジュールは、Qt 6.10 / PySide6 6.10 以降、公式に非推奨となりました[1] [2]。
従来 QChart で作成していたプロットを(Matplotlib や PyQtGraph を利用せずに)新しいやり方で作成できたので、備忘録的にサンプルを紹介します。現時点では自由に使いこなせるほど理解ができているとは言えないので、より良い方法が判れば、その都度、投稿していく予定です。
下記の環境で動作確認をしています。
|
Fedora Linux 44 KDE Plasma Desktop x86_64 |
|
| Python | 3.14.7 | |
| PySide6 | 6.11.2 | |
| NumPy | 2.5.2 | |
まずは、実行例を示します。乱数を生成して散布図にプロットしただけのサンプルです。
ひとつのファイルにまとめたコードを示しました。
import sys
import numpy as np
from PySide6.QtCore import QUrl
from PySide6.QtGraphs import (
QGraphsTheme,
QScatterSeries,
QValueAxis,
)
from PySide6.QtQml import QQmlComponent, QQmlEngine
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QStyle,
)
class MyScatter(QQuickWidget):
def __init__(self):
super().__init__()
self.setResizeMode(
QQuickWidget.ResizeMode.SizeRootObjectToView
)
self.setFixedSize(600, 600)
# --------------------------------
# Theme
# --------------------------------
self.theme = theme = QGraphsTheme()
grid = theme.grid()
grid.setMainWidth(1.0)
grid.setSubWidth(0.5)
theme.setGrid(grid)
# --------------------------------
# Axes
# --------------------------------
self.axis_x = axis_x = QValueAxis()
axis_x.setRange(0, 1)
self.axis_y = axis_y = QValueAxis()
axis_y.setRange(0, 1)
# --------------------------------
# Scatter series
# --------------------------------
self.series = series = QScatterSeries()
series.setAxisX(axis_x)
series.setAxisY(axis_y)
# ----------------------
# QMLで描画ポイントを修飾
# ----------------------
qml = """
import QtQuick
Rectangle {
width: 5
height: 5
radius: width / 2
}
"""
self.engine = QQmlEngine()
self.point_delegate = QQmlComponent(self.engine)
self.point_delegate.setData(qml.encode(), QUrl())
if self.point_delegate.isError():
print(self.point_delegate.errors())
else:
series.setPointDelegate(self.point_delegate)
# GraphsView に渡すプロパティ
self.setInitialProperties({
"theme": theme,
"axisX": axis_x,
"axisY": axis_y,
"seriesList": [series],
})
# QtGraphs の GraphsView をロード
self.loadFromModule(
"QtGraphs",
"GraphsView",
)
def append_data(self, x: float, y: float):
self.series.append(x, y)
class Example(QMainWindow):
def __init__(self):
super().__init__()
icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMenuButton
)
self.setWindowIcon(icon)
self.setWindowTitle("Scatter Chart")
# --------------------------------
# QQuickWidget
# --------------------------------
scatter = MyScatter()
self.setCentralWidget(scatter)
arr_xy = np.random.random(size=(200, 2))
for x, y in arr_xy:
scatter.append_data(x, y)
def main():
app = QApplication(sys.argv)
win = Example()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
まず main() が呼び出され、Qt アプリケーションの実行環境が作られます。その後、Example クラスのウィンドウが生成されます。
Example の初期化では、まずメインウィンドウのアイコンとタイトルを設定します。続いて MyScatter のインスタンスを生成し、メインウィンドウの中央ウィジェットに設定します。
MyScatter は QQuickWidget を継承しています。MyScatter の初期化では、
ここまでで MyScatter の準備が完了します。
Example に戻ると、MyScatter が中央ウィジェットとして配置された後、NumPy を使って 200 個のランダムな X・Y 座標を生成します。それぞれの座標について MyScatter の append_data() を呼び出し、QScatterSeries にデータポイントを追加します。
データが追加されると、Qt Graphs がそれぞれのポイントを描画します。その際、先ほど設定した pointDelegate が使われるため、通常の散布点ではなく、QML の Rectangle によって定義された直径 5 程度の円として表示されます。
最後に Example のウィンドウを表示し、Qt のイベントループを開始します。以降は Qt が画面の描画やイベント処理を担当し、生成された 200 個のデータポイントが散布図として表示されます。
Python の @dataclass は、データを保持するクラスを簡潔に記述するためのデコレータです [1]。通常であれば自分で実装する必要がある __init__() や __repr__()、__eq__() などの定型コードを自動生成してくれるため、コード量を大幅に減らすことができます。
通常のクラスであれば、次のようにコンストラクタを実装する必要があります。
class Config:
def __init__(self):
self.host = "localhost"
self.port = 8080
self.timeout = 30
一方、@dataclass を利用すると、次のようにメンバー変数(ここでは「フィールド」と呼びます)を宣言するだけで同じようなクラスを作成できます。
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
timeout: int = 30
@dataclass は、フィールドの宣言から __init__() を自動生成してくれるため、自分でコンストラクタを書く必要がありません。また、オブジェクトの内容を分かりやすく表示する __repr__() や、オブジェクト同士を比較する __eq__() なども自動的に用意されます。
このサンプルでは 3 つのフィールドだけなので、ありがたみが分かりにくいのですが、何十ものフィールドを扱うときには管理が楽なので、@dataclass デコレータを利用しています。
データクラス という名前から、メソッドを持たない単純なクラスを想像しがちですが、実際には通常のクラスとほぼ同じようにメソッドを追加できます。そのため、データだけでなく、そのデータに関連する処理も自然に記述できます。
今回は、@dataclass を利用する際によく悩む「他のクラスのインスタンスをどこで生成するか」について、__post_init__() を使った方法を備忘録としてまとめました。
@dataclass デコレータを付けたクラスでは、__init__() が自動生成されるため、他のクラスのインスタンスを生成したい場合には __post_init__() を利用します。
from dataclasses import dataclass, field
class Processor:
def __init__(self, size: int):
self.size = size
@dataclass
class Config:
buffer_size: int = 1024
processor: Processor = field(init=False)
def __post_init__(self):
self.processor = Processor(size=self.buffer_size)
このパターンを覚えておくと、@dataclass を利用しながら、通常のクラスと同じように他のオブジェクトを保持・初期化できるようになります。
KDE Plasmaは、KDE コミュニティによって Unix 系 OS 向けに開発されたグラフィカルシェルです。これは、ユーザーと OS 間のインターフェース層として機能し、アプリケーションの起動、ウィンドウの管理、ファイルやシステム設定の操作を行うためのグラフィカルユーザーインターフェース(GUI)およびワークスペース環境を提供します。Plasmaはモジュール式で適応性が高く設計されており、パーソナルコンピュータ向けの「Plasma Desktop」や、スマートフォン向けの「Plasma Mobile」など、特定のデバイスタイプに合わせて最適化されたさまざまなバリエーションが用意されています。
本来は、本ブログに投稿した記事から厳選したコンテンツを note にも公開していく予定でしたが、逆になってしまいました。
Python アプリを作っていると、動的にクラスをインポートしたいニーズがときどきあります。
覚えた機能について今後も使う可能性があれば、その時になってあれこれ調べ直さなくて済むように、簡単なサンプルを作成して本ブログに記事にまとめています。動的にクラスをインポートについても、何年か前に記事を書いていました [1]。しかし、読み直してみると、今ひとつよく解らなかったので、一般的な用途に合うようにサンプルを作り直しました。
下記の環境で動作確認をしています。
|
Fedora Linux 44 KDE Plasma Desktop x86_64 |
|
| Python | 3.14.5 | |
| PySide6 | 6.11.1 | |
まず、動作確認用にプラグイン用サンプルを用意します。プロジェクト内に plugins というディレクトリを作り、テンプレート用の抽象クラスを記述した abstract.py(PluginTemplate クラス)と、このクラスを継承して実装した simple_1.py と simple_2.py を用意します。
plugins/ ├ abstract.py ├ simple_1.py └ simple_2.py
from abc import ABC, abstractmethod
class PluginTemplate(ABC):
NAME = "template"
@abstractmethod
def run(self) -> None: ...
from plugins.abstract import PluginTemplate
class Plugin(PluginTemplate):
NAME = "simple 1"
def run(self) -> None:
print(self.NAME)
from plugins.abstract import PluginTemplate
class Plugin(PluginTemplate):
NAME = "simple 2"
def run(self) -> None:
print(self.NAME)
PluginTemplate クラスを継承して実装した simple_1.py と simple_2.py のクラス名は、ひとまず Plugin に統一しています。
指定したパス、パッケージ名、継承したクラス名に合致するモジュールをロードする関数 load_plugins です。
import importlib
import inspect
import pkgutil
def load_plugins(
path_plugin: str,
package_name: str,
plugin_base_class: type
) -> dict[str, type]:
# NOTE:
# 現在はプロジェクト配下のパッケージのみを対象としているため、
# importlib.import_module() を利用している。
#
# 将来的にユーザーが任意のディレクトリへ配置した外部プラグインを
# 読み込む場合は、sys.path の追加、または
# importlib.util.spec_from_file_location() を利用した
# ファイルパスベースのロード方式を検討すること。
dict_plugin = {}
for _, module_name, _ in pkgutil.iter_modules([path_plugin]):
module = importlib.import_module(f"{package_name}.{module_name}")
for _, cls in inspect.getmembers(module, inspect.isclass):
if issubclass(cls, plugin_base_class) and cls is not plugin_base_class:
dict_plugin[cls.NAME] = cls
return dict_plugin
引数 plugin_base_class に指定した抽象クラス(この例では PluginTemplate)を継承したクラス cls のみを辞書に cls.NAME をキーに登録して返す関数です。
動的にクラスをインポートするプラグインの機能を確認する PySide6 の GUI サンプルです。
import sys
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QMainWindow,
QStyle,
QToolBar,
)
from load_plugins import load_plugins
from plugins.abstract import PluginTemplate
class Example(QMainWindow):
def __init__(self):
super().__init__()
# プラグインの一覧を取得
plugins: dict[str, type] = load_plugins(
path_plugin="./plugins",
package_name="plugins",
plugin_base_class=PluginTemplate
)
self.setWindowTitle("Plugin Sample")
toolbar = QToolBar()
self.addToolBar(toolbar)
self.combo = combo = QComboBox()
for key in sorted(plugins.keys()):
cls = plugins[key]
combo.addItem(key, cls)
toolbar.addWidget(combo)
icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_MediaPlay
)
action_play = QAction(self)
action_play.setIcon(icon)
action_play.triggered.connect(self.on_play)
toolbar.addAction(action_play)
def on_play(self):
cls = self.combo.currentData()
obj = cls()
obj.run()
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
サンプルはコンボボックスに動的に読み込んだクラスの NAME の一覧を表示しています。隣の ▶ ボタンをクリックすると、コンボボックスに表示されている NAME に対応するクラスのインスタンスを生成して、run メソッドを実行します。
simple 1 simple 2
Matplotlib は、Python と NumPy のためのプロットライブラリです。Tkinter、wxPython、Qt、GTK のような汎用 GUI ツールキットを使ったアプリケーションにプロットを埋め込むためのオブジェクト指向 API を提供しています。
PySide6 の GUI で Matplotlib のチャートを扱うときに利用する、ズームや保存などの便利な機能を提供するナビゲーション・ツールバー NavigationToolbar2QT について、ユーザー定義のボタンを追加する方法(+α)をまとめました。
下記の環境で動作確認をしています。
|
Fedora Linux 44 KDE Plasma Desktop x86_64 |
|
| Python | 3.14.5 | |
| matplotlib | 3.11.0 | |
| pyside6 | 6.11.1 | |
| seaborn | 0.13.2 | |
サンプルとして正規分布の乱数のヒストグラムを表示していますが、今回のテーマは、このチャートの下に表示されているナビゲーション・ツールバーのカスタマイズです。
ナビゲーション・ツールバーのカスタマイズ例として、「保存」ボタンの右側に「Qt」アイコン (SP_TitleBarMenuButton) を表示しています。
NavigationToolbar2QT を継承して「Qt」アイコン (SP_TitleBarMenuButton)(ボタン)を追加しています。
from matplotlib.backends.backend_qtagg import (
NavigationToolbar2QT as NavigationToolbar,
)
class MyNavToolbar(NavigationToolbar):
def __init__(self, canvas: FigureCanvas):
super().__init__(canvas)
user_action = QAction("User", self)
icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMenuButton
)
user_action.setIcon(icon)
user_action.triggered.connect(self.on_user_action)
actions = self.actions()
n = len(actions)
self.insertAction(actions[n - 1], user_action)
def on_user_action(self):
# Zoomモードなら解除
if self._actions["zoom"].isChecked():
self._actions["zoom"].trigger()
# Panモードなら解除
if self._actions["pan"].isChecked():
self._actions["pan"].trigger()
print("User button clicked")
「Qt」アイコン (SP_TitleBarMenuButton)(ボタン)をクリックすると、on_user_action メソッドが実行されて文字列が標準出力されますが、その他の機能として、ズームボタン self._actions["zoom"] あるいはパン(移動)ボタン self._actions["pan"] が選択状態であれば解除するようにしています。
サンプルコード qt_matplotlib_toolbar.py を示しました。