0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

vscode風のファイルツリーをpythonで作ってみた

Last updated at Posted at 2025-07-13

Python ファイルエクスプローラー(VSCode風)

このアプリは PyQt5 を用いて構築された、シンプルで直感的な ファイルエクスプローラー を作ってみました。
VSCode のサイドバーのような使いやすさを目指して設計しました。


1. 機能一覧

🔍 基本操作

ディレクトリのツリー表示
ファイルをダブルクリックで開く(OSの標準アプリで)
フォルダをクリックで展開
「⬅ 上に戻る」ボタンで1階層上へ移動
表示中のパスを上部に表示
右クリックでディレクトリ操作が可能

🧷 お気に入り機能

「ファイル > お気に入りに追加」で任意のフォルダを登録可能
メニュー「お気に入り」からワンクリックで開く
「❌ 削除」でお気に入りから除去可能
.favorites.txt に永続保存(アプリ起動時に読み込まれます)

🧰 ツール機能

文字サイズの変更
ウィンドウを常に最前面に固定/解除


2. 🧰 必要なライブラリ

  • Python 3.x
  • PyQt5
pip install PyQt5

実行方法

tool_view.py

3. 対応ファイル

テキスト デフォルトのエディタで開く
画像 (.jpg, .png) ビューワーで開く
動画 (.mp4) メディアプレイヤーで開く
Excel, Word, PDFなど OS既定アプリで開く

4.🔧 カスタマイズ例

.setRootPath() で起動時の初期フォルダ変更

5.その他

ファイルフィルタ追加(特定の拡張子だけ表示)
コンテキストメニュー(右クリックで削除や名前変更など)

6.画像

動作.png

7.ライセンス

ほぼ生成AIによって作成、自由にカスタマイズ・商用利用可能

8.プログラム

tool_view.py
import sys
import os
import shutil
import subprocess
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QMenuBar,
    QAction, QFileDialog, QInputDialog, QPushButton, QHBoxLayout,
    QMessageBox, QTreeView, QFileSystemModel, QMenu
)
from PyQt5.QtCore import Qt, QDir, QPoint

FAVORITES_FILE = ".favorites.txt"

class FileExplorer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Python File")
        self.setGeometry(100, 100, 900, 600)

        self.current_path = QDir.homePath()
        self.favorites = []
        self.always_on_top = False

        # Main widget and layout
        central = QWidget()
        self.setCentralWidget(central)
        self.layout = QVBoxLayout(central)

        # Top bar with back button and path label
        top_bar = QHBoxLayout()
        self.back_button = QPushButton("⬅ 上に戻る")
        self.back_button.setFixedWidth(120)
        self.back_button.clicked.connect(self.go_up)
        self.path_label = QLabel()
        top_bar.addWidget(self.back_button)
        top_bar.addWidget(self.path_label)
        self.layout.addLayout(top_bar)

        # File tree view
        self.model = QFileSystemModel()
        self.model.setRootPath(self.current_path)
        self.tree = QTreeView()
        self.tree.setModel(self.model)
        self.tree.setRootIndex(self.model.index(self.current_path))
        self.tree.setColumnWidth(0, 300)
        self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
        self.tree.customContextMenuRequested.connect(self.show_context_menu)
        self.tree.clicked.connect(self.on_tree_clicked)
        self.tree.doubleClicked.connect(self.on_tree_double_clicked)
        self.layout.addWidget(self.tree)

        self.menu_bar = QMenuBar()
        self.setMenuBar(self.menu_bar)
        self.setup_menus()

        self.ensure_favorites_file()
        self.load_favorites()
        self.update_path_label()

    def setup_menus(self):
        file_menu = self.menu_bar.addMenu("ファイル")

        open_dir = QAction("別のディレクトリを開く", self)
        open_dir.triggered.connect(self.select_directory)
        file_menu.addAction(open_dir)

        add_fav = QAction("お気に入りに追加", self)
        add_fav.triggered.connect(self.add_to_favorites)
        file_menu.addAction(add_fav)

        exit_action = QAction("終了", self)
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)

        self.favorite_menu = self.menu_bar.addMenu("お気に入り")

        tool_menu = self.menu_bar.addMenu("ツール")

        font_size_action = QAction("文字サイズを変更", self)
        font_size_action.triggered.connect(self.change_font_size)
        tool_menu.addAction(font_size_action)

        topmost_action = QAction("常に前面に表示", self, checkable=True)
        topmost_action.triggered.connect(self.toggle_always_on_top)
        tool_menu.addAction(topmost_action)

    def update_path_label(self):
        self.path_label.setText(f"📁 現在のパス: {self.current_path}")

    def go_up(self):
        parent = os.path.dirname(self.current_path)
        if os.path.exists(parent):
            self.current_path = parent
            self.tree.setRootIndex(self.model.index(self.current_path))
            self.update_path_label()

    def on_tree_clicked(self, index):
        self.tree.expand(index)

    def on_tree_double_clicked(self, index):
        path = self.model.filePath(index)
        if os.path.isdir(path):
            self.current_path = path
            self.tree.setRootIndex(self.model.index(path))
            self.update_path_label()
        else:
            self.open_with_default_app(path)

    def open_with_default_app(self, path):
        try:
            if sys.platform == 'win32':
                os.startfile(path)
            elif sys.platform == 'darwin':
                subprocess.Popen(['open', path])
            else:
                subprocess.Popen(['xdg-open', path])
        except Exception as e:
            QMessageBox.warning(self, "エラー", f"ファイルを開けませんでした:\n{e}")

    def select_directory(self):
        dir_path = QFileDialog.getExistingDirectory(self, "ディレクトリを選択", self.current_path)
        if dir_path:
            self.current_path = dir_path
            self.tree.setRootIndex(self.model.index(self.current_path))
            self.update_path_label()

    def change_font_size(self):
        size, ok = QInputDialog.getInt(self, "フォントサイズ変更", "新しいフォントサイズ:", 10, 6, 40)
        if ok:
            font = self.tree.font()
            font.setPointSize(size)
            self.tree.setFont(font)
            self.path_label.setFont(font)

    def toggle_always_on_top(self, checked):
        self.always_on_top = checked
        flags = self.windowFlags()
        if self.always_on_top:
            self.setWindowFlags(flags | Qt.WindowStaysOnTopHint)
        else:
            self.setWindowFlags(flags & ~Qt.WindowStaysOnTopHint)
        self.show()

    def ensure_favorites_file(self):
        if not os.path.exists(FAVORITES_FILE):
            with open(FAVORITES_FILE, 'w', encoding='utf-8') as f:
                pass

    def add_to_favorites(self):
        folder = QFileDialog.getExistingDirectory(self, "お気に入りに追加するフォルダを選択", self.current_path)
        if folder and folder not in self.favorites:
            self.favorites.append(folder)
            self.save_favorites()
            self.refresh_favorite_menu()

    def load_favorites(self):
        self.favorites.clear()
        try:
            with open(FAVORITES_FILE, 'r', encoding='utf-8') as f:
                for line in f:
                    folder = line.strip()
                    if os.path.isdir(folder):
                        self.favorites.append(folder)
        except Exception:
            pass
        self.refresh_favorite_menu()

    def save_favorites(self):
        with open(FAVORITES_FILE, 'w', encoding='utf-8') as f:
            for path in self.favorites:
                f.write(path + '\n')

    def refresh_favorite_menu(self):
        self.favorite_menu.clear()
        for folder in self.favorites:
            open_action = QAction(folder, self)
            open_action.triggered.connect(lambda checked=False, path=folder: self.open_favorite(path))
            self.favorite_menu.addAction(open_action)

            del_action = QAction(f"{folder} を削除", self)
            del_action.triggered.connect(lambda checked=False, path=folder: self.remove_favorite(path))
            self.favorite_menu.addAction(del_action)

    def show_context_menu(self, pos: QPoint):
        index = self.tree.indexAt(pos)
        if not index.isValid():
            return

        file_path = self.model.filePath(index)

        menu = QMenu()
        copy_action = menu.addAction("📋 コピー")
        cut_action = menu.addAction("✂️ 切り取り")
        paste_action = menu.addAction("📄 貼り付け")
        delete_action = menu.addAction("🗑 削除")
        rename_action = menu.addAction("📝 名前の変更")
        new_folder_action = menu.addAction("📁 新しいフォルダを作成")

        action = menu.exec_(self.tree.viewport().mapToGlobal(pos))

        if action == copy_action:
            self.clipboard_path = file_path
            self.clipboard_cut = False
        elif action == cut_action:
            self.clipboard_path = file_path
            self.clipboard_cut = True
        elif action == paste_action:
            if self.clipboard_path:
                dst = os.path.join(file_path, os.path.basename(self.clipboard_path)) if os.path.isdir(file_path) else os.path.join(os.path.dirname(file_path), os.path.basename(self.clipboard_path))
                try:
                    if self.clipboard_cut:
                        shutil.move(self.clipboard_path, dst)
                    else:
                        if os.path.isdir(self.clipboard_path):
                            shutil.copytree(self.clipboard_path, dst)
                        else:
                            shutil.copy2(self.clipboard_path, dst)
                except Exception as e:
                    QMessageBox.warning(self, "貼り付けエラー", str(e))
        elif action == delete_action:
            reply = QMessageBox.question(self, "確認", f"{file_path} を削除しますか?", QMessageBox.Yes | QMessageBox.No)
            if reply == QMessageBox.Yes:
                try:
                    if os.path.isdir(file_path):
                        shutil.rmtree(file_path)
                    else:
                        os.remove(file_path)
                except Exception as e:
                    QMessageBox.warning(self, "削除エラー", str(e))
        elif action == rename_action:
            new_name, ok = QInputDialog.getText(self, "名前の変更", "新しい名前:", text=os.path.basename(file_path))
            if ok:
                new_path = os.path.join(os.path.dirname(file_path), new_name)
                try:
                    os.rename(file_path, new_path)
                except Exception as e:
                    QMessageBox.warning(self, "名前変更エラー", str(e))
        elif action == new_folder_action:
            new_name, ok = QInputDialog.getText(self, "新しいフォルダ", "フォルダ名:")
            if ok:
                try:
                    os.mkdir(os.path.join(file_path if os.path.isdir(file_path) else os.path.dirname(file_path), new_name))
                except Exception as e:
                    QMessageBox.warning(self, "作成エラー", str(e))

    def open_favorite(self, path):
        if os.path.isdir(path):
            self.current_path = path
            self.tree.setRootIndex(self.model.index(path))
            self.update_path_label()

    def remove_favorite(self, path):
        confirm = QMessageBox.question(self, "削除確認", f"{path} をお気に入りから削除しますか?",
                                       QMessageBox.Yes | QMessageBox.No)
        if confirm == QMessageBox.Yes:
            if path in self.favorites:
                self.favorites.remove(path)
                self.save_favorites()
                self.refresh_favorite_menu()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = FileExplorer()
    window.show()
    sys.exit(app.exec_())

最後に

少しでもツリーが見やすくなればと思ってます。
なお、このプログラムによってPCが壊れても補償しかねます。ご了承ください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?