◯ Python/Tkinterで作成、見やすい時報付きのデジタル時計。
Windowsのタスクバーの右端やMacのメニューバーの右端には、現在の日時が表示されています。でも、それが読み取れなくて、目をディスプレイに近付けたりした経験はないでしょうか?
筆者は老眼で小さい文字が読みにくいので、もうちょっと大きくならんかなー、と思っていました。そこで作ったのが図1の「DigiClock」です。
文字の大きさを設定するのが面倒だったので、ウインドウを大きくすると、それに合わせて文字が大きくなるようにしました。もちろん、ソフトを終了したときには、ウインドウの位置と大きさの設定を自動で保存します。次にソフトを起動したときに、同じ大きさ・同じ位置で現れるようにするためです。
図2は、メニューの表示です。メニューにある「表示」→「フォント」を選ぶと、図3のダイアログが出て、フォントファミリー(フォント名)を設定できます。「表示」→「色」を選ぶと、図4のダイアログが出て、文字色と背景色を、赤(red)、緑(green)、青(blue)のミックスで指定できます。「表示」→「日/英」を選ぶと、曜日の表示を日本語にするか英語にするかを切り替えることができます。
ここではさらに、付加機能として「時報」を実装しました。幼いころ、柱時計や鳩時計の音で現在時刻を知った人も多いことでしょう。「大音量」「中音量」「小音量」の設定ができますが、これは難しいことをしているわけではなくて、音量の違う音声ファイル(ここではWAVファイル)を3つ用意して、それを切り替えているだけです。プログラムのソースコードとWAVファイルは、本記事冒頭にあるリンクからダウンロードできますので、ぜひ動かして、鳴らしてみてください。
シンプルな時計を作ってみよう。
DigiClockは300行以上あるプログラムです。いきなりそんな長いコードを読んだり書いたりするのは難しいでしょう。また、筆者が普段プログラムを作る際も、最初はシンプルなプログラムを書いて動作実験をしてから、それをベースに実用を目指した長いプログラムを書くようにしています。
まずは、図5のシンプルなデジタル時計「DigitalClock」(完成版のDigiClockとは名前が違います)のコード(リスト1)を見てみましょう。
(1)で、日時を扱うdatetimeモジュール、ロケール(地域設定)を扱うlocaleモジュール、GUI(Graphical User Interface)を作るためのtkinterモジュールをインポートします。
いきなり末尾の方に飛びますが、(17)でTkクラスのインスタンスを作ってrootという名前で参照できるようにし、(18)でDigitalClockクラスにrootを与えてインスタンスを作っています。
このDigitalClockクラスは、また前の方に戻りますが、(2)に記述しています。
インスタンス作成時には、(3)の__init__関数(コンストラクタ)が呼び出されます。
(4)でウインドウのタイトルにクラス名を表示し、(5)で横幅と高さを指定し、(6)で背景が白のキャンバス(Canvasクラスのインスタンス)を作ります。背景はここでは、RGB値がすべて最大値であることを示す文字列「'#FFFFFF'」を指定していますが、「'white'」でも同じ色になります。
(7)で、キャンバスを表示します。expand=Trueで拡張可能であることを、fill=tkinter.BOTHで、X軸方向Y軸方向の両方を埋め尽くすことを指定します。
(8)のself.afterIdはあとで使うので、ここでは何も格納されていないことを示すNoneで初期化しておきます。
(9)で、ロケールを日本にします。
(10)は、ウインドウがリサイズされたときなどに(11)のconfiguredメソッド(クラスが持つ関数)を呼び出す指定です。(11)では、(12)のupdateメソッドを呼び出すようにしています。
afterメソッドで繰り返しを記述。
では、呼び出される側である(12)のupdateメソッドを見ていきましょう。
updateメソッドは画面上の日時表示を更新するもので、1秒より短い時間で繰り返し呼び出す必要があります。tkinterでは、一定時間ごとの繰り返し処理を記述するのにafterメソッドを使います。(16)では、100ミリ秒後に自分自身(updateメソッド)を呼び出すように記述しています。
afterメソッドの戻り値は、(8)で用意したクラスのメンバー変数afterIdに格納します。この値は、(13)でafter_cancelメソッドを呼び出すのに使います。
afterメソッドは、2回呼び出すとうまく動かなくなります。afterメソッドに次の処理が仕掛けられている場合は、いったんそれをキャンセルしてから再度呼び出す必要があります。
このプログラムでは、ウインドウのリサイズをしたときにもupdateメソッドを呼び出したいですし、前回updateメソッドを呼び出してから100ミリ秒後にもupdateメソッドを呼び出したいので、その2者が不整合を起こさないように、(8)(13)(16)のような書き方をしました。
(14)でキャンバスを消去し、(15)で日時を中央に表示します。winfo_widthでキャンバスの幅を、winfo_heightでキャンバスの高さを取得して、それを2で割って文字の位置を決めています。
リスト1を入力して動かしてみましょう。秒数が更新されるか、ウインドウをリサイズしたときに文字が中央に表示されるかなどをチェックしてみてください。筆者は、Pythonの3.11.4で動作をチェックしました。
winsoundモジュールで音声出力。
次に、図6のプログラム「Winsound1」を作ってみます。「サウンド再生」ボタンを押すと、「ぼーん」という音が5回鳴るプログラムです。コードはリスト2です。
(1)で、Windows用の音声再生インタフェースであるwinsoundモジュールを読み込んでいます。winsoundはPythonに標準装備されているモジュールなので、別途インストールする必要はありません。
(2)でボタンを作り、ユーザーがそれを押すと(3)のbuttonClickedメソッドを呼ぶようにします。
(4)ではPythonプログラム「winsound1.py」と同じフォルダー(ディレクトリ)にある「s1_minus06db.wav」のフルパス文字列を作って変数wavFilePathに格納しています。
(5)ではそれをファイルとして開き、変数wavに読み込んでいます。
そして(6)でwavを5回再生しています。
PlaySoundメソッドの第2引数として与えているSND_MEMORYフラグは、メモリー上のWAVファイルイメージを再生するときに使います。そこにSND_FILENAMEフラグを使うと、WAVファイルを直接指定することもできます。ただ、今回は5回再生するので、ファイルの読み込みが1回で済む方がよいだろうと判断しました。
pygameモジュールで音声出力。
しかし、winsoundはWindowsでしか使えません。MacやLinuxでも音声出力をしたいということであれば、Pythonでゲーム作成をするためのライブラリ「Pygame」を使うのが簡単でよさそうです。PygameのWebサイト(https://www.pygame.org/news)には、現行版のバージョン2.5.0を、
python -m pip install -U pygame==2.5.0 --user。
というコマンドでインストールできると書かれています。Windowsのコマンドプロンプトでそのコマンドを実行してインストールした様子を図7に示します。「python -m」の部分を省略して、pip以下を実行することもできます。
リスト2と同様の処理のプログラムを、Pygameを利用して書いたのがリスト3です。
(1)でPygameからmixerをインポートします。
(2)でmixerを初期化し、(3)でWAVファイルをロードします。
(4)で音声を出力します。引数に「5」を与えているので5回再生します。「0」または「1」にすると、1回だけ再生します。
このあとに作成する時報付きデジタル時計「DigiClock」は、winsoundを利用して音声を出力するので、MacやLinuxでは動きません。MacやLinuxをお使いの方は、Pygameを使った形に変えてみてください。さほど難しくはないだろう、と思います。
長いけど「DigiClock」を読んでみよう。
それでは、図1~4で紹介した時報付きデジタル時計「DigiClock」(DigiClock.py)のコードを読んでみましょう。前述したように長いコードですので、分割しながら説明します。本連載の第1回で解説した、メニュー作成、INIファイルへの設定の読み書き、フォント設定ダイアログ、HTML(HyperText Markup Language)ファイルの表示、バージョン情報の表示などについては、簡単な解説にとどめます。第1回と併せてお読みいただければ幸いです。
リスト4は、DigiClock.pyの冒頭部です。locale、winsoundなどをインポートしています。
リスト5は、DigiClock.pyの最後の部分です。
(1)はフォント名、(2)は背景色、(3)は前景色(文字色等)を格納するグローバル変数で、文字列として使うことを型ヒントで示しました。
(4)は現在の環境で使えるフォント名の一覧をFONTSに格納しています。
次に、DigiClockクラスの冒頭部とコンストラクタ(__init__メソッド)をリスト6に示します。
(5)は、変数の宣言および初期化です。
(6)では、リスト5の(1)(2)(3)で宣言したfontFamily(フォント名)、bgColor(背景色)、fgColor(前景色)を初期化しています。フォント名の初期値を「Courier New」にしているのは、筆者が使っているWindows機にもMacにもこのフォントがあったからです(たまたまかもしれませんが)。「#FFFFFF」は白、「#000000」は黒を示します。
(7)は、ロケールの初期値です。
(8)のsignalValueは時報の設定を格納するための変数で、tkinterのIntVarクラスのインスタンスを使っています。(14)で「variable=」の先に指定するためです。
(9)では、setメソッドを使ってsignalValueにゼロを代入して初期化しています。ゼロは図2(c)のメニュー「時報」の「なし」に対応する値です。
(10)のpreviousSignalは、「前回時報を鳴らしたのがいつなのか」を格納する変数です。これを記録しておかないと、例えば3時の時報を何度も鳴らしてしまう、ということになりかねません。
(11)で、INIファイルから設定を読み込みます。bgColor、fgColor、locale、signalValueなどの前回設定値を読み込みます。
(12)は、「表示」メニューです。「フォント」のイベントハンドラがmenuSettingFontFamilyで、「色」のイベントハンドラがmenuSettingColorで、「日/英」のイベントハンドラがmenuSettingLocaleです。それぞれのイベントハンドラは、後述するリストで宣言しています。そこでまた解説しますので、おぼえておいてください。
(13)は、「時報」メニューです。
(14)では「時報」メニューの、「なし」「大音量」「中音量」「小音量」の項目をラジオチェックメニューで作っています。ラジオチェックメニューとは、複数の中から1つを選択できるもので、選択した項目には図2(c)のようにチェックマークが表示されます。(17)でsignalValueの値を取得して、適切なものを選択します。
(15)はセパレータ(区切り線)、(16)は項目「テスト」で、そのハンドラは後述するリスト13の(73)です。
(18)はキャンバスの作成です。背景色は(2)で宣言したbgColorの値を取得して設定します。
(19)はロケールの設定で、(7)で宣言したメンバー変数の値をセットします。
リスト1と同様に、時計の中核部分はconfiguredメソッドとupdateメソッドです。それがリスト7です。
(20)のconfiguredは、(21)のupdateを呼び出すだけです。
updateメソッド内の(22)は、文字を表示するX座標とY座標の作成です。Y座標を「2.5」で割ったのは、見栄えを微調整したからです。
(23)で日付をtextDateに格納し、(24)で時刻をtextTimeに格納しています。(25)で日付表示のフォントを用意しています。フォント名はリスト5の(1)で宣言したfontFamilyから取得し、フォントの大きさはキャンバスの横幅を16で割ったものとします。
(26)で日付を表示します。anchorは、描く文字のどの部分をXY座標に接するようにするかの指定で、ここでは「s」(south)とし、文字の中央下端がXY座標に接する(文字がXY座標の上に表示される)ようにしました。
(27)で時刻表示のフォントを用意します。こちらは8で割って、日付より大きくしています。(28)ではanchorを「n」として、文字の中央上端がXY座標に接するようにしました。
(29)から(39)は、時報を鳴らすコードです。
(29)で分の数字2文字をminuteに入れ、(30)で時の数字2文字をhourに入れ、(31)で時・分の数字4文字をhourMinuteに入れます。
(32)では分が「00」または「30」だったら次に進み、(33)では今発しようとしている時報がリスト6の(10)で宣言したpreviousSignalと同じかどうかをチェックしています。時報を打つ必要がある場合、(34)でminuteが「00」であるかどうかをチェックし、(35)ではhourを12で割った余りをrepeatに入れます。ただ、12時のときにゼロ回では困るので、(36)ではその場合は12にしています。
(37)で、playSignalメソッドを呼び出して時報を鳴らします。
playSignalメソッドはリスト8です。
(40)では、鳴らす回数がゼロ以下であれば何もしないでメソッドを抜けます。
(41)では、図2の(c)が「なし」であればsignalValueはゼロなので、何もしないで抜けます。1の場合は音量が最も大きいファイル「s1_minus00db.wav」を、2の場合はそれより6デシベル小さい「s1_minus06db.wav」を、3の場合はさらに6デシベル小さい「s1_minus12db.wav」を使うことにします。その後はリスト2と同様です。
リスト7の(38)に戻ります。分(minute)が「30」の場合は時報を1回鳴らします。
(39)は、今回の時報の時・分の数字4文字を、リスト6の(10)で宣言したpreviousSignalに格納しています。これによって、(33)のチェックが働くようになります。
ダイアログ作りは大変だ。
メニューで「表示」→「フォント」を選ぶと呼び出されるのが、リスト9のmenuSettingFontFamilyメソッドです。先ほどのリスト6の(12)の「表示」メニューのところで「おぼえておいてください」と言った箇所ですね。
(42)では、リスト10のFontDialogクラスのインスタンスを生成することで、図3のダイアログが現れます。FontDialogクラスはsimpledialog.Dialogを継承したクラスです。
リスト10の(43)はコンストラクタで、title属性を使ってダイアログのタイトルバーの表示を「フォント設定」にして、親クラスのコンストラクタを呼び出しています。
(44)は図3の中央にあるリストボックスとスクロールバーの表示を作るbodyメソッドで、(45)はユーザーが「OK」ボタンを押すと呼び出されるapplyメソッドです。applyメソッドでは、リストボックスで選択されたフォント名をグローバル変数のfontFamilyに格納します。
「表示」→「色」で呼び出されるのが、リスト11のmenuSettingColorメソッドです。はい、ここも先ほどのリスト6の(12)の「表示」メニューのところで、「おぼえておいてください」と言った箇所です。
リスト11の(46)で、ColorDialogクラス(リスト12)のインスタンスを生成します。これだけで図4のダイアログが出てきます。ColorDialogクラスも、simpledialog.Dialogクラスの子クラスです。
リスト12の(48)以降のbodyメソッドで、図4の画面の「OK」「キャンセル」ボタンの上を作ります。
(49)は、tkinter.IntVarクラスのインスタンスです。これらの変数を(57)などのスケール(Scale)のvariableに指定して、スケールの値を変数に受け取ります。
次に前景色(文字色)と背景色のプレビュー表示を作ります。
(50)でキャンバスを作り、(51)でテキストのフォントを変数fに用意して、(52)でキャンバスにテキストを表示します。それぞれにcanvasSample、textSampleという名前を付けて、後で参照できるようにしています。
(53)でキャンバスをグリッドに表示します。columnspanを「2」と指定しているのは、グリッドの2列分を使うためです。
(54)は「文字色」というラベルです。(55)で「赤」という文字ラベルを作りグリッドに配置します。
(56)でスケールの横幅を指定するscaleLengthを用意し、240に初期化します。
(57)でスケールを作り、グリッドに配置します。同様に、(58)で文字色の緑と青のラベルとスケールを作ります。
(59)では、背景色の操作子を作っています。
(60)は、現在の色をスケールに反映するコードです。リスト5の(2)で宣言したbgColorと、(3)で宣言したfgColorの値を、(66)のcolorStringtoRgbメソッドに渡し、戻ってきたRGB(赤緑青)の値をスケールに反映しています。
スケールの値が変化すると、(61)のscaleChangedメソッドが呼び出されます。
(62)では、bgRed、bgGreen、bgBlueの値を(67)のrgbToColorStringメソッドに渡して、色を表す文字列を取得し、それをローカル変数colorにいったん格納しています。
(63)でプレビュー表示のキャンバスの色を変更します。
(64)では、fgRed、fgGreen、fgBlueから色文字列を得てcolorに格納し、(65)でプレビュー表示のテキストの色を変更します。
ユーザーが色設定ダイアログの「OK」ボタンを押すと、(68)のapplyメソッドが呼び出されます。fgRed、fgGreen、fgBlueの値に応じてグローバル変数fgColorを更新し、bgRed、bgGreen、bgBlueの値に応じてグローバル変数bgColorを更新します。ここでリスト11の(47)に処理が移り、キャンバスの背景色が更新されます。文字色は次回更新時(長くても100ミリ秒後)に変わります。
メニューの「表示」→「日/英」のイベントハンドラは、リスト13の(69)です。ここも、先ほどのリスト6の(12)の「表示」メニューのところで、「おぼえておいてください」と言った箇所です。
リスト13の(70)では、「ja_JP」であれば「en_US」に、そうでなければ「ja_JP」に変更し、(71)でロケールを更新し、(72)で画面を更新します。
「時報」→「テスト」のイベントハンドラは、(73)です。時報が「なし」になっている場合はその旨を表示して、音は鳴らしません。そうでなければ「1」を与えて、playSignalメソッドを呼び出します。
「ヘルプ」メニューのイベントハンドラをリスト14に、「ファイル」メニューのイベントハンドラをリスト15に示します。これらは第1回で解説したサンプルプログラムとほぼ同様です。ここでの解説は省略します。
時報付きデジタル時計、いかがでしたでしょうか? 1秒ごとに「カチッ」という音を出したいな、タイマー機能が欲しいな、アラーム機能が欲しいな、ストップウォッチ機能が欲しいな、といった気持ちはあなたの中にあるでしょうか? 欲しい機能を、あなたがぜひ実装してみてください!