試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

PyGTKでダイアログを出しつつ親ウィンドウも操作できるようにする

ダイアログと親ウィンドウについて

ダイアログを作成すると、そのダイアログに対するメインループがメインウィンドウのループの中で開始され、多重のメインループになる。このとき、ダイアログ側に対する操作は受け付けるが、メインウィンドウ側をいじることはできない。

#! /usr/bin/python
# -*- encoding: utf-8 -*-

import sys
try:
  import pygtk
  pygtk.require('2.0')
except:
  pass
try:
  import gtk
except:
  print >> sys.stderr, 'Error: PyGTK is not installed'
  sys.exit(1)


class TestDialog(gtk.Dialog):
  """
  テスト用ダイアログ
  """
  def __init__(self, *args, **kwargs):
    gtk.Dialog.__init__(self, *args, **kwargs)
    self.label1 = gtk.Label('Item1')
    self.label2 = gtk.Label('Item2')
    self.entry1 = gtk.Entry()
    self.entry1.set_activates_default(True)     # Enterで既定のレスポンス
    self.entry1.grab_focus()                    # 最初にフォーカス
    self.entry2 = gtk.Entry()
    self.entry2.set_activates_default(True)
    self.table = gtk.Table(2, 2)
    self.table.attach(self.label1, 0, 1, 0, 1)  # X:0-1 Y:0-1 左上
    self.table.attach(self.entry1, 1, 2, 0, 1)  # X:1-2 Y:0-1 右上
    self.table.attach(self.label2, 0, 1, 1, 2)  # X:0-1 Y:1-2 左下
    self.table.attach(self.entry2, 1, 2, 1, 2)  # X:1-2 Y:1-2 右下
    self.set_default_response(gtk.RESPONSE_OK)  # 既定のレスポンスをOKに
    self.vbox.add(self.table)
    self.vbox.show_all()                        # 子は全て表示可能にしておく
  def get_texts(self):
    """
    入力された文字列2つをタプルにして返す
    """
    return (self.entry1.get_text(), self.entry2.get_text())

class MainWindow(gtk.Window):
  """
  メインウィンドウ
  """
  def __init__(self, *args, **kwargs):
    gtk.Window.__init__(self, *args, **kwargs)
    # ショートカットキー(アクセラレータ)
    self.accelgroup = gtk.AccelGroup()
    self.add_accel_group(self.accelgroup)
    # メニュー項目
    self.item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.accelgroup)
    self.menu_file = gtk.Menu()
    self.menu_file.add(self.item_quit)
    self.item_file = gtk.MenuItem('_File')
    self.item_file.set_submenu(self.menu_file)
    self.menubar = gtk.MenuBar()
    self.menubar.append(self.item_file)
    # ボタン
    self.button = gtk.Button('Run dialog')
    # レイアウト用コンテナ
    self.vbox = gtk.VBox()
    self.vbox.pack_start(self.menubar, expand=False, fill=False)
    self.vbox.pack_start(self.button)
    # シグナル
    self.connect('delete_event', gtk.main_quit)
    self.item_quit.connect('activate', gtk.main_quit)
    self.button.connect('clicked', self.on_button_clicked)
    # ウィンドウ
    self.add(self.vbox)
    self.set_size_request(150, 100)
  def on_button_clicked(self, widget):
    """
    ボタンがクリックされたらダイアログを開く
    """
    dlg = TestDialog(title='Test dialog', flags=gtk.DIALOG_DESTROY_WITH_PARENT, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK))
    resid = dlg.run()
    dlg.destroy()
    if resid == gtk.RESPONSE_OK:
      print 'Item1:%s\nItem2:%s' % dlg.get_texts()  # 端末に入力値を表示する

class PyGTKDialogTest:
  """
  ダイアログのテスト
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


if __name__ == '__main__':
  app = PyGTKDialogTest()
  app.main()

ボタンを押すとダイアログが出てきてOKを押すと端末に入力文字列が表示される。ダイアログが出ているときにはメインウィンドウは操作できない。

多重のメインループにしないでダイアログを表示する

ダイアログ側のメインループはgtk.Dialogオブジェクトのメンバ関数run()を呼ぶと開始される。これの代わりに親ウィンドウのメインループを利用する書き方をすることで独立したダイアログが出せる。
ここではgtk.Dialogクラスを継承した子クラスを用いて下のようにすることにする。

  • ダイアログ初期化時に(gtk.Dialogオブジェクトのコンストラクタ引数として)「parent=[親ウィンドウのオブジェクト], flags=gtk.DIALOG_DESTROY_WITH_PARENT」を付ける・かつ、フラグにgtk.DIALOG_MODALは付けない
  • 子クラスの中で「response」シグナルを別途用意のハンドラに結びつける
  • クラスのメンバ変数として「レスポンスされたかどうか」の真偽値を用意し、ハンドラの中でこれをTrueにしつつ、再定義(オーバーライド)版のrun()でこの値を監視しつつメインループを回す処理(gtk.main_iteration()を繰り返し呼び出す)をする・ただし親ウィンドウが閉じられるなどしてgtk.main_quit()が呼ばれてgtk.main_iteration()がTrueを返した場合にもそこで繰り返しを抜けるようにしないとアプリケーションが終了できなくなる

下のようにすると、ダイアログが出ているときにメインウィンドウが操作できつつ、メインウィンドウを閉じたり終了したりするとダイアログも一緒に消える。

#! /usr/bin/python
# -*- encoding: utf-8 -*-

import sys
try:
  import pygtk
  pygtk.require('2.0')
except:
  pass
try:
  import gtk
except:
  print >> sys.stderr, 'Error: PyGTK is not installed'
  sys.exit(1)


class TestDialog(gtk.Dialog):
  """
  テスト用ダイアログ
  """
  def __init__(self, *args, **kwargs):
    gtk.Dialog.__init__(self, *args, **kwargs)
    self.label1 = gtk.Label('Item1')
    self.label2 = gtk.Label('Item2')
    self.entry1 = gtk.Entry()
    self.entry1.set_activates_default(True)     # Enterで既定のレスポンス
    self.entry1.grab_focus()                    # 最初にフォーカス
    self.entry2 = gtk.Entry()
    self.entry2.set_activates_default(True)
    self.table = gtk.Table(2, 2)
    self.table.attach(self.label1, 0, 1, 0, 1)  # X:0-1 Y:0-1 左上
    self.table.attach(self.entry1, 1, 2, 0, 1)  # X:1-2 Y:0-1 右上
    self.table.attach(self.label2, 0, 1, 1, 2)  # X:0-1 Y:1-2 左下
    self.table.attach(self.entry2, 1, 2, 1, 2)  # X:1-2 Y:1-2 右下
    self.set_default_response(gtk.RESPONSE_OK)  # 既定のレスポンスをOKに
    self.vbox.add(self.table)
    self.vbox.show_all()                        # 子は全て表示可能にしておく
    self.connect('response', self.on_response)  # 再定義版run()のために設定
  def on_response(self, widget, resid):
    """
    レスポンス時にそれをrun()側に知らせる
    """
    self.responsed = True
    self.resid = resid
  def run(self):
    """
    再定義(オーバーライド)したメンバ関数
    入れ子のメインループを使用しないでダイアログを表示
    """
    self.responsed = False
    self.resid = None
    self.show()
    # 戻り値がレスポンスIDになるようにするため
    # 未レスポンスの状態ではメインループを回す
    # ただしgtk.main_quit()が呼ばれてTrueを返すときには抜けるようにする
    while not self.responsed and not gtk.main_iteration():
      pass
    # 下の書き方だとダイアログが開いている間はアプリケーションが終了できない
#    while not self.responsed:
#      gtk.main_iteration()
    return self.resid  # レスポンスが行われたらそのIDを返す
  def get_texts(self):
    """
    入力された文字列2つをタプルにして返す
    """
    return (self.entry1.get_text(), self.entry2.get_text())

class MainWindow(gtk.Window):
  """
  メインウィンドウ
  """
  def __init__(self, *args, **kwargs):
    gtk.Window.__init__(self, *args, **kwargs)
    # ショートカットキー(アクセラレータ)
    self.accelgroup = gtk.AccelGroup()
    self.add_accel_group(self.accelgroup)
    # メニュー項目
    self.item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.accelgroup)
    self.menu_file = gtk.Menu()
    self.menu_file.add(self.item_quit)
    self.item_file = gtk.MenuItem('_File')
    self.item_file.set_submenu(self.menu_file)
    self.menubar = gtk.MenuBar()
    self.menubar.append(self.item_file)
    # ボタン
    self.button = gtk.Button('Run dialog')
    # レイアウト用コンテナ
    self.vbox = gtk.VBox()
    self.vbox.pack_start(self.menubar, expand=False, fill=False)
    self.vbox.pack_start(self.button)
    # シグナル
    self.connect('delete_event', gtk.main_quit)
    self.item_quit.connect('activate', gtk.main_quit)
    self.button.connect('clicked', self.on_button_clicked)
    # ウィンドウ
    self.add(self.vbox)
    self.set_size_request(150, 100)
  def on_button_clicked(self, widget):
    """
    ボタンがクリックされたらダイアログを開く
    """
    widget.set_sensitive(False)  # 多重に押せないようにする
    dlg = TestDialog(title='Test dialog', parent=self, flags=gtk.DIALOG_DESTROY_WITH_PARENT, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK))
    resid = dlg.run()
    dlg.destroy()
    if resid == gtk.RESPONSE_OK:
      print 'Item1:%s\nItem2:%s' % dlg.get_texts()  # 端末に入力値を表示する
    widget.set_sensitive(True)  # 戻す

class PyGTKDialogTest2:
  """
  ダイアログのテスト2
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


if __name__ == '__main__':
  app = PyGTKDialogTest2()
  app.main()

YouTubeに動作イメージとして動画をアップロードした。

参考URL: