【Python/tkinter】”目” をマウスカーソルの方向に移動させるアプリの作り方

マウスカーソルの方向に目を移動させるアプリの作り方の解説ページアイキャッチ

今回はお遊びで、tkinter で目を動かす GUI アプリの作り方を解説していきたいと思います。

下のアニメのように、マウスの方向に目が動く感じです。

目玉がマウスカーソルの方向に動く様子

本当は、ある人気キャラを描いて、そのキャラの目を動かすようにしたかったのですが、著作権的に良くないかなーと思って、このページで使う図は目だけを載せるようにしています。

最後に紹介するスクリプトでは、ある人気キャラを描画し、そのキャラの目が動くようにするアプリを作ることが出来ます。是非ご自身で試してみて、どのキャラが描画されるか試してみてくださいね!

目をマウスカーソルの方向に動かすアプリの作り方

では「目をマウスカーソルの方向に動かす」アプリの作り方について解説していきたいと思います。

キャンバスの作成

まずはメインウィンドウとキャンバスを作成します。

今回は幅 600px、高さ 600px、背景色白色のキャンバスを作成したいと思います。

スクリプト的には下記になります。

キャンバスの作成
import tkinter

# メインウィンドウ作成
app = tkinter.Tk()

# キャンバス作成
canvas = tkinter.Canvas(
	app,
	width=600,
	height=600,
	bg="white",
	highlightthickness=0
)
canvas.pack()

app.mainloop()

これにより下図のようにキャンバスが GUI アプリ上に用意できます。

スポンサーリンク

目を描画する

次はキャンバスに “目” を描画していきます。まずは白目部分を描画していきます。

単純に楕円を描画することで白目を描画することができます。

tkinter においては Canvas クラスのインスタンスに create_oval メソッドを実行させることで楕円を描画することができます。

白目の描画
import tkinter

WHITE_A = 50
WHITE_B = 60

LEFT_X = 250
LEFT_Y = 140

RIGHT_X = 350
RIGHT_Y = 140

# メインウィンドウ作成
app = tkinter.Tk()

# 〜略〜

# 白目
canvas.create_oval(
	LEFT_X - WHITE_A, LEFT_Y - WHITE_B,
	LEFT_X + WHITE_A, LEFT_Y + WHITE_B,
	fill="white"
)

canvas.create_oval(
	RIGHT_X - WHITE_A, RIGHT_Y - WHITE_B,
	RIGHT_X + WHITE_A, RIGHT_Y + WHITE_B,
	fill="white"
)

app.mainloop()

(LEFT_XLEFT_Y) は左側の白目の中心座標、(RIGHT_XRIGHT_Y) は右側の白目の中心座標を表しています。

また WHITE_AWHITE_B は、描画する楕円に接する長方形における、中心からの左上頂点および右上頂点までの横方向・縦方向の距離になります。

白目の描画

上記のような create_oval メソッドやキャンバスへの図形の描画方法については下記で詳しく説明していますので、詳しく知りたい方はこちらも是非読んでみてください。

tkinterキャンバスに図形を描画する方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで図形を描画する

黒目を描画する

続いて黒目を描画します。まずは先ほど白目の中心に黒目を描画していきます。

白目同様、黒目についても描画するのは楕円ですが、円になるように縦横サイズが同じ楕円を描画していきます。

円についても楕円を描画する create_oval メソッドにより描画することができます。

fill は円の中を黒く塗りつぶすためのオプション指定であり、tag は後から黒目を操作(移動)するためのタグ付になります。 

黒目の描画
import tkinter

# 〜略〜

LEFT_X = 250
LEFT_Y = 140

RIGHT_X = 350
RIGHT_Y = 140

BLACK_R = 16

# 〜略〜

# 黒目
canvas.create_oval(
	LEFT_X - BLACK_R, LEFT_Y - BLACK_R,
	LEFT_X + BLACK_R, LEFT_Y + BLACK_R,
	fill="black",
	tag="black_left"
)
canvas.create_oval(
	RIGHT_X - BLACK_R, RIGHT_Y - BLACK_R,
	RIGHT_X + BLACK_R, RIGHT_Y + BLACK_R,
	fill="black",
	tag="black_right"
)

app.mainloop()

