Python/wxPythonのwx.NotifyEventとは?基本的な使い方とイベント処理を解説

Python

PythonでGUIアプリケーションを作成できるライブラリ wxPython。多機能でクロスプラットフォーム対応という魅力がある一方、「イベント処理が複雑で分かりにくい」と感じている方も多いのではないでしょうか。

特に、wx.NotifyEventwx.CommandEvent の違いは、初心者がつまずきやすいポイントの一つです。

この記事では、wxPythonのイベントシステムの中でも「通知」を司る wx.NotifyEvent に焦点を当て、以下の点を初心者にも分かりやすく解説します。

  • wx.NotifyEvent の基本的な役割
  • wx.CommandEvent との決定的な違い (wx.CommandEventの詳しい記事はこちら)
  • イベントを「拒否」する Veto() の使い方
  • 具体的なサンプルコードを通じた実践的なイベント処理

この記事を読み終える頃には、wx.NotifyEvent を正しく理解し、ユーザーにとってより親切なGUIアプリケーションを構築するためのヒントが得られているはずです。

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

wx.NotifyEventとは何か?

wx.NotifyEvent とは、ウィジェット(GUIの部品)の状態変化や、これから起こるアクションを「通知」するためのイベントです。

これは、wxPython のイベントシステムの基底クラスの一つである wx.Event から派生した、基本的なイベントクラスです。

GUIからの「通知」を受け取るためのイベント

wx.NotifyEvent の役割を理解する鍵は、「命令(Command)」ではなく「通知(Notify)」であるという点です。

  • 命令 (Command): ユーザーが「保存ボタンを押した」「メニューを選んだ」といった、明確な「実行指示」。
  • 通知 (Notify): ウィジェットが「閉じられようとしている」「サイズが変わった」「フォーカスが移った」といった、状況の「報告・連絡」。

wx.NotifyEvent は、後者の「通知」を受け取るために使われます。

wx.NotifyEvent が使われる代表的な例

具体的には、以下のようなイベントが wx.NotifyEvent(またはそれを継承したクラス)として扱われます。

  • wx.EVT_CLOSE:ウィンドウが閉じられようとしている時(wx.CloseEvent)
  • wx.EVT_NOTEBOOK_PAGE_CHANGING:ノートブック(タブ)のページが切り替わる直前(wx.BookCtrlEvent)
  • wx.EVT_SIZE:ウィンドウのサイズが変更された時(wx.SizeEvent)
  • wx.EVT_MOVE:ウィンドウが移動した時(wx.MoveEvent)
  • wx.EVT_SET_FOCUS / wx.EVT_KILL_FOCUS:ウィジェットがフォーカスを受け取った時・失った時(wx.FocusEvent)

これらのイベントは、ユーザーの直接的な「命令」とは少し異なり、アプリケーションの「状態変化」を知らせるものであることが分かるかと思います。

最も重要! wx.NotifyEventとwx.CommandEventの違い

wx.NotifyEvent を学ぶ上で、避けて通れないのが wx.CommandEvent との違いです。この二つはwxPythonのイベント処理の核となりますが、振る舞いが大きく異なります。

**最大の違いは「イベントが親ウィンドウに伝播(Propagate)するかどうか」**です。

違い①: イベントの伝播方法 (親ウィンドウへ遡るか)

イベントの伝播とは、イベントが発生したウィジェットから、その親ウィジェット(コンテナ)へとイベントが「伝わっていく」仕組みです。

  • wx.CommandEvent は、伝播します。例えば、パネル(wx.Panel)の上に置かれたボタン(wx.Button)がクリックされた場合、そのイベント(wx.EVT_BUTTON)はボタン自身だけでなく、親であるパネル、さらにその親であるフレーム(wx.Frame)へと伝播していきます。これにより、wx.Frame 側でまとめてイベント処理を Bind() できるというメリットがあります。
  • wx.NotifyEvent は、原則として伝播しません。例えば、ウィンドウが閉じられようとするイベント(wx.EVT_CLOSE)は、そのウィンドウ自身(wx.Frame など)でのみ発生し、その親(もしあれば)には伝播しません。wx.NotifyEvent は、そのイベントが発生したウィジェット自身が処理(Bind)する必要があります。

違い②: 主な目的 (アクション vs 通知)

これは前述の通りですが、目的が明確に異なります。

  • wx.CommandEvent: ユーザーの「命令」を伝えます。(例: wx.EVT_BUTTON, wx.EVT_MENU, wx.EVT_TEXT_ENTER)
  • wx.NotifyEvent: ウィジェットからの「通知」を伝えます。(例: wx.EVT_CLOSE, wx.EVT_SIZE, wx.EVT_SET_FOCUS)

