Thor's Columns
PyQtでお手軽GUI開発♪―――は可能だったか? 第2回 Hello World! 編

PyQtでお手軽GUI開発♪―――は可能だったか? 第2回 Hello World! 編

 さて、とりあえず環境が何とかなったとすれば、次にまず行うのはハローワールドの表示です。


とりあえずのハローワールド

 GUIを作ろうと思ったならまずはウインドウが表示できなければ話になりませんが、ともかく最低限画面に何か出すというのであれば、以下の5行で十分です。

from PyQt5 import QtWidgets         #Qtのインポート
app = QtWidgets.QApplication([])    #アプリケーション本体のインスタンスを作る
wmain = QtWidgets.QMainWindow()     #メインウインドウのインスタンスを作る
wmain.show()                        #メインウインドウを表示する
app.exec()                          #アプリケーションのメッセージループを開始する

 Qtにおいては、QApplicationというクラスがメインのアプリケーションを定義するもので、QMainWindowというクラスがこの場合ウインドウを定義するクラスです。ここではアプリケーションとウインドウのインスタンスを作って、ウインドウを表示後アプリケーションを実行しています。

 Spyderを起動してこれをコピペして実行すれば小さなウインドウが現れるでしょう。それを見たら―――だから何だって感じだと思いますが、ともかくインストールが上手くいったことのテストにはなるででしょう。


 さてこのQMainWindowというクラスはいわば空っぽのウインドウなので、これにいろいろ機能を追加していく必要があります。その方法が要するにサブクラス化です。

 すなわち、QtでGUIアプリケーションを作るということは結局、QMainWindow(またはQDialog, QWidget)などを継承したクラスを作っていくという作業になるわけです。

QtDesignerで画面を作る

 そういうわけで自分で定義したウインドウを自分で出せないと真のハローワールドを出したことにはなりません。

 ではウインドウの定義をどうするかと言えば、ここでついにQtDesignerの登場です。

 QtDesignerは普通に入れたならユーザーフォルダのAnaconda3\Library\binの中にdesigner.exeという名前であるんじゃないかと思います。

 こいつを起動してやるとだいたい以下のような画面になると思いますが、最初にMainWindowを新規作成で作ってやると空のウインドウができるので、そこに左の「ウィジェットボックス」から適当なウィジェットをドラッグドロップします。

 ここではボタンとラベルを真ん中に置いただけの画面を作ります。

 位置や大きさはマウスでぐりぐりやれば何とかなるし、表示しているテキストとかフォントとかは右にある「プロパティエディタ」にtextとかfontといった項目があるので、そこを変えれば変わるでしょう。よく分からない項目も多いでしょうが細かいことを言わなければほぼ即座に使えるはずです。

 で、例えば以下のような画面を作って今回は“hello.ui”という名前で保存します。



 さて、このuiという拡張子のファイルの中身はただのXMLです。従ってこのままではPythonで使えないので、使える形式に変換する必要があります。これはコマンドラインで以下のコマンドを実行します。

pyuic5 hello.ui -o hello.py

 こうすると同じフォルダにhello.pyというファイルができあがります。

作った画面を表示する

 こうして作った画面を表示するには以下のようにします。

# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtCore as Qc, QtGui as Qg, QtWidgets as Qw    #(補足1
from PyQt5.QtCore import Qt

import hello                                #デザイナーで作った画面をインポートする

class MyForm(Qw.QMainWindow):               #MyFormという名前でQMainWindowのサブクラス作成

    def __init__(self, parent=None):        #クラスの初期化

        super().__init__(parent)            #上位クラスの初期化ルーチンを呼び出す(補足2
        self.ui = hello.Ui_MainWindow()     #先ほど作ったhello.pyの中にあるクラスの
        self.ui.setupUi(self)               #このコマンドを実行する