BLACK_R は描画する楕円に接する長方形の左上頂点・右下頂点と楕円の中心との縦方向&横方向の距離です。

目玉の描画

マウス移動イベントをバインドする

続いてマウスカーソルの移動により描画した黒目を移動させるために、マウスカーソル移動イベントに対してイベント処理の設定を行います。

これは下記のように mainloop の前に bind メソッドを実行することで設定することが可能です。

イベント設定
def motion(event):

# 〜略〜

# マウス移動時のイベントを設定
canvas.bind("<Motion>", motion)

app.mainloop()

これにより GUI アプリ上でマウスカーソルが移動した時に関数 motion が実行されるようになります。

イベントに関しては下記で詳細を解説していますので、イベントについて詳しく知りたい方はこちらを是非読んでみてください。

イベント処理解説ページのアイキャッチTkinterの使い方:イベント処理を行う

スポンサーリンク

マウスカーソルの方向に黒目を動かす

最後にマウスカーソルが移動された時に、黒目をその方向に移動させるようにしていきます。

前述の bind により、マウスカーソルが移動された時に関数 motion が自動的に実行されるようになっていますので、この関数 motion に、マウスカーソルの位置に基づいて黒目を動かすように処理を記述していきます。

マスカーソルの位置を取得する

マウスカーソルの方向に黒目を動かしたいので、まずはそのマウスカーソルがどの位置にあるかを取得する必要があります。

一方、先ほど実行した bind メソッドの第2引数で指定する関数には引数として event が渡されます。

この eventx 属性にはイベント発生時(つまりマウス移動時)のマウスカーソルの x 座標、y 属性にはマウスカーソルの y 座標がそれぞれ格納されていますので、マウスカーソルの位置(座標)は引数 event より取得することができます。

マウスカーソルの位置の取得

マウスカーソルの座標が白目内の座標であるかどうかを判断する

マウスカーソルの座標 (event.x, event.y) が白目内の座標の場合は、そのカーソルを中心とした位置に黒目を移動させてやれば良いです。

一方で、マウスカーソルの座標 (event.x, event.y) が白目外の座標の場合は、そのマウスカーソルの方向に&白目の外枠に接するように黒目を移動させます(白目の外にまで黒目を移動させてしまうと変ですよね…)。

こんな感じで、マウスカーソルが白目の中にあるかどうかで黒目の位置の決め方を変える必要があります。

で、これらを行うためには、まずは「マウスカーソルが白目の中にあるかどうかを判断する」必要があります。

これは、白目は前述の通り楕円で描画していますので、その楕円の中にマウスカーソルの座標 (event.x, event.y) が存在するかどうかを考えれば良いです。

この特定の座標が楕円の中に存在するかどうかを判断するのに便利なのが「楕円の方程式」です(高校生で習うだっけ?)。

「楕円の方程式」によれば、下図のような「中心が (x1, y1)、x 方向の長さが a、y 方向の長さが b の楕円」において、

楕円の方程式説明図

下記を満たす座標 (x, y) は楕円の線上に存在することになります。

$$ \frac {(x – x1) ^ 2}{a ^ 2} + \frac {(y – y1) ^ 2}{b ^ 2} = 1$$ 

さらに、これを応用して、座標 (x, y)  が楕円の内側に存在するかどうかは下記の式を満たすかどうかで判断することができます。

$$ \frac {(x – x1) ^ 2}{a ^ 2} + \frac {(y – y1) ^ 2}{b ^ 2} < 1$$ 

つまり、この式が成立するかどうかに応じて、黒目の位置の決め方を変更してやれば良いということになります。

例えば、左の白目は下記のように create_oval で楕円を描画していますので、

左の白目の描画
canvas.create_oval(
	LEFT_X - WHITE_A, LEFT_Y - WHITE_B,
	LEFT_X + WHITE_A, LEFT_Y + WHITE_B,
	fill="white"
)

上記の楕円の方程式におけるパラメータはそれぞれ下記のようになります。

  • x1:LEFT_X
  • y1:RIGHT_X
  • a:WHITE_A
  • b:WHITE_B
  • x:event.x
  • y:event.y

したがって、下のように if 文で判定すれば「マウスカーソルが白目の中にあるかどうかを判断する」ことができます。

