PythonとtkinterでシンプルなファイルマネージャーGUIを作る

## はじめに

この記事では、Pythonとtkinterライブラリを使用して、シンプルながら機能的なファイルマネージャーGUIアプリケーションを作成する方法を紹介します。

## 要件

このファイルマネージャーアプリケーションは、以下の要件を満たすように設計されています：

1. ユーザーフレンドリーなGUIインターフェース
2. ファイルとフォルダの階層構造の表示
3. 基本的なファイル操作機能（新規作成、追加、削除、移動、名前変更）
4. ファイル名による検索機能
5. 直感的な操作（右クリックメニュー、ダブルクリックでの展開）

## 仕様

アプリケーションは以下の仕様に基づいて実装されています：

1. GUI framework: tkinter
2. プログラミング言語: Python 3.x
3. ファイルシステム操作: os および shutil モジュール
4. ユーザーインターフェース:
   - メインウィンドウサイズ: 800x600 ピクセル
   - ツリービュー: ファイルとフォルダの階層構造表示
   - 検索バー: 画面上部に配置
   - スクロールバー: ツリービューの右側に配置
5. ファイル操作:
   - 新規フォルダ作成: 選択したディレクトリ内に新しいフォルダを作成
   - ファイル追加: 選択したディレクトリに外部ファイルをコピー
   - 削除: 選択したファイルまたはフォルダを削除（確認ダイアログあり）
   - 移動: 選択したファイルまたはフォルダを別の場所に移動
   - 名前変更: 選択したファイルまたはフォルダの名前を変更
6. 検索機能:
   - 部分一致によるファイル名検索
   - 検索結果をツリービューに表示
7. エラー処理:
   - アクセス権限エラーの処理（PermissionError）
   - 無効な操作に対するユーザーへのフィードバック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/7a9265bd-f37c-c0d2-951a-899e7f32cf4b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/ccb22253-c0b2-c0ce-bc49-11d1a2e7e1ef.png)
検索したとき

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/dd06303d-10c4-67a3-d4bf-eea40089e9d5.png)
ファイル操作のメニュー


## 必要なライブラリ

このプロジェクトでは、Pythonの標準ライブラリのみを使用します：

- `tkinter`: GUIの作成
- `os`: ファイルシステム操作
- `shutil`: 高水準のファイル操作

## コードの全体像

まず、完全なコードを示します。その後、主要な部分を詳しく説明していきます。

