Windows GUIプログラミング

※サムネイル画像は、生成AIで作成したイメージです
Pythonで業務を少し便利にするツールを作りたいと思ったとき、GUIがあると使い勝手はぐっと良くなります。
本記事では、学習用に用意した独自ツール「pyRenamer」のソースコード解説を通して、下記の習得を目指します。
- Windows GUIプログラムを作成する方法
- イベントハンドラの書き方
- Drag&Drop(以降、D&D)されたファイルやフォルダを処理する方法
- Windowsのフォルダ選択コモンダイアログを呼び出す方法
この記事は以下のターゲットを対象としています。
★5 Django の開発経験が 3 年以上。
★4 Django の開発経験が 1 年以上。
★3 WEB サイト開発経験あり。これから Django を学習します。
★2 Python 初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。
目次
pyRenamerツールの紹介
「複数ファイルの名前変更・コピーを一括で行うためのGUIツール」です。
以下、具体的な使い方について説明します。
入力ファイル一覧指定部(①)に、"hoge\abc.txt"というファイルをドラッグ&ドロップ(以下、D&D)するとテキストエリア欄に"hoge\abc.txt"というテキストが追記されます。
出力ファイル一覧指定部(②)のリセットボタンを押すと、テキストエリアに、①のファイル名のみを引き継いで、"abc.txt"というテキストが表示されます。
ここでは、"abc.txt"を書き換えて"abc_20260116.txt"に変更したとします。
この状態で、
- リネームボタン(③)を押した場合、①と②の各行を対応付けて、ファイル名のリネームを行います。
"hoge\abc.txt"が、"hoge\abc_20260116.txt"にリネームされます
- 複製ボタン(④)を押した場合は、フォルダ選択のコモンダイアログを開き、指定したフォルダ直下に
①で指定したファイルを②で指定した名前にリネームして複製します。
"hoge\abc.txt"が、"{指定フォルダ}\abc_20260116.txt"としてコピーされます (マスタファイルを管理するフォルダから、納品用フォルダを切り出す用途を想定した機能です)

