Tkinterで画像ビューワを作る

Tkinterでwindowsのフォトビューワを目指して作っていきます

# まず画像を表示するウィンドウ作成から


```python:TkinterTestClass.py
# -*- coding:utf-8 -*-
import os
import tkinter
import tkinter.filedialog

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

class TkinterTestClass:

    def __init__(self):
        geometry = "500x350"                   # 画面サイズ初期値
        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.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.root.mainloop()

    # ファイル読み込みダイアログ
    def fg(self):
        file_type = [("画像ファイル", "*.jpg;*.png;*.bmp")]
        file_path = tkinter.filedialog.askopenfilename(filetypes=file_type, initialdir=PICTURE_DIR)
        # ファイルが選択されていたら画像変更
        if file_path != "":
            try:
                self.img = tkinter.PhotoImage(file=file_path)  # ファイルを読み込んでPhotoImageオブジェクトを作成
                self.canvas.itemconfig(self.canvas_img, image=self.img)     # 画像を変更
                self.string_v.set(file_path)                   # ファイル名をラベルに表示
            except:
                self.img = None

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

本当に読み込んで表示ぐらいしか出来ません
pythonのバージョンなのかtkinterのバージョンなのか分かりませんが環境によってjpegが読めたり読めなかったりします
適当な画像が無かったのでペイントで適当に作った画像を表示させてみました
![result1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2532786/cbb962eb-af1b-51a2-d107-e8a2c45914e0.png)

***
# 気に入らないところを修正していく

* jpegが描画出来ずに白くなってしまうのでjpgを描画出来るようにする
* 原寸表示しか出来ないので、画面サイズによって拡大縮小する

次にこの二つを修正していきましょう
とはいえ自作なんか出来ないのでpillowで解決します
jpegを読めるようにするにはファイルを読み込んでいる場所をtkinterからpillowに変えるだけです
```python:TkinterTestClass追加部分
from PIL import Image, ImageTk, ImageOps    # pilllowのインポート

# self.img = tkinter.PhotoImage(file=file_path)
img_pil = Image.open(file_path)     # PhotoImageオブジェクト作成をpillowで行う
self.img = ImageTk.PhotoImage(image=img_pil)
```

簡単ですね

次に画像のサイズ変更ですが、サイズの変更だけなら以下で出来ます

```python:TkinterTestClass変更部分
canvas_width = self.canvas.winfo_width()       # キャンバスのサイズを取得
canvas_height = self.canvas.winfo_height()
img_pil = Image.open(file_path)           # PhotoImageオブジェクト作成をpillowで行う
img_pil2 = ImageOps.pad(img_pil, (self.picture_width, self.picture_height))
self.img = ImageTk.PhotoImage(image=img_pil2)
self.canvas.itemconfig(self.canvas_img, image=self.img)
```

しかしこれだとウィンドウの大きさを変えた後に手動で再読み込みする必要があり面倒なので
ウィンドウサイズを変更したときに自動で追従するようにします

```python:TkinterTestClass def __init__(self)変更部分
self.img_pil = None      # 読み込んだ画像を記憶しておく
self.picture_width = 0   # 現在の画像サイズを記憶しておく
self.picture_height = 0
self.root.bind('<Configure>', self.resize_root) # rootの大きさや位置を変更したときのイベント
self.root.mainloop()
```

img_pil, picture_width ,picture_height をインスタンス変数にして記憶しておきます
またメインウィンドウの変更イベントでresize_rootが呼ばれるようにしました
次にこのresize_root関数を作成します

```python:TkinterTestClass追加部分
# キャンバスサイズ取得
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 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)
```

まずキャンバスのサイズを取得して現在のサイズと比較し違いがあれば画像のサイズを変更します
チェックせずに毎回変更しても良いのですが、resize_rootはちょっとの移動でかなりの回数呼ばれることが分かったので必要なときだけサイズ変更するようにしました
このほかに読み込んだときにもサイズを変えてあげないといけないので同じように仕込みます
最終形がこちら
```python:TkinterTestClass
# -*- 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"    # マイピクチャ

class TkinterTestClass:

    def __init__(self):
        geometry = "500x350"                   # 画面サイズ初期値
        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.root.bind('<Configure>', self.resize_root) # rootの大きさや位置を変更したときのイベント
        self.root.mainloop()

    # ファイル読み込みダイアログ
    def fg(self):
        file_type = [("画像ファイル", "*.jpg;*.png;*.bmp")]
        file_path = tkinter.filedialog.askopenfilename(filetypes=file_type, initialdir=PICTURE_DIR)
        # ファイルが選択されていたら画像変更
        if 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

    # メインウィンドウの大きさを変えたとき
    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

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

今回はここまで