Files
yidaima_tools/editor_gui.py
王鹏 a2f5875d1b init
2026-04-09 14:55:54 +08:00

665 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
微信 Markdown 编辑器 GUI 模块
提供可视化编辑界面
"""
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, scrolledtext, colorchooser
import tempfile
import webbrowser
import os
# 使用 tkinterweb 进行 HTML 预览(支持完整 HTML 渲染)
from tkinterweb import HtmlFrame
from markdown_editor import (
WechatMarkdownEditor, ThemeConfig, FontConfig,
CodeBlockConfig, EditorConfig
)
class MarkdownEditorWindow:
"""Markdown 编辑器窗口"""
def __init__(self, parent=None):
self.editor = WechatMarkdownEditor()
self.config = self.editor.get_config()
# 创建窗口
if parent:
self.window = tk.Toplevel(parent)
else:
self.window = tk.Tk()
self.window.title("微信 Markdown 编辑器")
self.window.geometry("1400x900")
self.window.minsize(1200, 700)
# 设置样式
self.style = ttk.Style()
self.style.configure("Title.TLabel", font=("Microsoft YaHei", 14, "bold"))
self.style.configure("Subtitle.TLabel", font=("Microsoft YaHei", 10))
self.create_widgets()
self.update_preview()
def create_widgets(self):
"""创建界面组件"""
# 主框架 - 使用 PanedWindow 实现可调整的分割
main_paned = ttk.PanedWindow(self.window, orient=tk.HORIZONTAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# === 左侧:编辑区 ===
left_frame = ttk.Frame(main_paned)
main_paned.add(left_frame, weight=1)
# 编辑区标题
ttk.Label(left_frame, text="Markdown 编辑区", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 5))
# Markdown 编辑框
self.md_text = scrolledtext.ScrolledText(
left_frame,
wrap=tk.WORD,
font=("Consolas", 12),
undo=True,
maxundo=-1
)
self.md_text.pack(fill=tk.BOTH, expand=True)
self.md_text.bind("<KeyRelease>", lambda e: self.update_preview())
# 默认内容
default_content = """# 欢迎使用微信 Markdown 编辑器
这是一段示例文本,支持 **粗体**、*斜体*、`行内代码`。
## 代码示例
```python
def hello_world():
print("Hello, World!")
return True
```
## 列表
- 项目 1
- 项目 2
- 项目 3
## 引用
> 这是一段引用文本。
## 表格
| 列1 | 列2 | 列3 |
|-----|-----|-----|
| A | B | C |
| D | E | F |
---
在右侧设置面板中可以调整主题、字体、代码块样式等。
"""
self.md_text.insert("1.0", default_content)
# === 中间:预览区 ===
center_frame = ttk.Frame(main_paned)
main_paned.add(center_frame, weight=1)
# 预览区标题和工具栏
preview_header = ttk.Frame(center_frame)
preview_header.pack(fill=tk.X, pady=(0, 5))
ttk.Label(preview_header, text="实时预览", style="Title.TLabel").pack(side=tk.LEFT)
# 刷新按钮
ttk.Button(preview_header, text="🔄 刷新", command=self.update_preview).pack(side=tk.RIGHT, padx=5)
ttk.Button(preview_header, text="📋 复制 HTML", command=self.copy_html).pack(side=tk.RIGHT, padx=5)
ttk.Button(preview_header, text="💾 导出 HTML", command=self.export_html).pack(side=tk.RIGHT, padx=5)
# 预览区域
preview_notebook = ttk.Notebook(center_frame)
preview_notebook.pack(fill=tk.BOTH, expand=True)
# HTML 渲染预览标签页
preview_frame = ttk.Frame(preview_notebook)
preview_notebook.add(preview_frame, text="效果预览")
# 使用 HtmlFrame 显示渲染后的 HTML
self.html_preview = HtmlFrame(preview_frame, messages_enabled=False)
self.html_preview.pack(fill=tk.BOTH, expand=True)
# HTML 源码标签页
html_frame = ttk.Frame(preview_notebook)
preview_notebook.add(html_frame, text="HTML 源码")
self.html_text = scrolledtext.ScrolledText(
html_frame,
wrap=tk.NONE,
font=("Consolas", 10),
state=tk.DISABLED
)
self.html_text.pack(fill=tk.BOTH, expand=True)
# 浏览器预览标签页
browser_frame = ttk.Frame(preview_notebook)
preview_notebook.add(browser_frame, text="浏览器预览")
ttk.Label(browser_frame, text="点击按钮在浏览器中预览效果:").pack(pady=20)
ttk.Button(browser_frame, text="🌐 在浏览器中打开", command=self.open_in_browser).pack(pady=10)
# === 右侧:设置面板 ===
right_frame = ttk.Frame(main_paned, width=350)
main_paned.add(right_frame, weight=0)
right_frame.pack_propagate(False)
# 设置面板标题
ttk.Label(right_frame, text="设置面板", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 10))
# 创建设置区域的 Canvas 和 Scrollbar
settings_canvas = tk.Canvas(right_frame, highlightthickness=0)
scrollbar = ttk.Scrollbar(right_frame, orient=tk.VERTICAL, command=settings_canvas.yview)
settings_canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
settings_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 设置内容框架
self.settings_frame = ttk.Frame(settings_canvas)
settings_canvas.create_window((0, 0), window=self.settings_frame, anchor=tk.NW, width=330)
# 绑定滚动
self.settings_frame.bind("<Configure>", lambda e: settings_canvas.configure(scrollregion=settings_canvas.bbox("all")))
settings_canvas.bind("<Configure>", lambda e: settings_canvas.itemconfig(settings_canvas.find_all()[0], width=e.width-5))
# 创建设置区域
self.create_theme_settings()
self.create_font_settings()
self.create_code_block_settings()
self.create_custom_css_settings()
def create_theme_settings(self):
"""创建主题设置区域"""
theme_frame = ttk.LabelFrame(self.settings_frame, text="主题设置", padding=10)
theme_frame.pack(fill=tk.X, pady=(0, 10))
# 主题选择
ttk.Label(theme_frame, text="选择主题:").pack(anchor=tk.W)
self.theme_var = tk.StringVar(value=self.config.theme)
themes = self.editor.theme_manager.get_all_themes()
theme_names = [f"{t.id}:{t.name}" for t in themes]
self.theme_combo = ttk.Combobox(
theme_frame,
values=theme_names,
textvariable=self.theme_var,
state="readonly"
)
self.theme_combo.pack(fill=tk.X, pady=(5, 10))
self.theme_combo.bind("<<ComboboxSelected>>", self.on_theme_change)
# 主题色
color_frame = ttk.Frame(theme_frame)
color_frame.pack(fill=tk.X, pady=5)
ttk.Label(color_frame, text="主题色:").pack(side=tk.LEFT)
self.primary_color_var = tk.StringVar(value=self.editor.theme_manager.get_current_theme().primary_color)
self.color_btn = tk.Button(
color_frame,
text=" ",
bg=self.primary_color_var.get(),
width=3,
command=self.choose_primary_color
)
self.color_btn.pack(side=tk.LEFT, padx=5)
ttk.Entry(color_frame, textvariable=self.primary_color_var, width=10).pack(side=tk.LEFT)
ttk.Button(color_frame, text="应用", command=self.apply_primary_color).pack(side=tk.LEFT, padx=5)
def create_font_settings(self):
"""创建字体设置区域"""
font_frame = ttk.LabelFrame(self.settings_frame, text="字体设置", padding=10)
font_frame.pack(fill=tk.X, pady=(0, 10))
# 正文字体
ttk.Label(font_frame, text="正文字体:").pack(anchor=tk.W)
self.body_font_var = tk.StringVar(value=self.config.font.body_font)
ttk.Entry(font_frame, textvariable=self.body_font_var).pack(fill=tk.X, pady=(0, 5))
# 正文字号
size_frame = ttk.Frame(font_frame)
size_frame.pack(fill=tk.X, pady=5)
ttk.Label(size_frame, text="正文字号:").pack(side=tk.LEFT)
self.body_size_var = tk.IntVar(value=self.config.font.body_size)
ttk.Spinbox(size_frame, from_=12, to=24, textvariable=self.body_size_var, width=5).pack(side=tk.LEFT, padx=5)
ttk.Label(size_frame, text="px").pack(side=tk.LEFT)
# 代码字体
ttk.Label(font_frame, text="代码字体:").pack(anchor=tk.W, pady=(10, 0))
self.code_font_var = tk.StringVar(value=self.config.font.code_font)
ttk.Entry(font_frame, textvariable=self.code_font_var).pack(fill=tk.X, pady=(0, 5))
# 行高
lh_frame = ttk.Frame(font_frame)
lh_frame.pack(fill=tk.X, pady=5)
ttk.Label(lh_frame, text="行高:").pack(side=tk.LEFT)
self.line_height_var = tk.DoubleVar(value=self.config.font.line_height)
ttk.Spinbox(lh_frame, from_=1.0, to=2.5, increment=0.1, textvariable=self.line_height_var, width=5).pack(side=tk.LEFT, padx=5)
# 应用按钮
ttk.Button(font_frame, text="应用字体设置", command=self.apply_font_settings).pack(fill=tk.X, pady=(10, 0))
def create_code_block_settings(self):
"""创建代码块设置区域"""
code_frame = ttk.LabelFrame(self.settings_frame, text="代码块设置", padding=10)
code_frame.pack(fill=tk.X, pady=(0, 10))
# 代码主题
ttk.Label(code_frame, text="代码高亮主题:").pack(anchor=tk.W)
self.code_theme_var = tk.StringVar(value=self.config.code_block.theme)
code_themes = self.config.code_block.available_themes
self.code_theme_combo = ttk.Combobox(
code_frame,
values=sorted(code_themes),
textvariable=self.code_theme_var,
state="readonly"
)
self.code_theme_combo.pack(fill=tk.X, pady=(5, 10))
self.code_theme_combo.bind("<<ComboboxSelected>>", lambda e: self.apply_code_settings())
# 代码块背景色
bg_frame = ttk.Frame(code_frame)
bg_frame.pack(fill=tk.X, pady=5)
ttk.Label(bg_frame, text="代码块背景:").pack(side=tk.LEFT)
self.code_bg_var = tk.StringVar(value=self.config.code_block.background)
self.code_bg_btn = tk.Button(
bg_frame,
text=" ",
bg=self.code_bg_var.get(),
width=3,
command=self.choose_code_bg
)
self.code_bg_btn.pack(side=tk.LEFT, padx=5)
ttk.Entry(bg_frame, textvariable=self.code_bg_var, width=10).pack(side=tk.LEFT)
def create_custom_css_settings(self):
"""创建自定义 CSS 设置区域"""
css_frame = ttk.LabelFrame(self.settings_frame, text="自定义 CSS", padding=10)
css_frame.pack(fill=tk.X, pady=(0, 10))
# 已保存的方案选择
scheme_frame = ttk.Frame(css_frame)
scheme_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(scheme_frame, text="已保存的方案:").pack(side=tk.LEFT)
self.scheme_var = tk.StringVar()
self.scheme_combo = ttk.Combobox(
scheme_frame,
values=self._get_scheme_names(),
textvariable=self.scheme_var,
state="readonly",
width=20
)
self.scheme_combo.pack(side=tk.LEFT, padx=5)
ttk.Button(scheme_frame, text="加载", command=self.load_css_scheme).pack(side=tk.LEFT, padx=2)
ttk.Button(scheme_frame, text="删除", command=self.delete_css_scheme).pack(side=tk.LEFT, padx=2)
# CSS 编辑区
self.css_text = scrolledtext.ScrolledText(
css_frame,
wrap=tk.WORD,
font=("Consolas", 9),
height=8
)
self.css_text.pack(fill=tk.BOTH, expand=True)
self.css_text.insert("1.0", self.config.custom_css)
# 保存方案区域
save_frame = ttk.Frame(css_frame)
save_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Label(save_frame, text="方案名称:").pack(side=tk.LEFT)
self.scheme_name_var = tk.StringVar()
ttk.Entry(save_frame, textvariable=self.scheme_name_var, width=20).pack(side=tk.LEFT, padx=5)
ttk.Button(save_frame, text="💾 保存方案", command=self.save_css_scheme).pack(side=tk.LEFT, padx=5)
# CSS 按钮
btn_frame = ttk.Frame(css_frame)
btn_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Button(btn_frame, text="应用 CSS", command=self.apply_custom_css).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(btn_frame, text="导出 CSS", command=self.export_css).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="重置", command=self.reset_custom_css).pack(side=tk.LEFT, padx=5)
def _get_scheme_names(self):
"""获取方案名称列表(只显示名称)"""
schemes = self.editor.theme_manager.get_all_css_schemes()
return [s.name for s in schemes]
def on_theme_change(self, event=None):
"""主题变更处理"""
theme_id = self.theme_var.get().split(":")[0]
self.editor.theme_manager.set_theme(theme_id)
# 更新主题色显示
theme = self.editor.theme_manager.get_current_theme()
self.primary_color_var.set(theme.primary_color)
self.color_btn.config(bg=theme.primary_color)
self.update_preview()
def choose_primary_color(self):
"""选择主题色"""
color = colorchooser.askcolor(initialcolor=self.primary_color_var.get())
if color[1]:
self.primary_color_var.set(color[1])
self.color_btn.config(bg=color[1])
def apply_primary_color(self):
"""应用主题色"""
# 创建自定义主题
current_theme = self.editor.theme_manager.get_current_theme()
custom_theme = ThemeConfig(
id=f"custom_{current_theme.id}",
name=f"自定义 - {current_theme.name}",
primary_color=self.primary_color_var.get(),
background=current_theme.background,
text_color=current_theme.text_color,
link_color=current_theme.link_color,
code_bg=current_theme.code_bg
)
self.editor.theme_manager.add_custom_theme(custom_theme)
self.editor.theme_manager.set_theme(custom_theme.id)
self.update_preview()
messagebox.showinfo("成功", "主题色已应用!")
def apply_font_settings(self):
"""应用字体设置"""
self.editor.update_font_config(
body_font=self.body_font_var.get(),
code_font=self.code_font_var.get(),
body_size=self.body_size_var.get(),
line_height=self.line_height_var.get()
)
self.update_preview()
messagebox.showinfo("成功", "字体设置已应用!")
def choose_code_bg(self):
"""选择代码块背景色"""
color = colorchooser.askcolor(initialcolor=self.code_bg_var.get())
if color[1]:
self.code_bg_var.set(color[1])
self.apply_code_settings()
def apply_code_settings(self):
"""应用代码块设置"""
self.editor.update_code_block_config(
theme=self.code_theme_var.get(),
background=self.code_bg_var.get()
)
self.update_preview()
def apply_custom_css(self):
"""应用自定义 CSS"""
css = self.css_text.get("1.0", tk.END).strip()
self.editor.update_custom_css(css)
self.update_preview()
messagebox.showinfo("成功", "自定义 CSS 已应用!")
def reset_custom_css(self):
"""重置自定义 CSS"""
if messagebox.askyesno("确认", "确定要清空自定义 CSS 吗?"):
self.css_text.delete("1.0", tk.END)
self.editor.update_custom_css("")
self.update_preview()
def export_css(self):
"""导出 CSS 文件"""
file_path = filedialog.asksaveasfilename(
defaultextension=".css",
filetypes=[("CSS 文件", "*.css"), ("所有文件", "*.*")]
)
if file_path:
self.editor.export_css(file_path)
messagebox.showinfo("成功", f"CSS 已导出到:\n{file_path}")
def save_css_scheme(self):
"""保存 CSS 方案(同名更新,新名添加)"""
name = self.scheme_name_var.get().strip()
if not name:
messagebox.showwarning("提示", "请输入方案名称")
return
css = self.css_text.get("1.0", tk.END).strip()
# 收集当前字体和代码块配置
font_config = {
"body_font": self.body_font_var.get(),
"code_font": self.code_font_var.get(),
"body_size": self.body_size_var.get(),
"line_height": self.line_height_var.get()
}
code_block_config = {
"theme": self.code_theme_var.get(),
"background": self.code_bg_var.get()
}
# 获取当前主题和主题色
theme_id = self.theme_var.get().split(":")[0] if ":" in self.theme_var.get() else self.theme_var.get()
primary_color = self.primary_color_var.get()
# 检查是否已存在同名方案
existing_scheme = None
for scheme in self.editor.theme_manager.get_all_css_schemes():
if scheme.name == name:
existing_scheme = scheme
break
from markdown_editor import CssScheme
if existing_scheme:
# 更新现有方案
if messagebox.askyesno("确认", f"方案 '{name}' 已存在,是否更新?"):
# 删除旧方案,添加新方案(保留原 ID
self.editor.theme_manager.remove_css_scheme(existing_scheme.id)
updated_scheme = CssScheme(
id=existing_scheme.id,
name=name,
css=css,
font_config=font_config,
code_block_config=code_block_config,
theme_id=theme_id,
primary_color=primary_color
)
self.editor.theme_manager.add_css_scheme(updated_scheme)
messagebox.showinfo("成功", f"方案 '{name}' 已更新!")
else:
return
else:
# 添加新方案
import uuid
scheme_id = str(uuid.uuid4())[:8]
scheme = CssScheme(
id=scheme_id,
name=name,
css=css,
font_config=font_config,
code_block_config=code_block_config,
theme_id=theme_id,
primary_color=primary_color
)
self.editor.theme_manager.add_css_scheme(scheme)
messagebox.showinfo("成功", f"方案 '{name}' 已保存!")
# 更新下拉列表
self.scheme_combo['values'] = self._get_scheme_names()
self.scheme_name_var.set("")
def load_css_scheme(self):
"""加载 CSS 方案"""
scheme_name = self.scheme_var.get()
if not scheme_name:
messagebox.showwarning("提示", "请选择一个方案")
return
# 根据名称查找方案
scheme = None
for s in self.editor.theme_manager.get_all_css_schemes():
if s.name == scheme_name:
scheme = s
break
if not scheme:
messagebox.showerror("错误", "方案不存在")
return
# 应用 CSS
self.css_text.delete("1.0", tk.END)
self.css_text.insert("1.0", scheme.css)
# 应用字体配置
if scheme.font_config:
if "body_font" in scheme.font_config:
self.body_font_var.set(scheme.font_config["body_font"])
if "code_font" in scheme.font_config:
self.code_font_var.set(scheme.font_config["code_font"])
if "body_size" in scheme.font_config:
self.body_size_var.set(scheme.font_config["body_size"])
if "line_height" in scheme.font_config:
self.line_height_var.set(scheme.font_config["line_height"])
# 应用代码块配置
if scheme.code_block_config:
if "theme" in scheme.code_block_config:
self.code_theme_var.set(scheme.code_block_config["theme"])
if "background" in scheme.code_block_config:
self.code_bg_var.set(scheme.code_block_config["background"])
self.code_bg_btn.config(bg=scheme.code_block_config["background"])
# 应用主题和主题色
if scheme.theme_id:
self.theme_var.set(scheme.theme_id)
self.editor.theme_manager.set_theme(scheme.theme_id)
if scheme.primary_color:
self.primary_color_var.set(scheme.primary_color)
self.color_btn.config(bg=scheme.primary_color)
# 应用所有配置
self.apply_custom_css()
self.apply_font_settings()
self.apply_code_settings()
# 将方案名称写入方案名称框
self.scheme_name_var.set(scheme.name)
messagebox.showinfo("成功", f"方案 '{scheme.name}' 已加载!")
def delete_css_scheme(self):
"""删除 CSS 方案"""
scheme_name = self.scheme_var.get()
if not scheme_name:
messagebox.showwarning("提示", "请选择一个方案")
return
# 根据名称查找方案
scheme = None
for s in self.editor.theme_manager.get_all_css_schemes():
if s.name == scheme_name:
scheme = s
break
if not scheme:
messagebox.showerror("错误", "方案不存在")
return
if messagebox.askyesno("确认", f"确定要删除方案 '{scheme.name}' 吗?"):
self.editor.theme_manager.remove_css_scheme(scheme.id)
self.scheme_combo['values'] = self._get_scheme_names()
self.scheme_var.set("")
messagebox.showinfo("成功", "方案已删除!")
def update_preview(self):
"""更新预览"""
md_content = self.md_text.get("1.0", tk.END)
html = self.editor.render(md_content)
# 更新 HTML 源码显示
self.html_text.config(state=tk.NORMAL)
self.html_text.delete("1.0", tk.END)
self.html_text.insert("1.0", html)
self.html_text.config(state=tk.DISABLED)
# 更新 HTML 渲染预览
try:
# 清理 HTML 中的图片 URL移除反引号和多余空格
import re
cleaned_html = html
# 移除 src 属性中的反引号
cleaned_html = re.sub(r'src="\s*`\s*([^"`]+)\s*`\s*"', r'src="\1"', cleaned_html)
cleaned_html = re.sub(r"src='\s*`\s*([^'`]+)\s*`'", r'src="\1"', cleaned_html)
# 移除 src 属性值前后的空格
cleaned_html = re.sub(r'src="\s*([^"]+)\s*"', r'src="\1"', cleaned_html)
# 使用 tkinterweb 的 HtmlFrame 显示预览
# 创建临时文件并加载,以便图片能正确显示
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
f.write(cleaned_html)
temp_path = f.name
self.html_preview.load_file(temp_path)
except Exception as e:
print(f"[Editor] 预览更新失败: {e}")
def copy_html(self):
"""复制 HTML 到剪贴板"""
html = self.html_text.get("1.0", tk.END)
self.window.clipboard_clear()
self.window.clipboard_append(html)
messagebox.showinfo("成功", "HTML 已复制到剪贴板!")
def export_html(self):
"""导出 HTML 文件"""
file_path = filedialog.asksaveasfilename(
defaultextension=".html",
filetypes=[("HTML 文件", "*.html"), ("所有文件", "*.*")]
)
if file_path:
md_content = self.md_text.get("1.0", tk.END)
self.editor.export_html(md_content, file_path)
messagebox.showinfo("成功", f"HTML 已导出到:\n{file_path}")
def open_in_browser(self):
"""在浏览器中打开预览"""
md_content = self.md_text.get("1.0", tk.END)
html = self.editor.render(md_content)
# 创建临时文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
f.write(html)
temp_path = f.name
# 在浏览器中打开
webbrowser.open(f"file://{temp_path}")
def run(self):
"""运行编辑器"""
if not hasattr(self.window, '_is_main'):
self.window.mainloop()
def open_editor(parent=None):
"""打开 Markdown 编辑器窗口"""
editor = MarkdownEditorWindow(parent)
return editor
if __name__ == "__main__":
editor = MarkdownEditorWindow()
editor.run()