プログラムでおかえしできるかな

定年を過ぎて何かの役に立てないかなと始めた元SEのブログです

シンプル画像ビューアの作り方(マウスホイール対応)【Python】

このエントリーをはてなブックマークに追加

Python Tkinter と pillow を使用して、シンプルな画像ビューアを作成しました。

たった8行でコマンドライン引数で画像を指定して表示するビューア。
たった18行でドラッグアンドドロップで画像を指定して表示するビューア。

どちらも実用できますが、既定のプログラムとしても使えるよう両方の仕組みを取り入れたビューアも作成しました。

WebP 画像も表示できます。

アプリの作り方をサンプルコードも交えて説明します。

アプリ(exe)も提供しています。興味があったら使ってみてください。

目次

◆たった8行のシンプル画像ビューア

コマンドラインで画像を指定

コマンドラインで画像を指定」ということは、アプリケーションの割り当てをしていれば、エクスプローラーで画像ファイルをダブルクリックすると起動して画像を表示できるということです。

コードの説明はコードのコメントを参照してください。

以下に、補足します。

ImageTk.PhotoImage(file=sys.argv[1]) は pillow で tkinter の PhotoImage オブジェクトを作成するコンストラクタです。ファイル指定で使えます。pillow を使うことで扱える画像の種類が多くなります。

from PIL import ImageTk
import tkinter as tk
import sys
root = tk.Tk()              # トップレベルウィンドウの作成
root.title("画像 viewer")   # タイトル
img = ImageTk.PhotoImage(file=sys.argv[1])  # 画像の読み込み コマンドライン引数の1番目のファイル名
tk.Label(root, image=img).pack()   # ラベルウィジェットの作成、画像指定
root.mainloop()

◆たった18行のシンプル画像ビューア

ドラッグアンドドロップで画像を指定

ドラッグアンドドロップで画像を指定」ということは、このプログラムを起動しておいて、そこに画像ファイルをドラッグアンドドロップすれば画像を表示できるということです。

コードの説明はコードのコメントを参照してください。

以下に、補足します。

set_image 関数は、ドロップイベントで呼べるように関数にしてあります。
dnd_bind メソッドでドロップと関数の紐付けをしています。

img 変数は、PhotoImage オブジェクトを set_image 関数終了後も保持するためにグローバル変数にしています。
こうしないと、オブジェクトが gc の対象となり、画像が表示されなくなります。

ドラッグアンドドロップについては「ドラッグアンドドロップの実装(TkinterDnD2の使い方)」を参照してください。

from PIL import ImageTk
import tkinter as tk
from tkinterdnd2 import *

def set_image(event):
    global img      # グローバル変数の宣言(グローバルにしないとオブジェクトがgcされる)
    paths = root.tk.splitlist(event.data)   # ドロップされた複数パス名をパス名ごとに分割
    img = ImageTk.PhotoImage(file=paths[0])    # 画像オブジェクトの作成。1番目だけを対象にする
    label1.configure(image=img)             # ラベルの画像の更新

root = TkinterDnD.Tk()      # トップレベルウィンドウの作成  tkinterdnd2の適用
img = None                  # グローバル変数の定義
root.title("画像 viewer")   # タイトル
label1 = tk.Label(root, text="ここに画像ファイルを\nドロップしてください")  # ラベルの作成
label1.pack()
root.drop_target_register(DND_FILES)    # ドロップ受け取りを登録
root.dnd_bind("<<Drop>>", set_image)    # ドロップ後に実行するメソッドを登録
root.mainloop()

◆シンプル画像ビューア

前述の2つのシンプル画像ビューアをまとめました。
コマンドラインで画像を指定」でき、「ドラッグアンドドロップで画像を指定」できます。
既定のプログラムとして使えるように例外処理を入れました。
さらにマウスのホイールで画像の拡大縮小とフォルダ内の別画像の表示もできます。

import pathlib
from PIL import ImageTk
import tkinter as tk
import sys
from tkinterdnd2 import *

def set_image(event=None, path=""):
    global img      # グローバル変数の宣言(グローバルにしないとオブジェクトがgcされる)
    if event:
        paths = root.tk.splitlist(event.data)   # ドロップされた複数パス名をパス名ごとに分割
        path = paths[0]                         # 1番目だけを対象にする

    try:
        img = Image.open(path)                  # 画像オブジェクトの作成
    except Exception as e:                      # 例外の時imgは
        label1.configure(image="", text="エラー:" + path)
    else:
        photo_img = ImageTk.PhotoImage(img)     # PhotoImageオブジェクト作成
        if img.getexif():
            exif = "Exifあり"
        else:
            exif = "Exifなし"
        info = f"{img.width} x {img.height} | {exif}"
        label1.configure(image=photo_img, text=info)  # 画像をラベルに設定
    root.title(path)                            # パス名をタイトルに(後で参照する)

