乐于分享
好东西不私藏

用 Python 打造专业医疗病例编辑器:Markdown 实时预览 + 一键导出

用 Python 打造专业医疗病例编辑器:Markdown 实时预览 + 一键导出

用Python打造专业医疗病例编辑器:Markdown实时预览+一键导出PDF/DOCX

在医疗信息化快速发展的今天,电子病历已经成为医院日常工作的核心组成部分。然而,很多基层医疗机构和诊所仍然在使用传统的Word文档编辑病例,不仅格式难以统一,每次调整都需要花费大量时间,而且导出的PDF经常出现字体错乱、排版偏移等问题。特别是对于肠胃炎、感冒等常见疾病的标准化病例,医生们每天都要重复填写相似的内容,效率极其低下。

有没有一种方法,既能让医生用最简单的方式编写病例,又能保证输出格式的专业性和一致性?答案是肯定的。今天我就给大家分享一个我用Python开发的专业医疗病例编辑器,它采用Markdown作为编辑语言,支持实时可视化预览,一键导出符合医疗规范的PDF和DOCX文档,还可以自由拖拽调整医院LOGO和医疗专用章的位置。整个界面采用现代化设计,美观大方,操作简单,即使是不懂编程的医生也能快速上手。

这个编辑器解决了传统病例编辑的三大痛点:一是格式统一问题,所有输出都严格遵循医疗文书规范;二是效率问题,Markdown语法简单易学,配合预设模板可以大幅缩短病例书写时间;三是兼容性问题,导出的PDF和DOCX在任何设备上打开都能保持一致的排版效果。下面我就带大家深入了解这个工具的实现原理和核心代码。

一、界面架构:左右分栏+动态纸张,告别内容截断

整个编辑器采用经典的左右分栏布局,左侧是Markdown编辑区,右侧是实时预览区。这种设计让用户在编写内容的同时就能看到最终的输出效果,实现了”所见即所得”。

1.1 主界面布局设计

我使用Tkinter的Frame组件构建了三层嵌套的界面结构:

  • 最外层是主容器,采用浅灰色背景,营造出专业、简洁的视觉效果
  • 左侧是白色背景的控制区,分为上下两部分:上方是Markdown编辑器,下方是功能按钮区
  • 右侧是预览区,包含一个Canvas画布和垂直滚动条,用于显示模拟的A4纸张效果
# 主容器
self.main_container = tk.Frame(self.root, bg="#f5f7fa")
self.main_container.pack(fill=tk.BOTH, expand=True)

# 左侧控制区
self.left_frame = tk.Frame(self.main_container, width=400, bg="#ffffff")
self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=10, pady=10)
self.left_frame.pack_propagate(False)

# 右侧预览区
self.preview_frame = tk.Frame(self.main_container, bg="#e5e7eb")
self.preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)

1.2 动态纸张高度实现

这是解决之前内容截断问题的关键。原代码使用固定的纸张高度,当内容超过这个高度时就会被截断。我改进了渲染逻辑,在绘制内容之前先计算所有内容的总高度,然后动态调整纸张高度:

# 先计算所有内容的总高度,动态调整纸张高度
elements = self.parse_markdown(self.md_content)
temp_y = content_y

for elem_type, elem_content in elements:
if elem_type == 'h1':
        temp_y += 60
elif elem_type == 'h2':
        temp_y += 50
elif elem_type == 'h3':
        temp_y += 35
elif elem_type == 'hr':
        temp_y += 30
elif elem_type == 'p':
# 估算段落高度
        lines = (len(elem_content) // (content_width // self.normal_font.measure('一'))) + 1
        temp_y += lines * 24 + 6
elif elem_type == 'list':
for item in elem_content:
            lines = (len(item) // ((content_width - 24) // self.normal_font.measure('一'))) + 1
            temp_y += lines * 24
        temp_y += 10

# 动态设置纸张高度,确保能容纳所有内容
self.paper_height = max(self.base_paper_height, temp_y + self.paper_margin + 100)

这样无论内容多长,纸张都会自动延伸,再也不会出现内容被截断的情况。同时,滚动区域也会根据纸张高度自动调整,确保用户可以滚动查看所有内容。

二、富文本渲染:精确计算+智能换行,解决文字重叠

文字渲染是整个编辑器最核心的部分,也是最容易出问题的地方。原代码通过在画布上创建临时文本对象来计算宽度,这会产生大量不可见的垃圾对象,导致计算不准确和性能问题。我重写了整个渲染引擎,使用tkinter.font.Font对象精确计算文本宽度。

2.1 字体对象预初始化

在程序启动时,我就预创建了所有需要的字体对象,这样可以避免在渲染过程中反复创建和销毁字体,提高性能:

# 初始化字体对象(用于精确计算文本宽度)
self.normal_font = Font(family="SimSun", size=12)
self.bold_font = Font(family="SimSun", size=12, weight="bold")
self.h1_font = Font(family="SimSun", size=22, weight="bold")
self.h2_font = Font(family="SimSun", size=20, weight="bold")
self.h3_font = Font(family="SimSun", size=16, weight="bold")

2.2 智能富文本渲染引擎

我实现了一个通用的draw_rich_text方法,它可以同时处理普通文本和加粗文本,并且支持在文本内部自动换行:

defdraw_rich_text(self, text, x, y, max_width, line_height=24):
"""
    绘制支持加粗的富文本,自动换行
    返回绘制完成后的y坐标
    """

ifnot text:
return y

# 解析加粗标记
    parts = []
    tokens = text.split('**')
    is_bold = False

for token in tokens:
if token:
            parts.append((token, is_bold))
        is_bold = not is_bold

    current_x = x
    current_y = y

for content, bold in parts:
ifnot content:
continue

        font = self.bold_font if bold else self.normal_font
        chars = list(content)
        line_buffer = ""

for char in chars:
            test_line = line_buffer + char
            text_width = font.measure(test_line)

if current_x + text_width > x + max_width:
# 绘制当前行
if line_buffer:
                    self.canvas.create_text(
                        current_x, 
                        current_y, 
                        text=line_buffer, 
                        font=font, 
                        anchor=tk.W
                    )
# 换行
                current_y += line_height
                current_x = x
                line_buffer = char
else:
                line_buffer = test_line

# 绘制剩余内容
if line_buffer:
            text_width = font.measure(line_buffer)
            self.canvas.create_text(
                current_x, 
                current_y, 
                text=line_buffer, 
                font=font, 
                anchor=tk.W
            )
            current_x += text_width

# 返回下一行的y坐标
return current_y + line_height

这个方法的工作原理是:

  1. 首先解析文本中的**标记,将文本分割成普通部分和加粗部分
  2. 然后逐字符处理每个部分,计算当前行的宽度
  3. 当行宽超过最大宽度时,自动换行并继续绘制
  4. 最后返回绘制完成后的y坐标,方便后续内容的绘制

三、多格式导出:兼容性处理+错误修复,确保导出成功

导出功能是医疗病例编辑器最重要的功能之一,因为医生需要将病例保存为PDF或DOCX格式存档。原代码在导出时存在两个严重的问题:PDF字体注册错误和DOCX负数间距错误。我对这两个问题进行了彻底的修复。

3.1 PDF字体注册修复

原代码在注册SimSun-Bold字体时没有指定subfontIndex参数,而simsun.ttc是一个字体集合文件,包含多个子字体。正确的注册方式是:

# 注册中文字体(reportlab用)- 修复字体注册问题
try:
# 宋体常规体
    pdfmetrics.registerFont(TTFont('SimSun', resource_path('simsun.ttc'), subfontIndex=0))
# 宋体加粗体(simsun.ttc的第二个子字体)
    pdfmetrics.registerFont(TTFont('SimSun-Bold', resource_path('simsun.ttc'), subfontIndex=1))
except Exception as e:
    print(f"字体注册警告: {e},将使用系统默认字体")

同时,我还添加了完善的异常处理,在字体加载失败时自动使用系统默认字体并给出提示,避免程序崩溃。

3.2 DOCX负数间距错误修复

这是用户遇到的最严重的错误,错误信息是”value must be in range 0 to 18446744073709551615 inclusive, got -1143000″。问题的原因是python-docx库不支持负数的段前间距。原代码中:

# 错误的写法
p.paragraph_format.space_before = Pt(-90)

我将其改为在文档末尾添加两个空行后再插入印章,这样既保证了印章位置合理,又避免了格式错误:

# 添加印章 - 修复负数间距问题
if self.seal_visible and self.seal_image:
    temp_seal = "temp_seal.png"
    self.seal_image.save(temp_seal)

# 在文档末尾添加两个空行,然后插入印章
    doc.add_paragraph('')
    doc.add_paragraph('')

    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    run = p.add_run()
    run.add_picture(temp_seal, width=Inches(self.seal_size[0]/96), height=Inches(self.seal_size[1]/96))

    os.remove(temp_seal)

完整代码实现

import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
from tkinter.font import Font
from PIL import Image, ImageTk, ImageDraw
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_PARAGRAPH_ALIGNMENT
from docx.enum.style import WD_STYLE_TYPE
import os
import sys
import markdown
from markdown.extensions import fenced_code, tables

# 解决打包后字体路径问题
defresource_path(relative_path):
"""获取资源绝对路径"""
try:
        base_path = sys._MEIPASS
except Exception:
        base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)

classMedicalCaseEditor:
def__init__(self, root):
        self.root = root
        self.root.title("肠胃炎病例编辑器")
        self.root.geometry("1600x900")
        self.root.minsize(1400800)

# 注册中文字体(reportlab用)- 修复字体注册问题
try:
# 宋体常规体
            pdfmetrics.registerFont(TTFont('SimSun', resource_path('simsun.ttc'), subfontIndex=0))
# 宋体加粗体(simsun.ttc的第二个子字体)
            pdfmetrics.registerFont(TTFont('SimSun-Bold', resource_path('simsun.ttc'), subfontIndex=1))
except Exception as e:
            print(f"字体注册警告: {e},将使用系统默认字体")

# 初始化字体对象(用于精确计算文本宽度)
        self.normal_font = Font(family="SimSun", size=12)
        self.bold_font = Font(family="SimSun", size=12, weight="bold")
        self.h1_font = Font(family="SimSun", size=22, weight="bold")
        self.h2_font = Font(family="SimSun", size=20, weight="bold")
        self.h3_font = Font(family="SimSun", size=16, weight="bold")

# 初始化变量
        self.logo_image = None
        self.logo_tk = None
        self.logo_pos = (5050)
        self.logo_size = (8080)
        self.logo_visible = False

        self.seal_image = None
        self.seal_tk = None
        self.seal_pos = (6501050)
        self.seal_size = (120120)
        self.seal_visible = False

        self.dragging = None# 'logo', 'seal', 'logo_resize', 'seal_resize'
        self.drag_offset = (00)

# 纸张参数
        self.paper_width = 850
        self.base_paper_height = 1250# 基础纸张高度
        self.paper_margin = 60

# MD内容变量
        self.md_content = self.get_demo_md()

# 创建界面
        self.create_widgets()
        self.draw_case()

defget_demo_md(self):
"""返回肠胃炎病例demo样例"""
return"""# 伊春林业中心医院

## 肠胃炎病例

**编号:CY20260513001**

---

**病历号:G2026051326196**

**姓名:** 学在坚持    **性别:** 男    **年龄:** 19岁    **民族:** 汉族  
**婚否:** 未婚    **职业:** 学生    **就诊时间:** 2026年5月13日 11时20分  
**病史陈述者:** 患者本人(可靠)

---

### 主诉
腹痛、腹泻伴恶心、呕吐2天,加重3小时。

### 现病史
患者2天前无明显诱因出现腹部阵发性绞痛,以脐周为主,疼痛程度中等,可忍受,随后出现腹泻症状,每日排便3-8次,粪便为黄色稀水样便,无脓血、黏液,无里急后重感。同时伴随恶心,间断出现呕吐,呕吐物为胃内容物,非喷射性,无咖啡样物质。

发病以来,患者自觉乏力、食欲减退,偶有腹胀、反酸,无发热、寒战,无头晕、头痛,无胸闷、心悸。3小时前上述症状加重,腹泻频次增至每日10余次,呕吐频繁,无法正常进食水,为求进一步诊治,遂来我院就诊,门诊以"急性肠胃炎"收入院。

患者自发病以来,精神状态欠佳,睡眠较差,小便量偏少,体重无明显变化。

### 既往史
既往体健,否认高血压、糖尿病、冠心病等慢性病史,否认肝炎、结核等传染病史,否认重大外伤、手术史,否认输血史,否认食物、药物过敏史,预防接种史随当地计划进行。

### 个人史
生于原籍,无长期外地旅居史,无疫区、疫水接触史。生活作息规律,无吸烟、饮酒史,无特殊饮食偏好,发病前1天曾进食生冷、不洁食物。婚育史无特殊,月经史(女性):经期规律,经量正常,无痛经。

### 家族史
父母及直系亲属体健,否认家族性遗传病史、传染病史。

### 体格检查
体温:36.8℃,脉搏:92次/分,呼吸:20次/分,血压:115/75mmHg。

**一般情况:** 神志清楚,精神萎靡,急性病容,体型中等,步入病房,查体合作。全身皮肤黏膜无黄染、皮疹及出血点,浅表淋巴结未触及肿大。

**头颅五官:** 头颅无畸形,眼睑无水肿,结膜无充血,巩膜无黄染,双侧瞳孔等大等圆,对光反射灵敏。耳鼻咽喉未见异常。

**颈部:** 颈软,无抵抗,颈静脉无怒张,甲状腺未触及肿大。

**胸部:** 胸廓对称,双侧呼吸动度一致,双肺呼吸音清晰,未闻及干湿性啰音。

**心脏:** 心前区无隆起,心率92次/分,律齐,各瓣膜听诊区未闻及病理性杂音。

**腹部:** 腹部平软,脐周及上腹部有轻度压痛,无反跳痛及肌紧张,肝脾肋下未触及,墨菲氏征阴性,移动性浊音阴性,肠鸣音活跃,每分钟8-10次。

**四肢:** 四肢活动自如,无水肿,生理反射存在,病理反射未引出。

### 辅助检查
1. 血常规:白细胞计数10.5×10⁹/L,中性粒细胞百分比72%,淋巴细胞百分比24%,血红蛋白125g/L,血小板计数220×10⁹/L。
2. 粪便常规:外观黄色稀水样便,镜检未见白细胞、红细胞、脓细胞,潜血试验阴性。
3. 电解质:血钾3.2mmol/L,血钠135mmol/L,血氯98mmol/L,提示轻度低钾血症。

### 初步诊断
急性肠胃炎

### 诊断依据
1. 患者以腹痛、腹泻、恶心、呕吐为主要临床表现,发病前有不洁饮食史;
2. 体格检查示脐周及上腹部压痛,肠鸣音活跃;
3. 血常规提示轻度炎症反应,粪便常规无明显细菌感染征象,电解质提示低钾血症。

### 鉴别诊断
1. **急性阑尾炎:** 典型表现为转移性右下腹痛,麦氏点压痛、反跳痛明显,血常规白细胞及中性粒细胞显著升高,本患者症状、体征不符,可排除。
2. **细菌性痢疾:** 多有脓血便、里急后重,粪便镜检可见大量白细胞、脓细胞及红细胞,与本患者粪便检查结果不符,予以排除。
3. **消化性溃疡:** 多为慢性、周期性、节律性上腹痛,无明显腹泻症状,胃镜检查可明确鉴别,本患者无相关病史,暂不考虑。

### 诊疗计划
1. 完善相关检查:行肝肾功能、淀粉酶、腹部超声等检查,进一步排除其他腹部疾病;
2. 药物治疗:给予抑酸护胃、止吐、止泻药物,静脉补液纠正水电解质紊乱,补钾治疗;
3. 饮食指导:暂禁食或给予流质清淡饮食,避免生冷、油腻、辛辣刺激性食物;
4. 密切观察患者腹痛、腹泻、呕吐症状变化,监测生命体征及电解质情况,根据病情调整治疗方案。

---

**医师签名:** _________________  
**日期:** 2026年5月13日

---

*注:(1) 需加盖我院"医疗专用章"方可生效;(2) 本文件仅作医疗记录使用,复印无效*
"""


defcreate_widgets(self):
# 主容器
        self.main_container = tk.Frame(self.root, bg="#f5f7fa")
        self.main_container.pack(fill=tk.BOTH, expand=True)

# 左侧控制区(分为上下两部分:MD编辑区和功能按钮区)
        self.left_frame = tk.Frame(self.main_container, width=400, bg="#ffffff")
        self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=10, pady=10)
        self.left_frame.pack_propagate(False)

# MD编辑区标题
        md_title_frame = tk.Frame(self.left_frame, bg="#ffffff")
        md_title_frame.pack(fill=tk.X, pady=(05))

        tk.Label(
            md_title_frame, 
            text="Markdown 编辑器"
            font=("Microsoft YaHei"14"bold"), 
            bg="#ffffff",
            fg="#1f2937"
        ).pack(side=tk.LEFT, padx=10)

# MD操作按钮
        btn_frame = tk.Frame(md_title_frame, bg="#ffffff")
        btn_frame.pack(side=tk.RIGHT, padx=5)

        self.import_md_btn = tk.Button(
            btn_frame, 
            text="导入MD"
            command=self.import_md, 
            bg="#165DFF",
            fg="white",
            font=("Microsoft YaHei"9),
            relief=tk.FLAT,
            cursor="hand2",
            width=6
        )
        self.import_md_btn.pack(side=tk.LEFT, padx=2)

        self.export_md_btn = tk.Button(
            btn_frame, 
            text="导出MD"
            command=self.export_md, 
            bg="#165DFF",
            fg="white",
            font=("Microsoft YaHei"9),
            relief=tk.FLAT,
            cursor="hand2",
            width=6
        )
        self.export_md_btn.pack(side=tk.LEFT, padx=2)

        self.load_demo_btn = tk.Button(
            btn_frame, 
            text="加载Demo"
            command=self.load_demo, 
            bg="#f59e0b",
            fg="white",
            font=("Microsoft YaHei"9),
            relief=tk.FLAT,
            cursor="hand2",
            width=7
        )
        self.load_demo_btn.pack(side=tk.LEFT, padx=2)

# MD文本编辑框
        self.md_text = scrolledtext.ScrolledText(
            self.left_frame, 
            font=("Consolas"11),
            wrap=tk.WORD,
            bg="#f8fafc",
            fg="#1e293b",
            insertbackground="#165DFF",
            padx=10,
            pady=10
        )
        self.md_text.pack(fill=tk.BOTH, expand=True, pady=(010))
        self.md_text.insert(tk.END, self.md_content)

# 绑定文本变化事件,实时预览
        self.md_text.bind("<KeyRelease>", self.on_md_change)

# 功能按钮区
        self.control_frame = tk.Frame(self.left_frame, bg="#ffffff", padx=15, pady=15)
        self.control_frame.pack(fill=tk.X)

# 分为两列布局
        self.control_frame.columnconfigure(0, weight=1)
        self.control_frame.columnconfigure(1, weight=1)

# LOGO控制组
        logo_group = tk.LabelFrame(
            self.control_frame, 
            text="医院LOGO"
            font=("Microsoft YaHei"11"bold"),
            bg="#ffffff",
            fg="#4b5563",
            padx=10,
            pady=10
        )
        logo_group.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")

        self.upload_logo_btn = tk.Button(
            logo_group, 
            text="上传LOGO"
            command=self.upload_logo, 
            width=12,
            height=1,
            bg="#165DFF",
            fg="white",
            font=("Microsoft YaHei"9),
            relief=tk.FLAT,
            cursor="hand2"
        )
        self.upload_logo_btn.pack(pady=(05))

        self.remove_logo_btn = tk.Button(
            logo_group, 
            text="移除LOGO"
            command=self.remove_logo, 
            width=12,
            height=1,
            bg="#f3f4f6",
            fg="#4b5563",
            font=("Microsoft YaHei"9),
            relief=tk.FLAT,
            cursor="hand2",
            state=tk.DISABLED
        )
        self.remove_logo_btn.pack()

# 印章控制组
        seal_group = tk.LabelFrame(
            self.control_frame, 
            text="医疗专用章"
            font=("Microsoft YaHei"11"bold"),
            bg="#ffffff",
            fg="#4b5563",
            padx=10,
            pady=10
        )
        seal_group.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")

        self.upload_seal_btn = tk.Button(
            seal_group, 
            text="上传印章"
            command=self.upload_seal, 
            width=12,
            height=1,
            bg="#165DFF",
            fg="white",
            font=("Microsoft YaHei"9),
            relief=tk.FLAT,
            cursor="hand2"
        )
        self.upload_seal_btn.pack(pady=(05))

        self.remove_seal_btn = tk.Button(
            seal_group, 
            text="移除印章"
            command=self.remove_seal, 
            width=12,
            height=1,
            bg="#f3f4f6",
            fg="#4b5563",
            font=("Microsoft YaHei"9),
            relief=tk.FLAT,
            cursor="hand2",
            state=tk.DISABLED
        )
        self.remove_seal_btn.pack()

# 导出控制组
        export_group = tk.LabelFrame(
            self.control_frame, 
            text="导出功能"
            font=("Microsoft YaHei"11"bold"),
            bg="#ffffff",
            fg="#4b5563",
            padx=10,
            pady=10
        )
        export_group.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="nsew")

        export_btn_frame = tk.Frame(export_group, bg="#ffffff")
        export_btn_frame.pack(fill=tk.X)
        export_btn_frame.columnconfigure(0, weight=1)
        export_btn_frame.columnconfigure(1, weight=1)

        self.download_pdf_btn = tk.Button(
            export_btn_frame, 
            text="下载为PDF"
            command=self.download_pdf, 
            bg="#22c55e",
            fg="white",
            font=("Microsoft YaHei"10"bold"),
            relief=tk.FLAT,
            cursor="hand2",
            height=1
        )
        self.download_pdf_btn.grid(row=0, column=0, padx=5, pady=2, sticky="ew")

        self.download_docx_btn = tk.Button(
            export_btn_frame, 
            text="下载为DOCX"
            command=self.download_docx, 
            bg="#3b82f6",
            fg="white",
            font=("Microsoft YaHei"10"bold"),
            relief=tk.FLAT,
            cursor="hand2",
            height=1
        )
        self.download_docx_btn.grid(row=0, column=1, padx=5, pady=2, sticky="ew")

# 右侧预览区
        self.preview_frame = tk.Frame(self.main_container, bg="#e5e7eb")
        self.preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)

# 滚动条
        self.scrollbar = tk.Scrollbar(self.preview_frame, orient=tk.VERTICAL, width=12)
        self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

# 预览画布容器(用于居中显示纸张)
        self.canvas_container = tk.Frame(self.preview_frame, bg="#e5e7eb")
        self.canvas_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

# 预览画布
        self.canvas = tk.Canvas(
            self.canvas_container, 
            bg="#e5e7eb"
            yscrollcommand=self.scrollbar.set,
            highlightthickness=0
        )
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.scrollbar.config(command=self.canvas.yview)

# 绑定鼠标事件
        self.canvas.bind("<Button-1>", self.on_mouse_down)
        self.canvas.bind("<B1-Motion>", self.on_mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_mouse_up)
        self.canvas.bind("<MouseWheel>", self.on_mouse_wheel)

# 绑定窗口大小变化事件
        self.canvas_container.bind("<Configure>", self.on_canvas_resize)

defon_md_change(self, event):
"""MD文本变化时重新渲染"""
        self.md_content = self.md_text.get("1.0", tk.END)
        self.draw_case()

defimport_md(self):
"""导入MD文件"""
        file_path = filedialog.askopenfilename(
            title="选择Markdown文件",
            filetypes=[("Markdown文件""*.md"), ("所有文件""*.*")]
        )
