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

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

PyGTKでタブ(ノートブック)に「閉じる」ボタンを付ける(後半)

PyGTKでタブ(ノートブック)に「閉じる」ボタンを付ける(前半)」の続き。

タブの「閉じる」ボタンのクリックをタブのページ全体のイベントとして処理する

タブの「閉じる」ボタンをクリックしたときにそのページ全体のイベントとして処理できるようにすると便利で

  • ページ全体を1つのクラスとする
  • 「閉じる」ボタンが押されたときの「ページを閉じる」イベント(GObjectシグナル)を定義
  • 「閉じる」ボタンが押されたときのハンドラにおいてその(「ページを閉じる」イベントの)シグナルを起こす

として、かつ、このクラスの利用側で

  • 「ページを閉じる」イベントとハンドラを関連付ける
  • そのハンドラの中でページ全体のGUI部品の破棄処理(gtk.Widgetクラスメンバ関数destroy())を記述

とすることで、押された「閉じる」ボタンを含んだタブを閉じる(消す)ようにできる。
ユーザ定義のGObjectシグナルは以前扱った「PyGObjectで gobject.GObjectクラスを継承してGObjectプロパティを用いる(前半)」の参考ページをここでも参考にして

class TextViewTab(gtk.ScrolledWindow):
  __gsignals__ = \
  {
    'closed' : (gobject.SIGNAL_RUN_FIRST,  # ハンドラが呼ばれるタイミング
                                           # シグナル発行ステージの段階を指定
                                           # gobject.SIGNAL_RUN_FIRSTでは1段階目
                                           # gobject.SIGNAL_RUN_LAST では3段階目
                gobject.TYPE_NONE,         # ハンドラの戻り値 or gobject.TYPE_NONE
                ())                        # ハンドラの追加引数タプル
  }
  ...

のようにした。「__gsignals__」という辞書にシグナル名とハンドラの設定を記述する形となる。今回は単純なシグナルなのでこのようになった。

例1

前半と本記事のこれまでの内容を踏まえると下のような例ができる。

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

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


class TextViewTab(gtk.ScrolledWindow):
  """
  テキストビューを含んだタブ
  """
  # ユーザ定義のプロパティとシグナルについては
  # http://www.pygtk.org/articles/subclassing-gobject/sub-classing-gobject-in-python.htm
  # を参照
  # ユーザ定義プロパティ
  __gproperties__ = \
  {
    'label' : (gobject.TYPE_OBJECT,
               'tablabel',
               'gtk.Label for this tab',
               gobject.PARAM_READABLE),
  }
  # ユーザ定義シグナル
  # http://web.archive.org/web/20071118225440/www.gnome.gr.jp/docs/glib-2.4.x-refs/gobject/html/gobject-Signals.html#g-signal-new
  # http://web.archive.org/web/20071118225440/www.gnome.gr.jp/docs/glib-2.4.x-refs/gobject/html/gobject-Signals.html#GSignalFlags
  __gsignals__ = \
  {
    'closed' : (gobject.SIGNAL_RUN_FIRST,  # ハンドラが呼ばれるタイミング
                                           # シグナル発行ステージの段階を指定
                                           # gobject.SIGNAL_RUN_FIRSTでは1段階目
                                           # gobject.SIGNAL_RUN_LAST では3段階目
                gobject.TYPE_NONE,         # ハンドラの戻り値 or gobject.TYPE_NONE
                ())                        # ハンドラの追加引数タプル
  }
  __number = 0  # 表示に用いるタブ番号
  def __init__(self):
    gtk.ScrolledWindow.__init__(self, hadjustment=None, vadjustment=None)
    TextViewTab.__number += 1
    # テキストビューとバッファ
    self.__textbuf = gtk.TextBuffer()
    self.__textview = gtk.TextView(self.__textbuf)
    self.__textview.props.wrap_mode = gtk.WRAP_CHAR
    self.add(self.__textview)
    # スクロール
    self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
    # ラベルテキストとボタン
    self.__label = gtk.Label('Tab %d' % TextViewTab.__number)
    self.__button_close = gtk.Button()
    self.__button_close.props.image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
    self.__button_close.props.relief = gtk.RELIEF_NONE  # ボタンのスタイル
    self.__button_close.connect('clicked', self.__on_button_close_clicked)
    # 横に並べる
    self.__hbox = gtk.HBox()
    self.__hbox.pack_start(self.__label)
    self.__hbox.pack_start(self.__button_close, expand=False, fill=False)
    self.__hbox.show_all()
    # ツールチップ
    self.__label.props.tooltip_text = self.__label.props.label
    self.__button_close.props.tooltip_text = 'Close this tab'
  def __on_button_close_clicked(self, widget):
    """
    ボタンがクリックされた
    """
    dlg = gtk.MessageDialog(type=gtk.MESSAGE_QUESTION, message_format='Do you really want to close "%s" ?' % self.__label.props.label)
    dlg.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
    resid = dlg.run()
    dlg.destroy()
    if resid == gtk.RESPONSE_CLOSE:
      # もし閉じる場合はユーザ定義シグナル「closed」を出すことにする
      self.emit('closed')
  def do_get_property(self, property):
    if property.name == 'label':
      return self.__hbox

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.__item_new = gtk.ImageMenuItem(gtk.STOCK_NEW, self.__accelgroup)
    self.__menu_file = gtk.Menu()
    self.__menu_file.prepend(self.__item_quit)
    self.__menu_file.prepend(self.__item_new)
    self.__item_file = gtk.MenuItem('_File')
    self.__item_file.props.submenu = self.__menu_file
    self.__menubar = gtk.MenuBar()
    self.__menubar.append(self.__item_file)
    # ノートブック
    self.__nb = gtk.Notebook()
    self.__nb.props.tab_border = 0
    self.__nb.props.tab_hborder = 0
    self.__nb.props.tab_vborder = 0
    self.__nb.props.scrollable = True
    # レイアウト用コンテナ
    self.__vbox = gtk.VBox()
    self.__vbox.pack_start(self.__menubar, expand=False, fill=False)
    self.__vbox.pack_start(self.__nb)
    # シグナル
    self.connect('delete_event', gtk.main_quit)
    self.__item_quit.connect('activate', gtk.main_quit)
    self.__item_new.connect('activate', self.__on_item_new_activate)
    # ウィンドウ
    self.add(self.__vbox)
    self.set_size_request(350, 300)
  def __on_item_new_activate(self, widget):
    """
    タブを追加
    """
    tab = TextViewTab()
    tab.show_all()
    tab.connect('closed', self.__on_tab_closed)
    self.__nb.append_page(tab, tab_label=tab.props.label)
  def __on_tab_closed(self, widget):
    """
    タブが閉じられた
    """
    print '__on_tab_closed()'
    # destroy()はその子ウィジェット全てを含めて消す
    # ここではTextViewTabオブジェクトが消える
    widget.destroy()

class NotebookCloseButtonTest:
  """
  ノートブックの閉じるボタンのテスト
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


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

geditなどと比べると、タブの高さやボタンの大きさの違いが目につくが、これは後述の方法により解決する。

タブの表示

タブの「閉じる」ボタンを押したときの確認ダイアログ

できるだけ小さなタブとボタンを作る

geditなどを参考にしたところ、下のような方法によってタブとボタンを小さくできることが分かった。一部の要領は先に「PyGTKでツリービューの各テキストセルに対してスタイルを適用する(後半)」の中で扱っている。

  • gtk.Widgetクラスのスタイルプロパティfocus-padding,focus-line-widthをともに0にし、同様にxthickness,ythicknessの値も0にした「閉じる」ボタン用スタイルを定義
  • 「閉じる」ボタンのオブジェクトのGObjectプロパティnameをそのスタイルのものに変更・もしくは(gtk.Widgetクラスの)set_name()を呼ぶ
  • 「閉じる」ボタンのオブジェクトの「style-set」シグナルのハンドラでgtk.icon_size_lookup_for_settings()の戻り値タプルの幅と高さをそれぞれ2足したものを引数にしてメンバ関数set_size_request()を呼ぶ

スタイルプロパティについては「PyGTKでツリービューの各テキストセルに対してスタイルを適用する(後半)」を参照。

例2

例1をもとに、タブ/ボタンをできるだけ小さくしたものとなる。

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

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


class TextViewTab(gtk.ScrolledWindow):
  """
  テキストビューを含んだタブ
  """
  # ユーザ定義のプロパティとシグナルについては
  # http://www.pygtk.org/articles/subclassing-gobject/sub-classing-gobject-in-python.htm
  # を参照
  # ユーザ定義プロパティ
  __gproperties__ = \
  {
    'label' : (gobject.TYPE_OBJECT,
               'tablabel',
               'gtk.Label for this tab',
               gobject.PARAM_READABLE),
  }
  # ユーザ定義シグナル
  # http://web.archive.org/web/20071118225440/www.gnome.gr.jp/docs/glib-2.4.x-refs/gobject/html/gobject-Signals.html#g-signal-new
  # http://web.archive.org/web/20071118225440/www.gnome.gr.jp/docs/glib-2.4.x-refs/gobject/html/gobject-Signals.html#GSignalFlags
  __gsignals__ = \
  {
    'closed' : (gobject.SIGNAL_RUN_FIRST,  # ハンドラが呼ばれるタイミング
                                           # シグナル発行ステージの段階を指定
                                           # gobject.SIGNAL_RUN_FIRSTでは1段階目
                                           # gobject.SIGNAL_RUN_LAST では3段階目
                gobject.TYPE_NONE,         # ハンドラの戻り値 or gobject.TYPE_NONE
                ())                        # ハンドラの追加引数タプル
  }
  __number = 0  # 表示に用いるタブ番号
  def __init__(self):
    gtk.ScrolledWindow.__init__(self, hadjustment=None, vadjustment=None)
    TextViewTab.__number += 1
    # テキストビューとバッファ
    self.__textbuf = gtk.TextBuffer()
    self.__textview = gtk.TextView(self.__textbuf)
    self.__textview.props.wrap_mode = gtk.WRAP_CHAR
    self.add(self.__textview)
    # スクロール
    self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
    # ラベルテキストとボタン
    self.__label = gtk.Label('Tab %d' % TextViewTab.__number)
    self.__button_close = gtk.Button()
    self.__button_close.props.image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
    self.__button_close.props.relief = gtk.RELIEF_NONE  # ボタンのスタイル
    self.__button_close.props.name = 'small-close-button'
    self.__button_close.connect('clicked', self.__on_button_close_clicked)
    self.__button_close.connect('style-set', self.__on_button_close_style_set)
    # 横に並べる
    self.__hbox = gtk.HBox()
    self.__hbox.pack_start(self.__label)
    self.__hbox.pack_start(self.__button_close, expand=False, fill=False)
    self.__hbox.show_all()
    # ツールチップ
    self.__label.props.tooltip_text = self.__label.props.label
    self.__button_close.props.tooltip_text = 'Close this tab'
  def __on_button_close_style_set(self, widget, prev_style):
    (width, height) = gtk.icon_size_lookup_for_settings(self.__button_close.get_settings(), gtk.ICON_SIZE_MENU)
    self.__button_close.set_size_request(width + 2, height + 2)
  def __on_button_close_clicked(self, widget):
    """
    ボタンがクリックされた
    """
    dlg = gtk.MessageDialog(type=gtk.MESSAGE_QUESTION, message_format='Do you really want to close "%s" ?' % self.__label.props.label)
    dlg.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
    resid = dlg.run()
    dlg.destroy()
    if resid == gtk.RESPONSE_CLOSE:
      # もし閉じる場合はユーザ定義シグナル「closed」を出すことにする
      self.emit('closed')
  def do_get_property(self, property):
    if property.name == 'label':
      return self.__hbox

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.__item_new = gtk.ImageMenuItem(gtk.STOCK_NEW, self.__accelgroup)
    self.__menu_file = gtk.Menu()
    self.__menu_file.prepend(self.__item_quit)
    self.__menu_file.prepend(self.__item_new)
    self.__item_file = gtk.MenuItem('_File')
    self.__item_file.props.submenu = self.__menu_file
    self.__menubar = gtk.MenuBar()
    self.__menubar.append(self.__item_file)
    # ノートブック
    self.__nb = gtk.Notebook()
    self.__nb.props.tab_border = 0
    self.__nb.props.tab_hborder = 0
    self.__nb.props.tab_vborder = 0
    self.__nb.props.scrollable = True
    # レイアウト用コンテナ
    self.__vbox = gtk.VBox()
    self.__vbox.pack_start(self.__menubar, expand=False, fill=False)
    self.__vbox.pack_start(self.__nb)
    # シグナル
    self.connect('delete_event', gtk.main_quit)
    self.__item_quit.connect('activate', gtk.main_quit)
    self.__item_new.connect('activate', self.__on_item_new_activate)
    # ウィンドウ
    self.add(self.__vbox)
    self.set_size_request(350, 300)
  def __on_item_new_activate(self, widget):
    """
    タブを追加
    """
    tab = TextViewTab()
    tab.show_all()
    tab.connect('closed', self.__on_tab_closed)
    self.__nb.append_page(tab, tab_label=tab.props.label)
  def __on_tab_closed(self, widget):
    """
    タブが閉じられた
    """
    print '__on_tab_closed()'
    # destroy()はその子ウィジェット全てを含めて消す
    # ここではTextViewTabオブジェクトが消える
    widget.destroy()

class NotebookCloseButtonTest2:
  """
  ノートブックの閉じるボタンのテスト2
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    # タブの閉じるボタンのスタイル定義・geditと同じ要領
    gtk.rc_parse_string('''
style "small-close-button-style"
{
  GtkWidget::focus-padding = 0
  GtkWidget::focus-line-width = 0
  xthickness = 0
  ythickness = 0
}
widget "*.small-close-button" style "small-close-button-style"
''')
    win = MainWindow()
    win.show_all()
    gtk.main()


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

関連記事:

関連URL:

使用したバージョン: