PythonのGUIライブラリwxPythonで、「ボタンを押して3秒後に何か処理をしたい」や「1秒ごとに時計の表示を更新したい」と考えたことはありませんか?
そんな時、Python標準のtime.sleep()を使おうとして、GUI(画面)が真っ白に固まってしまった…という経験は、多くの初学者が通る道です。
この記事では、なぜtime.sleep()がGUIアプリで問題を起こすのか、そしてその代わりに使うべきwx.Timerの正しい使い方を、初心者にも分かりやすく解説します。wx.Timerをマスターすれば、wxPythonアプリで「遅延実行」や「定期実行」を自由自在に実装できるようになります。
はじめに:wxPythonで「待つ」処理、time.sleep()でGUIが固っていませんか?
wxPythonアプリでtime.sleep()を使うと、なぜGUIがフリーズ(固まる)のでしょうか。
結論から言うと、time.sleep()はwxPythonの「イベントループ」と呼ばれるメインの処理を強制的に停止させてしまうからです。
wxPythonアプリは、ボタンのクリック、マウスの移動、ウィンドウのリサイズといった「イベント」を常に監視し、それに対応することで動作しています(これをイベントループと呼びます)。
time.sleep(3)を実行すると、このイベントループが3秒間完全に停止します。その結果、ユーザーがウィンドウを動かしようとしても反応せず、OSからは「応答なし」と判断されてしまうのです。
この記事で紹介するwx.Timerは、このイベントループを止めずに「指定した時間後にある処理を実行する」という予約を可能にする、wxPython開発に必須の機能です。
wx.Timerとは? GUIを止めずに待つための必須コンポーネント
wx.Timerは、指定した時間(ミリ秒単位)が経過するたびに、wx.EVT_TIMERという特別な「イベント」を発生させるコンポーネントです。
time.sleep()が処理を「ブロック(停止)」するのに対し、wx.Timerはイベントループに「予約」を入れるだけです。そのため、タイマーが待機している間も、ユーザーはボタンを押したりウィンドウを操作したりでき、GUIはフリーズしません。
wx.Timerの主な使い道は2つです。
- 定期実行: 1秒ごと、0.5秒ごとなど、一定間隔で繰り返し処理を実行する(例:時計の更新、データの自動リロード)。
- 遅延実行: 今すぐではなく、数秒後などに一度だけ処理を実行する(例:処理完了のメッセージを3秒後に表示する)。
wx.Timerの基本的な使い方(定期実行)
まずは、最も基本的な「定期実行」の方法を見ていきましょう。
結論として、タイマーの使い方は「1. インスタンス作成とイベントの紐付け」「2. Start()で起動」の2ステップです。
1. wx.Timerインスタンスの作成とEVT_TIMERのバインド
タイマーを使うには、まずwx.Timerクラスのインスタンスを作成します。このとき、owner(所有者)として、タイマーが属するFrameやPanel(通常はself)を指定します。
次に、タイマーイベント(wx.EVT_TIMER)が発生したときに呼び出すメソッド(イベントハンドラ)をBind()で紐付けます。
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, title):
super().__init__(parent, title=title)
# 1. タイマーインスタンスを作成 (ownerはself)
self.timer = wx.Timer(self)
# 2. タイマーイベントと呼び出すメソッドを紐付け(Bind)
# 第3引数にself.timerを指定するのが重要
self.Bind(wx.EVT_TIMER, self.on_timer_event, self.timer)
# ... (中略) ...
def on_timer_event(self, event):
# タイマーイベント発生時にここが呼ばれる
print("タイマーイベントが発生しました!")
# ... (中略) ...2. Start(milliseconds)でタイマーを開始する
タイマーを起動するにはStart()メソッドを使います。引数には実行間隔を**ミリ秒(1000分の1秒)**で指定します。
例えば「1秒ごと」なら1000、「0.5秒ごと」なら500を指定します。
# ( __init__ の中で )
# 1000ミリ秒 (1秒) ごとに定期実行を開始
self.timer.Start(1000)Start()に引数を指定しない、または0より大きい値を指定すると、タイマーは定期的にイベントを発生させ続けます。
3. Stop()でタイマーを停止する
定期実行しているタイマーを停止するにはStop()メソッドを呼び出します。
def stop_timer_method(self):
# タイマーが動作中か確認してから停止
if self.timer.IsRunning():
self.timer.Stop()
print("タイマーを停止しました。")実践コード例:wx.Timerで簡単なデジタル時計(ラベル更新)を作る
wx.Timerによる定期実行の最も良い例が、デジタル時計です。1秒ごとに現在時刻を取得し、wx.StaticText(ラベル)の表示を更新します。
import wx
import datetime
class MyFrame(wx.Frame):
def __init__(self, parent, title):
super().__init__(parent, title=title, size=(300, 150))
panel = wx.Panel(self)
# 時刻を表示するためのラベル
self.clock_label = wx.StaticText(panel, label="", style=wx.ALIGN_CENTER)
font = self.clock_label.GetFont()
font.PointSize += 10 # フォントサイズを大きく
self.clock_label.SetFont(font)
# Sizerでラベルを中央に配置
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.clock_label, 1, wx.EXPAND | wx.ALL, 20)
panel.SetSizer(sizer)
# --- タイマーの設定 ---
# 1. タイマーインスタンスを作成
self.timer = wx.Timer(self)
# 2. イベントをバインド
self.Bind(wx.EVT_TIMER, self.on_timer_update_clock, self.timer)
# 3. タイマーを開始 (1000ms = 1秒ごと)
self.timer.Start(1000)
# 起動時にも一度時刻を更新しておく
self.update_clock_label()
self.Centre()
self.Show()
def on_timer_update_clock(self, event):
# 1秒ごとにこのメソッドが呼ばれる
self.update_clock_label()
def update_clock_label(self):
# 現在時刻を取得してラベルにセット
now = datetime.datetime.now()
time_str = now.strftime("%Y-%m-%d %H:%M:%S")
self.clock_label.SetLabel(time_str)
if __name__ == '__main__':
app = wx.App()
frame = MyFrame(None, title="wx.Timer Digital Clock")
app.MainLoop()このコードを実行すると、time.sleep()を使っていないにもかかわらず、1秒ごとにラベルが更新され、かつウィンドウは自由に操作できる(フリーズしない)ことが確認できます。
wx.Timerで処理を「遅延実行」する(一回きり)
次に、「定期実行」ではなく「一回だけ遅延実行」する方法です。
結論から言うと、StartOnce(milliseconds)メソッドを使うのが最も簡単です。
StartOnce()の使い方とコード例
StartOnce()は、指定したミリ秒後に一度だけEVT_TIMERイベントを発生させ、その後自動的にタイマーを停止してくれます。
以下の例は、「ボタンを押したら3秒後にダイアログを出す」処理を実装したものです。
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, title):
super().__init__(parent, title=title, size=(300, 150))
panel = wx.Panel(self)
button = wx.Button(panel, label="3秒後にダイアログ表示")
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(button, 1, wx.EXPAND | wx.ALL, 20)
panel.SetSizer(sizer)
# --- タイマーの設定 ---
# 1. タイマーインスタンスを作成
self.delay_timer = wx.Timer(self)
# 2. イベントをバインド
self.Bind(wx.EVT_TIMER, self.on_show_dialog, self.delay_timer)
# ボタンクリックイベント
button.Bind(wx.EVT_BUTTON, self.on_button_click)
self.Centre()
self.Show()
def on_button_click(self, event):
print("ボタンが押されました。3秒後に処理を実行します...")
# 3000ms (3秒) 後に一度だけ実行
self.delay_timer.StartOnce(3000)
def on_show_dialog(self, event):
# 3秒後にここが呼ばれる
print("タイマーイベント発生!")
wx.MessageBox("3秒が経過しました!", "通知", wx.OK | wx.ICON_INFORMATION)
if __name__ == '__main__':
app = wx.App()
frame = MyFrame(None, title="StartOnce Example")
app.MainLoop()(応用) Start()とStop()で遅延実行を実現する方法
StartOnce()が導入される前は、イベントハンドラ内でStop()を呼び出すことで遅延実行を実現していました。
StartOnce()は、この処理の便利なショートカットというわけです。
def on_button_click(self, event):
# Start(3000) でも開始できるが...
self.delay_timer.Start(3000)
def on_show_dialog(self, event):
# (1) まず自分でタイマーを止める
self.delay_timer.Stop()
# (2) 目的の処理を実行
wx.MessageBox("3秒が経過しました!", "通知", wx.OK | wx.ICON_INFORMATION)特別な理由がない限りは、一回きりの遅延実行にはStartOnce()を使うのがシンプルで推奨されます。
もっと手軽に遅延実行! wx.Timer.CallLater()
StartOnce()でも十分便利ですが、そのためだけにwx.Timerインスタンスをself.delay_timerのように管理し、Bindでイベントハンドラを紐付けるのは少し面倒です。
結論:wx.Timer.CallLater()を使えば、タイマーインスタンスの管理すら不要になります。
wx.Timer.CallLater()は、wx.Timerの静的メソッド(クラスメソッド)で、以下のように使います。
wx.Timer.CallLater(milliseconds, callable, *args)
milliseconds: 遅延させる時間(ミリ秒)callable: 遅延実行したい関数(メソッド)*args: その関数に渡す引数
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, title):
super().__init__(parent, title=title, size=(300, 150))
panel = wx.Panel(self)
button = wx.Button(panel, label="3秒後にダイアログ表示 (CallLater)")
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(button, 1, wx.EXPAND | wx.ALL, 20)
panel.SetSizer(sizer)
button.Bind(wx.EVT_BUTTON, self.on_button_click)
self.Centre()
self.Show()
def on_button_click(self, event):
print("ボタンが押されました。CallLaterで3秒後に処理を実行します...")
# インスタンス管理もBindも不要!
wx.Timer.CallLater(3000, self.show_delayed_message, "これが遅延されたメッセージです")
def show_delayed_message(self, message):
# 3秒後にここが直接呼ばれる
print(f"CallLaterイベント発生: {message}")
wx.MessageBox(message, "通知", wx.OK | wx.ICON_INFORMATION)
if __name__ == '__main__':
app = wx.App()
frame = MyFrame(None, title="wx.Timer.CallLater Example")
app.MainLoop()self.timerの管理やBindが一切不要になり、コードが非常にスッキリしました。
StartOnce()とCallLater()は、以下のように使い分けると良いでしょう。
StartOnce(): 既にwx.Timerインスタンスを持っており、状況に応じてStart()(定期)とStartOnce()(一回)を切り替えたい場合。wx.Timer.CallLater(): とにかくシンプルに「この関数をN秒後に一度だけ呼びたい」場合。
wx.Timerを使う上での注意点
wx.Timerは非常に便利ですが、いくつかの注意点があります。
注意点1:タイマーイベント内で重い処理をしない
これが最も重要な注意点です。 EVT_TIMERのイベントハンドラ(例:on_timer_event)は、GUIと同じメインスレッドで実行されます。
もし、このハンドラ内で時間のかかる処理(例:大きなファイルの読み書き、複雑な計算、time.sleep())を行えば、結局GUIはフリーズします。
wx.Timerは、あくまで「処理を開始するキック(きっかけ)」に過ぎません。重い処理はThread(スレッド)で行い、その結果をGUIに反映させる際はwx.CallAfterなどを使う必要があります。
注意点2:タイマーの精度について
wx.Timerのmilliseconds指定は、「厳密にその時間で実行されること」を保証するものではありません。OSのスケジューリングに依存するため、実行タイミングは若干(数ミリ秒〜数十ミリ秒)前後することがあります。
GUIの更新などには十分な精度ですが、ミリ秒単位の厳密な制御が必要な処理には向きません。
注意点3:タイマーの所有者(owner)とライフサイクル
self.timer = wx.Timer(self)のように、ownerにself(FrameやPanelなど)を指定しておくと、そのFrameが閉じて破棄されるときに、wx.Timerも自動的にクリーンアップ(停止・破棄)されます。
ownerを省略することも可能ですが、アプリ終了時にタイマーが停止しないなどの問題が起きる可能性があるため、必ずownerを指定することを推奨します。
まとめ:wx.Timerを使いこなしてwxPythonアプリをレベルアップしよう
今回は、wxPythonでGUIをフリーズさせずに処理を遅延・定期実行するためのwx.Timerについて解説しました。
time.sleep()はNG: GUIのイベントループをブロックし、フリーズの原因になります。wx.Timerを使う: イベントループを止めずに、時間経過でEVT_TIMERイベントを発生させます。- 定期実行:
self.timer = wx.Timer(self)で作成、Bindし、Start(1000)で1秒ごとに実行。Stop()で停止。 - 一回きりの遅延実行 (推奨):
wx.Timer.CallLater(3000, self.my_func): 最も手軽な方法。self.timer.StartOnce(3000): タイマーインスタンスを管理している場合に便利。
wx.Timerは、時計アプリ、アニメーション、データの定期ポーリング(問い合わせ)、処理の遅延実行など、動的なGUIアプリケーションを作る上で絶対に欠かせない機能です。
time.sleep()を卒業し、wx.Timerを正しく使いこなして、応答性の高い快適なwxPythonアプリを開発しましょう。


コメント