PyQt5实战:打造一个高颜值的批量图片处理工具
前言
在日常工作和生活中,我们经常会遇到需要批量修改图片尺寸或调整图片质量的场景,例如上传网站的缩略图、统一文章配图大小等。一张张手动处理不仅效率低下,而且容易出错。为了解决这个问题,我们利用 Python 的 PyQt5 和 Pillow 库,打造了一个既美观又实用的批量图片处理工具。
本文将详细介绍这个工具的设计思路、功能实现以及关键代码,带你从零开始构建一个属于自己的桌面应用。

功能概览
这款批量图片处理器主要具备以下功能:
- 批量选择图片:支持一次性选择多张图片(
.png,.jpg,.jpeg,.bmp)。 - 自定义尺寸:可以设置目标图片的宽度和高度。如果高度设置为 0,程序会自动按宽度进行等比缩放,保持图片比例不变。
- 调节图片质量:通过滑动条,可以直观地设置输出图片的质量(1-100),从而控制文件大小。
- 指定输出目录:用户可以自由选择处理后图片的保存位置。
- 实时进度反馈:处理过程中有进度条显示当前进度,处理完成后会弹出提示。
技术栈
- Python 3
- PyQt5:用于构建图形用户界面(GUI)。
- Pillow:强大的图片处理库,用于读取、修改和保存图片。
安装依赖
在运行代码之前,请确保你已经安装了必要的库:
pip install PyQt5 Pillow
界面设计
一个好的工具离不开一个直观、美观的界面。我们使用 PyQt5 来布局,并通过 QSS (类似 CSS) 来美化样式,提升用户体验。
界面主要分为四个部分:
- 文件选择区:左侧是一个列表,用于显示已选择的图片;右侧是一个“选择图片”按钮。
- 参数设置区:包括宽度、高度输入框和图片质量滑动条。
- 输出设置区:用于选择处理后图片的保存目录。
- 任务执行区:包含一个进度条和一个“开始处理”按钮。
核心代码解析
我们的主程序 main.py 包含一个 ImageProcessorApp 类,它负责界面的构建和事件的处理。
1. 初始化界面 (initUI)
这个方法负责创建所有的UI组件,如按钮、输入框、滑动条等,并使用 QVBoxLayout 和 QHBoxLayout 进行布局。我们还通过 setStyleSheet 添加了一些 QSS 样式,让界面看起来更加现代化。
2. 图片处理逻辑 (start_processing)
这是整个工具的核心,当用户点击“开始处理”按钮时,这个方法会被调用。
def start_processing(self):
# 检查是否已选择文件和输出目录
if not self.selected_files or not self.output_directory:
QMessageBox.warning(self, '警告', '请选择图片和输出目录')
return
# 获取用户设置的参数
width = int(self.width_input.text())
height = int(self.height_input.text())
quality = self.quality_slider.value()
total_files = len(self.selected_files)
self.progress_bar.setMaximum(total_files)
# 遍历并处理每张图片
for i, file_path in enumerate(self.selected_files):
try:
img = Image.open(file_path)
# 调整尺寸
if width > 0:
if height > 0:
new_size = (width, height)
else: # 高度为0,等比缩放
w_percent = (width / float(img.size[0]))
h_size = int((float(img.size[1]) * float(w_percent)))
new_size = (width, h_size)
img = img.resize(new_size, Image.Resampling.LANCZOS)
# 转换图片模式为 RGB (保存为jpg需要)
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
# 保存图片
base_name = os.path.basename(file_path)
name, ext = os.path.splitext(base_name)
output_path = os.path.join(self.output_directory, f'{name}_processed.jpg')
img.save(output_path, 'jpeg', quality=quality)
# 更新进度条
self.progress_bar.setValue(i + 1)
except Exception as e:
print(f"处理文件失败 {file_path}: {e}")
# 处理完成
QMessageBox.information(self, '完成', '所有图片处理完成!')
# 重置状态
self.progress_bar.setValue(0)
self.file_list_widget.clear()
self.selected_files = []
关键点解析:
- 等比缩放:当用户只输入宽度时,我们通过计算原始宽高比来确定新的高度,保证图片不会被拉伸变形。
- 图片模式转换:
Pillow打开的图片可能有不同的模式(如RGBA带有透明通道)。在保存为JPG格式时,需要先将其转换为RGB模式。 - 异常处理:使用
try...except包裹处理逻辑,防止因个别图片文件损坏而导致整个程序崩溃。 - 用户反馈:处理开始前进行输入校验,处理完成后通过
QMessageBox给予用户明确的提示,并清空文件列表,方便进行下一次操作。
完整代码
import sys
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QLineEdit, QSlider, QListWidget,
QFileDialog, QProgressBar, QListWidgetItem)
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon, QFont
class ImageProcessorApp(QWidget):
def __init__(self):
super().__init__()
self.selected_files = []
self.output_directory = ''
self.initUI()
def initUI(self):
self.setWindowTitle('批量图片处理器 作者:小庄-Python办公(公众号同名)')
self.setGeometry(100, 100, 800, 600)
self.setStyleSheet("""
QWidget {
font-size: 14px;
}
QPushButton {
background-color: #4CAF50;
color: white;
padding: 10px;
border: none;
border-radius: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
QLineEdit, QListWidget {
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
QProgressBar {
text-align: center;
}
""")
self.setAcceptDrops(True)
main_layout = QVBoxLayout()
self.setLayout(main_layout)
# File selection
file_layout = QHBoxLayout()
self.file_list_widget = QListWidget()
select_button = QPushButton('选择图片')
select_button.clicked.connect(self.select_files)
file_layout.addWidget(self.file_list_widget)
file_layout.addWidget(select_button)
main_layout.addLayout(file_layout)
# Dimensions and Quality
settings_layout = QHBoxLayout()
dim_layout = QVBoxLayout()
width_layout = QHBoxLayout()
width_layout.addWidget(QLabel('宽度:'))
self.width_input = QLineEdit('800')
width_layout.addWidget(self.width_input)
dim_layout.addLayout(width_layout)
height_layout = QHBoxLayout()
height_layout.addWidget(QLabel('高度:'))
self.height_input = QLineEdit('0')
height_layout.addWidget(self.height_input)
dim_layout.addLayout(height_layout)
dim_layout.addWidget(QLabel('(0 表示自动等比缩放)'))
settings_layout.addLayout(dim_layout)
quality_layout = QVBoxLayout()
quality_slider_layout = QHBoxLayout()
quality_slider_layout.addWidget(QLabel('质量 (1-100):'))
self.quality_slider = QSlider(Qt.Horizontal)
self.quality_slider.setRange(1, 100)
self.quality_slider.setValue(85)
self.quality_label = QLabel('85')
self.quality_slider.valueChanged.connect(lambda v: self.quality_label.setText(str(v)))
quality_slider_layout.addWidget(self.quality_slider)
quality_slider_layout.addWidget(self.quality_label)
quality_layout.addLayout(quality_slider_layout)
settings_layout.addLayout(quality_layout)
main_layout.addLayout(settings_layout)
# Output directory
output_layout = QHBoxLayout()
self.output_dir_label = QLabel(f'输出目录: {self.output_directory}')
self.output_dir_button = QPushButton('选择目录')
self.output_dir_button.clicked.connect(self.select_output_directory)
output_layout.addWidget(self.output_dir_label)
output_layout.addWidget(self.output_dir_button)
main_layout.addLayout(output_layout)
# Progress and Start
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
start_button = QPushButton('开始处理')
start_button.clicked.connect(self.start_processing)
main_layout.addWidget(self.progress_bar)
main_layout.addWidget(start_button)
self.show()
def start_processing(self):
if not self.selected_files or not self.output_directory:
QMessageBox.warning(self, '警告', '请选择图片和输出目录')
return
try:
width = int(self.width_input.text())
height = int(self.height_input.text())
except ValueError:
QMessageBox.warning(self, '警告', '请输入有效的宽度和高度')
return
quality = self.quality_slider.value()
total_files = len(self.selected_files)
self.progress_bar.setMaximum(total_files)
for i, file_path in enumerate(self.selected_files):
try:
img = Image.open(file_path)
if width > 0:
if height > 0:
new_size = (width, height)
else: # Auto scale height
w_percent = (width / float(img.size[0]))
h_size = int((float(img.size[1]) * float(w_percent)))
new_size = (width, h_size)
img = img.resize(new_size, Image.Resampling.LANCZOS)
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
base_name = os.path.basename(file_path)
name, ext = os.path.splitext(base_name)
output_path = os.path.join(self.output_directory, f'{name}_processed.jpg')
img.save(output_path, 'jpeg', quality=quality)
self.progress_bar.setValue(i + 1)
except Exception as e:
print(f"处理文件失败 {file_path}: {e}")
QMessageBox.information(self, '完成', '所有图片处理完成!')
self.progress_bar.setValue(0)
self.file_list_widget.clear()
self.selected_files = []
def select_files(self):
files, _ = QFileDialog.getOpenFileNames(self, '选择图片', '', '图片文件 (*.png *.jpg *.jpeg *.bmp)')
if files:
self.selected_files.extend(files)
self.file_list_widget.addItems(files)
def select_output_directory(self):
directory = QFileDialog.getExistingDirectory(self, '选择输出目录')
if directory:
self.output_directory = directory
self.output_dir_label.setText(f'输出目录: {self.output_directory}')
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
urls = event.mimeData().urls()
files_to_add = []
for url in urls:
path = url.toLocalFile()
if os.path.isdir(path):
for root, _, filenames in os.walk(path):
for filename in filenames:
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')):
files_to_add.append(os.path.join(root, filename))
elif os.path.isfile(path):
if path.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')):
files_to_add.append(path)
if files_to_add:
self.selected_files.extend(files_to_add)
self.file_list_widget.addItems(files_to_add)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ImageProcessorApp()
sys.exit(app.exec_())
如何运行
- 确保已安装
PyQt5和Pillow。 - 将上面的代码保存为
main.py。 - 在终端中运行:
python main.py
总结
通过这个项目,我们不仅实现了一个非常实用的工具,还学习了如何将 PyQt5 的界面设计与 Pillow 的强大功能相结合,来解决实际问题。你可以根据自己的需求,进一步扩展这个工具,例如添加图片格式转换、添加水印等功能。
希望这篇文章能对你有所帮助,快去动手试试吧!