PySide6(Qt6)の覚書

この間Qtを初めてちゃんと触ったんですが、その際に学んだTips等をまとめておきます。なお、今回はQMLではなくQtWidgetを対象としています。

外観

テーマの設定

QMLだとMaterialやUniveral等素敵なデザインが用意されているんですが、QtWidgetの場合はおそらく「windowsvista」「Windows」「Fusion」の3つ。 下記のコードで有効なスタイルリストを取得することも可能。

print(QStyleFactory.keys())  # ['windowsvista', 'Windows', 'Fusion']
app = QApplication()
app.setStyle("Fusion")

左から"windowsvista" "Windows" "Fusion"

ダークテーマ(標準)

上で挙げたスタイルのうち、「Fusion」にはダークテーマが存在します。 Qtのバージョンが5.15以降6.5未満の場合はコマランドライン引数から設定できたようです。

アプリケーションの起動時にコマンドラインオプションで設定することができます。

> gallery.exe -platform windows:darkmode=1

> gallery.exe -platform windows:darkmode=2 Qt公式

この場合は、QApplicationの引数にwindows:darkmode=2等を渡してあげれば強制的にダークモードを適用できるようです。

python - How do I use QT6 Dark Theme with PySide6? - Stack Overflow

問題は6.5以降の場合なんですが、このコマンドライン指定はできなくなったようで、Windows側のライト/ダークモード設定に従うようになったようです。 詳しくは公式ページを確認してください。

ということで、強制的に暗い感じのデザインにするにはスタイルシート等で設定するしか無いようです。Windowsの設定に合わせる方がユーザのためになるだろ!というQt側の公式見解ですが、デフォルトで暗くしたいのです....

ダークテーマ(qdarktheme)

有志がダークテーマかつMaterialデザインなQSSを設定するライブラリを公開しています。前回はこれにお世話になりました。

pyqtdarktheme · PyPI

QApplicationインスタンスを生成後に、メソッドを呼び出せば暗めでいい感じのQSSが適用されます。

app = QApplication()
qdarktheme.setup_theme()

また、Primaryカラー等を変更すれば自分好みの色を適用できます。加えて、追加のQSSも設定可能です。

qdarktheme.setup_theme(custom_colors={"primary": "#32a852"}, additional_qss="")

なお、qdarkthemeがどんなQSSを出力するかは下記で確認可能です。

print(qdarktheme.load_stylesheet())

QToolTipが見えづらい

Issueとしても挙がってましたが、qtdarkthemeのQToolTipの視認性が絶望的です。改善したい場合は、addtional_qss引数でQToolTipの設定を上書きしましょう。

QSSを別ファイルにまとめる

QSSはQWidgetのメソッドsetStyleSheetで設定するわけですが、つまるところただのテキストファイルなので、Webページのようにstyle.qssなどとして別途ファイルにまとめることも可能です。 下記はqrcファイル(Resource Collection Files)でqssをロードした場合の読み込み例です。

stylefile = QFile(":/styles/main.qss")

if not stylefile.open(QIODevice.ReadOnly | QIODevice.Text):
    logger.critical("Failed to apply qss styles")
qsstext = QTextStream(stylefile).readAll()

QSSセレクタで特定のQWidgetを選ぶ

QSSはCSSとほぼフォーマットを同じくしています。公式リファレンスにも詳しく説明がありますが、「ここのQLabelだけ色変えたいなぁ」といった場合の設定方法をメモっときます。

lb = QLabel("少し大きめな見出し")
lb.setObjectName("h2")
QLabel#h2 {
  font-size: 24px;
}

Font family

使用できるフォントファミリーの一覧は下記で確認できます。QFontDatabaseはQGuiApplicationインスタンスを作らないと使えないので注意です。

from PySide6.QtGui import QFontDatabase, QGuiApplication

tmp = QGuiApplication()
print("\n".join(QFontDatabase.families()))

スレッディング

QThreadの理解につながる記事

コードはC++ですが、QThread概念、特にイベントループやシグナル周りの理解に大変おすすめです。

Qtでスレッドを使う前に知っておこう - Qiita

上を踏まえたマルチスレッド実装についてはこちらの記事が参考になりました。

[PySide] QThread を使って時間のかかる処理をスレッド化する - へっぽこプログラマーの備忘録

動作が保証されないQPixmapまわり

UIをセカンダリスレッドから変更するのは動作保証の観点から避ける必要がありますが、QPixmapが一番レイヤーの低い画像クラスだと勘違いしていてハマりました。 こちらの記事にきれいにまとまっていますが、QPixmapはあくまで画像GUI上で扱うためのクラスなので、ワーカースレッドから扱ってはいけません。代わりにQImageであれば、裏側でロードしていても問題ありません。

それに合わせてQPixmapCacheもメインスレッド以外からは使えません。setCacheLimitメソッドで無理やりできるかと半ば実験的にやってみましたが、セカンダリスレッドからの利用は完全に無効化されています(C++のソースも確認してみました)。

QAbstractTableModel dataメソッドのロール

テーブルの各セルの中身をrowやcolumnに合わせて返すメソッドですが、色んなロールがあるのでリファレンスページをメモっときます。 Qt名前空間のリファレンスなので若干見つけづらい。

Qt Namespace | Qt Core 6.5.1

例えばですが、Qt.TextAlignmentRoleの評価タイミングでQt.AlignmentFlag.AlignCenterを渡してあげれば、特定のrowやcolumnのセルを中央揃えなんてことも可能です。

フレームレスウィンドウのDrag&Drop移動

ウィジェットのmouseイベント周りをオーバーライドすれば意外と簡単に実装できます。

# 左クリックによるウィンドウの移動制御
def mousePressEvent(self, event: QMouseEvent) -> None:
    if event.button() == Qt.MouseButton.LeftButton:
        self._press_pos = event.position()
        self._is_moving = True

def mouseMoveEvent(self, event: QMouseEvent) -> None:
    if self._is_moving:
        diff = event.position() - self._press_pos
        self.move(self.pos() + diff.toPoint())

def mouseReleaseEvent(self, event: QMouseEvent) -> None:
    if event.button() == Qt.MouseButton.LeftButton:
        self._is_moving = False

PySideで動かしているQtバージョンの確認

print(PySide6.__version__) # 6.5.1.1
print(PySide6.QtCore.qVersion()) # 6.5.1