🐍

Python:tkinterでドラッグ&ドロップに対応した画像表示ビューアーをつくる

2023/06/25に公開

はじめに

Pythonでtkinterを用いて、こちらの機能を有したGUIアプリケーションをつくります。

  • ファイルを選択して画像を表示
  • ドラッグ&ドロップで画像を表示
  • 表示した画像をGUI上から非表示にする

実際の動作は下記のとおりです。

試した動作環境

  • Python 3.11.3
  • tkinter 8.6
    • pythonでGUIを作成するためのライブラリ
  • tkinterdnd2 0.3.0
    • tkinterでドラッグ&ドロップを実現するためのライブラリ
  • py2app 0.28.6

ソースコード

ソースコード
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk
import pathlib
from tkinterdnd2 import DND_FILES, TkinterDnD

# ウィンドウサイズ
WIDTH  = 600
HEIGHT = 500

# ドラッグ&ドロップ機能を有効にするためメインウィンドウはTkinterDnD.Tkで作成
class MyApp(TkinterDnD.Tk):
    def __init__(self):
        super().__init__()

        # 画像が表示されているかどうか管理するフラグ
        # 表示されている場合 -> True
        # 表示されていない場合 -> False
        self.flag = False

        self.title("Image Viewer")
        self.geometry(f'{WIDTH}x{HEIGHT}')
        
        buttonFrame = tk.Frame(self)
        # 画像を読み込むためのボタン
        load_button = tk.Button(buttonFrame, text="Load Image",
                                command=self.load_image, width=10)
        load_button.grid(column=0, row=0)
        # 画像を非表示にするためのボタン
        clear_button = tk.Button(
            buttonFrame, text="Clear", command=self.clear_image, width=10)
        clear_button.grid(column=1, row=0)
        buttonFrame.pack(pady=20)
        # 画像表示部分の枠を表示
        self.labelFrame = tk.LabelFrame(
            self, width=400, height=400, text="画像をドラッグ&ドロップ", labelanchor="n")
        self.labelFrame.drop_target_register(DND_FILES)
        self.labelFrame.dnd_bind('<<Drop>>', self.funcDragAndDrop)
        self.labelFrame.pack()
        
    # 画像表示
    def load_image(self):
        # 画像を表示する前に画像が表示されていた場合、前の画像を削除
        if self.flag:
            self.clear_image()
        # ファイルを開くダイアログボックスを表示する
        self.path = filedialog.askopenfilename()
        path = self.path
        print(path)

        image = Image.open(open(path, 'rb'))
        # アスペクトを維持しながら、指定したサイズ以下に画像を縮小
        image.thumbnail((400, 400))
        photoImage = ImageTk.PhotoImage(image)

        # 画像を表示
        self.image_label = tk.Label(self.labelFrame, image=photoImage)
        self.image_label.image = photoImage
        self.image_label.pack()

        self.flag = True
    
    # ファイル削除
    def delete_image(self):
        p = pathlib.Path(self.path)
        p.unlink()

    # 画像を非表示にする
    def clear_image(self):
        try:
            # 画像非表示
            self.image_label.image = None
            # 新しいLabelが生成されることを防ぐため削除
            self.image_label.destroy()
        except :
            print("Not found image_label...")

        self.flag = False

    def funcDragAndDrop(self, event):
        # ファイル名にスペースがあると{$path}で返却される
        # 参考:https://juu7g.hatenablog.com/entry/Python/csv/viewer
        self.path = event.data
        # 画像を表示
        self.load_image_drog_and_drop()
    
        #画像表示
    def load_image_drog_and_drop(self):

        if self.flag:
            self.clear_image()

        file_path = self.path
        image = Image.open(open(file_path, 'rb'))
        # アスペクトを維持しながら、指定したサイズ以下に画像を縮小
        image.thumbnail((400, 400))
        photoImage = ImageTk.PhotoImage(image)
        # 画像を表示
        self.image_label = tk.Label(self.labelFrame, image=photoImage)
        self.image_label.image = photoImage
        self.image_label.pack()

        self.flag = True

if __name__ == "__main__":
    app = MyApp()
    app.mainloop()

解説

GUIパーツの配置

まず、ドラッグ&ドロップ機能を有効にするためメインウィンドウはTkinterDnD.Tkで作成します。

class MyApp(TkinterDnD.Tk):
# 以下省略...

そして、MyAppクラス内のコンストラクタ(クラスのインスタンスを生成する際に自動的に呼び出されるメソッド)で必要なGUIのパーツ配置といった処理を記述しています。

class MyApp(TkinterDnD.Tk):
    def __init__(self):
        super().__init__()
	# 以下略

まず、ウィンドウサイズを指定しています。

self.geometry(f'{WIDTH}x{HEIGHT}')

そして、画像の読み込みボタンと非表示ボタンの設定を行います。。
まずFrameを指定して、その中にgridをつかってボタンを配置しています。
画像読み込みボタンにはload_image関数、画像の非表示ボタンにはclear_image関数がボタン押下時のイベント処理の関数として指定されています。

