PythonでデスクトップGUIアプリケーションを開発できる wxPython。高機能なアプリケーションを作れる反面、ユーザーの操作にきめ細かく対応しようとすると、イベント処理の仕組みを深く理解する必要があります。
その中でも、アプリケーションの「終了処理」は、ユーザーのデータを守るために非常に重要です。
この記事では、wxPythonでウィンドウの「閉じる」動作を制御する鍵となる wx.CloseEvent に焦点を当て、以下の点を初心者にも分かりやすく解説します。
wx.CloseEventの基本的な役割と仕組み- ウィンドウを閉じるのをキャンセルする
Veto()の使い方 - 「本当に閉じますか?」という確認ダイアログの実装方法
Close()とDestroy()の重要な違い
この記事を読み終えれば、ユーザーが誤って「閉じる」ボタンを押しても、編集中のデータを失うことのない、安全で親切なアプリケーションを作る方法が身につくはずです。
本記事で解説するwx.CloseEventの公式ドキュメントはこちらです。
はじめに: アプリを「安全に」終了させる重要性
あなたが作ったアプリケーションにテキストエディタ機能があったとします。ユーザーが長時間かけて文章を入力した後、保存する前にうっかりウィンドウ右上の「✕」ボタン(閉じるボタン)を押してしまったらどうなるでしょうか?
もし何の対策もしていなければ、アプリケーションはそのまま終了し、編集中のデータはすべて失われてしまいます。これは非常に不親切な設計です。
wx.CloseEvent が活躍する具体的なシーン
wx.CloseEvent は、まさにこの問題を解決するために存在します。
- ユーザーが閉じるボタンを押した時
- プログラムが
Close()メソッドを呼び出した時
wx.CloseEvent を捕捉(キャッチ)することで、ウィンドウが閉じる直前に「待った!」をかけ、「編集中のデータが保存されていません。保存しますか?」といった確認ダイアログを表示することが可能になります。
wx.CloseEventとは?
wx.CloseEvent とは、その名の通り、ウィンドウが閉じられようとしていることを「通知」するためのイベントクラスです。
ウィンドウが閉じられる「直前」に発生する通知イベント
重要なのは、これが「閉じた後」ではなく「閉じる直前」に発生するイベントであるという点です。これにより、私たちはその「閉じる」という動作に対して介入するチャンスを得ることができます。
wx.NotifyEvent の子クラス(サブクラス)であること
wx.CloseEvent は、wxPythonのイベントシステムの基盤クラスの一つである wx.NotifyEvent を直接継承(子クラスとして)しています。
wx.NotifyEvent は「通知」を目的としたイベントのグループであり、wx.CloseEvent はその代表格です。また、wx.NotifyEvent は、wxPythonのすべてのイベントの頂点に立つ wx.Event のサブクラスです。 この「wx.NotifyEvent の仲間である」という事実が、Veto() という重要な機能(後述)の土台となっています。(wx.NotifyEventの詳しい記事はこちらです)
イベントタイプ wx.EVT_CLOSE との関係
プログラムで wx.CloseEvent を扱う(Bind する)際は、イベントタイプ wx.EVT_CLOSE を使います。
wx.CloseEvent: イベントそのものを表す「クラス名」。wx.EVT_CLOSE: イベントと処理関数を紐付ける(Bindする)ために使う「イベントタイプ識別子」。
この2つはセットで使われると覚えておきましょう。
wx.CloseEvent の最も重要な機能: Veto()
wx.CloseEvent(そしてその親である wx.NotifyEvent)を理解する上で、最も重要で強力な機能が Veto()(拒否) メソッドです。
Veto() とは? (イベントのキャンセル=ウィンドウを閉じるのを止める)
Veto とは、ラテン語で「私は禁じる」という意味の言葉で、一般的には「拒否権」と訳されます。
wxPythonにおける event.Veto() は、文字通り**「そのイベントが引き起こそうとしているデフォルトの動作(=ウィンドウを閉じること)を拒否する」** という命令です。
wx.EVT_CLOSE のイベントハンドラ内で event.Veto() を呼び出すと、wxPythonは「あ、Veto されたな」と認識し、ウィンドウを閉じる処理を中断します。
なぜ Veto() が必要なのか? (例: 編集中のデータを保存確認)
既にお分かりかと思いますが、これは「はじめに」で挙げた「保存確認」のシナリオを実現するために不可欠です。
- ユーザーが「✕」ボタンを押す。
wx.EVT_CLOSEが発生し、wx.CloseEventがハンドラに渡される。- ハンドラが「保存されていません。閉じますか?」と尋ねる。
- ユーザーが「いいえ(閉じない)」を選ぶ。
- ハンドラが
event.Veto()を呼び出す。 - ウィンドウは閉じられず、ユーザーは編集を続行できる。
このように、Veto() はアプリケーションの状態に応じてデフォルトの動作を制御し、ユーザーの意図しないデータ損失を防ぐためのセーフティネットとして機能します。
Allow() メソッド (Vetoの取り消し ※参考)
Veto() の反対の動作をする Allow() メソッドも存在します。これは、何らかの理由で一度 Veto() されたイベントを、後から「やはり許可する」と変更するためのものですが、wx.CloseEvent の処理では Veto() を呼ぶか呼ばないかで制御するのが一般的なため、Allow() を明示的に呼ぶ機会は少ないです。
CanVeto() メソッド (Vetoが可能か確認する)
また、event.CanVeto() メソッドを使えば、そのイベントが Veto() によるキャンセルをサポートしているかどうかを(理論上は)確認できます。wx.CloseEvent に関しては、これは常に True を返すため、このメソッドを明示的に呼び出す必要は通常ありません。
wx.CloseEvent の基本的な使い方
wx.CloseEvent を処理する流れは、他のwxPythonイベント処理と基本は同じです。Bind() メソッドを使います。
Bind() メソッドで wx.EVT_CLOSE とハンドラを紐付ける
イベントを処理するには、イベントを発生させるウィジェット(通常は wx.Frame や wx.Dialog)で Bind() メソッドを呼び出します。 Bind() メソッドは、これらウィジェットクラスが継承している wx.EvtHandler クラスによって提供される、イベント処理の根幹となる機能です。
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, title):
super().__init__(parent, title=title)
# ... 他のウィジェット初期化 ...
# wx.EVT_CLOSE イベントと self.OnClose メソッドを紐付ける
self.Bind(wx.EVT_CLOSE, self.OnClose)
# ... (以下に OnClose メソッドを定義)wx.NotifyEvent の仲間である wx.CloseEvent は、イベントが発生したウィジェット(この場合は MyFrame 自身)で Bind する必要があります。
イベントハンドラ(コールバック関数)の定義
Bind() に指定した関数(この例では self.OnClose)を定義します。この関数は、イベント発生時にwxPythonから呼び出され、引数として wx.CloseEvent オブジェクト(通常は event という変数名で受け取る)を受け取ります。
def OnClose(self, event: wx.CloseEvent):
# ここにウィンドウが閉じられようとした時の処理を書く
# もし閉じたくない条件なら...
if some_condition_to_stop:
event.Veto() # 拒否する
else:
# 閉じても良い場合
# Veto() を呼ばなければ、デフォルトの処理(閉じる)が実行される
# 明示的にデフォルト処理を続行させるために event.Skip() を呼ぶ
event.Skip() イベントオブジェクト event の受け取り方
引数 event は、wx.CloseEvent クラスのインスタンスです。このオブジェクトが Veto() や CanVeto() といったメソッドを持っています。
event.Skip() については少し注意が必要です。event.Skip() を呼ぶと、wxPythonに対して「このイベントの処理はまだ終わっていないので、デフォルトの処理(ウィンドウを閉じて破棄する)も実行してください」と伝えます。
Veto() を呼ばず、Skip() も呼ばない(または Skip(False) を呼ぶ)と、デフォルトの「閉じる」動作も実行されない可能性があります。 したがって、Veto() しない(=閉じることを許可する)場合は、event.Skip() を呼び出してデフォルトの動作を確実に行わせるのが安全な実装です。
実践!サンプルコードで学ぶ「閉じる確認」の実装
それでは、これまでの知識を総動員して、編集状態(フラグ)を持ち、閉じる際に確認ダイアログを表示するサンプルコードを見てみましょう。
wx.EVT_CLOSE を使った確認ダイアログの表示
ここでは、wx.MessageDialog という、標準的な確認ダイアログを表示するクラスを利用します。
import wx
class ConfirmOnCloseFrame(wx.Frame):
""" 閉じる際に確認ダイアログを出すフレーム """
def __init__(self, parent, title):
# wx.Frame は wx.Window のサブクラスです
super().__init__(parent, title=title)
panel = wx.Panel(self)
# ユーザーが何か編集したかどうかのフラグ
# 本来はテキストコントロールの変更などで
# このフラグをTrueにします
self.is_dirty = False
# ダミーのテキスト入力(ここで編集すると is_dirty が True になる)
self.text_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE)
self.text_ctrl.Bind(wx.EVT_TEXT, self.OnTextChange)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.text_ctrl, 1, wx.EXPAND | wx.ALL, 5)
panel.SetSizer(sizer)
# wx.EVT_CLOSE イベントと OnClose メソッドをBindします
# Bindメソッドは wx.EvtHandler から継承されています
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.SetSize((400, 300))
self.Centre()
self.Show()
def OnTextChange(self, event):
""" テキストが変更されたらフラグを立てる """
self.is_dirty = True
# 一度Trueになったら、このハンドラのBindingを解除しても良い
# self.text_ctrl.Unbind(wx.EVT_TEXT)
def OnClose(self, event: wx.CloseEvent):
""" wx.EVT_CLOSE のハンドラ """
# 編集フラグが立っていない (is_dirty == False) なら、
# 何もせず閉じる(Veto() せず、Skip() する)
if not self.is_dirty:
print("編集されていないので、そのまま閉じます。")
event.Skip() # デフォルトの閉じる処理を実行させる
return
# --- ここからが is_dirty == True の場合の処理 ---
print("編集されています!確認ダイアログを表示します。")
# wx.MessageDialog を使って確認する
dlg = wx.MessageDialog(self,
"編集内容が保存されていません。\n本当に閉じますか?",
"閉じる確認",
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
# ダイアログを表示し、ユーザーの応答(wx.ID_YES または wx.ID_NO)を取得
result = dlg.ShowModal()
# ダイアログは使い終わったら破棄する
dlg.Destroy()
# ユーザーが「YES (はい)」を押した場合
if result == wx.ID_YES:
print("ユーザーが「はい」を選択。ウィンドウを閉じます。")
# 閉じることを許可する
# Veto() を呼ばず、event.Skip() を呼んで
# デフォルトの閉じる処理(ウィンドウの破棄)を実行させる
event.Skip()
else:
# ユーザーが「NO (いいえ)」を押した場合
print("ユーザーが「いいえ」を選択。ウィンドウを閉じるのを Veto します。")
# event.Veto() を呼び出し、ウィンドウが閉じるのを「拒否」する
event.Veto()
# アプリケーション実行部分
if __name__ == '__main__':
app = wx.App(False)
frame = ConfirmOnCloseFrame(None, "wx.CloseEvent Veto() サンプル")
app.MainLoop()このコードを実行し、テキストエリアに何か入力(is_dirty が True になる)してから「✕」ボタンを押すと、確認ダイアログが表示されます。「いいえ」を押すと event.Veto() が呼ばれてウィンドウが閉じず、「はい」を押すと event.Skip() が呼ばれて(Veto() が呼ばれず)、ウィンドウが正常に閉じることが確認できます。
注意点: Close() と Destroy() の違い
wx.CloseEvent を扱う際、ウィンドウを閉じる2つのメソッド、Close() と Destroy() の違いを理解しておくことが非常に重要です。これらはどちらも wx.Window クラス(およびその子クラスである wx.Frame や wx.Dialog)に存在するメソッドです。
frame.Close() は wx.CloseEvent を発生させる (推奨)
window.Close() を呼び出すと、wx.CloseEvent が生成され、wx.EVT_CLOSE ハンドラが(もしBindされていれば)呼び出されます。
これは、ユーザーが「✕」ボタンを押した時と全く同じ動作です。 したがって、プログラム側からウィンドウを閉じさせたい場合(例:「ファイル」→「閉じる」メニュー)は、Close() を使うのが「行儀の良い」方法です。これにより、前述の「保存確認」のロジックが正しく動作します。
frame.Destroy() は wx.CloseEvent を発生させない (強制終了)
一方、window.Destroy() を呼び出すと、wx.CloseEvent は一切発生せず、イベントハンドラも呼び出されません。 Destroy() は、有無を言わさずそのウィンドウ(と、その子が持つすべてのリソース)を即座に破棄(強制終了)します。
どちらを呼び出すべきか?
Close(): ユーザーに「閉じてよいか」を尋ねる可能性がある場合。通常の終了処理。Destroy(): 「保存確認」などをすべて終え、アプリケーションが「本当に」終了する最後の瞬間や、wx.CloseEventのハンドラ内で「閉じる」と確定した後のデフォルト処理として(event.Skip()によって間接的に)呼び出されます。
wx.EVT_CLOSE ハンドラの中で「はい」が押されたからといって、self.Destroy() を直接呼び出すべきではありません。 イベント処理の最中にイベントの発生源を Destroy() すると、予期せぬ動作を引き起こす可能性があります。 event.Skip() を呼び、wxPythonのデフォルトの処理に任せるのが正解です。
まとめ: wx.CloseEvent をマスターして親切なGUIを作ろう
今回は、wxPythonの wx.CloseEvent と、それを使ったウィンドウの安全な終了処理について解説しました。
wx.CloseEventは、ウィンドウが閉じる「直前」に発生するwx.NotifyEventの仲間です。wx.EVT_CLOSEでBind()します。- 最大の特徴は
event.Veto()で、これを呼ぶとウィンドウが閉じるのをキャンセルできます。 Veto()を利用して、「保存確認」などのユーザー保護機能を実装できます。Close()はwx.CloseEventを発生させ、Destroy()はさせません。
ユーザーの意図しないデータ損失を防ぐことは、優れたアプリケーションの必須条件です。wx.CloseEvent の仕組みを正しく理解し、Veto() を使いこなして、より安全でユーザーフレンドリーなwxPythonアプリケーションを構築してください。


コメント