Python wxPython: wx.BusyCursorでユーザーに「処理中」を伝えるテクニック

Python

あなたが作ったwxPythonアプリケーションで、ボタンを押した後に重い処理(ファイルの読み込み、データの計算など)をさせると、数秒間、画面が真っ白になったり、操作を一切受け付けなくなったりしていませんか?

ユーザーから見れば、その現象は「フリーズした」「クラッシュした?」としか思えず、非常に大きな不安を与えてしまいます。最悪の場合、アプリを強制終了されてしまうかもしれません。

この記事では、wxPythonに標準で備わっているwx.BusyCursorを使い、**「今、ちゃんと処理していますよ」**とユーザーに視覚的に伝える、簡単かつ効果的なテクニックを解説します。


はじめに:その処理、ユーザーには「フリーズ」に見えていませんか?

wxPythonアプリを開発していると、どうしても時間のかかる処理を実行しなければならない場面が出てきます。

  • 大きな設定ファイルの読み書き
  • データベースへの重いクエリ発行
  • 複雑な計算やデータ処理
  • ネットワーク通信の待ち時間

これらの処理をボタンクリックイベントなどでそのまま実行すると、wxPythonのメインイベントループがブロック(停止)され、GUIは一切の操作に応答しなくなります。これが「フリーズ」の正体です。

ユーザーは、自分の操作が無視された(ように見える)状態を極端に嫌います。UX(ユーザー体験)の観点から、**「アプリは自分の操作に応答している」**と感じさせ続けることが非常に重要です。

wx.BusyCursorは、この問題を「解決」するものではありませんが、ユーザーの不安を「軽減」するための、最も手軽な第一歩となります。

本記事で解説するwx.BusyCursorの公式ドキュメントはこちらです。


wx.BusyCursorとは? カーソルを変えるだけのシンプルな機能

結論から言うと、wx.BusyCursorは、マウスカーソルの形状をOS標準の「ビジーカーソル」に一時的に変更するためのクラスです。

Windowsであれば砂時計、macOSやLinuxであれば回転するアイコン(スピニングカーソル)などが表示されます。

ユーザーはOSの操作でこのアイコンに慣れ親しんでいるため、これを見るだけで「何か処理が実行されているんだな」と直感的に理解できます。

wx.BusyCursorの動作原理は非常にシンプルです。

  1. wx.BusyCursorインスタンスが作成された瞬間に、マウスカーソルがビジーカーソルに変わります。
  2. そのインスタンスが破棄(デストラクト)された瞬間に、マウスカーソルが元の矢印カーソルに戻ります。

この「作成時」と「破棄時」という特性を理解することが、wx.BusyCursorを正しく使いこなす鍵となります。


【推奨】with文を使ったwx.BusyCursorの最も簡単な使い方

wx.BusyCursorを手軽かつ安全に使う方法は、Pythonのwith文(コンテキストマネージャ)と組み合わせることです。

なぜwith文が推奨されるのか?

with文を使うと、前述の「インスタンスの作成と破棄」をPythonが自動的に管理してくれます。

  • メリット1:自動復元(正常時) withブロック内の処理がすべて正常に終われば、ブロックを抜ける際に自動的にカーソルが元に戻ります。
  • メリット2:自動復G元(エラー時) これが最も重要です。もしwithブロック内の処理でエラー(例外)が発生しても、ブロックを抜ける際に確実にカーソルが元に戻ります
  • メリット3:可読性 「ここからここまでが処理中の区間である」ということがコード上で明確になります。

基本的なコード例(デモ)

with文を使ったwx.BusyCursorの基本的なデモコードです。ボタンを押すと、2秒間のダミー処理(time.sleep)が実行され、その間だけカーソルが変わります。

import wx
import time

class MyFrame(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title=title, size=(300, 200))

        panel = wx.Panel(self)
        self.button = wx.Button(panel, label="重い処理を実行 (2秒)")
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.button, 1, wx.EXPAND | wx.ALL, 30)
        panel.SetSizer(sizer)

        # ボタンクリックイベントをバインド
        self.button.Bind(wx.EVT_BUTTON, self.on_heavy_process)
        
        self.Centre()
        self.Show()

    def on_heavy_process(self, event):
        print("処理開始... カーソルを変更します。")
        
        # with文でwx.BusyCursorを呼び出す
        # このブロックに入った瞬間にカーソルが変わる
        with wx.BusyCursor():
            # ここに時間のかかる処理を書く
            # (これはダミーの重い処理)
            try:
                time.sleep(2) # 2秒間フリーズさせる
                print("2秒間の処理が完了しました。")
                
                # (オプション) エラー発生時のテスト
                # raise ValueError("テストエラー") 
                
            except Exception as e:
                print(f"処理中にエラーが発生: {e}")

        # withブロックを抜けた瞬間にカーソルは自動で元に戻る
        print("処理終了... カーソルが戻りました。")
        wx.MessageBox("処理が完了しました", "完了", wx.OK | wx.ICON_INFORMATION)


if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame(None, title="wx.BusyCursor with 'with' statement")
    app.MainLoop()

このコードを実行しボタンを押すと、2秒間カーソルが砂時計(または回転アイコン)になり、処理が終わると自動で元に戻ることが確認できます。コード内のraise ValueErrorのコメントを外してテストしても、エラーが発生した上でカーソルだけはちゃんと元に戻ります。


(応用)wx.BusyCursorを手動で制御する方法

with文を使わずに、wx.BusyCursorのインスタンスを直接変数に格納して手動で制御することも可能です。しかし、この方法には大きな落とし穴があります。

手動制御のコード例

    def on_heavy_process_manual(self, event):
        print("処理開始... (手動制御)")
        
        # 1. インスタンスを作成し、変数に保持する
        #    この瞬間にカーソルが変わる
        self.busy_cursor_instance = wx.BusyCursor()

        # (重い処理)
        time.sleep(2)
        print("処理完了。")

        # 2. インスタンスを破棄(del)してカーソルを戻す
        del self.busy_cursor_instance
        # または self.busy_cursor_instance = None でもOK
        
        print("処理終了... (手動制御)")

一見、これでも問題なく動くように見えます。

手動制御の大きな注意点(非推奨の理由)

問題は、処理中にエラーが発生した場合です。

もしtime.sleep(2)の箇所でエラーが発生すると、del self.busy_cursor_instanceの行が実行されません。その結果、インスタンスが破棄されず、カーソルがビジーカーソルのまま元に戻らなくなってしまいます

これを防ぐには、Pythonのtry...finally構文を使い、エラーが発生しようがしまいが必ずfinally節でインスタンスを破棄するコードを書かなければなりません。

    def on_heavy_process_manual_safe(self, event):
        print("処理開始... (安全な手動制御)")
        self.busy_cursor_instance = wx.BusyCursor() # カーソル変更

        try:
            # --- 重い処理 ---
            print("重い処理を開始...")
            time.sleep(1)
            # もしここでエラーが発生すると...
            raise ValueError("手動制御中のエラー") 
            time.sleep(1) # ここは実行されない
            # --- 重い処理ここまで ---
            
        except Exception as e:
            print(f"エラーをキャッチ: {e}")
            
        finally:
            # finallyブロックはエラーの有無に関わらず必ず実行される
            print("finallyブロック実行。カーソルを戻します。")
            del self.busy_cursor_instance # これで確実にカーソルが戻る

        print("処理終了... (安全な手動制御)")

このtry...finallyを使った定型文は、with文が内部的にやっていること(コンテキスト管理プロトコル)と本質的に同じです。

with wx.BusyCursor():という1行は、この面倒でバグを生みやすいtry...finally構造を、安全かつシンプルに置き換えてくれる素晴らしい糖衣構文(シンタックスシュガー)なのです。特別な理由がない限り、with文を使いましょう。


wx.BusyCursorの限界と「本当の」重い処理への対策

ここで、wx.BusyCursorに関する最も重要な注意点を説明します。

結論から言うと、wx.BusyCursorは、GUIのフリーズ(イベントループのブロック)自体は一切解決しません

wx.BusyCursorのデモの問題点

先ほどのwith文のデモコードをよく観察してみてください。ボタンを押した瞬間、カーソルはwx.BusyCursorによってビジーカーソルに変わりますが、同時にクリックしたボタンは「押されたまま」の状態で固まり、ウィンドウの移動などもできなくなります

これは、time.sleep(2)がGUIのメインスレッドをブロックしているためです。wx.BusyCursorは、あくまで「フリーズしている間、せめてカーソルだけでも変えておく」という対症療法に過ぎません。

wx.BusyCursorが有効な場面

この対症療法が有効なのは、処理時間が0.5秒〜長くても2秒程度の「一瞬だけ固まる」処理です。ユーザーが「お、ちょっと待たされたな」と感じる程度の時間であれば、カーソルが変わるだけで体感的なストレスはかなり軽減されます。