if __name__ == '__main__':

    app = Qw.QApplication(sys.argv)         #パラメータは正しくはコマンドライン引数を与える
    wmain = MyForm()                        #MyFormのインスタンスを作って
    wmain.show()                            #表示する
    sys.exit(app.exec())                    #こうやって終了コードを渡して抜けるのが礼儀

 上記のコードを実行してみると、確かに先ほど作ったウインドウが表示されると思います。後ろの方に書いてあることは、最初の短いサンプルとやっていることはほぼ同じです。

 前の方にQMainWindowを継承したMyFormというサブクラスの定義があります。

 その中で何をやっているかというと、初期化メソッド__init__中でまずhello.pyの中にあるUi_MainWindowというクラスのインスタンスを作り、それから自身を引数にとってsetupUiというメソッドを呼んでいます。

hello.pyの中身

 ではこれは何をやっているのでしょうか?

 hello.pyの中を覗いてみると、Ui_MainWindowやsetupUiというのは以下のように定義されています。

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(460, 275)
        self.centralwidget = QtWidgets.QWidget(MainWindow)              #全体のコンテナを作る
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)     #ボタンを作り
        self.pushButton.setGeometry(QtCore.QRect(160, 50, 131, 41))     #位置とサイズ設定
        font = QtGui.QFont()                                            #フォントを作り
        font.setPointSize(12)
        font.setBold(True)
        font.setWeight(75)
        self.pushButton.setFont(font)                                   #ボタンに設定
        self.pushButton.setObjectName("pushButton")
        self.label = QtWidgets.QLabel(self.centralwidget)               #ラベルを作って
        self.label.setGeometry(QtCore.QRect(100, 140, 271, 41))         #位置とサイズ設定
        font = QtGui.QFont()                                            #またフォントを作り
        font.setFamily("メイリオ")
        font.setPointSize(22)
        self.label.setFont(font)                                        #ラベルに設定
        self.label.setObjectName("label")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)                   #その他メニューとか
        self.menubar.setGeometry(QtCore.QRect(0, 0, 460, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)               #ステータスバーなども
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
    
    #表示するテキストは多国語対応のためかこうやって一括に
    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "押すな"))
        self.label.setText(_translate("MainWindow", "はろーわーるどっ!"))

 見ると分かるとおり、ウインドウの上にボタンなどのウィジェットを乗せたい場合、まずインスタンスを作り、親のコンテナウィジェットを指定し、位置や大きさ、その他の属性を設定してやらなければなりませんが、たったこの程度のウインドウでもけっこうな量です。複雑なインターフェースを持ったアプリでこんな物をちまちま書くなんて考えるだけで気が遠くなってきますが、QtDesignerとpyuicexeを使えばそのあたりを全部自動でやってくれるわけです。

 というわけで上記のテンプレから開始すれば、Uiを作る上での一番面倒なところをすっ飛ばすことができて大変お手軽なのでした。


 なおプログラムからデザイナーで作ったuiの要素を参照したい場合、例えば上記の例のボタンならself.ui.pushButtonという名前で参照できることになります。


 以下は補足です。

◆補足1 import文の書き方について

 Qt5関連でプログラムを組んでいくと、QtCore, QtGui, QtWidgetの3つのモジュールを頻繁に使うことになります。しかし毎回QtWidgets.QMainWindowみたいに長い名前を打つのは鬱陶しいし、from QtWidgets import * なんてものを使うのは邪悪なので、こんな感じで短く書ける名前にしてみました。

 またQtCore.Qtというのはいろんな定数を集めたネームスペースで、あちこちで非常に頻繁に使うことになるのでこれもQt一発で使えるように読んでおきます。

◆補足2 上位クラスの呼び出しについて

 Web上にあるサンプルなどでよく上位クラスのメソッド呼び出し(要するにinherited)に、例えば

Qw.QMainWindow.__init__(self, parent)

などと上のクラスの__init__を直接呼び出している例が多いようです。もちろん間違いではなく、Python2の頃はこう書くのが普通だったようですが、Python3からはこのsuper()という関数を使うのが推奨されてるみたいなのでここではこれを使っていくことにします。

Spyderにたいせつなこと

 というわけでハローワールド編は終わりですが、ここまで素直に書かれたとおりに実行した人がいたとしたら(いるわけないでしょうが)かなり困惑していることでしょう。

 というのは、Spyderを起動して例えば最初の5行のサンプルをやってみたら、まずウインドウがSpyder画面の後ろに現れるんで、ウインドウが出ていることに気づかないことでしょう。

 その上、それじゃもう一度やってみるかと同じスクリプトを再度実行してみると……

