Raspberry Piで図形を描画(Python、Tkinter)
ラズパイに機器を繋げてデータを取る時、とりあえずターミナルなどに数字をずらーっと表示させます。でも数字の羅列だと特に傾向が掴みにくい。そういうのはやっぱりグラフや図形で見たいと思うわけです。
Pythonでグラフを描くライブラリは色々とあります。折れ線や棒グラフなどが適しているデータならもちろんそれを使うのが早いんですが、時にオリジナルな表示もしたくなるんですね。例えば先日ジャイロセンサーを熱かった時に「水平器のような表示をしたいなぁ」って思ったんです。
そこで今回はラズパイのウィンドウに図形を表示してみます。
Tkinterのcanvasで円を描いてみよう
今回使うのはPythonでGUIを表示するライブラリの一つであるtkinter。この基本的な所は以前取り上げましたので、「tkinterって何?」という方はご一読下さい:
Tkinterにはcanvasという図形を描画する専用の仕組みが用意されています。まずは実際に動かして感触を掴みましょうか。円を描いてみましょう:
# 円を描いてみる
import tkinter
# ルートウィンドウを作成
root = tkinter.Tk()
root.geometry( "400x400" )
# 白地のcanvasを作成
# ルートウィンドウのサイズに拡大
canvas = tkinter.Canvas( root, bg = "white" )
canvas.pack( fill = "both", expand = True )
# 円を描画
canvas.create_oval( 50.0, 50.0, 350.0, 350.0 )
# ウィンドウスタート
root.mainloop()
この短いコードで、
円が描かれたウィンドウが表示しました。
最初なのでコードを詳しく説明します。まずtkinter.Tkオブジェクトをrootとして作成しています。これはメインウィンドウですね。続くgeometryメソッドでウィンドウのクライアントサイズを指定します。このメソッド、数字では無く文字列で指定するので注意下さい。
次に図形を描画するレイヤーであるtkinter.Canvasオブジェクトを作成しています:
# 白地のcanvasを作成
# ルートウィンドウのサイズに拡大
canvas = tkinter.Canvas( root, bg = "white" )
canvas.pack( fill = "both", expand = True )
文字通り絵を描画するキャンバスのような役目をしてくれます。第1引数に親レイヤーを渡すので、ここではメインウィンドウであるrootを渡しています。bgオプションはキャンバスの地の色を指定します。
続くcanvas.packは親レイヤーであるrootにこのキャンバスを配置するメソッドです。これを呼ぶ事で目で見える物になります。fillオプションはキャンバスを親レイヤーのサイズに対してどう引き伸ばすかを指定します。bothを指定すると上下左右方向に引き伸ばしてくれます。ただし引き延ばしを許可するexpandオプションをTrueにしないと効果はありません。
キャンバスを配置したら、キャンバス上に図形を描く事が出来ます:
# 円を描画
canvas.create_oval( 50.0, 50.0, 350.0, 350.0 )
図形を描くメソッドは沢山ありますが、上ではcreate_ovalメソッドで円を描いています。このメソッドは本来楕円を描くんですが、引数の描画範囲を正方形にすると円を描けます。(x0, y0, x1, y1)という並びで、左上(x0,y0)から右下(x1,y1)の長方形に内接する円が描かれます。
最後にroot.mainloop()を呼ぶとウィンドウが表示されます。
描いた円を動かす
Tkinterで描く図形は個々で分離していまして、個別に動かしたりする事が出来ます。冒頭で描いた円を右に動かしてみましょう:
# 円を描いてみる
import tkinter
# ルートウィンドウを作成
root = tkinter.Tk()
root.geometry( "400x400" )
# 白地のcanvasを作成
# ルートウィンドウのサイズに拡大
canvas = tkinter.Canvas( root, bg = "white" )
canvas.pack( fill = "both", expand = True )
# 円を描画
circle = canvas.create_oval( 50.0, 50.0, 350.0, 350.0 )
# 円を右に再配置してみる
canvas.moveto( circle, 100, 100 )
# ウィンドウスタート
root.mainloop()
先のコードに動かす(再配置する)部分を追加しました。canvas.movetoメソッドがそれです。
円などを描画した時、実はその図形を表すオブジェクトIDが返って来ます。上では描画した円のIDをcircleとして受け取っています。canvas.movetoメソッドの第1引数に動かしたい図形オブジェクトのIDを指定し、第2、3引数に左上座標を指定するとその位置に再配置してくれます。
これは割とありがたい仕様です。もし円をキャンバスにピクセルとして穿つ仕様だった場合、円を動かすために一度キャンバスを白地に塗って再描画する必要があります。tkinterの図形描画はそうでは無くて、各図形が透明レイヤーのように分離しています。そのため再配置や表示・非表示が個別で出来るんです。
描いた円を消す
canvasに描いた図形を消す事もとても大切です。沢山の図形を描画するのはコスト高ですから。図形を消すにはcanvas.deleteメソッドで図形IDを指定します:
# 円を描いてみる
import tkinter
# ルートウィンドウを作成
root = tkinter.Tk()
root.geometry( "400x400" )
# 白地のcanvasを作成
# ルートウィンドウのサイズに拡大
canvas = tkinter.Canvas( root, bg = "white" )
canvas.pack( fill = "both", expand = True )
# 円を描画
circle = canvas.create_oval( 50.0, 50.0, 350.0, 350.0 )
# 円を消す
canvas.delete( circle )
# ウィンドウスタート
root.mainloop()
上ではcanvas.deleteメソッドで描いた円を消しています。実行すると何も描画されていないウィンドウ(キャンバス)になります。ぱっと見意味は無いですが、処理としては重要ですよ。
円を動かし続ける
さてここまではウィンドウ表示と同時に描画済みのキャンバスを表示して終わりでしたが、それだとあまり実用的では無いんですよね。外部機器から送られてくる情報に対応し即座に描画位置等が変化しないといけないわけです。Tkinterはroot.mainloopメソッドを呼んでしまうとそこから先プログラムが進まなくなってしまうため、そういう動的な更新は別スレッドで動作させる必要があります。
例として、小さな円をぐるぐると円運動させ続けてみましょう。
# 円を描いてみる
import tkinter
import threading
import time
import math
# スレッド対象の関数
def startThread():
# 定期的に更新関数を呼ぶ
while True:
start = time.perf_counter()
update( 0.033 )
end = time.perf_counter()
while end - start < 0.0333:
time.sleep( 0.0001 )
end = time.perf_counter()
# ルートウィンドウを作成
root = tkinter.Tk()
root.geometry( "400x400" )
# 白地のcanvasを作成
# ルートウィンドウのサイズに拡大
canvas = tkinter.Canvas( root, bg = "white" )
canvas.pack( fill = "both", expand = True )
# 円を描画
circleR = 25.0
circle = canvas.create_oval( 0.0, 0.0, circleR * 2, circleR * 2 )
t = 0.0
# 更新関数
def update( delta ):
# 円を回転
global t
global circleR
r = 100.0 # 回転半径
cx = 200.0 # 回転中心X
cy = 200.0 # 回転中心Y
canvas.moveto( circle, cx + r * math.cos( t ) - circleR, cy + r * math.sin( t ) - circleR )
# 時刻更新
t += delta
# 別スレッドで更新処理を行う
thread = threading.Thread( target = startThread, daemon = True )
thread.start()
# ウィンドウスタート
root.mainloop()
まずはスレッドを作っている下部のコードを見て下さい:
# 別スレッドで更新処理を行う
thread = threading.Thread( target = startThread, daemon = True )
thread.start()
threading.Threadメソッドでスレッドを1つ追加する事が出来ます。targetオプションにスレッド対象となる関数を指定します。deamonというオプションに注目です。これは「デーモンスレッド」と言って、Trueにするとメインスレッドが終了した時に一緒にこのスレッドも終了させる事が出来ます。これをFalse(規定値)にするとメインスレッドが終了しても作成したスレッドが動いている場合はアプリケーションが終了しません。今回はそれだと不都合なためデーモンスレッドを有効にしました。
作成したスレッドはstartメソッドで開始出来ます。
targetで指定したstartThread関数はコードの冒頭で定義しています:
# スレッド対象の関数
def startThread():
# 定期的に更新関数を呼ぶ
while True:
start = time.perf_counter()
update( 0.033 )
end = time.perf_counter()
while end - start < 0.0333:
time.sleep( 0.0001 )
end = time.perf_counter()
ごにょごにょ書いていますが、要は定期的にupdate関数を呼びたいだけです。whiteを無加工で使うととんでもない勢いでupdate関数が呼ばれ続けてしまいますので、呼ぶ前と呼んだ後で時刻を記録して、0.033秒ごとにupdate関数が呼ばれるようにしています。あまり正確ではありませんが、まぁ良いでしょう(^-^;
本丸のupdate関数では経過時間tを使って小さな円を半径100で円運動させています。今回は円運動ですが、update関数は定期的に呼ばれるので自由に何でも出来ます。ただフレームレートは安定していないので、時刻にシビアな事は出来ません。
プログラムを動かすと小円がクルクル円運動します:
…静止画だとわからんですな(^-^;;。ラズパイじゃなくてWindowsでもMacでもUbuntuでも全く同じコードで動きますので、コピペしてcanvas.pyなどとして保存して、コマンドプロンプトやPowerShellで、
python ./canvas.py
としてみて下さい。
終わりに
tkinterはPythonの標準GUIライブラリなのですぐに使え便利です。それで図形描画も出来るのですからお手軽なものです(^-^)。
今回は描画、移動、削除をピックアップしてみました。これらは図形描画のベースとなる基礎知識です。またupdate関数を定期的に呼び出す事で図形を動かし続ける事もできました。これを押さえておけば、後は円なり線なり好きなように描けるはずです。
図形描画を駆使して扱っている機器の状態をグラフィカルに確認出来るようにすれば、機器の制御やテストをより直感的に行えるようになると思います。GUIは偉大です!
ではまた(^-^)/