どうもです、タドスケです。
Qt を扱う上での土台となる QObject クラス。
これまで派生クラスの QWidget の使い方などを調べるうちに、何となく QObject 側の機能を使っていましたが、ふと「QObject にどんな機能があるのか」をちゃんと調べたことがなかったなぁと思いまして。
QObject クラスに関して、「できること」ベースの逆引きリファレンスを作ることにしました。
執筆時点での検証環境は以下です。
- Windows 11
- Python 3.11
- Pyside6
- PyCharm 2023.1.4 (Community Edition)
バージョンが異なる場合、そのままのコードでは動かないことがありますのでご了承ください。
コード
このページで紹介しているサンプルのコードは、以下にアップしています。
pyside6/object at master · tadosuke/pyside6 · GitHub
シグナルの発行を一時的に抑える
from PySide6.QtCore import QObject, Signal
def _on_done():
print('done')
class MyObject(QObject):
done = Signal()
def do_something(self):
self.done.emit()
my_obj = MyObject()
my_obj.done.connect(_on_done)
print(my_obj.signalsBlocked())
# False
my_obj.do_something()
# done
# シグナルをブロック
my_obj.blockSignals(True)
print(my_obj.signalsBlocked())
# True
my_obj.do_something()
# (何も出ない)
子オブジェクトを取得する
from PySide6.QtCore import QObject
parent = QObject()
child1 = QObject(parent=parent)
child2 = QObject(parent=parent)
print(parent.children())
# [child1, child2]
シグナルとスロットを接続/切断する
from PySide6.QtCore import QObject, Signal, QMetaMethod
def slot():
print('slot')
class MyObject(QObject):
something_done = Signal()
def do_something(self):
self.something_done.emit()
obj = MyObject()
# 接続
obj.something_done.connect(slot)
# シグナルが接続されているかを調べる
meta_method = QMetaMethod.fromSignal(obj.something_done)
print(obj.isSignalConnected(meta_method))
# True
obj.do_something()
# slot
# 切断
obj.something_done.disconnect(slot)
obj.do_something()
# (何も表示されない)
print(obj.isSignalConnected(meta_method))
# False
オブジェクトのデバッグ情報を出力する
from PySide6.QtCore import QObject, Signal
class MyObject(QObject):
signal = Signal()
def __init__(self):
super().__init__()
self.value = 1
def slot():
pass
obj = MyObject()
obj.signal.connect(slot)
obj.dumpObjectInfo()
# OBJECT MyObject::unnamed
# SIGNALS OUT
# signal: signal()
# --> __GlobalReceiver__::unnamed slot29559d86480()
# SIGNALS IN
# <None>
dumpObjectInfo の実行結果はデバッグ出力に出るので、PyCharm などの IDE 上から実行すると何も出ないことがあります。
今回のサンプルでは、コマンドプロンプトからスクリプトを実行して確認しています。
オブジェクトツリーの構造を出力する
from PySide6.QtCore import QObject
obj_a = QObject()
obj_a.setObjectName('A')
obj_b = QObject(parent=obj_a)
obj_b.setObjectName('B')
obj_c = QObject(parent=obj_a)
obj_c.setObjectName('C')
obj_d = QObject(parent=obj_b)
obj_d.setObjectName('D')
obj_a.dumpObjectTree()
# QObject::A
# QObject::B
# QObject::D
# QObject::C
出力内容には、setObjectName で設定したオブジェクト名が表示される。
dumpObjectInfo の実行結果はデバッグ出力に出るので、PyCharm などの IDE 上から実行すると何も出ないことがあります。
今回のサンプルでは、コマンドプロンプトからスクリプトを実行して確認しています。
プロパティを取得/設定する
from PySide6.QtCore import QObject, Property
class MyObject(QObject):
def __init__(self):
super().__init__()
self._a = 1
def get_a(self):
return self._a
def set_a(self, value):
self._a = value
property_a = Property(int, get_a, set_a)
obj = MyObject()
# プロパティを取得
print(obj.property('property_a'))
# 1
# 動的プロパティを追加
obj.setProperty('property_b', 'hoge')
# 動的に追加されたプロパティも取得できる
print(obj.property('property_b'))
# hoge
# 動的プロパティの名前だけを取得する
for name in obj.dynamicPropertyNames():
# dynamicPropertyNames は QByteArray を返すので、
# 文字列表示するために変換
print(bytes(name).decode())
# property_b
ここでいう「プロパティ」とは、python 標準のプロパティ(@property で定義できるもの)とは違い、Qt 独自のものです。
Qt 側の プロパティには、変更時にシグナルを発行するなど、Qt ならではの機能があります。
シグナルを手動で発行する
from PySide6.QtWidgets import QPushButton, QApplication
class MyButton(QPushButton):
def __init__(self):
super().__init__('Button')
def emulate_click(self):
"""クリックしたことにする."""
self.clicked.emit()
def _on_clicked():
print('clicked')
app = QApplication()
button = MyButton()
button.clicked.connect(_on_clicked)
button.emulate_click()
# クリックしていなくても「clicked」が出力される
通常はユーザー操作でしか発生しない、QPushButton の clicked シグナルを発行している。
これを利用すれば、スクリプトによる自動操作などもできる。
子オブジェクトを検索する
from PySide6.QtCore import QObject
obj_a = QObject()
obj_a.setObjectName('A')
# 子オブジェクトを追加
obj_b = QObject(parent=obj_a) # B の親を A に設定
obj_b.setObjectName('B')
obj_c = QObject(parent=obj_a)
obj_c.setObjectName('C')
# 子オブジェクトを一つだけ検索する
print(obj_a.findChild(QObject))
# 最初に登録した B のみが取得される
# QObject タイプの子オブジェクトを全て取得する
print(obj_a.findChildren(QObject))
# B, C が取得される
# 名前が「C」の QObject を検索する
print(obj_a.findChildren(QObject, name='C'))
# C のみが取得される
指定クラスを継承しているかを調べる
from PySide6.QtWidgets import QWidget, QApplication, QPushButton
app = QApplication()
widget = QWidget()
button = QPushButton('Button')
print(widget.inherits('QObject'))
# True
print(button.inherits('QWidget'))
# True
print(button.inherits('QObject')) # 親の親でも判定可能
# True
print(widget.inherits('QPushButton'))
# False
【派生クラス専用】オブジェクトのイベントを検知する
from PySide6.QtCore import QObject, QEvent, Qt
from PySide6.QtWidgets import QPushButton, QApplication
class MyButton(QPushButton):
def __init__(self):
super().__init__('Button')
# イベント発生時に eventFilter が呼ばれるようになる
self.installEventFilter(self)
# イベントフィルターを削除したいときに呼ぶ
# self.removeEventFilter(self)
def eventFilter(self, watched: QObject, event: QEvent):
# 右クリックイベントを処理する
if event.type() == QEvent.MouseButtonPress:
mouse_event = event
if mouse_event.button() == Qt.RightButton:
print("Right Click")
return True # 処理したら True を返す
return False # 処理しなかったら False を返す
app = QApplication()
button = MyButton()
button.show()
app.exec()
installEventFilter にQObject を渡すと、イベント発生時にオブジェクトの eventFilter という関数が呼ばれる。
QObject クラスの eventFilter は空になっており、何もしない。
イベントの種類は、QObject の派生クラス(QWidget、QPushButton など)に応じて様々なものがある。
↓詳しくはこちら
今回のサンプルでは、ボタンを右クリックするとメッセージが出力される。
removeEventFilter を呼ぶと、eventFilter は呼ばれなくなる。
ウィジェット/ウィンドウかどうかを調べる
from PySide6 import QtGui, QtWidgets
from PySide6.QtWidgets import QApplication
app = QApplication()
main_window = QtWidgets.QMainWindow()
print(main_window.isWidgetType())
print(main_window.isWindowType())
# True, False
gui_window = QtGui.QWindow()
print(gui_window.isWidgetType())
print(gui_window.isWindowType())
# False, True
ややこしいのは、QtWidgets にある QMainWindow はウィジェットとして扱われる点。
公式のリファレンスによれば、inherits(“QWindow”) を呼ぶのと同じとのこと。
QtWidgets 以下のクラスを使っている限りは、使う機会は無いように思える。
よく似た関数に QWidget.isWindow() があり、そちらは QMainWindow でも True を返します。
一定間隔でイベントを発生させる
from PySide6.QtCore import QObject, QTimerEvent
from PySide6.QtWidgets import QApplication
class MyObject(QObject):
def __init__(self) -> None:
super(MyObject, self).__init__()
# 1000ミリ秒(1秒)ごとにタイマーイベントを発生させる
self.timer_id = self.startTimer(1000)
self.num = 0
def timerEvent(self, event: QTimerEvent) -> None:
print(f'timerEvent:{self.num}')
self.num += 1
if 5 <= self.num:
# タイマーイベントを停止させる
self.killTimer(self.timer_id)
app = QApplication()
obj = MyObject()
app.exec()
実行すると1秒ごとにログが出力され、5回呼ばれると停止する。
オブジェクトの名前を取得/設定する
from PySide6.QtCore import QObject
def _on_object_name_changed(name):
"""オブジェクト名が変更されたときに呼ばれる."""
print(f'ObjectNameChanged : {name}')
obj = QObject()
obj.objectNameChanged.connect(_on_object_name_changed) # 変更時のシグナルに接続
obj.setObjectName('A')
# ObjectNameChanged : A
print(obj.objectName())
# A
親オブジェクトを取得/設定する
from PySide6.QtCore import QObject
obj_a = QObject()
obj_a.setObjectName('A')
obj_b = QObject(parent=obj_a) # B の親を A に設定
obj_b.setObjectName('B')
print(obj_b.parent().objectName())
# A
obj_c = QObject()
obj_c.setObjectName('C')
obj_b.setParent(obj_c) # 親を A → C に付け替える
print(obj_b.parent().objectName())
# C
シグナルに接続されているスロットの数を取得する
from PySide6.QtCore import QObject, Signal, SIGNAL
def slot1():
pass
def slot2():
pass
def slot3():
pass
class MyObject(QObject):
my_signal = Signal()
obj = MyObject()
obj.my_signal.connect(slot1)
obj.my_signal.connect(slot2)
obj.my_signal.connect(slot3)
print(obj.receivers(SIGNAL('my_signal()')))
# 3
SIGNAL を使わずに文字列だけを指定したり、シグナル名のカッコ「()」を付け忘れると正しく取得できないので注意。
receivers のリファレンスを見ると、引数の型が str になっているが、ソースコード側の型ヒントは bytes になっている。
リファレンス通りのやり方で動いたので、ソースコード側の更新忘れかも?
シグナルを送信したオブジェクトについて調べる
from PySide6.QtCore import QObject, Signal
class MySlot(QObject):
def func(self):
print(self.sender())
print(self.senderSignalIndex())
class MyObjectA(QObject):
signal1 = Signal()
signal2 = Signal()
def do_something(self):
self.signal1.emit()
self.signal2.emit()
class MyObjectB(QObject):
signal = Signal()
def do_something(self):
self.signal.emit()
slot = MySlot()
obj1 = MyObjectA()
obj1.signal1.connect(slot.func)
obj1.signal2.connect(slot.func)
obj1.do_something()
# <__main__.MyObjectA(*) at *>
# 5
# <__main__.MyObjectA(*) at *>
# 6
obj2 = MyObjectB()
obj2.signal.connect(slot.func)
obj2.do_something()
# <__main__.MyObjectB(*) at *>
# 5
オブジェクトの削除を予約する/削除されたことを知らせる
from PySide6.QtWidgets import QApplication, QPushButton, QWidget
def _on_destroy():
print('destroyed')
if widget is not None: # 破棄されても None にはならないので注意!
# 既に破棄されているのでエラーになる
print(widget)
def _on_clicked():
print('deleteLater')
widget.deleteLater() # 削除を予約する
# このタイミングではまだ生きている
print(widget)
app = QApplication()
button = QPushButton('Button')
button.clicked.connect(_on_clicked)
widget = QWidget()
widget.destroyed.connect(_on_destroy) # 削除されたときのシグナル
button.show()
app.exec()
deleteLater が呼ばれた瞬間ではなく、次のイベントループで削除されること、オブジェクトが削除されても変数は None にならないことに注意。
このサンプルでは、削除されたオブジェクトにアクセスしようとしてエラーが発生する。
オブジェクトを別スレッドで実行する/実行しているスレッドを調べる
import sys
from PySide6.QtCore import QObject, QThread
from PySide6.QtWidgets import QApplication
class Worker(QObject):
def do_work(self) -> None:
# 今いるスレッドを表示
print(f"{self.thread()=}")
app = QApplication(sys.argv)
worker = Worker()
worker.do_work()
# ワーカーをスレッドに移動
thread = QThread()
worker.moveToThread(thread)
thread.start()
worker.do_work()
この方法で QWidget などの GUI 要素を別スレッドに移動してはいけません。
Qt に限らず、一般的に GUI はメインスレッドで実行されることが前提になっています。
別スレッドで実行した場合、予期せぬ不具合やクラッシュが発生する恐れがあります。
(QObject は GUI 要素ではないので大丈夫です)
翻訳されたテキストを取得する
from PySide6.QtCore import QObject, QTranslator
from PySide6.QtWidgets import QApplication
class MyObject(QObject):
def __init__(self) -> None:
super().__init__()
print(self.tr("Hello!"))
# 翻訳データを読み込んでいれば、翻訳テキストが代わりに表示される
if __name__ == "__main__":
app = QApplication()
# 翻訳データを読み込む
translator = QTranslator()
if translator.load("translation.qm"):
app.installTranslator(translator)
obj = MyObject()
翻訳データは別に用意する必要があります。
このサンプルでは、以下の記事を参考に qm ファイルを用意しました。
【PySide】ソフト文字列をQTranslatorで翻訳・多言語化する手順を解説! | PisukeCode – Web開発まとめ (pisuke-code.com)
※僕は pyside6 のツール類にパスを通していなかったので、ターミナルから exe を直接実行しました。
コメント
コメント一覧 (1件)
[…] 【Qt】コード付き、QObject 逆引きリファレンス | しぬまでワクワクしていた… どうもです、タドスケです。 Qt を扱う上での土台となる QObject クラス。 […]