排序方法:点击其中一张图片,再点击另一个图片可互换位置
![图片[1]-AI图片重命名工具:提高效率_优化管理_必备神器](https://www.afxw6.com/wp-content/uploads/2025/07/4f32d71e5820250728160051-1024x595.gif)
import sys
import os
import time
import tempfile
import shutil
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QListWidget, QListWidgetItem, QLabel, QTextEdit,
QComboBox, QFileDialog, QLineEdit, QMessageBox, QAbstractItemView,
QStatusBar, QAction, QMenu, QFrame
)
from PyQt5.QtGui import QPixmap, QIcon, QKeySequence, QFont
from PyQt5.QtCore import Qt, QSize, QSettings, QUrl
from PIL import Image
APP_ICON_PATH = 'icon.png'
class ImageBatchProcessor(QMainWindow):
DEFAULT_FONT_SIZE = 10
SUPPORTED_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff')
def __init__(self):
super().__init__()
self.settings = self.init_settings()
self.temp_dir = None
self.item_to_move = None # 用于“点击-移动”功能的状态变量
self.initUI()
self.load_settings()
self.setAcceptDrops(True)
def init_settings(self):
config_file_name = 'config.ini'
if getattr(sys, 'frozen', False):
application_path = os.path.dirname(sys.executable)
else:
application_path = os.path.dirname(os.path.abspath(__file__))
self.config_path = os.path.join(application_path, config_file_name)
return QSettings(self.config_path, QSettings.IniFormat)
def initUI(self):
self.setWindowTitle('批量图片重命名与格式转换工具')
self.setGeometry(100, 100, 1000, 650)
if os.path.exists(APP_ICON_PATH): self.setWindowIcon(QIcon(APP_ICON_PATH))
self.create_menus()
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
self.statusBar = QStatusBar()
self.setStatusBar(self.statusBar)
self.statusBar.showMessage('准备就绪。单击选择,按Delete删除,单击移动。')
left_layout = QVBoxLayout()
left_label = QLabel('<h3>1. 添加并排序图片</h3>')
self.image_list_widget = QListWidget()
self.image_list_widget.setDragDropMode(QAbstractItemView.NoDragDrop)
self.image_list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.image_list_widget.itemClicked.connect(self.handle_item_click)
self.image_list_widget.currentItemChanged.connect(self.update_preview)
self.image_list_widget.setIconSize(QSize(64, 64))
top_buttons_layout = QHBoxLayout()
self.add_button = QPushButton('添加图片'); self.add_button.clicked.connect(self.add_images)
self.paste_button = QPushButton('粘贴 (Ctrl+V)'); self.paste_button.clicked.connect(self.paste_from_clipboard)
self.clear_button = QPushButton('清空列表'); self.clear_button.clicked.connect(self.clear_all)
top_buttons_layout.addWidget(self.add_button); top_buttons_layout.addWidget(self.paste_button); top_buttons_layout.addWidget(self.clear_button)
sort_group_label = QLabel("<b>预排序:</b>")
sort_buttons_layout = QHBoxLayout()
self.sort_name_asc_btn = QPushButton("名称 ↑"); self.sort_name_asc_btn.clicked.connect(lambda: self.sort_items(by='name'))
self.sort_name_desc_btn = QPushButton("名称 ↓"); self.sort_name_desc_btn.clicked.connect(lambda: self.sort_items(by='name', reverse=True))
self.sort_date_asc_btn = QPushButton("时间 ↑"); self.sort_date_asc_btn.clicked.connect(lambda: self.sort_items(by='mtime'))
self.sort_date_desc_btn = QPushButton("时间 ↓"); self.sort_date_desc_btn.clicked.connect(lambda: self.sort_items(by='mtime', reverse=True))
self.sort_name_asc_btn.setToolTip("按文件名升序排列"); self.sort_name_desc_btn.setToolTip("按文件名降序排列")
self.sort_date_asc_btn.setToolTip("按修改时间升序排列 (旧->新)"); self.sort_date_desc_btn.setToolTip("按修改时间降序排列 (新->旧)")
sort_buttons_layout.addWidget(sort_group_label); sort_buttons_layout.addWidget(self.sort_name_asc_btn); sort_buttons_layout.addWidget(self.sort_name_desc_btn); sort_buttons_layout.addWidget(self.sort_date_asc_btn); sort_buttons_layout.addWidget(self.sort_date_desc_btn)
left_layout.addWidget(left_label); left_layout.addWidget(self.image_list_widget); left_layout.addLayout(top_buttons_layout)
line = QFrame(); line.setFrameShape(QFrame.HLine); line.setFrameShadow(QFrame.Sunken)
left_layout.addWidget(line); left_layout.addLayout(sort_buttons_layout)
center_layout = QVBoxLayout(); preview_label_title = QLabel('<h3>图片预览</h3>'); self.preview_label = QLabel('请先在左侧选择一张图片'); self.preview_label.setAlignment(Qt.AlignCenter); self.preview_label.setFixedSize(350, 350); self.preview_label.setStyleSheet("border: 1px solid #ccc; background-color: #f0f0f0;"); center_layout.addWidget(preview_label_title); center_layout.addWidget(self.preview_label); center_layout.addStretch()
right_layout = QVBoxLayout(); name_label = QLabel('<h3>2. 输入新名称 (每行一个)</h3>'); self.name_input = QTextEdit(); self.name_input.setPlaceholderText('新名字1\n新名字2\n新名字3\n...'); settings_label = QLabel('<h3>3. 输出设置</h3>'); format_layout = QHBoxLayout(); format_label = QLabel('输出格式:'); self.format_combo = QComboBox(); self.format_combo.addItems(['.jpg', '.png', '.bmp', '.gif', '.tiff']); format_layout.addWidget(format_label); format_layout.addWidget(self.format_combo); folder_layout = QHBoxLayout(); folder_label = QLabel('输出文件夹:'); self.output_path_line = QLineEdit(); self.output_path_line.setReadOnly(True); self.output_folder_button = QPushButton('浏览...'); self.output_folder_button.clicked.connect(self.select_output_folder); folder_layout.addWidget(folder_label); folder_layout.addWidget(self.output_path_line); folder_layout.addWidget(self.output_folder_button); self.start_button = QPushButton('开始处理'); self.start_button.setMinimumHeight(40); self.start_button.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;"); self.start_button.clicked.connect(self.start_processing); right_layout.addWidget(name_label); right_layout.addWidget(self.name_input); right_layout.addSpacing(20); right_layout.addWidget(settings_label); right_layout.addLayout(format_layout); right_layout.addLayout(folder_layout); right_layout.addStretch(); right_layout.addWidget(self.start_button)
main_layout.addLayout(left_layout, 2); main_layout.addLayout(center_layout, 2); main_layout.addLayout(right_layout, 2)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete: self.delete_selected_items()
elif event.key() == Qt.Key_Escape: self.cancel_move()
elif event.matches(QKeySequence.Paste): self.paste_from_clipboard()
else: super().keyPressEvent(event)
def delete_selected_items(self):
selected_items = self.image_list_widget.selectedItems()
if not selected_items: return
reply = QMessageBox.question(self, '确认删除', f'确定要从列表中删除这 {len(selected_items)} 个项目吗?\n(此操作不可撤销)', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
for item in selected_items:
file_path = item.data(Qt.UserRole)
if self.temp_dir and self.temp_dir in file_path:
try: os.remove(file_path)
except OSError as e: print(f"删除临时文件失败: {e}")
row = self.image_list_widget.row(item); self.image_list_widget.takeItem(row)
self.statusBar.showMessage(f"成功删除 {len(selected_items)} 个项目。")
if self.item_to_move in selected_items: self.item_to_move = None
def handle_item_click(self, clicked_item):
if not self.item_to_move:
self.item_to_move = clicked_item; font = self.item_to_move.font(); font.setBold(True); self.item_to_move.setFont(font)
self.statusBar.showMessage(f"已选择 '{os.path.basename(self.item_to_move.data(Qt.UserRole))}'。请点击目标位置以移动。")
else:
font = self.item_to_move.font(); font.setBold(False); self.item_to_move.setFont(font)
if self.item_to_move is not clicked_item:
from_row = self.image_list_widget.row(self.item_to_move); to_row = self.image_list_widget.row(clicked_item)
item = self.image_list_widget.takeItem(from_row); self.image_list_widget.insertItem(to_row, item)
self.image_list_widget.setCurrentItem(item)
self.item_to_move = None; self.statusBar.showMessage("移动操作完成。")
def cancel_move(self):
if self.item_to_move:
font = self.item_to_move.font(); font.setBold(False); self.item_to_move.setFont(font)
self.item_to_move = None; self.statusBar.showMessage("移动操作已取消。")
def sort_items(self, by='name', reverse=False):
self.cancel_move()
item_data_list = []
for i in range(self.image_list_widget.count()):
item = self.image_list_widget.item(i)
item_data_list.append((item.data(Qt.UserRole), item.text()))
if not item_data_list: return
if by == 'name': key_func = lambda data: data[1].lower()
elif by == 'mtime': key_func = lambda data: os.path.getmtime(data[0])
else: return
item_data_list.sort(key=key_func, reverse=reverse)
self.image_list_widget.clear()
for file_path, display_text in item_data_list:
new_item = QListWidgetItem(display_text)
new_item.setData(Qt.UserRole, file_path)
new_item.setIcon(QIcon(file_path))
self.image_list_widget.addItem(new_item)
self.statusBar.showMessage(f"列表已按 {'降序' if reverse else '升序'} 排列。")
def dragEnterEvent(self, event):
if event.mimeData().hasUrls(): event.acceptProposedAction()
else: event.ignore()
def dropEvent(self, event):
if event.mimeData().hasUrls():
urls = event.mimeData().urls(); self._process_file_urls(urls, "拖拽"); event.acceptProposedAction()
def paste_from_clipboard(self):
clipboard = QApplication.clipboard(); mime_data = clipboard.mimeData()
if mime_data.hasImage(): self._paste_image_data(clipboard.image())
elif mime_data.hasUrls(): self._process_file_urls(mime_data.urls(), "粘贴")
else: self.statusBar.showMessage('剪贴板中没有可识别的图片或图片文件。')
def _paste_image_data(self, q_image):
if q_image.isNull(): self.statusBar.showMessage('无法获取剪贴板中的图片数据。'); return
if self.temp_dir is None: self.temp_dir = tempfile.mkdtemp(prefix="ImageTool_")
timestamp = int(time.time() * 1000); temp_filename = f"pasted_image_{timestamp}.png"
temp_filepath = os.path.join(self.temp_dir, temp_filename); q_image.save(temp_filepath, 'PNG')
self._add_image_item(temp_filepath); self.statusBar.showMessage(f'已从剪贴板粘贴图片: {temp_filename}')
def _process_file_urls(self, urls, source_action="添加"):
added_count = 0
for url in urls:
if url.isLocalFile():
file_path = url.toLocalFile()
if os.path.isfile(file_path) and file_path.lower().endswith(self.SUPPORTED_EXTENSIONS):
self._add_image_item(file_path); added_count += 1
if added_count > 0: self.statusBar.showMessage(f'通过{source_action}添加了 {added_count} 张图片。')
else: self.statusBar.showMessage(f'没有通过{source_action}添加有效的图片文件。')
def _add_image_item(self, file_path):
item = QListWidgetItem(os.path.basename(file_path)); item.setData(Qt.UserRole, file_path)
item.setIcon(QIcon(file_path)); self.image_list_widget.addItem(item)
self.image_list_widget.scrollToItem(item)
def add_images(self):
files, _ = QFileDialog.getOpenFileNames(self, "选择图片文件", "", f"Image Files (*{' *'.join(self.SUPPORTED_EXTENSIONS)})")
if files: self._process_file_urls([QUrl.fromLocalFile(f) for f in files], "选择文件")
def closeEvent(self, event):
self.save_settings()
if self.temp_dir and os.path.exists(self.temp_dir):
try: shutil.rmtree(self.temp_dir)
except Exception as e: print(f"清理临时文件夹失败: {e}")
super(ImageBatchProcessor, self).closeEvent(event)
def create_menus(self):
menu_bar = self.menuBar(); settings_menu = menu_bar.addMenu('设置 (&S)'); font_menu = QMenu('字体大小', self); increase_font_action = QAction('增大字体 (+)', self); increase_font_action.triggered.connect(lambda: self.adjust_font_size(1)); increase_font_action.setShortcut('Ctrl++'); decrease_font_action = QAction('减小字体 (-)', self); decrease_font_action.triggered.connect(lambda: self.adjust_font_size(-1)); decrease_font_action.setShortcut('Ctrl+-'); reset_font_action = QAction('重置为默认', self); reset_font_action.triggered.connect(self.reset_font_size); reset_font_action.setShortcut('Ctrl+0'); font_menu.addAction(increase_font_action); font_menu.addAction(decrease_font_action); font_menu.addSeparator(); font_menu.addAction(reset_font_action); settings_menu.addMenu(font_menu)
def adjust_font_size(self, delta):
font = QApplication.font(); current_size = font.pointSize(); new_size = current_size + delta;
if 5 < new_size < 30: font.setPointSize(new_size); QApplication.setFont(font); self.statusBar.showMessage(f"字体大小已设置为 {new_size}pt")
def reset_font_size(self):
font = QApplication.font(); font.setPointSize(self.DEFAULT_FONT_SIZE); QApplication.setFont(font); self.statusBar.showMessage(f"字体大小已重置为 {self.DEFAULT_FONT_SIZE}pt")
def load_settings(self):
self.statusBar.showMessage(f"从 {os.path.basename(self.config_path)} 加载配置..."); geometry = self.settings.value("geometry", self.saveGeometry()); self.restoreGeometry(geometry); state = self.settings.value("windowState", self.saveState()); self.restoreState(state); font_size = self.settings.value("fontSize", self.DEFAULT_FONT_SIZE, type=int); font = QApplication.font(); font.setPointSize(font_size); QApplication.setFont(font); self.statusBar.showMessage(f"配置已加载,当前字体: {font_size}pt")
def save_settings(self):
self.settings.setValue("geometry", self.saveGeometry()); self.settings.setValue("windowState", self.saveState()); self.settings.setValue("fontSize", QApplication.font().pointSize()); self.statusBar.showMessage(f"配置已保存到 {os.path.basename(self.config_path)}")
def update_preview(self, current_item, previous_item):
if not current_item: self.preview_label.clear(); self.preview_label.setText('请先在左侧选择一张图片'); return
path = current_item.data(Qt.UserRole); pixmap = QPixmap(path); scaled_pixmap = pixmap.scaled(self.preview_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation); self.preview_label.setPixmap(scaled_pixmap)
def select_output_folder(self):
folder = QFileDialog.getExistingDirectory(self, "选择输出文件夹");
if folder: self.output_path_line.setText(folder); self.statusBar.showMessage(f'输出文件夹已选择: {folder}')
def clear_all(self):
self.cancel_move()
if self.temp_dir and os.path.exists(self.temp_dir): shutil.rmtree(self.temp_dir); self.temp_dir = None
self.image_list_widget.clear(); self.name_input.clear(); self.output_path_line.clear(); self.preview_label.clear(); self.preview_label.setText('请先在左侧选择一张图片'); self.statusBar.showMessage('已清空所有内容。')
def start_processing(self):
self.cancel_move()
image_count = self.image_list_widget.count()
if image_count == 0: QMessageBox.warning(self, '错误', '请先添加图片!'); return
new_names = [line for line in self.name_input.toPlainText().splitlines() if line.strip()]
if len(new_names) != image_count: QMessageBox.warning(self, '错误', f'图片数量 ({image_count}) 与新名称数量 ({len(new_names)}) 不匹配!'); return
output_dir = self.output_path_line.text()
if not output_dir or not os.path.isdir(output_dir): QMessageBox.warning(self, '错误', '请选择一个有效的输出文件夹!'); return
output_format = self.format_combo.currentText(); processed_count = 0
for i in range(image_count):
try:
item = self.image_list_widget.item(i); original_path = item.data(Qt.UserRole); new_name_base = new_names[i]; new_filename = f"{new_name_base}{output_format}"; new_filepath = os.path.join(output_dir, new_filename); self.statusBar.showMessage(f'正在处理: {os.path.basename(original_path)} -> {new_filename}')
with Image.open(original_path) as img:
if img.mode in ('RGBA', 'P') and output_format.lower() in ['.jpg', '.jpeg']: img = img.convert('RGB')
img.save(new_filepath)
except Exception as e: QMessageBox.critical(self, '处理失败', f'处理文件 {os.path.basename(original_path)} 时发生错误:\n{str(e)}'); self.statusBar.showMessage('处理中断。'); return
processed_count += 1
QMessageBox.information(self, '成功', f'成功处理了 {processed_count} 张图片!\n文件已保存到: {output_dir}')
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ImageBatchProcessor()
ex.show()
sys.exit(app.exec_())
© 版权声明
THE END