用文件夹名批量重命名工具

[复制链接]
71 |11
发表于 3 天前 | 显示全部楼层 |阅读模式
用文件夹名批量重命名工具 软件说明书

一、软件概述
本软件是一款功能强大的文件批量重命名工具,具有用文件夹名批量重命名的选项,专为需要高效管理大量文件的用户设计。支持多种重命名规则组合,提供实时预览功能,确保操作安全可靠。软件具有直观的图形界面,操作简单便捷,是文件整理、媒体管理、文档归档的理想助手。

二、系统要求
- 操作系统:Windows 7/8/10/11 (64位)
- 运行环境:Python 3.6+ (已内置在安装包中)
- 内存:至少2GB RAM
- 硬盘空间:50MB可用空间

三、主要功能
1. 多种重命名规则组合:
   - 添加前缀/后缀
   - 插入序列号(可设置起始值和位数)
   - 添加日期/时间戳
   - 使用父目录名(可设置层级)
   - 替换/删除指定文字
   - 修改文件扩展名

2. 智能预览功能:
   - 实时预览重命名效果
   - 自动检测文件名冲突
   - 显示完整文件路径

3. 文件管理:
   - 支持拖放添加文件
   - 文件类型筛选(图片/文档/视频/音频)
   - 包含子目录处理
   - 删除不需要的文件条目

4. 安全保障:
   - 操作前确认提示
   - 一键撤销功能
   - 自动备份重命名记录

四、界面说明



1. 目录选择区:
   - 当前路径显示
   - 路径输入框(支持回车确认)
   - "前往"和"选择目录"按钮
   - 文件树视图(显示目录结构)

2. 重命名规则区:
   - 父目录选项(层级选择)
   - 前缀/后缀输入框
   - 序列号设置(位置、位数、起始值)
   - 日期添加选项
   - 文件类型筛选
   - 替换/删除文字功能
   - 扩展名修改

3. 操作按钮区:
   - 执行重命名
   - 撤销操作
   - 删除选中文件

4. 预览区:
   - 原始文件名
   - 新文件名
   - 状态指示(有效/冲突)
   - 文件路径
   - 右键点击预览表格的原始名称行,弹出菜单:上移、下移、移到首行、移到尾行;

五、高级技巧
1. 批量删除文字:使用删除功能快速移除文件名中不需要的广告词或前缀
2. 媒体文件整理:使用"图片/视频/音频"筛选+日期+序列号,快速整理照片和视频
3. 文档归档:使用父目录名+日期+序列号,创建结构化文件名
4. 多文件夹独立编号:勾选"每文件夹独立编号",为每个文件夹的文件单独编号
5. 自定义日期格式:选择"自定义..."后输入Python日期格式代码(如:%Y%m%d_%H%M%S)

六、注意事项
1. 重名前请确认预览效果,避免误操作
2. 撤销功能仅支持软件内执行的重命名操作
3. 避免在文件名中使用非法字符:\/:*?"|
4. 修改扩展名不会改变文件实际格式,仅修改文件名
5. 大量文件操作时建议先小批量测试

七、常见问题解答

Q1: 为什么预览没有变化?
A: 检查规则是否启用(复选框已勾选),确认输入内容正确,尝试调整规则顺序

Q2: 撤销功能失效怎么办?
A: 1) 确保只撤销最近一次操作 2) 检查备份文件是否被删除 3) 文件已被其他程序占用

Q3: 如何批量添加序号?
A: 启用序列号功能,设置起始值和位数,选择位置(前置/后置)

Q4: 文件名冲突如何解决?
A: 预览区状态列会显示冲突,调整前缀、序列号或删除部分文件条目

Q5: 能否恢复多次操作?
A: 当前仅支持撤销最近一次操作,建议分批操作重要文件


