HOME Python 書き込む

wxPython でお手軽 gui


1. 初めに

この間作ったメディアからハードディスクへ画像ファイルを移動するスクリプトは 残念ながら家族に不評でした。
家族曰く:
「なんでファイルを移動させるだけなのに薄気味悪い黒い窓が開くの?」
薄気味悪い黒い窓とは DOS プロンプトのことです。これだから素人は困ると思いつつ、gui version を 書いてみました。

Python には Tkinter, PyQt, PyGTK, wxPython などの gui toolkit があります。 いろいろと物色した結果次のことが分かりました。

  1. これらの toolkit のマニュアル、チュートリアルは極めて不備である。
  2. wxPython はデモスクリプトがあるだけまだましである。
  3. wxPython はいろいろな widget が揃っている。
  4. wxPython は最近人気があるようである。(その結果として web resources が他の toolkit より多い。)
というわけで wxPython を使うことにしました。 今回使うプログレスダイアログがあるのも wxPython を選んだ理由です。 (付属で付いてくる Tkinter にはこれが無い。)

2. gui 版画像保存スクリプト

画像保存スクリプトの gui 版は以下の通りです。
01:     #! usr/bin/env python
02:     
03:     r"""
04:     script to achive photos in the removal media into HD
05:     gui version using wxPython
06:     by T.Shido
07:     January 22, 2005
08:     """
09:     import sys, os, shutil, filecmp, wx, wx.lib.dialogs
10:     import photo as ph
11:     from datetime import date
12:     
13:     class Aphoto(wx.App):
14:         def OnInit(self):
15:             ph.Search_Media(ph.MEDIA)
16:             frame = wx.Frame(None)
17:             self.SetTopWindow(frame)
18:             pt = ph.sum([len(val) for val in ph.PHASH.itervalues()])
19:             
20:             if pt:
21:                 dlg = wx.ProgressDialog("Sending Photos",                                 \
22:                                         ("0/%d" % pt),                                    \
23:                                         maximum = pt,                                     \
24:                                         parent=frame,                                     \
25:                                         style = wx.PD_AUTO_HIDE | wx.PD_APP_MODAL)
26:                 failed = self.Move_Photos(pt, dlg)
27:                 if failed:                # if exist photos failed to archive 
28:                     fdlg = wx.lib.dialogs.ScrolledMessageDialog(frame, failed,            \
29:                                               "Copy Failed for these Photos: Try Again!")
30:                     fdlg.ShowModal()
31:     
32:                 dlg.Destroy()
33:                 os.execl(ph.PHOTO_VIEWER, ph.PHOTO_VIEWER, self.pd0)
34:                 frame.Destroy()
35:                 return True
36:             
37:             else:
38:                 dlg = wx.MessageDialog(frame, 'No Photos in the media!',   \
39:                                        'Warning',                          \
40:                                         wx.OK | wx.ICON_INFORMATION )   
41:                 dlg.ShowModal()
42:                 dlg.Destroy()
43:                 sys.exit()
44:     
45:         def Move_Photos(self, pt, dlg):
46:             md = ph.HD + date.today().strftime("%y-%m/")
47:             np = ph.nPdir(md)
48:             sf = 0
49:             failed_log = ''
50:             i0 = True
51:             
52:             os.chdir(md)
53:     
54:             for d, fs in ph.PHASH.iteritems():
55:                 np += 1
56:                 pd = md + "photo%02d" % np
57:                 if i0:
58:                     self.pd0 = pd
59:                     i0 = False
60:                 os.mkdir(pd)
61:                 for f in fs:
62:                     f1 = d + f
63:                     f2 = pd + '/' + f
64:                     shutil.copyfile(f1, f2)
65:                     if filecmp.cmp(f1, f2):
66:                         os.remove(f1)
67:                     else:
68:                         failed_log += "copy failed: %s => %s\n" % (f1, f2)
69:                     sf += 1
70:                     dlg.Update(sf, ("%d/%d" % (sf, pt)))
71:             return failed_log
72:     #-----------       
73:     
74:     if __name__ == '__main__':
75:         Aphoto(redirect=False)
説明
09wx, wx.lib.dialog をインポートする。
10以前作ったコンソール版をインポートする。 python は perl に比べてスクリプトの再利用がしやすい。 photo.py は同じディレクトリにあるか、 PYTHONPATH の通ったディレクトリにあることが必要。
13wx.App を親クラスとしてクラス Aphotoを定義する。
14OnInit はwx.App クラス(のサブクラス)を初期化する時に実行される。
15メディアをサーチする。
16wx.Frame のインスタンス frame を作る。 このスクリプトでは、ダイアログボックスを出すだけだが、 ダイアログボックスの親フレームとして必要。
17frame を topwindow にする。
18メディアにある画像ファイルの枚数を計算する。
20もし、メディアに画像ファイルがあれば、
21プログレスダイアログを作る。
21,22最初の引数はプログレスダイアログのタイトル、2番目の引数は表示される文字。
23プログレスダイアログのゲージの最大値を画像ファイルの枚数に設定。
24親フレームは frame
25表示スタイルの設定。終了後自動的に消えるようにする。
26メディアからハードディスクに画像を移動する。 返り値は移動できなかった画像ファイルのリストの文字列
27もし、移動できなかった画像ファイルがあれば、スクロールメッセージダイアログを開く
32プログレスダイアログを閉じる。
33閲覧ソフトを起動させる。
34frame を閉じる。
35OnInt は真偽値を返す必要がある。
37もし、メディアに画像ファイルがなければ、その旨のメッセージダイアログを表示する。
45画像ファイルを移動させるメソッドの定義
70画像ファイルを1枚移動させるたびにダイアログをアップデートする。
75redirect=False にしておくとエラーメッセージなどはコンソール (DOS prompt) に表示される。デバッグのときに便利。
ソースを見ていただくと分かるように、 以前作ったコンソール版が簡単に再利用できます。 もし、最初からスクリプトの再利用を意識して書けばもっと再利用できたでしょう。 再利用することを意識した場合コンソール版(photo.py) と gui 版(photo_wx.py) は次のようになります。

photo.py

01:     #! usr/bin/env python
02:     
03:     r"""
04:     script to achive photos in the removal media into HD
05:     by T.Shido
06:     January 12, 2005: 10/27/12
07:     """
08:     
09:     from __future__ import nested_scopes
10:     import glob, string, os, os.path, shutil, filecmp, re, sys
11:     from datetime import date
12:     
13:     __all__ = ['Move_Photos', 'HD', 'MEDIA', 'PHOTO_VIEWER', 'PREGEXP']
14:     
15:     # global parameters
16:     HD    = 'D:/doc/'
17:     MEDIA = 'G:/'
18:     PHOTO_VIEWER = 'D:/WBIN/linar160/linar.exe'
19:     PREGEXP = re.compile(".(gif|bmp|jpe?g|tiff?)$", re.I)
20:     
21:     # functions
22:     def sum(ls):
23:         total = 0
24:         for x in ls:
25:             total += x
26:         return total
27:     
28:     def nPdir(dir):
29:         lst = [ x for x in glob.glob(dir + "photo[0-9][0-9]") if os.path.isdir(x)]
30:         return lst and int(lst[-1][-2:]) or 0
31:     
32:     def Search_Media(dir):
33:         """search Media and returns a hash
34:         whose keys are directory name and the values are lists of photo files."""
35:         def search_sub(dir):
36:             os.chdir(dir)
37:             items = os.listdir(dir)
38:             ls = [x for x in items if PREGEXP.search(x)]
39:             if ls:
40:                 hash[dir] = ls
41:             for d in [dir + x + '/' for x in items if os.path.isdir(x)]:
42:                 search_sub(d)
43:         hash={}
44:         search_sub(dir)
45:         return hash
46:     
47:     def on_empty():
48:         """called by Move_Photos if media is empty."""
49:         print "No photos in the media: give return"
50:         sys.stdin.readline()
51:         sys.exit()
52:             
53:     def on_error(f1, f2):
54:         """called by Move_Photos when filecopy is failed."""
55:         print "copy failed: %s => %s\n" % (f1, f2)
56:            
57:     def on_progress(sf, pt):
58:         """called by Move_Photos during coping files."""
59:         print "%d/%d\r" % (sf, pt),
60:             
61:     def Move_Photos(on_progress, on_empty, on_error, on_start, on_end):
62:         r"""
63:         Moveing photo files from media into HD,
64:         This function takes five parameters.
65:         1. pointer to function called during file copy
66:         2. pointer to function called if the media is empty
67:         3. pointer to function called when copyfile is failed
68:         4. pointer to function called before coping
69:         5. pointer to function called after coping
70:         """
71:         md = HD + date.today().strftime("%y-%m/")
72:         np = nPdir(md)
73:         hash = Search_Media(MEDIA)
74:         pt = sum([len(val) for val in hash.itervalues()])
75:         sf = 0
76:         i0 = True
77:     
78:         if not pt:
79:             on_empty()
80:                 
81:         if on_start:
82:             on_start(pt)
83:     
84:         if not os.path.isdir(md):
85:             os.mkdir(md)
86:             
87:         os.chdir(md)
88:     
89:         for d, fs in hash.iteritems():
90:             np += 1
91:             pd = md + "photo%02d" % np
92:             if i0:
93:                 pd0 = pd
94:                 i0 = False
95:             os.mkdir(pd)
96:             for f in fs:
97:                 f1 = d + f
98:                 f2 = pd + '/' + f
99:                 shutil.copyfile(f1, f2)
100:                 if filecmp.cmp(f1, f2):
101:                     os.remove(f1)
102:                 else:
103:                     on_error(f1, f2)
104:                 sf += 1
105:                 on_progress(sf, pt)
106:                 
107:         if on_end:
108:             on_end()
109:             
110:         os.execv(PHOTO_VIEWER, [' ', pd0])
111:     
112:     if __name__=='__main__':
113:         Move_Photos(on_progress, on_empty, on_error, None, None)
61 行目で Move_Photos を5つの関数を引数にとる関数として定義しています。また、Search_Media は Move_Photos が呼び出すようにしています。つまり、photo を import するスクリプトは Move_Photos だけを使えば良いようになっています。
引数としてとる5つの関数は以下の通りです。
  1. ファイルの移動の進行状況を表示する関数、全体のファイル数と、コピー済みファイル数の2つの引数をとる。
  2. メディアが空の場合その旨を表示する関数
  3. ファイルのコピーに失敗したときに呼び出される関数。コピー元ファイル名とコピー先ファイル名の2つの 引数をとる。
  4. ファイルの移動に先立って呼ばれる関数。全体のファイル数を引数に取る。
  5. ファイルの移動後に呼ばれる関数。
python は簡単に関数を引数として別の渡すことが出来るので、 関数の抽象化が促進されます。つまり、実際に処理を行う部分と gui を完全に分けることが出来、将来別の gui を使うとき変更する部分が 少なくて済みます。

photo_wx.py

01:     #! usr/bin/env python
02:     
03:     r"""
04:     script to achive photos in the removal media into HD
05:     gui version using wxPython
06:     by T.Shido
07:     January 22, 2005
08:     """
09:     import photo as ph
10:     import sys, wx, wx.lib.dialogs
11:     
12:     class Aphoto(wx.App):
13:         def OnInit(self):
14:             self.frame = wx.Frame(None)
15:             self.SetTopWindow(self.frame)
16:             ph.Move_Photos(self.on_progress, self.on_empty, self.on_error, self.on_start, self.on_end)
17:             self.pdlg.Destroy()
18:             frame.Destroy()
19:             return True
20:             
21:         def on_start(self, total):
22:             self.failed_log = ''
23:             self.pdlg = wx.ProgressDialog("Sending Photos",                                 \
24:                                          ("0/%d" % total),                                  \
25:                                           maximum = total,                                  \
26:                                           parent=self.frame,                                \
27:                                           style = wx.PD_AUTO_HIDE | wx.PD_APP_MODAL)
28:     
29:         def on_progress(self, count, total):
30:             self.pdlg.Update(count, ("%d/%d" % (count, total)))
31:             
32:         def on_error(self, f_from, f_to):
33:             self.failed_log += "copy failed: %s => %s\n" % (f_from, f_to)
34:     
35:         def on_empty(self):
36:             dlg = wx.MessageDialog(self.frame, 'No Photos in the media!',   \
37:                                    'Warning',                               \
38:                                     wx.OK | wx.ICON_INFORMATION )   
39:             dlg.ShowModal()
40:             dlg.Destroy()
41:             sys.exit()
42:     
43:         def on_end(self):
44:             if self.failed_log:                # if exist photos failed to archive 
45:                 fdlg = wx.lib.dialogs.ScrolledMessageDialog(self.frame, self.failed_log,            \
46:                                                           "Copy Failed for these Photos: Try Again!")
47:                 fdlg.ShowModal()
48:     
49:     if __name__ == '__main__':
50:         Aphoto(redirect=False)
Move_Photos を呼び出しているのは、16 行目です。21--47 行目で Move_Photos に与える 5 つの 関数を定義しています。
関数説明
on_startプログレスダイアログを作成します。
on_progressプログレスダイアログを更新します。
on_error移動に失敗したファイル名を記録します。
on_emptyメディアが空の場合、その旨を表示するダイアログを作成します。
on_end移動に失敗したファイルがある場合、それを表示するダイアログを作成します。
photo_wx.py は gui に関するコードだけになり、メンテナンスがやり易くなりました。

3. 終わりに

wxPython の使い方をごく簡単に説明しました。また、処理を行うコードと gui コードの分け方についても述べました。
最後に wxPython に関する web resourses を挙げておきます。