PyQtでお手軽GUI開発♪―――は可能だったか? 第3回 イベント編
さて、前回ウインドウが出たわけですが、それだけでは文字通り絵に描いた餅です。
GUIというのはイベントドリブン、例えば「ボタンを押した」という行為(イベント)がトリガーとなってコードが実行される仕組みになっています。すなわちそこができなければプログラムにはなりません。
Delphiではボタンコントロール(=ウィジェット)などのプロパティに“OnClickイベント”という項目があって、そこを選択すればそのボタンをクリックしたら実行されるコードが記述できるようになっています。ではQtではそのあたりどうなのでしょうか?
イベントハンドリング方法
というわけでざっと調べてみると、Qtではシグナル+スロットという仕組みでイベントのハンドリングを行うようです。見つけたデモでは押されたボタンのclicked()というシグナルに、ウインドウのclose()というスロットを結びつけて、こうしたらボタンを押したらウインドウが閉じますよ、とかやっています。
そのあたりを軽くチェックした後、それじゃちょっとウインドウをクリックした場所に何か描いてみるかと思ったわけです。Delphiではそういう場合メインフォームに“OnMouseDown”というイベントがあって、クリックした場所の座標とかが分かります。Qtでももちろんそれに相当するシグナルがあるはずです。
ところが―――なぜかどこにも見当たりません。シグナルとスロットの接続設定画面に「□ QWidgetから継承したシグナルとスロット表示する」などという項目があるので、これか! と思ってチェックしても同じです。
えーっと……これって基本中の基本なんですが?
そうして色々調べ回ったあげく分かったことは、そんなシグナルはない! ということでした。
ではウインドウに対するMouseDownイベントはどうやって取得するかというと―――それはQMainWindow(正確にはQWidget)のプロテクトメソッドでした。Qtではウインドウをマウスでクリックしたような場合は、そのメソッドが呼び出される仕掛けになっていたのです。
そしてその種のイベントに対して独自処理を行うには、メソッドのオーバーライドをしなければならないということです!
結局、Qtでイベントのハンドリングをするには大きく以下の3つの異なった手順があるということが判明しました。
1. イベントメソッドをオーバーライドする
2. シグナルとスロットを接続する
3. アクションを経由する
というわけでこれからこの3種類のやり方についての説明をしようと思います。
イベントメソッドのオーバーライド
前述のようにQtでは例えば以下のような自分自身に対するイベントはQMainWindowクラスの(厳密にはその親クラスQWidgetのProtected Functionsとして定義されています。
その中を見てみると……
◆マウスイベント
mousePressEvent(QMouseEvent *event)
mouseReleaseEvent(QMouseEvent *event)
mouseMoveEvent(QMouseEvent *event)
mouseDoubleClickEvent(QMouseEvent *event)
wheelEvent(QWheelEvent *event)
ありました。前述のOnMouseDownイベントはこの中のmousePressEventに相当するようです。
これ以外にも……
◆キーボードイベント
keyPressEvent(QKeyEvent *event)
keyReleaseEvent(QKeyEvent *event)
◆描画イベント
paintEvent(QPaintEvent *event)
◆ウインドウに対するイベント
showEvent(QShowEvent *event)
hideEvent(QHideEvent *event)
resizeEvent(QResizeEvent *event)
closeEvent(QCloseEvent *event)
こんな感じの基本的なイベントがたくさん定義されています。
- なお、こういったことを調べるためにQtのドキュメントを見に行くことは今後頻繁に出てきますが、イベント関連の関数を調べるために直接の継承元のQMainWindowのページを見に行っても何も書いてません。これらはその親クラスであるQWidgetで定義されているので、そちらを見なければならないことに注意して下さい。
さて、メソッドのオーバーライドといっても特にPythonの場合は大したことありません。要するに上記と同じ名前・同数の引数のメソッド(もちろんselfは別ですが)を書くだけです。そのうえPythonなんで型なんかも気にする必要はありません
PyQtのプログラムとは元々QMainWindowなどのサブクラスを作ることなので、要するに今作っているクラスに……
def mousePressEvent(self, mouseevent):
pass
というメソッドを追加すれば終わりです。QtDesignerを使う必要がない分、これだけならむしろ楽といえるでしょう。
このメソッドのパラメータであるmouseeventというのは、QMouseEventというクラスのインスタンスで、この中に例えば以下のようにマウスを押されたときの各種情報が納められています。
mouseevent.button() --- 押されたボタン(Qtネームスペース内で定義されてます)
mouseevent.x() --- 座標
mouseevent.y()
mouseevent.globalX() --- グローバル座標
mouseevent.globalY()
イベントの種類ごとにこういったパラメータを定義するQ**Eventというのがありますが、それらに関する説明はQtドキュメントのQEventのサブクラス(Inherited By:云々)の中に見つかるでしょう。
- 残念ながらドキュメントは英語しかありませんが、どういうメソッドがあるかとかいうようなことなら難しくないと思います。それに昨今ではGoogle翻訳の精度が上がっているので、特にこういったドキュメントのたぐいは翻訳がなくてもほぼそれで間に合っています。
シグナル+スロット
メソッドのオーバーライドは自分自身に対してしか使えないのは明白です。そこで異なったウィジェット間のイベント応答をするために、Qtにはシグナル+スロットという仕組みがあります。
その概念は簡単です。あるウィジェットに何かのイベントが発生した場合、シグナルと呼ばれるものが発行されて、それに結びつけられているスロットと呼ばれる関数が実行されるという仕組みです。それがどう実装されてるかなんて当面は知らなくても問題ないでしょう。
例えばQPushButtonの例だと
clicked(bool checked = false)
pressed()
released()
toggled(bool checked)
―――などというシグナルがあります。
またメインウインドウには例えば以下のスロットがあります。
close()
hide()
show()
...
これを結びつけるためには以下のように書きます。
self.ui.pushButton.clicked.connect(self.close)
こうすることでボタンが押されたらウインドウがクローズするようになるわけです。
ちなみにシグナルには例えば上の例のtoggled(bool checked)のように、何らかのパラメータを持った物もあります。そういったシグナルを受けるスロットは当然それを受け取るための引数がなければなりません。
QtDesignerでのシグナル+スロット処理
しかし、このようなことをいちいち手で書いているのは野蛮だと言えるでしょう。そんな単調作業はなるべく他人に押しつけたいものです。
そこでまたQtDesignerの登場です。これを使えばシグナル/スロット関係の設定はほぼみんなこれだけでできてしまいます。
そこでまずボタンを押したらウインドウが閉じるという機能を実装してみます。
- 編集メニューからシグナル/スロットを編集というのを選択します。
- ボタンをドラッグしてウインドウにドロップすると画面のような線が出て、シグナル/スロット接続を設定というダイアログが開きます。
- ダイアログの左のペインには接続元のPushButtonにあるシグナル、右のペインには接続先のMainWindowのスロットが表示されているので、まず接続元のシグナルを選択します(この場合はClicked())
- 最初は右側が寂しいですが、左下の「QWidgetから継承したシグナルとスロットを表示する」をチェックすると、右のペインにclose()というのが現れるので、それを選択してOKします。
- 戻ると画面がこんな感じになっているので……
- これをセーブして、pyuic5で変換して実行すると、ボタンを押したらウインドウが閉じるようになります。
- デザイナーのフォームというメニュー内にプレビューという項目があります。そこでこれらを動作も含めて確認することができます。
こういう物だったらQtDesignerだけで完結してしまうわけです。
スロットを自作する
しかし、普通プログラムを組むということは、何かもっと気の利いたことをさせたいから組んでいるわけです。すなわちボタンが押されたらあなたの作ったもっとスペシャルな関数が呼び出されるようにしたいわけで―――これは要するにスロットを自作するということに他なりませんが、実はこれもほぼQtDesignerに任せてしまうことができます。
以下はボタンが押されたらclose()の代わりにmySpecialFunction()が呼ばれるようにする手順です。
1~3は前節と同じなので省略します。
- スロット画面の下にある編集ボタンを押します。
- MainWindow のシグナル/スロットというダイアログが開くので、緑色の+ボタンを押します。
- slot1() という名前のスロットが勝手にできてますが……
- これを今回のmySpecialFunction()に変更してOKを押します(関数の()の中のパラメータは特に書かなくて大丈夫です)
- 戻ると接続設定画面に今作ったスロットが現れているので、それを選択します。
- フォームを保存してコンバートし、プログラムを実行すると……
―――“MyForm”には“mySpecialFunction”なんてねーぞ! と、真っ赤になって怒られます(そこまでは自動でやってくれません)
- そこでMyFormの中に以下のような関数を追加します。
def mySpecialFunction(self):
self.ui.label.setText('押すなっつったろうがあっ!')
- そうすれば今度は(とりあえず)OKのようです。
ちなみにHello.pyの中を見てみると後ろの方に
self.pushButton.clicked.connect(MainWindow.mySpecialFunction)
というのがあるのが分かるでしょう。
このようにシグナルとスロット、さらにその接続はQtDesignerだけで大体できてしまいます。
メニューとアクション
以上でボタンを押したら何か起こる系のことは終わりですが、ではそれじゃ今度はメニューをつけてみようとか思った瞬間にまた困ったことが起こります。
ここに入力とかがあるので入力してみると
こんな風になって、メニューが簡単にできていきます。
ここまでは問題ありません。そこでメニューのシグナルをスロットに連結しようと思ってシグナル/スロットの編集モードにしますが……
なぜかメニューが開きません!
実はメニューの処理はボタンとはまた異なっていて、メニュー項目を作成するとそれに対応するアクションという物ができる仕掛けになっていたのです。
そこで右下の方にあるアクションエディタというのを開いてみると……(右下にドックしたままでは狭いので、以下の画像はフロート状態にしています)
今作ったメニューの項目らしきものがあります! 名前がactionClose_Cとか何とかで、テキストは入力したものそのままです。
で、それをどうするかというと―――続いて今度はその横のタブにあるシグナルスロットエディタというのを開いてみましょう。
するとさっきボタンに設定したシグナルとスロットが表示されていると思いますが、そこで左上の緑の+ボタンを押すと、以下のように<発信者><シグナル><受信者><スロット>というのが表示されるでしょう。
その<発信者>をダブルクリックすると薄っぺらいコンボボックスが現れます。その中を見てみると、今できたactionClose_Cというのがいます!
そこでまずそのactionClose_Cを選択します。
続いてその<シグナル>を見てみると中にtriggered()というのがあるので、それを選択します。
さらに<受信者>からMainWindowを選び
<スロット>からClose()を選びます。
これは何をしたかというと結局、actionClose_Cというものがtriggered()というシグナルを発行したら、MainWindowのclose()というメソッドが呼ばれるという仕掛けを作ったわけです。
そしてもう想像の通りに、メニューのClose(&C)をクリックしたらこのアクションのtrigeerd()が発行されます。
すなわちメニューからウインドウが閉じられるようになったわけです。
―――最初はちょっとややこしいように思うでしょうが、実はこれはかなり優れた仕掛けでしょう。
たとえばプログラムがちょっと複雑になってくると、コマンドをメニューからだけでなくツールバーやコンテキストメニュー、ショートカットキーなどからも起動したくなってきます。また、キー割り当てというのは自分の流儀にうるさい人が多く、キー割り当ての変更メニューをつけろなどととねじ込まれます。
そんなような場合、コマンドの起動をactionというオブジェクトで管理し、それが様々なトリガーから起動するという仕組みにしておくと非常に柔軟に対応できるわけです。
アクションからメニューなど作る
ところで前節ではメニューからアクションを作りましたが、その逆も可能です。すなわち最初にアクションエディタでアクションを定義して、それをメニューやツールバーにドラッグドロップすることもできるのです。以下はその手順です。
- まず一番トップレベルのメニューはあらかじめ作っておきます。
- アクションエディタを開き、左上の新規のアイコンをクリックすると
- 以下のような新しいアクションの定義画面が開きます。
- テキストを入力するとオブジェクト名が自動的にできていきます。例えば“Save(&S)”と入れたなら“actionSave_S”というオブジェクト名ができます(ただしAscii文字でないとダメみたいです)
- その他の項目、ツールチップとかアイコンとかショートカットなども必要に応じて定義します(後からいくらでも修正できるのでとりあえず放っておいてもいいでしょう)
- できたらOKボタンを押します。するといまのアクションができています。
- 新しいアクションをアクションエディタからドラッグして、メニューの上に持っていくとメニューが自動的に開くので、挿入したい場所にドロップすればメニューができあがります。
- ツールバーを作りたければ、ウインドウを右クリックして出てくるコンテキストメニューにツールバーを追加というのがあるので、それでツールバーを作ってその上にアクションをドロップすればOKです。
なお、このアクションのシグナルはスロットとしても機能します。
すなわち、ボタンを押したときのシグナルをアクションのシグナルに連結することができるわけです。
これは言い換えると、様々なコマンドをアクションとして定義しておくことで、どういうところからでも呼び出せるようになるということです。
というわけでちょっと大きめのアプリケーションを作りたければ、まずはどういうアクションがあるかという設計から始めるのがいいでしょう。とりあえずそうしておけば後からそれをどう起動するかは好きに変更できるからです。
イベントの発行元をチェックする
このようにQtではシグナル、アクション、スロットというものを使ってイベントのハンドリングをするわけですが、やっているうちに複数のシグナルを一つのスロットで処理したくなることは多々あるでしょう。
例えばエディタみたいな物を作っていれば、上下方向の画面スクロールコマンドなどが必要になります。しかしこのたぐいは処理が普通は共通なので、別々にするより一つの関数に書いた方が便利です。
こういった場合はself.sender()という関数を利用します。これはシグナルを発行したオブジェクトを返す関数です。
そこで例えば“actionScrollUp”と“actionScrollDown”というアクションがあって、それを共通の“scrollWindow()”というスロットで処理したかったとすれば、
def scrollWindow(self): if self.sender() == self.ui.actionScrollUp: #上スクロールの処理 elif self.sender() == self.ui.actionScrollDown: #下スクロールの処理
このように記述して場合分けができます。
- ちなみにself.sender()というのは、QWidgetのさらに上位クラスのQObjectのプロテクトメソッドなので、QWidgetのところを見ていてもその存在に気づかないでしょう。Qtのドキュメントというのはこんな感じであちこちに情報が分散しているので注意が必要です。
ウィジェットの格上げ
さて、GUIアプリを作ろうと思えば、多かれ少なかれ画面をマウスでぐりぐりする系の操作が発生するものです。しかしQtでは前述の通り画面へのマウスイベントはQWidgetのプロテクトメソッドで定義されています。
これがMainWindowに対するものならともかく、あるウィジェット上のローカル座標系で云々という話になったときには、結局そのウィジェットのサブクラスを作るという羽目になるわけです。
ところが当然のことながら、QtDesignerのウィジェットボックスにあなたの作ったクラスなどが入っているはずありません。では自前のウィジェットを画面に貼りつけてデザインしたいような場合にはどうすればいいのでしょうか?
こういった場合に格上げという操作を使います(サブクラスなんだから「格下げ」のような気もするんですが……)
ここであなたが作ったmywidgetsというモジュール内の、QLabelを継承したMySpecialLabelを画面に表示したいとします。
- まず貼りたい位置に普通のラベルを貼ります
- そのラベルを選択して右クリックするとポップアップメニューに格上げ先を指定...という項目があるのでそれを選択します。
- すると以下のような画面が出るので、ベースクラス名にQLabelを選択し、格上げされたクラス名にMySpecialLabelと入力します。またヘッダファイルの欄にはそのクラスのモジュール名の拡張子を.hに変えたもの、今回の場合ならmywidgets.hと記述して右の追加ボタンを押します。
- 格上げされたクラスの欄にMySpecialLabelというのが現れて、下の格上げボタンが押せるようになるので、これを押せば格上げ完了です。
いま格上げしたラベルがどのように読まれているかというと、自動生成されるhello.pyを見てみればその途中くらいにlabel_2という名前で
... self.label_2 = MySpecialLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(30, 90, 50, 12)) self.label_2.setObjectName("label_2") ...
と、オリジナルラベルを生成して位置などを指定しているところがあります。
また一番最後には
from mywidgets import MySpecialLabel
という形でインポートされています。
- Pythonの場合あまり行儀はよくありませんが、コード中のどこでもインポートすることができます。
こうすることでQtDesigner上でオリジナルラベルを貼りつけて、その位置や大きさ・その他のプロパティなどを設定できるわけです。
ということで……
とまあ、PyQtでのイベントのハンドリングをざっと見てきたわけですが、これはDelphiとかと比べると少々どころか“かなり”ややこしいと言わざるを得ないでしょう。
とはいえ、そんな操作も個々に見ればそれぞれは単純な機械的作業といえます。従って分かってみれば大したことないとは言えるかもしれませんが―――まあ、その「分かるまで」が大変だったんですが……
というわけで続いては画面描画についてです。
- なお、何か間違いなどがありましたらこちらの方にコメントしてください(質問等でも構いませんが、多分答える能力があまりないのではないかと思います)