どちらを使うべきか? 簡単な使い分けガイド

この違いを表にまとめます。

特徴wx.NotifyEventwx.CommandEvent
主な目的通知 (Notify)命令 (Command)
イベント伝播しない (No Propagation)する (Propagates)
代表例wx.EVT_CLOSE (閉じる)wx.EVT_BUTTON (ボタン)
Veto機能Veto可能なものが多い基本的にVetoしない

wxPythonでは、Bind() メソッドを使ってイベントと処理関数(ハンドラ)を紐付けます。この Bind() メソッドは、wx.Window(すべてのウィジェットの親)や wx.App が継承している wx.EvtHandler クラスによって提供されています。

wx.CommandEvent は伝播するため親でBindできますが、wx.NotifyEvent は伝播しないため、イベント発生元のウィジェットでBindする必要がある、と覚えておきましょう。

wx.NotifyEventの最大の特徴: Veto() と Allow()

wx.NotifyEvent(およびそのサブクラス)の多くが持つ、最も強力で重要な機能が Veto()(拒否) です。

Veto() とは、そのイベントが引き起こそうとしている「デフォルトの動作」をキャンセルするためのメソッドです。

Veto(): イベントの処理を「拒否」する

イベントハンドラ関数(event を引数に取る関数)の中で event.Veto() を呼び出すと、wxPythonはその後のデフォルト処理を中断します。

例えば、ウィンドウのクローズイベント(wx.EVT_CLOSE)で event.Veto() を呼ぶと、ウィンドウは閉じなくなります

Allow(): 「拒否」を取り消し、処理を「許可」する

Allow()Veto() の反対です。一度 Veto() されたイベントを、再び「許可」するために使います。ただし、Veto() されたものを後から Allow() するケースは稀で、主に Veto() を使うかどうかの制御が中心となります。

なぜ Veto() が必要なのか? (例: 閉じる前の確認)

Veto() がなぜ強力なのか、具体的な例で考えてみましょう。

アプリケーションにテキストエディタ機能があり、ユーザーが「閉じるボタン」を押しました。

もしテキストが編集された後、保存されていなかったらどうでしょう?

  1. アプリは wx.EVT_CLOSE イベント(通知)を受け取ります。
  2. イベントハンドラが起動し、「編集内容が保存されていません。保存しますか?」という確認ダイアログを表示します。
  3. ユーザーが「キャンセル」ボタンを押しました。
  4. この時、プログラムが event.Veto() を呼び出します。
  5. Veto() されたため、wxPythonは「ウィンドウを閉じる」というデフォルトの動作を中断します。

結果として、ユーザーは作業内容を失うことなく、編集に戻ることができます。

このように Veto() は、アプリケーションの動作をより安全でユーザーフレンドリーにするために不可欠な機能です。

wx.NotifyEventの基本的な使い方 (イベントハンドラの設定)

wx.NotifyEvent を処理する基本的な流れは、他のイベント処理と同じく Bind() メソッドを使います。

Bind() メソッドを使ったイベントの紐付け

イベントを処理するには、イベントを発生させるウィジェット(wx.Window やそのサブクラス)の Bind() メソッドを使います。

# self は wx.Frame や wx.Panel などの wx.EvtHandler を継承したオブジェクト
# wx.EVT_CLOSE イベントと self.OnClose メソッドを紐付ける
self.Bind(wx.EVT_CLOSE, self.OnClose)

wx.NotifyEvent は伝播しないため、基本的にイベントを発生させるオブジェクト自身(この例では self)で Bind する必要があります。

イベントハンドラ関数(コールバック関数)の定義方法

Bind() に指定する関数(イベントハンドラ)は、通常、第1引数に event オブジェクトを受け取ります。

def OnClose(self, event):
    # ここにイベント発生時の処理を書く
    # ...
    # 必要に応じて Veto() を呼ぶ
    # event.Veto()
    
    # [重要] Veto() しない場合は、event.Skip() を呼ぶか、
    # 何もせずに終了する(デフォルトの処理が行われる)
    
    # もし、このハンドラで処理が完結し、
    # 親クラスやデフォルトの処理をさせたくない場合は...
    # event.Skip(False) # 明示的にSkipしないことを示す
    
    # Veto() せず、かつ他のハンドラにも処理を続けさせたい場合は
    event.Skip() # 次のハンドラ(デフォルト処理など)へ処理を渡す

