14.GUIプログラムの基礎 (2015.4.9-2020.2.27)

Raspberry Piは、外部制御だけでは無くGUIのプログラムも作れます。Pythonではいくつかの方法があるようですが、テキストでは標準で用意されているTkinterライブラリを使った方法が紹介されています。あらかじめ用意されたウィジェット(GUIの部品)を、ウインドウに配置するだけで機能が利用できるので、容易にGUIの画面を実現できるのです。今後、Raspberry Piによって制御を実現するに当たり、人とのインターフェースの要となる重要な要素なので、少し力を入れてマスターしたいと思います。では、早速簡単なプログラムで実験してみましょう。なお、本書では部品を配置する元となるウインドウをrootとしていて、ネットでもそうした例が多いようなので同様にします。(好みで付けて構わないですが)

まずは、テキストに従ってウインドウにHello World!の文字を表示してみます。結果を示すまでも無いので、直接ご自身で確認してみて下さい。

ところで、テキストはTkinterの頭文字が大文字になっていますが、Pythonのバージョン3ではtkinterと全て小文字にしないとエラーになります。調査したところ、バージョン2までは逆にテキストのように記述しないとエラーになるようです。print文の変更といい、こういった重要コマンドの変更は混乱の元なのでやめて欲しいものです。本来であれば互換性のために両方を許容すべきで(ツールとしては洗練されていないかもしれませんが)、どうもLinux系は有志による開発のせいか、いつまでたっても自作気分が抜けなくて困ります。変更される度にプログラムを修正ではたまりません。いずれにせよバージョン3以降は記述に注意して下さい。

なお、OSが古いバージョンだとTkinterライブラリがインストールされていない場合があります。私の場合、NOOBS2.1では入っていませんでした。別途インストールする場合はターミナルで下記を実行します。
sudo apt-get install tk-dev
またはオプションを記述して
sudo apt-get install tk-dev --fix-missing
とします。念のため次のように事前にアップデートした方が良い場合があるかもしれません。
sudo apt-get update
sudo apt-get upgrade
必要に応じて実行して下さい。なお、時間帯やRasPiの処理能力等によってはうまく接続が確立されなくて、各種ファイルのインストールやアップデートが何度も失敗することがあるようです。あきらめずに時間を置く等して、根気良く取り組む必要がありそうです。

プログラムの説明に戻りましょう。上記プログラムを実行すると小さなウインドウ一杯に文字が表示されるのがわかります。ウインドウサイズを指定しないため、文字を最小限に表示する形になるようです。もしかすると環境によっては適当なサイズ(デフォルト)のウインドウが表示されるかもしれません。それぞれの命令については、テキストの説明を参考に願います。概略はこうです。最初にTkinterのライブラリをインポートし、次にベースとなるTkオブジェクトのインスタンスを生成します。変数名は慣例に従ってrootにしています(名前は自由です)。そしてメインとなる文字表示のLabelウィジェットを生成し、pack()メソッドによってそれを配置します。最後にmainloop()メソッドを実行して表示を続けます。

ついでに少し応用して、ウインドウサイズは(ヨコ)600×(タテ)400ピクセルで指定し、文字はウインドウの中央辺り(260, 190)に置くことにしましょう。場所を指定するためにpack()の代わりにplace()メソッドを使います。ウインドウ名は「GUIテスト」、ウインドウ内の表示文字は「Hello World!」にします。

一般的なウインドウにおいて、必要な部品を指定場所に表示するという最も基本的なことが可能になったと思います。次はテキストに従って色々なウィジェットを置いて実験を続けます。最初はButtonウィジェットです。テキストではボタンを押すと表示が変わるようにしています。少しアレンジしてウインドウは上記のサイズで実行します。ボタンと文字はウインドウ中央付近で、場所は何回かトライして適当な位置を決めました。テキストでも書いているように、ボタンが押されて表示を変えるだけでは、次に押した時に何も変化がありません。そのため一定時間経過後に元の表示に戻す処理を加えています。下図が最初に書いたプログラムですが、実はこれではうまく動きません。実際に試してみればわかります。

ここではラベル表示を「押してね!」から「押された!」に変更した後に、タイマーで1秒の時間待ちを入れています。ところが、文字変化はタイマー待ちの後になってしまい、結果的に次の「押してね!」がすぐに実行されて文字の変化がわかりません。試しにタイマー待ちとその後の「押してね!」の命令をコメントアウトすると、ボタンを押した時に「押された!」に変わることがわかります。なぜこんなことになるのか正確にはわかりませんが、sleep()メソッドは一時停止命令なので、文字変化を画面表示する前に停止状態になってしまうのだと思います。ハードウェア絡みで恐らく画面表示の変更には時間がかかり、表示変更が間に合わないのではと推測します。それで停止が解除された瞬間に表示が復帰しているのではないでしょうか。Pythonのバグの疑いもありますが、やはり不確実な命令は避けた方が賢明です。この回避策として、タイマー割り込みを利用した形に変更してみます。そのためthreadingモジュールをインポートしています。なお、待ち時間はより確認しやすいように2秒としました。

これで下図のように期待通りに動くようになりました。

 

テキストではbind()メソッドを使って、マウスカーソルがボタンの外に出たらラベルを書き換えるようにしているので、比較してみるのも面白いと思います。確認は簡単なので、上記の命令にテキストの関連部分を追加しトライしてみて下さい。追加関数はact3とでもしておきましょうか。act2で共用できるかと思ったのですが、テキストの注釈通りbind()メソッドから呼び出される関数には引数が1つ必要です。なお、タイマー待ちを5秒にしておくと確認しやすいと思います。改変したものがうまく動けば、ボタンを押した後ラベルが「押された!」に変化し、そのままマウスを動かさずに5秒すれば「押してね!」に戻ります。一方、待つことなくすぐにマウスカーソルをボタンの外に移動しても「押してね!」に戻ります。

それでは次にRadiobuttonウィジェットに移ります。少々欲張って、テキストボックスと絵を描くを一度にやってみましょう。ラジオボタンを2つ作って、各ボタンをクリックするとテキストボックスにそれぞれのボタンが押されたことを表示し、更にその下のランプが黒からボタン1なら赤に、ボタン2なら緑にします。オプション類の説明はテキストを参照して下さい。

少し長くなりますが、テキストの例を元に少しアレンジする形で実現できます。テキストボックスはEntryウィジェットを使い、ランプの描画はCanvasウィジェットをウインドウの指定位置に作り、そこに丸いアイテムを置いて色を指定します。命令によっては記述が長くなるので、これからは括弧内でのスペースはカンマ区切りの後のみにします。オプションが多い時は逆に読みやすくなると思います。生成するウインドウ自体もコンパクトにし、ウィジェットの配置位置は例によってカット&トライで決めました。

実行すると下図のようになります。変数selで1を指定しているため、初期状態でボタン1が選択状態になっていますが、2を指定するとボタン2を選択、0だとどちらも非選択状態になります。テスト結果は左から初期状態→ボタン1を押した時→ボタン2を押した時です。(スクリーンショットを少し縮小しています)

  

テキストで残るはスライドバーです。ボリュームやサイズ調整等色々な用途に使えます。テキストの例を元に、スライドバーを動かすとラベル値が変化するものを作ってみましょう。スライドバーにはラベルがセットになっているので、変化する値のみを別のラベルで表現することにします。

スライダーが一番左の時の値は0で、右に行くにつれて値が大きくなり、一番右では100になります。値を直接表示することはあまり無いと思いますが、この変化する数値を使って色々な操作ができます。

 

最後にPhotoImageを使ってウインドウに画像を表示してみましょう。テキストではコラム扱いですが、GUIパネルを作る上で使用する機会は多いと思います。覚えておいて損は無いでしょう。表示するのは当サイトのシンボルの「robodog」です。Tkinterで画像を扱うにはクラスImageを使います。具体的にはサブクラスであるカラーを扱うPhotoImageの利用です。簡単な例としてLabelウィジェットで画像を表示してみます。左がプログラム、右が実行結果です。画像はgifアニメですが、静止画状態の表示になっていました。

    

今度はCanvasウィジェットを配置して、そこに画像を表示してみます。Canvasの背景を黒にして、画像と区別しやすくしています。Canvasウィジェットを使えば、画像に変化を与えたい時に応用できるのではと思います。

実行すると下図のように表示します。

GUIについては更に学習の必要性を感じているので、機会を見てまた改めて取り組みたいと考えています。