Kernel died, restarting


Kernel died, restarting


Kernel died, restarting

と大切だから二度どころか、3回も出てコンソールが再起動します。


 こちらもいきなりで最初は正直面食らいました。

 そこでいろいろやってるうちに設定→実行の中に

□ Clear all variables befor excection (IPython consoles)

とかがあったんでこれをチェックしたら死ななくなりました。Spyderってのは終了時の後始末もまともにできねーのかよ、しょうもない奴だなあ、所詮云々―――と、そのときは思ってそういう環境でしばらく使っておりました。


 しかしそれでもやっぱり動作が不安定です。一発目にウインドウが後ろに行くのはそのままだし、IPythonではなくPythonコンソールでも動作が不審です。そのうちにもっと複雑なプログラムを組み始めてみると、何だか理不尽な死亡が多発します。

 こちらもまだ始めたばかりなので途方にくれました。原因がQtにあるのか、Pythonなのか、PyQtなのか、Spyderの設定なのか、Windowsでやってることがそもそもの間違いなのか、といったことさえ分かりません。質問しようにも「なんかよく落ちるんですが」では門前払いでしょう。

 あーもうお手軽GUIの夢は諦めるか、と思いかけたところでふとこれまで特に深い考えなしにデフォルト実行設定の「現在のPython/IPythonコンソールで実行」というのを選択したままだったことに思いあたります。

 しかし考えてみたらそもそもこれって「対話型」の実行環境です。またSpyderのドキュメンテーションをみてみると真っ先に、

a powerful interactive development environment for the Python language ...

―――なんてことが書いてあります。


 えーと、対話型と言うなら今言ったことを覚えててくれてないと会話になりません。例えば一生懸命データ入力して計算させてみて、ちょっとあそこを変えてみようかなとか思ったときに、今入れたデータがまっさらに消えてたら、まあ人はそこでぶち切れるでしょう。

 Spyderにも「セルを実行」とか意味不明なコマンドがありましたが、要するにそういった文化の産物なのです。

 しかしアプリ開発の場合、前回の起動の残骸が残っている状態というのは非常に困るわけです。実行のたびに綺麗さっぱりになってくれないとまずいわけで……


 そこでもう一度設定をよく見ると、実行の設定にPythonに特化した新規コンソールで実行というのがあります。新規ということは毎回新しく起動しなおすということで―――これに変えてみたらなんと全て問題解決ではないですか!



 ―――要するに何が言いたかったかというと、SpyderでPyQtとかの開発をするときは上のこの設定を忘れずに、ということでした。


 あとついでに最初に設定しておいた方がいい項目ですが、

 他の設定はおいおい覚えていけばいいんではないかと思います。


 ということで次回はイベントのハンドリングです。

さらなるおまけ

 ところでもともとQtというのは日本語関係が結構アレだったみたいで、今は表示の方はかなり改善はされているようですが、細かいところでは至らぬ点がまだまだあるようです。

 特に困ったのが日本語入力関係で、Spyderで日本語入力すると変換中の文字列と通常の文字列の区別が全くつきません。特に文節区切りがどこか分からないのは非常に困ります。

 また確定後の再変換をやろうと思うと、行のテキストが丸ごと消えてしまったりします。

 前者の方はSpyderの実装の方の問題のようですが、後者の方はQtのテキストボックスでもそういう挙動なので根本的な問題でしょう。

 まあ、ソースを書く上ではあまり長文は書かないので細かく文節を切って変換したり、行が消えてしまっても慌てずに^Zで戻せるので全然どうしようもないわけではありませんが、このあたりの改善は望みたいところです。

 あとインタラクティブツアーはやらないほうがいいかもしれません(うちじゃタスクバーからプロセスツリーを消す羽目になりました……)


 なお、上記のようなトラブルは環境によっては起こらないかもしれないので、うちの実行環境を書いておきます。

2017-03-26