- 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_())
复制代码
|