リンク

2017年6月27日火曜日

wxPythonでダイアログの多重起動を防止する

突然ですが、Excelを使ったことはありますか?
圧倒的大多数の方はYESだと思います。

Excelの機能の一つに「検索」があります。
「Ctrl」 + 「F」キーでダイアログが表示されますね。
あのダイアログ、いくつも同時に表示することはできません。
でも表示させたままで他の作業をすることはできます。

よくよく考えたら不思議。
どうなっているんだろう?

今回はそんなお話です。

環境
  • macOS Sierra 10.12.5
  • python 3.4
  • wxPython Phoenix 4.0.0

悪いスクリプト
まずは悪い見本のスクリプトを記述します。
# -*- coding: UTF-8 -*-

import wx

class App(wx.Frame):
    """ GUI """
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(300, 300))

        p = wx.Panel(self, wx.ID_ANY)

        button = wx.Button(p, wx.ID_ANY, 'click')
        button.Bind(wx.EVT_BUTTON, self.show_dialog)

        layout = wx.BoxSizer(wx.VERTICAL)
        layout.Add(button, flag=wx.ALL, border=10)
        p.SetSizer(layout)

        self.Show()

    def show_dialog(self, event):
        dialog = CustomDialog(self)
        dialog.Show()


class CustomDialog(wx.Dialog):
    """ カスタムダイアログ """

    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'だいあろぐ', size=(150, 100))

        p = wx.Panel(self, wx.ID_ANY)

        label = wx.StaticText(p, wx.ID_ANY, 'さんぷる だいあろぐ')

        layout = wx.BoxSizer(wx.VERTICAL)
        layout.Add(label, flag=wx.ALL, border=10)
        p.SetSizer(layout)

        self.Centre()


app = wx.App()
App(None, wx.ID_ANY, 'タイトル')
app.MainLoop()

<実行結果>


解 説
悪いスクリプトでは、ボタンのクリックイベント内で別途作成したカスタムダイアログ・クラスを呼び出しています。
Excelの検索ダイアログのように、ダイアログを表示しながら他の作業ができるようにするにはモードレス表示にする必要がありますので「.ShowModal()」ではなく「.Show()」とする必要があります。

この場合、多重起動を制御する文を記述していませんので、ボタンをクリックする度にカスタムダイアログが表示されてしまいます。

良いスクリプト
# -*- coding: UTF-8 -*-

import wx

class App(wx.Frame):
    """ GUI """
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(300, 300), style=wx.DEFAULT_FRAME_STYLE)

        p = wx.Panel(self, wx.ID_ANY)

        button = wx.Button(p, wx.ID_ANY, 'click')
        button.Bind(wx.EVT_BUTTON, self.show_dialog)

        layout = wx.BoxSizer(wx.VERTICAL)
        layout.Add(button, flag=wx.ALL, border=10)
        p.SetSizer(layout)

        self.Show()

    def show_dialog(self, event):

        if self.FindWindowByName('だいあろぐ') is None:
            dialog = CustomDialog(self)
            dialog.Show()
        else:
            dialog = wx.MessageDialog(self, 'すでに表示されています', 'エラー', wx.ICON_ERROR) 
            dialog.ShowModal() 
            dialog.Destroy()


class CustomDialog(wx.Dialog):
    """ カスタムダイアログ """

    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'だいあろぐ', size=(150, 100))

        p = wx.Panel(self, wx.ID_ANY)

        label = wx.StaticText(p, wx.ID_ANY, 'さんぷる だいあろぐ')

        layout = wx.BoxSizer(wx.VERTICAL)
        layout.Add(label, flag=wx.ALL, border=10)
        p.SetSizer(layout)

        self.Centre()


app = wx.App()
App(None, wx.ID_ANY, 'タイトル')
app.MainLoop()

<実行結果> ↓ ↓ ↓




解 説
良いスクリプトと悪いスクリプトの違いは、ボタンのイベントハンドラであるshow_dialogメソッドの内容です。

悪いスクリプトではカスタムダイアログをすぐにインスタンス化→Showしていますが、良いスクリプトではFindWindowByNameメソッドによる条件分岐を行なっています。

FindWindowByNameメソッドは引数に与えられた文字列を検索し、それに一致するタイトルをもつウィンドウがなければ「None」を返すというものです。
(検索対象は内部から派生したウィンドウのみ。外部アプリケーションは対象外。)

今回の例ではカスタムダイアログのタイトルが「だいあろぐ」に設定されていますので、 FindWindowByNameメソッドの引数も「だいあろぐ」にしています。

これにより、すでに「だいあろぐ」というタイトルのダイアログが表示されている場合、FindWindowByNameメソッドはNone以外を返す(正確にはwx.Windowを返す)ので、ダイアログの多重起動を防ぐことができます。

まとめ
ダイアログはモーダル表示させることの方が多いかもしれません。
しかし、Excelの検索ダイアログのようにモードレス表示させる必要がある場合、今回ご紹介した内容を実装することは必要になると思いますので、今は必要なくとも、覚えておいて損はないと思います。

参考
https://wxpython.org/Phoenix/docs/html/wx.functions.html#wx.FindWindowByName


3 件のコメント:

  1. 途中から別のダイアログやフレームをアクティブ(最前面)に表示させたいのですが、できますか?

    返信削除
    返信
    1. コメントありがとうございます。
      ご質問の件ですが、「常に最前面に表示させる」ことはできるようですが、「途中から最前面に表示させる」ことはできないようです。
      参考↓
      https://wxpython.org/Phoenix/docs/html/wx.Dialog.html

      お力になれず、申し訳ありませんm(_ _)m

      削除
    2. こちらこそ、難しい質問をして申し訳ございませんでした。

      いつも分かりやすい説明をありがとうございます!!

      削除