root = TkinterDnD.Tk()      # トップレベルウィンドウの作成  tkinterdnd2の適用
img = None                  # グローバル変数の定義
photo_img = None            # グローバル変数の定義
root.title("画像 viewer")   # 初期タイトル
label1 = tk.Label(root, text="ここに画像ファイルを\nドロップしてください", compound="bottom")
label1.pack()
# bind
root.bind("<MouseWheel>", resize_image)
root.bind("<Control-MouseWheel>", preview_other_image)
# DnD
root.drop_target_register(DND_FILES)    # ドロップ受け取りを登録
root.dnd_bind("<<Drop>>", set_image)    # ドロップ後に実行するメソッドを登録
# コマンドライン引数からドラッグ&ドロップされたファイル情報を取得
if len(sys.argv) > 1:
    set_image(path = sys.argv[1])       # コマンドラインの1つ目だけを引数で渡す
root.mainloop()

◆pillowで画像の読み込み

pillow には tkinter で扱えるように画像を読み込んで PhotoImage オブジェクトを作成するコンストラクタが用意されています。

  • 【構文】ImageTk.PhotoImage(image=None, size=None, **kw)
  • サンプル
    【構文】ImageTk.PhotoImage(file="画像ファイル名")

  • 引数

    • image:Image オブジェクトかモード(モードの場合 size も必要)
    • size:画像のサイズ(幅、高さのタプル)
    • file:画像ファイル名(内部的に Image.open(file)が使われる)

◆pillowで表示可能な画像フォーマット

pillow がサポートしている画像フォーマットです。

BMP, DDS, DIB, EPS, GIF, ICNS, ICO, IM, JPEG, JPEG 2000, MSP, PCX, PNG, PPM, SGI, SPIDER, TGA, TIFF, WebP, XBM, BLP, CUR, DCX, FLI, FLC, FPX, FTEX, GBR, GD, IMT, IPTC/NAA, MCIDAS, MIC, MPO, PCD, PIXAR, PSD, WAL, WMF, XPM

Tkinterのラベルウィジェット(Label widget)に画像とテキストを表示

ラベルウィジェットには、画像とテキストを表示することができます。 オプション image= で画像、オプション text= でテキストを指定します。 この時、画像とテキストの両方が指定されいる時の表示の仕方を compound オプションで指定します。

compound オプション:画像とテキストの表示位置の指定

  • center:画像に重ねて、中央に文字を表示
  • top:文字の上に画像を表示
  • bottom:文字の下に画像を表示
  • left:文字の左に画像を表示
  • right:文字の右に画像を表示
  • none:画像が指定されていれば画像を表示、画像が指定されていない場合は文字を表示
    ※画像の未指定の方法:image=""
  • text:text オプションにセットした文字だけを表示(ttk)
  • image:image オプションにセットした画像だけを表示(ttk)
更新:2022-02-23

他にも compound オプションが使えるウィジェットがあります。
言い換えると画像とテキストが表示できるウィジェットです。

compoundが有効なウィジェット

Tkinterのマウスホイールで画像の拡大縮小とフォルダ内移動

シンプル画像ビューアはシンプルすぎて画像を100%の大きさで表示します。
そのままだと画面からはみ出してしまうので縮小できるように機能を追加します。

あわせて同じフォルダ内の別の画像を表示できる対応もします。

どちらもマウスホイールで動作するようにします。

Tkinterのマウスホイールで画像の拡大縮小

マウスホイールで回した方向で拡大、縮小します。

ホイールの変化量
は、イベントオブジェクトの delta 属性で取得できます。
Windows の場合、±120 単位の値で返ります。ここから画像の変化量を決めます。

画像の変化量
は、画像の幅を10pxずつ変化させます。
画像の高さは、アスペクト比が変わらないように幅から求めます。

画像の拡大、縮小
は、pillow の Image オブジェクトの resize() メソッドで行います。
拡大、縮小の素にする Image オブジェクトは、オリジナルサイズのオブジェクトにします。そうしないと拡大、縮小を繰り返している間に画像が劣化してしまいます。
拡大、縮小が済んだら PhotoImage オブジェクトを作成してラベルに設定します。

Tkinterのマウスホイールでフォルダ内の別画像表示

Ctrl キーを押しながらマウスホイールを操作した場合に現在表示している画像と同じフォルダにある別の画像を表示します。

