前回の記事からの続きです。未完成ですので注意!
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_())