```python
import os
import shutil
from tkinter import *
from tkinter import ttk, filedialog, messagebox, simpledialog

class FileManagerGUI:
    def __init__(self, master):
        self.master = master
        self.master.title("ファイルマネージャー")
        self.master.geometry("800x600")

        # 検索バーの追加
        self.search_frame = Frame(self.master)
        self.search_frame.pack(side=TOP, fill=X)
        self.search_entry = Entry(self.search_frame)
        self.search_entry.pack(side=LEFT, expand=True, fill=X)
        self.search_button = Button(self.search_frame, text="検索", command=self.search_files)
        self.search_button.pack(side=RIGHT)

        self.tree = ttk.Treeview(self.master)
        self.tree.pack(side=LEFT, fill=BOTH, expand=True)

        self.scrollbar = ttk.Scrollbar(self.master, orient="vertical", command=self.tree.yview)
        self.scrollbar.pack(side=RIGHT, fill=Y)

        self.tree.configure(yscrollcommand=self.scrollbar.set)

        self.root_path = os.path.dirname(os.path.abspath(__file__))

        self.populate_tree()

        self.tree.bind("<Double-1>", self.on_double_click)
        self.tree.bind("<Button-3>", self.show_context_menu)

        # 右クリックメニューの作成
        self.context_menu = Menu(self.master, tearoff=0)
        self.context_menu.add_command(label="新規フォルダ作成", command=self.create_folder)
        self.context_menu.add_command(label="ファイル追加", command=self.add_file)
        self.context_menu.add_command(label="削除", command=self.delete_item)
        self.context_menu.add_command(label="移動", command=self.move_item)
        self.context_menu.add_command(label="名前変更", command=self.rename_item)

    def populate_tree(self, node=""):
        if node == "":
            self.tree.delete(*self.tree.get_children())
            node = self.tree.insert('', 'end', text=self.root_path, open=True)
            self.populate_tree(node)
        else:
            path = self.tree.item(node, "text")
            try:
                for item in os.listdir(path):
                    full_path = os.path.join(path, item)
                    try:
                        if os.path.isdir(full_path):
                            child = self.tree.insert(node, 'end', text=full_path, open=False)
                            self.populate_tree(child)
                        else:
                            self.tree.insert(node, 'end', text=full_path, open=False)
                    except PermissionError:
                        continue
            except PermissionError:
                pass

    def on_double_click(self, event):
        item = self.tree.selection()[0]
        path = self.tree.item(item, "text")
        if os.path.isdir(path):
            self.tree.delete(*self.tree.get_children(item))
            self.populate_tree(item)

    def show_context_menu(self, event):
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
            self.context_menu.post(event.x_root, event.y_root)

    def create_folder(self):
        selected_item = self.tree.selection()[0]
        parent_path = self.tree.item(selected_item, "text")
        
        if not os.path.isdir(parent_path):
            parent_path = os.path.dirname(parent_path)

        new_folder = simpledialog.askstring("新規フォルダ", "フォルダ名を入力してください:")
        if new_folder:
            new_path = os.path.join(parent_path, new_folder)
            os.makedirs(new_path, exist_ok=True)
            self.refresh_tree()

    def add_file(self):
        selected_item = self.tree.selection()[0]
        parent_path = self.tree.item(selected_item, "text")
        
        if not os.path.isdir(parent_path):
            parent_path = os.path.dirname(parent_path)

        file_path = filedialog.askopenfilename()
        if file_path:
            shutil.copy(file_path, parent_path)
            self.refresh_tree()

    def delete_item(self):
        selected_item = self.tree.selection()[0]
        path = self.tree.item(selected_item, "text")
        
        if messagebox.askyesno("削除確認", f"{path} を削除しますか？"):
            if os.path.isdir(path):
                shutil.rmtree(path)
            else:
                os.remove(path)
            self.refresh_tree()

    def move_item(self):
        selected_item = self.tree.selection()[0]
        source_path = self.tree.item(selected_item, "text")
        
        target_path = filedialog.askdirectory()
        if target_path:
            shutil.move(source_path, target_path)
            self.refresh_tree()

    def rename_item(self):
        selected_item = self.tree.selection()[0]
        old_path = self.tree.item(selected_item, "text")
        old_name = os.path.basename(old_path)
        
        new_name = simpledialog.askstring("名前変更", "新しい名前を入力してください:", initialvalue=old_name)
        if new_name:
            new_path = os.path.join(os.path.dirname(old_path), new_name)
            os.rename(old_path, new_path)
            self.refresh_tree()

    def search_files(self):
        search_term = self.search_entry.get().lower()
        self.tree.delete(*self.tree.get_children())
        self._search_recursive(self.root_path, search_term)

    def _search_recursive(self, path, search_term, parent=""):
        try:
            for item in os.listdir(path):
                full_path = os.path.join(path, item)
                if search_term in item.lower():
                    node = self.tree.insert(parent, 'end', text=full_path, open=False)
                    if os.path.isdir(full_path):
                        self._search_recursive(full_path, search_term, node)
                elif os.path.isdir(full_path):
                    self._search_recursive(full_path, search_term, parent)
        except PermissionError:
            pass

    def refresh_tree(self):
        self.populate_tree()

if __name__ == "__main__":
    root = Tk()
    app = FileManagerGUI(root)
    root.mainloop()
```

## 主要な機能の説明

### 1. ツリービューの構築

`populate_tree`メソッドは、指定されたディレクトリ（デフォルトではスクリプトの親ディレクトリ）からファイルとフォルダの階層構造を再帰的に構築します。

```python
def populate_tree(self, node=""):
    if node == "":
        self.tree.delete(*self.tree.get_children())
        node = self.tree.insert('', 'end', text=self.root_path, open=True)
        self.populate_tree(node)
    else:
        path = self.tree.item(node, "text")
        try:
            for item in os.listdir(path):
                full_path = os.path.join(path, item)
                try:
                    if os.path.isdir(full_path):
                        child = self.tree.insert(node, 'end', text=full_path, open=False)
                        self.populate_tree(child)
                    else:
                        self.tree.insert(node, 'end', text=full_path, open=False)
                except PermissionError:
                    continue
        except PermissionError:
            pass
```