マウスカーソルが楕円内にあるかの判断
x = event.x - LEFT_X
y = event.y - LEFT_Y

a = WHITE_A
b = WHITE_B

# 楕円方程式からマウスカーソルが白目の中にあるかを判断
if (x * x) / (a * a) + (y * y) / (b * b) < 1:
	# 白目の中にある場合の黒目の移動
else:
	# 白目の中にない場合の黒目の移動

マウスカーソルが “白目の中にある場合” の黒目の移動

マウスカーソルが “白目の中にある場合” は、そのマウスカーソルの位置に黒目を移動すれば良いだけです。

目玉の移動1

tkinter の Canvas クラスで描画した図形を移動させるためには coords メソッドを用いれば良いです。

例えば create_oval メソッドでは2つの座標(引数としては4つ)を指定し、これらの座標に基づいて楕円が描画されますが、描画した後にこの座標を coords メソッドにより変更することができます。

描画する座標が変わるので楕円の位置が変わることになります。

例えば、マウスカーソルが “白目の中にある場合” に左側の黒目を移動する場合は、下記のように coords メソッドを実行します。

白目内の黒目の移動
lx = event.x
ly = event.y

canvas.coords(
	"black_left",
	lx - BLACK_R, ly - BLACK_R,
	lx + BLACK_R, ly + BLACK_R
)

上記により、左側の黒目がマウスカーソルの座標 (event.x, event.y) が中心の半径 BLACK_R の円として再配置されることになります。

coords の第1引数には、create_oval メソッドで tag に指定したタグ名を設定します。これにより、このタグ名が付けられた図形に対して coords メソッドを実行することができます。

今回の場合は左側の黒目を coords メソッドで移動したいので、左側の黒目を描画する時に指定した "black_left" を設定しています。

マウスカーソルが “白目の中にない場合” の黒目の移動

マウスカーソルが “白目の中にない場合” は、そのマウスカーソルの方向に&白目の枠線に接するように黒目を移動させます。

目玉の移動2

これを行うには下記の2つを行います。

  • マウスカーソルの座標と x 軸との成す角度を求める
  • 角度から楕円上の点の座標を求める

まずは「マウスカーソルの座標と x 軸との成す角度を求める」を行います。

角度の算出

これは、楕円の中心座標とマウスカーソルの座標の差を x 方向と y 方向それぞれに対して求めてやれば、

角度の算出2

この差から math.atan2 関数により求めることができます。

角度の算出
import math
rad = math.atan2(y方向の差, x方向の差)

例えば左側の白目の中心座標 (LEFT_X, LEFT_Y) とマウスカーソルの座標 (event.x, event.y) から角度を求める場合は下記のように math.atan2 を実行します。

左目とマウスカーソルの成す角の算出
import math

x = event.x - LEFT_X
y = event.y - LEFT_Y

rad = math.atan2(y, x)

math.atan2 の返却値の単位はラジアンになります(math.pimath.pi の間の値)。

続いて「角度から楕円上の点の座標を求める」を行います。上記により角度 rad が求めれば、あとは下記を実行することで、楕円上の点の座標を求めることが可能です。

楕円上の座標の算出
楕円上の点の座標 x = 楕円の中心座標 x + 楕円の x 方向の長さ * cos(rad)
楕円上の点の座標 y = 楕円の中心座標 y + 楕円の y 方向の長さ * sin(rad)

例えばマウスカーソルが “白目の中にない場合” の左側の黒目の座標 (lx, ly)は下記のように求めることができます。

左目の楕円上の座標の算出
lx = LEFT_X + WHITE_A * cos(rad)
ly = LEFT_Y + WHITE_B * sin(rad)

黒目が白目からはみ出ないように改良

ここまでの解説を反映したスクリプトが下記のようになります。

目を動かすスクリプト
import tkinter
import math

# 目の描画パラメータ
WHITE_A = 50
WHITE_B = 60

LEFT_X = 250
LEFT_Y = 140

RIGHT_X = 350
RIGHT_Y = 140

BLACK_R = 16


def motion(event):
    global canvas

    # 左の目の楕円のパラメータ算出
    x = event.x - LEFT_X
    y = event.y - LEFT_Y
    a = WHITE_A
    b = WHITE_B

    # 楕円方程式からマウスカーソルが白目の中にあるかを判断
    if (x * x) / (a * a) + (y * y) / (b * b) < 1:
        # 白目の中ならマウスカーソルの位置に黒目を描画
        lx = event.x
        ly = event.y
    else:
        # 白目の外なら楕円の線上に黒目を描画
        rad = math.atan2(y, x)
        lx = LEFT_X + a * math.cos(rad)
        ly = LEFT_Y + b * math.sin(rad)

    # 右の目の楕円のパラメータ算出
    x = event.x - RIGHT_X
    y = event.y - RIGHT_Y
    a = WHITE_A
    b = WHITE_B

    # 楕円方程式からマウスカーソルが白目の中にあるかを判断
    if (x * x) / (a * a) + (y * y) / (b * b) < 1:
        # 白目の中ならマウスカーソルの位置に黒目を描画
        rx = event.x
        ry = event.y
    else:
        # 白目の外なら楕円の線上に黒目を描画
        rad = math.atan2(y, x)
        rx = RIGHT_X + a * math.cos(rad)
        ry = RIGHT_Y + b * math.sin(rad)

    # 求めた位置に左側の黒目の座標を移動
    canvas.coords(
        "black_left",
        lx - BLACK_R, ly - BLACK_R,
        lx + BLACK_R, ly + BLACK_R
    )

    # 求めた位置に右側の黒目の座標を設定
    canvas.coords(
        "black_right",
        rx - BLACK_R, ry - BLACK_R,
        rx + BLACK_R, ry + BLACK_R
    )


app = tkinter.Tk()

canvas = tkinter.Canvas(
    app,
    width=600,
    height=600,
    bg="white",
    highlightthickness=0
)
canvas.pack()

# 白目
canvas.create_oval(
    LEFT_X - WHITE_A, LEFT_Y - WHITE_B,
    LEFT_X + WHITE_A, LEFT_Y + WHITE_B,
    fill="white"
)

canvas.create_oval(
    RIGHT_X - WHITE_A, RIGHT_Y - WHITE_B,
    RIGHT_X + WHITE_A, RIGHT_Y + WHITE_B,
    fill="white"
)

# 黒目
canvas.create_oval(
    LEFT_X - BLACK_R, LEFT_Y - BLACK_R,
    LEFT_X + BLACK_R, LEFT_Y + BLACK_R,
    fill="black",
    tag="black_left"
)

canvas.create_oval(
    RIGHT_X - BLACK_R, RIGHT_Y - BLACK_R,
    RIGHT_X + BLACK_R, RIGHT_Y + BLACK_R,
    fill="black",
    tag="black_right"
)

# マウス移動時のイベントを設定
canvas.bind("<Motion>", motion)

app.mainloop()

実行すると、下のアニメのようにマウスカーソルの位置に合わせて黒目が移動するようになります。

スクリプトの動作

が、白目から黒目がはみ出てしまってちょっと気持ち悪いですね…。

そこで白目から黒目がはみ出ないように改良したいと思います。

この改良は簡単で、今まで白目の楕円に対して判断や計算を行ってきた部分を、「黒目の半径分白目を小さくした楕円」に対して行うように変更すれば良いだけです。

下記が改良後のスクリプトです。黄色背景部分を変更しただけになります。

目を動かすスクリプト改良版
import tkinter
import math

# 目の描画パラメータ
WHITE_A = 50
WHITE_B = 60

LEFT_X = 250
LEFT_Y = 140

RIGHT_X = 350
RIGHT_Y = 140

BLACK_R = 16


