[Python] - import os
- import sys
- import json
- import datetime
- from PyQt5.QtWidgets import (
- QApplication, QMainWindow, QPushButton, QFileDialog, QLabel,
- QLineEdit, QComboBox, QCheckBox, QSpinBox, QTableView,
- QMessageBox, QTreeView, QWidget, QHBoxLayout, QVBoxLayout, QGroupBox,
- QProgressDialog, QFileSystemModel, QGridLayout, QFileIconProvider,
- QStyledItemDelegate, QAbstractItemView
- )
- from PyQt5.QtCore import Qt, QDir, QModelIndex, QThread, pyqtSignal, QTimer, QUrl
- from PyQt5.QtGui import QStandardItemModel, QStandardItem, QGuiApplication, QIcon
- class RenameWorker(QThread):
- """文件重命名的后台工作线程"""
- progress_updated = pyqtSignal(int)
- rename_finished = pyqtSignal(bool)
- error_occurred = pyqtSignal(str)
- canceled = pyqtSignal()
- def __init__(self, file_list, operations):
- super().__init__()
- self.file_list = file_list
- self.operations = operations
- self.backup_list = []
- def run(self):
- try:
- count = len(self.file_list)
- for i, file_info in enumerate(self.file_list, start=1):
- if self.isInterruptionRequested():
- self.canceled.emit()
- return
- old_path = file_info['path']
- new_name = RenamerGUI.generate_preview_name(file_info, self.operations)
- new_path = os.path.join(file_info['dir'], new_name)
- if old_path == new_path:
- continue
- if os.path.exists(new_path):
- raise FileExistsError(f"文件已存在:{new_name}")
- os.rename(old_path, new_path)
- self.backup_list.append((old_path, new_path))
- self.progress_updated.emit(int(i / count * 100))
- self.rename_finished.emit(True)
- except Exception as e:
- self.error_occurred.emit(str(e))
- class CustomLabel(QLabel):
- """自定义标签,用于显示完整路径(鼠标悬停时)"""
- def __init__(self, parent=None):
- super().__init__(parent)
- self.full_path = ""
- def setFullPath(self, path):
- self.full_path = path
- def enterEvent(self, event):
- if self.full_path:
- self.setText(self.full_path)
- def leaveEvent(self, event):
- short_path = os.path.basename(self.full_path) if self.full_path else ""
- self.setText(f"当前目录: {short_path}")
- class MyItemDelegate(QStyledItemDelegate):
- """自定义表格项委托,用于显示文件和文件夹图标"""
- def __init__(self, folder_icon, file_icon, parent=None):
- super().__init__(parent)
- self.folder_icon = folder_icon
- self.file_icon = file_icon
- def paint(self, painter, option, index):
- if index.isValid():
- if index.model().fileInfo(index).isDir():
- icon = self.folder_icon
- else:
- icon = self.file_icon
- pixmap = icon.pixmap(option.rect.size())
- painter.drawPixmap(option.rect, pixmap)
- text_rect = option.rect.adjusted(icon.actualSize(option.rect.size()).width(), 0, 0, 0)
- painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, index.model().data(index, Qt.DisplayRole))
- else:
- super().paint(painter, option, index)
- class RenamerGUI(QMainWindow):
- """文件批量重命名工具的主窗口类"""
- def __init__(self):
- super().__init__()
- self.current_dir = QDir.homePath()
- self.file_list = []
- self.backup_info = []
- self.backup_file = "rename_backup.json"
- self.setAcceptDrops(True)
- self.setWindowTitle("文件批量重命名工具 www.52pojie.cn")
- self.resize(800, 650)
- self.init_ui()
- self.load_backup_from_file()
- self.preview_timer = None
- @staticmethod
- def generate_preview_name(file_info, operations):
- """根据操作参数生成预览文件名"""
- name = file_info['name']
-
- # 处理文字替换
- if operations.get('replace_enabled', False):
- old_text = operations.get('replace_old', '')
- new_text = operations.get('replace_new', '')
- if old_text:
- name = name.replace(old_text, new_text)
-
- # 处理文字删除
- if operations.get('delete_enabled', False):
- delete_text = operations.get('delete_text', '')
- if delete_text:
- name = name.replace(delete_text, '')
-
- parts = []
-
- # 处理父目录名(可选择前置或后置)
- parent_dirs = RenamerGUI.get_parent_dirs(file_info['dir'], operations.get('parent_level', 0))
- if operations.get('use_parent', False):
- if operations.get('parent_pos') == "前置":
- parts.extend(parent_dirs)
-
- # 添加前缀
- if operations.get('prefix'):
- parts.append(operations['prefix'])
-
- # 添加序列号(可选择前置或后置)
- if operations.get('sequence', False):
- seq_text = f"{file_info['seq']:0{operations.get('seq_digits', 2)}d}"
- if operations.get('seq_pos') == "前置":
- parts.append(seq_text)
-
- # 添加处理后的文件名(根据保留原文件名选项)
- if operations.get('keep_original', False):
- parts.append(name)
-
- # 添加序列号(后置情况)
- if operations.get('sequence', False) and operations.get('seq_pos') == "后置":
- parts.append(seq_text)
-
- # 添加后缀
- if operations.get('suffix'):
- parts.append(operations['suffix'])
-
- # 添加日期
- if operations.get('date_enabled', False) and operations.get('date_format'):
- try:
- date_str = datetime.datetime.now().strftime(operations['date_format'])
- except ValueError:
- date_str = "格式错误"
- parts.append(date_str)
-
- # 处理父目录名后置的情况
- if operations.get('use_parent', False) and operations.get('parent_pos') == "后置":
- parts.extend(parent_dirs)
-
- # 处理扩展名
- ext = operations.get('new_ext', file_info['ext'])
- if operations.get('change_ext', False) and ext:
- if not ext.startswith('.'):
- ext = f".{ext}"
- else:
- ext = file_info['ext']
-
- base = '_'.join(filter(None, parts))
-
- # 组合最终文件名 - 确保总有一个名称部分
- if base:
- return f"{base}{ext}"
- elif name:
- return f"{name}{ext}"
- else:
- return f"{file_info['name']}{ext}"
- @staticmethod
- def get_parent_dirs(path, levels):
- """获取指定层级的父目录名列表"""
- parents = []
- for _ in range(levels):
- path, dirname = os.path.split(path)
- if dirname:
- parents.insert(0, dirname)
- else:
- break
- return parents[:levels]
- def init_ui(self):
- """初始化用户界面"""
- main_widget = QWidget()
- self.setCentralWidget(main_widget)
- layout = QVBoxLayout(main_widget)
- # 目录选择区域
- dir_layout = QHBoxLayout()
- self.dir_label = CustomLabel()
- self.dir_label.setFullPath(self.current_dir)
- self.path_input = QLineEdit(self.current_dir)
- self.path_input.returnPressed.connect(self.go_to_path)
- go_btn = QPushButton("前往")
- go_btn.clicked.connect(self.go_to_path)
- select_btn = QPushButton("选择目录")
- select_btn.clicked.connect(self.select_directory)
- dir_layout.addWidget(self.dir_label, 1)
- dir_layout.addWidget(self.path_input, 3)
- dir_layout.addWidget(go_btn)
- dir_layout.addWidget(select_btn)
- # 文件树视图
- self.file_system_model = QFileSystemModel()
- self.file_tree = QTreeView()
- self.file_tree.setModel(self.file_system_model)
- self.file_tree.clicked.connect(self.on_dir_selected)
- self.file_system_model.setRootPath(QDir.rootPath())
- icon_provider = QFileIconProvider()
- folder_icon = icon_provider.icon(QFileIconProvider.Folder)
- file_icon = icon_provider.icon(QFileIconProvider.File)
- self.file_tree.setItemDelegate(MyItemDelegate(folder_icon, file_icon))
- # 重命名规则设置区域
- settings_group = QGroupBox("重命名规则")
- settings_layout = QGridLayout(settings_group)
- settings_layout.setVerticalSpacing(5)
- settings_layout.setHorizontalSpacing(5)
- # 第三行设置:父目录、前缀、后缀
- row1 = QHBoxLayout()
- self.parent_cb = QCheckBox("使用父目录名")
- self.parent_level = QSpinBox()
- self.parent_level.setRange(1, 5)
- self.parent_level.setFixedWidth(40)
- self.parent_pos = QComboBox() # 父目录位置选择
- self.parent_pos.addItems(["前置", "后置"])
-
- row1.addWidget(self.parent_cb)
- row1.addWidget(QLabel("层级"))
- row1.addWidget(self.parent_level)
- row1.addWidget(QLabel("位置"))
- row1.addWidget(self.parent_pos)
- row1.addWidget(QLabel("前缀"))
- self.prefix_input = QLineEdit()
- row1.addWidget(self.prefix_input)
- row1.addWidget(QLabel("后缀"))
- self.suffix_input = QLineEdit()
- row1.addWidget(self.suffix_input)
- settings_layout.addLayout(row1, 2, 0, 1, 7)
- # 第二行设置:保留原文件名、序列号
- row2 = QHBoxLayout()
- self.keep_original = QCheckBox("保留原文件名")
- self.sequence_cb = QCheckBox("序列号")
- self.seq_start = QSpinBox()
- self.seq_start.setRange(1, 9999)
- self.seq_start.setFixedWidth(40)
- self.seq_digits = QSpinBox()
- self.seq_digits.setRange(2, 6)
- self.seq_digits.setFixedWidth(40)
- self.seq_pos = QComboBox() # 序列号位置选择
- self.seq_pos.addItems(["前置", "后置"])
- self.separate_seq = QCheckBox("每文件夹独立编号")
-
- row2.addWidget(self.keep_original)
- row2.addWidget(self.sequence_cb)
- row2.addWidget(QLabel("起始"))
- row2.addWidget(self.seq_start)
- row2.addWidget(QLabel("位数"))
- row2.addWidget(self.seq_digits)
- row2.addWidget(QLabel("位置"))
- row2.addWidget(self.seq_pos)
- row2.addWidget(self.separate_seq)
- settings_layout.addLayout(row2, 1, 0, 1, 4)
- # 第一行设置:文件类型过滤、包含子目录、添加日期格式、修改扩展名
- row3 = QHBoxLayout()
-
- # 文件类型过滤
- self.file_type = QComboBox()
- self.file_type.addItems(["所有文件", "图片", "文档", "视频", "音频", "自定义类型"])
- self.file_type.setFixedWidth(120)
- row3.addWidget(self.file_type)
-
- # 自定义扩展名输入
- self.custom_ext = QLineEdit()
- self.custom_ext.setPlaceholderText("如 jpg png")
- self.custom_ext.setFixedWidth(100)
- row3.addWidget(self.custom_ext)
-
- # 包含子目录
- self.recursive = QCheckBox("包含子目录")
- row3.addWidget(self.recursive)
-
- # 添加日期功能
- self.date_cb = QCheckBox("添加日期")
- row3.addWidget(self.date_cb)
-
- # 日期格式
- self.date_format = QComboBox()
- self.date_format.setFixedWidth(120)
- self.date_format.addItems([
- "yyyy-mm-dd", "yyyy/mm/dd", "yyyymmdd",
- "yy-mm-dd", "yy/mm/dd", "yyyymm",
- "yyyy年mm月dd日", "自定义..."
- ])
- self.date_format.setCurrentIndex(2)
- self.date_format.currentIndexChanged.connect(self.on_date_format_changed)
- row3.addWidget(self.date_format)
-
- # 自定义日期格式输入框
- self.custom_date_format = QLineEdit("%Y%m%d")
- self.custom_date_format.setFixedWidth(120)
- self.custom_date_format.setVisible(False)
- row3.addWidget(self.custom_date_format)
-
- # 修改扩展名
- self.change_ext_cb = QCheckBox("修改扩展名")
- row3.addWidget(self.change_ext_cb)
-
- # 新扩展名输入框
- self.new_ext_input = QLineEdit()
- self.new_ext_input.setPlaceholderText("如 .png")
- self.new_ext_input.setFixedWidth(80)
- self.new_ext_input.setEnabled(False)
- self.change_ext_cb.toggled.connect(lambda: self.new_ext_input.setEnabled(self.change_ext_cb.isChecked()))
- row3.addWidget(self.new_ext_input)
-
- settings_layout.addLayout(row3, 0, 0, 1, 5)
- # 第四行设置:替换和删除文字
- row4 = QHBoxLayout()
- self.replace_enabled = QCheckBox("替换原文件名文字")
- self.replace_old = QLineEdit()
- self.replace_old.setPlaceholderText("原文件名中要替换的文字")
- self.replace_new = QLineEdit()
- self.replace_new.setPlaceholderText("替换为")
-
- self.delete_enabled = QCheckBox("删除原文件名文字")
- self.delete_text = QLineEdit()
- self.delete_text.setPlaceholderText("原文件名中要删除的文字")
-
- row4.addWidget(self.replace_enabled)
- row4.addWidget(self.replace_old)
- row4.addWidget(QLabel("→"))
- row4.addWidget(self.replace_new)
- row4.addWidget(self.delete_enabled)
- row4.addWidget(self.delete_text)
- settings_layout.addLayout(row4, 3, 0, 1, 7)
- # 按钮区域
- btn_layout = QHBoxLayout()
- self.rename_btn = QPushButton("执行重命名")
- self.rename_btn.clicked.connect(self.start_rename)
- self.undo_btn = QPushButton("撤销")
- self.undo_btn.clicked.connect(self.undo_rename)
- self.delete_btn = QPushButton("删除选中")
- self.delete_btn.clicked.connect(self.delete_selected)
- btn_layout.addWidget(self.rename_btn)
- btn_layout.addWidget(self.undo_btn)
- btn_layout.addWidget(self.delete_btn)
- # 预览表格
- self.preview_model = QStandardItemModel()
- self.preview_table = QTableView()
- self.preview_table.setModel(self.preview_model)
- self.preview_table.setSelectionBehavior(QAbstractItemView.SelectRows)
- self.preview_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
- self.preview_model.setHorizontalHeaderLabels(["原始名称", "新名称", "状态", "文件路径"])
- # 组装界面
- layout.addLayout(dir_layout)
- layout.addWidget(self.file_tree, 1)
- layout.addWidget(settings_group, 0)
- layout.addLayout(btn_layout)
- layout.addWidget(self.preview_table, 2)
- # 连接信号和槽
- self.parent_cb.stateChanged.connect(self.auto_update_preview)
- self.parent_level.valueChanged.connect(self.auto_update_preview)
- self.parent_pos.currentIndexChanged.connect(self.auto_update_preview)
- self.prefix_input.textChanged.connect(self.auto_update_preview)
- self.suffix_input.textChanged.connect(self.auto_update_preview)
- self.keep_original.toggled.connect(self.auto_update_preview)
- self.sequence_cb.toggled.connect(self.auto_update_preview)
- self.seq_start.valueChanged.connect(self.auto_update_preview)
- self.seq_digits.valueChanged.connect(self.auto_update_preview)
- self.seq_pos.currentIndexChanged.connect(self.auto_update_preview)
- self.separate_seq.toggled.connect(self.auto_update_preview)
- self.date_cb.toggled.connect(self.auto_update_preview)
- self.date_format.currentIndexChanged.connect(self.auto_update_preview)
- self.custom_date_format.textChanged.connect(self.auto_update_preview)
- self.file_type.currentIndexChanged.connect(self.auto_update_preview)
- self.custom_ext.textChanged.connect(self.auto_update_preview)
- self.recursive.toggled.connect(self.auto_update_preview)
- self.change_ext_cb.toggled.connect(self.auto_update_preview)
- self.new_ext_input.textChanged.connect(self.auto_update_preview)
- self.replace_enabled.toggled.connect(self.auto_update_preview)
- self.replace_old.textChanged.connect(self.auto_update_preview)
- self.replace_new.textChanged.connect(self.auto_update_preview)
- self.delete_enabled.toggled.connect(self.auto_update_preview)
- self.delete_text.textChanged.connect(self.auto_update_preview)
- self.status_bar = self.statusBar()
- self.status_bar.showMessage("就绪")
- def go_to_path(self):
- """跳转到用户输入的路径"""
- input_path = self.path_input.text()
- if os.path.isdir(input_path):
- self.current_dir = input_path
- self.dir_label.setFullPath(self.current_dir)
- self.file_tree.setRootIndex(self.file_system_model.index(self.current_dir))
- self.auto_update_preview()
- else:
- QMessageBox.warning(self, "路径错误", "输入的路径无效,请检查后重试。")
- def validate_name(self, name, dir_path):
- """验证文件名是否有效"""
- if not name:
- return False
- invalid_chars = ['\\', '/', ':', '*', '?', '"', '<', '>', '|']
- if any(c in name for c in invalid_chars):
- return False
- return not os.path.exists(os.path.join(dir_path, name))
- def auto_update_preview(self, *args):
- """自动更新预览(延迟执行,避免频繁操作)"""
- if self.preview_timer:
- self.preview_timer.stop()
- self.preview_timer = QTimer(self)
- self.preview_timer.setSingleShot(True)
- self.preview_timer.timeout.connect(self.refresh_preview)
- self.preview_timer.start(300)
- def refresh_preview(self):
- """刷新预览表格"""
- self.scan_files()
- self.preview_model.setRowCount(0)
- operations = self.get_operations()
- seq = self.seq_start.value()
- dir_groups = {}
- for file_info in self.file_list:
- dir_path = file_info['dir']
- if dir_path not in dir_groups:
- dir_groups[dir_path] = {'seq': seq, 'files': []}
- dir_groups[dir_path]['files'].append(file_info)
- for dir_path, group in dir_groups.items():
- for file_info in group['files']:
- file_info['seq'] = group['seq']
- group['seq'] += 1
- if not self.separate_seq.isChecked():
- seq = group['seq']
- for file_info in self.file_list:
- try:
- new_name = self.generate_preview_name(file_info, operations)
- status = self.validate_name(new_name, file_info['dir'])
- item_old = QStandardItem(file_info['name'] + file_info['ext'])
- item_new = QStandardItem(new_name)
- item_status = QStandardItem("有效" if status else "冲突")
- item_status.setForeground(Qt.red if not status else Qt.black)
- item_path = QStandardItem(file_info['path'])
- item_path.setToolTip(file_info['path'])
- self.preview_model.appendRow([item_old, item_new, item_status, item_path])
- except Exception as e:
- QMessageBox.warning(self, "预览错误", str(e))
- self.preview_table.resizeColumnsToContents()
- self.status_bar.showMessage(f"预览:{len(self.file_list)} 个文件")
- def scan_files(self):
- """扫描目录中的文件"""
- self.file_list = []
- ext_list = self.get_filter_extensions()
- start_seq = self.seq_start.value()
- if self.current_dir:
- try:
- if self.recursive.isChecked():
- # 递归模式下,按文件夹分组处理
- seq = start_seq
- for root, dirs, files in os.walk(self.current_dir):
- valid_files = []
- for filename in files:
- if self.is_valid_file(filename, ext_list):
- valid_files.append(filename)
-
- if valid_files:
- # 处理当前目录的文件
- dir_seq = seq
- for filename in valid_files:
- base, ext = os.path.splitext(filename)
- self.file_list.append({
- 'dir': root,
- 'name': base,
- 'ext': ext,
- 'path': os.path.join(root, filename),
- 'seq': dir_seq
- })
- dir_seq += 1
-
- # 如果启用独立编号,则下一个目录从起始编号开始
- if self.separate_seq.isChecked():
- seq = start_seq
- else:
- seq = dir_seq
- else:
- # 非递归模式
- files = [f for f in os.listdir(self.current_dir) if os.path.isfile(os.path.join(self.current_dir, f))]
- seq = start_seq
- for filename in files:
- if self.is_valid_file(filename, ext_list):
- base, ext = os.path.splitext(filename)
- self.file_list.append({
- 'dir': self.current_dir,
- 'name': base,
- 'ext': ext,
- 'path': os.path.join(self.current_dir, filename),
- 'seq': seq
- })
- seq += 1
- except Exception as e:
- QMessageBox.critical(self, "扫描错误", f"文件扫描失败:{str(e)}")
- def is_valid_file(self, filename, ext_list):
- """检查文件是否符合扩展名过滤条件"""
- if not ext_list:
- return True
- ext = os.path.splitext(filename)[1].lower()
- return ext in ext_list
- def get_filter_extensions(self):
- """获取过滤扩展名列表"""
- type_map = {
- "图片": [".jpg", ".jpeg", ".png", ".gif", ".bmp"],
- "文档": [".doc", ".docx", ".pdf", ".txt", ".xls"],
- "视频": [".mp4", ".avi", ".mkv", ".mov"],
- "音频": [".mp3", ".wav", ".flac", ".ogg"],
- "自定义类型": self.parse_custom_ext()
- }
- key = self.file_type.currentText()
- return type_map.get(key, []) if key != "所有文件" else []
- def parse_custom_ext(self):
- """解析自定义扩展名输入"""
- raw = self.custom_ext.text().strip().lower()
- exts = []
- for ext in raw.split():
- ext = ext.lstrip('.')
- if ext:
- exts.append(f".{ext}")
- return exts or []
- def on_date_format_changed(self, index):
- """日期格式选择变化时的处理"""
- if index == len(self.date_format) - 1:
- self.custom_date_format.setVisible(True)
- else:
- self.custom_date_format.setVisible(False)
- self.auto_update_preview()
- def get_operations(self):
- """获取当前重命名操作参数"""
- date_enabled = self.date_cb.isChecked()
- if date_enabled:
- if self.date_format.currentIndex() == self.date_format.count() - 1:
- date_format = self.custom_date_format.text()
- else:
- date_format = self.get_date_format_string(self.date_format.currentIndex())
- else:
- date_format = None
- replace_enabled = self.replace_enabled.isChecked()
- replace_old = self.replace_old.text().strip()
- replace_new = self.replace_new.text().strip()
- delete_enabled = self.delete_enabled.isChecked()
- delete_text = self.delete_text.text().strip()
- return {
- "use_parent": self.parent_cb.isChecked(),
- "parent_level": self.parent_level.value(),
- "parent_pos": self.parent_pos.currentText(),
- "prefix": self.prefix_input.text(),
- "suffix": self.suffix_input.text(),
- "keep_original": self.keep_original.isChecked(),
- "sequence": self.sequence_cb.isChecked(),
- "seq_digits": self.seq_digits.value(),
- "seq_pos": self.seq_pos.currentText(),
- "date_enabled": date_enabled,
- "date_format": date_format,
- "change_ext": self.change_ext_cb.isChecked(),
- "new_ext": self.new_ext_input.text().strip(),
- "replace_enabled": replace_enabled,
- "replace_old": replace_old,
- "replace_new": replace_new,
- "delete_enabled": delete_enabled,
- "delete_text": delete_text,
- }
- def get_date_format_string(self, index):
- """获取日期格式字符串"""
- formats = [
- "%Y-%m-%d", "%Y/%m/%d", "%Y%m%d",
- "%y-%m-%d", "%y/%m/%d", "%Y%m",
- "%Y年%m月%d日"
- ]
- return formats[index]
- def select_directory(self):
- """选择目录对话框"""
- dir_path = QFileDialog.getExistingDirectory(
- self, "选择目录", self.current_dir, QFileDialog.ShowDirsOnly
- )
- if dir_path:
- self.current_dir = dir_path
- self.dir_label.setFullPath(self.current_dir)
- self.file_tree.setRootIndex(self.file_system_model.index(dir_path))
- self.auto_update_preview()
- def on_dir_selected(self, index):
- """文件树中选择目录时的处理"""
- path = self.file_system_model.filePath(index)
- if os.path.isdir(path):
- self.current_dir = path
- self.dir_label.setFullPath(self.current_dir)
- self.auto_update_preview()
- def start_rename(self):
- """开始重命名操作"""
- if not self.file_list:
- QMessageBox.warning(self, "警告", "没有文件可重命名")
- return
-
- confirm = QMessageBox.question(
- self, "确认", f"确定要重命名 {len(self.file_list)} 个文件吗?",
- QMessageBox.Yes | QMessageBox.No
- )
- if confirm != QMessageBox.Yes:
- return
-
- self.progress_dialog = QProgressDialog("正在重命名...", "取消", 0, 100, self)
- self.progress_dialog.setWindowTitle("请稍候")
- self.progress_dialog.setWindowModality(Qt.WindowModal)
-
- self.worker = RenameWorker(self.file_list.copy(), self.get_operations())
- self.worker.progress_updated.connect(self.progress_dialog.setValue)
- self.worker.rename_finished.connect(self.on_rename_finished)
- self.worker.error_occurred.connect(self.show_error)
- self.worker.canceled.connect(self.progress_dialog.close)
- self.progress_dialog.canceled.connect(self.worker.requestInterruption)
- self.worker.start()
- def on_rename_finished(self, success):
- """重命名完成后的处理"""
- self.progress_dialog.close()
- if success:
- self.backup_info = self.worker.backup_list
- self.save_backup_to_file()
- QMessageBox.information(self, "成功", "文件重命名完成")
- self.auto_update_preview()
- def show_error(self, message):
- """显示错误消息"""
- self.progress_dialog.close()
- QMessageBox.critical(self, "错误", message)
- def undo_rename(self):
- """撤销上次重命名操作"""
- if not self.backup_info:
- QMessageBox.information(self, "提示", "没有可撤销的操作")
- return
-
- confirm = QMessageBox.question(
- self, "确认", "确定要撤销上次重命名操作吗?",
- QMessageBox.Yes | QMessageBox.No
- )
- if confirm != QMessageBox.Yes:
- return
-
- try:
- for old_path, new_path in reversed(self.backup_info):
- if os.path.exists(new_path):
- os.rename(new_path, old_path)
- self.backup_info = []
- self.save_backup_to_file()
- QMessageBox.information(self, "成功", "重命名已撤销")
- self.auto_update_preview()
- except Exception as e:
- QMessageBox.critical(self, "错误", f"撤销失败: {str(e)}")
- def save_backup_to_file(self):
- """保存备份信息到文件"""
- try:
- with open(self.backup_file, 'w', encoding='utf-8') as f:
- json.dump(self.backup_info, f, ensure_ascii=False, indent=2)
- except Exception as e:
- print(f"保存备份失败: {str(e)}")
- def load_backup_from_file(self):
- """从文件加载备份信息"""
- try:
- if os.path.exists(self.backup_file):
- with open(self.backup_file, 'r', encoding='utf-8') as f:
- self.backup_info = json.load(f)
- except Exception as e:
- print(f"加载备份失败: {str(e)}")
- def delete_selected(self):
- """删除选中的文件条目"""
- selected_rows = self.preview_table.selectionModel().selectedRows()
- if not selected_rows:
- QMessageBox.information(self, "提示", "请先选择要删除的文件")
- return
-
- rows_to_delete = sorted([index.row() for index in selected_rows], reverse=True)
- for row in rows_to_delete:
- if 0 <= row < len(self.file_list):
- del self.file_list[row]
- self.preview_model.removeRow(row)
-
- self.status_bar.showMessage(f"已删除 {len(rows_to_delete)} 个文件条目")
- def dragEnterEvent(self, event):
- """拖放事件处理 - 进入"""
- if event.mimeData().hasUrls():
- event.acceptProposedAction()
- def dropEvent(self, event):
- """拖放事件处理 - 放下"""
- urls = event.mimeData().urls()
- file_paths = []
- dir_paths = []
- for url in urls:
- path = url.toLocalFile()
- if os.path.isdir(path):
- dir_paths.append(path)
- elif os.path.isfile(path):
- file_paths.append(path)
-
- if dir_paths:
- self.current_dir = dir_paths[0]
- self.dir_label.setFullPath(self.current_dir)
- self.file_tree.setRootIndex(self.file_system_model.index(self.current_dir))
- self.auto_update_preview()
- elif file_paths:
- self.current_dir = os.path.dirname(file_paths[0]) if file_paths else QDir.homePath()
- self.file_list = []
- ext_list = self.get_filter_extensions()
-
- seq = self.seq_start.value()
- for path in file_paths:
- filename = os.path.basename(path)
- if self.is_valid_file(filename, ext_list):
- base, ext = os.path.splitext(filename)
- self.file_list.append({
- 'dir': os.path.dirname(path),
- 'name': base,
- 'ext': ext,
- 'path': path,
- 'seq': seq
- })
- seq += 1
-
- self.auto_update_preview()
- event.acceptProposedAction()
- def excepthook(exctype, value, traceback):
- """全局异常处理"""
- error_msg = f"程序崩溃:\n类型: {exctype.__name__}\n信息: {str(value)}"
- QMessageBox.critical(None, "致命错误", error_msg)
- sys.__excepthook__(exctype, value, traceback)
- sys.excepthook = excepthook
- if __name__ == '__main__':
- """程序入口点"""
- app = QApplication(sys.argv)
- window = RenamerGUI()
- window.show()
- sys.exit(app.exec_())
复制代码 |