ソースコード(pyRenamer.py)
以下、ソースコード
import os
import shutil
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from tkinterdnd2 import DND_FILES, TkinterDnD
# ============================================================
# ファイルリネーム・コピーツール
#
# 機能概要:
# ・ファイルパスを複数行で入力(D&D対応)
# ・対応する新ファイル名を行単位で指定
# ・一括リネーム/一括コピーを実行
# ============================================================
class RenameTool(TkinterDnD.Tk):
def __init__(self):
super().__init__()
# ウィンドウ基本設定
self.title("ファイルリネーム・コピーツール")
self.geometry("1000x560")
# UI 構築開始
self._build_ui()
# ========================================================
# UI 構築(エントリポイント)
#
# UI 構築は以下のフェーズに分割する:
# ① レイアウト骨組みの構築
# ② ファイル一覧エリアの構築
# ③ 操作エリアの構築
# ④ 各種イベント・挙動の接続
#
# UI構成ツリー
# RenameTool(Tk / メインウィンドウ)
# └─ root_pane(PanedWindow|縦分割:全体レイアウト)
# ├─ io_section(PanedWindow|横分割:入出力エリア)
# │ ├─ input_frame(Frame|入力側コンテナ)
# │ │ ├─ input_header(Frame|ラベル・補助UI)
# │ │ │ └─ input_label(Label|「入力ファイル」等)
# │ │ ├─ input_line_numbers(Text / Canvas|行番号)
# │ │ ├─ input_text(Text|入力ファイル一覧)
# │ │ └─ input_scrollbar(Scrollbar|縦スクロール)
# │ │
# │ └─ output_frame(Frame|出力側コンテナ)
# │ ├─ output_header(Frame|ラベル・補助UI)
# │ │ └─ output_label(Label|「出力ファイル名」等)
# │ ├─ output_line_numbers(Text / Canvas|行番号)
# │ ├─ output_text(Text|出力ファイル一覧)
# │ └─ output_scrollbar(Scrollbar|縦スクロール)
# │
# └─ action_section(Frame|操作エリア)
# ├─ action_buttons_frame(Frame|ボタン群)
# │ ├─ rename_button(Button|リネーム実行)
# │ ├─ copy_button(Button|コピー実行)
# │ └─ clear_button(Button|クリア)
# │
# └─ status_frame(Frame|状態表示)
# └─ status_label(Label|メッセージ表示)
# ========================================================
def _build_ui(self):
self._build_layout()
self._build_file_list_area()
self._build_action_area(self.action_area)
self._setup_scroll_sync()
self._bind_mousewheel()
self._setup_drag_and_drop()
# ========================================================
# ① レイアウト骨組み
#
# root_pane(縦分割)
# ├─ file_list_section(①②)
# └─ action_area(③④)
# ========================================================
def _build_layout(self):
# 画面全体を上下に分割
self.root_pane = tk.PanedWindow(
self,
orient=tk.VERTICAL,
sashwidth=6,
sashrelief=tk.RAISED,
showhandle=True
)
self.root_pane.pack(fill=tk.BOTH, expand=True)
# 上部:ファイル一覧エリア(①②)
self.file_list_section = ttk.Frame(self.root_pane)
self.root_pane.add(self.file_list_section, stretch="always")
# 下部:操作エリア(③④)
self.action_area = ttk.Frame(self.root_pane)
self.root_pane.add(self.action_area)
# ========================================================
# ② ファイル一覧エリア(①②)
#
# 左右に分割可能な PanedWindow + 共通スクロールバー
# ========================================================
def _build_file_list_area(self):
# 左右分割用 PanedWindow
self.file_list_pane = tk.PanedWindow(
self.file_list_section,
orient=tk.HORIZONTAL,
sashwidth=6,
sashrelief=tk.RAISED,
showhandle=True
)
self.file_list_pane.pack(fill=tk.BOTH, expand=True)
# -------- ① 入力ファイル一覧 --------
self.input_frame = ttk.Frame(self.file_list_pane)
self.file_list_pane.add(self.input_frame, stretch="always")
self.input_text, self.input_lines = self._build_text_area(
self.input_frame,
title="① 入力ファイル",
clear_button=True
)
# -------- ② 出力ファイル名一覧 --------
self.output_frame = ttk.Frame(self.file_list_pane)
self.file_list_pane.add(self.output_frame, stretch="always")
self.output_text, self.output_lines = self._build_text_area(
self.output_frame,
title="② 出力ファイル",
reset_button=True
)
# 共通縦スクロールバー
self.v_scroll = ttk.Scrollbar(
self.file_list_section,
orient=tk.VERTICAL
)
self.v_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# ========================================================
# 行番号付きテキストエリア生成
#
# ・左:行番号(編集不可)
# ・右:テキスト入力欄
# ========================================================
def _build_text_area(
self,
parent,
title,
clear_button=False,
reset_button=False
):
# ヘッダ部(タイトル+操作ボタン)
header = ttk.Frame(parent)
header.pack(fill=tk.X)
ttk.Label(header, text=title).pack(side=tk.LEFT)
if clear_button:
ttk.Button(
header,
text="クリア",
command=self._clear_input
).pack(side=tk.RIGHT, padx=4)
if reset_button:
ttk.Button(
header,
text="リセット",
command=self._reset_output
).pack(side=tk.RIGHT, padx=4)
# 本体部
body = ttk.Frame(parent)
body.pack(fill=tk.BOTH, expand=True)
# 行番号欄
lines = tk.Text(
body,
width=4,
padx=4,
takefocus=0,
state="disabled",
background="#f0f0f0"
)
lines.pack(side=tk.LEFT, fill=tk.Y)
# メインテキスト欄
text = tk.Text(body, wrap="none")
text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 横スクロールバー
hbar = ttk.Scrollbar(
parent,
orient=tk.HORIZONTAL,
command=text.xview
)
hbar.pack(fill=tk.X)
text.config(xscrollcommand=hbar.set)
# 入力変更時に行番号を再生成
text.bind(
"<KeyRelease>",
lambda e: self._update_lines(text, lines)
)
return text, lines
# ========================================================
# ③④ 操作エリア
# ========================================================
def _build_action_area(self, parent):
wrapper = ttk.Frame(parent)
wrapper.pack(fill=tk.X, padx=10, pady=10)
# --- ③ リネーム ---
rename_area = ttk.Frame(wrapper)
rename_area.pack(anchor=tk.W, pady=5)
ttk.Label(
rename_area,
text="③ リネーム:①→②の対応で元ファイルを直接改名"
).pack(anchor=tk.W)
tk.Button(
rename_area,
text="リネーム",
bg="#ffcc66",
command=self._rename
).pack(anchor=tk.W, pady=3)
# --- ④ コピー ---
copy_area = ttk.Frame(wrapper)
copy_area.pack(anchor=tk.W, pady=5)
ttk.Label(
copy_area,
text="④ 指定フォルダ直下にコピー:①内容を②名でコピー"
).pack(anchor=tk.W)
tk.Button(
copy_area,
text="コピー",
bg="#99ccff",
command=self._duplicate
).pack(anchor=tk.W, pady=3)
# ========================================================
# ④ UI 挙動・イベント設定
# ========================================================
# 縦スクロール同期
def _setup_scroll_sync(self):
self.v_scroll.config(command=self._yview_all)
self.input_text.config(yscrollcommand=self.v_scroll.set)
self.output_text.config(yscrollcommand=self.v_scroll.set)
# マウスホイール同期
def _bind_mousewheel(self):
for w in (
self.input_text,
self.output_text,
self.input_lines,
self.output_lines,
):
w.bind("<MouseWheel>", self._on_mousewheel)
# ドラッグ&ドロップ設定
def _setup_drag_and_drop(self):
self.input_text.drop_target_register(DND_FILES)
self.input_text.dnd_bind("<<Drop>>", self._on_drop)
# ========================================================
# スクロール制御
# ========================================================
def _yview_all(self, *args):
self.input_text.yview(*args)
self.output_text.yview(*args)
self.input_lines.yview(*args)
self.output_lines.yview(*args)
def _on_mousewheel(self, event):
delta = -1 if event.delta > 0 else 1
self._yview_all("scroll", delta, "units")
return "break"
# ========================================================
# ロジック処理
# ========================================================
def _get_lines(self, text):
return [
l for l in text.get("1.0", tk.END).splitlines()
if l.strip()
]
def _validate(self):
src = self._get_lines(self.input_text)
dst = self._get_lines(self.output_text)
if len(src) != len(dst):
raise ValueError("①と②の行数が一致していません")
if len(set(src)) != len(src):
raise ValueError("①に重複パスがあります")
if len(set(dst)) != len(dst):
raise ValueError("②の出力名が重複します")
for p in src:
if not os.path.isfile(p):
raise ValueError(f"存在しないファイル: {p}")
return src, dst
def _rename(self):
try:
src, dst = self._validate()
for s, d in zip(src, dst):
os.rename(s, os.path.join(os.path.dirname(s), d))
messagebox.showinfo("完了", "リネームが完了しました")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _duplicate(self):
try:
src, dst = self._validate()
except Exception as e:
messagebox.showerror("エラー", str(e))
return
folder = filedialog.askdirectory()
if not folder:
return
for s, d in zip(src, dst):
shutil.copy2(s, os.path.join(folder, d))
messagebox.showinfo("完了", "コピーが完了しました")
# ========================================================
# 補助処理
# ========================================================
def _reset_output(self):
self.output_text.delete("1.0", tk.END)
for p in self._get_lines(self.input_text):
self.output_text.insert(
tk.END,
os.path.basename(p) + "\n"
)
def _clear_input(self):
self.input_text.delete("1.0", tk.END)
self.output_text.delete("1.0", tk.END)
def _update_lines(self, text, lines):
count = int(text.index("end-1c").split(".")[0])
lines.config(state="normal")
lines.delete("1.0", tk.END)
for i in range(1, count + 1):
lines.insert(tk.END, f"{i:>3}\n")
lines.config(state="disabled")
def _on_drop(self, event):
paths = self.tk.splitlist(event.data)
for p in paths:
if os.path.isdir(p):
for r, _, fs in os.walk(p):
for f in fs:
self.input_text.insert(
tk.END, os.path.join(r, f) + "\n"
)
else:
self.input_text.insert(tk.END, p + "\n")
if __name__ == "__main__":
RenameTool().mainloop()
実行方法
- Pythonをインストールして、環境変数PATHにpython.exeへのパスを登録します。
- tkinterdnd2のインストール
pip install tkinterdnd2 - コマンドプロンプトなどで、カレントフォルダをpyRenamer.pyのあるフォルダに移動し、下記を実行します。
python pyRenamer.py
ソースコード説明 1: Windows GUIプログラムを作成する方法
GUIプログラムのポイントを説明していきます。
import tkinter as tk
→tkinterというライブラリを使用しています。
これは、Pythonの標準ライブラリでGUIプログラミングを行うためのライブラリです。 (WindowsだけでなくLinuxでも同じコードで動作します)
class RenameTool(TkinterDnD.Tk):
→TkinterDnD.Tkを継承したクラスRenameToolを定義しています
RenameTool().mainloop()
→RenameTool()により、通常のクラス同様に、インスタンスが生成され、コンストラクタとして__init__()が実行されます。
各種初期化処理を行った後、self._build_ui()の呼び出しで、UI要素を順次作成していきます。
インスタンス作成後は、作成したインスタンスのmainloop()を実行しています。
mainloop()は、内部に無限ループの構造を持っており、イベントが発生していないかを監視し、イベントが発生したら対応したイベントハンドラをコールバックするという処理を延々と繰り返しています。
例えば、ボタンを押したり、D&Dしたり、ウィンドウサイズを広げたりするとイベントが発生します。
ソースコード説明 2: イベントハンドラの書き方
リネームボタンを押した時は、_rename()を呼ぶことで、リネーム処理を行います。
tk.Button(
rename_area,
text="リネーム",
bg="#ffcc66",
command=self._rename
).pack(anchor=tk.W, pady=3)
→引数commandに関数self._renameの参照を設定しています。
これにより、ボタン押下イベント発生時に、イベントハンドラとしてself._renameが呼ばれるようになります。
ソースコード説明 3: D&Dされたファイルやフォルダを処理する方法
D&Dを実現している方法について説明します
from tkinterdnd2 import DND_FILES, TkinterDnD
→tkinterdnd2というライブラリを使用しています。
これはファイルやフォルダのD&Dを実現するためのライブラリです。
self.input_text.drop_target_register(DND_FILES)
self.input_text.dnd_bind("<<Drop>>", self._on_drop)
→D&Dの受け取り対象にDND_FILESを指定することで、「複数ファイル(またはフォルダ)」のドロップに対応できるようにしています。
また、ドロップイベントに、self._on_dropをイベントハンドラとして紐づけています
def _on_drop(self, event):
paths = self.tk.splitlist(event.data)
for p in paths:
if os.path.isdir(p):
for r, _, fs in os.walk(p):
for f in fs:
self.input_text.insert(
tk.END, os.path.join(r, f) + "\n"
)
else:
self.input_text.insert(tk.END, p + "\n")
→_on_dropでは、ドロップされたものが、
- フォルダであれば、そのフォルダ配下のサブフォルダも再帰的に参照し、見つかったファイルパスを①のテキストエリア末尾に追記します。
- ファイルであれば、そのファイルパスを①のテキストエリア末尾に追記します。
ソースコード説明 4: Windowsのフォルダ選択コモンダイアログを呼び出す方法
フォルダ選択のコモンダイアログを表示する方法は簡単です。
folder = filedialog.askdirectory()
→フォルダ選択ダイアログを開き、選択結果のフォルダがfolderに格納されます。
まとめ
過去に.NET FrameWork等を使ってのGUIアプリケーションやツールを作成した経験はありましたが、今回Python環境で作成してみて感じたメリットは、開発をスタートするまでの敷居の低さと、動作確認→修正→動作確認のサイクルが短く回せることでした。
デザイナツール(画面要素をビジュアル的に配置し、コードを自動生成するツール)を今回は使用しなかったこともあり、実際に実行してみると期待している画面の動きと食い違うことが、しばしばありましたが、「少し修正して確認」というのが短時間で回せたので、集中力が途切れることなく作業できました。

