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

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

ScrolledFrameとwrapped_gridで作る画像一覧の作り方【Python】

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

画像一覧(アルバム)のアプリの作成方法を紹介します。
自分で作成したライブラリの ScrolledFrame クラスと wrapped_grid() メソッドを使用しています。
画像のサムネイルが格子状に並んで表示されます。
ウィンドウの右端で折り返しウィンドウサイズを変えるとそれに伴って表示する画像の数が変わります。

画像を選択してプレビューすることもできます。

画像表示の仕方やチェックボックスの使い方、アプリの作り方をサンプルコードも交えて説明します。

画像一覧アプリ(exe)としても提供します。

目次

◆アプリのサンプル画像

▽画像を表示した時 と ▽画像を選択した時 のサンプル画像です。

◆機能・特長

  • GIF、PNGJPEG、WebP ファイルを読み込み画像のサムネイルを格子状に表示
  • 画像のファイル名、サイズ、Exif 情報の有無を表示
  • 画像を選択して原寸大でプレビューをダイアログ表示
  • ドラッグアンドドロップでファイルを指定可能(TkinterDnD2使用)
  • exeにドラッグアンドドロップでファイルを指定可能(TkinterDnD2使用でも)

プログラムについて知りたい方は次へ

早速、アプリを使ってみたい方はこちらへ「バイナリ(アプリ)」

◆画像表示にCheckbuttonクラスを使用

画像を表示するのに使えるウィジェットには、Label, Button, Radiobutton, Checkbutton, Notebook があります。

今回、一覧表示した画像を選択してプレビューさせたいので、Checkbutton を使用します。

◇Checkbuttonのコンストラク

  • 【構文】 tk.Checkbutton(親ウィジェット, オプション)
  • よく使うオプションを使った時の構文
    【構文】 tk.Checkbutton(parent, image = PhotoImage オブジェクト, compound = tk.NONE, variable=ウィジェット変数)

  • 【主なオプション】

    オプション 説明 設定値
    indicatoron インジケータの描画の有無 True:チェックボックスあり
    False:チェックボックスなし(押しボタンスタイル)
    image 画像を指定 PhotoImage オブジェクト
    width 画像あり:ピクセル値、画像なし:文字数
    compound*1 画像とテキストの表示位置の指定 centertopbottomleftrightnone
    variable ウィジェット変数の指定 tk.BooleanVar()オブジェクト*2
  • 主なメソッド

    • deselect():選択解除
    • select():選択

◇選択形式と選択状態

Checkbutton は、選択のためのチェックボックス(□)を表示するかどうかを選べます。
今回はチェックボックスを使用せず、Checkbutton 自体がフラット(未選択)な状態と、へこんだ(選択)状態で選択状態を表します。

ウィジェットの選択状態は、variable オプションで指定したウィジェット変数で取得、設定します。

  • 取得:w_var.get()
  • 設定:w_var.set(value)

選択状態はウィジェット変数で取得できるのですが、どの Checkbutton オブジェクトを選択しているのかを分かりやすくするため、表示している画像のパスをセット型にして保持します。
それが、checked_image_paths です。
そこにセットされているパスが選択されている画像を示します。

◇すべて選択ボタンと選択解除ボタンの処理

すべての画像を選択するボタンと選択解除するボタンを用意しました。
前提として self.frame_children に checkbutton オブジェクトを含んだ Frame オブジェクトを保持してあります。
※保持している理由は、wrapped_grid() メソッドで使用するため

▶考え方

  • すべての画像表示用フレーム(self.frame_childrenに保持されている)に対して処理を行う
  • 画像表示用のフレームにはチェックボタンとラベルが入っている
  • チェックボタンを選択状態、または非選択状態にする(メソッドで)
  • チェックボタンの text オプションに画像のパスが入っているので、checked_image_paths に追加、削除する

【コード】

ウィジェットの作成

    boolen_var = tk.BooleanVar(False)
    # チェックボックスの作成(imageに画像、textに画像パス(ダブルクリックプレビューで使用))
    check_box = tk.Checkbutton(frame1, image=image, width=self.thumbnail_xy + 10
                              , text=row[0], compound=tk.NONE, variable=boolen_var, indicatoron=False)
    check_box.config(command = lambda x=boolen_var, o=row[0]: self.on_check(var_check=x, obj_check=o))
    # pack
    label_f_name.pack(side=tk.BOTTOM)
    check_box.pack()

