potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

Python 3 tkinterでWhiteBrowserの簡単な操作を提供するGUIアプリケーション

2022/12/5更新:タグファイルの作成対応、WBDirInfoクラスの作成、設定ファイル及び一部変数名のアンダーライン削減。

動画管理ソフトWhiteBrowserの簡単な操作を提供するGUIアプリケーションです。Python標準ライブラリーtkinterモジュールの練習で作成しています。おかしなコードが含まれている可能性にご留意ください。

実行前に同一ディレクトリ(作業ディレクトリ)にwb.exeのディレクトリパスを記載したUTF-8の「wbdir.txt」が必要です。

コード

コンソールが不要なので拡張子は「.pyw」にします。

# WhiteBrowserTool.pyw
import os
import shutil
import sqlite3
from tkinter import *
from tkinter import messagebox
from tkinter.ttk import *
from pathlib import Path
from contextlib import closing
from typing import Iterator

def vacuum_sqlite_file(path: Path) -> None:
    with closing(sqlite3.connect(path)) as connect:
        with closing(connect.cursor()) as cur:
            cur.execute("VACUUM")

class WBDirInfo:
    __path: Path

    def __init__(self, path):
        self.__path = Path(path)

    @property
    def path(self) -> Path:
        return self.__path

    @property
    def thumdir_path(self) -> Path:
        return self.__path / "thum"

    @property
    def tempdir_path(self) -> Path:
        return self.__path / "temp"

    def iter_wbfile_path(self) -> Iterator[Path]:
        return (p for p in self.path.glob("*.wb") if p.is_file())

    def iter_thumdir_path(self) -> Iterator[str]:
        return (p for p in self.thumdir_path.iterdir() if p.is_dir())

    def iter_thumdir_name(self) -> Iterator[str]:
        return (p.name for p in self.iter_thumdir_path())

    def iter_tagfile_path(self) -> tuple[Path]:
        return (p for p in self.tempdir_path.glob("*.tag") if p.is_file())

    def iter_tagfile_name(self) -> tuple[str]:
        return (p.name for p in self.iter_tagfile_path())

    def make_tagfile_path(self, name:str) -> Path:
        return self.tempdir_path / name

class MyApp(Tk):
    __wbdir: WBDirInfo

    def __init__(self):
        super().__init__()
        #外部設定の読み込み
        try:
            self.__wbdir = WBDirInfo(Path("wbdir.txt").read_text())
        except:
            messagebox.showerror(
                "エラー",
                "同フォルダにWhiteBrowser.exeのディレクトリを記述したwbdir.txtを作成してください。",
                parent=self)
            exit()
        if not self.__wbdir.path.is_dir():
            messagebox.showerror(
                "エラー",
                "wbdir.txtで指定されたパスがディレクトリではありません。",
                parent=self)
            exit()

        # ウィンドウとウィジェットの準備
        self.resizable(True, False)
        self.title("WhiteBrowserTool")

        frame1 = Frame(self, padding=16)

        open_wbdir_button = Button(
            frame1, text="WhiteBrowserディレクトリを開く...",
            command=lambda: os.startfile(self.wbdir.path))

        delete_thumbnail_button = Button(
            frame1, text="サムネイルの削除▼",
            command=self.show_thumbnail_delete_menu)

        show_tagfile_button = Button(
            frame1, text="タグファイルを開く▼",
            command=self.show_tagfile_open_menu)

        make_tagfile_button = Button(
            frame1, text="タグファイルの作成▼",
            command=self.show_tagfile_make_menu)

        vacuum_db_button = Button(
            frame1, text="DBの空き領域解放▼",
            command=self.vacuum_db_menu)

        vacuum_all_db_button = Button(
            frame1, text="全DBの空き領域解放",
            command=self.vacuum_all_db_menu)

        sep1 = Separator(frame1)

        exit_button = Button(
            frame1, text="終了",
            command=lambda: self.destroy())

        button_params = dict(fill="x", ipadx=5, expand=True, anchor="n")
        open_wbdir_button.pack(**button_params)
        delete_thumbnail_button.pack(**button_params)
        show_tagfile_button.pack(**button_params)
        make_tagfile_button.pack(**button_params)
        vacuum_db_button.pack(**button_params)
        vacuum_all_db_button.pack(**button_params)
        sep1.pack(fill="x", ipadx=5, expand=True, anchor="center")
        exit_button.pack(**button_params)
        frame1.pack(fill="both", expand=True)

    @property
    def wbdir(self) -> WBDirInfo:
        return self.__wbdir

    #「サムネイルの削除」
    def show_thumbnail_delete_menu(self):
        menu = Menu(self, tearoff=False)
        for p in self.wbdir.iter_thumdir_path():
            menu.add_command(
                label=p.name,
                command=lambda p=p: shutil.rmtree(p))
        menu.post(self.winfo_pointerx(), self.winfo_pointery())

    #「タグファイルを開く▼」
    def show_tagfile_open_menu(self):
        menu = Menu(self, tearoff=False)
        for p in self.wbdir.iter_tagfile_path():
            menu.add_command(
                label=p.name,
                command=lambda p=p: os.startfile(p))
        menu.post(self.winfo_pointerx(), self.winfo_pointery())

    #「タグファイルの作成▼」
    def show_tagfile_make_menu(self):
        #タグファイルの存在しないサムネイルディレクトリ名の抽出
        tempdir_names = set(self.wbdir.iter_thumdir_name())
        #TODO:正規表現への置き換え
        tagfile_names = set((s.lower().rstrip(".tag") for s in self.wbdir.iter_tagfile_name()))

        menu = Menu(self, tearoff=False)
        for name in (tempdir_names - tagfile_names):
            path = self.wbdir.make_tagfile_path(name + ".tag")
            menu.add_command(
                label=name,
                command=lambda p=path: self.open_and_create_tagfile(p))
        menu.post(self.winfo_pointerx(), self.winfo_pointery())

    def open_and_create_tagfile(p: Path):
        p.write_text(
            "[評価]\n★★★★★\n★★★★\n★★★\n★★\n\n\n",
            "shift-jis")
        os.startfile(p)

    #「DBの空き領域解放▼」
    def vacuum_and_report_wb_file(self, path: Path) -> None:
        prev_size = os.path.getsize(path)
        vacuum_sqlite_file(path)
        messagebox.showinfo(
            "報告",
            f"{path.name}のサイズ増減:{os.path.getsize(path) - prev_size}バイト",
            parent=self)

    def vacuum_db_menu(self):
        menu = Menu(self, tearoff=False)
        for p in self.wbdir.iter_wbfile_path():
            menu.add_command(
                label=p.name,
                command=lambda p=p: self.vacuum_and_report_wb_file(p))
        menu.post(self.winfo_pointerx(), self.winfo_pointery())

    #「全DBの空き領域解放」
    def vacuum_all_db_menu(self):
        ret = messagebox.askquestion(
            "確認",
            "DBのバキュームには時間がかかる場合があります。実行しますか?",
            parent=self)
        if ret != "yes":
            return
        size_diff = 0
        for p in self.wbdir.iter_wbfile_path():
            prev_size = os.path.getsize(p)
            vacuum_sqlite_file(p)
            size_diff += prev_size - os.path.getsize(p)
        messagebox.showinfo(
            "報告",
            f"サイズ:-{size_diff}バイト")

MyApp().mainloop()

メモ

forを使ってcommand付きのメニュー項目を動的に作成するような場合、値束縛に注意が必要です。詳しくは以下の方のページが分かりやすかったです。このコードではラムダ式で引数の既定値を指定する方法を採用しています。