Tkinterで画像ビューワを作る 第３回

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

[前々回](https://qiita.com/kuwabatake/items/b5b98b78cc1ba3ed4623)、[前回](https://qiita.com/kuwabatake/items/cc1fe15f67269258cd01)作成した画像ビューワを更に改良していきます
今回は
- 全画面表示
- ファイルの削除

の二つをやっていきます

## 全画面表示
アプリケーションを全画面表示にします
windowsではroot.attributesで'-fullscreen',Ubuntuでは'-zoomed'で更新できます

```python:Windows全画面
root.attributes('-fullscreen', False)
```

```python:Ubuntu全画面
root.attributes('-zoomed', True)
```

windowsしか持っていないので、以後windowsのみ説明します
attributes('-fullscreen')は二番目の引数なしで呼ぶと現在の状態が取得できるので
現在の状態を保存しておく用の変数が要らないです

```python:全画面
if root.attributes('-fullscreen'):         # 全画面表示のとき
    root.attributes('-fullscreen', False)  # 全画面表示解除
else:
    root.attributes('-fullscreen', True)   # 全画面表示にする
```

基本的にはこれで動くのですが、私の環境だと全画面を戻した後ウィンドウが最前面に固定されてしまいました。なので全画面表示を解除したときに最前面も解除することにします
普通は呼ぶ必要ないですが呼んでも特に害は無いのでそのまま載せておきます

```python:最前面解除
root.attributes('-topmost', False)  # 最前面解除
```

さて画面は最大化しましたが上の方に出ている読込ボタンとかが邪魔なので
最大化したときにこれを除去することにしましょう
packで設置したフレームはpack_forget()で表示から消し
再度packすることで表示できます
ただしpackは先にあるモノの後にどんどん積んで表示されるので
今表示されている描画用フレームより前に挿入する必要があります
![位置が変わる](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2532786/ade7b13d-0662-9184-f189-da7d4ec85f04.png "位置が変わる") 
　　　　　fig.普通にpackするだけだと位置が変わってしまう

先にpackしてあるものより前に挿入するには以下のようにします

```python:frame_2の前にframe_1を挿入
frame_1.pack(fill=tkinter.X, before=frame_2)
```

後はこれをボタンなりショートカットキーで呼び出すだけです
せっかくなので他のアプリに合わせてF11キーに割り当てましょう
rootに新しくbindしても良いのですが前回左右キーを取得するために<KeyPress>をbind済みなので
このkey_func関数内に継ぎ足していきましょう

```python:全画面表示機能追加
root.bind("<KeyPress>", self.key_func)

def key_func(self, event):
    # 前略
    if event.keysym == "F11":
        if self.root.attributes('-fullscreen'):         # 全画面の時
            self.root.attributes('-fullscreen', False)  # 全画面解除
            self.root.attributes('-topmost', False)     # 最上位解除
            self.frame_c.pack(fill=tkinter.X, before=self.frame_b)  # 上部フレームを再表示
        else:
            self.root.attributes('-fullscreen', True)   # 全画面
            self.frame_c.pack_forget()                  # 上部フレームを除去
    elif event.keysym == "Escape":
        if self.root.attributes('-fullscreen'):         # 全画面の時
            self.root.attributes('-fullscreen', False)  # 全画面解除
            self.root.attributes('-topmost', False)     # 最上位解除
            self.frame_c.pack(fill=tkinter.X, before=self.frame_b)  # 上部フレームを再表示
```

# ファイルの削除
ファイルの削除はos.remove()で出来ますがこれでそのまま削除すると間違ったときに取り返しがつかなくなるので、通常はゴミ箱に移動させます
ゴミ箱に移動はsend2trashを使えば一発で出来ます

```python:ファイルをゴミ箱に入れる
import send2trash
send2trash.send2trash('kari.jpg')
```

send2trashは標準モジュールではないのでpipなりcondaなりでインストールしてください
ですが私の環境でpipのインストールがなんかうまくいかなかったのでwin32apiでゴミ箱に移動させてみました。当然windows専用になります

```python:ファイルをゴミ箱に入れる win32api
import ctypes

FO_DELETE = 0x0003      # 削除
FOF_ALLOWUNDO = 0x0040	# ごみ箱に入れる

class SHFILEOPSTRUCT(ctypes.Structure):
    _fields_ = [("hwnd", ctypes.c_void_p),
                ("wFunc", ctypes.c_int32),
                ("pFrom", ctypes.c_wchar_p),
                ("pTo", ctypes.c_wchar_p),
                ("fFlags", ctypes.c_ushort),
                ("fAnyOperationsAborted", ctypes.c_bool),
                ("lpszProgressTitle", ctypes.c_wchar_p)
                ]

def send_trash_exec(file_path):
    file_path_del = file_path + "\x00\x00"  # ヌル文字二個追加
    shell32 = ctypes.WinDLL("shell32")
    shell32.SHFileOperationW.restype = ctypes.c_int32
    shell32.SHFileOperationW.argtypes = (ctypes.POINTER(SHFILEOPSTRUCT),)
    shfs = SHFILEOPSTRUCT()
    shfs.hwnd = None
    shfs.wFunc = FO_DELETE
    shfs.pFrom = file_path_del
    shfs.pTo = None
    shfs.fFlags = FOF_ALLOWUNDO
    shfs.lpszProgressTitle = None
    shell32.SHFileOperationW(shfs)
```

こんなことするよりsend2trashモジュールをインストールした方が早くて確実です
おすすめはしません。標準以外のモジュールを使いたくなって人はたまに居ますが、そもそもすでにpillowが無いと動かないですしね

ではこれを前回までのコードに組み込んでいきます
デフォルトコンストラクタがかなり長くなってきたのでインスタンス変数の初期化とウィンドウの初期設定部分を分けます。以下最終コード

```python:TkinterTestClass
# -*- coding:utf-8 -*-

import os
import ctypes
import tkinter
import tkinter.filedialog
from PIL import Image, ImageTk, ImageOps

FO_DELETE = 0x0003              # SHFILEOPSTRUCT構造体用定数 削除
FOF_ALLOWUNDO = 0x0040		    # SHFILEOPSTRUCT構造体用定数 ごみ箱に入れる

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

# win32api SHFileOperationW用構造体の定義
class SHFILEOPSTRUCT(ctypes.Structure):
    _fields_ = [("hwnd", ctypes.c_void_p),
                ("wFunc", ctypes.c_int32),
                ("pFrom", ctypes.c_wchar_p),
                ("pTo", ctypes.c_wchar_p),
                ("fFlags", ctypes.c_ushort),
                ("fAnyOperationsAborted", ctypes.c_bool),
                ("lpszProgressTitle", ctypes.c_wchar_p)
                ]

class TkinterTestClass:

    def __init__(self):
        self.pre_info = []  # setting.txtの中身
        self.root = None        # メインウィンドウ
        self.string_v = None    # ラベルの文字
        self.frame_c = None     # フレーム コントロールパネル
        self.frame_b = None     # フレーム 画像描画部
        self.img = None
        self.img_pil = None
        self.picture_width = 0
        self.picture_height = 0
        self.file_array = []    # 画像ファイル配列
        self.file_no = 0        # 表示しているファイル

        self.init()
    
    # 初期設定
    def init(self):
        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)
        self.frame_c = tkinter.Frame(self.root)  # 制御エリア
        self.frame_b = tkinter.Frame(self.root)  # 画像描画エリア
        # ファイル読み込みボタン
        button = tkinter.Button(self.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(self.frame_c, textvariable=self.string_v, bg="white")
        label.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH)

        self.frame_c.pack(fill=tkinter.X)
        self.frame_b.pack(expand=True, fill=tkinter.BOTH, padx=1, pady=1)
        # 画像表示用キャンバス作成
        self.canvas = tkinter.Canvas(self.frame_b, bg="white")
        self.canvas_img = self.canvas.create_image(0, 0, anchor=tkinter.NW)
        self.canvas.pack(expand=True, fill=tkinter.BOTH)  # キャンバス描画

        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で行う
            if self.picture_width == 0 or self.picture_height == 0:
                self.img = ImageTk.PhotoImage(image=self.img_pil)
            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

    # キー押されたとき
    def key_func(self, event):
        mode = -1
        if event.keysym == "Right":
            self.next_picture(1)
        elif event.keysym == "Left":
            self.next_picture(-1)
        elif event.keysym == "Escape":
            if self.root.attributes('-fullscreen'):         # 全画面の時
                self.root.attributes('-fullscreen', False)  # 全画面解除
                self.root.attributes('-topmost', False)     # 最前面解除
                self.frame_c.pack(fill=tkinter.X, before=self.frame_b)  # コントロールパネルを復旧
                self.frame_c.pack(fill=tkinter.X)
        elif event.keysym == "F11":
            if self.root.attributes('-fullscreen'):         # 全画面の時
                self.root.attributes('-fullscreen', False)  # 全画面解除
                self.root.attributes('-topmost', False)     # 最前面解除
                self.frame_c.pack(fill=tkinter.X, before=self.frame_b)  # コントロールパネルを復旧
            else:
                self.root.attributes('-fullscreen', True)   # 全画面に切り替え
                self.frame_c.pack_forget()  # コントロールパネルを除去
        elif event.keysym == "Delete":
            self.send_trash()
        else:
            pass

    # 次のファイルを表示
    # offset: 1:次のファイル
    #        -1:前のファイル
    def next_picture(self, offset):
        cnt = len(self.file_array)
        if cnt == 0:  # 画像ファイル配列が空の時何もしない
            return False
        file_no = self.file_no
        file_path = ""
        while file_path == "":
            self.file_no += offset
            if self.file_no == cnt:     # 配列上限オーバー時は0に戻る
                self.file_no = 0
            elif self.file_no == -1:    # 配列上限オーバー時は0に戻る
                self.file_no = cnt - 1
            file_path = self.file_array[self.file_no]  # どのファイルを表示するか決定する
            if file_no == self.file_no:
                break
        if file_path == "":  # 配列を一周して全部空だったとき
            self.file_array = []
            self.file_no = 0
            self.img_pil = None
            self.img = None
            self.canvas.itemconfig(self.canvas_img, image=self.img)
            return False
        self.image_change(file_path)  # 画像の変更

    # ゴミ箱へ送る
    def send_trash(self):
        file_path = self.file_array[self.file_no]
        # send2trash.send2trash(file_path)
        self.send_trash_exec(file_path)
        self.file_array[self.file_no] = ""
        self.next_picture(1)

    # ファイルをゴミ箱へ送る
    def send_trash_exec(self, file_path):
        file_path_del = file_path + "\x00\x00"    # ヌル文字二個追加
        shell32 = ctypes.WinDLL("shell32")
        shell32.SHFileOperationW.restype = ctypes.c_int32
        shell32.SHFileOperationW.argtypes = (ctypes.POINTER(SHFILEOPSTRUCT),)
        shfs = SHFILEOPSTRUCT()
        shfs.hwnd = None
        shfs.wFunc = FO_DELETE
        shfs.pFrom = file_path_del
        shfs.pTo = None
        shfs.fFlags = FOF_ALLOWUNDO
        shfs.lpszProgressTitle = None
        shell32.SHFileOperationW(shfs)

    # ファイル読み込みダイアログ
    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()
```