「PythonでGUIアプリ開発」に挑戦!Tkinter/Ttk編 その15
「PythonでGUIアプリ開発」に挑戦!Tkinter/Ttk編 その15ステップ01 ウインドウを表示するステップ02 ボタンでメッセージボックスステップ03 クラスでコーディングを楽にステップ04 「終了」メニューを作ろうステップ05 必須の「バージョン情報」ステップ06 ヘルプはWebサイトに置くステップ07 Textで複数行の処理ステップ08 スクロールバーを付けようステップ09 ウインドウの大きさを取得ステップ10 INIファイルに設定を保持ステップ11 空のメニュー項目を作るステップ12 ファイル処理を実装するおまけ .pyファイルのexe化結構苦労しましたが、ようやく動くようになりました。リストと実行結果は次のとおりです。# # ステップ12 ファイル処理を実装する# 『日経ソフトウエア』2021年9月号「特集1 PythonでGUIアプリ開発」の基礎(p.17~p.21)## リスト17~20 TextEdit12.py# # リスト17●「ファイル」→「開く」のイベントハンドラーmenuFileOpenメソッド。[(1)~(15)]# リスト18●TextEditクラスのコンストラクター(__ini__メソッド)に、複数のメソッドで共用する変数を記述する。[(16)~(22)]# リスト19●TextEditクラスの冒頭部にtextFilenameプロパティのコードを書く。[(23)~(34)]# リスト20●ファイル保存処理の実装。[(35)~(49)]# # リスト16に付け加えた部分は、(0)~(49)# モジュール(tkinter)のインポートfrom tkinter import *# モジュール(filedialog,messagebox,ttk)のインポート# filedialogモジュール:を表示する# messageboxモジュール:メッセージボックスを表示するfrom tkinter import filedialog, messagebox, ttk# (a)ScrolledTextモジュールをインポートするfrom tkinter.scrolledtext import ScrolledText# (0)configparser、os、webbrowserモジュールをインポートする。import configparser, os, webbrowser# 「TextEdit」クラスの定義class TextEdit: # # リスト19●TextEditクラスの冒頭部にtextFilenameプロパティのコードを書く。[(23)~(34)] # (23) _textFilename = '' # (24) プロパティは、変数のように参照したり、代入したりでき、その裏側に処理を仕込むことができる。 # textFilename関数の処理は、変数self._textFilenameの値を呼び出し、元に戻す。 # 他のコードは、textFilenameプロパティを変数のように利用できる。 # 参照した場合は、「@property」が付いたtextFilename関数が呼ばれる。 @property def textFilename(self): return self._textFilename # (25) 代入した場合は、「@textFilename.setter」が付いたtextFilename関数が呼ばれる。 # 呼び出し元から受け取った値は、変数「value」に入る。 @textFilename.setter def textFilename(self, value): # (26) 「value」を「self._textFilename」に代入。 self._textFilename = value # (27) 「value」が空の文字列かどうかを判定する。 if value == '': # (28) 「value」が空であった場合、タイトルバーの表示を「TextEdit」にする。 root.title(self.__class__.__name__) # (29) 「ファイル」→「保存」メニューを無効にする。 # 保存すべき先が未定の状態で「保存」が選ばれると困るため、無効にする。 self.menuFile.entryconfigure( '保存(S)', state=DISABLED) else: # (30) 絶対パス名からファイル名を抽出し、変数「s」に格納する。 s = os.path.basename(value) # (31) 「seilf.isSjis」が真であれば、半角スペースと「(SJIS)」を追加する。 # 「seilf.isSjis」が偽であれば、半角スペースと「 (UTF-8)」を追加する。 if self.isSjis: s += ' (SJIS)' else: s += ' (UTF-8)' # (32) タイトルバーに「s」を表示する。 root.title(s) # (33) 「ファイル」→「保存」メニューを有効にする。 self.menuFile.entryconfigure( '保存(S)', state=NORMAL) # (34) 変数「value」の文字列から、ディレクトリの部分を抽出し、self.directoryに代入する。 # この処理により、今回開いたフォルダーを記憶し、次にダイアログを開いた際に、そのフォルダーを開くようにする。 self.directory = os.path.dirname(value) def __init__(self, root): # # リスト18●TextEditクラスのコンストラクター(__ini__メソッド)に、複数のメソッドで共用する変数を記述する。[(16)~(22)] # # (16) ScrolledText # (b)Textウィジェットのインスタンスを作成し、self.textという名前で参照できるようにする。 self.text = ScrolledText(root) # self.textのpackメソッドを呼び出す。 # expandオプションを1に、fillオプションをBOTHに設定 # このような設定にしないと、ウインドウの大きさを変えた時に、テキスト領域がクライアント領域いっぱいに広がらない。 self.text.pack(expand=1, fill=BOTH) # (17)ファイルタイプの設定 # self.fileTypesの宣言・初期化 # self.を付けて宣言した変数は、クラスのメンバー変数になり、他のメソッドから利用できる。 self.fileTypes = [ ('テキストファイル', '*.txt'), ('すべてのファイル', '*.*')] # (18)self.directoryの宣言・初期化 # 初期値は、Windowsの「ドキュメント」フォルダーを指す文字列。 self.directory = os.getenv('HOMEDRIVE') \ + os.getenv('HOMEPATH') + '\\Documents' # 変数clientHeightとclientWidthを用意し、文字列50と300で初期化しておき、 # INIファイルからの値の読み込みが失敗した時(INIファイルがなかった時など)に初期値を使う。 clientHeight = '50' clientWidth = '300' # ConfigParserのインスタンスcpを用意する。 cp = configparser.ConfigParser() try: # INIファイルを読み込む準備 cp.read(self.__class__.__name__ + '.ini') # 「client」の「Height」を読み込む。 clientHeight = cp['Client']['Height'] # 「client」の「Width」を読み込む。 clientWidth = cp['Client']['Width'] # (19) self.directoryは、プログラム終了時に、(22)でINIファイルに書き込み、プログラム起動時に self.directory = cp['File']['Directory'] except: # で例外が発生した場合に、「TextEdit:Use default value(s)」とエラー出力にメッセージを出す。 print(self.__class__.__name__ + ':Use default value(s)', file=sys.stderr) # root.geometry(clientWidth + 'x' + clientHeight) # root.protocol('WM_DELETE_WINDOW', self.menuFileExit) # メニューの切り離し(tear off)を禁じるコード root.option_add('*tearOff', FALSE) # Menuクラスのインスタンスを生成し、名前「menu」で参照可能にする menu = Menu(root) # もう一つMenuクラスのインスタンスを生成し、名前「menuFile」で参照可能にする self.menuFile = Menu(menu) # menuの「add_cascade」メソッドを呼び出し、menu・label・underlineオプションを指定する # menuオプションに、「self.menuFile」を指定 # labelオプションに、メニューとして表示する文字列「ファイル(F)」を指定する。 # underlineオプションは、文字列の何番目に下線を引くかを設定するもので、 # 「F」は文字列「ファイル(F)」の6番目であるため、「5」(最初の文字が「0」)を設定。 menu.add_cascade(menu=self.menuFile, label='ファイル(F)', underline=5) # menuFileにコマンド「新規(N)」を追加 self.menuFile.add_command(label='新規(N)', underline=3, command=self.menuFileNew) # menuFileにコマンド「開く(O)」を追加 self.menuFile.add_command(label='開く(O)', underline=3, command=self.menuFileOpen) # menuFileにコマンド「保存(S)」を追加 self.menuFile.add_command(label='保存(S)', underline=3, command=self.menuFileSave) # menuFileにコマンド「保存(A)」を追加 self.menuFile.add_command( label='名前を付けてシフトJISで保存(A)', underline=16, command=self.menuFileSaveAsSjis) # menuFileにコマンド「保存(U)」を追加 self.menuFile.add_command( label='名前を付けてUTF-8で保存(U)', underline=15, command=self.menuFileSaveAsUtf8) # セパレータを追加 self.menuFile.add_separator() # menuFileにコマンド「終了(X)」を追加 self.menuFile.add_command(label='終了(X)', underline=3, command=self.menuFileExit) # もう一つMenuクラスのインスタンスを生成し、名前「menuHelp」で参照可能にする menuHelp = Menu(menu) # menuの「add_cascade」メソッドを呼び出し、menu・label・underlineオプションを指定する # menuオプションに、「menuHelp」を指定 # labelオプションに、メニューとして表示する文字列「ヘルプ(H)」を指定する。 # underlineオプションに、「H」に下線を引くため、「H」は文字列「ヘルプ(H)」の5番目なので、「4」を設定。 menu.add_cascade(menu=menuHelp, label='ヘルプ(H)', underline=4) # menuHelpにコマンド「Webサイトを開く(W)」を追加 menuHelp.add_command(label='Webサイトを開く(W)', underline=10, command=self.menuHelpOpenWeb) # 区切り線(セパレーター)を表示するコード menuHelp.add_separator() # menuHelpにコマンド「バージョン情報(V)」を追加 menuHelp.add_command(label='バージョン情報(V)', underline=8, command=self.menuHelpVersion) root['menu'] = menu # (20) self.menuFileNewの呼び出し # コンストラクターの最後なので、プログラム起動時にも実行される。 self.menuFileNew() # TextEditクラスに、メニューコマンドのイベントハンドラーを追加する。 # # 「ファイル」→「新規」メニューのイベントハンドラー def menuFileNew(self): # (21) self.isSjisの宣言・初期化 self.isSjis = TRUE self.textFilename ='' self.text.delete('1.0', 'end') # リスト17(1)~(15) # # ●「ファイル」→「開く」のイベントハンドラーmenuFileOpenメソッド。 def menuFileOpen(self): # (1) initialdirオプションに(18)で宣言・初期化したself.directoryを代入 filename = filedialog.askopenfilename( filetypes=self.fileTypes, initialdir=self.directory) # (2) filenameに文字列が入っていない場合に、この関数の処理を中断して抜けるコード。 if not filename: return # (3) 変数newTextの初期化 newText = '' try: # (4) ファイルを「読み込みモード」で読み込む。 f = open(filename, 'r') # (5) ファイルの内容を読み込みnewTextに格納する。 newText = f.read() # (6) (4)と(5)で例外が発生しなかった場合には、読み込んだテキストファイルは、 # 日本語Windowsのコマンドプロンプトで標準エンコーディングとなっている # 「シフトJIS(CP932)」であろうと判断し、self.isSjisを真(TRUE)にする。 self.isSjis = TRUE except: # (7) (4)~(6)で例外が発生した場合、ファイルを「UTF-8」エンコーディングで開く。 f = open(filename, 'r', encoding='UTF-8') # (8) ファイルの内容を読み込みnewTextに格納する。 newText = f,read() # (9) self.isSjisを偽(FALSE)にする。 self.isSjis = FALSE finally: # (10) 途中で例外が発生してもファイルをクローズする。 # このコードは簡易なものなので、エンコーディングの判定がうまくいかないことがある。 f.close() # (11) newTextに何らかのテキストが入ってきたかを判定する。 if newText == '': # (12) 「ファイルを開けませんでした」という黄色い警告マーク付きのメッセージボックスを表示。 messagebox.showwarning( self.__class__.__name__, 'ファイルを開けませんでした') else: # (13) newTextに内容があった場合は、self.textの内容を消去する。 # 1番目の引数「'1.0'」は、1行目0文字目を、2番目の「'end'」は最後を示すします。 self.text.delete('1.0', 'end') # (14) newTextをself.textの先頭に挿入する。 self.text.insert('1.0', newText) # (15) self.textFilenameにfilenameにする。 # self.textFilenameは、「プロパティ(propaty)」で、そのコードはリスト19。 self.textFilename = filename # リスト20●ファイル保存処理の実装。[(35)~(49)] # (35) 「保存」のイベントハンドラー # self.textFilenameとself.isSjisを使って、(36)のfilesaveを呼ぶ。 def menuFileSave(self): self.fileSave(self.textFilename, self.isSjis) # (36) fileSaveメソッド def fileSave(self, saveFilename, saveIsSjis): # (37) self.textの内容を変数sに代入する。 s =self.text.get('1.0', 'end') # (38) sの長さが1であれば(末尾に改行が入っていて、0にならない。)、 # 「保存するテキストがありません」と表示して、メソッドを抜ける。 if len(s) == 1: messagebox.showwarning( self.__class__.__name__, '保存するテキストがありません') return # (39) 受け取ったsaveIsSjisの値が真であれば、エンコーディングをシフトJISにして開き、 # そうでなければ、UTF-8でファイルを開く。 if saveIsSjis == TRUE: f = open(saveFilename, 'w') else: f = open(saveFilename, 'w', encoding='UTF-8') # (40) sの最後の1文字(改行)を除く部分をファイルに書き込む。 f.write(s[:-1]) # (41) ファイル変数fを閉じる。 f.close() # (42) saveIsSjisの値をself.isSjisに代入する。 self.isSjis = saveIsSjis # (43) saveFilenameをself.textFilenameに代入する。 self.textFilename = saveFilename # (44) 「名前を付けてシフトJISで保存」イベントハンドラー # TRUEを引数にして(46)のfileSaveAsメソッドを呼ぶ。 def menuFileSaveAsSjis(self): self.fileSaveAs(TRUE) # (45) 名前を付けてシフトUTF-8で保存」イベントハンドラー # FALSEを引数にして(46)のfileSaveAsメソッドを呼ぶ。 def menuFileSaveAsUtf8(self): self.fileSaveAs(FALSE) # (46) fileSaveAsメソッド def fileSaveAs(self, saveIsSjis): # (47) 「名前を付けて保存」ダイアログを開く。 # self.fileTypesを使って、ファイルの種類欄を設定し、 # 開くフォルダーを適切に設定し、ファイル名も、 # 今編集中のファイル名があれば、それを提示する。 filename = filedialog.asksaveasfilename( filetypes=self.fileTypes, initialdir=self.directory, initialfile=os.path.basename( self.textFilename)) # (48) ユーザーがダイアログをキャンセルした場合にメソッドを抜けるコード。 if not filename: return # (49) (36)のfileSaveメソッドを呼び出す。 self.fileSave(filename, saveIsSjis) def menuFileExit(self): # ConfigParserクラスのインスタンスを生成し、cpという名前で参照できるようにする。 cp = configparser.ConfigParser() #「Client]というセクションに「Height」というキーを作り、その値としてクライアント領域の高さを文字列にしたものを与える。 # 同じセクションに「Width」というキーを作り、その値としてクライアント領域の幅を文字列にしたものを与える。 cp['Client'] = { 'Height': str(root.winfo_height()), 'Width': str(root.winfo_width())} # (22)プログラム終了時に、self.directoryをINIファイルに書き込む。 cp['File'] = {'Directory': self.directory} #「TextEdit.」というファイルを書き込み可能にして開き、それをconfigfileという名前で参照できるようにし、configfileにデータを書き込む。 with open(self.__class__.__name__ + '.ini', 'w') as f: cp.write(f) # プログラムを終了する。 root.destroy() # 「Webサイトを開く(W)」メニューの処理 def menuHelpOpenWeb(self): #変数sを宣言し、そこにクラス名「TextEdit」を入れる webbrowser.open( 'https://info.nikkeibp.co.jp/media/NSW/') # バージョン情報メニューの処理 def menuHelpVersion(self): # 変数sを宣言し、そこにクラス名「TextEdit」を入れる s = self.__class__.__name__ # バージョン番号(0.01)、年月日、改行(\n) s += ' Version 0.01(2021/03/10)\n' # 著作権表記 s += ' ©2021 Hideo Harada\n' # Pythonのシステム情報 s += 'with Python ' + sys.version # 変数sに格納された文字列をメッセージボックスに表示する messagebox.showinfo( self.__class__.__name__, s) # 「root」root = Tk()TextEdit(root)# root.mainloopの開始root.mainloop()リスト17~20の実行結果