0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

pythonでvscode風のツリービュー part2 エクセルタブ編(未完成)

Last updated at Posted at 2025-07-14

前回の記事からの続きです。未完成ですので注意!

A1.追加内容(未完成な部分もあるので参考までに)

✅ Excel連携機能(Windows限定)
ツール > 「Excelを読み込む」チェックボックス追加
チェック時のみ win32com.client で 起動中のExcelアプリケーションからブックとシートを取得
タブ操作が可能に

✅ エラー対策
Excelが起動していない/COMエラーのときは QMessageBox で通知(クラッシュなし)

A2.実行方法

tool_view_excel.py

A3.プログラム

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

FAVORITES_FILE = ".favorites.txt"

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

        self.current_path = QDir.homePath()
        self.favorites = []
        self.always_on_top = False
        self.excel_enabled = False
        self.excel_tabs_visible = False
        self.clipboard_path = None
        self.clipboard_cut = False
        self.excel_app = None
        self.current_workbook = None

        central = QWidget()
        self.setCentralWidget(central)
        self.layout = QVBoxLayout(central)

        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)

        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)

        excel_toggle = QAction("Excelを読み込む", self, checkable=True)
        excel_toggle.triggered.connect(self.toggle_excel)
        tool_menu.addAction(excel_toggle)

    def update_path_label(self):
        max_width = self.width() - 150  # ボタン等を考慮した余白
        metrics = self.path_label.fontMetrics()
        elided = metrics.elidedText(f"📁 現在のパス: {self.current_path}", Qt.ElideLeft, max_width)
        self.path_label.setText(elided)

    def go_up(self):
        if self.excel_tabs_visible:
            self.populate_tree()
            self.excel_tabs_visible = False
        else:
            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:
            ext = os.path.splitext(path)[1].lower()
            if self.excel_enabled and ext in [".xls", ".xlsx"]:
                self.handle_excel_file(path)
            else:
                self.open_with_default_app(path)

    def handle_excel_file(self, path):
        try:
            pythoncom.CoInitialize()
            self.excel_app = win32com.client.Dispatch("Excel.Application")
            wb = None
            for book in self.excel_app.Workbooks:
                if os.path.abspath(book.FullName) == os.path.abspath(path):
                    wb = book
                    break
            if wb is None:
                wb = self.excel_app.Workbooks.Open(path)
                self.excel_app.Visible = True
            self.current_workbook = wb
            self.show_excel_tabs(wb)
        except Exception as e:
            QMessageBox.warning(self, "Excelエラー", f"Excelファイルの処理中にエラーが発生しました:\n{e}")

    def show_excel_tabs(self, workbook):
        model = QStandardItemModel()
        model.setHorizontalHeaderLabels(["Excelシート"])
        for sheet in workbook.Sheets:
            item = QStandardItem(sheet.Name)
            item.setData(sheet.Name, Qt.UserRole)
            model.appendRow(item)
        self.tree.setModel(model)
        self.tree.doubleClicked.connect(self.activate_excel_sheet)
        self.excel_tabs_visible = True

    def activate_excel_sheet(self, index):
        if not self.current_workbook:
            return
        sheet_name = index.data()
        try:
            self.current_workbook.Sheets(sheet_name).Activate()
        except Exception as e:
            QMessageBox.warning(self, "シート切り替えエラー", str(e))

    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 toggle_excel(self, checked):
        self.excel_enabled = checked

    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 open_favorite(self, path):
        if os.path.isdir(path):
            self.current_path = path
            self.tree.setModel(self.model)
            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()

    def populate_tree(self):
        self.tree.setModel(self.model)
        self.tree.setRootIndex(self.model.index(self.current_path))
        self.update_path_label()
        self.tree.doubleClicked.connect(self.on_tree_double_clicked)

    def show_context_menu(self, position):
        index = self.tree.indexAt(position)
        if not index.isValid():
            return
        path = self.model.filePath(index)
        menu = QMenu()
        open_action = QAction("開く", self)
        open_action.triggered.connect(lambda: self.open_with_default_app(path))
        menu.addAction(open_action)

        if os.path.isfile(path):
            delete_action = QAction("削除", self)
            delete_action.triggered.connect(lambda: self.delete_file(path))
            menu.addAction(delete_action)

            rename_action = QAction("名前の変更", self)
            rename_action.triggered.connect(lambda: self.rename_file(path))
            menu.addAction(rename_action)

        menu.exec_(self.tree.viewport().mapToGlobal(position))

    def delete_file(self, path):
        try:
            os.remove(path)
            QMessageBox.information(self, "削除完了", f"{os.path.basename(path)} を削除しました")
            self.model.refresh()
        except Exception as e:
            QMessageBox.warning(self, "削除失敗", str(e))

    def rename_file(self, path):
        new_name, ok = QInputDialog.getText(self, "名前の変更", "新しい名前:", text=os.path.basename(path))
        if ok and new_name:
            new_path = os.path.join(os.path.dirname(path), new_name)
            try:
                os.rename(path, new_path)
                QMessageBox.information(self, "名前変更", f"{path}{new_path}")
                self.model.refresh()
            except Exception as e:
                QMessageBox.warning(self, "名前変更失敗", str(e))



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

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?