本当に重い処理(数秒以上)はどうすべきか?

数秒、あるいは数十秒かかるような本当に重い処理は、wx.BusyCursorだけではごまかせません。

根本的な解決策は、重い処理を別スレッド(Thread)で実行し、GUIのメインスレッドをブロックしないようにすることです。

wx.BusyCursorは、このスレッド処理と組み合わせて使うのが最も効果的です。

  1. (ユーザーがボタンをクリック)
  2. with wx.BusyCursor():使わない。(スレッドが終わるまでwith文が終わらないため)
  3. self.busy = wx.BusyCursor()手動でカーソルを変更する。
  4. 重い処理(my_heavy_task)を別スレッドで開始する。
  5. GUIスレッドの処理(on_heavy_process)はすぐに終了する(カーソルはビジーカーソルのまま)。
  6. (…別スレッドがバックグラウンドで重い処理を実行中…)
  7. (…GUIはフリーズせず、操作可能…)
  8. 別スレッドが処理を完了したら、wx.CallAfterを使ってメインスレッドに通知する。
  9. 通知を受け取ったメインスレッド側のメソッド(例:on_task_complete)で、del self.busy を実行し、カーソルを元に戻す

このように、スレッド処理と組み合わせる場合は、with文ではなく手動での制御が必要になります。(この記事ではスレッドの詳細は扱いませんが、wx.BusyCursorの本来の活躍場所として覚えておいてください)


(補足)wx.BusyInfoとの違いと使い分け

wx.BusyCursorと似た機能にwx.BusyInfoがあります。

結論から言うと、wx.BusyInfoは、ビジーカーソルに加えて、メッセージ付きの小さなポップアップウィンドウを表示します

wx.BusyInfowith文と組み合わせて使うのが最も簡単です。

import wx
import time

class MyFrame(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title=title, size=(300, 200))
        panel = wx.Panel(self)
        button = wx.Button(panel, label="BusyInfoを表示 (2秒)")
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(button, 1, wx.EXPAND | wx.ALL, 30)
        panel.SetSizer(sizer)
        button.Bind(wx.EVT_BUTTON, self.on_show_busy_info)
        self.Centre()
        self.Show()

    def on_show_busy_info(self, event):
        message = "データを処理中です...\nしばらくお待ちください。"
        
        # with文でwx.BusyInfoを呼び出す
        with wx.BusyInfo(message, parent=self):
            # (ダミーの重い処理)
            time.sleep(2)
            
        wx.MessageBox("処理が完了しました", "完了", wx.OK)

# (以下、appの実行部分は同じ)
if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame(None, title="wx.BusyInfo Example")
    app.MainLoop()

これを実行すると、カーソルが変わると同時に「データを処理中です…」というメッセージボックスが表示され、withブロックを抜けると自動で消えます。

使い分けの目安:

  • wx.BusyCursor: 処理中であることを「さりげなく」伝えたい場合。(0.5〜1秒程度)
  • wx.BusyInfo: 処理中であることを「明確に」伝えたい場合。何の処理をしているかメッセージも表示したい場合。(1〜3秒程度)

wx.BusyCursorwx.BusyInfoを同時に使うことも可能です(with wx.BusyCursor(): のネストの内側で with wx.BusyInfo(...): を呼び出すなど)。


まとめ:wx.BusyCursorでユーザーに優しいアプリを作ろう

wx.BusyCursorは、重い処理中にユーザーが感じる「フリーズしたかも?」という不安を軽減するための、シンプルで強力なUX改善機能です。

  • wx.BusyCursorは、カーソルを砂時計やスピナーに変えます。
  • 最も安全で推奨される使い方は、with wx.BusyCursor(): ブロックで処理を囲むことです。
  • with文を使えば、処理中にエラーが発生してもカーソルは確実に元に戻ります。
  • ただし、wx.BusyCursorGUIのフリーズ自体を解決するものではありません
  • 本当のフリーズ対策(数秒以上の処理)には、スレッド処理とwx.CallAfterの組み合わせが必要です。

まずはtime.sleep()のような重い処理のダミーをwith wx.BusyCursor():で囲み、その効果と限界を体感してみることから始めてみてください。たったこれだけの配慮が、あなたのアプリケーションを格段に「ユーザーフレンドリー」にします。

コメント

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