Python+Tkinterで作る画像ビューワ

# Tkinterとの出会い

Pythonの勉強を進めているうちに、ふとある疑問がわきました。
__GUIを表示するツールって、Pythonにあるのかな？__
JavaのSwingやAWTみたいなものを想像しながら探していると、Tkinterってのがあるらしいというのが分かったので
(他にもあるんですが)今回この__Tkinter__の勉強を兼ねて画像ビューワを作ったお話を書きたいと思います。

# Tkinterとは
ローカル環境でPythonのコードを書いていると
「__ウインドウ__に__テキストエディタ__を配置して、__ボタンが押された__ら処理を実行して、、、」
というようなコンポーネントやイベント駆動の処理が使いたくなる場合があります。
それを実現するための、Pythonソースコード内に埋め込んで使用する、GUI表現するツールキットです。
フレームワークではありませんので、記載方法や記載場所にルールがあるわけではありませんし、何かしら
枠組みがあるわけでもありません。
また、Web系のHTMLとは全く異なりますので、独自の解釈が必要となります。
このあたりご注意ください。

# TkinterでGUIを表現しよう！
実際にTkinterでGUIを表現してみたいと思います。

## スペック
OS:Windows10 Home
Python:3.7.7
`C:\Users\ユーザー名\Documents\workspace`に作業用スペースを作りました。
`C:\Users\ユーザー名\Documents\workspace\images`を作り、ここに表示したい画像を配置します。

## Tkinterのインストール
実はPythonと一緒にインストールされます！なので改めてインストールの必要はございません。

TkinterのバージョンはPythonのシェルコマンドで以下を実行することで確認できます。

```
$ python
Python 3.7.7 (tags/v3.7.7:d7c567b08f, Mar 10 2020, 10:41:24) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import tkinter
>>> tkinter._test()
```
なぜかPOPUPウインドウが開く🤔
![version.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482024/46d91f36-9dda-2973-e641-cdc44fc301c1.png)

★ 【どうでもいい一言】ここでは`Tk=ツールキット`ですが、私の世代では`Tk=小室哲哉`です。


## まずはウインドウを出してみよう

簡単にWindowを表示してみましょう。
以下のコードだけでWindowを表示することができます。

```py:ViewerSample.py
import tkinter

window = tkinter.Tk()
window.geometry("800x600")     
window.title("ViewSample")
window.mainloop()
```
横：800px、縦：600pxのメインとなるWindowが表示されます。
ただしこれだけではWindowのみですので、これから必要なウイジェットを色々と配置していきましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482024/ded3b1c5-bcb2-9194-7ebb-574bb6de49b9.png" width=50%>

# 画像ビューワを実装してみよう
Windowの表示方法もわかったところで、実際に実装するための画像ビューワについて考えてみましょう。

## ウイジェットを配置する前に仕様を考える
今回はこの記事のタイトルにもあるように、画像ビューワを作成することを目的にしています。
本来は__スクレイピングしたサムネイル画像を画像ビューワで表示し、必要な画像のみをクリックして保存する__という
ことを想定しているのですが、そこまで絡めると記事が長くなってしまいますので、とりあえず今回は__ローカルディレクトリ
にある画像を表示する__までを目的として作成したいと思います。

必要なウイジェットは以下を想定します。
1. ローカルディレクトリのパスを入力するテキストエディット
2. 入力されたパスから画像の取得を開始するボタン
3. 画像を表示するキャンバス
この3つを配置します。
それ以外にもラベルやデザインにかかわるフレームを適宜配置していきます。

## 画像ビューワのソースコードと完成イメージ
まずは先ほどの仕様を満たす画像ビューアのソースコードを紹介します😊
各処理についてはのちのち捕捉いたします。

```py:PictureViewer.py
import tkinter
import glob
from tkinter import ttk, scrolledtext
from PIL import Image, ImageTk


# 各種定数宣言
INT_COLUMN_COUNTS = 5
INT_PICTURE_SIZE = 200

# ボタンが押下された時の処理
def click_button_get(event):
    # ビューワで表示する画像のパスを取得
    # 今回はjpgファイルで固定
    pictures = glob.glob(file_edit.get() + "\\*.jpg")
    if len(pictures) == 0 :
        print("対象ディレクトリ、画像が存在しません")
        return 0 
    # リストの初期化
    picture_list = []
    for picture in pictures:
        picture_list.append(picture)
    show_pictures(scroll_canvas, picture_list)

# 取得した画像を表示
def show_pictures(canvas, picture_path):
    canvas.delete('all')

    for index, file_name in enumerate(picture_path):
        img = arrange_image(file_name)
        img = ImageTk.PhotoImage(image=img)
        row_no =  int(index / INT_COLUMN_COUNTS)
        column_no = int(index % INT_COLUMN_COUNTS)
        canvas.create_image(column_no * INT_PICTURE_SIZE , row_no * INT_PICTURE_SIZE, anchor='nw', image=img)
        # 画像を配置
        img_list.append(img)

# 指定のサイズにファイルを成形
def arrange_image(file_name):
    img = Image.open(file_name)
    img_width, img_height = img.size
    reducation_size =  img_width if img_width >= img_height else img_height
    return img.resize(( int( img_width * (INT_PICTURE_SIZE/reducation_size)), int(img_height * (INT_PICTURE_SIZE/reducation_size)) ))

# メインウインドウ
window = tkinter.Tk()
window.geometry("1100x600")     
window.title("PictureViewer")

# ガベコレに消されないようにイメージリストに画像を入れておく用のリスト
img_list = []

# メインフレーム
## input_frame
input_frame = tkinter.Frame(window, width=1100, height=100, bg='black')
input_frame.pack()

## output_frame
output_frame = tkinter.Frame(window, width=1100, height=500, bg='yellow')
output_frame.pack()

# 画像表示用のcanvasを作成
scroll_canvas = tkinter.Canvas(output_frame, width=1050, height=500, bg='red')
scroll_canvas.grid(column=0, row=0)
bar = tkinter.Scrollbar(output_frame, orient=tkinter.VERTICAL)
bar.grid(column=1, row=0, sticky='ns')
bar.config(command=scroll_canvas.yview)
scroll_canvas.config(yscrollcommand=bar.set)
# scroll_canvasの表示範囲
scroll_canvas.config(scrollregion=(0, 0, 1100, 1200))

# ウイジェット設定
## input_frame
file_label = ttk.Label(input_frame, text='パス')
file_edit = ttk.Entry(input_frame)
file_btn = ttk.Button(input_frame, text='取得')

# ボタンにイベントをバインド
file_btn.bind('<Button-1>', click_button_get)

# ウィジェット配置
## input_frame
file_label.grid(column=0, row=0, pady=10, padx=5)
file_edit.grid(column=1, row=0, sticky=tkinter.EW, padx=5)
file_btn.grid(column=2, row=0, padx=5)

window.mainloop()
```

完成した画像ビューワはこんな感じです。バックグラウンドの配色はそのウィジェットの範囲がわかるように
あえて原色を使用しています。

<img SRC="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482024/662d0928-bab8-96e3-a5c5-419d156c5172.png" width="75%">

★ 【どうでもいい一言】猫の画像を使うと、あざとさが増しますね😼

## ソースコードの簡単な補足
今回の画像ビューワは
- Window>メインフレーム(画面上部)>ラベル、テキストエディット、ボタン→ソースコード内`input_frame`にあたる箇所
- Window>メインフレーム(画面下部)>キャンバス→ソースコード内`output_frame`にあたる箇所
の2段で構成しています。

- キャンバスを生成する際、画像が複数あることを想定して、スクロールバーを生成し埋め込んでいます。
スクロールバーは自動で生成されるわけではないので、適宜判断して用意する必要があります。
- ボタンには押下された時のイベントを紐づけます。
- 各種ウィジェットの位置は`input_frame`のプロパティ`column`と`row`で指定しています。
- `def click_button_get(event):`はボタンが押下された場合に呼ばれる関数です。ボタン押下イベントを拾います。
- `def show_pictures(canvas, picture_path):`は指定されたパスにある画像ファイル一覧を取得し、キャンバスに並べて表示します。
- `def arrange_image(file_name):`は画像を200px*200pxにリサイズして返します。これによりキャンバスに表示される画像は一定のサイズに揃えることができます。

# 最後に
改良の余地は多々あるのですが、画像ビューワとしては当初の仕様を満たすものができました。
Pythonは豊富なライブラリが用意されていることもあり、他の言語に比べても自作する部分が少なく、効率よく開発できるところがいいですね。
次回はスクレイピングと絡めた、画像取得機能つきビューワにパワーアップさせたいと思います。