if file_path:
try:
with open(file_path, "r", encoding="utf-8"as f:
                    content = f.read()
                self.md_text.delete("1.0", tk.END)
                self.md_text.insert(tk.END, content)
                self.md_content = content
                self.draw_case()
                messagebox.showinfo("成功""Markdown文件导入成功!")
except Exception as e:
                messagebox.showerror("错误"f"无法导入文件:{str(e)}")

defexport_md(self):
"""导出MD文件"""
        file_path = filedialog.asksaveasfilename(
            defaultextension=".md",
            filetypes=[("Markdown文件""*.md"), ("所有文件""*.*")],
            title="保存Markdown文件"
        )
ifnot file_path:
return

try:
            content = self.md_text.get("1.0", tk.END)
with open(file_path, "w", encoding="utf-8"as f:
                f.write(content)
            messagebox.showinfo("成功""Markdown文件导出成功!")
except Exception as e:
            messagebox.showerror("错误"f"无法导出文件:{str(e)}")

defload_demo(self):
"""加载demo样例"""
        self.md_text.delete("1.0", tk.END)
        self.md_text.insert(tk.END, self.get_demo_md())
        self.md_content = self.get_demo_md()
        self.draw_case()

defon_canvas_resize(self, event):
"""画布大小变化时重新绘制"""
        self.draw_case()

defparse_markdown(self, md_text):
"""解析Markdown文本为结构化数据"""
        lines = md_text.strip().split('\n')
        elements = []
        current_list = None

for line in lines:
            line = line.rstrip()
ifnot line:
if current_list:
                    elements.append(('list', current_list))
                    current_list = None
continue

# 一级标题
if line.startswith('# '):
if current_list:
                    elements.append(('list', current_list))
                    current_list = None
                elements.append(('h1', line[2:].strip()))
# 二级标题
elif line.startswith('## '):
if current_list:
                    elements.append(('list', current_list))
                    current_list = None
                elements.append(('h2', line[3:].strip()))
# 三级标题
elif line.startswith('### '):
if current_list:
                    elements.append(('list', current_list))
                    current_list = None
                elements.append(('h3', line[4:].strip()))
# 分隔线
elif line.startswith('---'or line.startswith('***'):
if current_list:
                    elements.append(('list', current_list))
                    current_list = None
                elements.append(('hr'None))
# 无序列表
elif line.startswith('- 'or line.startswith('* '):
if current_list isNone:
                    current_list = []
                current_list.append(line[2:].strip())
# 有序列表
elif line.strip().startswith(('1.''2.''3.''4.''5.''6.''7.''8.''9.')):
if current_list isNone:
                    current_list = []
# 去掉数字和点
                content = line.strip().split('.'1)[1].strip()
                current_list.append(content)
# 普通段落
else:
if current_list:
                    elements.append(('list', current_list))
                    current_list = None
                elements.append(('p', line.strip()))

# 处理最后一个列表
if current_list:
            elements.append(('list', current_list))

return elements

defdraw_case(self):
"""绘制病例内容 - 修复内容截断问题"""
        self.canvas.delete("all")

# 计算纸张居中位置
        container_width = self.canvas_container.winfo_width()
        container_height = self.canvas_container.winfo_height()

        paper_x = max(50, (container_width - self.paper_width) // 2)
        paper_y = 50

# 内容区域
        content_x = paper_x + self.paper_margin
        content_y = paper_y + self.paper_margin
        content_width = self.paper_width - 2 * self.paper_margin

        y = content_y

# 先计算所有内容的总高度,动态调整纸张高度
        elements = self.parse_markdown(self.md_content)
        temp_y = content_y

for elem_type, elem_content in elements:
if elem_type == 'h1':
                temp_y += 60
elif elem_type == 'h2':
                temp_y += 50
elif elem_type == 'h3':
                temp_y += 35
elif elem_type == 'hr':
                temp_y += 30
elif elem_type == 'p':
# 估算段落高度
                lines = (len(elem_content) // (content_width // self.normal_font.measure('一'))) + 1
                temp_y += lines * 24 + 6
elif elem_type == 'list':
for item in elem_content:
                    lines = (len(item) // ((content_width - 24) // self.normal_font.measure('一'))) + 1
                    temp_y += lines * 24
                temp_y += 10

# 动态设置纸张高度,确保能容纳所有内容
        self.paper_height = max(self.base_paper_height, temp_y + self.paper_margin + 100)

# 绘制纸张阴影
        shadow_offset = 8
        self.canvas.create_rectangle(
            paper_x + shadow_offset, 
            paper_y + shadow_offset, 
            paper_x + self.paper_width + shadow_offset, 
            paper_y + self.paper_height + shadow_offset,
            fill="#d1d5db",
            outline=""
        )

# 绘制纸张
        self.canvas.create_rectangle(
            paper_x, 
            paper_y, 
            paper_x + self.paper_width, 
            paper_y + self.paper_height,
            fill="white",
            outline="#e5e7eb",
            width=1
        )

# 重新渲染所有内容(不再有截断)
        y = content_y

for elem_type, elem_content in elements:
if elem_type == 'h1':
# 医院名称(一级标题)
                self.canvas.create_text(
                    content_x + content_width/2
                    y, 
                    text=elem_content, 
                    font=self.h1_font, 
                    fill="black",
                    anchor=tk.CENTER
                )
                y += 60# 增加一级标题下方间距

elif elem_type == 'h2':
# 病例标题(二级标题)
                self.canvas.create_text(
                    content_x + content_width/2
                    y, 
                    text=elem_content, 
                    font=self.h2_font,
                    anchor=tk.CENTER
                )
                y += 50# 增加二级标题下方间距

elif elem_type == 'h3':
# 节标题(三级标题)
                self.canvas.create_text(
                    content_x, 
                    y, 
                    text=elem_content, 
                    font=self.h3_font, 
                    anchor=tk.W
                )
                y += 35

elif elem_type == 'hr':
# 分隔线
                self.canvas.create_line(content_x, y, content_x + content_width, y, width=2)
                y += 8
                self.canvas.create_line(content_x, y, content_x + content_width, y, width=1)
                y += 30

elif elem_type == 'p':
# 普通段落(支持加粗)
                y = self.draw_rich_text(
                    elem_content,
                    content_x,
                    y,
                    content_width,
                    line_height=24
                )
                y += 6# 段落之间的间距

elif elem_type == 'list':
# 列表
for i, item in enumerate(elem_content):
# 绘制列表符号
                    self.canvas.create_text(
                        content_x + 12
                        y, 
                        text=f"{i+1}."
                        font=self.normal_font, 
                        anchor=tk.E
                    )
# 绘制列表内容(支持加粗)
                    y = self.draw_rich_text(
                        item,
                        content_x + 24,
                        y,
                        content_width - 24,
                        line_height=24
                    )

                y += 10# 列表下方的间距

# 更新滚动区域
        total_height = paper_y + self.paper_height + 100
        self.canvas.config(scrollregion=(00, container_width, total_height))

# 绘制LOGO和印章(转换为纸张相对坐标)
        self.draw_logo(paper_x, paper_y)
        self.draw_seal(paper_x, paper_y)

defdraw_rich_text(self, text, x, y, max_width, line_height=24):
"""
        绘制支持加粗的富文本,自动换行
        返回绘制完成后的y坐标
        """

ifnot text:
return y

# 解析加粗标记
        parts = []
        tokens = text.split('**')
        is_bold = False

for token in tokens:
if token:
                parts.append((token, is_bold))
            is_bold = not is_bold

        current_x = x
        current_y = y

for content, bold in parts:
ifnot content:
continue

            font = self.bold_font if bold else self.normal_font
            chars = list(content)
            line_buffer = ""

for char in chars:
                test_line = line_buffer + char
                text_width = font.measure(test_line)

if current_x + text_width > x + max_width:
# 绘制当前行
if line_buffer:
                        self.canvas.create_text(
                            current_x, 
                            current_y, 
                            text=line_buffer, 
                            font=font, 
                            anchor=tk.W
                        )
# 换行
                    current_y += line_height
                    current_x = x
                    line_buffer = char
else:
                    line_buffer = test_line

# 绘制剩余内容
if line_buffer:
                text_width = font.measure(line_buffer)
                self.canvas.create_text(
                    current_x, 
                    current_y, 
                    text=line_buffer, 
                    font=font, 
                    anchor=tk.W
                )
                current_x += text_width

# 返回下一行的y坐标
return current_y + line_height

defdraw_logo(self, paper_x, paper_y):
"""绘制LOGO"""
if self.logo_visible and self.logo_tk:
            x = paper_x + self.logo_pos[0]
            y = paper_y + self.logo_pos[1]
            w, h = self.logo_size
            self.canvas.create_image(x, y, image=self.logo_tk, anchor=tk.NW)
# 绘制缩放手柄
            self.canvas.create_rectangle(x+w-5, y+h-5, x+w+5, y+h+5, fill="#165DFF", outline="")

defdraw_seal(self, paper_x, paper_y):
"""绘制印章"""
if self.seal_visible and self.seal_tk:
            x = paper_x + self.seal_pos[0]
            y = paper_y + self.seal_pos[1]
            w, h = self.seal_size
            self.canvas.create_image(x, y, image=self.seal_tk, anchor=tk.NW)
# 绘制缩放手柄
            self.canvas.create_rectangle(x+w-5, y+h-5, x+w+5, y+h+5, fill="#165DFF", outline="")

defupload_logo(self):
"""上传LOGO"""
        file_path = filedialog.askopenfilename(
            title="选择LOGO图片",
            filetypes=[("图片文件""*.png;*.jpg;*.jpeg;*.bmp")]
        )
if file_path:
try:
                self.logo_image = Image.open(file_path).convert("RGBA")
                self.logo_image = self.logo_image.resize(self.logo_size, Image.Resampling.LANCZOS)
                self.logo_tk = ImageTk.PhotoImage(self.logo_image)
                self.logo_visible = True
                self.remove_logo_btn.config(state=tk.NORMAL)
                self.draw_case()
except Exception as e:
                messagebox.showerror("错误"f"无法加载图片:{str(e)}")

defremove_logo(self):
"""移除LOGO"""
        self.logo_image = None
        self.logo_tk = None
        self.logo_visible = False
        self.remove_logo_btn.config(state=tk.DISABLED)
        self.draw_case()

defupload_seal(self):
"""上传印章"""
        file_path = filedialog.askopenfilename(
            title="选择印章图片",
            filetypes=[("图片文件""*.png;*.jpg;*.jpeg;*.bmp")]
        )
if file_path:
try:
                self.seal_image = Image.open(file_path).convert("RGBA")
                self.seal_image = self.seal_image.resize(self.seal_size, Image.Resampling.LANCZOS)
                self.seal_tk = ImageTk.PhotoImage(self.seal_image)
                self.seal_visible = True
                self.remove_seal_btn.config(state=tk.NORMAL)
                self.draw_case()
except Exception as e:
                messagebox.showerror("错误"f"无法加载图片:{str(e)}")

defremove_seal(self):
"""移除印章"""
        self.seal_image = None
        self.seal_tk = None
        self.seal_visible = False
        self.remove_seal_btn.config(state=tk.DISABLED)
        self.draw_case()

defon_mouse_down(self, event):
"""鼠标按下事件"""
        x = self.canvas.canvasx(event.x)
        y = self.canvas.canvasy(event.y)

# 计算纸张位置
        container_width = self.canvas_container.winfo_width()
        paper_x = max(50, (container_width - self.paper_width) // 2)
        paper_y = 50

# 转换为纸张相对坐标
        rel_x = x - paper_x
        rel_y = y - paper_y

# 检查是否点击LOGO缩放手柄
if self.logo_visible:
            lx, ly = self.logo_pos
            lw, lh = self.logo_size
if lx+lw-5 <= rel_x <= lx+lw+5and ly+lh-5 <= rel_y <= ly+lh+5:
                self.dragging = "logo_resize"
return

# 检查是否点击印章缩放手柄
if self.seal_visible:
            sx, sy = self.seal_pos
            sw, sh = self.seal_size
if sx+sw-5 <= rel_x <= sx+sw+5and sy+sh-5 <= rel_y <= sy+sh+5:
                self.dragging = "seal_resize"
return

# 检查是否点击LOGO
if self.logo_visible:
            lx, ly = self.logo_pos
            lw, lh = self.logo_size
if lx <= rel_x <= lx+lw and ly <= rel_y <= ly+lh:
                self.dragging = "logo"
                self.drag_offset = (rel_x - lx, rel_y - ly)
return

# 检查是否点击印章
if self.seal_visible:
            sx, sy = self.seal_pos
            sw, sh = self.seal_size
if sx <= rel_x <= sx+sw and sy <= rel_y <= sy+sh:
                self.dragging = "seal"
                self.drag_offset = (rel_x - sx, rel_y - sy)
return

defon_mouse_drag(self, event):
"""鼠标拖动事件"""
ifnot self.dragging:
return

        x = self.canvas.canvasx(event.x)
        y = self.canvas.canvasy(event.y)

# 计算纸张位置
        container_width = self.canvas_container.winfo_width()
        paper_x = max(50, (container_width - self.paper_width) // 2)
        paper_y = 50

# 转换为纸张相对坐标
        rel_x = x - paper_x
        rel_y = y - paper_y

if self.dragging == "logo":
# 限制在纸张内
            new_x = max(0, min(rel_x - self.drag_offset[0], self.paper_width - self.logo_size[0]))
            new_y = max(0, min(rel_y - self.drag_offset[1], self.paper_height - self.logo_size[1]))
            self.logo_pos = (new_x, new_y)

elif self.dragging == "seal":
            new_x = max(0, min(rel_x - self.drag_offset[0], self.paper_width - self.seal_size[0]))
            new_y = max(0, min(rel_y - self.drag_offset[1], self.paper_height - self.seal_size[1]))
            self.seal_pos = (new_x, new_y)

elif self.dragging == "logo_resize":
            new_w = max(50, min(rel_x - self.logo_pos[0], self.paper_width - self.logo_pos[0]))
            new_h = max(50, min(rel_y - self.logo_pos[1], self.paper_height - self.logo_pos[1]))
            self.logo_size = (new_w, new_h)
# 调整图片大小
            self.logo_image = self.logo_image.resize(self.logo_size, Image.Resampling.LANCZOS)
            self.logo_tk = ImageTk.PhotoImage(self.logo_image)

elif self.dragging == "seal_resize":
            new_w = max(50, min(rel_x - self.seal_pos[0], self.paper_width - self.seal_pos[0]))
            new_h = max(50, min(rel_y - self.seal_pos[1], self.paper_height - self.seal_pos[1]))
            self.seal_size = (new_w, new_h)
# 调整图片大小
            self.seal_image = self.seal_image.resize(self.seal_size, Image.Resampling.LANCZOS)
            self.seal_tk = ImageTk.PhotoImage(self.seal_image)

        self.draw_case()

defon_mouse_up(self, event):
"""鼠标释放事件"""
        self.dragging = None

defon_mouse_wheel(self, event):
"""鼠标滚轮事件"""
        self.canvas.yview_scroll(-int(event.delta/120), "units")

defdownload_pdf(self):
"""下载为PDF - 修复字体问题"""
        file_path = filedialog.asksaveasfilename(
            defaultextension=".pdf",
            filetypes=[("PDF文件""*.pdf")],
            title="保存PDF文件"
        )
ifnot file_path:
return

try:
# 创建PDF画布
            c = canvas.Canvas(file_path, pagesize=A4)
            width, height = A4

# 设置默认字体
try:
                c.setFont("SimSun"11)
except:
                c.setFont("Helvetica"11)
                messagebox.showwarning("警告""未找到宋体字体,PDF将使用默认字体显示")

            y = height - 50

# 解析MD内容
            elements = self.parse_markdown(self.md_content)

for elem_type, elem_content in elements:
if y < 100:
                    c.showPage()
                    y = height - 50
try:
                        c.setFont("SimSun"11)
except:
                        c.setFont("Helvetica"11)

if elem_type == 'h1':
try:
                        c.setFont("SimSun-Bold"20)
except:
                        c.setFont("Helvetica-Bold"20)
                    c.drawCentredString(width/2, y, elem_content)
                    y -= 50

elif elem_type == 'h2':
try:
                        c.setFont("SimSun-Bold"18)
except:
                        c.setFont("Helvetica-Bold"18)
                    c.drawCentredString(width/2, y, elem_content)
                    y -= 40

elif elem_type == 'h3':
try:
                        c.setFont("SimSun-Bold"14)
except:
                        c.setFont("Helvetica-Bold"14)
                    c.drawString(50, y, elem_content)
                    y -= 30

elif elem_type == 'hr':
                    c.setLineWidth(2)
                    c.line(50, y, width-50, y)
                    y -= 5
                    c.setLineWidth(1)
                    c.line(50, y, width-50, y)
                    y -= 25

elif elem_type == 'p':
try:
                        c.setFont("SimSun"11)
except:
                        c.setFont("Helvetica"11)
                    self.draw_pdf_wrapped_text(c, elem_content, 50, y, width-100)
# 计算行数
                    lines = self.count_pdf_lines(c, elem_content, width-100)
                    y -= lines * 20 + 10

elif elem_type == 'list':
try:
                        c.setFont("SimSun"11)
except:
                        c.setFont("Helvetica"11)
for i, item in enumerate(elem_content):
                        self.draw_pdf_wrapped_text(c, item, 70, y, width-120)
                        c.drawString(55, y, f"{i+1}.")
                        lines = self.count_pdf_lines(c, item, width-120)
                        y -= lines * 20
                    y -= 10

# 绘制LOGO
if self.logo_visible and self.logo_image:
# 转换坐标(PDF原点在左下角)
                pdf_x = self.logo_pos[0] * (width / self.paper_width)
                pdf_y = height - (self.logo_pos[1] + self.logo_size[1]) * (height / self.base_paper_height)
                pdf_w = self.logo_size[0] * (width / self.paper_width)
                pdf_h = self.logo_size[1] * (height / self.base_paper_height)

# 保存临时图片
                temp_logo = "temp_logo.png"
                self.logo_image.save(temp_logo)
                c.drawImage(temp_logo, pdf_x, pdf_y, width=pdf_w, height=pdf_h, mask='auto')
                os.remove(temp_logo)

# 绘制印章
if self.seal_visible and self.seal_image:
                pdf_x = self.seal_pos[0] * (width / self.paper_width)
                pdf_y = height - (self.seal_pos[1] + self.seal_size[1]) * (height / self.base_paper_height)
                pdf_w = self.seal_size[0] * (width / self.paper_width)
                pdf_h = self.seal_size[1] * (height / self.base_paper_height)

                temp_seal = "temp_seal.png"
                self.seal_image.save(temp_seal)
                c.drawImage(temp_seal, pdf_x, pdf_y, width=pdf_w, height=pdf_h, mask='auto')
                os.remove(temp_seal)

            c.save()
            messagebox.showinfo("成功""PDF文件已保存!")

except Exception as e:
            messagebox.showerror("错误"f"生成PDF失败:{str(e)}")
import traceback
            traceback.print_exc()

defcount_pdf_lines(self, c, text, max_width):
"""计算PDF文本行数"""
        words = list(text)
        line = ""
        lines = 1

for word in words:
            test_line = line + word
try:
                text_width = c.stringWidth(test_line, "SimSun"11)
except:
                text_width = c.stringWidth(test_line, "Helvetica"11)

if text_width > max_width:
                line = word
                lines += 1
else:
                line = test_line

return lines

defdraw_pdf_wrapped_text(self, c, text, x, y, max_width):
"""绘制PDF自动换行文本"""
        words = list(text)
        line = ""
        current_y = y

for word in words:
            test_line = line + word
try:
                text_width = c.stringWidth(test_line, "SimSun"11)
except:
                text_width = c.stringWidth(test_line, "Helvetica"11)

if text_width > max_width:
                c.drawString(x, current_y, line)
                line = word
                current_y -= 20
else:
                line = test_line

if line:
            c.drawString(x, current_y, line)

defdownload_docx(self):
"""下载为DOCX - 修复负数间距错误"""
        file_path = filedialog.asksaveasfilename(
            defaultextension=".docx",
            filetypes=[("Word文档""*.docx")],
            title="保存Word文件"
        )
ifnot file_path:
return

try:
            doc = Document()

# 设置默认字体为宋体
            style = doc.styles['Normal']
            font = style.font
            font.name = '宋体'
            font.size = Pt(12)
            font.color.rgb = RGBColor(000)
            paragraph_format = style.paragraph_format
            paragraph_format.line_spacing = Pt(24)
            paragraph_format.first_line_indent = Pt(24)  # 首行缩进2字符

# 创建标题样式
            h1_style = doc.styles.add_style('H1 Style', WD_STYLE_TYPE.PARAGRAPH)
            h1_style.font.name = '宋体'
            h1_style.font.size = Pt(22)
            h1_style.font.bold = True
            h1_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
            h1_style.paragraph_format.space_after = Pt(30)
            h1_style.paragraph_format.first_line_indent = Pt(0)

            h2_style = doc.styles.add_style('H2 Style', WD_STYLE_TYPE.PARAGRAPH)
            h2_style.font.name = '宋体'
            h2_style.font.size = Pt(20)
            h2_style.font.bold = True
            h2_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
            h2_style.paragraph_format.space_after = Pt(24)
            h2_style.paragraph_format.first_line_indent = Pt(0)

            h3_style = doc.styles.add_style('H3 Style', WD_STYLE_TYPE.PARAGRAPH)
            h3_style.font.name = '宋体'
            h3_style.font.size = Pt(16)
            h3_style.font.bold = True
            h3_style.paragraph_format.space_before = Pt(15)
            h3_style.paragraph_format.space_after = Pt(8)
            h3_style.paragraph_format.first_line_indent = Pt(0)

# 创建列表样式
            list_style = doc.styles.add_style('List Style', WD_STYLE_TYPE.PARAGRAPH)
            list_style.font.name = '宋体'
            list_style.font.size = Pt(12)
            list_style.paragraph_format.left_indent = Pt(24)
            list_style.paragraph_format.first_line_indent = Pt(0)
            list_style.paragraph_format.line_spacing = Pt(24)

# 创建右对齐样式
            right_style = doc.styles.add_style('Right Style', WD_STYLE_TYPE.PARAGRAPH)
            right_style.font.name = '宋体'
            right_style.font.size = Pt(12)
            right_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.RIGHT
            right_style.paragraph_format.first_line_indent = Pt(0)

# 创建小号灰色样式
            small_gray_style = doc.styles.add_style('Small Gray Style', WD_STYLE_TYPE.PARAGRAPH)
            small_gray_style.font.name = '宋体'
            small_gray_style.font.size = Pt(10)
            small_gray_style.font.color.rgb = RGBColor(107114128)
            small_gray_style.paragraph_format.first_line_indent = Pt(0)

# 解析MD内容
            elements = self.parse_markdown(self.md_content)

for elem_type, elem_content in elements:
if elem_type == 'h1':
                    doc.add_paragraph(elem_content, style='H1 Style')
elif elem_type == 'h2':
                    doc.add_paragraph(elem_content, style='H2 Style')
elif elem_type == 'h3':
                    doc.add_paragraph(elem_content, style='H3 Style')
elif elem_type == 'hr':
                    p = doc.add_paragraph('─' * 55)
                    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
                    p.paragraph_format.space_after = Pt(15)
elif elem_type == 'p':
# 处理加粗文本
if'**'in elem_content:
                        p = doc.add_paragraph()
                        parts = elem_content.split('**')
                        is_bold = False
for part in parts:
ifnot part:
                                is_bold = not is_bold
continue
                            run = p.add_run(part)
                            run.bold = is_bold
else:
                        doc.add_paragraph(elem_content)
elif elem_type == 'list':
for i, item in enumerate(elem_content):
if'**'in item:
                            p = doc.add_paragraph(style='List Style')
                            parts = item.split('**')
                            is_bold = False
                            p.add_run(f"{i+1}. ")
for part in parts:
ifnot part:
                                    is_bold = not is_bold
continue
                                run = p.add_run(part)
                                run.bold = is_bold
else:
                            doc.add_paragraph(f"{i+1}{item}", style='List Style')

# 添加LOGO
if self.logo_visible and self.logo_image:
                temp_logo = "temp_logo.png"
                self.logo_image.save(temp_logo)
# 插入到第一个段落(医院名称)
                p = doc.paragraphs[0]
                run = p.runs[0]
                run.add_picture(temp_logo, width=Inches(self.logo_size[0]/96), height=Inches(self.logo_size[1]/96))
                run.add_text(' ')
                os.remove(temp_logo)

# 添加印章 - 修复负数间距问题
if self.seal_visible and self.seal_image:
                temp_seal = "temp_seal.png"
                self.seal_image.save(temp_seal)

# 在文档末尾添加两个空行,然后插入印章
                doc.add_paragraph('')
                doc.add_paragraph('')

                p = doc.add_paragraph()
                p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
                run = p.add_run()
                run.add_picture(temp_seal, width=Inches(self.seal_size[0]/96), height=Inches(self.seal_size[1]/96))

                os.remove(temp_seal)

            doc.save(file_path)
            messagebox.showinfo("成功""Word文件已保存!")

except Exception as e:
            messagebox.showerror("错误"f"生成Word失败:{str(e)}")
import traceback
            traceback.print_exc()

if __name__ == "__main__":
    root = tk.Tk()
# 设置窗口图标(可选)
# root.iconbitmap("icon.ico")
    app = MedicalCaseEditor(root)
    root.mainloop()

核心知识点总结

通过这个医疗病例编辑器的开发,我们掌握了以下几个重要的Python编程知识点:

  1. Tkinter Canvas高级用法:学会了如何在Canvas上绘制文本、图片和图形,实现了模拟纸张效果和滚动预览功能。特别是动态调整画布大小和滚动区域的技巧,对于开发长文档预览工具非常有用。

  2. 富文本渲染技术:深入理解了文本宽度计算和自动换行的原理,实现了支持加粗格式的富文本渲染引擎。使用tkinter.font.Font.measure()方法可以精确计算文本宽度,避免了创建临时对象带来的性能问题。

  3. Markdown解析与渲染:掌握了如何手动解析简单的Markdown语法,包括标题、列表、分隔线和加粗格式。这种轻量级的解析方式比使用完整的Markdown解析库更加高效,适合特定场景的需求。

  4. PDF和DOCX生成:学会了使用reportlab库生成PDF文档,以及使用python-docx库生成Word文档。重点掌握了中文字体处理、页面布局和图片插入的方法,解决了跨平台兼容性问题。

  5. 异常处理与用户体验:学会了如何在程序中添加完善的异常处理机制,避免程序崩溃。同时,通过友好的错误提示和警告信息,提升了用户体验。

拓展场景与测试步骤

拓展应用场景

这个医疗病例编辑器的架构非常灵活,可以很容易地拓展到其他领域:

  1. 通用病历模板系统:可以添加模板管理功能,预设各种常见疾病的病历模板,医生只需要填写关键信息即可生成完整的病历。

  2. 电子病历系统前端:可以将这个编辑器作为电子病历系统的前端组件,与后端数据库集成,实现病历的在线存储和管理。

  3. 检验报告生成器:修改模板和导出格式,可以用于生成各种检验报告、检查报告和诊断证明。

  4. 合同与法律文书编辑器:调整界面和样式,可以用于生成各种合同、协议和法律文书,支持自定义LOGO和印章。

  5. 学术论文排版工具:增加对更多Markdown语法的支持,如公式、引用和参考文献,可以用于学术论文的排版和导出。

完整测试步骤

为了确保编辑器的稳定性和正确性,建议按照以下步骤进行测试:

  1. 界面测试

    • 启动程序,检查界面是否正常显示
    • 调整窗口大小,检查左右分栏是否正常缩放
    • 滚动右侧预览区,检查滚动条是否正常工作
  2. 编辑功能测试

    • 在左侧编辑区输入文本,检查右侧是否实时预览
    • 测试Markdown语法:标题、列表、分隔线、加粗
    • 点击”加载Demo”按钮,检查是否正确加载示例病例
    • 测试”导入MD”和”导出MD”功能
  3. 导出功能测试

    • 点击”下载为PDF”,检查导出的PDF是否格式正确
    • 点击”下载为DOCX”,检查导出的Word文档是否格式正确
    • 测试不同长度的内容导出,检查是否有分页问题
  4. 图片功能测试

    • 上传医院LOGO,检查是否正常显示
    • 拖拽LOGO调整位置,检查是否正常
    • 拖拽LOGO右下角调整大小,检查是否正常
    • 同样测试医疗专用章的上传、拖拽和缩放功能
    • 测试移除LOGO和印章功能
  5. 边界测试

    • 输入非常长的文本,检查是否会出现内容截断
    • 输入包含特殊字符的文本,检查是否正常显示
    • 测试在没有安装宋体字体的系统上运行,检查是否有友好的提示