骑着悟空看八戒 发表于 2025-8-31 22:26:35

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

用文件夹名批量重命名工具 软件说明书

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

二、系统要求
- 操作系统: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 上传
点击文件名下载附件
下载地址

风之影赫 发表于 2025-8-31 22:27:30

已添加相应功能,请再次试用。

huoxianghui913 发表于 2025-8-31 22:28:18


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 = {'seq': seq, 'files': []}
            dir_groups['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()
            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 =
                  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).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

    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(, reverse=True)
      for row in rows_to_delete:
            if 0 <= row < len(self.file_list):
                del self.file_list
                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
            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) 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_())

寒哥Gh61ac8 发表于 2025-8-31 22:28:54

大佬好,软件重命名功能齐全,总的很好,能不能加上一个功能:能将预览区文件拖动移动位置,或者设置上移、下移、置顶、置尾右键,以便放置到自已需的地方再重命名。

寒哥Gh61ac8 发表于 2025-8-31 22:29:28

以前一直使用ACDSee Pro 6这样来批量命名每组图片,有了这个必须试下。

huoxianghui913 发表于 2025-8-31 22:29:43

谢谢,这个真实用

风之影赫 发表于 2025-8-31 22:30:14

刚好需要 谢谢

寒哥Gh61ac8 发表于 2025-8-31 22:30:28

这个功能实用

风之影赫 发表于 2025-8-31 22:30:48

一直用拖把重命名工具,这个也很好

huoxianghui913 发表于 2025-8-31 22:30:56

感谢分享,下载看看。
页: [1] 2
查看完整版本: 用文件夹名批量重命名工具