メッセージダイアログボックスを表示する

呼称: メッセージダイアログ表示のサンプル
目的: メッセージダイアログを表示し、そのダイアログ内のボタンの返り値を取得する
特徴: ダイアログをモーダル表示してベース画面の背面へ回り込ませない
用例: ユーザへの確認やワーニング/エラー検出の有無を伝える
備考: GUI ツールを作る上での基本ウィジェット

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

import pygtk
pygtk.require('2.0')
import gtk

"""メッセージダイアログ表示関数(INFO, WARNING, QUESTION, ERROR)
引数 :
 - pwin : 呼び出し元画面
 - msg : 表示するメッセージ
返値 :
 - ボタンが1つの場合 : gtk.MessageDialog.run() の返値
 - ボタンが2つの場合 : True or False
MessageDialog :
 - parent : ダイアログの一時的な親画面を指定
            (モーダル表示の場合、背面への回り込みを防止)
 - flags : モーダル表示 + 親画面の終了時にダイアログを削除
 - type : ダイアログの種類
 - buttons : ダイアログに表示するボタンの種類
 - message_format : ダイアログに表示するメッセージ
"""
def info_dialog(pwin, msg):
    dialog = gtk.MessageDialog(
            parent = pwin.window,
            flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
            type = gtk.MESSAGE_INFO,
            buttons = gtk.BUTTONS_OK_CANCEL,
            message_format = msg)
    r = dialog.run()
    dialog.destroy()
    if r == gtk.RESPONSE_OK:
        return True
    else:
        return False

def warning_dialog(pwin, msg):
    dialog = gtk.MessageDialog(
            parent = pwin.window,
            flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
            type = gtk.MESSAGE_WARNING,
            buttons = gtk.BUTTONS_OK,
            message_format = msg)
    r = dialog.run()
    dialog.destroy()
    return r

def question_dialog(pwin, msg):
    dialog = gtk.MessageDialog(
            parent = pwin.window,
            flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
            type = gtk.MESSAGE_QUESTION,
            buttons = gtk.BUTTONS_YES_NO,
            message_format = msg)
    r = dialog.run()
    dialog.destroy()
    if r == gtk.RESPONSE_YES:
        return True
    else:
        return False

def error_dialog(pwin, msg):
    dialog = gtk.MessageDialog(
            parent = pwin.window,
            flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
            type = gtk.MESSAGE_ERROR,
            buttons = gtk.BUTTONS_CLOSE,
            message_format = msg)
    r = dialog.run()
    dialog.destroy()
    return r


class BaseWindow:

    def __init__(self):
        """ベース画面作成
        画面やフレーム、ラベル、ボックス、ボタン等、
        構成要素のレイアウト配置と初期化を行う
        """
        # make TopLevelWindow
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title(self.__class__.__name__)
        self.window.set_border_width(10)
        self.window.set_position(gtk.WIN_POS_CENTER)
        self.window.connect('destroy', lambda w: gtk.main_quit())

        # make Label for return value of button
        self.lbl = gtk.Label('')
        self.lbl.set_alignment(0.1, 0.5)

        # make VBox
        vbox = gtk.VBox(False, 3)
        self.window.add(vbox)

        # make Frame
        frame = gtk.Frame('MessageDialogTest')
        vbox.pack_start(frame, False, False, 5)
        vbox.pack_start(self.lbl, False, False, 5)

        # make VBox
        in_vbox = gtk.VBox(False, 3)
        frame.add(in_vbox)

        # make HButtonBox and Buttons
        in_hbox = gtk.HButtonBox()
        in_hbox.set_layout(gtk.BUTTONBOX_START)
        in_hbox.set_border_width(5)
        for b in ['info', 'warning', 'question', 'error']:
            btn = gtk.Button(b)
            btn.connect('clicked', eval('self.on_btn_' + b + '_clicked'))
            in_hbox.add(btn)

        # add Hbox
        in_vbox.pack_start(in_hbox, False, False, 5)

        # make Label
        in_lbl = gtk.Label('ダイアログボックスの表示テスト')
        in_vbox.pack_start(in_lbl, False, False, 5)

        # make HBox and add Quit button
        hbox = gtk.HBox(False, 4)
        btn = gtk.Button(stock='gtk-quit')
        btn.connect('clicked', self.on_btn_quit_clicked)
        hbox.pack_end(btn, False, False, 5)
        vbox.pack_start(hbox, False, False, 5)

        # display
        self.window.show_all()

    def on_btn_info_clicked(self, btn):
        """
        info ボタン選択時の処理
        """
        msg = 'info'
        r = info_dialog(self, msg)
        if r:
            self.lbl.set_label('OK が選択されました。')
        else:
            self.lbl.set_label('キャンセルが選択されました。')

    def on_btn_warning_clicked(self, btn):
        """
        warning ボタン選択時の処理
        """
        msg = 'warning'
        r = warning_dialog(self, msg)
        if r == gtk.RESPONSE_OK:
            self.lbl.set_label('OK が選択されました。')

    def on_btn_question_clicked(self, btn):
        """
        question ボタン選択時の処理
        """
        msg = 'question'
        r = question_dialog(self, msg)
        if r:
            self.lbl.set_label('はいが選択されました。')
        else:
            self.lbl.set_label('いいえが選択されました。')

    def on_btn_error_clicked(self, btn):
        """
        error ボタン選択時の処理
        """
        msg = 'error'
        r = error_dialog(self, msg)
        if r == gtk.RESPONSE_CLOSE:
            self.lbl.set_label('閉じるが選択されました。')

    def on_btn_quit_clicked(self, btn):
        """
        終了ボタン選択時の処理
        """
        self.destroy(btn)

    def destroy(self, widget, data=None):
        gtk.main_quit()


def main():
    BaseWindow()
    gtk.main()

if __name__ == '__main__':
    main()

実行結果。




リファレンス:
PyGTK 2.0 Reference Manual
PyGTK 2.0 Tutorial

PyGTK アプリケーションの拡張

Extending our PyGTK Application by Mark Mruss

# 本稿は上記リンク元の和訳になります
# 本稿はクリエイティブ・コモンズの帰属 - 非営利 - 同一条件許諾 2.5でライセンスされています
# 転載ミス、誤訳等については適宜修正します

このチュートリアルでは、常に全リストを保持しなくて良いように、wine リストを作成して保存・ロードすることで、ユーザに追加したアイテムの編集を許可する PyWine アプリケーションを拡張しています。

このチュートリアルの全てのコードはここからダウンロードできます。

もし、あなたが PyWine アプリケーション、又は Glade や PyGTK に明るくないならば、それらについて取り上げた私の2つのチュートリアルを最初に読むことをお奨めします:

The GUI - Glade

最初にする事は、PyWine glade プロジェクトを開き、ツールバーに編集ボタンを追加する事です:

  1. ツールバーを選択して編集ボタンにスペースを作成し、そのプロパティサイズを2でセットしてください。
  2. 新たに作成した空白のスペースにツールバーボタンを追加してください。
  3. そのボタンを btnEditWine と呼び、そのラベルを "ワインを編集する"、stock 編集アイコンでセットします。次に clicked イベントハンドラを追加します。
  4. 少しメニューを変更します。追加 | ワイン メニューから ワイン | 追加 と ワイン | 編集 を読み込むためにメニューにセットしています。以前の PyWine チュートリアルで行った事と同様にして、btnEditWine ボタンの clicked イベントハンドラとして同機能の ワイン | 編集 メニューの clicked ハンドラを作成してください。


The Code

そのコードを動作させる編集ボタンを取得してみましょう、最初にしなければいけない事は、現在、選択されている gtk.TreeView の行からどんな情報も取得する事です。これを行うのに2つの方法があります。1つめの方法は、目に見える4つのカラムから全てのデータを読み込む事です。2つめの方法は、gtk.ListStore (モデル) に Wine オブジェクトを実際に追加する事です、しかし、それは gtk.TreeView では表示されません。

今後、それは、よりシンプルに、使い易くなるかもしれないので、もし wine クラスが余分な情報を含む、又はユーザに gtk.TreeView からカラムを追加/削除させるならば、私は後者のアプローチを選択します。これは幾つかのコードを少し変更する事を意味します。

最初に pyWine の __init__ 関数において、カラム定義変数へ追加カラムを追加しなければなりません:

self.cWineObject = 0
self.cWine = 1
self.cWinery = 2
self.cGrape = 3
self.cYear = 4

そのリストのポジション 0 として実際の wine オブジェクトを設けています、従って、私たちは、以下のような、同じ関数内で gtk.ListStore 作成コードを適応させなければなりません:

# wineView で使うために listStore モデルを作成する
self.wineList = gtk.ListStore(gobject.TYPE_PYOBJECT
				, gobject.TYPE_STRING
				, gobject.TYPE_STRING
				, gobject.TYPE_STRING
				, gobject.TYPE_STRING)

gtk.ListStore で最初のアイテムが python オブジェクトになるのを除いて、全て以前と同様です。上述のコードを取得して、コンパイルするために、私たちはファイルの先頭に以下のコードを追加しなければなりません:

import gobject

次に、実際に Wine オブジェクトを含むために gtk.ListStore へ私たちの wine を追加する方法を変更する必要があります。幸運な事に、私たちの Wine クラスへ getList() 関数を追加した以前のチュートリアルで、それは gtk.ListStore() へ追加するためのリストを返しています。従って、私たちがしなければならない全ての事は編集されています:

def getList(self):
	"""この関数は wine 情報を格納したリストを返します
	wineList へ簡単に wine を追加するために使われます"""
	return [self, self.wine, self.winery, self.grape, self.year]

それは大きな変更ではありません。私たちは、シンプルに、getList() がリストの先頭に Wine クラスを格納できるようにしなければなりません。

次のステップは実際にユーザに wine エントリを編集するのを許可させる事です。しかし、そうする前にもう1つ変更する必要があります。チュートリアルでは、wine ダイアログの __init__ 関数は、初期パラメータとして wine クラスを作成する全てのアイテムを受け取りました。

def __init__(self, wine="", winery="", grape="", year=""):

これはパラメータや数が小さいならば、問題はありません。しかし、Wine クラスの wineDialog クラス初期化が複雑になっていった場合、悩みの種になります。従って、私たちは Wine クラスオブジェクトを受容するために __init__ 関数を変更しています。正確に言えば、その部分の全てです:

