Tkinterで画像ビューワを作る 続き


前回作成した画像ビューワをもうちょっと改善していきます
今回は方向キーで次の画像を表示するようにします
この機能を実現するためには
・キーイベントを取得する
・フォルダ内のファイル一覧を取得する
が出来ればとりあえず動きそうです

では早速力尽くでコーディングしていきます
キー入力の取得は以下の一文でとれます。
何かキーが押されたときにkey_funcが呼ばれます


```python
self.root.bind("<KeyPress>", self.key_func)
```

フォルダ内のファイル一覧取得は調べるといくつか方法があります
今回はos.listdirで取得して拡張子をos.path.splitext()で調べることにします
フォルダ内の画像ファイルを配列に格納して今表示されている画像の配列添え字を
別に保持しておき、キーを押したときにその番号を足したり引いたりして次に表示するファイルを決定します

```python
def __init__(self):
    self.file_no = 0        # 表示している画像の配列添え字
    self.file_array = []    # 画像ファイル配列
    # 後略

# ファイル読み込みダイアログ
def fg(self):
    file_type = [("画像ファイル", "*.jpg;*.png;*.bmp")]
    file_path = tkinter.filedialog.askopenfilename(filetypes=file_type, initialdir=PICTURE_DIR)
    # ファイルが選択されていたら画像変更
    if file_path != "":
        self.image_change(file_path)        # 画像を変更
        self.create_file_list(file_path)    # ディレクトリのファイル一覧を取得

# フォルダ内の画像ファイルリストを作成
def create_file_list(self, file_path):
    self.file_array = []                  # 画像ファイル配列
    tmp_arr = os.path.split(file_path)    # ファイルのパスをディレクトリとファイル名に分解
    dir_name = tmp_arr[0]      # ディレクトリ名
    file_name = tmp_arr[1]     # ファイル名
    n = 0
    for fname in os.listdir(dir_name):                  # ディレクトリ内のファイル一覧を取得
        file_ext = os.path.splitext(fname)[1].lower()   # 拡張子を小文字にして取得
        if file_ext == ".jpg" or file_ext == ".png" or file_ext == ".tif" or file_ext == ".jpeg" :
            self.file_array.append(os.path.join(dir_name ,fname))   # 画像ファイルのとき、配列に格納
            if file_name == fname:
                self.file_no = n                # 開いたファイルの配列番号を残しておく
            n += 1

# キー押されたとき
def key_func(self, event):
    cnt = len(self.file_array)
    if cnt == 0:                                # 画像ファイル配列が空の時何もしない
        return
    if event.keysym == "Right":                 # 右キーのとき 配列の次のファイル
        self.file_no += 1
        if cnt == self.file_no:                 # 配列上限オーバー時は0に戻る
            self.file_no = 0
    elif event.keysym == "Left"::               # 左キーのとき 配列の一個前のファイル
        self.file_no -= 1
        if self.file_no == -1:                  # 一番最初より前のとき最後のファイルにする
            self.file_no = cnt - 1
    else:
        return                                  # その他のキーでは何もしない
    file_path = self.file_array[self.file_no]   # どのファイルを表示するか決定
    self.string_v.set(file_path)                # 表示するファイルパスをラベルに表示
    self.ti.img_change(file_path)               # 画像の変更            

# 画像変更
def image_change(self, file_path):
    # キャンバスサイズ取得
    self.check_canvas_size()
    try:
        # self.img = tkinter.PhotoImage(file=file_path)# ファイルを読み込んでPhotoImageオブジェクトを作成
        self.img_pil = Image.open(file_path)           # PhotoImageオブジェクト作成をpillowで行う
        img_pil2 = ImageOps.pad(self.img_pil, (self.picture_width, self.picture_height))
        self.img = ImageTk.PhotoImage(image=img_pil2)
        self.canvas.itemconfig(self.canvas_img, image=self.img)     # 画像を変更
        self.string_v.set(file_path)                   # ファイル名をラベルに表示
    except:
        self.img = None
```

今までfgの中でのみ画像を変更できたのがよそでも変更できるようになったので
fg関数の中にあった画像変更処理を新しく関数にしました

## 前回終了時のウィンドウ位置と大きさを覚えておきたい

何回か使ってみると起動の度に大きさが初期値の戻るのが面倒になってきたので
ウィンドウ位置と大きさを覚えておいて次回起動時に同じ位置に表示するようにします

必要な処理は以下三つ
・ウィンドウ位置と大きさの取得
・アプリケーションの終了イベントの取得
・ファイルへの読み込み書き込み
tkinterの位置と大きさはroot.geometry()を引数なしで呼ぶと取得できます

試しに呼んでみると幅x高さ+横方向位置+縦方向位置という風に出力されます
この値はウィンドウ生成時に使えるのでそのまま保存しえて起動時に読み込んで割り当てることにしましょう
ウィンドウ終了時のイベントはroot.protocol("WM_DELETE_WINDOW", func)で取得できます
呼ばれ先のfuncの最後にroot.destroy()を入れ忘れるとアプリケーションが落とせなくなります

```python
root.protocol("WM_DELETE_WINDOW", func)  # ウィンドウ終了時にfuncを呼ぶ

def func():
    # 処理...
    root.destroy()    # ウィンドウを閉じる これを忘れると閉じられなくなる
```

後はこれらを組み合わせるだけですね
最後に全体を掲示します

```python:TkinterTestClass.py
# -*- coding:utf-8 -*-
import os
import tkinter
import tkinter.filedialog
from PIL import Image, ImageTk, ImageOps

PICTURE_DIR = os.getenv("HOMEDRIVE") + os.getenv("HOMEPATH") + "\\Pictures"    # マイピクチャ
SETTING_FILE = "setting.txt"

class TkinterTestClass:

    def __init__(self):
        self.pre_info = []                      # ファイル保存データ
        self.settingfile_read()                 # 前回保存データ読み込み
        geometry = "500x350"                    # 画面サイズ初期値
        if len(self.pre_info) > 0:              # 前回データの読み込みに成功したとき
            geometry = self.pre_info[0]
        self.root = tkinter.Tk()
        self.root.title("無題")
        self.root.geometry(geometry)
        frame_c = tkinter.Frame(self.root)      # 制御エリア
        frame_b = tkinter.Frame(self.root)      # 画像描画エリア
        # ファイル読み込みボタン
        button = tkinter.Button(frame_c, font=("メイリオ", "10", "bold"), text="読込", command=lambda: [self.fg()])
        button.pack(side=tkinter.LEFT)
        # ラベル制御用StringVar
        self.string_v = tkinter.StringVar()
        self.string_v.set("")
        # ファイル名表示用ラベル
        label = tkinter.Label(frame_c, textvariable=self.string_v, bg="white")
        label.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH)
        
        frame_c.pack(fill=tkinter.X)
        frame_b.pack(expand=True, fill=tkinter.BOTH, padx=1, pady=1)
        # 画像表示用キャンバス作成
        self.img = None
        self.img_pil = None
        self.picture_width = 0
        self.picture_height = 0

        self.canvas = tkinter.Canvas(frame_b, bg="white")  
        self.canvas_img = self.canvas.create_image(0, 0, anchor=tkinter.NW)
        self.canvas.pack(expand=True, fill=tkinter.BOTH)  # キャンバス描画

        self.file_array = []    # 画像ファイル配列
        self.file_no = 0        # 表示しているファイル

        if len(self.pre_info) > 1:              # 前回データの読み込みに成功したとき
            file_path = self.pre_info[1]
            self.image_change(file_path)        # 画像を変更
            self.create_file_list(file_path)    # ディレクトリのファイル一覧を取得
            self.string_v.set(file_path)

        self.root.bind('<Configure>', self.resize_root) # rootの大きさや位置を変更したときのイベント
        self.root.bind("<KeyPress>", self.key_func)
        self.root.protocol("WM_DELETE_WINDOW", self.settingfile_write)
        self.root.mainloop()

    # フォルダ内の画像ファイルリストを作成
    def create_file_list(self, file_path):
        self.file_array = []                  # 画像ファイル配列
        tmp_arr = os.path.split(file_path)    # ファイルのパスをディレクトリとファイル名に分解
        dir_name = tmp_arr[0]      # ディレクトリ
        file_name = tmp_arr[1]     # ファイル名
        n = 0
        for fname in os.listdir(dir_name):    # ディレクトリ内のファイル一覧を取得
            file_ext = os.path.splitext(fname)[1].lower()   # 拡張子を小文字にして取得
            if file_ext == ".jpg" or file_ext == ".png" or file_ext == ".tif" or file_ext == ".jpeg" :
                self.file_array.append(os.path.join(dir_name ,fname))   # 画像ファイルのとき、配列に格納
                if file_name == fname:
                    self.file_no = n                # 開いたファイルの配列番号を残しておく
                n += 1

    # 画像変更
    def image_change(self, file_path):
        # キャンバスサイズ取得
        self.check_canvas_size()
        try:
            # self.img = tkinter.PhotoImage(file=file_path)# ファイルを読み込んでPhotoImageオブジェクトを作成
            self.img_pil = Image.open(file_path)           # PhotoImageオブジェクト作成をpillowで行う
            # 2022-02-21 修正 ここから
            if self.picture_width == 0 or self.picture_height == 0:
                self.img = ImageTk.PhotoImage(image=self.img_pil)       # キャンバスサイズの縦横どちらかが0の時 拡大縮小しない
                self.canvas.itemconfig(self.canvas_img, image=self.img)
            else:
                img_pil2 = ImageOps.pad(self.img_pil, (self.picture_width, self.picture_height))
                self.img = ImageTk.PhotoImage(image=img_pil2)
            # 2022-02-21 修正 ここまで
            self.canvas.itemconfig(self.canvas_img, image=self.img)     # 画像を変更
            self.string_v.set(file_path)            # ファイル名をラベルに表示
        except:
            self.img = None
    
    # キー押されたとき
    def key_func(self, event):
        cnt = len(self.file_array)
        if cnt == 0:                                # 画像ファイル配列が空の時何もしない
            return
        if event.keysym == "Right":                 # 右キーのとき 配列の次のファイル
            self.file_no += 1
            if cnt == self.file_no:                 # 配列上限オーバー時は0に戻る
                self.file_no = 0
        elif event.keysym == "Left":                # 左キーのとき 配列の一個前のファイル
            self.file_no -= 1
            if self.file_no == -1:                  # 一番最初より前のとき最後のファイルにする
                self.file_no = cnt - 1
        else:
            return                                  # その他のキーでは何もしない
        
        file_path = self.file_array[self.file_no]   # どのファイルを表示するか決定する
        self.string_v.set(file_path)                # 表示するファイルパスをラベルに表示
        self.image_change(file_path)                # 画像の変更

    # ファイル読み込みダイアログ
    def fg(self):
        file_type = [("画像ファイル", "*.jpg;*.png;*.bmp")]
        file_path = tkinter.filedialog.askopenfilename(filetypes=file_type, initialdir=PICTURE_DIR)
        # ファイルが選択されていたら画像変更
        if file_path != "":
            self.image_change(file_path)        # 画像を変更
            self.create_file_list(file_path)    # ディレクトリのファイル一覧を取得

    # メインウィンドウの大きさを変えたとき
    def resize_root(self, event):
        # イメージが無いときは何もしない
        if self.img_pil is None:
            return False
        # サイズを変更する必要があるかチェック
        if self.check_canvas_size():
            # イメージをリサイズ
            img_pil2 = ImageOps.pad(self.img_pil, (self.picture_width, self.picture_height))
            self.img = ImageTk.PhotoImage(image=img_pil2)
            self.canvas.itemconfig(self.canvas_img, image=self.img)

    # キャンバスサイズ取得
    def check_canvas_size(self):
        canvas_width = 0
        canvas_height = 0
        try:
            canvas_width = self.canvas.winfo_width()
            canvas_height = self.canvas.winfo_height()
        except:
            return False    # キャンバスサイズが取得できないときFalse
        # キャンバスサイズの縦横どちらかが2より小さいならならFalse
        if canvas_width < 2 or canvas_height < 2:
            return False
        # 前回のサイズから変わってなければFalse
        if canvas_width == self.picture_width and canvas_height == self.picture_height:
            return False
        # キャンバスサイズを記憶しておく
        self.picture_width = canvas_width
        self.picture_height = canvas_height
        return True

    # 設定ファイル読み込み
    def settingfile_read(self):
        try:
            with open(SETTING_FILE) as f:
                s = f.read()
            self.pre_info = s.splitlines()
        except FileNotFoundError:
            pass

    # 設定ファイル書き込み
    def settingfile_write(self):
        str_tmp = self.root.geometry() + "\n"       # メインウィンドウの位置と大きさ
        if len(self.file_array)> 0 :
            str_tmp += self.file_array[self.file_no] + "\n"
        with open(SETTING_FILE, mode="w") as f:
            f.write(str_tmp)
        self.root.destroy()     # ウィンドウを閉じる これを忘れると閉じられなくなる

if __name__ == '__main__':
    TkinterTestClass()
```

今回はここまで


更新履歴 2022-02-21
本文誤字修正　引数梨　-> 引数なし
ソース修正 
　・print(len(self.pre_info))  デバッグ用のprintが残っていたので除去
　・キャンバスサイズが取得できなかった時にZeroDivisionErrorが発生していたにもかかわらず
　　try-exceptで握りつぶしていた箇所を修正

```python:TkinterTestClass.py image_change変更箇所
# 画像変更
def image_change(self, file_path):
    # キャンバスサイズ取得
    self.check_canvas_size()
    try:
        # self.img = tkinter.PhotoImage(file=file_path)# ファイルを読み込んでPhotoImageオブジェクトを作成
        self.img_pil = Image.open(file_path)           # PhotoImageオブジェクト作成をpillowで行う
        # ここでZeroDivisionErrorが発生する(修正前)
        # img_pil2 = ImageOps.pad(self.img_pil, (self.picture_width, self.picture_height))
        # self.img = ImageTk.PhotoImage(image=img_pil2)
        
        # キャンバスサイズがどちらか0の時 拡大縮小しないように変更
        if self.picture_width == 0 or self.picture_height == 0:
            self.img = ImageTk.PhotoImage(image=self.img_pil)
            self.canvas.itemconfig(self.canvas_img, image=self.img)
        else:
            img_pil2 = ImageOps.pad(self.img_pil, (self.picture_width, self.picture_height))
            self.img = ImageTk.PhotoImage(image=img_pil2)

        self.canvas.itemconfig(self.canvas_img, image=self.img)     # 画像を変更
        self.string_v.set(file_path)            # ファイル名をラベルに表示
    except:
        self.img = None
```