Python:tkinterでドラッグ&ドロップに対応した画像表示ビューアーをつくる
はじめに
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のバージョンによって手順が異なる場合があります。なので公式ドキュメントを確認しその手順に従ったほうが、手っ取り早いです。
py2appのかわりに、pyinstaller
を使って実行ファイルを作成したところ、ダブルクリックしても起動いない現象が発生しました。起動時のログを出して確認すると、tkinterdnd2
のモジュールが見つからないため、起動に失敗していることがわかりました。そこでオプション--collect-data
をつかってtkinterdnd2
を実行ファイルに含めたところ、実行ファイルを正しく作成することができました。
python3 -m PyInstaller {$ファイル名}.py --noconsole --onefile --collect-data tkinterdnd2
おわりに
tkinterとtkinterdnd2をつかって、かなりお手軽にドラッグ&ドロップに対応したGUIアプリケーションをつくることができました。ぜひ参考になれば幸いです。
Discussion