```mermaid
sequenceDiagram
    participant GUI
    participant FileManagerGUI
    participant TreeView
    participant OS

    GUI->>FileManagerGUI: __init__()
    FileManagerGUI->>FileManagerGUI: populate_tree()
    FileManagerGUI->>TreeView: delete(all children)
    FileManagerGUI->>OS: os.path.dirname(__file__)
    OS->>FileManagerGUI: root_path
    FileManagerGUI->>TreeView: insert(root_path)
    
    rect rgb(200, 220, 240)
    Note over FileManagerGUI,OS: 再帰的なツリー構築プロセス
    loop 各ディレクトリに対して
        FileManagerGUI->>OS: os.listdir(path)
        OS->>FileManagerGUI: ファイル/フォルダリスト
        loop 各アイテムに対して
            FileManagerGUI->>OS: os.path.isdir(item)
            OS->>FileManagerGUI: ディレクトリかどうか
            alt ディレクトリの場合
                FileManagerGUI->>TreeView: insert(item as directory)
                FileManagerGUI->>FileManagerGUI: populate_tree(new node)
            else ファイルの場合
                FileManagerGUI->>TreeView: insert(item as file)
            end
        end
    end
    end

    FileManagerGUI->>GUI: ツリービュー構築完了
```



### 2. 右クリックメニュー

`show_context_menu`メソッドは、ツリービュー上での右クリックイベントを処理し、コンテキストメニューを表示します。

```python
def show_context_menu(self, event):
    item = self.tree.identify_row(event.y)
    if item:
        self.tree.selection_set(item)
        self.context_menu.post(event.x_root, event.y_root)
```

```mermaid
sequenceDiagram
    actor User
    participant GUI
    participant FileManagerGUI
    participant OS

    User->>GUI: 右クリックでコンテキストメニュー表示
    GUI->>FileManagerGUI: show_context_menu()
    FileManagerGUI->>GUI: コンテキストメニュー表示
    User->>GUI: 「名前変更」選択
    GUI->>FileManagerGUI: rename_item()
    FileManagerGUI->>GUI: 新しい名前の入力ダイアログ表示
    User->>GUI: 新しい名前を入力
    GUI->>FileManagerGUI: 新しい名前を渡す
    FileManagerGUI->>OS: os.rename()でファイル名変更
    FileManagerGUI->>FileManagerGUI: refresh_tree()
    FileManagerGUI->>GUI: ツリービュー更新
```

### 3. ファイル操作

各ファイル操作（新規フォルダ作成、ファイル追加、削除、移動、名前変更）は、それぞれのメソッドで実装されています。例えば、`rename_item`メソッドは以下のようになっています：

```python
def rename_item(self):
    selected_item = self.tree.selection()[0]
    old_path = self.tree.item(selected_item, "text")
    old_name = os.path.basename(old_path)
    
    new_name = simpledialog.askstring("名前変更", "新しい名前を入力してください:", initialvalue=old_name)
    if new_name:
        new_path = os.path.join(os.path.dirname(old_path), new_name)
        os.rename(old_path, new_path)
        self.refresh_tree()
```

### 4. 検索機能

`search_files`メソッドと`_search_recursive`メソッドを組み合わせて、ファイル名の部分一致検索を実装しています。

```python
def search_files(self):
    search_term = self.search_entry.get().lower()
    self.tree.delete(*self.tree.get_children())
    self._search_recursive(self.root_path, search_term)

def _search_recursive(self, path, search_term, parent=""):
    try:
        for item in os.listdir(path):
            full_path = os.path.join(path, item)
            if search_term in item.lower():
                node = self.tree.insert(parent, 'end', text=full_path, open=False)
                if os.path.isdir(full_path):
                    self._search_recursive(full_path, search_term, node)
            elif os.path.isdir(full_path):
                self._search_recursive(full_path, search_term, parent)
    except PermissionError:
        pass
```

```mermaid
sequenceDiagram
    actor User
    participant GUI
    participant FileManagerGUI
    participant OS

    User->>GUI: 検索語を入力
    User->>GUI: 検索ボタンをクリック
    GUI->>FileManagerGUI: search_files()
    FileManagerGUI->>FileManagerGUI: _search_recursive()
    loop 各ディレクトリ
        FileManagerGUI->>OS: os.listdir()
        OS->>FileManagerGUI: ファイル/フォルダリスト
        FileManagerGUI->>FileManagerGUI: 検索語とファイル名を比較
    end
    FileManagerGUI->>GUI: 検索結果をツリービューに追加
    GUI->>User: 検索結果を表示
```

## 実装の詳細

### 1. GUIの構築

アプリケーションのGUIは、`tkinter`を使用して構築されています。主要なコンポーネントは以下の通りです：

- `Tk()`: メインウィンドウの作成
- `ttk.Treeview`: ファイルとフォルダの階層構造表示
- `ttk.Scrollbar`: ツリービューのスクロール機能
- `Entry`: 検索バー
- `Button`: 検索ボタン
- `Menu`: 右クリックコンテキストメニュー

### 2. ファイルシステム操作

ファイルシステムの操作には、`os`モジュールと`shutil`モジュールを使用しています：

- `os.path`: パス操作（結合、ディレクトリ名取得など）
- `os.listdir()`: ディレクトリ内のファイルとフォルダのリスト取得
- `os.makedirs()`: ディレクトリ作成

## 実装の詳細（続き）

### 2. ファイルシステム操作（続き）

- `os.rename()`: ファイルまたはフォルダの名前変更
- `shutil.copy()`: ファイルのコピー
- `shutil.move()`: ファイルまたはフォルダの移動
- `shutil.rmtree()`: ディレクトリとその中身の削除

### 3. エラー処理

アプリケーションは、主に`PermissionError`を捕捉し、アクセス権限のないファイルやフォルダを適切にスキップします。また、ユーザーの操作に対するフィードバック（確認ダイアログなど）も提供しています。

```python
try:
    # ファイルシステム操作
except PermissionError:
    # エラーをスキップ
    pass
```

確認ダイアログの例：

```python
if messagebox.askyesno("削除確認", f"{path} を削除しますか？"):
    # 削除処理
```

## まとめ

このプログラムは、Pythonとtkinterを使用して、基本的なファイル管理機能を持つGUIアプリケーションを作成する方法を示しています。要件と仕様に基づいて設計されたこのアプリケーションは、ツリービュー、右クリックメニュー、ファイル操作、検索機能などの実装方法を学ぶ良い例となっています。

このコードをベースに、さらに機能を追加したり、UIをカスタマイズしたりすることで、より高度なファイルマネージャーを作成することができます。例えば、以下のような拡張が考えられます：

- ファイルのプレビュー機能
- ドラッグ＆ドロップによるファイル操作
- ファイルの詳細情報表示
- 複数ファイルの一括操作
- ファイルの種類別フィルタリング機能

Pythonの強力なライブラリと柔軟性を活用することで、様々なGUIアプリケーションを効率的に開発できることがわかります。

## 発展的なトピック

1. **マルチスレッディング**: 大規模なディレクトリ構造を扱う場合、ファイル操作やツリービューの更新を別スレッドで行うことで、UIの応答性を向上させることができます。

2. **ファイルシステムイベントの監視**: `watchdog`ライブラリを使用して、ファイルシステムの変更をリアルタイムで監視し、自動的にツリービューを更新することができます。

3. **ファイルタイプアイコン**: ファイルの種類に応じたアイコンを表示することで、視覚的な情報を強化できます。

4. **設定の保存と読み込み**: ユーザーの設定（ウィンドウサイズ、最後に開いたディレクトリなど）をJSONファイルに保存し、次回起動時に読み込むことで、ユーザーエクスペリエンスを向上させることができます。

## 注意点

- このアプリケーションは、ユーザーのファイルシステムに直接アクセスするため、誤操作によるデータ損失のリスクがあります。重要なファイルを扱う際は十分に注意し、必要に応じてバックアップを取ることをお勧めします。

- 大規模なディレクトリ構造を扱う場合、初期ロードに時間がかかる可能性があります。この問題は、遅延ロードや仮想化されたツリービューの実装により改善できます。

- セキュリティ上の理由から、一部のシステムフォルダやファイルへのアクセスが制限される場合があります。アプリケーションは、これらの制限を適切に処理する必要があります。

## 参考リンク

- [Python公式ドキュメント - tkinter](https://docs.python.org/ja/3/library/tkinter.html)
- [Python公式ドキュメント - os](https://docs.python.org/ja/3/library/os.html)
- [Python公式ドキュメント - shutil](https://docs.python.org/ja/3/library/shutil.html)
- [Tkinter チュートリアル](https://docs.python.org/ja/3/library/tkinter.html#a-simple-hello-world-program)
- [Python GUIプログラミング with Tkinter](https://realpython.com/python-gui-tkinter/)

以上で、PythonとtkinterによるシンプルなファイルマネージャーGUIの作成に関する解説を終わります。このプロジェクトを通じて、GUIアプリケーション開発の基礎と、Pythonを使用したファイルシステム操作の実践的な方法を学ぶことができました。さらなる改善や機能追加に挑戦して、プログラミングスキルを磨いていってください。