Python wxPythonのwx.Timerで処理を遅延・定期実行する簡単な方法

Python

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. 定期実行: 1秒ごと、0.5秒ごとなど、一定間隔で繰り返し処理を実行する(例:時計の更新、データの自動リロード)。
  2. 遅延実行: 今すぐではなく、数秒後などに一度だけ処理を実行する(例:処理完了のメッセージを3秒後に表示する)。

wx.Timerの基本的な使い方(定期実行)

まずは、最も基本的な「定期実行」の方法を見ていきましょう。

結論として、タイマーの使い方は「1. インスタンス作成とイベントの紐付け」「2. Start()で起動」の2ステップです。

1. wx.Timerインスタンスの作成とEVT_TIMERのバインド

タイマーを使うには、まずwx.Timerクラスのインスタンスを作成します。このとき、owner(所有者)として、タイマーが属するFramePanel(通常は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.Timermilliseconds指定は、「厳密にその時間で実行されること」を保証するものではありません。OSのスケジューリングに依存するため、実行タイミングは若干(数ミリ秒〜数十ミリ秒)前後することがあります。

GUIの更新などには十分な精度ですが、ミリ秒単位の厳密な制御が必要な処理には向きません。

注意点3:タイマーの所有者(owner)とライフサイクル

self.timer = wx.Timer(self)のように、ownerselfFramePanelなど)を指定しておくと、その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アプリを開発しましょう。

コメント

タイトルとURLをコピーしました