



[Python] 纯文本查看 复制代码import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import random
from openai import OpenAI
import os
import json
import markdown # Import markdown library
import webbrowser # Import webbrowser for opening html files
import configparser # Import configparser
class MentalHealthApp:
def __init__(self, root):
self.root = root
self.root.title("中小学生心理健康筛查系统")
self.root.geometry("900x700")
self.root.configure(bg='#f0f8ff')
self.root.state('zoomed') # Maximize window on startup
# 定义Markdown渲染所需的字体和标签
self.setup_markdown_styles()
# 配置文件路径
self.config_file = "config.ini"
# DeepSeek API Key (将从配置文件加载或通过对话框输入)
self.deepseek_api_key = None
self.client = None
# 尝试加载API Key或提示用户输入
self.load_or_prompt_api_key()
if not self.deepseek_api_key:
messagebox.showerror("启动错误", "未能获取DeepSeek API Key,程序将退出。")
self.root.destroy()
return
else:
self.client = OpenAI(api_key=self.deepseek_api_key, base_url="https://api.deepseek.com")
# 创建菜单栏
self.create_menubar()
# 应用标题
self.title_frame = tk.Frame(root, bg='#4b86b4')
self.title_frame.pack(fill='x', pady=(0, 20))
self.title = tk.Label(
self.title_frame,
text="中小学生心理健康筛查系统",
font=('微软雅黑', 24, 'bold'),
fg='white',
bg='#4b86b4',
pady=20
)
self.title.pack()
# 创建问题框架
self.main_frame = tk.Frame(root, bg='#f0f8ff')
self.main_frame.pack(fill='both', expand=True, padx=40, pady=(0, 20))
# 初始化变量
self.questions = []
self.current_question = 0
self.answers = []
self.user_info = {}
# 创建问题容器
self.question_container = tk.Frame(self.main_frame, bg='#f0f8ff')
self.question_container.pack(fill='both', expand=True)
# 创建用户信息页面
self.show_user_info_page()
def show_user_info_page(self):
"""显示用户信息输入页面"""
for widget in self.question_container.winfo_children():
widget.destroy()
# 用户信息表单
info_frame = tk.Frame(self.question_container, bg='#e6f2ff', padx=20, pady=20, bd=2, relief='groove')
info_frame.pack(fill='both', expand=True, padx=20, pady=20)
tk.Label(
info_frame,
text="个人信息",
font=('微软雅黑', 18, 'bold'),
bg='#e6f2ff',
fg='#2a4d69'
).pack(pady=(0, 20))
# 姓名
name_frame = tk.Frame(info_frame, bg='#e6f2ff')
name_frame.pack(fill='x', pady=5)
tk.Label(name_frame, text="姓名:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
self.name_entry = tk.Entry(name_frame, font=('微软雅黑', 12), width=30)
self.name_entry.pack(side='left', fill='x', expand=True)
# 年龄
age_frame = tk.Frame(info_frame, bg='#e6f2ff')
age_frame.pack(fill='x', pady=5)
tk.Label(age_frame, text="年龄:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
self.age_entry = tk.Entry(age_frame, font=('微软雅黑', 12), width=30)
self.age_entry.pack(side='left', fill='x', expand=True)
# 性别
gender_frame = tk.Frame(info_frame, bg='#e6f2ff')
gender_frame.pack(fill='x', pady=5)
tk.Label(gender_frame, text="性别:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
self.gender_var = tk.StringVar(value="男")
tk.Radiobutton(
gender_frame, text="男", variable=self.gender_var, value="男",
font=('微软雅黑', 12), bg='#e6f2ff'
).pack(side='left', padx=(0, 20))
tk.Radiobutton(
gender_frame, text="女", variable=self.gender_var, value="女",
font=('微软雅黑', 12), bg='#e6f2ff'
).pack(side='left')
# 年级
grade_frame = tk.Frame(info_frame, bg='#e6f2ff')
grade_frame.pack(fill='x', pady=5)
tk.Label(grade_frame, text="年级:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
self.grade_var = tk.StringVar()
grades = ["小学一年级", "小学二年级", "小学三年级", "小学四年级", "小学五年级", "小学六年级",
"初中一年级", "初中二年级", "初中三年级",
"高中一年级", "高中二年级", "高中三年级"]
self.grade_combo = ttk.Combobox(grade_frame, textvariable=self.grade_var, values=grades,
font=('微软雅黑', 12), width=27, state="readonly")
self.grade_combo.current(0)
self.grade_combo.pack(side='left')
# 开始测试按钮
start_button = tk.Button(
info_frame,
text="开始测试",
font=('微软雅黑', 14, 'bold'),
bg='#63a4ff',
fg='white',
relief='flat',
command=self.start_test,
padx=20,
pady=10
)
start_button.pack(pady=20)
start_button.bind("", lambda e: start_button.config(bg='#4a86e8'))
start_button.bind("", lambda e: start_button.config(bg='#63a4ff'))
def start_test(self):
"""开始测试"""
# 保存用户信息
self.user_info = {
"name": self.name_entry.get().strip() or "匿名",
"age": self.age_entry.get().strip() or "未填写",
"gender": self.gender_var.get(),
"grade": self.grade_var.get()
}
if not self.user_info["name"]:
messagebox.showwarning("输入错误", "请输入姓名")
return
# 调用DeepSeek生成题库
self.generate_questions_with_deepseek()
if not self.questions: # 如果题库生成失败,则不开始测试
messagebox.showerror("题库生成失败", "未能生成心理健康题库,请稍后重试。")
return
self.show_question()
def generate_questions_with_deepseek(self):
"""根据用户信息调用DeepSeek API生成心理健康题库"""
# 显示提示信息窗口
tip_window = tk.Toplevel(self.root)
tip_window.title("正在生成题目...")
tip_window.geometry("400x200")
tip_window.transient(self.root) # Set to be on top of the main window
tip_window.grab_set() # Make it modal
tk.Label(tip_window, text="请稍候,DeepSeek AI正在为您生成专属心理健康测试题...",
font=('微软雅黑', 12, 'bold'), wraplength=350, justify='center').pack(pady=20)
tk.Label(tip_window, text="小贴士:保持积极心态,关注情绪变化,及时寻求帮助是心理健康的关键!",
font=('微软雅黑', 10), wraplength=350, justify='center', fg='blue').pack(pady=10)
progress_bar = ttk.Progressbar(tip_window, orient='horizontal', length=300, mode='indeterminate')
progress_bar.pack(pady=10)
progress_bar.start()
self.root.update_idletasks() # Update the GUI to show the window
if not self.deepseek_api_key or self.deepseek_api_key == "YOUR_DEEPSEEK_API_KEY":
messagebox.showerror("API Key 错误", "请在代码中设置您的DeepSeek API Key 或设置环境变量 DEEPSEEK_API_KEY。")
self.questions = []
tip_window.destroy() # Close tip window on error
return
user_prompt = f"""
你是一名专业的心理健康评估专家,请根据用户提供的个人信息,为中小学生设计一套心理健康筛查问卷。问卷的特点如下:
1. **问题侧面提问**:问题应避免直接询问学生的感受,而是通过行为、习惯、反应等侧面反映其心理状态。例如,不直接问"你是否感到焦虑?",而是问"在面对考试或重要任务时,你是否会感到心跳加速、手心出汗?"
2. **随机性**:每次生成的题库应有所不同,以增加测试的随机性。
3. **针对用户特点**:结合学生的年龄、性别、年级,生成更贴切、更具代入感的问题。例如,对于小学低年级学生,问题可以更生活化、具体化;对于初高中生,可以涉及学习压力、社交关系等。
4. **结构化输出**:请以JSON数组格式输出,每个对象包含 'question' (问题内容), 'options' (**务必严格遵循以下两种选项格式之一**), 'category' (问题所属的心理健康维度,例如 '抑郁', '焦虑', '饮食', '自尊', '注意力', '风险', '情绪')。
* **普通问题选项**: ["几乎没有", "有时(几天)", "经常(超过一周)", "几乎每天"]
* **风险问题选项**: ["从来没有", "偶尔想过", "经常想到", "几乎每天都有这种想法"]
**示例 (普通问题):**
```json
{{"question": "你是否发现自己对以前很喜欢的游戏或活动提不起兴趣?", "options": ["几乎没有", "有时(几天)", "经常(超过一周)", "几乎每天"], "category": "抑郁"}}
```
**示例 (风险问题):**
```json
{{"question": "当遇到非常大的挫折时,你是否会有活得没意思的想法?", "options": ["从来没有", "偶尔想过", "经常想到", "几乎每天都有这种想法"], "category": "风险"}}
```
5. **数量**:生成15个问题。
用户个人信息:
姓名:{self.user_info.get("name", "匿名")}
年龄:{self.user_info.get("age", "未填写")}
性别:{self.user_info.get("gender", "未填写")}
年级:{self.user_info.get("grade", "未填写")}
"""
try:
response = self.client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": "你是一名专业的心理健康评估专家,请根据学生提供的个人信息设计心理健康筛查问卷,注意问题要侧面提问,并且务必严格遵循规定的JSON数组格式和选项格式。"},
{"role": "user", "content": user_prompt}
],
stream=False,
response_format={"type": "json_object"} # Request JSON output
)
# 解析JSON响应
generated_content = response.choices[0].message.content
# import json # Already imported at the top
temp_questions = json.loads(generated_content).get("questions", []) # Expecting a key like "questions" in the JSON
if not isinstance(temp_questions, list):
raise ValueError("DeepSeek did not return a list of questions.")
# 校验生成的问题选项格式
expected_options_normal = ["几乎没有", "有时(几天)", "经常(超过一周)", "几乎每天"]
expected_options_risk = ["从来没有", "偶尔想过", "经常想到", "几乎每天都有这种想法"]
self.questions = []
for i, q in enumerate(temp_questions):
if "question" in q and "options" in q and "category" in q:
if q["options"] == expected_options_normal or q["options"] == expected_options_risk:
self.questions.append(q)
else:
print(f"警告:第 {i+1} 个问题选项格式不匹配,已跳过。问题: {q.get("question", "未知")}, 选项: {q.get("options", "未知")}")
else:
print(f"警告:第 {i+1} 个问题缺少必要字段,已跳过。问题内容: {q}")
# 验证生成的问题数量
if len(self.questions) 0:
prev_button = tk.Button(
nav_frame,
text="上一题",
font=('微软雅黑', 12),
bg='#a3c1ad',
fg='white',
command=self.prev_question,
padx=15
)
prev_button.pack(side='left', padx=10)
if self.current_question
心理健康筛查评估报告
body {{ font-family: 'Microsoft YaHei', sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; }}
h1, h2, h3 {{ color: #2c3e50; }}
h1 {{ text-align: center; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 30px; }}
h2 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 25px; margin-bottom: 15px; }}
ul {{ list-style-type: disc; margin-left: 20px; }}
ol {{ list-style-type: decimal; margin-left: 20px; }}
table {{ width: 100%; border-collapse: collapse; margin-top: 15px; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
pre {{ background-color: #f8f8f8; border: 1px solid #ddd; padding: 10px; overflow-x: auto; }}
.disclaimer {{ font-size: 0.9em; color: #777; margin-top: 30px; border-top: 1px solid #eee; padding-top: 10px; text-align: center; }}
{html_content}
"""
try:
# Save to a temporary HTML file
import tempfile
temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.html', encoding='utf-8')
temp_file.write(full_html)
temp_file.close()
webbrowser.open(f'file://{temp_file.name}')
except Exception as e:
messagebox.showerror("打开报告失败", f"无法在浏览器中打开报告: {e}")
def save_report_as_html(self, markdown_content):
"""将Markdown内容转换为HTML并允许用户保存"""
print("Debug: Attempting to save report as HTML...") # Debug print
html_content = markdown.markdown(markdown_content)
# Create a full HTML page (same as open_report_in_browser)
full_html = f"""
心理健康筛查评估报告
body {{ font-family: 'Microsoft YaHei', sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; }}
h1, h2, h3 {{ color: #2c3e50; }}
h1 {{ text-align: center; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 30px; }}
h2 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 25px; margin-bottom: 15px; }}
ul {{ list-style-type: disc; margin-left: 20px; }}
ol {{ list-style-type: decimal; margin-left: 20px; }}
table {{ width: 100%; border-collapse: collapse; margin-top: 15px; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
pre {{ background-color: #f8f8f8; border: 1px solid #ddd; padding: 10px; overflow-x: auto; }}
.disclaimer {{ font-size: 0.9em; color: #777; margin-top: 30px; border-top: 1px solid #eee; padding-top: 10px; text-align: center; }}
{html_content}
"""
file_path = filedialog.asksaveasfilename(
defaultextension=".html",
filetypes=[("HTML files", "*.html"), ("All files", "*.*")],
title="保存评估报告"
)
if file_path:
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(full_html)
messagebox.showinfo("保存成功", f"评估报告已保存到:\n{file_path}")
except Exception as e:
messagebox.showerror("保存失败", f"保存评估报告失败: {e}")
def setup_markdown_styles(self):
"""设置Text组件的Markdown渲染样式"""
# 定义字体
self.h1_font = ('微软雅黑', 20, 'bold')
self.h2_font = ('微软雅黑', 16, 'bold')
self.bold_font = ('微软雅黑', 12, 'bold')
self.normal_font = ('微软雅黑', 12)
self.code_font = ('Courier New', 10)
# 配置Text组件的标签
# 这些标签需要在Text组件创建后应用
pass # Actual tag application happens in render_markdown_to_text_widget
def render_markdown_to_text_widget(self, text_widget, markdown_content):
"""将Markdown内容渲染到Tkinter Text组件中"""
text_widget.config(state='normal') # Enable editing temporarily
text_widget.delete('1.0', tk.END)
# Configure tags (must be done after text_widget is created)
text_widget.tag_configure('h1', font=self.h1_font, foreground='#2c3e50', justify='center', spacing3=15)
text_widget.tag_configure('h2', font=self.h2_font, foreground='#2c3e50', spacing3=10)
text_widget.tag_configure('bold', font=self.bold_font)
text_widget.tag_configure('normal', font=self.normal_font)
text_widget.tag_configure('list', lmargin1=20, lmargin2=40)
text_widget.tag_configure('code', font=self.code_font, background='#f0f0f0', relief='groove', borderwidth=1, lmargin1=20, lmargin2=20)
text_widget.tag_configure('table_header', font=self.bold_font, background='#f2f2f2', justify='center')
text_widget.tag_configure('table_cell', font=self.normal_font, justify='left')
text_widget.tag_configure('disclaimer', font=('微软雅黑', 10, 'italic'), foreground='#777', justify='center', spacing1=10, spacing3=10)
lines = markdown_content.split('\n')
in_code_block = False
in_table = False
table_header_parsed = False
for line in lines:
stripped_line = line.strip()
if stripped_line.startswith("```"): # Basic code block detection
in_code_block = not in_code_block
if not in_code_block: # End of code block
text_widget.insert(tk.END, "\n", 'normal')
continue
if in_code_block:
text_widget.insert(tk.END, line + "\n", 'code')
continue
if stripped_line.startswith('# '):
text_widget.insert(tk.END, stripped_line[2:] + "\n\n", 'h1')
in_table = False # Reset table state
elif stripped_line.startswith('## '):
text_widget.insert(tk.END, stripped_line[3:] + "\n\n", 'h2')
in_table = False # Reset table state
elif stripped_line.startswith('* ') or stripped_line.startswith('- '):
text_widget.insert(tk.END, line + "\n", 'list')
in_table = False # Reset table state
elif stripped_line.startswith('| ') and not in_table:
# Likely a table header, check for separator line next
text_widget.insert(tk.END, stripped_line + "\n", 'table_header')
in_table = True
table_header_parsed = False # Expecting the separator line next
elif stripped_line.startswith('| :---'): # Table separator line
# Don't display the separator line, just mark header parsed
table_header_parsed = True
text_widget.insert(tk.END, "\n") # Add a newline after table header
elif stripped_line.startswith('|') and in_table and table_header_parsed:
# Table row
cells = [cell.strip() for cell in stripped_line.split('|') if cell.strip()]
for cell in cells:
text_widget.insert(tk.END, f" {cell: