【Python/tkinter】デジタル時計の作り方(7セグ表示版)

デジタル時計の作り方の解説ページアイキャッチ

今回は Python で tkinter を用いた「デジタル時計」の作り方について解説していきたいと思います。

出来上がりは下のアニメのようになります。

作成するデジタル時計の仕上がりを示すアニメ

ポイントは、単に時間を文字列として表示するわけではなくて、よく見かける実際のデジタル時計のように、数字を7セグ風に表示するところです。

数字を表示する7セグを示した図

もしかしたら文字を7セグ風に表示してくれるフォントがあるかもしれませんが、今回は実際に7セグをキャンバス上に描画し、時刻に合わせて各セグの色を変更させることで、このデジタル時計を作成していきたいと思います。

色を変更することで数字をセグ表示する様子

ちなみに、時刻の取得方法や時刻の表示の定期的な更新を after メソッドを利用して行う点などはアナログ時計と同じです。ですので、この辺りについてはこのページでは説明をせず、”7セグで時刻を表示する” ところに焦点を当てて、デジタル時計の作り方について解説していきたいと思います。

時刻の取得方法や時刻の表示の定期的な更新方法については、下記ページの「アナログ時計」の作り方の解説の中で説明していますので、これらについて知りたい方は下記ページを読んでいただければと思います。

アナログ時計の作り方の解説ページアイキャッチ【Python/tkinter】アナログ時計の作り方

では早速、デジタル時計の作り方について解説していきたいと思います。

キャンバスを作成する

まずは、時刻をセグ表示するためのキャンバスを作成していきます。

今回は8つのキャンバスを作成したいと思います。

つまり、時刻の各数字をセグ表示する6つのキャンバスと、時刻の時と分、分と秒を区切るコロン(:)を表示する2つのキャンバスを作成します。

作成するキャンバスの説明図

キャンバスを作成するためには、tkinter.Canvas() を実行します。このキャンバスの作り方の詳細については下記ページで解説していますので、詳しく知りたい方はこちらを参照していただければと思います。

tkinterのキャンバスの作り方解説ページアイキャッチTkinterの使い方:キャンバスウィジェットの作り方

これらのキャンバスは、デジタル時計のように見えるように、”横方向” に並べて配置します。

今回は、横方向にキャンバスを並べるために、pack メソッドを使って配置していきたいと思います。pack メソッドは上から下に向かってウィジェットを配置するイメージが強いと思いますが、pack メソッド実行時に side=tkinter.LEFT オプションを指定してやれば、左から右に向かってウィジェットを配置することができます。

packでウィジェットを横に並べる
# canvasはtkinter.Canvas()の戻り値
canvas.pack(side=tkinter.LEFT)

ただし、pack メソッドを実行したウィジェットから順に左から配置されていく点に注意が必要です。

また、数字を表すキャンバスでは、時刻の一桁分の数字に対するセグ表示のみを行います。

例えば時刻が 12:34:56 である場合、各キャンバスでは左から順に 123456 の数字をセグ表示します(コロンを表示するキャンバスは飛ばす)。

キャンバスに時刻をセグ表示する様子

キャンバス上にセグを描画する

続いて、数字をセグ表示するキャンバスそれぞれに対してセグを描画していきます。

セグは下の図のような “六角形” を描画することで表現することができます。

六角形がセグを表現する様子

六角形を描画する際には、tkinter の Canvas クラスの create_polygon を利用します。

create_polygon
def create_polygon(x1, y1, x2, y2, .... , xn, yn, option, ....)

この create_polygon を実行することで、引数に指定した座標 (x1, y1) 〜 座標 (xn, yn) を頂点とする多角形を描画することができます。

create_polygonで多角形を描画する例

今回は “六角形” を描画するので、create_polygon には座標 (x1, y1) 〜 座標 (x6, y6) の6つの頂点の座標を指定して実行することになります。

で、この create_polygon を1回実行することで描画できるのは六角形1つなので、7つのセグを描画するためには、描画する位置に合わせて指定する座標を変更しながら7回 create_polygon を実行する必要があります。

各セグをcreate_polygonで描画する様子

さらに、数字を表すキャンバスが6つあるので、それぞれのキャンバスに対して上記を繰り返す必要があります(つまり合計42回 create_polygon を実行する)。

じゃあ具体的に create_polygon に指定する座標の値はどう計算すれば良いのかというと、これについては複雑なので、作り方の解説の最後のセグの座標を計算するで別途説明させていただきたいと思います。

また、create_polygon 実行時にオプションを指定することにより、描画する多角形の見た目の変更等を行うことが可能です。下記ページでこれらのオプションについても解説していますので、興味のある方はぜひ読んでみてください(他の図形を描画する方法と合わせて解説しています)。

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

スポンサーリンク

時刻に合わせてセグを点灯させる

セグが描画できれば時刻をセグ表示する準備が整ったことになります。

あとは、取得した時刻に合わせて描画したセグを点灯 or 消灯させれば良いです。

セグの点灯と消灯

各セグの点灯と消灯は、描画したセグ(つまり六角形)の塗りつぶし色を変更することで実現することができます。

tkinter の Canvas クラスには itemconfig メソッドが用意されており、オプションをキーワード指定してこのメソッドを実行することで、描画済みの図形の各種設定を変更することが可能です。

itemconfig
# canvasはtkinter.Canvas()の戻り値
canvas.itemconfig(tagOrId, option, ...)

図形描画時に実行するメソッド(例えば create_polygoncreate_oval など)では、引数にオプションを指定することで描画する図形の設定(線の太さや塗りつぶし色など)を行うことができます。

上記の itemconfig は、すでに描画した図形の設定を後から変更するためのメソッドになります。 

itemconfig メソッドを実行するのは、設定を変更したい図形を描画しているキャンバスのオブジェクトです。要は、tkinter.Canvas() の戻り値です。

itemconfig メソッドの第1引数に指定するのは図形の ID or タグ名になります。例えば描画した多角形の ID を指定する場合は、create_polygon の戻り値を指定することになります。

さらに、以降の引数で変更したいオプションをキーワード指定します。図形の塗りつぶし色を変更するためには、itemconfig メソッドに fill オプションを指定して実行します。

例えば下記のように itemconfig メソッドを実行すれば、canvas に描画された図形 seg の塗りつぶし色を "orange" に変更することが可能です。

itemconfigの使用例
canvas.itemconfig(seg, fill="orange")

ですので、点灯させる位置のセグの色と消灯させる位置のセグの色を別のものにし、上記のように itemconfig を実行して塗りつぶし色を変更することで、各セグの点灯・消灯を表現することが可能です。

itemconfigでセグの点灯消灯を切り替える様子

ただし、itemconfig を実行するためには、セグの描画先のキャンバスのオブジェクトと、さらにそのキャンバスに描画したセグの ID or タグ名が必要になります。

ですので、キャンバス作成時やセグの描画時にこれらの情報を覚えておくようにしましょう(複数の情報を覚えておく必要があるので、リストなどで覚えておくと良いと思います)。

今回紹介した itemconfig のように、描画済みの図形を操作するようなメソッドは他にもたくさんあります。下記ページで各メソッドの紹介をしていますので、興味のある方はぜひ読んでみてください。

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

数字に応じた点灯と消灯の切り替え

7セグで数字を表示するためには、数字に応じて各位置のセグの点灯 or 消灯を上手く切り替えてやる必要があります。

これを行うためには、09 の各数字をセグ表示する際に、各セグを点灯 or 消灯のどちらに切り替えるかのリストを用意しておくのが良いと思います。

ここでまず、以降の説明しやすくするために、7セグの各セグに下図のようにセグ 0 〜 セグ 6 の名前を割り振りたいと思います。

各セグへの名前の割り振りを示した図

この時、例えば数字の 3 を7セグで表示する場合、下記のように各セグを点灯・消灯させることになります。

  • セグ 0:点灯
  • セグ 1:消灯
  • セグ 2:点灯
  • セグ 3:点灯
  • セグ 4:消灯
  • セグ 5:点灯
  • セグ 6:点灯

上記の点灯・消灯に基づいて各セグの色を切り替えた場合、下図のようになります(点灯はオレンジ、消灯は灰色で色を切り替えています)。

各セグを点灯・消灯した様子

数字の 3 のように見えますね!

つまり、上記のような点灯 or 消灯の情報をリストとして保持しておけば、後はそのリストを参照しながら itemconfig メソッドで図形に塗る色を切り替えることで、数字に合わせたセグの点灯・消灯を行うことができます。

例えば下記は、数字 3 をセグ表示する時に参照するリストの一例になります。

3をセグ表示する際の各セグの点灯or消灯
# 上, 上左, 上右, 中, 下左, 下右, 下
on_off_info = [True, False, True, True, False, True, True]

各要素は先頭から順にそれぞれセグ 0 からセグ 6 に対応しており、セグを点灯する場合は True を、消灯する場合は False を示すようになっています。

例えばリスト segs にセグ 0 からセグ 6 を表す六角形の ID or タグ名が格納されているとすれば、数字の 3 は、先ほど示した on_off_info を用いて下記のような for ループでセグ表示することができます(canvas は塗りつぶし色を変更したいセグが描画されているキャンバスオブジェクトです)。

3のセグ表示
for seg, is_on in zip(segs, on_off_info):
				
	if is_on:
		color = "orange"
	else:
		color = "gray"

	# セグを表現する多角形の色を変更		
	canvas.itemconfig(seg, fill=color)

また、上記のリストは各数字に対して用意しておく必要があるので、デジタル時計を起動する前に、下記のような数字 09 に対して各セグを点灯するか消灯するかの情報をまとめた二次元リストを用意しておくのが良いと思います。

各数字に対する点灯or消灯の情報
ON_OFF_INFOS = [
	# 上, 上左, 上右, 中, 下左, 下右, 下
	[True, True, True, False, True, True, True], # 0
	[False, False, True, False, False, True, False], # 1
	[True, False, True, True, True, False, True], # 2
	[True, False, True, True, False, True, True], # 3
	[False, True, True, True, False, True, False], # 4
	[True, True, False, True, False, True, True], # 5
	[True, True, False, True, True, True, True], # 6
	[True, True, True, False, False, True, False], # 7
	[True, True, True, True, True, True, True], # 8
	[True, True, True, True, False, True, True] # 9

キャンバスを作成するで説明したように、キャンバスには各数字を1桁ずつ独立してセグ表示するようにしているため、用意する情報は09 の1桁の数字に対するものだけで良いです。

あとは上記の二次元リストからセグ表示したい数字に対応したリストを取得し(数字 num をセグ表示するのであれば ON_OFF_INFOS[num] で取得)、さらに、そのリストの True or False に応じて塗りつぶす色を切り替えれば、各数字に対応したセグ表示を行うことができます。

このスクリプト例は下記のようになります(canvas は塗りつぶし色を変更したいセグが描画されているキャンバスオブジェクトです)。

numのセグ表示
for seg, is_on in zip(segs, ON_OFF_INFOS[num]):
				
	if is_on:
		color = "orange"
	else:
		color = "gray"

	# セグを表現する多角形の色を変更		
	canvas.itemconfig(seg, fill=color)

スポンサーリンク

セグの座標を計算する

以上の説明に基づいてプログラミングすることで、セグさえ描画すれば数字をセグ表示できるようになると思います。

ただ、まだセグ描画時(create_polygon 実行時)に必要になる座標の計算の仕方を説明していなかったので、ここでその仕方を説明したいと思います。

割とゴリ押しで座標を求めていますので、もっと綺麗に座標を決める方法をご存知の方がおられましたら、Twitter で教えていただけると幸いです。

座標の決め方の方針

今回は、基準となるセグ(以降は基準セグと呼びます)の6つの頂点の座標を決め、その基準セグの回転および移動により7つのセグを描画することを考えたいと思います。

基準セグの移動と回転で各セグを描画する様子

これは、各セグの頂点の座標を個別に決めるのが面倒だからです。

上記の方針でセグの頂点の座標を決めるのであれば、基準セグの頂点の座標さえ決まれば、各セグの頂点の座標は、その基準セグの座標の回転(90度回転)と足し算(移動)を行うだけで求めることができます(多分こっちの方が楽だと思います)。

また、セグ 3 を除く各セグは、キャンバスに接するように配置することを前提にして座標を決めていきたいと思います。さらに、各セグ同士も接するように配置することを前提にして座標を決めていきます。つまり、ここで決めた座標に基づいて各セグを描画すれば下の図のように配置されることになります。

セグとキャンバスおよびセグ同士が接するように配置される様子

さらにもう一つ、セグの鋭角(下の図の矢印で示す位置の角)の角度は90度であることを前提としたいと思います。

セグの鋭角が90度である前提であることを示す図

これらの前提があると、基準セグの頂点の座標や、各セグを描画するときの基準セグからの移動量を比較的簡単に求めることができるようになります。

基準セグの頂点の座標

では、まずは基準セグの頂点の座標を求めていきたいと思います。

基準セグの定義

この基準セグは、セグ 3 を “セグの中心がキャンバスの原点 (0, 0) になるように移動したセグ” であるとして考えていきたいと思います(キャンバスでは y 方向の正方向が下であることと、原点は左上であることに点に注意してください)。

基準セグとセグ3の関係を示す図

わざわざセグの中心が原点 (0, 0) に移動させる理由は、セグの中心を (0, 0) にしておくことで回転後のセグの中心も (0, 0) になる&移動する量が計算しやすいからです。

セグ 3 はキャンバスの中心に存在しますので、セグ 3 の中心座標は (キャンバスの幅 / 2, キャンバスの高さ / 2) となります。

また、基準セグは “セグ 3 をセグの中心がキャンバスの原点 (0, 0) になるように移動したセグ” ですので、まずセグ 3 の各頂点の座標を求め、各座標からキャンバスの中心座標を引いてやる、つまり各座標の x 座標から キャンバスの幅 / 2 を、y 座標から キャンバスの高さ / 2 をそれぞれ引くことで、基準セグの座標を求めることが可能です。

セグ3の各頂点の座標から基準セグの座標を求める方法を示す図

ですので、基準セグの各頂点の座標を求めるために、まずはセグ 3 の各頂点の座標を求めていきたいと思います。

セグ 3 の各頂点の座標

ここからは、キャンバスの幅を canvas_width、キャンバスの高さを canvas_height、セグの太さを seg_width として座標を考えていきたいと思います。

canvas_widthとcanvas_heightとseg_widthの説明図

まずはセグ 3 の左中の頂点の座標について考えてみましょう。

セグ3の左中の頂点を示す図

ひとまず簡単な y 座標から考えてみましょう。前述のとおり、セグ 3 はちょうどキャンバスの中心に存在するセグになります。なので、セグ 3 の中心座標は (canvas_width / 2, canvas_height / 2) ということになります。

さらに、今注目している左中の頂点の y 座標は、セグ 3 の中心座標の y 座標と同じ位置にありますので、セグ 3 の左中の頂点の y 座標は canvas_height / 2 ということになります。

次は x 座標について考えてみましょう!

現在注目している左中の頂点は、キャンバスの左端から “セグの太さの半分” の長さ分右側に離れていることが確認できると思います。キャンバスの左端の x 座標は 0 であり、さらにセグの太さは seg_width であることを考慮すれば、この頂点の x 座標は seg_width / 2 と考えることができますね!

x座標を求めるために必要な情報を追記した図

つまり、セグ 3 の左中の頂点の座標は (seg_width / 2, canvas_height / 2) です。

続いて左上の頂点について考えてみます。

セグ3の左上の頂点を示す図

y 座標は左中の頂点から seg_width / 2 分だけ上方向にあるので、canvas_height / 2 - seg_width / 2 と求めることができます。x 座標はどうでしょう?

座標の決め方の方針で説明したように、セグの鋭角の角度は90度ですので、下の図の青色部分の三角形は二等辺三角形であることが分かります。

鋭角部分の三角形が二等辺三角形であることを示す図

この青色の三角形の縦線の長さは、セグの太さの半分なので seg_width / 2 ということになります。さらに、二等辺三角形ですので、横線の長さも seg_width / 2 ということになります。

したがって、左上の頂点の x 座標は、左中の頂点から seg_width / 2 分だけ右側にあるので、seg_width と求めることができます。

つまり、セグ 3 の左上の頂点の座標は (seg_width, canvas_height / 2 - seg_width / 2) です。

左下も同様に、左中からどの方向にどれだけ離れているかを考えることで求めることが可能ですので、求め方の解説は省略します。

続いて右中の頂点の座標について考えてみましょう。

セグ3の右中の頂点を示す図

これもまぁ左中と同様の考え方で求められそうですね!この頂点はキャンバスの右端から seg_width / 2 分だけ左側に離れているので、x 座標は canvas_width - seg_width / 2 と求めることができます。さらに左中の頂点と同様に y 座標は canvas_height / 2 になります。

右下や右上の頂点に関しても、右中の頂点からどの方向にどれだけ離れているかを考えると求めることができると思います。

上記の考え方で求めたセグ 3 の各頂点の座標は下記のようになります。

  • 左上:
    • xseg_width
    • ycanvas_height / 2 - seg_width / 2
  • 左中:
    • xseg_width - 2
    • ycanvas_height / 2
  • 左下:
    • xseg_width
    • ycanvas_height / 2 + seg_width / 2
  • 右下:
    • xcanvas_width - seg_width
    • ycanvas_height / 2 + seg_width / 2
  • 右中:
    • xcanvas_width - seg_width / 2
    • ycanvas_height / 2
  • 右上:
    • xcanvas_width - seg_width
    • ycanvas_height / 2 - seg_width / 2

基準セグの各頂点の座標

セグ 3 の各頂点の座標が求められましたので、次はこの座標から基準セグの座標を求めていきたいと思います。

前述の通り、基準セグの各頂点の x 座標は、セグ 3 の各頂点の x 座標から canvas_width / 2 を引くことで、基準セグの各頂点の y 座標は、セグ 3 の各頂点の y 座標から canvas_height / 2 を引くことでそれぞれ求めることができます。

上記に基づいて求めた基準セグの各頂点の座標は下記のようになります。

  • 左上:
    • x- canvas_width / 2 + seg_width
    • y- seg_width / 2
  • 左中:
    • x- canvas_width / 2 + seg_width - 2
    • y0
  • 左下:
    • x- canvas_width / 2 + seg_width
    • yseg_width / 2
  • 右下:
    • xcanvas_width / 2 - seg_width
    • yseg_width / 2
  • 右中:
    • xcanvas_width / 2 - seg_width / 2
    • y0
  • 右上:
    • xcanvas_width / 2 - seg_width
    • y- seg_width / 2

上記のように求めた各頂点の座標をリスト形式にすると下記のようになります(x 座標と y 座標をそれぞれ別のリストとしています)。

基準セグの頂点の座標
# 基準セグの各頂点のx座標
XS = [
	- canvas_width / 2 + seg_width, # 左上
	- canvas_width / 2 + seg_width / 2, # 左中
	- canvas_width / 2 + seg_width, # 左下
	canvas_width / 2 - seg_width, # 右下
	canvas_width / 2 - seg_width / 2, # 右中
	canvas_width / 2 - seg_width # 右上
]

# 基準セグの各頂点のy座標
YS = [
	- seg_width / 2, # 左上
	0, # 左中
	seg_width / 2, # 左下
	seg_width / 2, # 右下
	0, # 右中
	- seg_width / 2 # 右上
]

この座標のリストを利用すれば、例えば下記のように create_polygon を実行することでセグを描画することができます。

基準セグの描画
canvas.create_polygon(
	XS[0], YS[0],
	XS[1], YS[1],
	XS[2], YS[2],
	XS[3], YS[3],
	XS[4], YS[4],
	XS[5], YS[5],
	fill="gray",
	width=0,
)

ただ、上記で描画できるセグは基準セグ(横長のセグ&中心がキャンバスの座標 (0, 0))なので、7セグを描画する際には、事前にこの基準セグを回転(必要な場合のみ)&移動させてから描画することになります。

スポンサーリンク

基準セグの回転

で、基準セグが横長のセグなので、縦長のセグを描画する際には回転を事前に行う必要があります。

具体的には下記のセグを描画する際には、事前に基準セグを回転させておき、それから適切な位置に移動させて描画する必要があります。

  • セグ 1
  • セグ 2
  • セグ 4
  • セグ 5

図形の回転のプログラミングと考えると具体的なイメージが湧かないかも知れませんが、要はセグの各頂点の座標を、回転後の頂点の座標に移動させると考えると良いです。

各頂点を回転後の座標に移動することでセグを回転することができることを示す図

さらに、回転後の座標を (rx, ry) とすれば、この rxry は回転前の座標 (x, y) より、下記の式で求めることができます(回転角度は θ としています)。行列に詳しい方であれば、回転行列と回転前の座標の積であると考えた方がわかりやすいかもしれません。

回転後の座標
rx = x * cosθ - y * sinθ
ry = x * sinθ + y * cosθ

今回、回転を行う理由は、横長である基準セグを縦長のセグにするためなので、回転角度は90度になります(270度でも良い)。

上式における θ = 90度 とすれば、cosθ = 0sinθ = 1 なので、結局回転後の座標 (rx, ry) は下記により求めることができることになります。かなり簡単になりましたね!

90度回転後の座標
rx = - y
ry = x

この回転後の基準セグの各頂点の座標を求める処理は、基準セグの頂点の座標で示した XSYS を用いれば、例えば内包表記を用いて下記により求めることができます(NumPy など用いて求めても良いと思います)。

各座標の回転後座標を求める処理
r_xs = [-n for n in YS]
r_ys = [n for n in XS]

上記により、基準セグが横長から縦長に変化することになります。

基準セグの移動

回転を行えば基準セグと描画するセグとの方向を合わせることができますので、あとは7セグを表現できるように位置を移動させてから描画を行えば良いです。

基準セグの移動と回転で各セグを描画する様子

基準セグの中心座標は (0, 0) としていますので(回転しても中心座標は変わらない)、基準セグの各頂点の x 座標と y 座標それぞれに、描画先となるセグの中心の x 座標と y 座標の値を足すことで、その描画先に移動させた時の基準セグの座標を求めることができます。

基準セグの各頂点にセグの中心座標を足すことで描画位置に移動する様子

ただ、中心座標は描画するセグごとに異なりますので、下図の各セグ(セグ 0 〜 セグ 6)の中心座標がいくつになるのかについて、個別に考える必要があります。

各セグへの名前の割り振りを示した図

で、この中心座標を考えるのに、基準セグの頂点の座標で用いた canvas_width と seg_widthcanvas_height に追加して、セグの長さの値が利用できると便利なので、それを seg_length として考えていきたいと思います。

各パラメータの意味合いを示す図

さらに、各セグを表す図に補助線を入れ、各線の間の距離を、canvas_widthseg_widthseg_lengthcanvas_height を用いて表現すれば下の図のようになります。

各セグの中心座標を求める際に必要になるパラメータを追記した図

この上の図から、各セグの中心座標を読み取ることができると思います。

例えばセグ 2 の中心座標について考えると、x 座標に関しては原点から canvas_width - seg_width / 2 離れた位置に、y 座標に関しては原点から canvas_height / 2 - seg_length / 2 離れた位置に存在するため、セグ 2 の中心座標は (canvas_width - seg_width / 2, canvas_height / 2 - seg_length / 2) であると言えます。

こんな感じで考えていくと、各セグの中心座標はそれぞれ下記のようになります。

  • セグ 0:(canvas_width / 2, seg_width / 2)
  • セグ 1:(seg_width / 2, canvas_height / 2 - seg_length / 2)
  • セグ 2:(canvas_width - seg_width / 2, canvas_height / 2 - seg_length / 2)
  • セグ 3:(canvas_width / 2, canvas_height / 2)
  • セグ 4:(seg_width / 2, canvas_height / 2 + seg_length / 2)
  • セグ 5:(canvas_width - seg_width / 2, canvas_height / 2 + seg_length / 2)
  • セグ 6:(canvas_width / 2, canvas_height - seg_width / 2)

各セグを描画する際には、上記の情報を参照しながら基準セグのそれぞれの頂点を移動してからセグを描画することになります。

そのため、これらの情報は描画時に参照しやすいよう、下記のようにリスト形式にしておくと良いと思います(各セグを回転するかどうかの情報をリストの先頭に保持するようにしています)。

セグの描画時のパラメータリスト
SEG_DRAW_PARAMS = [
	# [回転するか?, 横方向の移動量, 縦方向の移動量]
	[False, canvas_width / 2, seg_width / 2],
	[True, seg_width / 2, canvas_height / 2 - seg_length / 2],
	[True, canvas_width - seg_width / 2, canvas_height / 2 - seg_length / 2],
	[False, canvas_width / 2, canvas_height / 2],
	[True, seg_width / 2, canvas_height / 2 + seg_length / 2],
	[True, canvas_width - seg_width / 2, canvas_height / 2 + seg_length / 2],
	[False, canvas_width / 2, canvas_height - seg_width / 2]
]

このリストを利用すれば、キャンバスへの各セグの描画は下記の処理により実現することができます。

セグの描画
# 描画時のパラメータに従ってセグを描画
for draw_param in SEG_DRAW_PARAMS:

	is_rotate, x_shift, y_shift = draw_param

	if is_rotate:
		# 回転必要な場合は、基準セグの頂点の座標をを90度を回転
		r_xs = [-n for n in YS]
		r_ys = [n for n in XS]
		
	else:
		# 回転不要な場合は基準セグの頂点の座標をそのまま使用
		r_xs = XS
		r_ys = YS
		
	# 基準セグの各頂点を移動
	t_xs = [n + x_shift for n in r_xs]
	t_ys = [n + y_shift for n in r_ys]

	# 移動後の座標に六角形を描画
	seg =  canvas.create_polygon(
		t_xs[0], t_ys[0],
		t_xs[1], t_ys[1],
		t_xs[2], t_ys[2],
		t_xs[3], t_ys[3],
		t_xs[4], t_ys[4],
		t_xs[5], t_ys[5],
		fill="gray",
		width=0
	)

キャンバスの高さとセグの長さ

ここまでキャンバスの高さとセグの長さをそれぞれ単に canvas_heightseg_length と表してきましたが、これらは座標の決め方の方針に基づけば、キャンバスの幅 canvas_width とセグの太さ seg_width から一意に求めることができます。

seg_lengthcanvas_height を求めるために、下の図のように補助線を入れて各パラメータの関係性を整理したいと思います。

seg_lengthとcanvas_heightを求める際に必要になるパラメータを追記した図

まずは seg_length について考えてみましょう。オレンジ色で示した seg_length に注目すると、seg_lengthcanvas_width から seg_width / 2 * 2 を引いた値であると考えられます。

つまり、seg_lengthcanvas_width と seg_width によって下記のように求めることができます。

seg_lengthを求める
seg_length = canvas_width - seg_width

次は canvas_height について考えてみましょう。オレンジ色で示した canvas_height に注目すると、canvas_height は seg_length * 2 と seg_width / 2 * 2 を足した値であると考えられます。

さらに、seg_length = canvas_width - seg_width ですので、これを代入して考えれば canvas_height は下記の式で求めることができることになります。

canvas_heightを求める
canvas_height = canvas_width * 2 - seg_width

上記により、canvas_heightseg_lengthcanvas_widthseg_width で求めることができることが確認できたと思います。

逆にいうと、これらの関係性を満たさないような canvas_heightseg_length を用いてしまうと、今回説明したようにはセグを描画することができなくなってしまいますので注意してください(セグの位置がずれたりする)。

これは、キャンバス作成時に指定するキャンバスの高さ(height)は自由に設定することができないことを意味するので、キャンバス作成時には特に注意するようにしてください。

スポンサーリンク

デジタル時計のサンプルスクリプト

最後に、デジタル時計のサンプルスクリプトを紹介していきます。

スクリプト

ここまで解説してきた内容に基づいて作成したデジタル時計のサンプルスクリプトは下記のようになります。

デジタル時計
# -*- coding: utf-8 -*-
import tkinter
from datetime import datetime, timedelta, timezone

# セグの太さ
SEG_WIDTH = 20

# 数字表示用のキャンバスのサイズの設定
CANVAS_WIDTH_NUMBER = 100

# コロン表示用のキャンバスのサイズ
CANVAS_WIDTH_COLON = 50

# 色の設定
COLOR_BG = "black"  # 時計の背景色
COLOR_SEG_ON = "orange"  # セグ点灯時の色
COLOR_SEG_OFF = "gray20"  # セグ消灯時の色


class ColonCanvas(tkinter.Canvas):
	'''コロンを表現する図形を描画するキャンバス'''

	canvas_width = CANVAS_WIDTH_COLON
	canvas_height = CANVAS_WIDTH_NUMBER * 2 - SEG_WIDTH

	def __init__(self, master, **kw):
		super().__init__(master, kw)

	def draw(self):
		'''コロンを長方形として描画する'''

		for i in range(2):
			# 横方向の中心はキャンバスの中心
			center_x = ColonCanvas.canvas_width / 2

			# 縦方向の中心はキャンバスの1/3 or 2/3の位置
			center_y = (i + 1) * ColonCanvas.canvas_height / 3

			# 長方形の確変の長さが20となるように座標を設定
			x1 = center_x - 10
			y1 = center_y - 10
			x2 = center_x + 10
			y2 = center_y + 10

			# 長方形を描画
			self.create_rectangle(
				x1, y1, x2, y2,
				fill=COLOR_SEG_ON,
				width=0
			)


class NumberCanvas(tkinter.Canvas):
	'''数字を表現するセグを描画するキャンバス'''

	# 各種パラメータの設定
	canvas_width = CANVAS_WIDTH_NUMBER
	seg_width = SEG_WIDTH
	canvas_height = canvas_width * 2 - seg_width
	seg_length = canvas_width - seg_width

	# 数字をセグ表示する際の、各セグの点灯or消灯の情報のリスト
	ON_OFF_INFOS = [
		# 上, 上左, 上右, 中, 下左, 下右, 下
		[True, True, True, False, True, True, True],  # 0
		[False, False, True, False, False, True, False],  # 1
		[True, False, True, True, True, False, True],  # 2
		[True, False, True, True, False, True, True],  # 3
		[False, True, True, True, False, True, False],  # 4
		[True, True, False, True, False, True, True],  # 5
		[True, True, False, True, True, True, True],  # 6
		[True, True, True, False, False, True, False],  # 7
		[True, True, True, True, True, True, True],  # 8
		[True, True, True, True, False, True, True]  # 9
	]

	# 各セグを描画するときの情報のリスト
	SEG_DRAW_PARAMS = [
		# [回転するか?, 横方向の移動量, 縦方向の移動量]
		[False, canvas_width / 2, seg_width / 2],
		[True, seg_width / 2, canvas_height / 2 - seg_length / 2],
		[True, canvas_width - seg_width / 2,
		 canvas_height / 2 - seg_length / 2],
		[False, canvas_width / 2, canvas_height / 2],
		[True, seg_width / 2, canvas_height / 2 + seg_length / 2],
		[True, canvas_width - seg_width / 2,
		 canvas_height / 2 + seg_length / 2],
		[False, canvas_width / 2, canvas_height - seg_width / 2]
	]

	# 基準セグの各頂点のx座標
	XS = [
		- canvas_width / 2 + seg_width,  # 左上
		- canvas_width / 2 + seg_width / 2,  # 左中
		- canvas_width / 2 + seg_width,  # 左下
		canvas_width / 2 - seg_width,  # 右下
		canvas_width / 2 - seg_width / 2,  # 右中
		canvas_width / 2 - seg_width  # 右上
	]

	# 基準セグの各頂点のy座標
	YS = [
		- seg_width / 2,  # 左上
		0,  # 左中
		seg_width / 2,  # 左下
		seg_width / 2,  # 右下
		0,  # 右中
		- seg_width / 2  # 右上
	]

	def __init__(self, master, **kw):
		super().__init__(master, kw)

		# 描画した六角形のIDを管理するリスト
		self.segs = []

	def draw(self):
		'''セグを描画する'''

		# 描画時のパラメータに従ってセグを描画
		for draw_param in NumberCanvas.SEG_DRAW_PARAMS:

			is_rotate, x_shift, y_shift = draw_param

			if is_rotate:
				# 回転必要な場合は、基準セグの頂点の座標をを90度を回転
				r_xs = [-n for n in NumberCanvas.YS]
				r_ys = [n for n in NumberCanvas.XS]

			else:
				# 回転不要な場合は基準セグの頂点の座標をそのまま使用
				r_xs = NumberCanvas.XS
				r_ys = NumberCanvas.YS

			# 基準セグの各頂点を移動
			t_xs = [n + x_shift for n in r_xs]
			t_ys = [n + y_shift for n in r_ys]

			# 移動後の座標に六角形を描画
			seg = self.create_polygon(
				t_xs[0], t_ys[0],
				t_xs[1], t_ys[1],
				t_xs[2], t_ys[2],
				t_xs[3], t_ys[3],
				t_xs[4], t_ys[4],
				t_xs[5], t_ys[5],
				fill=COLOR_SEG_OFF,
				width=0,
			)

			# 描画した六角形のIDをリストに格納
			self.segs.append(seg)

	def update(self, num):
		'''数字numをセグ表示する'''

		for seg, is_on in zip(self.segs, NumberCanvas.ON_OFF_INFOS[num]):

			if is_on:
				# 点灯する場合のセグの色
				color = COLOR_SEG_ON
			else:
				# 消灯する場合のセグの色
				color = COLOR_SEG_OFF

			# セグを表現する多角形の色を変更
			self.itemconfig(seg, fill=color)


class Timer:
	'''時刻を取得するクラス'''

	def __init__(self):

		# タイムゾーンの設定
		self.JST = timezone(timedelta(hours=9))

	def time(self):

		# 時刻の取得
		now = datetime.now(tz=self.JST)

		# 時・分・秒にわけて返却
		return now.hour, now.minute, now.second


class Drawer:
	'''時計を描画するクラス'''

	CANVAS_NUMBER = 1
	CANVAS_COLON = 2

	def __init__(self, master):

		# 各種設定を行なった後に時計の盤面を描画
		self.initSetting(master)
		self.createClock()

	def initSetting(self, master):
		'''時計描画に必要な設定を行う'''

		# ウィジェットの作成先を設定
		self.master = master

		# 作成するキャンバスの種類
		self.canvas_types = [
			Drawer.CANVAS_NUMBER,
			Drawer.CANVAS_NUMBER,
			Drawer.CANVAS_COLON,
			Drawer.CANVAS_NUMBER,
			Drawer.CANVAS_NUMBER,
			Drawer.CANVAS_COLON,
			Drawer.CANVAS_NUMBER,
			Drawer.CANVAS_NUMBER
		]

		self.number_canvases = []
		self.colon_canvases = []

	def createClock(self):
		'''時計の盤面を作成する'''

		# 数字のセグ表示用のキャンバスとコロン表示用のキャンバスを作成
		for canvas_type in self.canvas_types:

			if canvas_type == Drawer.CANVAS_NUMBER:

				# 数字のセグ表示用のキャンバスを作成
				canvas = NumberCanvas(
					self.master,
					width=NumberCanvas.canvas_width,
					height=NumberCanvas.canvas_height,
					bg=COLOR_BG,
					highlightthickness=0,
				)
				self.number_canvases.append(canvas)

			else:
				# コロン表示用のキャンバスを作成
				canvas = ColonCanvas(
					self.master,
					width=ColonCanvas.canvas_width,
					height=ColonCanvas.canvas_height,
					bg=COLOR_BG,
					highlightthickness=0,
				)
				self.colon_canvases.append(canvas)

			# 左から順番にpackで詰めていく
			canvas.pack(side=tkinter.LEFT, padx=10, pady=10)

	def draw(self):
		'''各キャンバスに描画する'''

		for canvas in self.colon_canvases:
			# コロンを描画するよう依頼
			canvas.draw()

		for canvas in self.number_canvases:
			# セグを描画するよう依頼
			canvas.draw()

	def update(self, hour, minute, second):
		'''セグ表示を更新する'''

		# 時刻を1桁ずつに分割する
		nums = []
		nums.append(hour // 10)
		nums.append(hour % 10)
		nums.append(minute // 10)
		nums.append(minute % 10)
		nums.append(second // 10)
		nums.append(second % 10)

		for canvas, num in zip(self.number_canvases, nums):
			# 各キャンバスに対応する数字numをセグ表示するように依頼
			canvas.update(num)


class DigitalClock:
	'''デジタル時計を実現するクラス'''

	def __init__(self, master):

		# after実行用にウィジェットのインスタンスを保持
		self.master = master

		# 各種クラスのオブジェクトを生成
		self.timer = Timer()
		self.drawer = Drawer(master)

		# コロンとセグを描画する
		self.draw()

		# 時刻をセグ表示する
		self.update()

	def draw(self):
		'''コロンとセグを描画する'''

		self.drawer.draw()

	def update(self):
		'''時刻をセグ表示を更新する'''

		# 時刻を取得し、その時刻に合わせて針を進める
		hour, minute, second = self.timer.time()
		self.drawer.update(hour, minute, second)

		# 1秒後に再度セグ表示を行う
		self.master.after(1000, self.update)


if __name__ == "__main__":
	app = tkinter.Tk()

	# 背景色を設定
	app.config(bg=COLOR_BG)

	DigitalClock(app)
	app.mainloop()

スクリプトの実行

スクリプトを実行すると、下の図のような画面のアプリが起動します。時刻がセグ表示されており、1秒ごとに時刻が更新されることが確認できると思います。

アプリの起動画面を示す図

スポンサーリンク

スクリプトの設定

下記部分でスクリプトを設定することが可能です。

スクリプトの設定
# セグの太さ
SEG_WIDTH = 20  # 自由に設定可能

# 数字表示用のキャンバスのサイズの設定
CANVAS_WIDTH_NUMBER = 100  # 自由に設定可能

# コロン表示用のキャンバスのサイズ
CANVAS_WIDTH_COLON = 50  # 自由に設定可能

# 色の設定
COLOR_BG = "white"  # 時計の背景色
COLOR_SEG_ON = "blue"  # セグ点灯時の色
COLOR_SEG_OFF = "gray90"  # セグ消灯時の色

上記の設定を変更することにより、例えばアプリの見た目を下の図のように変更することも可能です。

アプリの設定変更後のアプリの見た目を示す図

スクリプトの解説

最後にスクリプトの解説を行なっておきます。

このスクリプトでは下記の5つのクラスを用意していますので、これらの各クラスについて簡単に解説をしていきたいと思います。

  • ColonCanvas
  • NumberCanvas
  • Timer
  • Drawer
  • DigitalClock

ColonCanvas

ColonCanvastkinter.Canvas のサブクラスで、コロンの描画専用のキャンバスとしています。

draw メソッドを持っており、この draw メソッドが実行された際にキャンバスへのコロンの描画を行います。

コロンは単に長方形(正方形)で表現しており、親クラスの持つ create_rectangle メソッドにより描画しています。

また、コロンは1つのキャンバスに2つ描画しており、x 軸方向に関してはキャンバスの真ん中に、y 軸方向に関してはキャンバスの高さの 1 / 3 の位置とキャンバスの高さの 2 / 3 の位置に描画するように描画先の座標を設定しています。

NumberCanvas

NumerCanvastkinter.Canvas のサブクラスで、セグの描画&セグの点灯専用のキャンバスとしています。このクラスのインスタンス1つが、時刻の1桁分のセグの描画とセグの点灯を行います。

時刻に合わせてセグを点灯させるセグの座標を計算するで紹介した下記のリストについてはクラスで共通ですので、クラス変数として保持するようにしています。

  • ON_OFF_INFOS:数字をセグ表示する際に各セグの情報(点灯する or 点灯しない)を格納したリスト 
  • XS および XY:基準セグの各頂点の座標を格納したリスト
  • SEG_DRAW_PARAMS:セグの描画情報(回転する or 回転しない・各セグの中心座標)を格納したリスト

NumerCanvasdraw メソッドを持っており、この draw メソッドが実行された際にキャンバスへのセグの描画を行います。

このセグの描画は、キャンバス上にセグを描画するセグの座標を計算するで解説した内容に基づいて処理を行なっています。といっても、セグの座標を計算するで示した XS および XYSEG_DRAW_PARAMS から情報を取得しながらセグの描画を行っているだけです。

また、セグの描画には、描画したセグの ID を次に説明する update メソッドで使用できるように、リスト self.segs に格納するようにしています。

さらに、NumerCanvasupdate メソッドを持っており、この update メソッドが実行された際に、引数 num で指定される数字に合わせて各セグの点灯と消灯を行います。

この各セグの点灯と消灯は、時刻に合わせてセグを点灯させるで解説した内容に基づいて処理を行なっています。これもリストの ON_OFF_INFOS から各セグを点灯するかどうかの情報を取得し、その情報に基づいてセグの塗りつぶし色を itemconfig メソッドで変更しているだけです。また、itemconfig メソッドの第1引数には、draw メソッドによって self.segs に格納された図形の ID を指定しています。 

Timer

Timer は時刻を取得することを役割としたクラスになります。詳しくは下記ページで解説していますので、不明点がありましたら下記ページを参考にしていただければと思います。

アナログ時計の作り方の解説ページアイキャッチ【Python/tkinter】アナログ時計の作り方

Drawer

Drawer はキャンバスへの操作を役割とするクラスです。

キャンバスの作成・配置は createClock メソッドで行っています。

キャンバスを作成するで説明したように、時刻の各数字をセグ表示する6つのキャンバスとコロンを表示する2つのキャンバスを作成しています。つまり、NumerCanvas のインスタンス6つと ColonCanvas のインスタンス2つを作成しています。

このインスタンス作成時に、何回目のキャンバス作成時にどちらのクラスのインスタンスを作成するかが分かるよう、事前に self.canvas_types にその情報を格納するようにしています。

また、座標の決め方の方針で説明したように各セグはキャンバスに接するように配置するようにしています。

ですので、キャンバス同士をくっつけて配置してしまうと数字同士もくっついて見栄えが悪いので、離れて配置されるように pack メソッド際に padxpady を指定するようにしています。

Drawer クラスは draw メソッドを持っており、このメソッドでキャンバスへの描画を行うようにしています。具体的には、各キャンバスのクラスのインスタンスに対して draw メソッドを実行し、セグの描画やコロンの描画を行なっています。

さらに、Drawer クラスは update メソッドを持っており、このメソッドで時刻に合わせたセグ表示の更新を行うようにしています。具体的には、NumerCanvas のインスタンスに対して update メソッドを実行し、セグ表示の更新を行なっています。

ただ、各インスタンスが担当するのは1つの数字のセグの描画・更新のみですので、事前に時刻を1桁ずつに分割し、1桁ずつセグ表示を更新するように update メソッドを実行するようにしています。

DigitalClock

DigitalClock クラスは、Timer クラスと Drawer クラスのインスタンスの作成および各インスタンスのメソッドの実行を行います。

基本的にはインスタンスを作成したのちにキャンバスにコロンとセグの描画を行い、そのあとは1秒間隔でセグ表示の更新を行なっているだけです。

まとめ

このページでは、Python で tkinter を利用した「デジタル時計」の作り方の解説を行いました!

結構解説が長くなってしまいましたが、一番伝えたかったことは、”itemconfig で描画済みの図形の設定を変更できる” というところです。この itemconfig を利用することで、キャンバス上の図形に動きを持たせるようなことができますので、いろんな場面で活用できると思います。

セグの各頂点の座標の解説に関しては、あまり他の場面で応用できる気はしないですが、セグを描画するような記事が他にあまり見つからなかったことと、こういったちょっと複雑な図形の座標を求める際の参考にはなるかもしれないなぁと思って詳細に解説させていただきました。

ただ結構無理やり座標を求めているので、もっと綺麗に座標は求められるかもしれないですね…。ですが、アプリの見た目としては意図したものになっており、個人的には結構満足してます!

tkinter はこういった身近なものをアプリ化するのにも向いていると思いますので、是非皆さんも身近にあるもののアプリ化に挑戦してみてください!