画像と同じフォルダにあるファイルの取得
は、いろいろ検討した結果、pathLib.Path オブジェクトの iterdir() メソッドを使用します。
表示している画像のパス(タイトルに表示しているのでそれを使用(root.title() で取得))から pathlib.Path オブジェクトを作成します。
parent プロパティを参照して親フォルダを求め、iterdir() でフォルダ内のファイルを取得します。
iterdir() はフォルダも取得してしまうので、is_file() メソッドでファイルかどうか判断します。
iterdir() はジェネレーターを返します。ジェネレーターでは前に戻って参照ができないので、リストにします。

最終的に
paths = [p for p in file.parent.iterdir() if p.is_file()] で画像と同じフォルダにあるファイルを求めます。

リストは pathlib.Path 型を要素にしています。
パスの文字列を要素にしたリストにした場合に、リストの index() メソッドで画像のパスを見つけられないことがあったためです。

次に表示するファイルのパス
は、イベントオブジェクトの delta 属性で取得したホイールの変化量から決定します。±1を求めます。
リストの最後尾を超えて指定しようとする場合、先頭に戻します。
決定したパスを元に画像表示のメソッドを呼び出します。

◇イベントクラス

バインドされたコールバック関数が呼び出されると event オブジェクトが引数で提供されます。 event オブジェクトの主な属性を示します。

  • 主な属性
    • char:イベントがKeyPressかKeyReleaseの場合に文字をセット
    • num:イベントがマウスボタン関連の場合、パタン番号をセット
    • type:イベントのタイプを意味する番号(KeyPress, Active, Buttonなどの番号)
    • widget:イベント要因のウィジェット(オブジェクト)を常にセット
    • delta:MouseWheelイベント用。整数。正:上スクロール、負:下にスクロール
      Windowsでは120の倍数。例:-240は2ステップ下にスクロールされた
    • xウィジェットの左上隅を基準にした、イベント時のマウスのx座標
    • yウィジェットの左上隅を基準にした、イベント時のマウスのy座標

◇マウスバインド

マウスのホイールとCtrlキーを押してのホイールに対して、それぞれ画像の拡大縮小と同じフォルダ内の別の画像表示を割り当てます。

  • 【構文】ウィジェット.bind(シーケンス, コールバック関数, 追加フラグ)

  • 引数

    • シーケンス:イベント名(例:<Return>, <Button-1>, <MouseWheel>, <Control-MouseWheel>)
    • コールバック関数:関数には event 引数が必要
      ウィジェットの command オプションと共通の場合は、関数の引数定義を event=None とする
    • 追加フラグウィジェットに対してイベントを置き換えるか追加するか
      デフォルトは置き換え(引数を省略した場合)

【コード】

▽マウスホイールでリサイズ

def resize_image(event=None):
    global img          # グローバル変数の宣言(グローバルにしないとオブジェクトがgcされる)
    global photo_img    # グローバル変数の宣言(グローバルにしないとオブジェクトがgcされる)
    # event.deltaはwindowsの場合、+-120単位で返る
    new_width = photo_img.width() + int(event.delta / 12)
    new_height = round(img.height * new_width / img.width)
    img2 = img.resize((new_width, new_height))      # 読み込んだ画像をリサイズしないと画質が悪くなる
    photo_img = ImageTk.PhotoImage(img2)
    if img.getexif():
        exif = "Exifあり"
    else:
        exif = "Exifなし"
    info = f"{new_width} x {new_height}({img.width} x {img.height}) | {exif}"
    label1.configure(image=photo_img, text=info)  # 画像をラベルに設定

▽Ctrl+マウスホイールでフォルダ内別画像表示

def preview_other_image(event=None):
    file = pathlib.Path(root.title())           # タイトルから現対象のパスを取得
    paths = [p for p in file.parent.iterdir() if p.is_file()]   # 同フォルダのファイルを取得
    i = paths.index(file)                       # フォルダ内の位置を取得
    if event:
        i = i + int(event.delta / abs(event.delta))     # ホイールの移動を1単位にして次のファイルを決める
        if i >= len(paths):                     # はみ出したら戻す
            i = 0                               # -1はリストの再末尾なので許容
    # print(f"{i}:{paths[i]}")                  # debug用
    set_image(path = str(paths[i]))             # 画像表示

▽bind

root.bind("<MouseWheel>", resize_image)
root.bind("<Control-MouseWheel>", preview_other_image)

ドラッグアンドドロップの実装(TkinterDnD2の使い方)

解説は別記事を参照してください CSV viewerアプリの作り方(ドラッグアンドドロップ)【Python】 - ■ドラッグアンドドロップの実装(TkinterDnD2の使い方)

◆ファイルの関連付けの変更

本アプリ、画像ビューアを既定のプログラムとして動作するようにするには、関連付けが必要です。
そのための機能がないので、Windows の機能で関連付けをしてください。