event.Skip() について:

event.Skip() を呼ぶと、wxPythonは「このイベントの処理はまだ終わっていないので、他のハンドラ(基底クラスのハンドラやデフォルトの処理)も実行してください」と解釈します。

Veto() をしない場合、event.Skip() を呼んでデフォルト処理(例:ウィンドウが閉じる)を行わせるか、event.Skip(False)(または何も呼ばない)でデフォルト処理も抑制するか、を適切に選ぶ必要があります。

wx.EVT_CLOSE のように Veto() で制御する場合は、Veto() しない限りはデフォルトで閉じられるため、Skip() の呼び出しは必須ではありませんが、イベント処理の連鎖を意識することは重要です。

イベントオブジェクトから情報を取得する

ハンドラが受け取る event オブジェクトからは、様々な情報を取得できます。

  • event.GetEventObject(): イベントを発生させたウィジェットオブジェクトを取得します。
  • event.GetId(): イベントを発生させたウィジェットのIDを取得します。

また、wx.CloseEventwx.BookCtrlEvent のような wx.NotifyEvent のサブクラス(子クラス)は、さらに固有の情報(例:GetSelection())を持っている場合があります。

サンプルコードで学ぶ! wx.NotifyEventの実践

それでは、具体的なコードで wx.NotifyEvent の使い方を見ていきましょう。

例1: ウィンドウのクローズイベント (wx.EVT_CLOSE) を処理する

wx.EVT_CLOSE は、wx.NotifyEvent のサブクラスである wx.CloseEvent に対応します。

ウィンドウが閉じられる直前に発生し、Veto() を使って閉じる動作をキャンセルする典型的な例です。

import wx