def __init__(self, wine=None):
	"""クラスを初期化するwine - Wine オブジェクト"""

	# glade ファイルをセットする
	self.gladefile = "pywine.glade"
	# 返却される wine をセットする
	if (wine):
		# wine オブジェクトがパスされた
		self.wine = wine
	else:
		# ブランク wine を使うのみ
		self.wine = Wine()

次のステップは、最後に wine エントリを編集する事です。on_EditWine() 関数内で実現します。ワイン | 編集 メニューアイテムと "ワインを編集する" ボタンの clicked イベントをフックします:

def on_EditWine(self, widget):
	"""ユーザが wine エントリを編集したいときに呼ばれます"""

	# gtk.TreeView の selection を取得します
	selection = self.wineView.get_selection()
	# selection iter を取得します
	model, selection_iter = selection.get_selected()

	if (selection_iter):
		"""selection があります、従って、self.cWineObject のカラムでその値を取得します"""
		wine = self.wineList.get_value(selection_iter, self.cWineObject)
		# 現在の selection をベースとしない wine ダイアログを作成します
		wineDlg = wineDialog(wine);
		result,newWine = wineDlg.run()

		if (result == gtk.RESPONSE_OK):
			"""ユーザが Ok をクリックしました、変更を保存して gtk.ListStore へ戻します"""
			self.wineList.set(selection_iter
					, self.cWineObject, newWine
					, self.cWine, newWine.wine
					, self.cWinery, newWine.winery
					, self.cGrape, newWine.grape
					, self.cYear, newWine.year)

最初に私たちがする事は、gtk.TreeView と関連する gtk.TreeSelection オブジェクトを取得するために gtk.TreeView.get_selection() を呼び出す事です。それから 私たちの gtk.TreeModel(私たちが意識しない) と gtk.TreeView(私たちが意識する) で現在、選択されたノードを指す gtk.TreeIter を返却する gtk.TreeSelection.get_selected() を呼び出します。

gtk.TreeIter は、もし何も selection がない場合、get_selected() 関数によって None が返却されます。そうでなければ、私たちは gtk.TreeModel.get_value() を呼び出すことで、gtk.TreeView で、現在、選択された位置における Wine オブジェクトを取得するために gtk.TreeIter を使用します。かつて私たちは rest がとても単純な Wine オブジェクトを持ちました。私たちは wineDialog オブジェクトを作成して表示し、gtk.ListStore.set() 関数を使用する gtk.TreeView において Ok ボタンをクリックしたら選択された項目を更新しました。

gtk.ListStore.set() 関数は、実際、とても興味深いです。それは、その最初のパラメータ(値をセットする位置)と、そのパラメータの rest が1つないし、より多くの column_number, new_value ペアを取るためです!私の唯一のがっかりした事は、gtk.ListStore.append() 関数が行った同じ方法でリストを使った関数を発見していなかった事です。

従って、それは wine エントリを編集するためのものです!私たちは、アプリケーションを起動させる度に wine を毎回、再実行したくないです。wine リストのロードや保存について話し始める頃合いです。

Wine リストを保存・ロードする

最初にする事は、WordPy offline blogging tool チュートリアルから2つのヘルパー関数を借りる事です:

def show_error_dlg(self, error_string):
	"""この関数はエラー発生時にエラーダイアログを表示するために使用されます
	error_string - ダイアログ上で表示されるエラー文字列"""
	error_dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR
				, message_format=error_string
				, buttons=gtk.BUTTONS_OK)
	error_dlg.run()
	error_dlg.destroy()

この関数は、エラーダイアログを表示することで発生したエラーをユーザに知らせる簡単な方法を提供するだけです。私たちは pyWine クラスへこれを追加します。どのようにしてその関数が動作するかの詳細な情報は WordPy offline blogging tool チュートリアルをご参照ください。

また、私たちは browse_for_image() 関数も追加しています:

def browse_for_image(self):
	"""この関数は画像を表示させるために使用されますユーザが選択した場合、画像へのパスが返却されます
	しかしながら、キャンセルされたり、洗濯されなかった場合、空文字列が返却されます"""
	
	file_open = gtk.FileChooserDialog(title="Select Image"
				, action=gtk.FILE_CHOOSER_ACTION_OPEN
				, buttons=(gtk.STOCK_CANCEL
							, gtk.RESPONSE_CANCEL
							, gtk.STOCK_OPEN
							, gtk.RESPONSE_OK))
	"""イメージフィルタを作成・追加します"""		
	filter = gtk.FileFilter()
	filter.set_name("Images")
	filter.add_mime_type("image/png")
	filter.add_mime_type("image/jpeg")
	filter.add_mime_type("image/gif")
	filter.add_pattern("*.png")
	filter.add_pattern("*.jpg")
	filter.add_pattern("*.gif")
	file_open.add_filter(filter)
	"""'all files' フィルタを作成・追加します"""
	filter = gtk.FileFilter()
	filter.set_name("All files")
	filter.add_pattern("*")
	file_open.add_filter(filter)
	
	"""返却値を初期化します"""
	result = ""
	if file_open.run() == gtk.RESPONSE_OK:
		result = file_open.get_filename()
	file_open.destroy()
	
	return result