▼チェックされた時

    def on_check(self, event=None, var_check:tk.Variable=None, obj_check:str=None):
        """
        チェックボックスがクリックされたらチェックされている集合の内容を更新する
        Args:
            Variable:   ウィジェット変数
            str:        画像のパス
        """
        if var_check.get():
            self.checked_image_paths.add(obj_check)
        else:
            self.checked_image_paths.discard(obj_check)

▼すべて選択とすべて選択解除

    def select_all(self, event=None):
        """
        Checkbuttonをすべて選択する
        """
        self.set_all_checkbox(True)

    def deselect_all(self, event=None):
        """
        Checkbuttonをすべて選択解除する
        """
        self.set_all_checkbox(False)

    def set_all_checkbox(self, is_selected:bool):
        """
        Checkbuttonの選択状態をすべて設定する
      Args:
          bool: 選択するか選択解除するか
        """
        for child_ in self.frame_children:
            for item_ in child_.winfo_children():
                if type(item_) == tk.Checkbutton:
                    if is_selected:
                        item_.select()      # Checkbuttonを選択
                        self.checked_image_paths.add(item_.cget("text"))        # パスを登録
                    else:
                        item_.deselect()    # Checkbuttonを非選択
                        self.checked_image_paths.discard(item_.cget("text"))    # パスを削除

◆ScrolledFrame クラスの使い方

前節で画像を Checkbutton オブジェクトで実装することを説明しました。
実際の画像一覧には、画像の情報も表示したいので Labael オブジェクトも実装します。
それらを一つの Frame オブジェクトに配置して画像表示用 Frame オブジェクトとします。

  • 画像表示用 Frame オブジェクト
    • 画像表示用 Checkbutton オブジェクト
    • 画像情報表示用 Label オブジェクト

この画像表示用 Frame オブジェクトを ScrolledFrame オブジェクトに追加して画像一覧を実現します。

  • ScrolledFrame オブジェクト
    • 画像表示用 Frame オブジェクト
    • ・・・

◇ScrolledFrame クラスを使う時の注意点

先の画像表示用 Frame オブジェクトを画像の数だけ作成して ScrolledFrame オブジェクトに追加します。

注意点

  • ScrolledFrame オブジェクトは master に指定したウィジェットと ScrolledFarme オブジェクトの間に Frame オブジェクトと Canvas オブジェクトが存在します
    • parent_frame:parent_canvas をラップ。pack() や grid() はこれを使う
    • parent_canvas:ScrolledFrame をラップ。wrapped_grid() のバインドにはこれを使う
      • ScrolledFrame 内ですでにバインドしているのでオプション addTrue を指定する

▶ScrolledFrame クラスについては こちらの記事を参照してください。
 📔 ◆ScrolledFrame(スクロールバー付Fame)クラス - ラップするgrid(wrapped_grid)で作るフォント一覧の作り方【Python】 🔗

◇wrapped_grid() メソッドを使う時の注意点

先の画像表示用 Frame オブジェクトを格子状に配置するため wrapped_grid() メソッドを使用します。

注意点

  • wrapped_grid() メソッドにグリッドするウィジェットを渡す必要があるので用意する
  • 画像が消えないように画像表示用 Frame オブジェクトはインスタンス変数にする
  • ScrolledFrame オブジェクトで使用する場合は、bind() のオプション addTrue を指定する

▶wrapped_grid() メソッドについては こちらの記事を参照してください。
 📔 ◆wrapped_grid()メソッド - ラップするgrid(wrapped_grid)で作るフォント一覧の作り方【Python】 🔗

【コード】

▼ScrolledFrame オブジェクトの作成と wrapped_grid() メソッドのバインド

    self.frame4images = ScrolledFrame(self.b_frame, background="pink", padx=10)
    self.frame4images.parent_frame.pack(fill=tk.BOTH, expand=True)
    self.frame4images.parent_canvas.config(background="lightblue")
    # bind
    self.frame4images.bind_class("Checkbutton", "<Double 3>", self.preview_image)  # マウスを右ダブルクリックしたときの動作
    # wrapped_gridのbind
    self.frame_children = []
    self.frame4images.parent_canvas.bind("<Configure>", lambda event: TkinterLib.wrapped_grid(
        self.frame4images.parent_canvas, *self.frame_children, event=event, flex=False), add=True)

◆Entryオブジェクトの入力を数字だけにする(検証)

Entry オブジェクトは入力された文字を検証して入力を抑制したり書き換えたりすることができます。

ここでは、画像のサムネイルの幅を指定するため、数字だけの入力を受け付けるようにします。

◇検証の実装手順

  1. 検証を行う関数を作成する
    • 引数:後の「関数に渡す引数」で指定した引数(ひとつずつ指定、複数可)
    • 戻り値:True:妥当、False:妥当でない
  2. 1.で作成した関数を基に Tcl 関数を作成する
    • register(関数)メソッドで作成
  3. エントリーウィジェットを作成する オプションvalidate, vcmdを指定