def motion(event):
    global canvas

    # 左の目の楕円のパラメータ算出
    x = event.x - LEFT_X
    y = event.y - LEFT_Y
    a = WHITE_A - BLACK_R
    b = WHITE_B - BLACK_R

    # 楕円方程式からマウスカーソルが白目の中にあるかを判断
    if (x * x) / (a * a) + (y * y) / (b * b) < 1:
        # 白目の中ならマウスカーソルの位置に黒目を描画
        lx = event.x
        ly = event.y
    else:
        # 白目の外なら楕円の線上に黒目を描画
        rad = math.atan2(y, x)
        lx = LEFT_X + a * math.cos(rad)
        ly = LEFT_Y + b * math.sin(rad)

    # 右の目の楕円のパラメータ算出
    x = event.x - RIGHT_X
    y = event.y - RIGHT_Y
    a = WHITE_A - BLACK_R
    b = WHITE_B - BLACK_R

    # 楕円方程式からマウスカーソルが白目の中にあるかを判断
    if (x * x) / (a * a) + (y * y) / (b * b) < 1:
        # 白目の中ならマウスカーソルの位置に黒目を描画
        rx = event.x
        ry = event.y
    else:
        # 白目の外なら楕円の線上に黒目を描画
        rad = math.atan2(y, x)
        rx = RIGHT_X + a * math.cos(rad)
        ry = RIGHT_Y + b * math.sin(rad)

    # 求めた位置に左側の黒目の座標を移動
    canvas.coords(
        "black_left",
        lx - BLACK_R, ly - BLACK_R,
        lx + BLACK_R, ly + BLACK_R
    )

    # 求めた位置に右側の黒目の座標を設定
    canvas.coords(
        "black_right",
        rx - BLACK_R, ry - BLACK_R,
        rx + BLACK_R, ry + BLACK_R
    )


app = tkinter.Tk()

canvas = tkinter.Canvas(
    app,
    width=600,
    height=600,
    bg="white",
    highlightthickness=0
)
canvas.pack()

# 白目
canvas.create_oval(
    LEFT_X - WHITE_A, LEFT_Y - WHITE_B,
    LEFT_X + WHITE_A, LEFT_Y + WHITE_B,
    fill="white"
)

canvas.create_oval(
    RIGHT_X - WHITE_A, RIGHT_Y - WHITE_B,
    RIGHT_X + WHITE_A, RIGHT_Y + WHITE_B,
    fill="white"
)

# 黒目
canvas.create_oval(
    LEFT_X - BLACK_R, LEFT_Y - BLACK_R,
    LEFT_X + BLACK_R, LEFT_Y + BLACK_R,
    fill="black",
    tag="black_left"
)

canvas.create_oval(
    RIGHT_X - BLACK_R, RIGHT_Y - BLACK_R,
    RIGHT_X + BLACK_R, RIGHT_Y + BLACK_R,
    fill="black",
    tag="black_right"
)

# マウス移動時のイベントを設定
canvas.bind("<Motion>", motion)

app.mainloop()

これにより、白目の外に出ないように黒目を動かすようにすることができます。

改良盤スクリプトの動作

人気キャラの目を動かすスクリプト

最後にある人気キャラの目を動かすスクリプトを紹介しておきます。

といっても、目を動かす部分はここまで解説してきた内容と全く同じです。背景としてキャラクターを描画しているだけです。

実行結果載せると良くないかもしれないので、是非どんなキャラが描画されるかご自身で試してみてください。

あるキャラの目を動かすスクリプト
import tkinter
import math

# 目の描画パラメータ
WHITE_A = 50
WHITE_B = 60

LEFT_X = 250
LEFT_Y = 140

RIGHT_X = 350
RIGHT_Y = 140

BLACK_R = 16


def motion(event):
    global canvas

    # 左の目の楕円のパラメータ算出
    x = event.x - LEFT_X
    y = event.y - LEFT_Y
    a = WHITE_A - BLACK_R
    b = WHITE_B - BLACK_R

    # 楕円方程式からマウスカーソルが白目の中にあるかを判断
    if (x * x) / (a * a) + (y * y) / (b * b) < 1:
        # 白目の中ならマウスカーソルの位置に黒目を描画
        lx = event.x
        ly = event.y
    else:
        # 白目の外なら楕円の線上に黒目を描画
        rad = math.atan2(y, x)
        lx = LEFT_X + a * math.cos(rad)
        ly = LEFT_Y + b * math.sin(rad)

    # 右の目の楕円のパラメータ算出
    x = event.x - RIGHT_X
    y = event.y - RIGHT_Y
    a = WHITE_A - BLACK_R
    b = WHITE_B - BLACK_R

    # 楕円方程式からマウスカーソルが白目の中にあるかを判断
    if (x * x) / (a * a) + (y * y) / (b * b) < 1:
        # 白目の中ならマウスカーソルの位置に黒目を描画
        rx = event.x
        ry = event.y
    else:
        # 白目の外なら楕円の線上に黒目を描画
        rad = math.atan2(y, x)
        rx = RIGHT_X + a * math.cos(rad)
        ry = RIGHT_Y + b * math.sin(rad)

    # 求めた位置に左側の黒目の座標を移動
    canvas.coords(
        "black_left",
        lx - BLACK_R, ly - BLACK_R,
        lx + BLACK_R, ly + BLACK_R
    )

    # 求めた位置に右側の黒目の座標を設定
    canvas.coords(
        "black_right",
        rx - BLACK_R, ry - BLACK_R,
        rx + BLACK_R, ry + BLACK_R
    )