根据提议,新增鼠标右键上下移动位置,新增蓝奏云链接。
下载地址:如有解压密码,请用52pojie
新增蓝奏云下载地址
下载地址.txt(270 Bytes, 下载次数: 268)2025-6-13 14:45 上传
点击文件名下载附件
下载地址

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
已添加相应功能,请再次试用。
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
[Python]  
  1. import os
  2. import sys
  3. import json
  4. import datetime
  5. from PyQt5.QtWidgets import (
  6.     QApplication, QMainWindow, QPushButton, QFileDialog, QLabel,
  7.     QLineEdit, QComboBox, QCheckBox, QSpinBox, QTableView,
  8.     QMessageBox, QTreeView, QWidget, QHBoxLayout, QVBoxLayout, QGroupBox,
  9.     QProgressDialog, QFileSystemModel, QGridLayout, QFileIconProvider,
  10.     QStyledItemDelegate, QAbstractItemView
  11. )
  12. from PyQt5.QtCore import Qt, QDir, QModelIndex, QThread, pyqtSignal, QTimer, QUrl
  13. from PyQt5.QtGui import QStandardItemModel, QStandardItem, QGuiApplication, QIcon
  14. class RenameWorker(QThread):
  15.     """文件重命名的后台工作线程"""
  16.     progress_updated = pyqtSignal(int)
  17.     rename_finished = pyqtSignal(bool)
  18.     error_occurred = pyqtSignal(str)
  19.     canceled = pyqtSignal()
  20.     def __init__(self, file_list, operations):
  21.         super().__init__()
  22.         self.file_list = file_list
  23.         self.operations = operations
  24.         self.backup_list = []
  25.     def run(self):
  26.         try:
  27.             count = len(self.file_list)
  28.             for i, file_info in enumerate(self.file_list, start=1):
  29.                 if self.isInterruptionRequested():
  30.                     self.canceled.emit()
  31.                     return
  32.                 old_path = file_info['path']
  33.                 new_name = RenamerGUI.generate_preview_name(file_info, self.operations)
  34.                 new_path = os.path.join(file_info['dir'], new_name)
  35.                 if old_path == new_path:
  36.                     continue
  37.                 if os.path.exists(new_path):
  38.                     raise FileExistsError(f"文件已存在:{new_name}")
  39.                 os.rename(old_path, new_path)
  40.                 self.backup_list.append((old_path, new_path))
  41.                 self.progress_updated.emit(int(i / count * 100))
  42.             self.rename_finished.emit(True)
  43.         except Exception as e:
  44.             self.error_occurred.emit(str(e))
  45. class CustomLabel(QLabel):
  46.     """自定义标签,用于显示完整路径(鼠标悬停时)"""
  47.     def __init__(self, parent=None):
  48.         super().__init__(parent)
  49.         self.full_path = ""
  50.     def setFullPath(self, path):
  51.         self.full_path = path
  52.     def enterEvent(self, event):
  53.         if self.full_path:
  54.             self.setText(self.full_path)
  55.     def leaveEvent(self, event):
  56.         short_path = os.path.basename(self.full_path) if self.full_path else ""
  57.         self.setText(f"当前目录: {short_path}")
  58. class MyItemDelegate(QStyledItemDelegate):
  59.     """自定义表格项委托,用于显示文件和文件夹图标"""
  60.     def __init__(self, folder_icon, file_icon, parent=None):
  61.         super().__init__(parent)
  62.         self.folder_icon = folder_icon
  63.         self.file_icon = file_icon
  64.     def paint(self, painter, option, index):
  65.         if index.isValid():
  66.             if index.model().fileInfo(index).isDir():
  67.                 icon = self.folder_icon
  68.             else:
  69.                 icon = self.file_icon
  70.             pixmap = icon.pixmap(option.rect.size())
  71.             painter.drawPixmap(option.rect, pixmap)
  72.             text_rect = option.rect.adjusted(icon.actualSize(option.rect.size()).width(), 0, 0, 0)
  73.             painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, index.model().data(index, Qt.DisplayRole))
  74.         else:
  75.             super().paint(painter, option, index)
  76. class RenamerGUI(QMainWindow):
  77.     """文件批量重命名工具的主窗口类"""
  78.     def __init__(self):
  79.         super().__init__()
  80.         self.current_dir = QDir.homePath()
  81.         self.file_list = []
  82.         self.backup_info = []
  83.         self.backup_file = "rename_backup.json"
  84.         self.setAcceptDrops(True)
  85.         self.setWindowTitle("文件批量重命名工具 www.52pojie.cn")
  86.         self.resize(800, 650)
  87.         self.init_ui()
  88.         self.load_backup_from_file()
  89.         self.preview_timer = None
  90.     @staticmethod
  91.     def generate_preview_name(file_info, operations):
  92.         """根据操作参数生成预览文件名"""
  93.         name = file_info['name']
  94.         
  95.         # 处理文字替换
  96.         if operations.get('replace_enabled', False):
  97.             old_text = operations.get('replace_old', '')
  98.             new_text = operations.get('replace_new', '')
  99.             if old_text:
  100.                 name = name.replace(old_text, new_text)
  101.         
  102.         # 处理文字删除
  103.         if operations.get('delete_enabled', False):
  104.             delete_text = operations.get('delete_text', '')
  105.             if delete_text:
  106.                 name = name.replace(delete_text, '')
  107.         
  108.         parts = []
  109.         
  110.         # 处理父目录名(可选择前置或后置)
  111.         parent_dirs = RenamerGUI.get_parent_dirs(file_info['dir'], operations.get('parent_level', 0))
  112.         if operations.get('use_parent', False):
  113.             if operations.get('parent_pos') == "前置":
  114.                 parts.extend(parent_dirs)
  115.         
  116.         # 添加前缀
  117.         if operations.get('prefix'):
  118.             parts.append(operations['prefix'])
  119.         
  120.         # 添加序列号(可选择前置或后置)
  121.         if operations.get('sequence', False):
  122.             seq_text = f"{file_info['seq']:0{operations.get('seq_digits', 2)}d}"
  123.             if operations.get('seq_pos') == "前置":
  124.                 parts.append(seq_text)
  125.         
  126.         # 添加处理后的文件名(根据保留原文件名选项)
  127.         if operations.get('keep_original', False):
  128.             parts.append(name)
  129.         
  130.         # 添加序列号(后置情况)
  131.         if operations.get('sequence', False) and operations.get('seq_pos') == "后置":
  132.             parts.append(seq_text)
  133.         
  134.         # 添加后缀
  135.         if operations.get('suffix'):
  136.             parts.append(operations['suffix'])
  137.         
  138.         # 添加日期
  139.         if operations.get('date_enabled', False) and operations.get('date_format'):
  140.             try:
  141.                 date_str = datetime.datetime.now().strftime(operations['date_format'])
  142.             except ValueError:
  143.                 date_str = "格式错误"
  144.             parts.append(date_str)
  145.         
  146.         # 处理父目录名后置的情况
  147.         if operations.get('use_parent', False) and operations.get('parent_pos') == "后置":
  148.             parts.extend(parent_dirs)
  149.         
  150.         # 处理扩展名
  151.         ext = operations.get('new_ext', file_info['ext'])
  152.         if operations.get('change_ext', False) and ext:
  153.             if not ext.startswith('.'):
  154.                 ext = f".{ext}"
  155.         else:
  156.             ext = file_info['ext']
  157.         
  158.         base = '_'.join(filter(None, parts))
  159.         
  160.         # 组合最终文件名 - 确保总有一个名称部分
  161.         if base:
  162.             return f"{base}{ext}"
  163.         elif name:
  164.             return f"{name}{ext}"
  165.         else:
  166.             return f"{file_info['name']}{ext}"
  167.     @staticmethod
  168.     def get_parent_dirs(path, levels):
  169.         """获取指定层级的父目录名列表"""
  170.         parents = []
  171.         for _ in range(levels):
  172.             path, dirname = os.path.split(path)
  173.             if dirname:
  174.                 parents.insert(0, dirname)
  175.             else:
  176.                 break
  177.         return parents[:levels]
  178.     def init_ui(self):
  179.         """初始化用户界面"""
  180.         main_widget = QWidget()
  181.         self.setCentralWidget(main_widget)
  182.         layout = QVBoxLayout(main_widget)
  183.         # 目录选择区域
  184.         dir_layout = QHBoxLayout()
  185.         self.dir_label = CustomLabel()
  186.         self.dir_label.setFullPath(self.current_dir)
  187.         self.path_input = QLineEdit(self.current_dir)
  188.         self.path_input.returnPressed.connect(self.go_to_path)
  189.         go_btn = QPushButton("前往")
  190.         go_btn.clicked.connect(self.go_to_path)
  191.         select_btn = QPushButton("选择目录")
  192.         select_btn.clicked.connect(self.select_directory)
  193.         dir_layout.addWidget(self.dir_label, 1)
  194.         dir_layout.addWidget(self.path_input, 3)
  195.         dir_layout.addWidget(go_btn)
  196.         dir_layout.addWidget(select_btn)
  197.         # 文件树视图
  198.         self.file_system_model = QFileSystemModel()
  199.         self.file_tree = QTreeView()
  200.         self.file_tree.setModel(self.file_system_model)
  201.         self.file_tree.clicked.connect(self.on_dir_selected)
  202.         self.file_system_model.setRootPath(QDir.rootPath())
  203.         icon_provider = QFileIconProvider()
  204.         folder_icon = icon_provider.icon(QFileIconProvider.Folder)
  205.         file_icon = icon_provider.icon(QFileIconProvider.File)
  206.         self.file_tree.setItemDelegate(MyItemDelegate(folder_icon, file_icon))
  207.         # 重命名规则设置区域
  208.         settings_group = QGroupBox("重命名规则")
  209.         settings_layout = QGridLayout(settings_group)
  210.         settings_layout.setVerticalSpacing(5)
  211.         settings_layout.setHorizontalSpacing(5)
  212.         # 第三行设置:父目录、前缀、后缀
  213.         row1 = QHBoxLayout()
  214.         self.parent_cb = QCheckBox("使用父目录名")
  215.         self.parent_level = QSpinBox()
  216.         self.parent_level.setRange(1, 5)
  217.         self.parent_level.setFixedWidth(40)
  218.         self.parent_pos = QComboBox()  # 父目录位置选择
  219.         self.parent_pos.addItems(["前置", "后置"])
  220.         
  221.         row1.addWidget(self.parent_cb)
  222.         row1.addWidget(QLabel("层级"))
  223.         row1.addWidget(self.parent_level)
  224.         row1.addWidget(QLabel("位置"))
  225.         row1.addWidget(self.parent_pos)
  226.         row1.addWidget(QLabel("前缀"))
  227.         self.prefix_input = QLineEdit()
  228.         row1.addWidget(self.prefix_input)
  229.         row1.addWidget(QLabel("后缀"))
  230.         self.suffix_input = QLineEdit()
  231.         row1.addWidget(self.suffix_input)
  232.         settings_layout.addLayout(row1, 2, 0, 1, 7)
  233.         # 第二行设置:保留原文件名、序列号
  234.         row2 = QHBoxLayout()
  235.         self.keep_original = QCheckBox("保留原文件名")
  236.         self.sequence_cb = QCheckBox("序列号")
  237.         self.seq_start = QSpinBox()
  238.         self.seq_start.setRange(1, 9999)
  239.         self.seq_start.setFixedWidth(40)
  240.         self.seq_digits = QSpinBox()
  241.         self.seq_digits.setRange(2, 6)
  242.         self.seq_digits.setFixedWidth(40)
  243.         self.seq_pos = QComboBox()  # 序列号位置选择
  244.         self.seq_pos.addItems(["前置", "后置"])
  245.         self.separate_seq = QCheckBox("每文件夹独立编号")
  246.         
  247.         row2.addWidget(self.keep_original)
  248.         row2.addWidget(self.sequence_cb)
  249.         row2.addWidget(QLabel("起始"))
  250.         row2.addWidget(self.seq_start)
  251.         row2.addWidget(QLabel("位数"))
  252.         row2.addWidget(self.seq_digits)
  253.         row2.addWidget(QLabel("位置"))
  254.         row2.addWidget(self.seq_pos)
  255.         row2.addWidget(self.separate_seq)
  256.         settings_layout.addLayout(row2, 1, 0, 1, 4)
  257.         # 第一行设置:文件类型过滤、包含子目录、添加日期格式、修改扩展名
  258.         row3 = QHBoxLayout()
  259.         
  260.         # 文件类型过滤
  261.         self.file_type = QComboBox()
  262.         self.file_type.addItems(["所有文件", "图片", "文档", "视频", "音频", "自定义类型"])
  263.         self.file_type.setFixedWidth(120)
  264.         row3.addWidget(self.file_type)
  265.         
  266.         # 自定义扩展名输入
  267.         self.custom_ext = QLineEdit()
  268.         self.custom_ext.setPlaceholderText("如 jpg png")
  269.         self.custom_ext.setFixedWidth(100)
  270.         row3.addWidget(self.custom_ext)
  271.         
  272.         # 包含子目录
  273.         self.recursive = QCheckBox("包含子目录")
  274.         row3.addWidget(self.recursive)
  275.         
  276.         # 添加日期功能
  277.         self.date_cb = QCheckBox("添加日期")
  278.         row3.addWidget(self.date_cb)
  279.         
  280.         # 日期格式
  281.         self.date_format = QComboBox()
  282.         self.date_format.setFixedWidth(120)
  283.         self.date_format.addItems([
  284.             "yyyy-mm-dd", "yyyy/mm/dd", "yyyymmdd",
  285.             "yy-mm-dd", "yy/mm/dd", "yyyymm",
  286.             "yyyy年mm月dd日", "自定义..."
  287.         ])
  288.         self.date_format.setCurrentIndex(2)
  289.         self.date_format.currentIndexChanged.connect(self.on_date_format_changed)
  290.         row3.addWidget(self.date_format)
  291.         
  292.         # 自定义日期格式输入框
  293.         self.custom_date_format = QLineEdit("%Y%m%d")
  294.         self.custom_date_format.setFixedWidth(120)
  295.         self.custom_date_format.setVisible(False)
  296.         row3.addWidget(self.custom_date_format)
  297.         
  298.         # 修改扩展名
  299.         self.change_ext_cb = QCheckBox("修改扩展名")
  300.         row3.addWidget(self.change_ext_cb)
  301.         
  302.         # 新扩展名输入框
  303.         self.new_ext_input = QLineEdit()
  304.         self.new_ext_input.setPlaceholderText("如 .png")
  305.         self.new_ext_input.setFixedWidth(80)
  306.         self.new_ext_input.setEnabled(False)
  307.         self.change_ext_cb.toggled.connect(lambda: self.new_ext_input.setEnabled(self.change_ext_cb.isChecked()))
  308.         row3.addWidget(self.new_ext_input)
  309.         
  310.         settings_layout.addLayout(row3, 0, 0, 1, 5)
  311.         # 第四行设置:替换和删除文字
  312.         row4 = QHBoxLayout()
  313.         self.replace_enabled = QCheckBox("替换原文件名文字")
  314.         self.replace_old = QLineEdit()
  315.         self.replace_old.setPlaceholderText("原文件名中要替换的文字")
  316.         self.replace_new = QLineEdit()
  317.         self.replace_new.setPlaceholderText("替换为")
  318.         
  319.         self.delete_enabled = QCheckBox("删除原文件名文字")
  320.         self.delete_text = QLineEdit()
  321.         self.delete_text.setPlaceholderText("原文件名中要删除的文字")
  322.         
  323.         row4.addWidget(self.replace_enabled)
  324.         row4.addWidget(self.replace_old)
  325.         row4.addWidget(QLabel("→"))
  326.         row4.addWidget(self.replace_new)
  327.         row4.addWidget(self.delete_enabled)
  328.         row4.addWidget(self.delete_text)
  329.         settings_layout.addLayout(row4, 3, 0, 1, 7)
  330.         # 按钮区域
  331.         btn_layout = QHBoxLayout()
  332.         self.rename_btn = QPushButton("执行重命名")
  333.         self.rename_btn.clicked.connect(self.start_rename)
  334.         self.undo_btn = QPushButton("撤销")
  335.         self.undo_btn.clicked.connect(self.undo_rename)
  336.         self.delete_btn = QPushButton("删除选中")
  337.         self.delete_btn.clicked.connect(self.delete_selected)
  338.         btn_layout.addWidget(self.rename_btn)
  339.         btn_layout.addWidget(self.undo_btn)
  340.         btn_layout.addWidget(self.delete_btn)
  341.         # 预览表格
  342.         self.preview_model = QStandardItemModel()
  343.         self.preview_table = QTableView()
  344.         self.preview_table.setModel(self.preview_model)
  345.         self.preview_table.setSelectionBehavior(QAbstractItemView.SelectRows)
  346.         self.preview_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
  347.         self.preview_model.setHorizontalHeaderLabels(["原始名称", "新名称", "状态", "文件路径"])
  348.         # 组装界面
  349.         layout.addLayout(dir_layout)
  350.         layout.addWidget(self.file_tree, 1)
  351.         layout.addWidget(settings_group, 0)
  352.         layout.addLayout(btn_layout)
  353.         layout.addWidget(self.preview_table, 2)
  354.         # 连接信号和槽
  355.         self.parent_cb.stateChanged.connect(self.auto_update_preview)
  356.         self.parent_level.valueChanged.connect(self.auto_update_preview)
  357.         self.parent_pos.currentIndexChanged.connect(self.auto_update_preview)
  358.         self.prefix_input.textChanged.connect(self.auto_update_preview)
  359.         self.suffix_input.textChanged.connect(self.auto_update_preview)
  360.         self.keep_original.toggled.connect(self.auto_update_preview)
  361.         self.sequence_cb.toggled.connect(self.auto_update_preview)
  362.         self.seq_start.valueChanged.connect(self.auto_update_preview)
  363.         self.seq_digits.valueChanged.connect(self.auto_update_preview)
  364.         self.seq_pos.currentIndexChanged.connect(self.auto_update_preview)
  365.         self.separate_seq.toggled.connect(self.auto_update_preview)
  366.         self.date_cb.toggled.connect(self.auto_update_preview)
  367.         self.date_format.currentIndexChanged.connect(self.auto_update_preview)
  368.         self.custom_date_format.textChanged.connect(self.auto_update_preview)
  369.         self.file_type.currentIndexChanged.connect(self.auto_update_preview)
  370.         self.custom_ext.textChanged.connect(self.auto_update_preview)
  371.         self.recursive.toggled.connect(self.auto_update_preview)
  372.         self.change_ext_cb.toggled.connect(self.auto_update_preview)
  373.         self.new_ext_input.textChanged.connect(self.auto_update_preview)
  374.         self.replace_enabled.toggled.connect(self.auto_update_preview)
  375.         self.replace_old.textChanged.connect(self.auto_update_preview)
  376.         self.replace_new.textChanged.connect(self.auto_update_preview)
  377.         self.delete_enabled.toggled.connect(self.auto_update_preview)
  378.         self.delete_text.textChanged.connect(self.auto_update_preview)
  379.         self.status_bar = self.statusBar()
  380.         self.status_bar.showMessage("就绪")
  381.     def go_to_path(self):
  382.         """跳转到用户输入的路径"""
  383.         input_path = self.path_input.text()
  384.         if os.path.isdir(input_path):
  385.             self.current_dir = input_path
  386.             self.dir_label.setFullPath(self.current_dir)
  387.             self.file_tree.setRootIndex(self.file_system_model.index(self.current_dir))
  388.             self.auto_update_preview()
  389.         else:
  390.             QMessageBox.warning(self, "路径错误", "输入的路径无效,请检查后重试。")
  391.     def validate_name(self, name, dir_path):
  392.         """验证文件名是否有效"""
  393.         if not name:
  394.             return False
  395.         invalid_chars = ['\\', '/', ':', '*', '?', '"', '<', '>', '|']
  396.         if any(c in name for c in invalid_chars):
  397.             return False
  398.         return not os.path.exists(os.path.join(dir_path, name))
  399.     def auto_update_preview(self, *args):
  400.         """自动更新预览(延迟执行,避免频繁操作)"""
  401.         if self.preview_timer:
  402.             self.preview_timer.stop()
  403.         self.preview_timer = QTimer(self)
  404.         self.preview_timer.setSingleShot(True)
  405.         self.preview_timer.timeout.connect(self.refresh_preview)
  406.         self.preview_timer.start(300)
  407.     def refresh_preview(self):
  408.         """刷新预览表格"""
  409.         self.scan_files()
  410.         self.preview_model.setRowCount(0)
  411.         operations = self.get_operations()
  412.         seq = self.seq_start.value()
  413.         dir_groups = {}
  414.         for file_info in self.file_list:
  415.             dir_path = file_info['dir']
  416.             if dir_path not in dir_groups:
  417.                 dir_groups[dir_path] = {'seq': seq, 'files': []}
  418.             dir_groups[dir_path]['files'].append(file_info)
  419.         for dir_path, group in dir_groups.items():
  420.             for file_info in group['files']:
  421.                 file_info['seq'] = group['seq']
  422.                 group['seq'] += 1
  423.             if not self.separate_seq.isChecked():
  424.                 seq = group['seq']
  425.         for file_info in self.file_list:
  426.             try:
  427.                 new_name = self.generate_preview_name(file_info, operations)
  428.                 status = self.validate_name(new_name, file_info['dir'])
  429.                 item_old = QStandardItem(file_info['name'] + file_info['ext'])
  430.                 item_new = QStandardItem(new_name)
  431.                 item_status = QStandardItem("有效" if status else "冲突")
  432.                 item_status.setForeground(Qt.red if not status else Qt.black)
  433.                 item_path = QStandardItem(file_info['path'])
  434.                 item_path.setToolTip(file_info['path'])
  435.                 self.preview_model.appendRow([item_old, item_new, item_status, item_path])
  436.             except Exception as e:
  437.                 QMessageBox.warning(self, "预览错误", str(e))
  438.         self.preview_table.resizeColumnsToContents()
  439.         self.status_bar.showMessage(f"预览:{len(self.file_list)} 个文件")
  440.     def scan_files(self):
  441.         """扫描目录中的文件"""
  442.         self.file_list = []
  443.         ext_list = self.get_filter_extensions()
  444.         start_seq = self.seq_start.value()
  445.         if self.current_dir:
  446.             try:
  447.                 if self.recursive.isChecked():
  448.                     # 递归模式下,按文件夹分组处理
  449.                     seq = start_seq
  450.                     for root, dirs, files in os.walk(self.current_dir):
  451.                         valid_files = []
  452.                         for filename in files:
  453.                             if self.is_valid_file(filename, ext_list):
  454.                                 valid_files.append(filename)
  455.                         
  456.                         if valid_files:
  457.                             # 处理当前目录的文件
  458.                             dir_seq = seq
  459.                             for filename in valid_files:
  460.                                 base, ext = os.path.splitext(filename)
  461.                                 self.file_list.append({
  462.                                     'dir': root,
  463.                                     'name': base,
  464.                                     'ext': ext,
  465.                                     'path': os.path.join(root, filename),
  466.                                     'seq': dir_seq
  467.                                 })
  468.                                 dir_seq += 1
  469.                            
  470.                             # 如果启用独立编号,则下一个目录从起始编号开始
  471.                             if self.separate_seq.isChecked():
  472.                                 seq = start_seq
  473.                             else:
  474.                                 seq = dir_seq
  475.                 else:
  476.                     # 非递归模式
  477.                     files = [f for f in os.listdir(self.current_dir) if os.path.isfile(os.path.join(self.current_dir, f))]
  478.                     seq = start_seq
  479.                     for filename in files:
  480.                         if self.is_valid_file(filename, ext_list):
  481.                             base, ext = os.path.splitext(filename)
  482.                             self.file_list.append({
  483.                                 'dir': self.current_dir,
  484.                                 'name': base,
  485.                                 'ext': ext,
  486.                                 'path': os.path.join(self.current_dir, filename),
  487.                                 'seq': seq
  488.                             })
  489.                             seq += 1
  490.             except Exception as e:
  491.                 QMessageBox.critical(self, "扫描错误", f"文件扫描失败:{str(e)}")
  492.     def is_valid_file(self, filename, ext_list):
  493.         """检查文件是否符合扩展名过滤条件"""
  494.         if not ext_list:
  495.             return True
  496.         ext = os.path.splitext(filename)[1].lower()
  497.         return ext in ext_list
  498.     def get_filter_extensions(self):
  499.         """获取过滤扩展名列表"""
  500.         type_map = {
  501.             "图片": [".jpg", ".jpeg", ".png", ".gif", ".bmp"],
  502.             "文档": [".doc", ".docx", ".pdf", ".txt", ".xls"],
  503.             "视频": [".mp4", ".avi", ".mkv", ".mov"],
  504.             "音频": [".mp3", ".wav", ".flac", ".ogg"],
  505.             "自定义类型": self.parse_custom_ext()
  506.         }
  507.         key = self.file_type.currentText()
  508.         return type_map.get(key, []) if key != "所有文件" else []
  509.     def parse_custom_ext(self):
  510.         """解析自定义扩展名输入"""
  511.         raw = self.custom_ext.text().strip().lower()
  512.         exts = []
  513.         for ext in raw.split():
  514.             ext = ext.lstrip('.')
  515.             if ext:
  516.                 exts.append(f".{ext}")
  517.         return exts or []
  518.     def on_date_format_changed(self, index):
  519.         """日期格式选择变化时的处理"""
  520.         if index == len(self.date_format) - 1:
  521.             self.custom_date_format.setVisible(True)
  522.         else:
  523.             self.custom_date_format.setVisible(False)
  524.         self.auto_update_preview()
  525.     def get_operations(self):
  526.         """获取当前重命名操作参数"""
  527.         date_enabled = self.date_cb.isChecked()
  528.         if date_enabled:
  529.             if self.date_format.currentIndex() == self.date_format.count() - 1:
  530.                 date_format = self.custom_date_format.text()
  531.             else:
  532.                 date_format = self.get_date_format_string(self.date_format.currentIndex())
  533.         else:
  534.             date_format = None
  535.         replace_enabled = self.replace_enabled.isChecked()
  536.         replace_old = self.replace_old.text().strip()
  537.         replace_new = self.replace_new.text().strip()
  538.         delete_enabled = self.delete_enabled.isChecked()
  539.         delete_text = self.delete_text.text().strip()
  540.         return {
  541.             "use_parent": self.parent_cb.isChecked(),
  542.             "parent_level": self.parent_level.value(),
  543.             "parent_pos": self.parent_pos.currentText(),
  544.             "prefix": self.prefix_input.text(),
  545.             "suffix": self.suffix_input.text(),
  546.             "keep_original": self.keep_original.isChecked(),
  547.             "sequence": self.sequence_cb.isChecked(),
  548.             "seq_digits": self.seq_digits.value(),
  549.             "seq_pos": self.seq_pos.currentText(),
  550.             "date_enabled": date_enabled,
  551.             "date_format": date_format,
  552.             "change_ext": self.change_ext_cb.isChecked(),
  553.             "new_ext": self.new_ext_input.text().strip(),
  554.             "replace_enabled": replace_enabled,
  555.             "replace_old": replace_old,
  556.             "replace_new": replace_new,
  557.             "delete_enabled": delete_enabled,
  558.             "delete_text": delete_text,
  559.         }
  560.     def get_date_format_string(self, index):
  561.         """获取日期格式字符串"""
  562.         formats = [
  563.             "%Y-%m-%d", "%Y/%m/%d", "%Y%m%d",
  564.             "%y-%m-%d", "%y/%m/%d", "%Y%m",
  565.             "%Y年%m月%d日"
  566.         ]
  567.         return formats[index]
  568.     def select_directory(self):
  569.         """选择目录对话框"""
  570.         dir_path = QFileDialog.getExistingDirectory(
  571.             self, "选择目录", self.current_dir, QFileDialog.ShowDirsOnly
  572.         )
  573.         if dir_path:
  574.             self.current_dir = dir_path
  575.             self.dir_label.setFullPath(self.current_dir)
  576.             self.file_tree.setRootIndex(self.file_system_model.index(dir_path))
  577.             self.auto_update_preview()
  578.     def on_dir_selected(self, index):
  579.         """文件树中选择目录时的处理"""
  580.         path = self.file_system_model.filePath(index)
  581.         if os.path.isdir(path):
  582.             self.current_dir = path
  583.             self.dir_label.setFullPath(self.current_dir)
  584.             self.auto_update_preview()
  585.     def start_rename(self):
  586.         """开始重命名操作"""
  587.         if not self.file_list:
  588.             QMessageBox.warning(self, "警告", "没有文件可重命名")
  589.             return
  590.         
  591.         confirm = QMessageBox.question(
  592.             self, "确认", f"确定要重命名 {len(self.file_list)} 个文件吗?",
  593.             QMessageBox.Yes | QMessageBox.No
  594.         )
  595.         if confirm != QMessageBox.Yes:
  596.             return
  597.         
  598.         self.progress_dialog = QProgressDialog("正在重命名...", "取消", 0, 100, self)
  599.         self.progress_dialog.setWindowTitle("请稍候")
  600.         self.progress_dialog.setWindowModality(Qt.WindowModal)
  601.         
  602.         self.worker = RenameWorker(self.file_list.copy(), self.get_operations())
  603.         self.worker.progress_updated.connect(self.progress_dialog.setValue)
  604.         self.worker.rename_finished.connect(self.on_rename_finished)
  605.         self.worker.error_occurred.connect(self.show_error)
  606.         self.worker.canceled.connect(self.progress_dialog.close)
  607.         self.progress_dialog.canceled.connect(self.worker.requestInterruption)
  608.         self.worker.start()
  609.     def on_rename_finished(self, success):
  610.         """重命名完成后的处理"""
  611.         self.progress_dialog.close()
  612.         if success:
  613.             self.backup_info = self.worker.backup_list
  614.             self.save_backup_to_file()
  615.             QMessageBox.information(self, "成功", "文件重命名完成")
  616.             self.auto_update_preview()
  617.     def show_error(self, message):
  618.         """显示错误消息"""
  619.         self.progress_dialog.close()
  620.         QMessageBox.critical(self, "错误", message)
  621.     def undo_rename(self):
  622.         """撤销上次重命名操作"""
  623.         if not self.backup_info:
  624.             QMessageBox.information(self, "提示", "没有可撤销的操作")
  625.             return
  626.         
  627.         confirm = QMessageBox.question(
  628.             self, "确认", "确定要撤销上次重命名操作吗?",
  629.             QMessageBox.Yes | QMessageBox.No
  630.         )
  631.         if confirm != QMessageBox.Yes:
  632.             return
  633.         
  634.         try:
  635.             for old_path, new_path in reversed(self.backup_info):
  636.                 if os.path.exists(new_path):
  637.                     os.rename(new_path, old_path)
  638.             self.backup_info = []
  639.             self.save_backup_to_file()
  640.             QMessageBox.information(self, "成功", "重命名已撤销")
  641.             self.auto_update_preview()
  642.         except Exception as e:
  643.             QMessageBox.critical(self, "错误", f"撤销失败: {str(e)}")
  644.     def save_backup_to_file(self):
  645.         """保存备份信息到文件"""
  646.         try:
  647.             with open(self.backup_file, 'w', encoding='utf-8') as f:
  648.                 json.dump(self.backup_info, f, ensure_ascii=False, indent=2)
  649.         except Exception as e:
  650.             print(f"保存备份失败: {str(e)}")
  651.     def load_backup_from_file(self):
  652.         """从文件加载备份信息"""
  653.         try:
  654.             if os.path.exists(self.backup_file):
  655.                 with open(self.backup_file, 'r', encoding='utf-8') as f:
  656.                     self.backup_info = json.load(f)
  657.         except Exception as e:
  658.             print(f"加载备份失败: {str(e)}")
  659.     def delete_selected(self):
  660.         """删除选中的文件条目"""
  661.         selected_rows = self.preview_table.selectionModel().selectedRows()
  662.         if not selected_rows:
  663.             QMessageBox.information(self, "提示", "请先选择要删除的文件")
  664.             return
  665.         
  666.         rows_to_delete = sorted([index.row() for index in selected_rows], reverse=True)
  667.         for row in rows_to_delete:
  668.             if 0 <= row < len(self.file_list):
  669.                 del self.file_list[row]
  670.                 self.preview_model.removeRow(row)
  671.         
  672.         self.status_bar.showMessage(f"已删除 {len(rows_to_delete)} 个文件条目")
  673.     def dragEnterEvent(self, event):
  674.         """拖放事件处理 - 进入"""
  675.         if event.mimeData().hasUrls():
  676.             event.acceptProposedAction()
  677.     def dropEvent(self, event):
  678.         """拖放事件处理 - 放下"""
  679.         urls = event.mimeData().urls()
  680.         file_paths = []
  681.         dir_paths = []
  682.         for url in urls:
  683.             path = url.toLocalFile()
  684.             if os.path.isdir(path):
  685.                 dir_paths.append(path)
  686.             elif os.path.isfile(path):
  687.                 file_paths.append(path)
  688.         
  689.         if dir_paths:
  690.             self.current_dir = dir_paths[0]
  691.             self.dir_label.setFullPath(self.current_dir)
  692.             self.file_tree.setRootIndex(self.file_system_model.index(self.current_dir))
  693.             self.auto_update_preview()
  694.         elif file_paths:
  695.             self.current_dir = os.path.dirname(file_paths[0]) if file_paths else QDir.homePath()
  696.             self.file_list = []
  697.             ext_list = self.get_filter_extensions()
  698.             
  699.             seq = self.seq_start.value()
  700.             for path in file_paths:
  701.                 filename = os.path.basename(path)
  702.                 if self.is_valid_file(filename, ext_list):
  703.                     base, ext = os.path.splitext(filename)
  704.                     self.file_list.append({
  705.                         'dir': os.path.dirname(path),
  706.                         'name': base,
  707.                         'ext': ext,
  708.                         'path': path,
  709.                         'seq': seq
  710.                     })
  711.                     seq += 1
  712.             
  713.             self.auto_update_preview()
  714.         event.acceptProposedAction()
  715. def excepthook(exctype, value, traceback):
  716.     """全局异常处理"""
  717.     error_msg = f"程序崩溃:\n类型: {exctype.__name__}\n信息: {str(value)}"
  718.     QMessageBox.critical(None, "致命错误", error_msg)
  719.     sys.__excepthook__(exctype, value, traceback)
  720. sys.excepthook = excepthook
  721. if __name__ == '__main__':
  722.     """程序入口点"""
  723.     app = QApplication(sys.argv)
  724.     window = RenamerGUI()
  725.     window.show()
  726.     sys.exit(app.exec_())
复制代码
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
大佬好,软件重命名功能齐全,总的很好,能不能加上一个功能:能将预览区文件拖动移动位置,或者设置上移、下移、置顶、置尾右键,以便放置到自已需的地方再重命名。
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
以前一直使用ACDSee Pro 6这样来批量命名每组图片,有了这个必须试下。
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
谢谢,这个真实用
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
刚好需要 谢谢
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
这个功能实用
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
一直用拖把重命名工具,这个也很好
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
感谢分享,下载看看。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表