◇Entry の検証用オプション

  • validate:検証を行うタイミングを指定
    • focusin:フォーカスされた時
    • focusout:フォーカスが外れた時
    • focus:フォーカスされた時、またはフォーカスが外れた時
    • key:文字列に変化があった時 ユーザーの操作、プログラムの操作
    • all:上記4つのどれかが発生した時
    • none:どのタイミングでも検証を行わない
  • vcmd:検証を行う関数と引数を指定 タプル(Tcl関数名, 関数に渡す引数, ...)
    • Tcl関数名:register( )【関数登録】で登録した関数
    • 関数に渡す引数
      • %d:アクションコード(削除:0、挿入:1、その他:-1)
      • %i:挿入・削除の開始テキストインデックス(その他:-1)
      • %P:変更後のテキスト (予定)
      • %s:変更前のテキスト
      • %S:挿入・削除されるテキスト
      • %v:validate【検証モード】オプションの値
      • %V:呼び出し理由 ( 'focusin'・'focusout'・'key'・'forced' )
      • %Wインスタンス名 (フルパス)〔 name【インスタンス名】 〕

【コード】

▼検証のオプション指定と検証メソッド

    self.ety_size = tk.Entry(parent, width=8, textvariable=self.var_size
                            , validate="key", vcmd=(self.register(self.entry_validate), "%d", "%S"))


    def entry_validate(self, action:str, modify_str:str) -> bool:
        """
        エントリーの入力検証
        Args:
            str:    アクション(削除:0、挿入:1、その他:-1)
            str:    挿入、削除されるテキスト
        """
        if action != "1": return True   # 挿入の時だけ検証
        return modify_str.isdigit()     # 数字かどうか

◆プレビュー処理

画像をダイアログでプレビューできるように対応しました。
マウスの右ボタンでダブルクリックするとダイアログを表示します。

また、選択状態の画像を「プレビュー」ボタンのクリックで表示します。

pillow のイメージオブジェクトであれば、show() メソッドで表示することができます。
この時、画像ファイルに割りついているアプリが起動します。

本アプリでは、show() メソッドを用いず、Tkinter のダイアログを使用してプレビューします。
画像は Toplevel オブジェクトにオプションで画像を指定したラベルウィジェットを配置するだけで表示できます。

【処理】

  1. 画像のパスを取得
    • イベントの場合、ダブルクリックした Checkbutton の text オプションから
    • ボタンの場合、引数から
  2. モードレスでダイアログ作成
  3. PhotoImage オブジェクト作成
    複数表示するので画像を保持しておく
    そうしないと画像が消えてしまう
  4. ラベルのオプション image を使用してラベルを追加して pack
  5. ダイアログをフォーカスする
  6. ダイアログを閉じた時の処理を設定する
  7. Esc キーでダイアログを閉じられるよう設定する

◇ダイアログを閉じた時の処理

プレビューダイアログを複数起動するために画像のオブジェクトは保持しておきます。
プレビューダイアログを閉じたら画像のオブジェクトは不要なので削除します。そうしないとメモリーを消費してしまうからです。

【コード】

▽バインド

        # bind
        self.frame4images.bind_class("Checkbutton", "<Double 3>", self.preview_image)  # マウスを右ダブルクリックしたときの動作

▽ダイアログでプレビュー

    def preview_image(self, event=None, path=""):
        """
        画像のプレビュー
        ダイアログ表示
      Args:
            string:     ファイルパス(ない場合もある)
        """

        if event:
            if not event.widget.config("image"): return # imageオプションに指定がないなら抜ける
            path1 = event.widget.cget("text")   # ファイル名取得
        else:
            path1 = path

        # ダイアログ表示
        dialog_ = tk.Toplevel(self)      # モードレスダイアログの作成
        dialog_.title("Preview")         # タイトル
        self.images4dialog[path1] = ImageTk.PhotoImage(file=path1)    # 複数表示する時のために画像を残す
        label1 = tk.Label(dialog_, image=self.images4dialog[path1])      # 最後のものを表示
        label1.pack()
        dialog_.focus()
        # 閉じた時の動作を指定 最前面のウィジェットに設定しないと複数回発生する
        label1.bind("<Destroy>", lambda e: self.on_destroy(event=e, path=path1))
        # make Esc exit the preview
        dialog_.bind('<Escape>', lambda e: dialog_.destroy())

