この記事では、with文を使ってPyQtのウィジェットやレイアウトを整理するという書き方のアイデアについて説明します。
はじめに
これまでPyQtでデスクトップアプリを作るとき、いつも「なんだかわかりにくくて、Pythonっぽくないな」と感じていました。それも無理はありません。Qtは元々C++向けに作られたフレームワークで、PyQtはそれをPythonから使えるようにしたものですが、どうしてもC++的な冗長さやわかりにくさが残ってしまうようです。
もっとPythonらしく、わかりやすい書き方で使いたいなら、少し工夫して自分でクラスや関数を定義する必要があります。
その中で今回紹介したいのが、with文を使うという方法です。with文を使うとインデントによって構造が明確になり、コードがわかりやすく簡潔になります。また、Pythonの良さも引き立ちます。
with文と聞くと、ファイル処理のopenでしか使ったことがなく、「いまいち理解できていない」という人も多いでしょう。私も最初はそうでしたが、最近使う機会が増えるにつれて、そのメリットを実感するようになりました。そして、PyQtでもwith文を使うべきだという結論に至りました。
ただし、PyQtはもともとwith文で書くことを想定して作られていないため、自分でwith文に対応したクラスを用意する必要があります。
この記事では、簡単な例を挙げて説明します。
PyQtの基本的な使い方については、以前の記事で紹介しているので、そちらも参考にしてください。
なお、サンプルコードはすべてPyQt6を使っていますが、PyQt5でもほぼ同じように動くはずです。
with文の詳しい使い方については、すでに多くの記事があるので、ここでは詳しく説明しません。#参考の項目にリンクを載せてあります。
例として作るGUI
今回作成したGUIは、以下のようなものです。
レイアウトの階層構造を例として示したいだけなので、ボタンやラベルを適当に並べています。
元の書き方
まず、with文を使わない通常の書き方で作ると、次のようになります。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QPushButton,QHBoxLayout,QVBoxLayout,QLabel
from PyQt6.QtCore import Qt
class QMado(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('例の窓')
self.resize(480,280)
self.setStyleSheet('font-family: Kaiti SC; font-size: 20px;')
vbl = QVBoxLayout()
self.setLayout(vbl)
label1 = QLabel('上中のラベル')
vbl.addWidget(label1)
label1.setAlignment(Qt.AlignmentFlag.AlignCenter)
hb1 = QHBoxLayout()
vbl.addLayout(hb1)
vbl11 = QVBoxLayout()
hb1.addLayout(vbl11)
self.btn1 = QPushButton('桃色ボタン')
vbl11.addWidget(self.btn1)
self.btn1.setStyleSheet('background-color: #EEAABB;')
self.btn2 = QPushButton('しょぼいボタン')
vbl11.addWidget(self.btn2)
vbl12 = QVBoxLayout()
hb1.addLayout(vbl12)
self.btn3 = QPushButton('赤いボタン')
vbl12.addWidget(self.btn3)
self.btn3.setStyleSheet('background-color: #CC0000;')
self.btn4 = QPushButton('緑のボタン')
vbl12.addWidget(self.btn4)
self.btn4.setStyleSheet('background-color: #33AA33;')
hbl121 = QHBoxLayout()
vbl12.addLayout(hbl121)
self.btn5 = QPushButton('青いボタン1')
hbl121.addWidget(self.btn5)
self.btn5.setStyleSheet('background-color: #0000AA; color: #FFFFFF;')
self.btn6 = QPushButton('青いボタン2')
hbl121.addWidget(self.btn6)
self.btn6.setStyleSheet('background-color: #AAAAEE;')
vbl.addStretch()
vbl.addWidget(QLabel('下左のラベル'))
if __name__ == '__main__':
qAp = QApplication(sys.argv)
mado = QMado()
mado.show()
qAp.exec()
この書き方では、ウィジェットやレイアウトを作るたびにaddWidget、addLayout、setLayoutを使う必要があり、少し冗長に感じられます。どのレイアウトの中にいるのか、どこまでがそのレイアウトの範囲なのかが、ぱっと見でわかりにくいです。改行である程度区切ってはいますが、それでも階層関係を読み取るのは難しいですね。
withによる書き方
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QPushButton,QHBoxLayout,QVBoxLayout,QLabel
from PyQt6.QtCore import Qt
# withに使う関数のクラスの準備
class add_widget:
def __init__(self,widget,oyalayout):
oyalayout.addWidget(widget)
self.widget = widget
def __enter__(self):
return self.widget
def __exit__(self,exc_type,exc_value,traceback):
return
class add_layout:
def __init__(self,layout,oyalayout):
oyalayout.addLayout(layout)
self.layout = layout
def __enter__(self):
return self.layout
def __exit__(self,exc_type,exc_value,traceback):
return
class set_layout:
def __init__(self,layout,oyawidget):
oyawidget.setLayout(layout)
self.layout = layout
def __enter__(self):
return self.layout
def __exit__(self,exc_type,exc_value,traceback):
return
# GUIのクラスの定義
class QMado(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('例の窓')
self.resize(480,280)
self.setStyleSheet('font-family: Kaiti SC; font-size: 20px;')
with set_layout(QVBoxLayout(),self) as vbl:
with add_widget(QLabel('上中のラベル'),vbl) as label1:
label1.setAlignment(Qt.AlignmentFlag.AlignCenter)
with add_layout(QHBoxLayout(),vbl) as hbl1:
with add_layout(QVBoxLayout(),hbl1) as vbl11:
with add_widget(QPushButton('桃色ボタン'),vbl11) as self.btn1:
self.btn1.setStyleSheet('background-color: #EEAABB;')
self.btn2 = add_widget(QPushButton('しょぼいボタン'),vbl11)
with add_layout(QVBoxLayout(),hbl1) as vbl12:
with add_widget(QPushButton('赤いボタン'),vbl12) as self.btn3:
self.btn3.setStyleSheet('background-color: #CC0000;')
with add_widget(QPushButton('緑のボタン'),vbl12) as self.btn4:
self.btn4.setStyleSheet('background-color: #33AA33;')
with add_layout(QHBoxLayout(),vbl12) as hbl121:
with add_widget(QPushButton('青いボタン1'),hbl121) as self.btn5:
self.btn5.setStyleSheet('background-color: #0000AA; color: #FFFFFF;')
with add_widget(QPushButton('青いボタン2'),hbl121) as self.btn6:
self.btn6.setStyleSheet('background-color: #AAAAEE;')
vbl.addStretch()
add_widget(QLabel('下左のラベル'),vbl)
if __name__ == '__main__':
qAp = QApplication(sys.argv)
mado = QMado()
mado.show()
qAp.exec()
ここでは、次の3つのクラスを定義しています。
-
add_widget:親レイアウトにウィジェットを追加する -
add_layout:親レイアウトに子レイアウトを追加する -
set_layout:ウィジェットにレイアウトを設定する
いずれも、with文で使うために次の3つのメソッドを定義する必要があります。
-
__init__:最初に呼び出される処理。関数に渡すパラメータはここで受け取る。 -
__enter__:withブロック内で扱いたい戻り値を定義する。 -
__exit__:withブロックの処理が終わった後の処理を定義する。
__exit__メソッドには3つのパラメータ(例外の種類、値、トレースバック)を書く必要があります。これらは例外処理などに使われますが、今回は特に何もする必要がないので、単にreturnするだけにしています。
なお、通常クラス名は大文字で始めることが多いですが、今回はクラスというより関数呼び出しのように使いたいので、普通の関数と同じく小文字にしています。
このようにwith文を使うと、階層構造が視覚的にわかりやすくなりますね。
飾りのないシンプルなボタンなどを追加する場合には、無理にwith文を使う必要はなく、普通に=で代入する形でも使えます。
「with文が使える」ようにしておくだけで、必ずしもwithを使わなければならないわけではありません。
実際openも場合によってはwithを使わなくて済みますね。例えば、.read()で一気に読み込む場合です。
hoge = open('hoge.txt').read()
このように、普通の関数と同じような使い方ができます。
勿論、逆に普通の関数はいきなりwithを使うことができません。
contextlib.contextmanagerによる書き方
with文に対応する関数は、上記のようなクラス定義のほかに、contextlibを使って作る方法もあります。こちらのほうが簡潔で簡単に作れます。
例えば、add_layoutは次のように定義できます。
import contextlib
@contextlib.contextmanager
def add_layout(layout, oyalayout):
oyalayout.addLayout(layout)
yield layout
yieldやデコレータ(@)を使うので、わかりにくいと感じるかもしれませんが、基本的には普通の関数と同じです。ただ、上に@contextlib.contextmanagerを付け、戻り値として返したいオブジェクトをreturnではなくyieldで指定するだけです。
ただし、この方法で定義した関数は必ずwith文と一緒に使う必要があります。with文を使わなければ動作しません。
レイアウトの場合は毎回with文を使うと思うので、この方法で定義してもよいでしょう。しかし、ウィジェットの場合は1行で済んでwith文が不要なケースもあるので、普通にクラス定義したほうが良い場合もあります。