私たちがファイル保存ダイアログとファイルオープン操作のための変更を除いて、画像の代わりに pyWine ファイル(*.pwi) を表示します。私たちは dialog_action という追加パラメータを経由することで、ファイルオープン又はファイル保存ダイアログを制御するでしょう。それは gtk.FileChooserDialog のアクションプロパティをセットするために使用するという働きをします:

def file_browse(self, dialog_action, file_name=""):
	"""この関数は pyWine ファイルを表示するために使用されます
	dialog_action によってオープン又は保存することができます
	もしユーザが選択すればファイルパスが返却され、キャンセル又は選択しなければ、空文字列が返却されます
	dialog_action - gtk.FILE_CHOOSER_ACTION_OPEN 又は gtk.FILE_CHOOSER_ACTION_SAVE ダイアログのためのオープン又は保存
	file_name - 保存時のデフォルト名"""

	if (dialog_action==gtk.FILE_CHOOSER_ACTION_OPEN):
		dialog_buttons = (gtk.STOCK_CANCEL
							, gtk.RESPONSE_CANCEL
							, gtk.STOCK_OPEN
							, gtk.RESPONSE_OK)
	else:
		dialog_buttons = (gtk.STOCK_CANCEL
							, gtk.RESPONSE_CANCEL
							, gtk.STOCK_SAVE
							, gtk.RESPONSE_OK)

	file_dialog = gtk.FileChooserDialog(title="Select Project"
				, action=dialog_action
				, buttons=dialog_buttons)
	"""保存するならばファイル名をセットする"""
	if (dialog_action==gtk.FILE_CHOOSER_ACTION_SAVE):
		file_dialog.set_current_name(file_name)
	"""pywine フィルタを作成・追加する"""
	filter = gtk.FileFilter()
	filter.set_name("pyWine database")
	filter.add_pattern("*." + FILE_EXT)
	file_dialog.add_filter(filter)
	"""'all files' フィルタを作成・追加する"""
	filter = gtk.FileFilter()
	filter.set_name("All files")
	filter.add_pattern("*")
	file_dialog.add_filter(filter)

	"""返却値を初期化する"""
	result = ""
	if file_dialog.run() == gtk.RESPONSE_OK:
		result = file_dialog.get_filename()
	file_dialog.destroy()

	return result

以下のように FILE_EXT がシンプルに定義されます:

FILE_EXT = "pwi"

私たちは、 ワイン | 追加 と ワイン | 編集 メニューアイテムに行った同じメソッドを使用する glade プロジェクトで ファイル | 開く と ファイル | 保存 メニューコマンドのためにハンドラも追加していきます。私は on_file_open と on_file_save と名付けました:

# ディクショナリを作成して、コネクトする
dic = {"on_mainWindow_destroy" : self.on_Quit
		, "on_AddWine" : self.OnAddWine
		, "on_EditWine" : self.on_EditWine
		, "on_file_open" : self.on_file_open
		, "on_file_save" : self.on_file_save}
self.wTree.signal_autoconnect(dic)

Wine オブジェクトのロードや保存をするために、python shelve モジュールを使います。大半(全部ではない)は python オブジェクトの標準 python モジュールです。もちろん、これを実行できる他の方法があります。私たちは xml ファイル、又は全オブジェクトの単純な pickle を使うことができます。しかし、私は、shelve はこの状況では適していて、xml で保存やロードするのを紹介するのは簡単だと考えました。

ドキュメントによると:

"shelf" は永続的な、辞書のようなオブジェクトです。"dbm" データベースとの違いは、シェルフ内のその値(キーではない!)は、任意の Python オブジェクトを取る事ができる - 全て pickle モジュールが操作できますこれは、大半のクラスインスタンス再帰的データタイプや多くの共有のサブオブジェクトを包含するオブジェクトを含みます。そのキーは普通の文字列です。

保存する

on_file_save() 関数をセットアップしましょう。始めに、特定のファイル名と、ファイルを保存したい場所をユーザに閲覧させます次に、その上で、そのファイルが持つ拡張子を確保します。それから、gtk.TreeView の全てのアイテムをスルーしながらループして、shelve モジュールを使用する各々の Wine オブジェクトを保存します:

def on_file_save(self, widget):
	"""ユーザが wine リストを保存したいときに呼ばれます"""

	# ファイルを保存するパスを取得します
	save_file = self.file_browse(gtk.FILE_CHOOSER_ACTION_SAVE, self.project_file)
	if (save_file != ""):
		# パスと適切な拡張子を補足します
		save_file, extension = os.path.splitext(save_file)
		save_file = save_file + "." + FILE_EXT
		""" shelve ファイルを作成する保存場所に "実存する" ファイルがあるとき、新規作成するために "n" を使用します"""
		db = shelve.open(save_file,"n")
		"""gtk.ListStore の最初のアイテムを取得します、そして None でない間、各々のアイテムを保存しながらリストの前方へ移動します"""
		# リストの最初のアイテムを取得します
		iter = self.wineList.get_iter_root()
		while (iter):
			# 現在の gtk.TreeIter で wine を取得する
			wine = self.wineList.get_value(iter, self.cWineObject)
			# キー名としてリストの iters ポジションを使用するdb[self.wineList.get_string_from_iter(iter)] = wine
			# 次の iter を取得する
			iter = self.wineList.iter_next(iter)
		# ディスクへ変更を書き込みデータベースをクローズする
		db.close();
		# プロジェクトファイルをセットする
		root, self.project_file = os.path.split(save_file)

先に gtk.TreeIter オブジェクトを動作させると、このコードを理解するのは難しくありません。実際に、そのコードの本当に難しい部分は、rest がインラインコメントで説明されている以下の部分です:

while (iter):
	# 現在の gtk.TreeIter で wine を取得する
	wine = self.wineList.get_value(iter, self.cWineObject)
	# キー名としてリストの iters ポジションを使用する
	db[self.wineList.get_string_from_iter(iter)] = wine
	# 次の iter を取得する
	iter = self.wineList.iter_next(iter)

基本的に、gtk.ListStore で各々のアイテムをループさせて、wine オブジェクトである shelve ファイル内の現在の gtk.TreeIter ポジションでデータをセットしています。

db[self.wineList.get_string_from_iter(iter)] = wine

gtk.TreeModel.get_string_from_iter() 関数は、"iter によって指し示されたパスの文字表現を返却します。この文字列は ':' で区切られた数字のリストです。例えば、"4:10:0:3" は、この文字列の返却値として許容されます。"(pyGTK Docs)。私たちは gtk.ListStore を使用しているので、その値は常に先頭から末尾までのリストを移動させるにつれて増加して、1つの値が返されます。

従って、最初のアイテムは "0"、次は "1"、その次は "2" となります。これは、ファイルをオープンするときに役にたちます。shelve ファイルのキーは、(私が言える範囲内で)任意の順番であることが保証されていないからです。

あなたが shelve ファイルを閉じるとき、そのデータはディスクへ書き込まれます。

また、あなたはデフォルトファイル名として self.project_file アイテムに含まれている事に気付くでしょう。これはそのクラスへの新たな追加機能になります。それは現在のプロジェクトのファイル名です。私たちが保存しようとするとき gtk.FileChooserDialog でデフォルト名をセットしようとするだけです。以下のように __init__ 関数で定義されています:

self.project_file = ""

これは以下のようなポップアップをダイアログになります:


ロードする

もし、あなたが on_file_save 関数を理解したならば、on_file_open() 関数をセットアップして、理解するのは難しくないです:

def on_file_open(self, widget):
	"""ユーザが wine を開きたいときに呼ばれます"""

	# オープンするためにファイルを取得します
	open_file = self.file_browse(gtk.FILE_CHOOSER_ACTION_OPEN)
	if (open_file != ""):
		# パスをセットして、読み込み用にオープンしますtry:
			db = shelve.open(open_file,"r")
			if (db):
				# ファイルをオープンして、gtk.TreeView を空にしますself.wineList.clear()
				""" shelve ファイルは順番が保証されていないので、ファイルの iter 0 から始めて移動します"""
				count = 0;
				while db.has_key(str(count)):
					newwine = db[str(count)]
					self.wineList.append(newwine.getList())
					count = count +1
				db.close();
				# プロジェクトファイルをセットするroot, self.project_file = os.path.split(open_file)
			else:
				self.show_error_dlg("Error opening file")
		except:
			self.show_error_dlg("Error opening file")

has_key() 関数とカウンタを使用してリストからアイテムをロードするときに気付くでしょう。上述した gtk.TreeIter パスを使用する各々の Wine オブジェクトを保存します。gtk.ListStore を使用しているので1つの数値です。しかし、ゼロから始まる順番でファイルから各々のアイテムを取得する独自のカウンタを使用できません。数値によって key が表現されるまで続きます。(注意: shelve ファイル内の key は文字列に違いないので、数値を文字列へ変換します)

ファイルから Wine オブジェクトをロードするために、私たちは現在カウントされた key 上でシンプルに shelve ファイルに尋ねます:

newwine = db[str(count)]

それから、そのリストへ wine をただ追加して、.pwi ファイルをロードしました。例外コードを試してみましょう。基本的に、もし、ユーザが正しい pyWine プロジェクトファイルではないファイルをオープンしようとして発生させた如何なるエラーもキャッチします。

まとめ

このチュートリアル基本的なものです。しかし、ファイル | 新規 メニューハンドラをフックする、又はツールバーへ削除ボタンを追加する、又は現在のプロジェクトファイルへウィンドウのタイトルをセットするのでさえ、どのぐらい容易なのかを、あなたは理解しました。

あなたは、XML ファイルタイプを実装しようとしたり、異なったプロジェクトファイルタイプで挑戦してみたいとさえ思うかもしれない。今後、プロジェクトファイルのために使用したいファイルのタイプをユーザに決定させることを許可する巧妙なオプションになるだろうと、私は思います。

このチュートリアルの全てのコードはここからダウンロードできます。

もし、質問があったり、このチュートリアルの問題等に気付けば、コメントを投稿して、教えてください!

Glade3 におけるカスタム PyGTK ウィジット: パート2: カスタムアダプタ

Custom PyGTK Widgets in Glade3: Part 2: Custom widget adaptors by Ali Afshar

# 本稿は上記リンク元の和訳になります
# 本稿の規約・ライセンス等はオリジナルのそれに従います
# 転載ミス、誤訳等については適宜修正します

前回の投稿ではカスタム glade ウィジットについて、私たちは、ユーザインタフェースデザイナで使えるように PyGTK カスタムウィジットを glade-3 に追加する方法について簡潔に説明しました。本稿では、追加の振る舞いを定義するために、PyGTK ウィジット向けにカスタムアダプタを作成する方法を含めるようにしました。
例として、私が "Service View" と呼んでいるカスタムウィジットを使用します。私たちは、主要部分において、このウィジットがコンテンツセクションを持ち、下部でクローズボタンも含みものを予測します。私たちは、一般的なドックに収納可能なビューとして、トップレベルウィンドウというよりはむしろダイアログのような類のウィジットを使います。これは全く無意味のように見えるかもしれません。しかし、それは、ノートブックにおいて、デバッガ、ターミナル、ドキュメントブラウザ等々の IDE のように、異なったビューを作成するアプリケーションの便利なコンポーネントになります。これらは同一の基本的なレイアウトを共有することができます。しかし、ウィジットの1つの主要部分をただ置き換えるだけです。

先ずウィジットを書いてみましょう:

import gtk

class ServiceView(gtk.VBox):

__gtype_name__ = 'ServiceView'

def __init__(self):
    self.frame = gtk.Frame()
    self.pack_start(self.frame)
    self._bb = gtk.HButtonBox()
    self._bb.set_layout(gtk.BUTTONBOX_END)
    self._bb.pack_start(gtk.Button(stock=gtk.STOCK_CLOSE))
    self.pack_start(self._bb, expand=False)

このウィジットのように、私たちはフレーム内部で1つの子コンポーネントを追加させたいです。また self.frame のラベルをセットすることもできます。

私たちが、以下のようなカタログ(pywidgets.xml)を使用する Glade3 へこのウィジットを追加する場合:










そして、そのようなウィジットを単純にインポートするコード(pywidgets.py)をサポートします。

from serviceview import ServiceView

(モジュール "serviceview.py" では python のパスが通っていて、上述のウィジットを含みます)

正しい場所にカタログとモジュールファイルをインストールして、glade-3 を起動してください。ウィンドウを作成して、そこへ私たちのカスタムウィジットを追加してください。すぐにウィジットの要求されたサイズに応じて、入力ダイアログがクエリされるでしょう。この応答をするとすぐに、ウィジットは作成されます。

問題? そうです。それは以下の項目において動作しません。:

  • ウィジットのサイズを尋ねるクエリ
  • フレームの外側へ追加された余分なグレーセクション
  • フレームのコンテンツとそのラベルを変更できない機能


これらの全ての問題は同じ要因です。私たちのウィジットは gtk.VBox のサブクラスなので、そのライフタイムの延長上で VBox として取り扱われます。

そのため、どうやってこれらの問題を解決するのでしょうか?2つの事が必要です。先ずカタログファイルの幾つかのプロパティをオーバーライドします、次にカスタムアダプタを作成します。(私たちはそのアダプタから、これらの全ての事ができるようにするのに1日必要かもしれません。しかし、ただ今は不可能です。それは、"hybrid API" を持つ glade-3 開発者によって明らかにされた1つの課題です)

カタログを修正する

ここで read the excellent documentation を忘れないでください。glade とデフォルトカタログにおけるウィジットはクエリのための入力ウィンドウを開始する機能を提供します。これは、真っ当なデフォルトとして全く必要ではありません。その後、通常の方法において要求されるように変更されました。しかし、それは推測する幾つかの状況において使い易いものです。この例では、Box ウィジットのサブクラスを使用しているので、私たちが求めるサイズが尋ねられるでしょう。サイズは Box の本当のプロパティではなく、仮想プロパティです。Glade が、どのようにしてそれを表示するかを知るのを助けます。もし、デフォルトカタログのこのパートを見ると、私たちはこの現象を確認することができます:


...


glade_standard_int_spec
The number of items in the
box
glade_gtk_box_verify_size


...


glade-widget-class タグ内部のプロパティタグ内部にある各々のプロパティタグは、その id 属性を持たなければなりません。しかし、それは、ここではオプションとして示されています。ここで、私たちにとって重要な1つは、表示されるクエリボックスを引き起こす query="True" です。そして、"override" プロパティのソートにおいて、自身のカタログを置き換えることができます。プロパティに save="False" がセットされることを注意してください。それは本当のプロパティではないため、仮想プロパティです。そして glade XML ファイルにも書き込まれません

クエリボックスを無効にせずに、そうしたい場合:

  1. このデフォルト値にゼロをセットして、グレーなエリアがないこと、余分なウィジットがボックスに追加されない事を示す
  2. サイズウィジットの隠蔽 又は 無効化


再度、ドキュメントを見てください。私たちは default="0" とデフォルト値をセットする事と、そのプロパティを disabled="True" で無効化する、又は visible="False" で隠蔽する事を発見しました。

つまり、私たちのカタログのクラス定義は以下になります:






カタログを再インストールしてください。ウィンドウを作成して、ウィジットを追加してください。スタートサイズを尋ねられることはなく、プロパティリストにサイズ値も表れません。ここでは必要ない残された Box のプロパティがあります。それらを削除する事は、読者のための作業として残しておいて、次節へ進みます。

カスタムアダプタを使用する


アダプタクラスはサポートコードモジュールの中で表され、カタログを参照します。例えば:




ServiceView アダプタの __gtype_name__ を登録するクラスを参照します。そのクラスは glade.get_adaptor_for_type('SomeGTKType') から継承し、VBox サブクラスのケースでは、私たちのアダプタスーパークラス(自動的にそれ自身を使用した)として glade.get_adaptor_for_type('GtkVBox') を使用できました。

import glade

class ServiceViewAdaptor(glade.get_adaptor_for_type('GtkVBox')):
    __gtype_name__ = 'ServiceViewAdaptor'

アダプタコード内で、glade は、実行時のみ有効になる特殊な Python モジュールです。そして glade ユーザインタフェースバインディングを含みます。一見したければ、実行時にそのドキュメントを取得できます。しかし、次の投稿時にこの機能が実行されるでしょう。

今のところ何もオーバーライドしないので、私たちの実行コードは、以前のものと正確に同様の振る舞いをします。しかし、(何もしない)カスタムアダプタと名付けれています

今、カスタムアダプタのメソッドを追加して起動することができます。一般的に、有効なメソッドは以下のように呼ばれるでしょう。:

do_something(self, to_object, *args)


その何らかの動作が起こる場所で to_object は、ユーザインタフェースのオブジェクトとして作成されます。最初のメソッドの例です:

def do_post_create(self, obj, reason):
    # このウィジットへ追加できるようにするために Glade から都合良く見えるグレーエリアの1つを作成します
    obj.frame.add(gobject.new('GladePlaceholder'))


このステージでは、好みで、あなたのカタログとサポートコードをインストールできます。そして、このウィジットをテストしてください。私たちのフレーム内部でグレーエリアが正しく表示されるのが確認されるでしょう。今、ラベルのように、そこに何かを追加しようとしてください。そのラベルは私たちが望まない VBox の最後にただ追加されたことに気付くでしょう。これを修正するために、次の子メソッドをオーバーライドする必要があります:

def do_get_children(self, obj):
    """glade がウィジットの子を取得する"""
    return obj.frame.get_children()

def do_add(self, obj, child):
    """ウィジットへ子を追加する"""
    obj.frame.add(child)

def do_remove(self, sv, child):
    """ウィジットから子を削除する"""
    obj.frame.remove(child)

def do_replace_child(self, obj, old, new):
    """
    ウィジットの子を置き換える
    これはあなたが削除するとき glade によって呼ばれる事に注意してください
    又は、実際に GladePlaceholder でウィジットが置き換わるのでウィジットを切り捨てます
    """
    obj.frame.remove(old)
    obj.frame.add(new)

これらのメソッドの実装は、特に straight-forward で、私たちの目的を実現します。そのトリックは何をするのかを知っています。get_children メソッドは適当な場所に配置される必要があります。そのため、Glade ウィジットは階層的に子ウィジットを正しく表示します。do_add, do_remove と do_replace メソッドは、ウィジットを追加、削除、置換する機能を持ちます。
再度、あなたがカタログとサポートモジュールをインストールするならば、それが実際に動作することに気付くでしょう。そのウィジットは正しい場所に追加され、あなたのウィジットの1つの子として階層的に表示されます。子プロパティについて GTK から警告を受け取るでしょう。それは child_property メソッドを書くことで解消されます:

def do_child_get_property(self, obj, child, prop):
    """プロパティ値を取り出す"""
    if prop in ['expand', 'fill']:
        return True
    elif prop == 'padding':
        return 0
    elif prop == 'position':
        return 0
    elif prop == 'pack-type':
        return gtk.PACK_START
    return True

def do_child_set_property(self, obj, child, prop, val):
    """プロパティ値をセットする"""
    if prop in ['expand', 'fill', 'padding', 'pack-type']:
        pass

私たちのウィジットは本当のコンテナ(アダプタを介するのみですが)のように振る舞うので、それは子プロパティをセットする方法に関してクエリを取得するでしょう。これらのほとんどが私たちにとって重要です。なぜなら、フレームは拡張性とパッキングの方法がとても少なく、大箱であるためにスペースがありません。従って、シンプルな値をただ返すことができます。同様にそこに何かをセットする必要がありません。

やってみよう

今回 Glade で私たちの高機能なウィジットを紹介しました。作ってみましょう。新しいプロジェクトを作成し、ウィンドウに私たちのウィジットを追加し、コンテナへサンプルウィジットを追加した後、幾つかのコード('test.glade' のようなファイル)と共にロードしてみてください:

import gtk
import gtk.glade

# glade がロードできるように gobject でこのタイプを登録するのを確認する
from serviceview import ServiceView

gtk.glade.XML('test.glade').get_widget('window1').show_all()
gtk.main()

それはどう見えたでしょうか?ひどい!そうです。glade ローダーは、フレーム内部というよりむしろ VBox の内部に子ウィジットを配置します!大災難だ!glade ローダーがしている事をあなたが理解するまではそうでしょう。それは子を追加するために ServiceView オブジェクトでメソッドを呼んでいます。

なぜなら VBox のサブクラスだからです。このメソッドは(最後にオブジェクトを追加するために)VBox 群用として振る舞います。取るに足らない複雑さがあります。私たちが Pythongtk.VBox で有効なメソッドを追加することは Glade が使用しているモノではありません。実際は Glade も使えるメソッドを使用します。Glade が使用するメソッドをオーバーライドすることは、"C add をオーバーライドする"といった種類の方法で do_add メソッドの実装を必要とします。

かつて、私たちはそのように実装しました。:

def do_add(self, child):
    self.frame.add(child)

そのウィジットは正しい場所で子としてロードします。

ディスカッション

このシステムで試しにやってみたい人たちがいるかもしれません。今はウィジット自身の中で do_add を、カスタムアダプタの do_add メソッドを削除するために実装しました。そして、私たちが望む実際の動作として理解するでしょう。あなたは、私たちのアダプタのためにスーパークラスとして GtkFrame アダプタを使用することができました。そして、正しい機能を持っていると理解します。

あなたのウィジット又はアダプタの、機能的なウィジットを実装する場所に関しての普遍の疑問へ繋がります。私たちはウィジット内の全てのコンテナインタフェースをオーバーライドし、アダプタなしでも正しく動作しました。しかし、もはや VBox のような振る舞いができないという不都合もあります。

一般的に、あなたが決める方向性は、自身の要求をベースとすべきです。しかし、それはその時に Glade3 を使ってミックスする必要があります。:

  1. Glade3 のためのカタログ
  2. Glade3 のためのカスタムアダプタ
  3. Glade ローダーのためのウィジットメソッド


このシリーズはまだ続きます:

  • カスタムウィジットエディタ
  • Python からの glade UI 制御


注意: API は早期に変更され、後方互換性は重要視していません。従って、如何なるコードも使い捨てとして用意されています。

Glade3 におけるカスタム PyGTK ウィジット

Custom PyGTK Widgets in Glade3 by Ali Afshar

# 本稿は上記リンク元の和訳になります
# 本稿の規約・ライセンス等はオリジナルのそれに従います
# 転載ミス、誤訳等については適宜修正します

gimp のようなマルチウィンドウビューを取り除いて、一般的なアプリケーションのようになったリリース 3.1 以降の Glade 3 は、素晴らしい GTK ユーザインタフェースデザイナです。他の素晴らしい機能として、非 C 言語(PyGTK など)で作成されたウィジットがサポートされている事です。

プラグインコンポーネント:

  1. カタログファイル
  2. サポートモジュール
  3. イコン画像 (オプション)


例として、(私が書いた) Kiwi ハイパーリンクウィジットを使います。このウィジットは EventBox のサブクラスです。従って、私はユーザインタフェースデザイナ内で必要としない追加プロパティの幾つかを省略しながら説明します。

カタログファイル

これは http://glade.gnome.org/docs/catalogintro.html に記載されている内容の続きになります。原則として、カタログには2つのセクションがあります:

  1. ウィジット定義
  2. カタログリスト


ウィジット定義とは以下のようなものです:




大抵の場合、ウィジットクラスの属性として使用されます、しかし、ドキュメントページ上で概説される場合もあります。

リストしているカタログは、とても基本的なものです。そして、不変的に以下のフォームを取ります:



これは現在有効な全てのウィジットをリストしています。今のところ、ドキュメントから外れていません。これは、どのようにして非 PyGTK のウィジットを追加するかの方法です。

PyGTK を使っているあなたが glade に理解させるためにしなければいけない事は、これをカタログのためにメインドキュメント要素に使用する事です:


language="python" によって、kiwiwidgets.py が呼ばれるとき、 追加の "kiwiwidgets" ライブラリ属性が私たちのサポートするライブラリのロケーションを定義します。

カタログファイルは、glade3 カタログディレクトリ上の他のカタログと共に自分のシステム上に存在すべきです:

/usr/local/share/glade3/catalogs/kiwiwidgets.xml

widget-class タグで タグを使っているユーザインタフェースデザイナにおいて、表示されている様々なプロパティをオフにすることができます。この例は以下のようになります:



2つのプロパティを示しました。1番目は、UI デザイナ(ユーザが注意していない)の内部で表示したくない EventBox のプロパティです。シンプルに visible="False" として実現されます。2番目は、プロパティを変更する方法を示します。私たちは、この変換可能なハイパーリンクウィジットのテキストが欲しいです。それと共に Glade 内で全ての変換フックにアクセスします。

サポートモジュール

サポートモジュールは、私たちのウィジットを実行する python コードです。基本的な例では、カスタムアダプタのように好みで何かを定義ていません、又は私たちが優れていると感じたカスタムエディタでさえそうです。しかし、おそらく別のブログ記事として投稿するために残しておきます。

サポートモジュールは、glade モジュールディレクトリに存在すべきです。そして、カタログファイル内で定義された名前であるべきです。私のシステム上では、以下の場所になります:

/usr/local/lib/glade3/modules/kiwiwidgets.py

この単純なケースにおけるモジュールのコードは、HyperLink クラスの import ステートメント(GObject タイプとして、それを登録するために必要とされる全て)のたった1行だけ含みます。

from kiwi.ui.hyperlink import HyperLink

Glade3 は私たちに安息を与えてくれます。
以上です。他のアイテムは後ほどアップされるでしょう:

  • カスタムアダプタの定義
  • カスタムウィジットエディタの定義
  • box サブクラスのための glade クエリスライシング
  • Python からの glade UI 制御

他に興味深い事があれば私はそれに対応するでしょう。

注意: API は早期に変更され、後方互換性は重要視していません。従って、如何なるコードも使い捨てとして用意されています。