Tkinterで画像ビューワを作る 続き
前々回、前回作成した画像ビューワを更に改良していきます
今回は
- 全画面表示
- ファイルの削除
の二つをやっていきます
全画面表示
アプリケーションを全画面表示にします
windowsではroot.attributesで'-fullscreen',Ubuntuでは'-zoomed'で更新できます
root.attributes('-fullscreen', False)
root.attributes('-zoomed', True)
windowsしか持っていないので、以後windowsのみ説明します
attributes('-fullscreen')は二番目の引数なしで呼ぶと現在の状態が取得できるので
現在の状態を保存しておく用の変数が要らないです
if root.attributes('-fullscreen'): # 全画面表示のとき
root.attributes('-fullscreen', False) # 全画面表示解除
else:
root.attributes('-fullscreen', True) # 全画面表示にする
基本的にはこれで動くのですが、私の環境だと全画面を戻した後ウィンドウが最前面に固定されてしまいました。なので全画面表示を解除したときに最前面も解除することにします
普通は呼ぶ必要ないですが呼んでも特に害は無いのでそのまま載せておきます
root.attributes('-topmost', False) # 最前面解除
さて画面は最大化しましたが上の方に出ている読込ボタンとかが邪魔なので
最大化したときにこれを除去することにしましょう
packで設置したフレームはpack_forget()で表示から消し
再度packすることで表示できます
ただしpackは先にあるモノの後にどんどん積んで表示されるので
今表示されている描画用フレームより前に挿入する必要があります
fig.普通にpackするだけだと位置が変わってしまう
先にpackしてあるものより前に挿入するには以下のようにします
frame_1.pack(fill=tkinter.X, before=frame_2)
後はこれをボタンなりショートカットキーで呼び出すだけです
せっかくなので他のアプリに合わせてF11キーに割り当てましょう
rootに新しくbindしても良いのですが前回左右キーを取得するためにをbind済みなので
このkey_func関数内に継ぎ足していきましょう
root.bind("<KeyPress>", self.key_func)
def key_func(self, event):
# 前略
if event.keysym == "F11":
if self.root.attributes('-fullscreen'): # 全画面の時
self.root.attributes('-fullscreen', False) # 全画面解除
self.root.attributes('-topmost', False) # 最上位解除
self.frame_c.pack(fill=tkinter.X, before=self.frame_b) # 上部フレームを再表示
else:
self.root.attributes('-fullscreen', True) # 全画面
self.frame_c.pack_forget() # 上部フレームを除去
elif event.keysym == "Escape":
if self.root.attributes('-fullscreen'): # 全画面の時
self.root.attributes('-fullscreen', False) # 全画面解除
self.root.attributes('-topmost', False) # 最上位解除
self.frame_c.pack(fill=tkinter.X, before=self.frame_b) # 上部フレームを再表示
ファイルの削除
ファイルの削除はos.remove()で出来ますがこれでそのまま削除すると間違ったときに取り返しがつかなくなるので、通常はゴミ箱に移動させます
ゴミ箱に移動はsend2trashを使えば一発で出来ます
import send2trash
send2trash.send2trash('kari.jpg')
send2trashは標準モジュールではないのでpipなりcondaなりでインストールしてください
ですが私の環境でpipのインストールがなんかうまくいかなかったのでwin32apiでゴミ箱に移動させてみました。当然windows専用になります
import ctypes
FO_DELETE = 0x0003 # 削除
FOF_ALLOWUNDO = 0x0040 # ごみ箱に入れる
class SHFILEOPSTRUCT(ctypes.Structure):
_fields_ = [("hwnd", ctypes.c_void_p),
("wFunc", ctypes.c_int32),
("pFrom", ctypes.c_wchar_p),
("pTo", ctypes.c_wchar_p),
("fFlags", ctypes.c_ushort),
("fAnyOperationsAborted", ctypes.c_bool),
("lpszProgressTitle", ctypes.c_wchar_p)
]
def send_trash_exec(file_path):
file_path_del = file_path + "\x00\x00" # ヌル文字二個追加
shell32 = ctypes.WinDLL("shell32")
shell32.SHFileOperationW.restype = ctypes.c_int32
shell32.SHFileOperationW.argtypes = (ctypes.POINTER(SHFILEOPSTRUCT),)
shfs = SHFILEOPSTRUCT()
shfs.hwnd = None
shfs.wFunc = FO_DELETE
shfs.pFrom = file_path_del
shfs.pTo = None
shfs.fFlags = FOF_ALLOWUNDO
shfs.lpszProgressTitle = None
shell32.SHFileOperationW(shfs)
こんなことするよりsend2trashモジュールをインストールした方が早くて確実です
おすすめはしません。標準以外のモジュールを使いたくなって人はたまに居ますが、そもそもすでにpillowが無いと動かないですしね
ではこれを前回までのコードに組み込んでいきます
デフォルトコンストラクタがかなり長くなってきたのでインスタンス変数の初期化とウィンドウの初期設定部分を分けます。以下最終コード
# -*- coding:utf-8 -*-
import os
import ctypes
import tkinter
import tkinter.filedialog
from PIL import Image, ImageTk, ImageOps
FO_DELETE = 0x0003 # SHFILEOPSTRUCT構造体用定数 削除
FOF_ALLOWUNDO = 0x0040 # SHFILEOPSTRUCT構造体用定数 ごみ箱に入れる
PICTURE_DIR = os.getenv("HOMEDRIVE") + os.getenv("HOMEPATH") + "\\Pictures" # マイピクチャ
SETTING_FILE = "setting.txt"
# win32api SHFileOperationW用構造体の定義
class SHFILEOPSTRUCT(ctypes.Structure):
_fields_ = [("hwnd", ctypes.c_void_p),
("wFunc", ctypes.c_int32),
("pFrom", ctypes.c_wchar_p),
("pTo", ctypes.c_wchar_p),
("fFlags", ctypes.c_ushort),
("fAnyOperationsAborted", ctypes.c_bool),
("lpszProgressTitle", ctypes.c_wchar_p)
]
class TkinterTestClass:
def __init__(self):
self.pre_info = [] # setting.txtの中身
self.root = None # メインウィンドウ
self.string_v = None # ラベルの文字
self.frame_c = None # フレーム コントロールパネル
self.frame_b = None # フレーム 画像描画部
self.img = None
self.img_pil = None
self.picture_width = 0
self.picture_height = 0
self.file_array = [] # 画像ファイル配列
self.file_no = 0 # 表示しているファイル
self.init()
# 初期設定
def init(self):
self.settingfile_read() # 前回保存データ読み込み
geometry = "500x350" # 画面サイズ初期値
if len(self.pre_info) > 0: # 前回データの読み込みに成功したとき
geometry = self.pre_info[0]
self.root = tkinter.Tk()
self.root.title("無題")
self.root.geometry(geometry)
self.frame_c = tkinter.Frame(self.root) # 制御エリア
self.frame_b = tkinter.Frame(self.root) # 画像描画エリア
# ファイル読み込みボタン
button = tkinter.Button(self.frame_c, font=("メイリオ", "10", "bold"), text="読込", command=lambda: [self.fg()])
button.pack(side=tkinter.LEFT)
# ラベル制御用StringVar
self.string_v = tkinter.StringVar()
self.string_v.set("")
# ファイル名表示用ラベル
label = tkinter.Label(self.frame_c, textvariable=self.string_v, bg="white")
label.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH)
self.frame_c.pack(fill=tkinter.X)
self.frame_b.pack(expand=True, fill=tkinter.BOTH, padx=1, pady=1)
# 画像表示用キャンバス作成
self.canvas = tkinter.Canvas(self.frame_b, bg="white")
self.canvas_img = self.canvas.create_image(0, 0, anchor=tkinter.NW)
self.canvas.pack(expand=True, fill=tkinter.BOTH) # キャンバス描画
if len(self.pre_info) > 1: # 前回データの読み込みに成功したとき
file_path = self.pre_info[1]
self.image_change(file_path) # 画像を変更
self.create_file_list(file_path) # ディレクトリのファイル一覧を取得
self.string_v.set(file_path) # ラベルにファイル名を表示
self.root.bind('<Configure>', self.resize_root) # rootの大きさや位置を変更したときのイベント
self.root.bind("<KeyPress>", self.key_func)
self.root.protocol("WM_DELETE_WINDOW", self.settingfile_write)
self.root.mainloop()
# フォルダ内の画像ファイルリストを作成
def create_file_list(self, file_path):
self.file_array = [] # 画像ファイル配列
tmp_arr = os.path.split(file_path) # ファイルのパスをディレクトリとファイル名に分解
dir_name = tmp_arr[0] # ディレクトリ
file_name = tmp_arr[1] # ファイル名
n = 0
for fname in os.listdir(dir_name): # ディレクトリ内のファイル一覧を取得
file_ext = os.path.splitext(fname)[1].lower() # 拡張子を小文字にして取得
if file_ext == ".jpg" or file_ext == ".png" or file_ext == ".tif" or file_ext == ".jpeg":
self.file_array.append(os.path.join(dir_name, fname)) # 画像ファイルのとき、配列に格納
if file_name == fname:
self.file_no = n # 開いたファイルの配列番号を残しておく
n += 1
# 画像変更
def image_change(self, file_path):
# キャンバスサイズ取得
self.check_canvas_size()
try:
# self.img = tkinter.PhotoImage(file=file_path)# ファイルを読み込んでPhotoImageオブジェクトを作成
self.img_pil = Image.open(file_path) # PhotoImageオブジェクト作成をpillowで行う
if self.picture_width == 0 or self.picture_height == 0:
self.img = ImageTk.PhotoImage(image=self.img_pil)
else:
img_pil2 = ImageOps.pad(self.img_pil, (self.picture_width, self.picture_height))
self.img = ImageTk.PhotoImage(image=img_pil2)
self.canvas.itemconfig(self.canvas_img, image=self.img) # 画像を変更
self.string_v.set(file_path) # ファイル名をラベルに表示
except:
self.img = None
# キー押されたとき
def key_func(self, event):
mode = -1
if event.keysym == "Right":
self.next_picture(1)
elif event.keysym == "Left":
self.next_picture(-1)
elif event.keysym == "Escape":
if self.root.attributes('-fullscreen'): # 全画面の時
self.root.attributes('-fullscreen', False) # 全画面解除
self.root.attributes('-topmost', False) # 最前面解除
self.frame_c.pack(fill=tkinter.X, before=self.frame_b) # コントロールパネルを復旧
self.frame_c.pack(fill=tkinter.X)
elif event.keysym == "F11":
if self.root.attributes('-fullscreen'): # 全画面の時
self.root.attributes('-fullscreen', False) # 全画面解除
self.root.attributes('-topmost', False) # 最前面解除
self.frame_c.pack(fill=tkinter.X, before=self.frame_b) # コントロールパネルを復旧
else:
self.root.attributes('-fullscreen', True) # 全画面に切り替え
self.frame_c.pack_forget() # コントロールパネルを除去
elif event.keysym == "Delete":
self.send_trash()
else:
pass
# 次のファイルを表示
# offset: 1:次のファイル
# -1:前のファイル
def next_picture(self, offset):
cnt = len(self.file_array)
if cnt == 0: # 画像ファイル配列が空の時何もしない
return False
file_no = self.file_no
file_path = ""
while file_path == "":
self.file_no += offset
if self.file_no == cnt: # 配列上限オーバー時は0に戻る
self.file_no = 0
elif self.file_no == -1: # 配列上限オーバー時は0に戻る
self.file_no = cnt - 1
file_path = self.file_array[self.file_no] # どのファイルを表示するか決定する
if file_no == self.file_no:
break
if file_path == "": # 配列を一周して全部空だったとき
self.file_array = []
self.file_no = 0
self.img_pil = None
self.img = None
self.canvas.itemconfig(self.canvas_img, image=self.img)
return False
self.image_change(file_path) # 画像の変更
# ゴミ箱へ送る
def send_trash(self):
file_path = self.file_array[self.file_no]
# send2trash.send2trash(file_path)
self.send_trash_exec(file_path)
self.file_array[self.file_no] = ""
self.next_picture(1)
# ファイルをゴミ箱へ送る
def send_trash_exec(self, file_path):
file_path_del = file_path + "\x00\x00" # ヌル文字二個追加
shell32 = ctypes.WinDLL("shell32")
shell32.SHFileOperationW.restype = ctypes.c_int32
shell32.SHFileOperationW.argtypes = (ctypes.POINTER(SHFILEOPSTRUCT),)
shfs = SHFILEOPSTRUCT()
shfs.hwnd = None
shfs.wFunc = FO_DELETE
shfs.pFrom = file_path_del
shfs.pTo = None
shfs.fFlags = FOF_ALLOWUNDO
shfs.lpszProgressTitle = None
shell32.SHFileOperationW(shfs)
# ファイル読み込みダイアログ
def fg(self):
file_type = [("画像ファイル", "*.jpg;*.png;*.bmp")]
file_path = tkinter.filedialog.askopenfilename(filetypes=file_type, initialdir=PICTURE_DIR)
# ファイルが選択されていたら画像変更
if file_path != "":
self.image_change(file_path) # 画像を変更
self.create_file_list(file_path) # ディレクトリのファイル一覧を取得
# メインウィンドウの大きさを変えたとき
def resize_root(self, event):
# イメージが無いときは何もしない
if self.img_pil is None:
return False
# サイズを変更する必要があるかチェック
if self.check_canvas_size():
# イメージをリサイズ
img_pil2 = ImageOps.pad(self.img_pil, (self.picture_width, self.picture_height))
self.img = ImageTk.PhotoImage(image=img_pil2)
self.canvas.itemconfig(self.canvas_img, image=self.img)
# キャンバスサイズ取得
def check_canvas_size(self):
canvas_width = 0
canvas_height = 0
try:
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
except:
return False # キャンバスサイズが取得できないときFalse
# キャンバスサイズの縦横どちらかが2より小さいならならFalse
if canvas_width < 2 or canvas_height < 2:
return False
# 前回のサイズから変わってなければFalse
if canvas_width == self.picture_width and canvas_height == self.picture_height:
return False
# キャンバスサイズを記憶しておく
self.picture_width = canvas_width
self.picture_height = canvas_height
return True
# 設定ファイル読み込み
def settingfile_read(self):
try:
with open(SETTING_FILE) as f:
s = f.read()
self.pre_info = s.splitlines()
except FileNotFoundError:
pass
# 設定ファイル書き込み
def settingfile_write(self):
str_tmp = self.root.geometry() + "\n" # メインウィンドウの位置と大きさ
if len(self.file_array) > 0:
str_tmp += self.file_array[self.file_no] + "\n"
with open(SETTING_FILE, mode="w") as f:
f.write(str_tmp)
self.root.destroy() # ウィンドウを閉じる これを忘れると閉じられなくなる
if __name__ == '__main__':
TkinterTestClass()