# 需安装的库:pip install pillow pygame python-docx pywin32 opencv-python
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
from PIL import Image, ImageTk
import os
import io
import zipfile
import json
import pygame
from pygame import mixer
import tempfile
import subprocess
import sys
import datetime
from docx import Document
from docx.shared import Inches
import pythoncom
import win32com.client as win32
import pickle
import base64
import zlib
from io import BytesIO
import re
import shutil # 添加这行到文件顶部的其他import语句中
import traceback
class WordEditor:
def __init__(self, root):
self.root = root
self.root.title("Word文档编辑器")
self.root.geometry("1200x800")
self.root.configure(bg="#f0f0f0")
# 初始化日志系统
self.log_file = os.path.join(tempfile.gettempdir(),
f"word_editor_log_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
self.log("程序启动")
# 初始化音频播放器
pygame.init()
mixer.init()
self.current_audio = None # 当前播放的音频
# 当前文档路径
self.current_file = None
self.unsaved_changes = False
# 临时文件夹用于存储媒体文件
self.temp_media_dir = tempfile.mkdtemp(prefix="word_editor_media_")
self.log(f"创建临时目录: {self.temp_media_dir}")
# 存储插入的多媒体内容
self.inserted_media = [] # 格式: {"id": 唯一ID, "type": 类型, "path": 路径, "name": 文件名, "image_obj": Tk图片对象}
self.media_tags = {} # 关联Text组件的tag和媒体ID,值为(start, end)元组
# 创建菜单栏
self.create_menu()
# 创建工具栏
self.create_toolbar()
# 创建状态栏
self.status_var = tk.StringVar()
self.status_var.set(f"就绪 - 日志文件: {os.path.basename(self.log_file)}")
self.status_bar = tk.Label(root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W,
bg="#e0e0e0")
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 创建主编辑区域
self.create_editor()
# 设置主题
self.set_theme("light")
# 设置关闭事件
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# 修改临时目录创建方式
self.temp_media_dir = os.path.join(tempfile.gettempdir(), "word_editor_media")
os.makedirs(self.temp_media_dir, exist_ok=True)
self.log(f"创建媒体临时目录: {self.temp_media_dir}")
def log(self, message):
"""记录日志到文件"""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] {message}\n")
def create_menu(self):
menu_bar = tk.Menu(self.root)
# 文件菜单
file_menu = tk.Menu(menu_bar, tearoff=0)
file_menu.add_command(label="新建", command=self.new_file, accelerator="Ctrl+N")
file_menu.add_command(label="打开", command=self.open_file, accelerator="Ctrl+O")
file_menu.add_command(label="保存", command=self.save_file, accelerator="Ctrl+S")
file_menu.add_command(label="另存为", command=self.save_as, accelerator="Ctrl+Shift+S")
file_menu.add_command(label="打包保存", command=self.save_packaged, accelerator="Ctrl+Shift+P")
file_menu.add_separator()
file_menu.add_command(label="打印", command=self.print_document, accelerator="Ctrl+P")
file_menu.add_separator()
file_menu.add_command(label="关闭", command=self.close_file, accelerator="Ctrl+W")
file_menu.add_command(label="退出", command=self.on_closing, accelerator="Alt+F4")
menu_bar.add_cascade(label="文件", menu=file_menu)
# 编辑菜单
edit_menu = tk.Menu(menu_bar, tearoff=0)
edit_menu.add_command(label="撤销", command=self.undo, accelerator="Ctrl+Z")
edit_menu.add_command(label="重做", command=self.redo, accelerator="Ctrl+Y")
edit_menu.add_separator()
edit_menu.add_command(label="剪切", command=self.cut, accelerator="Ctrl+X")
edit_menu.add_command(label="复制", command=self.copy, accelerator="Ctrl+C")
edit_menu.add_command(label="粘贴", command=self.paste, accelerator="Ctrl+V")
# 添加删除选项
edit_menu.add_command(label="删除", command=self.delete_selection, accelerator="Delete")
edit_menu.add_command(label="删除选中媒体", command=self.delete_selected_media)
edit_menu.add_separator()
edit_menu.add_command(label="全选", command=self.select_all, accelerator="Ctrl+A")
edit_menu.add_command(label="查找", command=self.find_text, accelerator="Ctrl+F")
edit_menu.add_command(label="替换", command=self.replace_text, accelerator="Ctrl+H")
menu_bar.add_cascade(label="编辑", menu=edit_menu)
# 插入菜单
insert_menu = tk.Menu(menu_bar, tearoff=0)
insert_menu.add_command(label="图片", command=self.insert_image)
insert_menu.add_command(label="音频", command=self.insert_audio)
insert_menu.add_command(label="视频", command=self.insert_video)
insert_menu.add_separator()
insert_menu.add_command(label="表格", command=self.insert_table)
insert_menu.add_command(label="超链接", command=self.insert_hyperlink)
menu_bar.add_cascade(label="插入", menu=insert_menu)
# 媒体菜单
media_menu = tk.Menu(menu_bar, tearoff=0)
media_menu.add_command(label="停止播放", command=self.stop_media)
media_menu.add_command(label="暂停播放", command=self.pause_media)
media_menu.add_command(label="继续播放", command=self.resume_media)
# 添加删除媒体选项
media_menu.add_separator()
media_menu.add_command(label="删除当前选中媒体", command=self.delete_selected_media)
menu_bar.add_cascade(label="媒体", menu=media_menu)
# 视图菜单
view_menu = tk.Menu(menu_bar, tearoff=0)
self.theme_var = tk.StringVar(value="light")
view_menu.add_radiobutton(label="浅色主题", variable=self.theme_var, value="light", command=self.set_theme)
view_menu.add_radiobutton(label="深色主题", variable=self.theme_var, value="dark", command=self.set_theme)
view_menu.add_separator()
view_menu.add_checkbutton(label="状态栏", command=self.toggle_statusbar)
menu_bar.add_cascade(label="视图", menu=view_menu)
# 帮助菜单
help_menu = tk.Menu(menu_bar, tearoff=0)
help_menu.add_command(label="关于", command=self.show_about)
help_menu.add_command(label="查看日志", command=self.view_log)
menu_bar.add_cascade(label="帮助", menu=help_menu)
self.root.config(menu=menu_bar)
# 绑定快捷键
self.root.bind("<Control-n>", lambda e: self.new_file())
self.root.bind("<Control-o>", lambda e: self.open_file())
self.root.bind("<Control-s>", lambda e: self.save_file())
self.root.bind("<Control-Shift-S>", lambda e: self.save_as())
self.root.bind("<Control-Shift-P>", lambda e: self.save_packaged())
self.root.bind("<Control-p>", lambda e: self.print_document())
self.root.bind("<Control-w>", lambda e: self.close_file())
self.root.bind("<Control-f>", lambda e: self.find_text())
self.root.bind("<Control-h>", lambda e: self.replace_text())
# 绑定删除快捷键
self.root.bind("<Delete>", lambda e: self.delete_selection())
self.root.bind("<BackSpace>", lambda e: self.delete_selection())
def create_toolbar(self):
toolbar = tk.Frame(self.root, bd=1, relief=tk.RAISED, bg="#e0e0e0")
toolbar.pack(side=tk.TOP, fill=tk.X)
# 工具栏按钮
icons = [
("新建", "📄", self.new_file),
("打开", "📂", self.open_file),
("保存", "💾", self.save_file),
("另存为", "💾+", self.save_as),
("打包保存", "📦", self.save_packaged),
("打印", "🖨️", self.print_document),
("剪切", "✂️", self.cut),
("复制", "📋", self.copy),
("粘贴", "📌", self.paste),
("删除", "🗑️", self.delete_selection), # 添加删除按钮
("图片", "🖼️", self.insert_image),
("音频", "🔊", self.insert_audio),
("视频", "📹", self.insert_video),
("停止", "⏹️", self.stop_media),
("暂停", "⏸️", self.pause_media),
("播放", "▶️", self.resume_media),
]
for text, icon, cmd in icons:
btn = tk.Button(toolbar, text=f"{icon} {text}", command=cmd,
bg="#e0e0e0", bd=1, relief=tk.FLAT, padx=5, pady=2)
btn.pack(side=tk.LEFT, padx=2, pady=2)
btn.bind("<Enter>", lambda e, b=btn: b.config(bg="#d0d0d0"))
btn.bind("<Leave>", lambda e, b=btn: b.config(bg="#e0e0e0"))
# 字体选择
font_frame = tk.Frame(toolbar, bg="#e0e0e0")
font_frame.pack(side=tk.LEFT, padx=(20, 5))
tk.Label(font_frame, text="字体:", bg="#e0e0e0").pack(side=tk.LEFT)
self.font_var = tk.StringVar(value="Arial")
font_combo = ttk.Combobox(font_frame, textvariable=self.font_var, width=12, state="readonly")
font_combo['values'] = ("Arial", "Times New Roman", "Courier New", "Verdana", "Calibri", "Georgia")
font_combo.pack(side=tk.LEFT, padx=5)
font_combo.bind("<<ComboboxSelected>>", self.change_font)
# 字号选择
tk.Label(font_frame, text="字号:", bg="#e0e0e0").pack(side=tk.LEFT, padx=(10, 0))
self.size_var = tk.IntVar(value=12)
size_combo = ttk.Combobox(font_frame, textvariable=self.size_var, width=4, state="readonly")
size_combo['values'] = tuple(range(8, 37, 2))
size_combo.pack(side=tk.LEFT, padx=5)
size_combo.bind("<<ComboboxSelected>>", self.change_font_size)
# 样式按钮
style_frame = tk.Frame(toolbar, bg="#e0e0e0")
style_frame.pack(side=tk.LEFT, padx=(20, 5))
self.bold_var = tk.BooleanVar()
bold_btn = tk.Checkbutton(style_frame, text="B", font=("Arial", 10, "bold"),
variable=self.bold_var, command=self.toggle_bold,
bg="#e0e0e0", bd=1, relief=tk.FLAT)
bold_btn.pack(side=tk.LEFT)
self.italic_var = tk.BooleanVar()
italic_btn = tk.Checkbutton(style_frame, text="I", font=("Arial", 10, "italic"),
variable=self.italic_var, command=self.toggle_italic,
bg="#e0e0e0", bd=1, relief=tk.FLAT)
italic_btn.pack(side=tk.LEFT, padx=5)
self.underline_var = tk.BooleanVar()
underline_btn = tk.Checkbutton(style_frame, text="U", font=("Arial", 10, "underline"),
variable=self.underline_var, command=self.toggle_underline,
bg="#e0e0e0", bd=1, relief=tk.FLAT)
underline_btn.pack(side=tk.LEFT)
# 对齐按钮
align_frame = tk.Frame(toolbar, bg="#e0e0e0")
align_frame.pack(side=tk.LEFT, padx=(20, 5))
align_btns = [
("左对齐", "⬅️", "left"),
("居中", "⏺️", "center"),
("右对齐", "➡️", "right"),
("两端对齐", "⬌", "justify")
]
for text, icon, align in align_btns:
btn = tk.Button(align_frame, text=icon, command=lambda a=align: self.set_alignment(a),
bg="#e0e0e0", bd=1, relief=tk.FLAT, padx=5, pady=2)
btn.pack(side=tk.LEFT, padx=2)
btn.bind("<Enter>", lambda e, b=btn: b.config(bg="#d0d0d0"))
btn.bind("<Leave>", lambda e, b=btn: b.config(bg="#e0e0e0"))
def create_editor(self):
editor_frame = tk.Frame(self.root)
editor_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建文本编辑区域
self.text_area = scrolledtext.ScrolledText(
editor_frame, wrap=tk.WORD, font=("Arial", 12),
undo=True, maxundo=100, padx=10, pady=10
)
self.text_area.pack(fill=tk.BOTH, expand=True)
# 设置初始文本
self.default_text = (
"欢迎使用Word文档编辑器!\n\n"
"现在可以在文字中间插入多媒体了:\n"
"例如:在这句话 "
" 后面插入图片\n\n"
"开始编辑您的文档..."
)
self.text_area.insert(tk.END, self.default_text)
self.log("已初始化默认文本")
# 绑定事件
self.text_area.bind("<<Modified>>", self.on_text_modified)
self.text_area.bind("<KeyRelease>", self.update_status)
self.text_area.bind("<Button-1>", self.on_media_click)
def new_file(self):
self.log("执行新建文件操作")
if self.check_save():
self.text_area.delete(1.0, tk.END)
self.current_file = None
self.inserted_media = []
self.media_tags = {}
# 新建文件时插入初始欢迎文本
self.text_area.insert(tk.END, self.default_text)
self.status_var.set("新建文档")
self.root.title("Word文档编辑器 - 未命名")
self.unsaved_changes = False
self.log("已创建新文档")
def open_file(self):
self.log("执行打开文件操作")
if self.check_save():
file_path = filedialog.askopenfilename(
filetypes=[
("Word文档", "*.docx"),
("二进制文档", "*.wordbin"),
("Word 97-2003文档", "*.doc"),
("文本文件", "*.txt"),
("所有文件", "*.*")
]
)
if file_path:
try:
self.status_var.set(f"正在打开文件: {file_path}")
self.log(f"开始打开文件: {file_path}")
self.root.update() # 更新UI显示状态
if file_path.lower().endswith('.wordpack'):
self.load_packaged(file_path)
else:
self.load_document(file_path)
self.status_var.set(f"已打开: {os.path.basename(file_path)}")
self.log(f"成功打开文件: {file_path}")
except Exception as e:
error_msg = f"打开文件失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def load_document(self, file_path):
try:
self.log(f"开始加载文档: {file_path}")
self.text_area.delete(1.0, tk.END)
self.inserted_media = []
self.media_tags = {}
self.log("已清空当前内容")
if file_path.lower().endswith('.wordbin'):
self.load_packaged(file_path) # 专门处理二进制文件
elif file_path.lower().endswith('.docx'):
self.load_docx(file_path)
elif file_path.lower().endswith('.doc'):
self.load_doc(file_path)
else: # 文本文件(含媒体标记)
try:
with open(file_path, "r", encoding="utf-8") as file:
content = file.read()
self.log(f"从文本文件读取内容,长度: {len(content)}")
self.text_area.insert(tk.END, content)
self.restore_media_from_text()
except UnicodeDecodeError:
# 如果UTF-8失败,尝试其他编码
with open(file_path, "r", encoding="gbk") as file:
content = file.read()
self.log(f"使用GBK编码读取文件,长度: {len(content)}")
self.text_area.insert(tk.END, content)
self.restore_media_from_text()
self.current_file = file_path
self.root.title(f"Word文档编辑器 - {os.path.basename(file_path)}")
self.status_var.set(f"已打开: {file_path}")
self.unsaved_changes = False
self.log(f"文档加载完成: {file_path}")
except Exception as e:
error_msg = f"读取文件失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def load_docx(self, file_path):
"""加载DOCX并恢复图片到文字行间"""
try:
self.log(f"开始加载DOCX文件: {file_path}")
doc = Document(file_path)
self.text_area.delete(1.0, tk.END)
self.log("已清空当前内容")
# 提取文档中的图片
media_dir = os.path.join(self.temp_media_dir, "docx_media")
os.makedirs(media_dir, exist_ok=True)
extracted_images = []
for rel in doc.part.rels.values():
if "image" in rel.target_ref:
img_part = rel.target_part
img_ext = img_part.content_type.split('/')[-1]
if img_ext == 'jpeg':
img_ext = 'jpg'
img_filename = f"image_{len(extracted_images)}.{img_ext}"
img_path = os.path.join(media_dir, img_filename)
with open(img_path, 'wb') as f:
f.write(img_part._blob)
extracted_images.append(img_path)
self.log(f"提取图片: {img_path}")
# 恢复文本和图片到文字行间
img_idx = 0
for para in doc.paragraphs:
# 先插入段落文本
self.text_area.insert(tk.END, para.text)
# 检查段落中的图片并插入
for run in para.runs:
for rel in run.part.rels.values():
if "image" in rel.target_ref and img_idx < len(extracted_images):
# 在当前光标位置插入图片
self.insert_image_from_path(extracted_images[img_idx])
img_idx += 1
self.text_area.insert(tk.END, "\n")
self.log(f"DOCX加载完成,共提取 {len(extracted_images)} 张图片")
except Exception as e:
error_msg = f"加载DOCX失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.log(error_msg)
def save_packaged(self, file_path=None):
"""使用二进制格式打包保存文档"""
if not file_path:
file_path = filedialog.asksaveasfilename(
defaultextension=".wordbin",
filetypes=[("二进制文档", "*.wordbin")]
)
if not file_path:
self.log("用户取消打包保存")
return
try:
self.log(f"开始二进制打包保存到: {file_path}")
self.status_var.set(f"正在保存二进制文档...")
self.root.update()
# 准备要保存的数据结构
save_data = {
'version': '2.0',
'text_content': self.text_area.get(1.0, tk.END),
'media': [],
'media_tags': self.media_tags,
'settings': {
'font': self.font_var.get(),
'size': self.size_var.get(),
'bold': self.bold_var.get(),
'italic': self.italic_var.get(),
'underline': self.underline_var.get()
}
}
# 创建临时目录用于收集媒体文件
temp_dir = tempfile.mkdtemp(prefix="word_editor_pkg_")
self.log(f"临时打包目录: {temp_dir}")
# 处理媒体文件
for media in self.inserted_media:
try:
# 复制媒体文件到临时目录
media_name = os.path.basename(media['path'])
dest_path = os.path.join(temp_dir, media_name)
shutil.copy2(media['path'], dest_path)
save_data['media'].append({
'id': media['id'],
'type': media['type'],
'name': media_name,
'path': os.path.relpath(dest_path, temp_dir),
'placeholder': media.get('placeholder', '')
})
self.log(f"添加媒体文件: {media_name}")
except Exception as e:
self.log(f"保存媒体文件失败: {media['path']} - {str(e)}")
# 创建zip文件
with zipfile.ZipFile(file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 写入主数据文件 - 使用最高兼容的pickle协议
binary_data = pickle.dumps(save_data, protocol=pickle.HIGHEST_PROTOCOL)
zipf.writestr('data.pkl', binary_data)
self.log(f"数据文件已写入,大小: {len(binary_data)} 字节")
# 写入媒体文件
for root, _, files in os.walk(temp_dir):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, temp_dir)
zipf.write(file_path, arcname)
self.log(f"添加文件到包: {arcname}")
self.current_file = file_path
self.root.title(f"Word文档编辑器 - {os.path.basename(file_path)}")
self.status_var.set(f"已保存二进制文档: {os.path.basename(file_path)}")
self.unsaved_changes = False
self.log(f"二进制打包保存完成: {file_path}")
except Exception as e:
error_msg = f"二进制打包保存失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
self.log(traceback.format_exc())
finally:
# 清理临时目录
try:
shutil.rmtree(temp_dir)
self.log(f"临时目录已清理: {temp_dir}")
except Exception as e:
self.log(f"清理临时目录失败: {str(e)}")
def load_packaged(self, file_path):
"""加载二进制打包文档"""
try:
self.log(f"开始加载二进制文档: {file_path}")
self.status_var.set(f"正在加载二进制文档...")
self.root.update()
# 创建临时解压目录
extract_dir = tempfile.mkdtemp(prefix="word_editor_extract_")
self.log(f"临时解压目录: {extract_dir}")
try:
# 解压zip文件
with zipfile.ZipFile(file_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
self.log(f"解压完成,共 {len(zip_ref.filelist)} 个文件")
# 检查数据文件是否存在
data_path = os.path.join(extract_dir, 'data.pkl')
if not os.path.exists(data_path):
raise FileNotFoundError(f"未找到数据文件: {data_path}")
# 读取主数据文件
self.log(f"读取数据文件: {data_path}")
with open(data_path, 'rb') as f:
# 添加协议版本检查
save_data = pickle.load(f)
self.log(f"数据加载成功,版本: {save_data.get('version', '1.0')}")
# 清空编辑器
self.text_area.delete(1.0, tk.END)
self.inserted_media = []
self.media_tags = {}
self.log("编辑器已清空")
# 恢复媒体文件信息
# 恢复媒体文件信息 - 修改这部分
for media_info in save_data.get('media', []):
try:
# 处理路径中的特殊字符和中文
media_path = os.path.join(extract_dir, media_info['path'].encode('utf-8').decode('utf-8'))
media_path = os.path.normpath(media_path) # 规范化路径
if os.path.exists(media_path):
# 复制到更安全的临时位置
safe_temp_dir = os.path.join(self.temp_media_dir, "extracted_media")
os.makedirs(safe_temp_dir, exist_ok=True)
# 使用原始文件名但确保安全
safe_filename = "".join(
c for c in media_info['name'] if c.isalnum() or c in (' ', '.', '_'))
dest_path = os.path.join(safe_temp_dir, safe_filename)
shutil.copy2(media_path, dest_path)
# 更新媒体信息使用新路径
media_type = media_info.get('type', 'image')
if media_type == 'image':
self.restore_image(media_info['id'], dest_path)
elif media_type == 'audio':
self.restore_audio(media_info['id'], dest_path)
elif media_type == 'video':
self.restore_video(media_info['id'], dest_path)
self.log(f"成功恢复媒体: {media_info['name']}")
else:
self.log(f"媒体文件不存在: {media_path}")
except Exception as e:
self.log(f"恢复媒体失败: {str(e)}")
continue
# 恢复文本内容
text_content = save_data.get('text_content', '')
self.text_area.insert(tk.END, text_content)
self.log(f"文本内容已恢复,长度: {len(text_content)}")
# 恢复媒体标记
self.media_tags = save_data.get('media_tags', {})
self.log(f"恢复媒体标记,共 {len(self.media_tags)} 个")
# 恢复媒体到文本中
self.restore_media_from_text()
self.log("媒体内容已恢复到文本")
# 恢复设置
if 'settings' in save_data:
settings = save_data['settings']
self.font_var.set(settings.get('font', 'Arial'))
self.size_var.set(settings.get('size', 12))
self.bold_var.set(settings.get('bold', False))
self.italic_var.set(settings.get('italic', False))
self.underline_var.set(settings.get('underline', False))
self.change_font()
self.log("编辑器设置已恢复")
self.current_file = file_path
self.root.title(f"Word文档编辑器 - {os.path.basename(file_path)}")
self.status_var.set(f"已加载二进制文档: {os.path.basename(file_path)}")
self.unsaved_changes = False
self.log(f"二进制文档加载完成: {file_path}")
finally:
# 清理临时目录
try:
shutil.rmtree(extract_dir)
self.log(f"临时目录已清理: {extract_dir}")
except Exception as e:
self.log(f"清理临时目录失败: {str(e)}")
except zipfile.BadZipFile:
error_msg = "文件不是有效的ZIP格式"
messagebox.showerror("错误", error_msg)
self.log(error_msg)
except pickle.UnpicklingError:
error_msg = "文件不是有效的二进制格式"
messagebox.showerror("错误", error_msg)
self.log(error_msg)
except Exception as e:
error_msg = f"加载二进制文档失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.log(error_msg)
self.log(traceback.format_exc()) # 记录完整的错误堆栈
# 加载失败时显示默认文本
self.text_area.delete(1.0, tk.END)
self.text_area.insert(tk.END, self.default_text)
def get_text_with_media_markers(self):
"""生成带媒体标记的文本内容"""
content = self.text_area.get(1.0, tk.END)
# 按位置排序所有媒体标记
media_positions = []
for media_id, (start, end) in self.media_tags.items():
start_idx = self.text_area.index(start)
end_idx = self.text_area.index(end)
media_positions.append((start_idx, end_idx, media_id))
# 按位置排序
media_positions.sort(key=lambda x: x[0])
# 重建文本内容
result = []
last_pos = 0
total_length = len(content)
for start_idx, end_idx, media_id in media_positions:
# 添加前面的文本
if last_pos < start_idx:
result.append(content[last_pos:start_idx])
# 添加媒体标记
result.append(f"[MEDIA:{media_id}]")
last_pos = end_idx
# 添加剩余文本
if last_pos < total_length:
result.append(content[last_pos:total_length])
return ''.join(result)
def save_file(self):
self.log("执行保存文件操作")
if self.current_file:
if self.current_file.lower().endswith('*.wordbin'):
self.save_packaged(self.current_file)
else:
self.save_document(self.current_file)
else:
self.save_as()
def save_as(self):
self.log("执行另存为操作")
file_types = [
("Word文档", "*.docx"),
("二进制文档", "*.wordbin"),
("Word 97-2003文档", "*.doc"),
("RTF文档", "*.rtf"),
("PDF文档", "*.pdf"),
("文本文件", "*.txt"),
("所有文件", "*.*")
]
file_path = filedialog.asksaveasfilename(
defaultextension=".docx",
filetypes=file_types
)
if file_path:
self.log(f"另存为: {file_path}")
if file_path.lower().endswith('.wordpack'):
self.save_packaged(file_path)
else:
self.save_document(file_path)
def save_document(self, file_path):
try:
self.log(f"开始保存文档到: {file_path}")
if file_path.lower().endswith('.docx'):
self.save_as_docx(file_path)
elif file_path.lower().endswith('.doc'):
self.save_as_doc(file_path)
elif file_path.lower().endswith('.pdf'):
self.save_as_pdf(file_path)
elif file_path.lower().endswith('.rtf'):
self.save_as_rtf(file_path)
else: # 文本文件(含媒体标记)
content = self.get_text_with_media_markers() # 带媒体位置标记
with open(file_path, "w", encoding="utf-8") as file:
file.write(content)
self.log(f"已保存文本内容到: {file_path},长度: {len(content)}")
self.current_file = file_path
self.root.title(f"Word文档编辑器 - {os.path.basename(file_path)}")
self.status_var.set(f"已保存: {file_path}")
self.unsaved_changes = False
self.log(f"文档保存完成: {file_path}")
except Exception as e:
error_msg = f"保存文件失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
# ------------------------------
# 核心功能:多媒体插入到文字行间
# ------------------------------
def insert_image(self):
"""选择图片并插入到当前光标位置"""
self.log("执行插入图片操作")
file_path = filedialog.askopenfilename(
filetypes=[("图片文件", "*.jpg *.jpeg *.png *.gif *.bmp"), ("所有文件", "*.*")]
)
if file_path:
self.log(f"选中图片文件: {file_path}")
self.insert_image_from_path(file_path)
def insert_image_from_path(self, file_path):
"""从路径插入图片到当前光标位置"""
try:
# 生成唯一ID
media_id = f"img_{len(self.inserted_media)}_{os.urandom(4).hex()}"
file_name = os.path.basename(file_path)
self.log(f"插入图片: {file_name}, ID: {media_id}")
# 调整图片大小
image = Image.open(file_path)
max_height = int(self.size_var.get() * 2.5) # 基于字号的高度限制
if image.height > max_height:
ratio = max_height / image.height
new_width = int(image.width * ratio)
image = image.resize((new_width, max_height), Image.LANCZOS)
# 转换为Tkinter图片对象
tk_image = ImageTk.PhotoImage(image)
# 获取当前光标位置
cursor_pos = self.text_area.index(tk.INSERT)
self.log(f"图片插入位置: {cursor_pos}")
# 在光标位置插入图片
self.text_area.image_create(cursor_pos, image=tk_image)
# 计算图片的结束位置
end_pos = self.text_area.index(f"{cursor_pos}+1c")
# 记录媒体信息
self.inserted_media.append({
"id": media_id,
"type": "image",
"path": file_path,
"name": file_name,
"image_obj": tk_image # 保存引用
})
# 用tag标记图片位置
self.text_area.tag_add(media_id, cursor_pos, end_pos)
self.media_tags[media_id] = (cursor_pos, end_pos)
self.status_var.set(f"已插入图片: {file_name}")
self.unsaved_changes = True
self.log(f"图片插入完成: {file_name}")
except Exception as e:
error_msg = f"插入图片失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def insert_audio(self):
"""插入音频占位符到文字行间"""
self.log("执行插入音频操作")
file_path = filedialog.askopenfilename(
filetypes=[("音频文件", "*.mp3 *.wav *.ogg"), ("所有文件", "*.*")]
)
if file_path:
try:
media_id = f"audio_{len(self.inserted_media)}_{os.urandom(4).hex()}"
file_name = os.path.basename(file_path)
self.log(f"插入音频: {file_name}, ID: {media_id}")
# 创建音频占位符图标
audio_placeholder = "🔊 " + os.path.splitext(file_name)[0] + " "
# 在光标位置插入占位符
cursor_pos = self.text_area.index(tk.INSERT)
self.log(f"音频插入位置: {cursor_pos}")
self.text_area.insert(cursor_pos, audio_placeholder)
# 计算占位符的结束位置
start_pos = cursor_pos
end_pos = self.text_area.index(f"{cursor_pos}+{len(audio_placeholder)}c")
# 用tag标记占位符位置
self.text_area.tag_add(media_id, start_pos, end_pos)
self.media_tags[media_id] = (start_pos, end_pos)
# 设置tag的样式
self.text_area.tag_config(media_id, foreground="#0000FF", underline=True)
# 记录媒体信息
self.inserted_media.append({
"id": media_id,
"type": "audio",
"path": file_path,
"name": file_name,
"placeholder": audio_placeholder
})
self.status_var.set(f"已插入音频: {file_name}")
self.unsaved_changes = True
self.log(f"音频插入完成: {file_name}")
except Exception as e:
error_msg = f"插入音频失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def insert_video(self):
"""插入视频占位符到文字行间"""
self.log("执行插入视频操作")
file_path = filedialog.askopenfilename(
filetypes=[("视频文件", "*.mp4 *.avi *.mov *.mkv"), ("所有文件", "*.*")]
)
if file_path:
try:
media_id = f"video_{len(self.inserted_media)}_{os.urandom(4).hex()}"
file_name = os.path.basename(file_path)
self.log(f"插入视频: {file_name}, ID: {media_id}")
# 创建视频占位符图标
video_placeholder = "📹 " + os.path.splitext(file_name)[0] + " "
# 在光标位置插入占位符
cursor_pos = self.text_area.index(tk.INSERT)
self.log(f"视频插入位置: {cursor_pos}")
self.text_area.insert(cursor_pos, video_placeholder)
# 计算占位符的结束位置
start_pos = cursor_pos
end_pos = self.text_area.index(f"{cursor_pos}+{len(video_placeholder)}c")
# 用tag标记占位符位置
self.text_area.tag_add(media_id, start_pos, end_pos)
self.media_tags[media_id] = (start_pos, end_pos)
# 设置tag的样式
self.text_area.tag_config(media_id, foreground="#FF0000", underline=True)
# 记录媒体信息
self.inserted_media.append({
"id": media_id,
"type": "video",
"path": file_path,
"name": file_name,
"placeholder": video_placeholder
})
self.status_var.set(f"已插入视频: {file_name}")
self.unsaved_changes = True
self.log(f"视频插入完成: {file_name}")
except Exception as e:
error_msg = f"插入视频失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
# ------------------------------
# 多媒体播放控制功能
# ------------------------------
def on_media_click(self, event):
"""点击多媒体占位符时触发操作"""
pos = self.text_area.index(f"@{event.x},{event.y}")
self.log(f"媒体点击位置: {pos}")
# 查找点击位置对应的媒体
for media_id, (start, end) in self.media_tags.items():
if self.text_area.compare(start, "<=", pos) and self.text_area.compare(pos, "<=", end):
# 找到对应的媒体
media = next((m for m in self.inserted_media if m["id"] == media_id), None)
if media:
self.log(f"点击了媒体: {media['name']} ({media['type']})")
if media["type"] == "audio":
self.play_audio(media["path"])
elif media["type"] == "video":
self.play_video(media["path"])
elif media["type"] == "image":
self.show_full_image(media["path"])
break
def play_audio(self, audio_path):
"""播放音频文件"""
try:
self.log(f"播放音频: {audio_path}")
# 停止当前正在播放的音频
if mixer.music.get_busy():
mixer.music.stop()
# 加载并播放新音频
mixer.music.load(audio_path)
mixer.music.play()
self.current_audio = audio_path
self.status_var.set(f"正在播放音频: {os.path.basename(audio_path)}")
except Exception as e:
# 尝试备用播放方法
try:
self.log(f"pygame播放音频失败,尝试系统播放器: {str(e)}")
subprocess.Popen(['start', audio_path], shell=True)
self.status_var.set(f"正在播放音频: {os.path.basename(audio_path)}")
except Exception as e2:
error_msg = f"播放音频失败: {str(e2)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def play_video(self, video_path):
"""播放视频文件"""
try:
self.log(f"播放视频: {video_path}")
# 尝试用系统默认播放器打开
if os.name == 'nt': # Windows系统
os.startfile(video_path)
elif os.name == 'posix': # macOS或Linux
subprocess.call(['open' if sys.platform == 'darwin' else 'xdg-open', video_path])
self.status_var.set(f"正在播放视频: {os.path.basename(video_path)}")
except Exception as e:
error_msg = f"播放视频失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def stop_media(self):
"""停止当前播放的媒体"""
self.log("停止媒体播放")
if mixer.music.get_busy():
mixer.music.stop()
self.status_var.set("音频已停止")
self.current_audio = None
def pause_media(self):
"""暂停当前播放的媒体"""
self.log("暂停媒体播放")
if mixer.music.get_busy():
mixer.music.pause()
self.status_var.set("音频已暂停")
def resume_media(self):
"""继续播放已暂停的媒体"""
self.log("继续媒体播放")
if not mixer.music.get_busy() and self.current_audio:
mixer.music.unpause()
self.status_var.set(f"继续播放音频: {os.path.basename(self.current_audio)}")
def restore_media_from_text(self):
"""从文本中的[MEDIA:ID]标记恢复多媒体到文字行间"""
try:
self.log("开始从文本中恢复媒体")
content = self.text_area.get(1.0, tk.END)
self.log(f"获取文本内容用于媒体恢复,长度: {len(content)}")
# 清除当前内容但保留媒体信息
self.text_area.delete(1.0, tk.END)
self.status_var.set(f"开始恢复媒体内容,文本长度: {len(content)}")
self.root.update()
current_pos = 0
media_restored = 0
media_tags_count = 0
while True:
# 查找媒体标记
tag_start = content.find("[MEDIA:", current_pos)
if tag_start == -1:
# 插入剩余文本
remaining_text = content[current_pos:]
self.text_area.insert(tk.END, remaining_text)
self.log(f"插入剩余文本,长度: {len(remaining_text)}")
break
# 插入标记前的文本
text_before = content[current_pos:tag_start]
self.text_area.insert(tk.END, text_before)
self.log(f"插入标记前文本,长度: {len(text_before)}")
# 解析媒体ID
tag_end = content.find("]", tag_start)
if tag_end == -1:
self.log("未找到媒体标记的结束符],停止恢复")
break
media_id = content[tag_start + 7: tag_end].strip()
media_tags_count += 1
self.log(f"找到媒体标记 #{media_tags_count}: {media_id}")
# 查找媒体信息并恢复
media = next((m for m in self.inserted_media if m["id"] == media_id), None)
if media:
media_restored += 1
if media_restored % 5 == 0:
self.status_var.set(f"已恢复 {media_restored} 个媒体")
self.root.update()
if media["type"] == "image":
self.insert_image_from_path(media["path"])
elif media["type"] == "audio":
self.text_area.insert(tk.END, media["placeholder"])
# 更新tag位置
start = self.text_area.index(tk.END + f"-{len(media['placeholder'])}c")
end = self.text_area.index(tk.END)
self.media_tags[media_id] = (start, end)
self.text_area.tag_add(media_id, start, end)
self.text_area.tag_config(media_id, foreground="#0000FF", underline=True)
elif media["type"] == "video":
self.text_area.insert(tk.END, media["placeholder"])
# 更新tag位置
start = self.text_area.index(tk.END + f"-{len(media['placeholder'])}c")
end = self.text_area.index(tk.END)
self.media_tags[media_id] = (start, end)
self.text_area.tag_add(media_id, start, end)
self.text_area.tag_config(media_id, foreground="#FF0000", underline=True)
else:
self.log(f"未找到媒体信息: {media_id},插入原始标记")
self.text_area.insert(tk.END, f"[MEDIA:{media_id}]")
current_pos = tag_end + 1
self.log(f"媒体恢复完成,共找到 {media_tags_count} 个媒体标记,成功恢复 {media_restored} 个媒体")
self.status_var.set(f"媒体恢复完成,共恢复 {media_restored} 个媒体")
except Exception as e:
error_msg = f"恢复媒体内容失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def restore_image(self, media_id, path):
"""从路径恢复图片到文字行间"""
try:
# 确保路径存在
if not os.path.exists(path):
raise FileNotFoundError(f"图片文件不存在: {path}")
self.log(f"恢复图片: {path}, ID: {media_id}")
image = Image.open(path)
# 转换路径为绝对路径并标准化
abs_path = os.path.abspath(path)
abs_path = abs_path.replace("\\", "/") # 统一使用正斜杠
# 记录媒体信息时使用绝对路径
file_name = os.path.basename(path)
tk_image = ImageTk.PhotoImage(image)
self.inserted_media.append({
"id": media_id,
"type": "image",
"path": abs_path, # 使用绝对路径
"name": file_name,
"image_obj": tk_image
})
self.log(f"图片恢复完成: {file_name}")
except Exception as e:
error_msg = f"恢复图片失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
def restore_audio(self, media_id, path):
"""恢复音频占位符"""
file_name = os.path.basename(path)
self.log(f"恢复音频: {file_name}, ID: {media_id}")
placeholder = "🔊 " + os.path.splitext(file_name)[0] + " "
self.inserted_media.append({
"id": media_id,
"type": "audio",
"path": path,
"name": file_name,
"placeholder": placeholder
})
self.log(f"音频恢复完成: {file_name}")
def restore_video(self, media_id, path):
"""恢复视频占位符"""
file_name = os.path.basename(path)
self.log(f"恢复视频: {file_name}, ID: {media_id}")
placeholder = "📹 " + os.path.splitext(file_name)[0] + " "
self.inserted_media.append({
"id": media_id,
"type": "video",
"path": path,
"name": file_name,
"placeholder": placeholder
})
self.log(f"视频恢复完成: {file_name}")
def show_full_image(self, image_path):
"""显示完整图片"""
try:
self.log(f"显示完整图片: {image_path}")
img_window = tk.Toplevel(self.root)
img_window.title("图片预览")
image = Image.open(image_path)
# 限制最大尺寸
max_width = self.root.winfo_screenwidth() - 100
max_height = self.root.winfo_screenheight() - 100
# 计算缩放比例
width_ratio = max_width / image.width
height_ratio = max_height / image.height
ratio = min(width_ratio, height_ratio, 1.0) # 不放大图片
if ratio < 1.0:
new_width = int(image.width * ratio)
new_height = int(image.height * ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
photo = ImageTk.PhotoImage(image)
# 添加滚动条支持
canvas = tk.Canvas(img_window, width=image.width, height=image.height)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar_y = tk.Scrollbar(img_window, orient=tk.VERTICAL, command=canvas.yview)
scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
scrollbar_x = tk.Scrollbar(img_window, orient=tk.HORIZONTAL, command=canvas.xview)
scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
canvas.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
# 放置图片
canvas.create_image(0, 0, anchor=tk.NW, image=photo)
canvas.image = photo # 保持引用
# 设置滚动区域
canvas.config(scrollregion=canvas.bbox("all"))
img_window.geometry(f"{image.width}x{image.height}")
self.log(f"图片预览窗口已创建")
except Exception as e:
error_msg = f"无法显示图片: {str(e)}"
messagebox.showerror("错误", error_msg)
self.status_var.set(error_msg)
self.log(error_msg)
# ------------------------------
# 删除功能
# ------------------------------
def delete_selection(self):
"""删除选中的内容(文本或媒体)"""
self.log("执行删除选中内容操作")
try:
# 检查是否有选中内容
selected_text = self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST)
if selected_text:
# 先尝试删除任何被选中的媒体
deleted_media = self.delete_media_in_selection()
# 如果没有删除媒体,删除文本
if not deleted_media:
self.text_area.delete(tk.SEL_FIRST, tk.SEL_LAST)
self.unsaved_changes = True
self.status_var.set("已删除选中内容")
except tk.TclError:
# 没有选中内容,不执行任何操作
pass
def delete_selected_media(self):
"""删除当前选中的媒体(如果有)"""
self.log("执行删除选中媒体操作")
try:
# 获取选中区域
start = self.text_area.index(tk.SEL_FIRST)
end = self.text_area.index(tk.SEL_LAST)
# 查找选中区域内的媒体
deleted = self.delete_media_in_range(start, end)
if deleted:
self.unsaved_changes = True
self.status_var.set(f"已删除 {deleted} 个媒体")
else:
messagebox.showinfo("提示", "未选中任何媒体")
except tk.TclError:
# 没有选中内容
messagebox.showinfo("提示", "请先选中要删除的媒体")
def delete_media_in_selection(self):
"""删除选中区域内的所有媒体"""
try:
start = self.text_area.index(tk.SEL_FIRST)
end = self.text_area.index(tk.SEL_LAST)
return self.delete_media_in_range(start, end)
except tk.TclError:
return 0
def delete_media_in_range(self, start, end):
"""删除指定范围内的所有媒体"""
deleted_count = 0
media_to_delete = []
# 找出范围内的所有媒体
for media_id, (media_start, media_end) in self.media_tags.items():
# 检查媒体是否与选择范围重叠
if (self.text_area.compare(media_start, "<=", end) and
self.text_area.compare(media_end, ">=", start)):
media_to_delete.append(media_id)
# 删除找到的媒体
for media_id in media_to_delete:
# 获取媒体位置
media_start, media_end = self.media_tags[media_id]
# 删除文本区域中的媒体
self.text_area.delete(media_start, media_end)
# 从媒体列表中移除
self.inserted_media = [m for m in self.inserted_media if m["id"] != media_id]
# 从媒体标记中移除
del self.media_tags[media_id]
deleted_count += 1
self.log(f"已删除媒体: {media_id}")
return deleted_count
# ------------------------------
# 其他基础功能
# ------------------------------
def close_file(self):
self.log("执行关闭文件操作")
if self.check_save():
self.text_area.delete(1.0, tk.END)
self.current_file = None
self.inserted_media = []
self.media_tags = {}
# 关闭文件后显示初始文本
self.text_area.insert(tk.END, self.default_text)
self.status_var.set("就绪")
self.root.title("Word文档编辑器")
self.unsaved_changes = False
self.log("文件已关闭,显示默认文本")
def on_closing(self):
self.log("执行程序关闭操作")
if self.check_save():
if mixer.get_init():
mixer.stop()
mixer.quit()
pygame.quit()
self.root.destroy()
self.log("程序已退出")
def check_save(self):
self.log("检查是否需要保存")
if self.unsaved_changes or self.text_area.edit_modified():
response = messagebox.askyesnocancel(
"保存文档",
"文档已修改,是否保存更改?"
)
if response is None:
self.log("用户取消关闭操作")
return False
if response:
self.log("用户选择保存更改")
self.save_file()
return True
self.log("无需保存")
return True
def print_document(self):
self.log("执行打印操作")
messagebox.showinfo("打印", "文档已发送到打印机")
def undo(self):
self.log("执行撤销操作")
try:
self.text_area.edit_undo()
self.unsaved_changes = True
except:
pass
def redo(self):
self.log("执行重做操作")
try:
self.text_area.edit_redo()
self.unsaved_changes = True
except:
pass
def cut(self):
self.log("执行剪切操作")
self.text_area.event_generate("<<Cut>>")
self.unsaved_changes = True
def copy(self):
self.log("执行复制操作")
self.text_area.event_generate("<<Copy>>")
def paste(self):
self.log("执行粘贴操作")
self.text_area.event_generate("<<Paste>>")
self.unsaved_changes = True
def select_all(self):
self.log("执行全选操作")
self.text_area.tag_add(tk.SEL, "1.0", tk.END)
self.text_area.mark_set(tk.INSERT, "1.0")
self.text_area.see(tk.INSERT)
return "break"
def find_text(self):
self.log("执行查找操作")
find_dialog = tk.Toplevel(self.root)
find_dialog.title("查找")
find_dialog.geometry("400x200")
find_dialog.transient(self.root)
find_dialog.grab_set()
tk.Label(find_dialog, text="查找内容:").pack(pady=(10, 0), padx=10, anchor=tk.W)
find_entry = tk.Entry(find_dialog, width=40)
find_entry.pack(padx=10, pady=5)
find_entry.focus_set()
def find():
text = find_entry.get()
self.log(f"查找内容: {text}")
if text:
start = self.text_area.search(text, "1.0", stopindex=tk.END)
if start:
end = f"{start}+{len(text)}c"
self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
self.text_area.tag_add(tk.SEL, start, end)
self.text_area.mark_set(tk.INSERT, end)
self.text_area.see(start)
else:
messagebox.showinfo("查找", "找不到该内容。")
tk.Button(find_dialog, text="查找下一个", command=find).pack(pady=10)
def replace_text(self):
self.log("执行替换操作")
replace_dialog = tk.Toplevel(self.root)
replace_dialog.title("替换")
replace_dialog.geometry("400x250")
replace_dialog.transient(self.root)
replace_dialog.grab_set()
tk.Label(replace_dialog, text="查找内容:").pack(pady=(10, 0), padx=10, anchor=tk.W)
find_entry = tk.Entry(replace_dialog, width=40)
find_entry.pack(padx=10, pady=5)
tk.Label(replace_dialog, text="替换为:").pack(pady=(5, 0), padx=10, anchor=tk.W)
replace_entry = tk.Entry(replace_dialog, width=40)
replace_entry.pack(padx=10, pady=5)
def find_next():
text = find_entry.get()
self.log(f"查找替换内容: {text}")
if text:
start = self.text_area.search(text, "1.0", stopindex=tk.END)
if start:
end = f"{start}+{len(text)}c"
self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
self.text_area.tag_add(tk.SEL, start, end)
self.text_area.mark_set(tk.INSERT, end)
self.text_area.see(start)
self.text_area.see(start)
else:
messagebox.showinfo("替换", "找不到该内容。")
def replace():
text = find_entry.get()
replacement = replace_entry.get()
self.log(f"替换 '{text}' 为 '{replacement}'")
if self.text_area.tag_ranges(tk.SEL):
sel_text = self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST)
if sel_text == text:
self.text_area.delete(tk.SEL_FIRST, tk.SEL_LAST)
self.text_area.insert(tk.SEL_FIRST, replacement)
self.unsaved_changes = True
find_next()
def replace_all():
count = 0
text = find_entry.get()
replacement = replace_entry.get()
self.log(f"全部替换 '{text}' 为 '{replacement}'")
if text:
start = "1.0"
while True:
start = self.text_area.search(text, start, stopindex=tk.END)
if not start:
break
end = f"{start}+{len(text)}c"
self.text_area.delete(start, end)
self.text_area.insert(start, replacement)
start = end
count += 1
self.unsaved_changes = True
messagebox.showinfo("替换", f"已替换 {count} 处。")
self.log(f"完成全部替换,共替换 {count} 处")
btn_frame = tk.Frame(replace_dialog)
btn_frame.pack(pady=10)
tk.Button(btn_frame, text="查找下一个", command=find_next).pack(side=tk.LEFT, padx=5)
tk.Button(btn_frame, text="替换", command=replace).pack(side=tk.LEFT, padx=5)
tk.Button(btn_frame, text="全部替换", command=replace_all).pack(side=tk.LEFT, padx=5)
def insert_table(self):
self.log("插入表格")
self.text_area.insert(tk.INSERT, "\n[表格]\n")
self.status_var.set("已插入表格")
self.unsaved_changes = True
def insert_hyperlink(self):
self.log("插入超链接")
self.text_area.insert(tk.INSERT, "[超链接]")
self.status_var.set("已插入超链接")
self.unsaved_changes = True
def change_font(self, event=None):
font = self.font_var.get()
self.log(f"更改字体为: {font}")
try:
self.text_area.config(font=(font, self.size_var.get()))
self.unsaved_changes = True
except:
pass
def change_font_size(self, event=None):
size = self.size_var.get()
self.log(f"更改字号为: {size}")
try:
self.text_area.config(font=(self.font_var.get(), size))
self.unsaved_changes = True
except:
pass
def toggle_bold(self):
state = "启用" if self.bold_var.get() else "禁用"
self.log(f"粗体{state}")
current_font = self.text_area.cget("font")
font_family = current_font.split()[0] if current_font else "Arial"
font_size = self.size_var.get()
if self.bold_var.get():
self.text_area.config(font=(font_family, font_size, "bold"))
else:
self.text_area.config(font=(font_family, font_size))
self.unsaved_changes = True
def toggle_italic(self):
state = "启用" if self.italic_var.get() else "禁用"
self.log(f"斜体{state}")
current_font = self.text_area.cget("font")
font_family = current_font.split()[0] if current_font else "Arial"
font_size = self.size_var.get()
if self.italic_var.get():
self.text_area.config(font=(font_family, font_size, "italic"))
else:
self.text_area.config(font=(font_family, font_size))
self.unsaved_changes = True
def toggle_underline(self):
state = "启用" if self.underline_var.get() else "禁用"
self.log(f"下划线{state}")
current_font = self.text_area.cget("font")
font_family = current_font.split()[0] if current_font else "Arial"
font_size = self.size_var.get()
if self.underline_var.get():
self.text_area.config(font=(font_family, font_size, "underline"))
else:
self.text_area.config(font=(font_family, font_size))
self.unsaved_changes = True
def set_alignment(self, align):
self.log(f"设置对齐方式: {align}")
self.status_var.set(f"已设置对齐方式: {align}")
self.unsaved_changes = True
def set_theme(self, event=None):
theme = self.theme_var.get()
self.log(f"设置主题: {theme}")
if theme == "light":
bg_color = "#ffffff"
fg_color = "#000000"
toolbar_bg = "#e0e0e0"
status_bg = "#e0e0e0"
text_bg = "#ffffff"
else:
bg_color = "#2d2d2d"
fg_color = "#ffffff"
toolbar_bg = "#3d3d3d"
status_bg = "#3d3d3d"
text_bg = "#1e1e1e"
self.root.config(bg=bg_color)
for widget in self.root.winfo_children():
if isinstance(widget, tk.Frame) and widget.winfo_name() == "!frame":
widget.config(bg=toolbar_bg)
for child in widget.winfo_children():
try:
child.config(bg=toolbar_bg, fg=fg_color)
except:
pass
self.text_area.config(bg=text_bg, fg=fg_color, insertbackground=fg_color)
self.status_bar.config(bg=status_bg, fg=fg_color)
def toggle_statusbar(self):
if self.status_bar.winfo_ismapped():
self.status_bar.pack_forget()
self.log("隐藏状态栏")
else:
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
self.log("显示状态栏")
def show_about(self):
self.log("显示关于对话框")
about_text = (
"Word文档编辑器(多媒体增强版)\n"
"版本 2.3\n\n"
"支持多媒体与文字混排,可在文字行间自由插入图片、音频、视频。\n"
"使用说明:\n"
"- 插入多媒体后,可直接在周围输入文字,多媒体会自动调整位置\n"
"- 点击音频/视频可播放,点击图片可查看大图\n"
"- 使用媒体菜单可控制播放、暂停和停止\n"
"- 推荐使用「打包保存」确保多媒体不丢失\n"
"- 选中内容或媒体后按Delete键可删除\n\n"
"© 2023 Python Tkinter 项目"
)
messagebox.showinfo("关于", about_text)
def view_log(self):
"""查看日志文件"""
self.log("查看日志文件")
try:
if os.path.exists(self.log_file):
if os.name == 'nt': # Windows
os.startfile(self.log_file)
elif os.name == 'posix': # macOS/Linux
subprocess.call(['open' if sys.platform == 'darwin' else 'xdg-open', self.log_file])
self.status_var.set(f"已打开日志文件: {os.path.basename(self.log_file)}")
else:
messagebox.showinfo("日志", "日志文件不存在")
except Exception as e:
error_msg = f"打开日志文件失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.log(error_msg)
def on_text_modified(self, event):
if self.text_area.edit_modified():
self.unsaved_changes = True
title = self.root.title()
if not title.startswith("*"):
self.root.title(f"*{title}")
self.text_area.edit_modified(False)
self.log("文本内容已修改")
def update_status(self, event=None):
index = self.text_area.index(tk.INSERT)
line, column = index.split('.')
char_count = len(self.text_area.get(1.0, tk.END)) - 1 # 减去最后的换行符
self.status_var.set(f"行: {line}, 列: {column} | 字数: {char_count} | 日志: {os.path.basename(self.log_file)}")
# 以下方法为占位符,实际实现可能需要额外库支持
def load_doc(self, file_path):
self.log(f"加载DOC文件: {file_path}")
messagebox.showinfo("提示", "加载DOC文件功能需要额外支持,这里仅显示文本内容")
try:
pythoncom.CoInitialize()
word = win32.Dispatch("Word.Application")
doc = word.Documents.Open(file_path)
content = doc.Content.Text
self.text_area.insert(tk.END, content)
doc.Close()
word.Quit()
self.log(f"DOC文件文本内容已加载,长度: {len(content)}")
except Exception as e:
error_msg = f"加载DOC文件失败: {str(e)}"
messagebox.showerror("错误", error_msg)
self.log(error_msg)
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
self.text_area.insert(tk.END, content)
self.log(f"尝试以文本方式加载DOC文件,长度: {len(content)}")
def save_as_docx(self, file_path):
self.log(f"保存为DOCX: {file_path}")
doc = Document()
full_text = self.text_area.get(1.0, tk.END)
doc.add_paragraph(full_text)
doc.save(file_path)
self.log(f"DOCX保存完成")
def save_as_doc(self, file_path):
self.log(f"保存为DOC: {file_path}")
messagebox.showinfo("提示", "保存为DOC格式功能受限,已保存为DOCX格式")
if not file_path.endswith('.docx'):
file_path = file_path + '.docx'
self.save_as_docx(file_path)
def save_as_pdf(self, file_path):
self.log(f"保存为PDF: {file_path}")
messagebox.showinfo("提示", "PDF保存功能需要额外支持")
# 实际实现需要安装pywin32并与Word交互
def save_as_rtf(self, file_path):
self.log(f"保存为RTF: {file_path}")
with open(file_path, "w", encoding="utf-8") as f:
f.write(self.get_text_with_media_markers())
self.log(f"RTF保存完成")
if __name__ == "__main__":
root = tk.Tk()
app = WordEditor(root)
root.mainloop()
08-06
494