buttonFrame = tk.Frame(self)
# 画像を読み込むためのボタン
load_button = tk.Button(buttonFrame, text="Load Image",
command=self.load_image, width=10)
load_button.grid(column=0, row=0)
# 画像を非表示にするためのボタン
clear_button = tk.Button(buttonFrame, text="Clear", command=self.clear_image, width=10)
clear_button.grid(column=1, row=0)
buttonFrame.pack(pady=20)

さらに、画像をドラッグ&ドロップして表示する部分はLabelFrameで作成しています。
ドラッグ&ドロップの動作を受けて入れられるオブジェクト(ドロップターゲット)にするため、drop_target_registerメソッドを使用しています。ここではファイルのドロップに対応するためDND_Filesを指定しています。
また、イベントと関数を紐付けるためdnd_bindメソッドを使用しています。ウィジェット上でドロップしたときにイベントが発生するように<<Drop>>を指定しています。そしてfuncDragAndDrop関数が呼び出されます。

self.labelFrame = tk.LabelFrame(self, width=400, height=400, text="画像をドラッグ&ドロップ", labelanchor="n")
self.labelFrame.drop_target_register(DND_FILES)
self.labelFrame.dnd_bind('<<Drop>>', self.funcDragAndDrop)
self.labelFrame.pack()

それぞれの関数の説明

load_image関数

こちらは、画像読み込みボタンが押されたときに呼び出される関数です。
ダイアログボックスからファイルを開き、そのパスを取得し画像を表示します。
前の画像が表示部にある場合は、それを一度削除してから表示します。
これは、Flagによって管理します。

def load_image(self):
        # 画像を表示する前に画像が表示されていた場合、前の画像を削除
        if self.flag:
            self.clear_image()
        # ファイルを開くダイアログボックスを表示する
        self.path = filedialog.askopenfilename()
        path = self.path
        print(path)

        image = Image.open(open(path, 'rb'))
        # アスペクトを維持しながら、指定したサイズ以下に画像を縮小
        image.thumbnail((400, 400))
        photoImage = ImageTk.PhotoImage(image)

        # 画像を表示
        self.image_label = tk.Label(self.labelFrame, image=photoImage)
        self.image_label.image = photoImage
        self.image_label.pack()

        self.flag = True

clear_image関数

こちらは画像表示部から画像を削除する関数です。
画像表示部はLabelFrameをつかって表示されています。
こちらのオブジェクトをdestroyして削除しています。

def clear_image(self):
        try:
            # 画像非表示
            self.image_label.image = None
            # 新しいLabelが生成されることを防ぐため削除
            self.image_label.destroy()
        except :
            print("Not found image_label...")

        self.flag = False

funcDragAndDrop関数

こちらはファイルがドラッグ&ドロップされたときに呼び出される関数で、ファイルのパスを取得しています。
そして、画像を表示するためのload_image_drog_and_drop関数を呼び出しています。

def funcDragAndDrop(self, event):
        # ファイル名にスペースや日本語が含まれていると{$path}で返却されることに注意
        self.path = event.data
        # 画像を表示
        self.load_image_drog_and_drop()

        return self.path

load_image_drog_and_drop関数

こちらはファイルを指定するダイアログの処理が含まれないだけで、load_image関数と同じ処理です。

def load_image_drog_and_drop(self):

        if self.flag:
            self.clear_image()

        file_path = self.path
        image = Image.open(open(file_path, 'rb'))
        # アスペクトを維持しながら、指定したサイズ以下に画像を縮小
        image.thumbnail((400, 400))
        photoImage = ImageTk.PhotoImage(image)
        # 画像を表示
        self.image_label = tk.Label(self.labelFrame, image=photoImage)
        self.image_label.image = photoImage
        self.image_label.pack()

        self.flag = True

py2appをつかって実行ファイルをつくる

完成したコードがWindowsやMacで動作させるため、exeもしくはapp形式の実行ファイルをpy2appをつかって作成します。インターネット上で様々な記事がありますが、py2appのバージョンによって手順が異なる場合があります。なので公式ドキュメントを確認しその手順に従ったほうが、手っ取り早いです。

https://py2app.readthedocs.io/en/latest/
https://py2app.readthedocs.io/_/downloads/en/stable/pdf/

py2appのかわりに、pyinstallerを使って実行ファイルを作成したところ、ダブルクリックしても起動いない現象が発生しました。起動時のログを出して確認すると、tkinterdnd2のモジュールが見つからないため、起動に失敗していることがわかりました。そこでオプション--collect-dataをつかってtkinterdnd2を実行ファイルに含めたところ、実行ファイルを正しく作成することができました。

python3 -m PyInstaller {$ファイル名}.py --noconsole --onefile --collect-data tkinterdnd2

おわりに

tkinterとtkinterdnd2をつかって、かなりお手軽にドラッグ&ドロップに対応したGUIアプリケーションをつくることができました。ぜひ参考になれば幸いです。

Discussion