成品下载地址:https://wwej.lanzouu.com/iuP4m33ia6de密码:52pj
[Asm] 纯文本查看 复制代码import sys
import os
import win32com.client as win32
import pythoncom
import threading
import time
import shutil
import tempfile
import logging
import traceback
import re # 新增:导入正则表达式模块
from PIL import Image
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QLabel, QLineEdit, QPushButton, QListWidget, QProgressBar,
QGroupBox, QGridLayout, QHBoxLayout, QCheckBox, QFileDialog, QMessageBox,
QColorDialog, QFontDialog, QFrame
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QPixmap, QDragEnterEvent, QDropEvent, QFont, QColor, QPalette
# 确保 win32com 能找到类型库
if getattr(sys, 'frozen', False):
# 打包后模式 - 使用更安全的访问方式
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(sys.executable)))
gen_py_path = os.path.join(base_path, "gen_py")
if not os.path.exists(gen_py_path):
os.makedirs(gen_py_path)
os.environ["GENPY_PATH"] = gen_py_path
# 添加全局线程锁
com_lock = threading.Lock()
class WorkerThread(QThread):
progress_updated = Signal(int, str)
task_completed = Signal()
error_occurred = Signal(str)
compress_completed = Signal(list)
def __init__(self, target, args=None):
super().__init__()
self.target = target
self.args = args if args else []
def run(self):
try:
self.target(*self.args)
self.task_completed.emit()
except (OSError, IOError, pythoncom.com_error) as e:
self.error_occurred.emit(str(e))
logging.error(f"Worker thread error: {str(e)}\n{traceback.format_exc()}")
except Exception as e:
self.error_occurred.emit(f"Unexpected error: {str(e)}")
logging.error(f"Unexpected worker thread error: {str(e)}\n{traceback.format_exc()}")
class WordImageInserter(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Word表格图片插入工具")
self.resize(600, 300) # 增加高度以适应新控件
# 设置中文字体支持
self.font_family = "Microsoft YaHei"
self.small_font = QFont(self.font_family, 9)
self.normal_font = QFont(self.font_family, 10)
self.title_font = QFont(self.font_family, 10, QFont.Weight.Bold)
# 创建主部件
main_widget = QWidget()
self.setCentralWidget(main_widget)
# 主布局
main_layout = QHBoxLayout(main_widget)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(15)
# 左侧:图片插入设置
left_frame = QGroupBox("图片插入设置")
left_frame.setFont(self.title_font)
left_frame.setStyleSheet("""
QGroupBox {
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 5px;
margin-top: 1.5ex;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 5px;
color: #2c3e50;
}
""")
left_layout = QGridLayout(left_frame)
left_layout.setSpacing(10)
# 图片宽度(插入时)
width_label = QLabel("宽度(厘米):")
width_label.setFont(self.small_font)
self.insert_width = QLineEdit("8.0")
self.insert_width.setFont(self.small_font)
self.insert_width.setFixedWidth(100)
left_layout.addWidget(width_label, 0, 0)
left_layout.addWidget(self.insert_width, 0, 1)
# 图片高度(插入时)
height_label = QLabel("高度(厘米):")
height_label.setFont(self.small_font)
self.insert_height = QLineEdit("6.0")
self.insert_height.setFont(self.small_font)
self.insert_height.setFixedWidth(100)
left_layout.addWidget(height_label, 1, 0)
left_layout.addWidget(self.insert_height, 1, 1)
# 行间距
spacing_label = QLabel("行间距:")
spacing_label.setFont(self.small_font)
self.line_spacing = QLineEdit("8")
self.line_spacing.setFont(self.small_font)
self.line_spacing.setFixedWidth(100)
left_layout.addWidget(spacing_label, 3, 0)
left_layout.addWidget(self.line_spacing, 3, 1)
# 每行显示图片数(固定为2列)
per_row_label = QLabel("每行数量:")
per_row_label.setFont(self.small_font)
self.images_per_row = QLineEdit("2")
self.images_per_row.setFont(self.small_font)
self.images_per_row.setFixedWidth(100)
self.images_per_row.setReadOnly(True)
left_layout.addWidget(per_row_label, 4, 0)
left_layout.addWidget(self.images_per_row, 4, 1)
# 字体设置区域
font_frame = QFrame()
font_frame.setFrameShape(QFrame.Shape.NoFrame)
font_layout = QHBoxLayout(font_frame)
font_layout.setContentsMargins(0, 0, 0, 0)
font_layout.setSpacing(5)
# 字体选择按钮
self.font_btn = QPushButton("选择字体")
self.font_btn.setFont(self.small_font)
self.font_btn.setStyleSheet("padding: 5px;")
self.font_btn.clicked.connect(self.select_font)
font_layout.addWidget(self.font_btn)
# 字体颜色选择按钮
self.color_btn = QPushButton("选择颜色")
self.color_btn.setFont(self.small_font)
self.color_btn.setStyleSheet("padding: 5px;")
self.color_btn.clicked.connect(self.select_color)
font_layout.addWidget(self.color_btn)
# 当前字体预览
self.font_preview = QLabel("字体预览")
self.font_preview.setStyleSheet("border: 1px solid #cccccc; padding: 3px;")
font_layout.addWidget(self.font_preview)
left_layout.addWidget(font_frame, 5, 0, 1, 2)
# 功能按钮区
btn_frame = QWidget()
btn_layout = QHBoxLayout(btn_frame)
btn_layout.setContentsMargins(0, 0, 0, 0)
btn_layout.setSpacing(5)
# 图片选择按钮
self.select_btn = QPushButton("选择图片")
self.select_btn.setFont(self.small_font)
self.select_btn.setStyleSheet("background-color: #28a745; color: white; padding: 5px;")
self.select_btn.clicked.connect(self.select_images)
btn_layout.addWidget(self.select_btn)
# 插入按钮
self.insert_btn = QPushButton("插入到Word")
self.insert_btn.setFont(self.small_font)
self.insert_btn.setStyleSheet("background-color: #007bff; color: white; padding: 5px;")
self.insert_btn.clicked.connect(self.start_insert_thread)
self.insert_btn.setEnabled(False)
btn_layout.addWidget(self.insert_btn)
left_layout.addWidget(btn_frame, 6, 0, 1, 2)
# 图片压缩功能区
compress_frame = QWidget()
compress_layout = QHBoxLayout(compress_frame)
compress_layout.setContentsMargins(0, 0, 0, 0)
compress_layout.setSpacing(5)
compress_label = QLabel("压缩至(K):")
compress_label.setFont(self.small_font)
self.compression_quality = QLineEdit("200")
self.compression_quality.setFont(self.small_font)
compress_layout.addWidget(compress_label)
compress_layout.addWidget(self.compression_quality)
self.compress_btn = QPushButton("图片压缩")
self.compress_btn.setFont(self.small_font)
self.compress_btn.setStyleSheet("background-color: #17a2b8; color: white; padding: 5px;")
self.compress_btn.clicked.connect(self.start_compress_thread)
self.compress_btn.setEnabled(False)
compress_layout.addWidget(self.compress_btn)
left_layout.addWidget(compress_frame, 7, 0, 1, 2)
# 批量改名功能区
rename_frame = QWidget()
rename_layout = QHBoxLayout(rename_frame)
rename_layout.setContentsMargins(0, 0, 0, 0)
rename_layout.setSpacing(5)
rename_label = QLabel("头文件名:")
rename_label.setFont(self.small_font)
self.rename_prefix = QLineEdit("图片")
self.rename_prefix.setFont(self.small_font)
rename_layout.addWidget(rename_label)
rename_layout.addWidget(self.rename_prefix)
self.rename_btn = QPushButton("批量改名")
self.rename_btn.setFont(self.small_font)
self.rename_btn.setStyleSheet("background-color: #6610f2; color: white; padding: 5px;")
self.rename_btn.clicked.connect(self.batch_rename_images)
self.rename_btn.setEnabled(False)
rename_layout.addWidget(self.rename_btn)
left_layout.addWidget(rename_frame, 8, 0, 1, 2)
# 图片列表标题
list_label = QLabel("已选图片列表 (支持拖放图片到此处)")
list_label.setFont(self.small_font)
left_layout.addWidget(list_label, 9, 0, 1, 2)
# 图片列表框
self.image_list = QListWidget()
self.image_list.setFont(QFont("Consolas", 9))
self.image_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
self.image_list.itemSelectionChanged.connect(self.update_preview)
self.image_list.setAcceptDrops(True)
self.image_list.setStyleSheet("""
QListWidget {
border: 1px solid #cccccc;
background-color: white;
}
""")
left_layout.addWidget(self.image_list, 10, 0, 1, 2)
# 进度条
self.progress = QProgressBar()
self.progress.setRange(0, 100)
left_layout.addWidget(self.progress, 11, 0, 1, 2)
# 状态标签
self.status_label = QLabel("就绪")
self.status_label.setFont(QFont(self.font_family, 8))
self.status_label.setStyleSheet("color: #666666;")
left_layout.addWidget(self.status_label, 12, 0, 1, 2)
# 设置左侧布局权重
left_layout.setRowStretch(10, 1)
# 右侧:批量调整区域
right_frame = QGroupBox("批量调整图片")
right_frame.setFont(self.title_font)
right_frame.setStyleSheet("""
QGroupBox {
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 5px;
margin-top: 1.5ex;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 5px;
color: #2c3e50;
}
""")
right_layout = QGridLayout(right_frame)
right_layout.setSpacing(10)
# 调整宽度
resize_width_label = QLabel("新宽度(cm):")
resize_width_label.setFont(self.small_font)
self.resize_width = QLineEdit("8.0")
self.resize_width.setFont(self.small_font)
self.resize_width.setFixedWidth(100)
right_layout.addWidget(resize_width_label, 0, 0)
right_layout.addWidget(self.resize_width, 0, 1)
# 调整高度
resize_height_label = QLabel("新高度(cm):")
resize_height_label.setFont(self.small_font)
self.resize_height = QLineEdit("6.0")
self.resize_height.setFont(self.small_font)
self.resize_height.setFixedWidth(100)
right_layout.addWidget(resize_height_label, 1, 0)
right_layout.addWidget(self.resize_height, 1, 1)
# 保持纵横比
self.lock_aspect = QCheckBox("保持纵横比")
self.lock_aspect.setFont(self.small_font)
self.lock_aspect.setChecked(True)
right_layout.addWidget(self.lock_aspect, 2, 0, 1, 2)
# 应用按钮
self.resize_btn = QPushButton("应用到所有图片")
self.resize_btn.setFont(self.small_font)
self.resize_btn.setStyleSheet("background-color: #ff9800; color: white; padding: 5px;")
self.resize_btn.clicked.connect(self.start_resize_thread)
right_layout.addWidget(self.resize_btn, 3, 0, 1, 2)
# 一键清除所有图片按钮
self.clear_btn = QPushButton("清除文档中所有图片")
self.clear_btn.setFont(self.small_font)
self.clear_btn.setStyleSheet("background-color: #dc3545; color: white; padding: 5px;")
self.clear_btn.clicked.connect(self.start_clear_thread)
right_layout.addWidget(self.clear_btn, 4, 0, 1, 2)
# 复制设置按钮
copy_btn = QPushButton("从插入设置复制尺寸")
copy_btn.setFont(QFont(self.font_family, 8))
copy_btn.setStyleSheet("background-color: #f5f5f5; color: #555555; padding: 3px; border: 1px solid #cccccc;")
copy_btn.clicked.connect(self.copy_insert_settings)
right_layout.addWidget(copy_btn, 5, 0, 1, 2)
# 图片预览区域
preview_label = QLabel("图片预览")
preview_label.setFont(QFont(self.font_family, 9, QFont.Weight.Bold))
right_layout.addWidget(preview_label, 6, 0, 1, 2)
# 图片预览 - 增大预览区域
self.preview_label = QLabel()
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.preview_label.setStyleSheet("""
QLabel {
background-color: #f0f0f0;
border: 1px solid #cccccc;
}
""")
self.preview_label.setMinimumSize(200, 200) # 增大预览区域
self.preview_label.setText("请选择图片预览")
self.preview_label.setFont(QFont("Arial", 10))
self.preview_label.setStyleSheet("color: #999999;")
right_layout.addWidget(self.preview_label, 7, 0, 1, 2)
# 图片信息标签
self.info_label = QLabel()
self.info_label.setFont(QFont("Arial", 8))
self.info_label.setStyleSheet("color: #666666;")
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
right_layout.addWidget(self.info_label, 8, 0, 1, 2)
# 图片列表操作按钮
list_btn_frame = QFrame()
list_btn_layout = QHBoxLayout(list_btn_frame)
list_btn_layout.setContentsMargins(0, 0, 0, 0)
list_btn_layout.setSpacing(5)
# 删除选中图片按钮
self.delete_btn = QPushButton("删除选中图片")
self.delete_btn.setFont(self.small_font)
self.delete_btn.setStyleSheet("background-color: #dc3545; color: white; padding: 5px;")
self.delete_btn.clicked.connect(self.delete_selected_images)
self.delete_btn.setEnabled(False)
list_btn_layout.addWidget(self.delete_btn)
# 清空图片列表按钮
self.clear_list_btn = QPushButton("清空图片列表")
self.clear_list_btn.setFont(self.small_font)
self.clear_list_btn.setStyleSheet("background-color: #6c757d; color: white; padding: 5px;")
self.clear_list_btn.clicked.connect(self.clear_all_list_images)
self.clear_list_btn.setEnabled(False)
list_btn_layout.addWidget(self.clear_list_btn)
right_layout.addWidget(list_btn_frame, 9, 0, 1, 2)
# 设置右侧布局权重
right_layout.setRowStretch(7, 1)
# 添加左右框架到主布局
main_layout.addWidget(left_frame, 1)
main_layout.addWidget(right_frame, 1)
# 存储图片路径
self.image_paths = []
# 存储压缩后的图片路径
self.compressed_paths = []
# 字体设置
self.font_name = "宋体" # 使用界面默认字体
self.font_size = 9 # 使用正常字体大小
self.font_color = QColor(0, 0, 0) # 默认黑色
# 设置日志记录
logging.basicConfig(filename='word_image_inserter.log', level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s')
# 启用拖放功能
self.setAcceptDrops(True)
self.image_list.setAcceptDrops(True)
# 在创建完所有控件后调用 update_font_preview
self.update_font_preview()
# 初始化 worker_thread 属性
self.worker_thread = None
def select_font(self):
"""选择字体和大小"""
# 创建初始字体对象
initial_font = QFont(self.font_name, self.font_size)
# 打开字体对话框
dialog = QFontDialog(initial_font, self)
dialog.setWindowTitle("选择字体")
if dialog.exec() == QFontDialog.DialogCode.Accepted:
selected_font = dialog.selectedFont()
if selected_font:
# 更新字体属性
self.font_name = selected_font.family()
self.font_size = selected_font.pointSize()
# 更新预览
self.update_font_preview()
def select_color(self):
"""选择字体颜色"""
color = QColorDialog.getColor()
if color.isValid():
self.font_color = color
# 更新预览
self.update_font_preview()
def update_font_preview(self):
"""更新字体预览显示"""
# 设置预览文本
preview_text = f"{self.font_name} {self.font_size}pt"
self.font_preview.setText(preview_text)
# 使用选择的字体和大小设置预览标签
preview_font = QFont(self.font_name, self.font_size)
self.font_preview.setFont(preview_font)
# 设置文本颜色
palette = self.font_preview.palette()
palette.setColor(QPalette.ColorRole.WindowText, self.font_color)
self.font_preview.setPalette(palette)
# 强制重绘确保更新
self.font_preview.update()
def select_images(self):
"""选择图片文件 - 追加模式"""
file_dialog = QFileDialog()
file_dialog.setNameFilter("图片文件 (*.jpg *.jpeg *.png *.bmp *.gif)")
file_dialog.setFileMode(QFileDialog.FileMode.ExistingFiles)
if file_dialog.exec():
selected_files = file_dialog.selectedFiles()
if selected_files:
# 追加图片而不是覆盖
self.image_paths.extend(selected_files)
for path in selected_files:
self.image_list.addItem(os.path.basename(path))
# 启用相关按钮
self.insert_btn.setEnabled(True)
self.compress_btn.setEnabled(True)
self.rename_btn.setEnabled(True)
self.delete_btn.setEnabled(True)
self.clear_list_btn.setEnabled(True)
self.status_label.setText(f"已添加 {len(selected_files)} 张图片,共 {len(self.image_paths)} 张")
# 更新预览
self.update_preview()
def dragEnterEvent(self, event: QDragEnterEvent):
"""处理拖入事件"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event: QDropEvent):
"""处理拖放事件 - 追加图片而不是覆盖"""
urls = event.mimeData().urls()
if not urls:
return
new_paths = []
for url in urls:
path = url.toLocalFile()
if os.path.isfile(path) and self.is_image_file(path):
new_paths.append(path)
if new_paths:
# 追加图片而不是覆盖
self.image_paths.extend(new_paths)
for path in new_paths:
self.image_list.addItem(os.path.basename(path))
# 启用相关按钮
self.insert_btn.setEnabled(True)
self.compress_btn.setEnabled(True)
self.rename_btn.setEnabled(True)
self.delete_btn.setEnabled(True)
self.clear_list_btn.setEnabled(True)
self.status_label.setText(f"已添加 {len(new_paths)} 张图片,共 {len(self.image_paths)} 张")
# 更新预览
self.update_preview()
@staticmethod
def is_image_file(path):
"""检查文件是否为支持的图片格式(静态方法)"""
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
return os.path.splitext(path)[1].lower() in image_extensions
def delete_selected_images(self):
"""删除选中的图片"""
selected_items = self.image_list.selectedItems()
if not selected_items:
return
confirm = QMessageBox.question(
self,
"确认删除",
f"确定要删除选中的 {len(selected_items)} 张图片吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if confirm != QMessageBox.StandardButton.Yes:
return
# 获取选中的索引(从后往前删除,避免索引变化)
selected_indices = [self.image_list.row(item) for item in selected_items]
selected_indices.sort(reverse=True)
for index in selected_indices:
self.image_list.takeItem(index)
self.image_paths.pop(index)
# 更新状态
self.status_label.setText(f"已删除 {len(selected_indices)} 张图片")
# 如果没有图片了,禁用相关按钮
if not self.image_paths:
self.insert_btn.setEnabled(False)
self.compress_btn.setEnabled(False)
self.rename_btn.setEnabled(False)
self.delete_btn.setEnabled(False)
self.clear_list_btn.setEnabled(False)
self.preview_label.setText("无图片预览")
self.preview_label.setStyleSheet("color: #999999;")
self.info_label.setText("")
else:
# 更新预览
self.update_preview()
def clear_all_list_images(self):
"""清空图片列表中的所有图片"""
if not self.image_paths:
return
confirm = QMessageBox.question(
self,
"确认清空",
"确定要清空所有图片吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if confirm != QMessageBox.StandardButton.Yes:
return
self.image_list.clear()
self.image_paths = []
self.compressed_paths = []
# 禁用相关按钮
self.insert_btn.setEnabled(False)
self.compress_btn.setEnabled(False)
self.rename_btn.setEnabled(False)
self.delete_btn.setEnabled(False)
self.clear_list_btn.setEnabled(False)
self.preview_label.setText("无图片预览")
self.preview_label.setStyleSheet("color: #999999;")
self.info_label.setText("")
self.status_label.setText("已清空所有图片")
def update_preview(self):
"""更新图片预览 - 增大预览区域"""
selected_items = self.image_list.selectedItems()
if not selected_items:
self.delete_btn.setEnabled(False)
self.preview_label.setText("请选择图片预览")
self.preview_label.setStyleSheet("color: #999999;")
self.info_label.setText("")
return
# 启用删除按钮
self.delete_btn.setEnabled(True)
# 只显示第一张选中的图片
index = self.image_list.row(selected_items[0])
img_path = self.image_paths[index]
try:
# 使用Pillow获取图片信息
with Image.open(img_path) as img:
img_width, img_height = img.size
file_size = os.path.getsize(img_path) // 1024 # KB
# 创建QPixmap对象
pixmap = QPixmap(img_path)
if pixmap.isNull():
raise IOError("无法加载图片")
# 调整图片大小以适应预览区域
max_size = 450
if pixmap.width() > max_size or pixmap.height() > max_size:
pixmap = pixmap.scaled(max_size, max_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
# 显示图片
self.preview_label.setPixmap(pixmap)
self.preview_label.setStyleSheet("")
# 显示图片信息
self.info_label.setText(
f"文件名: {os.path.basename(img_path)}\n"
f"尺寸: {img_width}×{img_height}像素 | 大小: {file_size}KB"
)
except (IOError, OSError) as e:
self.preview_label.setText("无法预览图片")
self.preview_label.setStyleSheet("color: #cc0000;")
self.info_label.setText(f"错误: {str(e)}")
logging.error(f"预览图片时出错: {str(e)}")
except Exception as e:
self.preview_label.setText("无法预览图片")
self.preview_label.setStyleSheet("color: #cc0000;")
self.info_label.setText("未知错误")
logging.error(f"预览图片时发生未知错误: {str(e)}")
def copy_insert_settings(self):
"""将插入图片的尺寸设置复制到批量调整区域"""
self.resize_width.setText(self.insert_width.text())
self.resize_height.setText(self.insert_height.text())
self.status_label.setText("已复制插入图片的尺寸设置")
def start_compress_thread(self):
"""启动图片压缩线程"""
# 验证输入
try:
target_size_kb = int(self.compression_quality.text())
if target_size_kb 10240: # 限制在1-10240KB之间
QMessageBox.warning(self, "输入无效", "请输入1-10240之间的数值")
return
except ValueError:
QMessageBox.warning(self, "输入无效", "请输入有效的数值")
return
# 启动线程
self.set_buttons_enabled(False)
self.status_label.setText("正在压缩图片...")
self.progress.setValue(0)
self.worker_thread = WorkerThread(self.compress_images, (target_size_kb,))
self.worker_thread.progress_updated.connect(self.update_compress_progress)
self.worker_thread.task_completed.connect(self.compress_completed)
self.worker_thread.error_occurred.connect(self.handle_error)
self.worker_thread.start()
def compress_images(self, target_size_kb):
"""执行图片压缩操作"""
try:
total_images = len(self.image_paths)
target_size = target_size_kb * 1024 # 转换为字节
self.compressed_paths = []
# 创建压缩图片保存目录
compress_dir = os.path.join(os.path.dirname(self.image_paths[0]), "压缩图片")
os.makedirs(compress_dir, exist_ok=True)
for i, img_path in enumerate(self.image_paths):
try:
# 打开图片
with Image.open(img_path) as img:
# 获取原图片格式
img_format = img.format if img.format else "JPEG"
if img_format.upper() == "PNG" and img.mode in ('RGBA', 'LA'):
# 处理透明背景
background = Image.new(img.mode[:-1], img.size, (255, 255, 255))
background.paste(img, img.split()[-1])
img = background
# 生成保存路径
filename = os.path.basename(img_path)
name, ext = os.path.splitext(filename)
save_path = os.path.join(compress_dir, f"{name}_compressed{ext}")
# 初始质量
quality = 95
step = 5
# 循环调整质量直到达到目标大小
while quality > 0:
# 保存临时图片
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as temp_file:
img.save(temp_file, format=img_format, quality=quality, optimize=True)
temp_path = temp_file.name
# 检查文件大小
file_size = os.path.getsize(temp_path)
if file_size 2:
start_col = 1
self.worker_thread.progress_updated.emit(0, f"从表格位置: 行{start_row}, 列{start_col}开始插入")
except (pythoncom.com_error, AttributeError) as e:
start_row = 1
start_col = 1
try:
table.Cell(start_row, start_col).Select()
selection = word_app.Selection
self.worker_thread.progress_updated.emit(0, "从表格左上角开始插入")
except:
error_message = "无法定位表格起始位置。"
raise Exception(error_message) from e
# 插入图片和文件名(按照单个单元格陆续插入)
total_images = len(self.image_paths)
# 设置当前插入位置
current_row = start_row
current_col = start_col
for i, img_path in enumerate(self.image_paths):
# 更新进度
progress = (i + 1) / total_images * 100
self.worker_thread.progress_updated.emit(progress,
f"插入图片: {os.path.basename(img_path)} ({i + 1}/{total_images})")
try:
# 检查是否需要添加新行
if current_row > table.Rows.Count:
table.Rows.Add()
self.worker_thread.progress_updated.emit(progress, f"添加新行: 共{table.Rows.Count}行")
# 检查是否需要添加新列
if current_col > table.Columns.Count:
table.Columns.Add()
self.worker_thread.progress_updated.emit(progress, f"添加新列: 共{table.Columns.Count}列")
# 获取图片单元格
img_cell = table.Cell(current_row, current_col)
# 添加图片
success = self.add_image_to_cell(img_cell, img_path, params)
# 获取名称单元格(图片行的下一行)
name_row = current_row + 1
if name_row > table.Rows.Count:
table.Rows.Add()
self.worker_thread.progress_updated.emit(progress, f"补充名称行: 共{table.Rows.Count}行")
name_cell = table.Cell(name_row, current_col)
self.add_filename(name_cell, img_path, params, success)
# 移动到下一个位置
current_col += 1
if current_col > 2: # 如果超过2列(即一行最多2张图)
current_col = 1 # 回到第一列
current_row += 2 # 向下移动两行(图片行+文件名行)
except (OSError, IOError, pythoncom.com_error) as e:
error_msg = f"处理图片 {os.path.basename(img_path)} 时出错: {str(e)}"
self.worker_thread.progress_updated.emit(progress, error_msg)
logging.error(f"处理第{i + 1}张图片时出错: {error_msg}\n{traceback.format_exc()}")
except Exception as e:
error_msg = f"处理图片 {os.path.basename(img_path)} 时发生未知错误: {str(e)}"
self.worker_thread.progress_updated.emit(progress, error_msg)
logging.error(f"处理第{i + 1}张图片时发生未知错误: {error_msg}\n{traceback.format_exc()}")
# 完成
self.worker_thread.progress_updated.emit(100, f"成功插入 {total_images} 张图片到表格")
word_app.Activate()
except (pythoncom.com_error, OSError, IOError) as e:
error_message = str(e) or "发生错误"
self.worker_thread.error_occurred.emit(error_message)
logging.error(f"插入图片时出错: {error_message}\n{traceback.format_exc()}")
except Exception as e:
error_message = f"发生未知错误: {str(e)}"
self.worker_thread.error_occurred.emit(error_message)
logging.error(f"插入图片时发生未知错误: {error_message}\n{traceback.format_exc()}")
finally:
# 清理COM
with com_lock:
try:
pythoncom.CoUninitialize()
except:
pass
def update_insert_progress(self, progress, message):
"""更新插入进度"""
self.progress.setValue(int(progress))
self.status_label.setText(message)
def insert_completed(self):
"""插入完成处理"""
self.status_label.setText("图片插入操作完成")
self.progress.setValue(100)
self.set_buttons_enabled(True)
QMessageBox.information(self, "完成", "图片已成功插入到Word文档")
def start_resize_thread(self):
"""启动调整图片大小的线程"""
# 验证输入
try:
new_width = float(self.resize_width.text())
new_height = float(self.resize_height.text())
if new_width 30.0:
QMessageBox.warning(self, "输入无效", "图片宽度应在1-30厘米之间")
return
if new_height 30.0:
QMessageBox.warning(self, "输入无效", "图片高度应在1-30厘米之间")
return
except ValueError:
QMessageBox.warning(self, "输入无效", "请输入有效的数值")
return
# 启动线程
self.set_buttons_enabled(False)
self.status_label.setText("正在修改所有图片大小...")
self.progress.setValue(0)
lock_aspect = self.lock_aspect.isChecked()
self.worker_thread = WorkerThread(self.resize_all_images, (new_width, new_height, lock_aspect))
self.worker_thread.progress_updated.connect(self.update_resize_progress)
self.worker_thread.task_completed.connect(self.resize_completed)
self.worker_thread.error_occurred.connect(self.handle_error)
self.worker_thread.start()
def resize_all_images(self, new_width, new_height, lock_aspect):
"""实际执行修改所有图片大小的操作"""
try:
# 使用线程锁保护COM初始化
with com_lock:
pythoncom.CoInitialize()
# 连接到Word
try:
word_app = win32.gencache.EnsureDispatch("Word.Application")
word_app.Visible = True
except (pythoncom.com_error, AttributeError) as e:
error_msg = "无法连接到Word应用程序"
raise Exception(error_msg) from e
# 检查是否有打开的文档
if word_app.Documents.Count == 0:
error_msg = "没有打开的Word文档"
raise Exception(error_msg)
doc = word_app.ActiveDocument
self.worker_thread.progress_updated.emit(0, f"正在处理文档: {doc.Name}")
# 转换厘米到磅
cm_to_points = 28.3465
target_width = new_width * cm_to_points
target_height = new_height * cm_to_points
# 统计图片数量
total_images = doc.InlineShapes.Count
if total_images == 0:
self.worker_thread.progress_updated.emit(100, "文档中没有图片")
return
# 遍历所有图片并修改大小
for i in range(1, total_images + 1):
try:
shape = doc.InlineShapes(i)
if lock_aspect:
# 保持纵横比,按宽度计算高度
original_ratio = shape.Height / shape.Width
shape.Width = target_width
shape.Height = target_width * original_ratio
else:
# 不锁定纵横比,按指定尺寸调整
shape.LockAspectRatio = 0
shape.Width = target_width
shape.Height = target_height
# 更新进度
progress = (i / total_images) * 100
self.worker_thread.progress_updated.emit(progress, f"正在修改第{i}/{total_images}张图片")
time.sleep(0.1) # 短暂延迟
except (pythoncom.com_error, AttributeError) as e:
logging.error(f"修改第{i}张图片时出错: {str(e)}")
continue
except Exception as e:
logging.error(f"修改第{i}张图片时发生未知错误: {str(e)}")
continue
# 完成
self.worker_thread.progress_updated.emit(100, f"已完成!共修改了 {total_images} 张图片的大小")
except (pythoncom.com_error, OSError, IOError) as e:
error_msg = f"修改图片大小时出错: {str(e)}"
logging.error(error_msg, exc_info=True)
self.worker_thread.error_occurred.emit(error_msg)
except Exception as e:
error_msg = f"修改图片大小时发生未知错误: {str(e)}"
logging.error(error_msg, exc_info=True)
self.worker_thread.error_occurred.emit(error_msg)
finally:
# 清理
with com_lock:
try:
pythoncom.CoUninitialize()
except:
pass
def update_resize_progress(self, progress, message):
"""更新调整大小进度"""
self.progress.setValue(int(progress))
self.status_label.setText(message)
def resize_completed(self):
"""调整大小完成处理"""
self.status_label.setText("图片大小调整操作完成")
self.set_buttons_enabled(True)
QMessageBox.information(self, "完成", "图片大小调整操作完成")
def start_clear_thread(self):
"""启动清除所有图片的线程"""
# 确认操作
confirm = QMessageBox.question(
self,
"确认清除",
"确定要删除文档中所有图片吗?此操作不可恢复!",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if confirm != QMessageBox.StandardButton.Yes:
return
# 启动线程
self.set_buttons_enabled(False)
self.status_label.setText("正在清除所有图片...")
self.progress.setValue(0)
self.worker_thread = WorkerThread(self.clear_all_images)
self.worker_thread.progress_updated.connect(self.update_clear_progress)
self.worker_thread.task_completed.connect(self.clear_completed)
self.worker_thread.error_occurred.connect(self.handle_error)
self.worker_thread.start()
def clear_all_images(self):
"""实际执行清除所有图片的操作"""
try:
# 使用线程锁保护COM初始化
with com_lock:
pythoncom.CoInitialize()
# 连接到Word
try:
word_app = win32.gencache.EnsureDispatch("Word.Application")
word_app.Visible = True
except (pythoncom.com_error, AttributeError) as e:
error_msg = "无法连接到Word应用程序"
raise Exception(error_msg) from e
# 检查是否有打开的文档
if word_app.Documents.Count == 0:
error_msg = "没有打开的Word文档"
raise Exception(error_msg)
doc = word_app.ActiveDocument
self.worker_thread.progress_updated.emit(0, f"正在处理文档: {doc.Name}")
# 统计图片数量
total_images = doc.InlineShapes.Count
if total_images == 0:
self.worker_thread.progress_updated.emit(100, "文档中没有图片")
return
# 从后往前删除图片(避免索引变化问题)
removed = 0
for i in range(total_images, 0, -1):
try:
# 删除图片
doc.InlineShapes(i).Delete()
removed += 1
# 更新进度
progress = (removed / total_images) * 100
self.worker_thread.progress_updated.emit(progress, f"已清除 {removed}/{total_images} 张图片")
time.sleep(0.1) # 短暂延迟
except (pythoncom.com_error, AttributeError) as e:
logging.error(f"清除第{i}张图片时出错: {str(e)}")
continue
except Exception as e:
logging.error(f"清除第{i}张图片时发生未知错误: {str(e)}")
continue
# 完成
self.worker_thread.progress_updated.emit(100, f"已完成!共清除了 {removed} 张图片")
except (pythoncom.com_error, OSError, IOError) as e:
error_msg = f"清除图片时出错: {str(e)}"
logging.error(error_msg, exc_info=True)
self.worker_thread.error_occurred.emit(error_msg)
except Exception as e:
error_msg = f"清除图片时发生未知错误: {str(e)}"
logging.error(error_msg, exc_info=True)
self.worker_thread.error_occurred.emit(error_msg)
finally:
# 清理
with com_lock:
try:
pythoncom.CoUninitialize()
except:
pass
def update_clear_progress(self, progress, message):
"""更新清除进度"""
self.progress.setValue(int(progress))
self.status_label.setText(message)
def clear_completed(self):
"""清除图片完成处理"""
self.status_label.setText("图片清除操作完成")
self.set_buttons_enabled(True)
QMessageBox.information(self, "完成", "图片清除操作完成")
def handle_error(self, error_message):
"""处理错误"""
self.status_label.setText(f"错误: {error_message}")
self.progress.setValue(0)
self.set_buttons_enabled(True)
QMessageBox.critical(self, "错误", error_message)
def set_buttons_enabled(self, enabled):
"""设置按钮启用状态"""
self.insert_btn.setEnabled(enabled and bool(self.image_paths))
self.select_btn.setEnabled(enabled)
self.resize_btn.setEnabled(enabled)
self.clear_btn.setEnabled(enabled)
self.compress_btn.setEnabled(enabled and bool(self.image_paths))
self.rename_btn.setEnabled(enabled and bool(self.image_paths))
self.delete_btn.setEnabled(enabled and bool(self.image_list.selectedItems()))
self.clear_list_btn.setEnabled(enabled and bool(self.image_paths))
def add_image_to_cell(self, cell, img_path, params):
"""在Word表格单元格中添加图片并调整大小"""
temp_dir = None
try:
if not os.path.exists(img_path):
raise FileNotFoundError(f"图片文件不存在: {img_path}")
# 创建临时副本(解决特殊字符问题)
temp_dir = tempfile.mkdtemp()
safe_filename = f"img_{os.urandom(4).hex()}{os.path.splitext(img_path)[1]}"
temp_path = os.path.join(temp_dir, safe_filename)
shutil.copy2(img_path, temp_path)
# 计算目标尺寸(厘米转磅)
cm_to_points = 28.3465
target_width = params["width"] * cm_to_points
target_height = params["height"] * cm_to_points
# 清除单元格原有内容
cell.Range.Delete()
# 添加图片
inline_shape = cell.Range.InlineShapes.AddPicture(
FileName=temp_path,
LinkToFile=False,
SaveWithDocument=True
)
# 调整图片大小
inline_shape.LockAspectRatio = 0 # 不锁定纵横比
inline_shape.Width = target_width
inline_shape.Height = target_height
# 图片居中
cell.Range.Paragraphs.Alignment = 1 # 居中
return True
except (OSError, IOError, pythoncom.com_error) as e:
error_msg = f"无法处理图片 {os.path.basename(img_path)}: {str(e)}"
self.worker_thread.progress_updated.emit(0, error_msg)
return False
except Exception as e:
error_msg = f"无法处理图片 {os.path.basename(img_path)}: 未知错误 {str(e)}"
self.worker_thread.progress_updated.emit(0, error_msg)
return False
finally:
# 清理临时文件
if temp_dir and os.path.exists(temp_dir):
try:
shutil.rmtree(temp_dir)
except Exception as e:
logging.error(f"清理临时文件失败: {str(e)}", exc_info=True)
def add_filename(self, cell, img_path, params, success):
"""在图片下方的单元格中添加文件名 - 应用字体设置"""
try:
# 获取文件名(不含扩展名)
filename = os.path.splitext(os.path.basename(img_path))[0]
# 清除单元格原有内容
cell.Range.Delete()
# 添加文件名文本
if not success:
filename = f"[失败] {filename}"
# 先设置文本内容
cell.Range.Text = filename
# 获取文本范围
text_range = cell.Range
# 设置中文字体名称(重要!)
text_range.Font.NameFarEast = params["font_name"]
# 设置英文字体名称
text_range.Font.Name = params["font_name"]
# 设置字体大小(使用原始字体大小,不需要转换)
text_range.Font.Size = params["font_size"]
# 设置段落对齐
text_range.Paragraphs.Alignment = 1 # 居中
# 设置行间距
text_range.Paragraphs.LineSpacing = params["line_spacing"]
# 设置字体颜色
if not success:
text_range.Font.Color = win32.constants.wdColorRed
else:
# 将QColor转换为Word颜色常量
red = params["font_color"].red()
green = params["font_color"].green()
blue = params["font_color"].blue()
color_val = blue * 65536 + green * 256 + red
text_range.Font.Color = color_val
except (pythoncom.com_error, AttributeError) as e:
error_msg = f"添加文件名失败: {os.path.basename(img_path)}: {str(e)}"
self.worker_thread.progress_updated.emit(0, error_msg)
except Exception as e:
error_msg = f"添加文件名失败: {os.path.basename(img_path)}: 未知错误 {str(e)}"
self.worker_thread.progress_updated.emit(0, error_msg)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = WordImageInserter()
window.show()
sys.exit(app.exec())