エクスプローラーで関連付けしたい拡張子のファイルを選択し、次の方法を実施します。
拡張子の種類の数だけ実施します。

ファイルの関連付けの方法

  1. ファイルを右クリックして 「プログラムから開く」を選択(カーソルを合わせる)
  2. 「別のプログラムを選択」を選択
  3. 「その他のアプリ」 ⇒ 「この PC で別のアプリを探す」 をクリック
  4. 「simple_image_viewer.exe」を選択
    「常にこのアプリを使って.xxxファイルを開く」にチェックが付いていることを確認

Windows のバージョンによって操作が異なります。
参考:Windows 10 ファイルの関連付けと既定のプログラムを変更する

◆必要なパッケージ

◇必要な Python パッケージ

  • pillow
    【インストール】pip install Pillow
    【インポート】 from PIL import ImageTk
  • TkinterDnD2
    【インストール】pip install tkinterdnd2
    【インポート】 from tkinterdnd2 import *

TkinterPythonの標準パッケージです。

◆ソースの取得

ソースはこちらからも取得できます。
取得先:GitHub juu7g/Python-Simple-Image-Viewer

◆バイナリ(アプリ)

pyinstaller でバイナリ( exe ) ファイルを作成しました。

  • 作成コマンド: pyinstaller -F -w --additional-hooks-dir . ファイル名
    ※実行には hook-tkinterdnd2.py ファイルが必要です(作り方は別記事の「pyinstaller使用時の注意」を参照)

◇バイナリ取得先

バイナリも公開します。
こちらから取得してください。Python-Simple-Image-Viewer.zip を Github からダウンロード

もう一つのバイナリ
上記のバイナリは1ファイルで実行可能なバイナリです。
1ファイルでサイズも小さいのですが、起動に少し時間が掛かります。

それより起動を早くしたバイナリも提供します。
こちらは解凍すると「simple_image_viewer-D」フォルダが作成され、その中のファイルがすべて必要になります。
実行ファイルは「simple_image_viewer.exe」で同じです。

こちらから取得してください。simple_image_viewer-D.zip を Github からダウンロード

◇使い方

  • インストール

    ダウンロードした zip ファイルを任意のフォルダで解凍します

  • 実行

    • simple_image_viewer.exe を実行します
    • または simple_image_viewer.exe のアイコンに表示したいファイルをドラッグアンドドロップします
  • 操作

    • ドラッグ&ドロップでの操作
      • アプリ画面上の任意の位置に表示したいファイルをドラッグ&ドロップ
    • 画像の拡大、縮小
      • 表示された画像の上でマウスホイール
        上にホイールすると拡大
        下にホイールすると縮小
    • 同じフォルダの別の画像を表示

      • 表示された画像の上でctrl + マウスホイール
        上にホイールすると次の画像を表示
        下にホイールすると前の画像を表示
    • 画面の説明

      • 指定したファイルをプレビューします
      • ファイル名をタイトルに表示します
      • 画像の幅、高さ、Exif情報の有無を表示します
      • プレビューできない場合はファイル名だけ表示します
  • アンインストール

    解凍したファイルをすべて削除します

◇使用上の注意

制限事項があります。

  • 制限事項
    • ウィンドウのサイズを変更するとその中に画像を表示します
      初期状態では画像のサイズに合わせてウィンドウのサイズが変わります
    • 画像が大きいと画像情報が見えなくなります
      縮小すると見えます

◆さいごに

以前に、『画像ビューアの作り方(Treeviewに画像と疑似チェックボックス)【Python】』という記事を投稿しました。

画像ビューアと言っていますが、ファイラーのようでもあります。

機能の一つに画像をフルサイズでプレビューするものがあります。
Tkinter のラベルウィジェットで画像を表示するものです。

公開した後にプレビュー部分はそれだけでシンプルな画像ビューアになるなと思いました。
その上、WebP 形式の画像にも対応しています。

はじめは「たった8行のシンプル画像ビューア」ということで作り始めました。

そのうち、自分のPC環境でのデフォルトビューアであるフォトビューアを自分で作った画像ビューアに置き換えてもいいかなと思い始めました。

そうしたらもう少し機能を入れたくなりました。
せっかくなので使ってもらえたらうれしいです。

どなたかへのお歳暮になればうれしいのですが・・・

あわせて読みたい - Tkinter関連記事

あわせて読みたい - pillow 関連記事

◇ご注意

本記事は次のバージョンの下で動作した内容を元に記述しています。

  • Python 3.8.5
  • Pillow 8.3.0
  • TkinterDnD2 0.3.0

◇免責事項

ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
追加:2022-12-12

◆参考

投稿: 、更新: