今回は下のアニメのように、GUI アプリ上で惑星の周りを衛星に回転させる方法について解説していきたいと思います。
ポイントは衛星が惑星の周りを回転する部分と思うかもしれませんが(これも重要ですが)、最大のポイントはキャンバスに描画する図形の「奥行き」情報です。
この奥行きを考慮した図形描画を行うことで、アプリの見た目の自由度が格段にアップします!
是非今回紹介する「衛星が惑星の周りを回転する」アプリの作り方から、この奥行きの重要性について理解していただければと思います!
Contents
奥行きを考慮せずに図形を描画する
まずは奥行きを考慮せずに、惑星の周りを衛星が回転するアプリを作成していきたいと思います。
スクリプト
奥行きを考慮せずに惑星の周りを衛星が回転するアプリのスクリプトは下記のようになります。
import tkinter
import math
# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 300
# 中央座標
CENTER_X = CANVAS_WIDTH // 2
CENTER_Y = CANVAS_HEIGHT // 2
# 軌道の楕円の横方向・縦方向のサイズ
ORBIT_A = 200 # 横方向
ORBIT_B = 50 # 縦方向
# 惑星と衛星の半径
PLANET_R = 100
SATELITE_R = 20
# 衛星を再配置する時間の間隔
TIME_INTERVAL = 10
# 衛星を再配置する角度の感覚
RAD_INTERVAL = math.pi / 100
# 回転中かどうかのフラグ
rotate_flag = False
# afterのID(キャンセル実行よう)
after_id = None
# 衛星の回転角度
rad = 0
# キャンバス
canvas = None
def rotate():
'''衛星を回転させる関数'''
global canvas
global after_id
global rad
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)
# 求めた位置を中心に衛星再配置
canvas.coords(
"satelite",
x - SATELITE_R, y - SATELITE_R,
x + SATELITE_R, y + SATELITE_R
)
# 衛星を描画する角度を増加
rad += RAD_INTERVAL
if rad > math.pi:
rad -= 2 * math.pi
# TIME_INTERVAL[ms]後に再度rotate実行
after_id = canvas.after(TIME_INTERVAL, rotate)
def rotate_switch():
'''衛星回転をON/OFF切り替える関数'''
global canvas
global rotate_flag
global after_id
if rotate_flag:
# 回転動作中に実行された場合
# rotateの定期実行を終了する
rotate_flag = False
canvas.after_cancel(after_id)
else:
# 回転動作していない時に実行された場合
# rotateの定期実行を終了する
rotate_flag = True
# 回転実行
rotate()
def main():
'''main関数'''
global canvas
# メインウィンドウ作成
app = tkinter.Tk()
# キャンバス作成
canvas = tkinter.Canvas(
app,
width=CANVAS_WIDTH,
height=CANVAS_HEIGHT,
bg="white",
highlightthickness=0
)
canvas.pack()
# ボタン作成
button = tkinter.Button(
app,
text="ボタン",
command=rotate_switch # クリック時に実行する関数
)
button.pack()
# 惑星を表す楕円描画
canvas.create_oval(
CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
tag="planet",
fill="orange",
outline="orange"
)
# 衛星の軌道を表す楕円の描画
canvas.create_oval(
CENTER_X - ORBIT_A, CENTER_Y - ORBIT_B,
CENTER_X + ORBIT_A, CENTER_Y + ORBIT_B,
outline="lightgray",
width=10
)
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)
# 衛星を表す楕円の描画
canvas.create_oval(
x - SATELITE_R, y - SATELITE_R,
x + SATELITE_R, y + SATELITE_R,
tag="satelite",
fill="blue",
width=0
)
# メインループ
app.mainloop()
if __name__ == '__main__':
# main関数実行
main()
スポンサーリンク
スポンサーリンク
スクリプトの解説
簡単にスクリプトの解説をしておきます。
ウィジェットの作成
スクリプトの main
関数では、まず下の図のように「メインウィンドウ」と「キャンバスウィジェット」と「ボタンウィジェット」を作成しています。
惑星と軌道と衛星の描画
そして、キャンバスウィジェットの中に、惑星を表す楕円と、衛星の軌道を表す楕円と、衛星を表す楕円を順々に描画しています。
衛星の回転
また、ボタンウィジェットがクリックされた時に、rotate_switch
関数が実行されるようにしており、rotate_switch
関数の中で衛星の回転の ON / OFF を切り替えるようにしています。
回転 ON になった場合は、定期的に rotate
関数が実行されるようにしており、rotate
関数の中で衛星の回転を行っています。
定期的に rotate
関数が実行できるように after
メソッドを活用しています。
この after
メソッドについては下記で詳しく解説していますので、after
メソッドをご存知無い方は是非読んでみてください。tkinter を利用する上で必須級に重要なメソッドなので是非使いこなせるようになっていただきたいです。
衛星の軌道は下の図のような座標・サイズの楕円として描画しているので、
楕円上の衛星の中心座標 (x
, y
)は下記により求めることができます。
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)
なので、定期的に実行される rotate
関数の中で、「上記で求まる座標に衛星を表す楕円を移動する」と「回転角度を表す rad
の増加」を行うことで、定期的に衛星が軌道上を回転する動きを実現することができます。
楕円を移動するためには Canvas
クラスの coords
メソッドを利用します。
この coords
などのように描画した図形を後から操作するメソッドについては下記ページで解説していますので、詳しく知りたい方はこちらをご覧ください。
スクリプトの実行結果
で、このスクリプトを実行すると下の図のような画面が表示されます。
まずこの時点で見た目が変ですよね…。惑星の裏側にある軌道は惑星で隠れて非表示になって欲しいところですが、普通に表示されてしまっています。
ボタンをクリックすると衛星が回転し始めます(もう一度押すと回転が止まる)。
衛星も惑星の裏側にある時も惑星で隠れずに表示されてしまっています。
この理由はキャンバスの「奥行き」情報にあります。
次はこのキャンバスの「奥行き」について解説していきたいと思います。
キャンバスの奥行きについて
キャンバスでは、描画された1つ1つの図形が奥行き情報によって管理されています。
奥行き情報とは、描画された図形が重なった時に、どの図形が前面に表示され、どの図形が背面に表示されるかを決定する情報です。
そして、基本的にキャンバスに描画された図形は、後から描画された図形ほど前面に現れて表示されるようになります。
先ほどのスクリプトでは「惑星 → 軌道 → 衛星」の順で図形を描画したため、惑星が最も背面に描画されるようになります。
なので、軌道も衛星も惑星の前面に描画され、惑星でこれらが隠れずにいつでも表示される結果になっていたというわけです。
じゃあ「軌道 → 衛星 → 惑星」の順で描画すれば良いのかというと、これだと惑星が際前面に描画されるので、惑星と軌道や衛星が重なった時には、常に軌道や衛星が隠れてしまうことになります。
下のアニメのような動きにするためには、もうちょっと工夫が必要です。
ここからはこのアニメのように惑星の裏側にある時だけ衛星と軌道が隠れるようにするスクリプトを紹介していきたいと思います。
スポンサーリンク
キャンバスの奥行きを考慮して改良する1
それではスクリプトを改良していきたいと思います。
スポンサーリンク
考え方
まずは図形の描画順を工夫することで、惑星の裏側にある軌道と衛星が惑星で隠れるようにしていきたいと思います。
この方法は単純です。
要は惑星を上半分と下半分に真っ二つに分解し、下記の順序で図形の描画を行います。
- 惑星の下半分
- 軌道
- 衛星
- 惑星の上半分
これにより、惑星の下半分は最背面に描画されるので、軌道や衛星の方が前面に表示されます。
一方惑星の上半分は最前面に描画されるので、軌道や衛星の方が背面に表示され、図形が重なった時は惑星の方が表示されることになります。
スクリプト
惑星を上半分と下半分に分割して描画するスクリプトは下記のようになります。
import tkinter
import math
# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 300
# 中央座標
CENTER_X = CANVAS_WIDTH // 2
CENTER_Y = CANVAS_HEIGHT // 2
# 軌道の楕円の横方向・縦方向のサイズ
ORBIT_A = 200 # 横方向
ORBIT_B = 50 # 縦方向
# 惑星と衛星の半径
PLANET_R = 100
SATELITE_R = 20
# 衛星を再配置する時間の間隔
TIME_INTERVAL = 10
# 衛星を再配置する角度の感覚
RAD_INTERVAL = math.pi / 100
# 回転中かどうかのフラグ
rotate_flag = False
# afterのID(キャンセル実行よう)
after_id = None
# 衛星の回転角度
rad = 0
# キャンバス
canvas = None
def rotate():
'''衛星を回転させる関数'''
global canvas
global after_id
global rad
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)
# 求めた位置を中心に衛星再配置
canvas.coords(
"satelite",
x - SATELITE_R, y - SATELITE_R,
x + SATELITE_R, y + SATELITE_R
)
# 衛星を描画する角度を増加
rad += RAD_INTERVAL
if rad > math.pi:
rad -= 2 * math.pi
# TIME_INTERVAL[ms]後に再度rotate実行
after_id = canvas.after(TIME_INTERVAL, rotate)
def rotate_switch():
'''衛星回転をON/OFF切り替える関数'''
global canvas
global rotate_flag
global after_id
if rotate_flag:
# 回転動作中に実行された場合
# rotateの定期実行を終了する
rotate_flag = False
canvas.after_cancel(after_id)
else:
# 回転動作していない時に実行された場合
# rotateの定期実行を終了する
rotate_flag = True
# 回転実行
rotate()
def main():
'''main関数'''
global canvas
# メインウィンドウ作成
app = tkinter.Tk()
# キャンバス作成
canvas = tkinter.Canvas(
app,
width=CANVAS_WIDTH,
height=CANVAS_HEIGHT,
bg="white",
highlightthickness=0
)
canvas.pack()
# ボタン作成
button = tkinter.Button(
app,
text="ボタン",
command=rotate_switch # クリック時に実行する関数
)
button.pack()
# 惑星の下半分を表す円弧の描画
canvas.create_arc(
CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
tag="planet_lower",
start=180,
extent=180,
fill="orange",
outline="orange"
)
# 衛星の軌道を表す楕円の描画
canvas.create_oval(
CENTER_X - ORBIT_A, CENTER_Y - ORBIT_B,
CENTER_X + ORBIT_A, CENTER_Y + ORBIT_B,
outline="lightgray",
width=10
)
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)
# 衛星を表す楕円の描画
canvas.create_oval(
x - SATELITE_R, y - SATELITE_R,
x + SATELITE_R, y + SATELITE_R,
tag="satelite",
fill="blue",
width=0
)
# 惑星の下半分を表す円弧の描画
canvas.create_arc(
CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
tag="planet_upper",
start=0,
extent=180,
fill="orange",
outline="orange"
)
# メインループ
app.mainloop()
if __name__ == '__main__':
# main関数実行
main()
スポンサーリンク
スポンサーリンク
スクリプトの解説
最初にお見せしたスクリプトと比べて main
関数では惑星を楕円ではなく、円弧を2つ描画するように変更しています。
この円弧は create_arc
メソッドを実行することで描画しています。
create_arc
メソッドについては下記ページで解説していますので、詳しく知りたい方は是非こちらもご覧ください。
create_arc
メソッドに指定するオプションの中でポイントになるのが start
と extent
です。
start
:円弧を描画開始する角度extent
:円弧を描画する角度
図でこの start
と extent
の関係を表すと下の図のようになります。
1つ目の円弧を描画する際に実行する create_arc
メソッドには start=180
、extent=180
を指定していますので、惑星(円)の下半分が描画されることになります。
図形の描画としは最初に実行しているので、最背面に表示されることになります。したがって、この後に描画する軌道や衛星は惑星の下半分よりも常に前面に表示されることになります。
2つ目の円弧を描画する際に実行する create_arc
メソッドには start=0
、extent=180
を指定していますので、惑星(円)の上半分が描画されることになります。
図形の描画としは最後に実行しているので、最前面に表示されることになります。したがって、この前に描画される軌道や衛星は惑星の上半分よりも常に背面に表示される(隠れる)ことになります。
スポンサーリンク
スクリプト実行結果
スクリプトを実行してボタンを押すと、下のアニメのように衛星が惑星の周りを回転します。
衛星が惑星の上半分と重なる際には、背面に存在する衛星が前面に存在する惑星によって隠れるので、衛星が惑星の後ろ側を回転していることがわかりやすくなったと思います。
キャンバスの奥行きを考慮して改良する2
今度はちょっと違った方法で惑星の周りを衛星が回転しているように見えるようにスクリプトを改良していきたいと思います。
スポンサーリンク
考え方
前述の通り、図形は基本的に後から描画したものが前面に表示されることになります。
ただし、この奥行き情報は後から変更することが可能です。
下記はこの奥行き情報を変更する Canvas
クラスのメソッドになります。
lower(tagOrId, belowThis)
:tagOrId
で指定した図形をbelowThis
のすぐ背面に移動するlift(tagOrId, aboveThis)
:tagOrId
で指定した図形をaboveThis
のすぐ前面に移動する
衛星を惑星の背面に移動させたい時は lower
メソッドを、衛星を惑星の前面に移動させたい時は lift
メソッドをそれぞれ実行することで、「衛星を惑星で隠す or 衛星を惑星よりも前面に表示する」を制御することができます。
lower
メソッドと lift
メソッド等の「描画した図形を操作する方法」については下記ページで詳しく解説していますので、よろしければこちらもご覧ください。
スクリプト
lower
メソッドと lift
メソッドを利用したスクリプトは下記のようになります。
import tkinter
import math
# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 300
# 中央座標
CENTER_X = CANVAS_WIDTH // 2
CENTER_Y = CANVAS_HEIGHT // 2
# 軌道の楕円の横方向・縦方向のサイズ
ORBIT_A = 200 # 横方向
ORBIT_B = 50 # 縦方向
# 惑星と衛星の半径
PLANET_R = 100
SATELITE_R = 20
# 衛星を再配置する時間の間隔
TIME_INTERVAL = 10
# 衛星を再配置する角度の感覚
RAD_INTERVAL = math.pi / 100
# 回転中かどうかのフラグ
rotate_flag = False
# afterのID(キャンセル実行よう)
after_id = None
# 衛星の回転角度
rad = 0
# キャンバス
canvas = None
def rotate():
'''衛星を回転させる関数'''
global canvas
global after_id
global rad
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)
# 求めた位置を中心に衛星再配置
canvas.coords(
"satelite",
x - SATELITE_R, y - SATELITE_R,
x + SATELITE_R, y + SATELITE_R
)
if y > CENTER_Y:
# 衛星が惑星の中心座標よりも下側にある場合
# 衛星を惑星よりも前面に移動
canvas.lift("satelite", "planet")
else:
# 衛星が惑星の中心座標よりも上側にある場合
# 衛星を惑星よりも背面に移動
canvas.lower("satelite", "planet")
# 衛星を描画する角度を増加
rad += RAD_INTERVAL
if rad > math.pi:
rad -= 2 * math.pi
# TIME_INTERVAL[ms]後に再度rotate実行
after_id = canvas.after(TIME_INTERVAL, rotate)
def rotate_switch():
'''衛星回転をON/OFF切り替える関数'''
global canvas
global rotate_flag
global after_id
if rotate_flag:
# 回転動作中に実行された場合
# rotateの定期実行を終了する
rotate_flag = False
canvas.after_cancel(after_id)
else:
# 回転動作していない時に実行された場合
# rotateの定期実行を終了する
rotate_flag = True
# 回転実行
rotate()
def main():
'''main関数'''
global canvas
# メインウィンドウ作成
app = tkinter.Tk()
# キャンバス作成
canvas = tkinter.Canvas(
app,
width=CANVAS_WIDTH,
height=CANVAS_HEIGHT,
bg="white",
highlightthickness=0
)
canvas.pack()
# ボタン作成
button = tkinter.Button(
app,
text="ボタン",
command=rotate_switch # クリック時に実行する関数
)
button.pack()
# 惑星を表す楕円描画
canvas.create_oval(
CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
tag="planet",
fill="orange",
outline="orange"
)
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)
# 衛星を表す楕円の描画
canvas.create_oval(
x - SATELITE_R, y - SATELITE_R,
x + SATELITE_R, y + SATELITE_R,
tag="satelite",
fill="blue",
width=0
)
# メインループ
app.mainloop()
if __name__ == '__main__':
# main関数実行
main()
スポンサーリンク
スポンサーリンク
スクリプトの解説
今回は lower
メソッドと lift
メソッドの解説に焦点を当てたいと思ったので、軌道は省略させていただいています。
このスクリプトのポイントは rotate
関数で lower
メソッドと lift
メソッドを実行している下記部分です。
if y > CENTER_Y:
# 衛星が惑星の中心座標よりも下側にある場合
# 衛星を惑星よりも前面に移動
canvas.lift("satelite", "planet")
else:
# 衛星が惑星の中心座標よりも上側にある場合
# 衛星を惑星よりも背面に移動
canvas.lower("satelite", "planet")
y
は回転中の衛星の縦方向の中心座標、CENTER_Y
は惑星の縦方向の中心座標になります。
また lift
と lower
の引数で指定している "satelite"
と "planet"
は、それぞれ衛星と惑星を表す楕円を描画する時に tag
オプションで指定したタグ名です。
衛星を惑星の前面に表示したいのは「衛星の中心座標が軌道の中心座標よりも下側にいる」時なので、y > CENTER_Y
を満たすかどうかを判断し、満たす場合は lift
を実行して衛星を惑星よりも前面に表示するようにしています
さらに、y > CENTER_Y
を満たさない場合は、lower
を実行して衛星を惑星よりも背面に表示するようにしています。この場合は衛星は惑星よりも背面に表示されるので、惑星と重なった時に隠れます。
キャンバスの座標では、左上が原点かつ縦の正方向が下なので、y > CENTER_Y
を満たす y
は CENTER_Y
よりも下側に存在することになります
スポンサーリンク
スクリプト実行結果
スクリプトを実行してボタンを押すと、下のアニメのように衛星が惑星の周りを回転します。
今回は軌道の描画は省略したので、軌道は表示されません。
lift
と lower
メソッドの実行により、衛星と惑星の前面・背面関係が交互に入れ替わっていることが確認できると思います。
まとめ
今回は、特に「キャンバスの奥行き」について理解していただくために「惑星の周りを衛星に回転させる」アプリの作り方について解説しました。
具体的には、下記の2つの方法を用いて奥行きを考慮した図形の表示を実現しました。
- 図形を背面・前面に表示したい部分に分割して描画する
- 図形を
lift
とlower
メソッドで背面 or 前面に移動する
図形は先に描画した図形から順に背面に表示されるようになるので、基本的にはこれを利用して図形の描画順序を工夫するのが良いと思います。
ただし、前面・背面に表示したい部分を細かく制御したいような場合は「図形の分割」、ユーザーの操作によって図形を前面・背面入れ替えたいような場合は「lift
と lower
メソッド」が有効です。
これらのテクニック・メソッドを知っておくことで作成できるアプリ・実現できるアプリの見た目の自由度も上がりますので、是非この機会に覚えておいてください!