app = tkinter.Tk()

canvas = tkinter.Canvas(
    app,
    width=600,
    height=600,
    bg="white",
    highlightthickness=0
)
canvas.pack()

# 顔の外枠
canvas.create_arc(
    50, 75, 550, 550,
    start=-45,
    extent=270,
    style=tkinter.CHORD,
    fill="blue"
)

# 顔の内枠
canvas.create_arc(
    100, 150, 500, 550,
    start=-40,
    extent=260,
    style=tkinter.CHORD,
    fill="white"
)


# 鼻
canvas.create_oval(
    275, 175, 325, 225,
    fill="red"
)

# 口
canvas.create_arc(
    150, 200, 450, 450,
    start=180,
    extent=180,
    style=tkinter.CHORD,
    fill="red"
)

# 唇
canvas.create_arc(
    125, 275, 175, 325,
    start=90,
    extent=180,
    style=tkinter.ARC,
)

# 鼻の下
canvas.create_line(
    300, 225, 300, 325,
    fill="black"
)

# 髭を6本
canvas.create_line(
    250, 225, 150, 200,
    fill="black"
)

canvas.create_line(
    250, 250, 150, 250,
    fill="black"
)

canvas.create_line(
    250, 275, 150, 300,
    fill="black"
)

canvas.create_line(
    350, 225, 450, 200,
    fill="black"
)

canvas.create_line(
    350, 250, 450, 250,
    fill="black"
)

canvas.create_line(
    350, 275, 450, 300,
    fill="black"
)


# 白目
canvas.create_oval(
    LEFT_X - WHITE_A, LEFT_Y - WHITE_B,
    LEFT_X + WHITE_A, LEFT_Y + WHITE_B,
    fill="white"
)

canvas.create_oval(
    RIGHT_X - WHITE_A, RIGHT_Y - WHITE_B,
    RIGHT_X + WHITE_A, RIGHT_Y + WHITE_B,
    fill="white"
)

# 黒目
canvas.create_oval(
    LEFT_X - BLACK_R, LEFT_Y - BLACK_R,
    LEFT_X + BLACK_R, LEFT_Y + BLACK_R,
    fill="black",
    tag="black_left"
)

canvas.create_oval(
    RIGHT_X - BLACK_R, RIGHT_Y - BLACK_R,
    RIGHT_X + BLACK_R, RIGHT_Y + BLACK_R,
    fill="black",
    tag="black_right"
)

# マウス移動時のイベントを設定
canvas.bind("<Motion>", motion)

app.mainloop()

スポンサーリンク

まとめ

このページでは “目” をマウスカーソルの方向に移動させるアプリの作り方について解説しました。

単純なように思えて、楕円の方程式など数学の知識が必要で若干難易度は高いと思います。

数学が苦手・苦手だった人も多いと思いますが、今回のようにプログラミングに絡めればそんな数学も楽しく学ぶ・復習することができます。

特に今回のように絵を描画するようなプログラムを作る場合は楕円の方程式は結構使いますので、是非この機会に復習しておくと良いと思います!

また今回は「キャンバスへの図形の描画」や「キャンバスに描画した図形の操作」を行うことで目を動かしました。

この「キャンバスへの図形の描画」や「キャンバスに描画した図形の操作」については下記ページで解説していますので、キャンバスを使いこなしたい方は是非こちらのページもご覧ください!

tkinterキャンバスに図形を描画する方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで図形を描画する tkinterキャンバスの図形の操作方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで描画した図形を操作する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です