class VetoFrame(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title=title)
        
        panel = wx.Panel(self)
        
        # ユーザーが編集したかどうかのダミーフラグ
        self.is_dirty = True 

        # wx.EVT_CLOSE イベントと OnClose メソッドをBind
        # wx.EVT_CLOSE は wx.CloseEvent に対応し、
        # これは wx.NotifyEvent を継承しています。
        self.Bind(wx.EVT_CLOSE, self.OnClose)

        self.SetSize((300, 200))
        self.Centre()
        self.Show()

    def OnClose(self, event: wx.CloseEvent):
        """ ウィンドウが閉じられようとした時の処理 """
        
        # もし編集内容が保存されていなければ (is_dirty == True)
        if self.is_dirty:
            # 確認ダイアログを表示
            dlg = wx.MessageDialog(self, 
                                 "編集内容が保存されていません。\n本当に閉じますか?",
                                 "確認", 
                                 wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
            
            result = dlg.ShowModal()
            dlg.Destroy()
            
            # 「NO」が押された場合
            if result == wx.ID_NO:
                # event.Veto() を呼び出し、ウィンドウが閉じるのを「拒否」する
                event.Veto()
                print("Close event Vetoed!")
                return # 処理を終了
            else:
                # 「YES」が押された場合は Veto() しない
                print("Close event Allowed.")
        
        # Veto() しなかった場合、このハンドラが終了すると
        # デフォルトのクローズ処理が実行される
        
        # 厳密には、ここで event.Skip() を呼ぶと、
        # 基底クラスのデフォルト処理などが連鎖して実行されることが保証されます。
        # ただし、Veto しない場合は Skip() を呼ばなくても閉じるのが一般的です。
        # event.Skip() 

        # Veto() しない場合、フレームを破棄するために Destroy を呼ぶのが
        # 確実な場合もあります(特にこれがメインフレームでない場合)
        # self.Destroy() # <- もし明示的に破棄したい場合

        # Veto()しなかったが、デフォルト処理(Destroy)もさせたくない場合
        # (通常はあまりないケース)
        # event.Skip(False)
        pass


if __name__ == '__main__':
    app = wx.App(False)
    frame = VetoFrame(None, "wx.NotifyEvent Veto() サンプル")
    app.MainLoop()

このコードを実行し、ウィンドウの「閉じる」ボタンを押すと、確認ダイアログが表示されます。「いいえ」を選ぶと event.Veto() が呼ばれ、ウィンドウが閉じないことが確認できます。

例2: ノートブックのページ変更 (wx.EVT_NOTEBOOK_PAGE_CHANGING)

wx.EVT_NOTEBOOK_PAGE_CHANGING は、wx.Notebook(タブ)のページが切り替わる直前に発生するイベントです。

これは wx.NotifyEvent のサブクラスである wx.BookCtrlEvent に対応しており、これも Veto() でページ切り替えをキャンセルできます。

import wx

class VetoNotebookFrame(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title=title)

        panel = wx.Panel(self)
        self.notebook = wx.Notebook(panel)

        # 3つのページ(タブ)を作成
        page1 = wx.Panel(self.notebook)
        page1.SetBackgroundColour(wx.Colour(200, 255, 200)) # 緑
        wx.StaticText(page1, label="これはページ1です。", pos=(20, 20))
        
        page2 = wx.Panel(self.notebook)
        page2.SetBackgroundColour(wx.Colour(255, 200, 200)) # 赤
        wx.StaticText(page2, label="これはページ2です。\nこのページからは移動できません。", pos=(20, 20))

        page3 = wx.Panel(self.notebook)
        page3.SetBackgroundColour(wx.Colour(200, 200, 255)) # 青
        wx.StaticText(page3, label="これはページ3です。", pos=(20, 20))

        self.notebook.AddPage(page1, "ページ 1")
        self.notebook.AddPage(page2, "ページ 2 (Veto!)")
        self.notebook.AddPage(page3, "ページ 3")

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.notebook, 1, wx.EXPAND | wx.ALL, 5)
        panel.SetSizer(sizer)

        # wx.EVT_NOTEBOOK_PAGE_CHANGING イベントをBind
        # これは wx.BookCtrlEvent に対応し、wx.NotifyEvent を継承しています。
        # "CHANGING" (変更中=変更直前) であることに注意
        self.notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self.OnPageChanging)
        
        # 比較用: "CHANGED" (変更後) イベント
        self.notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)


        self.SetSize((400, 300))
        self.Centre()
        self.Show()

    def OnPageChanging(self, event: wx.BookCtrlEvent):
        """ ページが切り替わる直前に呼ばれる """
        
        # GetOldSelection(): 変更前のページインデックス
        # GetSelection()   : 変更先のページインデックス
        old_page_idx = event.GetOldSelection()
        new_page_idx = event.GetSelection()

        print(f"ページ変更中: {old_page_idx} -> {new_page_idx}")

        # もし変更前のページが 1 (ページ2) なら...
        if old_page_idx == 1:
            dlg = wx.MessageDialog(self, 
                                 "このページ (ページ2) からは移動できません!",
                                 "移動不可", 
                                 wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
            dlg.Destroy()
            
            # Veto() を呼び出し、ページ切り替えを「拒否」する
            event.Veto()
            print("Page change Vetoed!")

    def OnPageChanged(self, event: wx.BookCtrlEvent):
        """ ページが切り替わった後に呼ばれる """
        # このイベントは wx.CommandEvent を継承しているため、
        # Veto() は持っていません (すでに変更されてしまっているため)
        print(f"ページ変更完了: {event.GetSelection()}")
        # このイベントは伝播するため、event.Skip() が重要になる
        event.Skip()


if __name__ == '__main__':
    app = wx.App(False)
    frame = VetoNotebookFrame(None, "wx.NotifyEvent (BookCtrlEvent) サンプル")
    app.MainLoop()

このコードでは、「ページ2 (Veto!)」から他のページへ移動しようとすると、OnPageChanging が event.Veto() を呼び出し、ページ切り替えがキャンセルされます。

(EVT_NOTEBOOK_PAGE_CHANGED は wx.CommandEvent のサブクラスであり、変更後に通知され、Vetoできない点も対比として重要です。)

まとめ: wx.NotifyEventを理解してwxPythonを使いこなそう

今回は、wxPythonの wx.NotifyEvent について、その役割と使い方を解説しました。

  • wx.NotifyEvent は「通知」のためのイベントウィジェットの状態変化(閉じる、サイズ変更など)を伝えます。
  • wx.CommandEvent との違いは「伝播」wx.NotifyEvent は親ウィジェットに伝播しません。発生したウィジェット自身で Bind() する必要があります。
  • Veto() が最大の特徴event.Veto() を呼び出すことで、イベントが引き起こすデフォルトの動作(ウィンドウを閉じる、ページを切り替えるなど)を「拒否(キャンセル)」できます。
  • wx.CloseEventwx.BookCtrlEvent は、wx.NotifyEvent を継承した具体的なイベントクラスです。

Veto() の仕組みを理解することは、ユーザーが間違った操作をしたときにデータを保護したり、よりインタラクティブなGUI操作を実現したりするために不可欠です。

wx.NotifyEventwx.CommandEvent の違いを意識し、Veto() を適切に使いこなして、ワンランク上のwxPythonアプリケーション開発を目指しましょう。

コメント

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