▽ダイアログを閉じた時の処理

    def on_destroy(self, event=None, path=None):
        if event:
            # print(f"do pop key:{path} widget:{event.widget}")   # for debug
            self.images4dialog.pop(path)    # 表示用に残した画像を削除

Tkinterの使い方

Tkinter(ティーキンター)は Python が標準で提供している GUI パッケージです。
他のプログラムの開発ツールで提供されているようなルック&フィールな GUI ツールではありません。
従って、画面を構成するには、すべてコードを書く必要があります。

Tkinter を使用するには import が必要です。

   import tkinter as tk

慣例的に tk と別名を付けるようです。

◇初歩的な使い方

ウィンドウを出すだけのサンプルです。
Python を起動して次の命令を入れるとウィンドウが出てきてアプリらしさをちょっとだけ体験できます。

import tkinter as tk     #tkと省略することが慣例のようです  
win = tk.Tk()               #ウィンドウの作成と表示  
win.title("Hello, World")    #タイトル設定  
win.geometry("400x300")      #ウィンドウサイズ設定  
lbl = tk.Label(win, text="ラベル")    #ラベルウィジェットの作成  
lbl.pack()                  #ラベルウィジェットの配置  
win.mainloop()              #イベント待ち  

Tkinterの使用ウィジェット

Tkinter がサポートする GUI の部品をウィジェット (widget) と呼びます。

ここでは次のウィジェットを使用しています。

◇使用ウィジェット

ウィジェットは基本的にインスタンスを作成し、表示テキストや表示方法などを指定して使用します。

今回使用しているウィジェットです。

パッケージ ウィジェット 用途 見た目
tkinter Frame
フレーム
ウィジェットの受け皿
tkinter Label
ラベル
文字の表示
tkinter Button
ボタン
押すと処理が動くボタン
tkinter Entry
エントリー
テキストを入力するボックス
独自 ScrolledFrame
スクロールバー付フレーム
画面をスクロール

◇画面表示の特徴

画面表示には次の特徴があります。

  • 画像を格子状に配置(wrapped_grid() メソッドを使用)
  • ウィンドウのサイズを変更すると表示する画像の列数をウィンドウの幅に合わせる(wrapped_grid() メソッドを使用)
  • 縦スクロールバーを表示(ScrolledFrame クラスを使用)
  • マウスホイールでスクロール

◇画面構成 - フレーム構造

プログラムを作るにあたって、どのような画面にするか決めなくてはなりません。画面構成を考えるということですね。
画面を構成するというのは、GUI 部品(ウィジェット)を画面に配置することです。
ウィジェットは設定したテキストに合わせてサイズが決まるので、そのまま使用しても画面構成上問題ないものと、広げないと見栄えが良くないものが出てきます。
ウィジェットを広げる場合、他のウィジェットとの位置関係で思ったように配置できないことがあります。
そのようなときに Frame ウィジェットを使用して区分けをして希望する位置に配置します。
引き出しの整理に使うトレイのようなイメージでしょうか。

今回の画面構成です。

  • u_frame (frame)
    • btn_f_sel (Button)
    • btn_select_all (Button)
    • btn_deselection (Button)
    • btn_preview (Button)
    • lbl_size (Label)
    • ety_size (Entry)
    • lbl_msg (Label)
  • b_frame (frame)
    • frame4images (ScrolledFrame)
      • frame1 (Frame)
        • check_bon (Checkbutton)
        • label_f_name (Label)

※「u_frame」などはプログラムで使用した変数名です。

入力をする部分と出力をする部分で大きくフレームを分けています。
結果を表示する部分は余った領域をすべて使用してできるだけ大きく表示させるためです。

ウィジェットの配置 - pack

▶解説は別記事を参照してください
 📔 □ウィジェットの配置 - pack - SQLクライアントアプリの作り方(Tkinterで表)【Python】 🔗

◇動作をウィジェットに割り当てる - バインド

▶解説は別記事を参照してください
 📔 □動作をウィジェットに割り当てる - Excel viewerアプリの作り方(Tkinterでタブと表)【Python】 🔗

◆画像一覧の作り方

◇考え方

画像一覧は、格子状に画像を並べるのが一般的なので後はどのウィジェットで実装するかを決めるのが主な検討事項です。

ここでは、ScrolledFrame オブジェクトに画像とラベルをセットしたフレームを格子状に並べます。
ウィンドウのサイズを変更した時に幅に合わせて画像の個数を変えられるように wrapped_grid() メソッドを使用します。

後は取得した画像オブジェクト、画像情報の数だけウィジェットを作成します。

