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

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

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