画像ファイルを指定し直した時のために ScrolledFrame オブジェクトの子供は追加前にクリアします。
クリアは、widget.winfo_children() メソッドで子供のウィジェットを洗い出し、 widget.destroy() メソッドで削除します。

▶画像オブジェクトの作成方法については こちらの記事を参照してください。
 📔 □画像オブジェクトの作成 - 画像ビューアの作り方(Treeviewに画像と疑似チェックボックス)【Python】 🔗

【コード】

▼set_images2frame() メソッド

    def set_images2frame(self, parent:tk.Frame, rows:list, images:list):
        """
        parent(Frame)にimagesの要素分Frameを作成しgrid
        Frameはself.frame_childrenにappendして画像が残るようにする
        Frameには画像用Checkbuttonと情報用Labelを追加
        Args:
            Frame:      親Frame
            list:       行データ(行リストの列リスト((ファイル名、幅、高さ、ファイルサイズ、exif情報、gps情報)))
            list:       画像データ
        """
        if not rows:    # 要素が無ければ戻る
            return

        # チェックされた画像用セットを初期化
        self.checked_image_paths = set()

        # 要素の削除
        for w in parent.winfo_children():
            w.destroy()

        # 要素の追加
        self.frame_children = []
        for row, image in zip(rows, images):
            # 要素の追加(Frameに画像用Checkbuttonと情報用Label)
            frame1 = tk.Frame(parent, relief=tk.GROOVE, borderwidth=2)
            if row[4]:
                exif = " | Exifあり"
            else:
                exif = ""
            # ファイル名、Exif情報のあり/なしを表示するラベルの作成。文字はサムネイルの幅+10pxで折り返し
            disp_text = f"{os.path.basename(row[0])}\n{row[1]} x {row[2]}{exif}" 
            label_f_name = tk.Label(frame1, text=disp_text, wraplength=self.thumbnail_xy + 10)
            boolen_var = tk.BooleanVar(False)
            # チェックボックスの作成(imageに画像、textに画像パス(ダブルクリックプレビューで使用))
            check_box = tk.Checkbutton(frame1, image=image, width=self.thumbnail_xy + 10, text=row[0], compound=tk.NONE, variable=boolen_var, indicatoron=False)
            check_box.config(command = lambda x=boolen_var, o=row[0]: self.on_check(var_check=x, obj_check=o))
            # pack
            label_f_name.pack(side=tk.BOTTOM)
            check_box.pack()
            self.frame_children.append(frame1)
            frame1.grid(row=0, column=0)    # 一度仮にgridして個々のサイズが確定できるようにする
        
        # 親Frameの幅に合わせてgridする
        if self.frame_children:
            TkinterLib.wrapped_grid(parent.parent_canvas
                                , *self.frame_children, flex=False, force=True)

◆必要なパッケージ

□必要な Python パッケージ

  • pillow
  • TkinterDnD2

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

◇必要な Python モジュール

  • tkinter_libs.py ver 1.0.1 独自ライブラリ

◆ソースの取得

全体のソースはこちらから取得できます。

◆バイナリ(アプリ)

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

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

◇バイナリ取得先

バイナリも公開します。
こちらから取得してください。Github からダウンロード

◇アプリの使い方

  • インストール

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

  • 操作

    • 表示する画像の指定
    • 画像のサイズの変更
      • 「サイズ」の右の入力エリアに幅をピクセル単位で指定します
        数字以外は入力できません
        変更後、新たに表示した画像から有効です
    • プレビュー
      • 画像をマウスの右ボタンでダブルクリック
      • 複数の画像を選択状態で「プレビュー」ボタンを押す
  • 画面の説明

    • ボタン
      • ファイル選択:表示する画像を選択するダイアログを表示します
      • すべて選択:表示されている画像をすべて選択します
      • 選択解除:表示されている画像をすべて選択解除します
      • プレビュー:選択されている画像のプレビューを表示します
    • 入力エリア
    • 画像表示エリア
      • 画像を格子状に表示します
      • ウィンドウの幅に合わせて画像の列を調整します
      • 縦にスクロールします
  • アンインストール

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

◆免責事項

ご利用に際しては、『免責事項』をご確認ください。

お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
ただし、回答をお約束するものではありません。

◆さいごに

自分で作成したライブラリの ScrolledFrame クラスと wrapped_grid() メソッドは、元々を画像一覧を作るために用意したものです。

今回は画像ですが、何かを列挙するというニーズは時々あると思います。
そのようなときにお役に立てれば幸いです。

あわせて読みたい - ScrolledFrame

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

◇ご注意

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

  • Python 3.8.5
  • Pillow 8.3.0
  • TkinterDnD2 0.3.0

◆参考

投稿: 、更新: