Initial commit: 个性化饮食推荐助手 - 包含OCR识别、AI分析、现代化界面等功能

This commit is contained in:
赵杰
2025-09-25 14:20:11 +01:00
commit aea5f6bf74
27 changed files with 14015 additions and 0 deletions

1504
gui/main_window.py Normal file

File diff suppressed because it is too large Load Diff

1359
gui/mobile_main_window.py Normal file

File diff suppressed because it is too large Load Diff

625
gui/new_main_window.py Normal file
View File

@@ -0,0 +1,625 @@
"""
新的界面设计 - 信息录入/修改 + 随机转盘/扭蛋机
基于用户需求重新设计的界面
"""
import tkinter as tk
from tkinter import messagebox
import customtkinter as ctk
from typing import Dict, List, Optional, Any
import random
import math
import threading
import time
from datetime import datetime
import json
# 设置CustomTkinter主题
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
class SpinWheel(ctk.CTkCanvas):
"""随机转盘/扭蛋机组件"""
def __init__(self, parent, width=300, height=300, **kwargs):
super().__init__(parent, width=width, height=height, **kwargs)
self.width = width
self.height = height
self.center_x = width // 2
self.center_y = height // 2
self.radius = min(width, height) // 2 - 20
# 转盘状态
self.is_spinning = False
self.current_angle = 0
self.spin_speed = 0
self.target_angle = 0
# 转盘选项
self.options = [
{"text": "早餐推荐", "color": "#FF6B6B", "value": "breakfast"},
{"text": "午餐推荐", "color": "#4ECDC4", "value": "lunch"},
{"text": "晚餐推荐", "color": "#45B7D1", "value": "dinner"},
{"text": "健康建议", "color": "#96CEB4", "value": "health"},
{"text": "营养分析", "color": "#FFEAA7", "value": "nutrition"},
{"text": "运动建议", "color": "#DDA0DD", "value": "exercise"}
]
# 绑定点击事件
self.bind("<Button-1>", self._on_click)
# 绘制转盘
self._draw_wheel()
def _draw_wheel(self):
"""绘制转盘"""
self.delete("all")
# 绘制转盘背景
self.create_oval(
self.center_x - self.radius,
self.center_y - self.radius,
self.center_x + self.radius,
self.center_y + self.radius,
fill="#2B2B2B",
outline="#FFFFFF",
width=3
)
# 绘制扇形区域
angle_per_section = 360 / len(self.options)
for i, option in enumerate(self.options):
start_angle = i * angle_per_section + self.current_angle
end_angle = (i + 1) * angle_per_section + self.current_angle
# 绘制扇形
self.create_arc(
self.center_x - self.radius + 10,
self.center_y - self.radius + 10,
self.center_x + self.radius - 10,
self.center_y + self.radius - 10,
start=start_angle,
extent=angle_per_section,
fill=option["color"],
outline="#FFFFFF",
width=2
)
# 绘制文字
text_angle = start_angle + angle_per_section / 2
text_radius = self.radius * 0.7
text_x = self.center_x + text_radius * math.cos(math.radians(text_angle))
text_y = self.center_y + text_radius * math.sin(math.radians(text_angle))
self.create_text(
text_x, text_y,
text=option["text"],
fill="#FFFFFF",
font=("Arial", 10, "bold"),
angle=text_angle
)
# 绘制中心圆
self.create_oval(
self.center_x - 20,
self.center_y - 20,
self.center_x + 20,
self.center_y + 20,
fill="#FF6B6B",
outline="#FFFFFF",
width=2
)
# 绘制指针
pointer_length = self.radius - 30
pointer_x = self.center_x + pointer_length * math.cos(math.radians(self.current_angle))
pointer_y = self.center_y + pointer_length * math.sin(math.radians(self.current_angle))
self.create_line(
self.center_x, self.center_y,
pointer_x, pointer_y,
fill="#FFFFFF",
width=4
)
def _on_click(self, event):
"""点击转盘开始旋转"""
if not self.is_spinning:
self.spin()
def spin(self):
"""开始旋转"""
if self.is_spinning:
return
self.is_spinning = True
self.spin_speed = random.uniform(15, 25) # 初始速度
self.target_angle = random.uniform(720, 1440) # 随机旋转角度
self._animate_spin()
def _animate_spin(self):
"""动画旋转"""
if not self.is_spinning:
return
# 更新角度
self.current_angle += self.spin_speed
self.current_angle %= 360
# 减速
self.spin_speed *= 0.95
# 重绘转盘
self._draw_wheel()
# 检查是否停止
if self.spin_speed < 0.1:
self.is_spinning = False
self._on_spin_complete()
else:
self.after(50, self._animate_spin)
def _on_spin_complete(self):
"""旋转完成回调"""
# 计算选中的选项
angle_per_section = 360 / len(self.options)
selected_index = int(self.current_angle // angle_per_section)
selected_option = self.options[selected_index]
# 触发回调
if hasattr(self, 'on_spin_complete'):
self.on_spin_complete(selected_option)
class UserInfoForm(ctk.CTkFrame):
"""用户信息录入/修改表单"""
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.user_data = {}
self._create_widgets()
def _create_widgets(self):
"""创建表单组件"""
# 标题
title_label = ctk.CTkLabel(
self,
text="📝 个人信息管理",
font=ctk.CTkFont(size=24, weight="bold")
)
title_label.pack(pady=20)
# 表单框架
form_frame = ctk.CTkScrollableFrame(self)
form_frame.pack(fill="both", expand=True, padx=20, pady=10)
# 基本信息
self._create_basic_info_section(form_frame)
# 健康信息
self._create_health_info_section(form_frame)
# 饮食偏好
self._create_diet_preferences_section(form_frame)
# 按钮区域
self._create_buttons(form_frame)
def _create_basic_info_section(self, parent):
"""创建基本信息区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="👤 基本信息",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 信息网格
info_frame = ctk.CTkFrame(section_frame)
info_frame.pack(fill="x", padx=20, pady=10)
# 姓名
name_label = ctk.CTkLabel(info_frame, text="姓名:")
name_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.name_var = tk.StringVar()
name_entry = ctk.CTkEntry(info_frame, textvariable=self.name_var, width=200)
name_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 年龄
age_label = ctk.CTkLabel(info_frame, text="年龄:")
age_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.age_var = tk.StringVar(value="25")
age_entry = ctk.CTkEntry(info_frame, textvariable=self.age_var, width=100)
age_entry.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 性别
gender_label = ctk.CTkLabel(info_frame, text="性别:")
gender_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.gender_var = tk.StringVar(value="")
gender_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.gender_var,
values=["", ""]
)
gender_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 身高体重
height_label = ctk.CTkLabel(info_frame, text="身高(cm):")
height_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.height_var = tk.StringVar(value="165")
height_entry = ctk.CTkEntry(info_frame, textvariable=self.height_var, width=100)
height_entry.grid(row=1, column=3, sticky="w", padx=10, pady=5)
weight_label = ctk.CTkLabel(info_frame, text="体重(kg):")
weight_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.weight_var = tk.StringVar(value="55")
weight_entry = ctk.CTkEntry(info_frame, textvariable=self.weight_var, width=100)
weight_entry.grid(row=2, column=1, sticky="w", padx=10, pady=5)
def _create_health_info_section(self, parent):
"""创建健康信息区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="🏥 健康信息",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 健康信息网格
health_frame = ctk.CTkFrame(section_frame)
health_frame.pack(fill="x", padx=20, pady=10)
# 活动水平
activity_label = ctk.CTkLabel(health_frame, text="活动水平:")
activity_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.activity_var = tk.StringVar(value="中等")
activity_menu = ctk.CTkOptionMenu(
health_frame,
variable=self.activity_var,
values=["久坐", "轻度活动", "中等", "高度活动", "极度活动"]
)
activity_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 健康目标
goal_label = ctk.CTkLabel(health_frame, text="健康目标:")
goal_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.goal_var = tk.StringVar(value="保持健康")
goal_menu = ctk.CTkOptionMenu(
health_frame,
variable=self.goal_var,
values=["保持健康", "减重", "增重", "增肌", "改善消化", "提高免疫力"]
)
goal_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 过敏信息
allergy_label = ctk.CTkLabel(health_frame, text="过敏食物:")
allergy_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.allergy_var = tk.StringVar(value="")
allergy_entry = ctk.CTkEntry(health_frame, textvariable=self.allergy_var, width=200)
allergy_entry.grid(row=1, column=1, columnspan=2, sticky="w", padx=10, pady=5)
def _create_diet_preferences_section(self, parent):
"""创建饮食偏好区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="🍽️ 饮食偏好",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 偏好网格
pref_frame = ctk.CTkFrame(section_frame)
pref_frame.pack(fill="x", padx=20, pady=10)
# 口味偏好
taste_label = ctk.CTkLabel(pref_frame, text="主要口味:")
taste_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.taste_var = tk.StringVar(value="均衡")
taste_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.taste_var,
values=["均衡", "偏甜", "偏咸", "偏辣", "偏酸", "偏清淡"]
)
taste_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 饮食类型
diet_label = ctk.CTkLabel(pref_frame, text="饮食类型:")
diet_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.diet_var = tk.StringVar(value="普通饮食")
diet_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.diet_var,
values=["普通饮食", "素食", "低脂饮食", "低糖饮食", "高蛋白饮食"]
)
diet_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 不喜欢食物
dislike_label = ctk.CTkLabel(pref_frame, text="不喜欢食物:")
dislike_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.dislike_var = tk.StringVar(value="")
dislike_entry = ctk.CTkEntry(pref_frame, textvariable=self.dislike_var, width=200)
dislike_entry.grid(row=1, column=1, columnspan=2, sticky="w", padx=10, pady=5)
def _create_buttons(self, parent):
"""创建按钮区域"""
button_frame = ctk.CTkFrame(parent)
button_frame.pack(fill="x", padx=10, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="💾 保存信息",
command=self._save_data,
width=150,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
save_button.pack(side="left", padx=20, pady=10)
# 加载按钮
load_button = ctk.CTkButton(
button_frame,
text="📂 加载信息",
command=self._load_data,
width=150,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
load_button.pack(side="left", padx=20, pady=10)
# 重置按钮
reset_button = ctk.CTkButton(
button_frame,
text="🔄 重置表单",
command=self._reset_form,
width=150,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
reset_button.pack(side="right", padx=20, pady=10)
def _save_data(self):
"""保存数据"""
try:
self.user_data = {
'basic_info': {
'name': self.name_var.get(),
'age': int(self.age_var.get()),
'gender': self.gender_var.get(),
'height': int(self.height_var.get()),
'weight': int(self.weight_var.get())
},
'health_info': {
'activity_level': self.activity_var.get(),
'health_goal': self.goal_var.get(),
'allergies': self.allergy_var.get()
},
'diet_preferences': {
'taste': self.taste_var.get(),
'diet_type': self.diet_var.get(),
'dislikes': self.dislike_var.get()
},
'saved_at': datetime.now().isoformat()
}
# 保存到文件
with open('data/user_info.json', 'w', encoding='utf-8') as f:
json.dump(self.user_data, f, ensure_ascii=False, indent=2)
messagebox.showinfo("成功", "信息保存成功!")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def _load_data(self):
"""加载数据"""
try:
with open('data/user_info.json', 'r', encoding='utf-8') as f:
self.user_data = json.load(f)
# 填充表单
basic_info = self.user_data.get('basic_info', {})
self.name_var.set(basic_info.get('name', ''))
self.age_var.set(str(basic_info.get('age', 25)))
self.gender_var.set(basic_info.get('gender', ''))
self.height_var.set(str(basic_info.get('height', 165)))
self.weight_var.set(str(basic_info.get('weight', 55)))
health_info = self.user_data.get('health_info', {})
self.activity_var.set(health_info.get('activity_level', '中等'))
self.goal_var.set(health_info.get('health_goal', '保持健康'))
self.allergy_var.set(health_info.get('allergies', ''))
diet_prefs = self.user_data.get('diet_preferences', {})
self.taste_var.set(diet_prefs.get('taste', '均衡'))
self.diet_var.set(diet_prefs.get('diet_type', '普通饮食'))
self.dislike_var.set(diet_prefs.get('dislikes', ''))
messagebox.showinfo("成功", "信息加载成功!")
except FileNotFoundError:
messagebox.showwarning("警告", "未找到保存的信息文件")
except Exception as e:
messagebox.showerror("错误", f"加载失败: {str(e)}")
def _reset_form(self):
"""重置表单"""
self.name_var.set("")
self.age_var.set("25")
self.gender_var.set("")
self.height_var.set("165")
self.weight_var.set("55")
self.activity_var.set("中等")
self.goal_var.set("保持健康")
self.allergy_var.set("")
self.taste_var.set("均衡")
self.diet_var.set("普通饮食")
self.dislike_var.set("")
class NewMainWindow(ctk.CTk):
"""新的主窗口 - 信息录入/修改 + 随机转盘/扭蛋机"""
def __init__(self):
super().__init__()
# 设置窗口
self.title("🍎 智能饮食推荐助手")
self.geometry("1200x800")
self.minsize(1000, 700)
# 创建界面
self._create_widgets()
# 设置转盘回调
self.spin_wheel.on_spin_complete = self._on_spin_complete
def _create_widgets(self):
"""创建界面组件"""
# 主标题
title_label = ctk.CTkLabel(
self,
text="🎯 智能饮食推荐助手",
font=ctk.CTkFont(size=32, weight="bold")
)
title_label.pack(pady=20)
# 主内容区域
main_frame = ctk.CTkFrame(self)
main_frame.pack(fill="both", expand=True, padx=20, pady=10)
# 左侧 - 信息录入区域
left_frame = ctk.CTkFrame(main_frame)
left_frame.pack(side="left", fill="both", expand=True, padx=(0, 10))
self.user_info_form = UserInfoForm(left_frame)
self.user_info_form.pack(fill="both", expand=True)
# 右侧 - 转盘区域
right_frame = ctk.CTkFrame(main_frame)
right_frame.pack(side="right", fill="both", padx=(10, 0))
# 转盘标题
wheel_title = ctk.CTkLabel(
right_frame,
text="🎰 随机推荐转盘",
font=ctk.CTkFont(size=20, weight="bold")
)
wheel_title.pack(pady=20)
# 转盘说明
wheel_desc = ctk.CTkLabel(
right_frame,
text="点击转盘开始随机推荐!",
font=ctk.CTkFont(size=14)
)
wheel_desc.pack(pady=10)
# 转盘组件
self.spin_wheel = SpinWheel(right_frame, width=350, height=350)
self.spin_wheel.pack(pady=20)
# 结果显示区域
self.result_frame = ctk.CTkFrame(right_frame)
self.result_frame.pack(fill="x", padx=20, pady=20)
self.result_label = ctk.CTkLabel(
self.result_frame,
text="等待转盘结果...",
font=ctk.CTkFont(size=16),
wraplength=300
)
self.result_label.pack(pady=20)
# 底部状态栏
self.status_frame = ctk.CTkFrame(self)
self.status_frame.pack(fill="x", padx=20, pady=10)
self.status_label = ctk.CTkLabel(
self.status_frame,
text="就绪",
font=ctk.CTkFont(size=12)
)
self.status_label.pack(pady=5)
def _on_spin_complete(self, selected_option):
"""转盘完成回调"""
self._update_status(f"转盘选中: {selected_option['text']}")
# 显示结果
result_text = f"🎯 推荐结果: {selected_option['text']}\n\n"
# 根据选中的选项生成具体建议
if selected_option['value'] == 'breakfast':
result_text += "🌅 早餐建议:\n"
result_text += "• 燕麦粥 + 牛奶 + 香蕉\n"
result_text += "• 全麦面包 + 鸡蛋 + 蔬菜\n"
result_text += "• 小米粥 + 咸菜 + 煮蛋"
elif selected_option['value'] == 'lunch':
result_text += "🌞 午餐建议:\n"
result_text += "• 米饭 + 鸡肉 + 青菜\n"
result_text += "• 面条 + 牛肉 + 西红柿\n"
result_text += "• 饺子 + 汤"
elif selected_option['value'] == 'dinner':
result_text += "🌙 晚餐建议:\n"
result_text += "• 粥 + 咸菜 + 豆腐\n"
result_text += "• 蒸蛋 + 青菜 + 汤\n"
result_text += "• 面条 + 蔬菜"
elif selected_option['value'] == 'health':
result_text += "🏥 健康建议:\n"
result_text += "• 多喝水,保持水分平衡\n"
result_text += "• 适量运动,增强体质\n"
result_text += "• 规律作息,保证睡眠"
elif selected_option['value'] == 'nutrition':
result_text += "🥗 营养建议:\n"
result_text += "• 多吃蔬菜水果\n"
result_text += "• 适量蛋白质摄入\n"
result_text += "• 控制糖分和盐分"
elif selected_option['value'] == 'exercise':
result_text += "🏃 运动建议:\n"
result_text += "• 每天30分钟有氧运动\n"
result_text += "• 适量力量训练\n"
result_text += "• 注意运动前后拉伸"
self.result_label.configure(text=result_text)
def _update_status(self, message):
"""更新状态栏"""
self.status_label.configure(text=f"{datetime.now().strftime('%H:%M:%S')} - {message}")
def main():
"""主函数"""
app = NewMainWindow()
app.mainloop()
if __name__ == "__main__":
main()

637
gui/ocr_calorie_gui.py Normal file
View File

@@ -0,0 +1,637 @@
"""
OCR热量识别GUI界面
提供图片上传、OCR识别、结果验证和修正功能
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from PIL import Image, ImageTk
import threading
from typing import Dict, List, Optional, Any
from pathlib import Path
import json
from datetime import datetime
from core.base import UserData, CalorieInfo, FoodRecognitionResult
class OCRCalorieGUI:
"""OCR热量识别GUI界面"""
def __init__(self, parent_window, app_core):
self.parent_window = parent_window
self.app_core = app_core
self.current_image_path = None
self.current_recognition_result = None
self.user_corrections = {}
# 创建主框架
self.main_frame = ttk.Frame(parent_window)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建界面
self._create_widgets()
self._setup_layout()
self._bind_events()
def _create_widgets(self):
"""创建界面组件"""
# 标题
self.title_label = ttk.Label(
self.main_frame,
text="📷 图片OCR热量识别",
font=("Arial", 18, "bold"),
foreground="#2c3e50"
)
# 图片上传区域
self.image_frame = ttk.LabelFrame(
self.main_frame,
text="📸 图片上传",
padding=15,
relief="solid",
borderwidth=1
)
self.upload_button = ttk.Button(
self.image_frame,
text="📁 选择图片",
command=self._select_image,
style="Accent.TButton"
)
self.image_label = ttk.Label(
self.image_frame,
text="请选择包含食物信息的图片",
background="#f8f9fa",
relief="solid",
borderwidth=1,
width=50,
height=15,
anchor="center"
)
# 识别控制区域
self.control_frame = ttk.LabelFrame(
self.main_frame,
text="⚙️ 识别控制",
padding=15,
relief="solid",
borderwidth=1
)
self.recognize_button = ttk.Button(
self.control_frame,
text="🚀 开始识别",
command=self._start_recognition,
state=tk.DISABLED,
style="Accent.TButton"
)
self.progress_bar = ttk.Progressbar(
self.control_frame,
mode='indeterminate',
style="Accent.TProgressbar"
)
self.status_label = ttk.Label(
self.control_frame,
text="✅ 准备就绪",
foreground="#27ae60"
)
# 识别结果区域
self.result_frame = ttk.LabelFrame(self.main_frame, text="识别结果", padding=10)
# 创建结果表格
self.result_tree = ttk.Treeview(
self.result_frame,
columns=('food_name', 'calories', 'confidence', 'source'),
show='headings',
height=8
)
# 设置列标题
self.result_tree.heading('food_name', text='食物名称')
self.result_tree.heading('calories', text='热量(卡路里)')
self.result_tree.heading('confidence', text='置信度')
self.result_tree.heading('source', text='来源')
# 设置列宽
self.result_tree.column('food_name', width=150)
self.result_tree.column('calories', width=100)
self.result_tree.column('confidence', width=80)
self.result_tree.column('source', width=100)
# 结果操作按钮
self.result_button_frame = ttk.Frame(self.result_frame)
self.edit_button = ttk.Button(
self.result_button_frame,
text="编辑结果",
command=self._edit_result,
state=tk.DISABLED
)
self.confirm_button = ttk.Button(
self.result_button_frame,
text="确认结果",
command=self._confirm_result,
state=tk.DISABLED
)
self.clear_button = ttk.Button(
self.result_button_frame,
text="清空结果",
command=self._clear_results
)
# 详细信息区域
self.detail_frame = ttk.LabelFrame(self.main_frame, text="详细信息", padding=10)
self.detail_text = scrolledtext.ScrolledText(
self.detail_frame,
height=8,
width=60
)
# 建议区域
self.suggestion_frame = ttk.LabelFrame(self.main_frame, text="建议", padding=10)
self.suggestion_text = scrolledtext.ScrolledText(
self.suggestion_frame,
height=4,
width=60,
state=tk.DISABLED
)
def _setup_layout(self):
"""设置布局"""
# 标题
self.title_label.pack(pady=(0, 10))
# 图片上传区域
self.image_frame.pack(fill=tk.X, pady=(0, 10))
self.upload_button.pack(side=tk.LEFT, padx=(0, 10))
self.image_label.pack(fill=tk.BOTH, expand=True)
# 识别控制区域
self.control_frame.pack(fill=tk.X, pady=(0, 10))
self.recognize_button.pack(side=tk.LEFT, padx=(0, 10))
self.progress_bar.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
self.status_label.pack(side=tk.LEFT)
# 识别结果区域
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 结果表格
result_scrollbar = ttk.Scrollbar(self.result_frame, orient=tk.VERTICAL, command=self.result_tree.yview)
self.result_tree.configure(yscrollcommand=result_scrollbar.set)
self.result_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
result_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 结果操作按钮
self.result_button_frame.pack(fill=tk.X, pady=(10, 0))
self.edit_button.pack(side=tk.LEFT, padx=(0, 10))
self.confirm_button.pack(side=tk.LEFT, padx=(0, 10))
self.clear_button.pack(side=tk.LEFT)
# 详细信息和建议区域
detail_suggestion_frame = ttk.Frame(self.main_frame)
detail_suggestion_frame.pack(fill=tk.BOTH, expand=True)
self.detail_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
self.detail_text.pack(fill=tk.BOTH, expand=True)
self.suggestion_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.suggestion_text.pack(fill=tk.BOTH, expand=True)
def _bind_events(self):
"""绑定事件"""
self.result_tree.bind('<<TreeviewSelect>>', self._on_result_select)
self.result_tree.bind('<Double-1>', self._on_result_double_click)
def _select_image(self):
"""选择图片文件"""
file_types = [
("图片文件", "*.jpg *.jpeg *.png *.bmp *.gif"),
("JPEG文件", "*.jpg *.jpeg"),
("PNG文件", "*.png"),
("所有文件", "*.*")
]
file_path = filedialog.askopenfilename(
title="选择包含食物信息的图片",
filetypes=file_types
)
if file_path:
self.current_image_path = file_path
self._display_image(file_path)
self.recognize_button.config(state=tk.NORMAL)
self.status_label.config(text=f"已选择图片: {Path(file_path).name}")
def _display_image(self, image_path: str):
"""显示图片"""
try:
# 加载图片
image = Image.open(image_path)
# 调整图片大小以适应显示区域
display_size = (400, 300)
image.thumbnail(display_size, Image.Resampling.LANCZOS)
# 转换为Tkinter可显示的格式
photo = ImageTk.PhotoImage(image)
# 更新标签
self.image_label.config(image=photo, text="")
self.image_label.image = photo # 保持引用
except Exception as e:
messagebox.showerror("错误", f"无法显示图片: {str(e)}")
self.image_label.config(image="", text="图片显示失败")
def _start_recognition(self):
"""开始OCR识别"""
if not self.current_image_path:
messagebox.showwarning("警告", "请先选择图片")
return
# 禁用按钮,显示进度条
self.recognize_button.config(state=tk.DISABLED)
self.progress_bar.start()
self.status_label.config(text="正在识别...")
# 在新线程中执行识别
thread = threading.Thread(target=self._perform_recognition)
thread.daemon = True
thread.start()
def _perform_recognition(self):
"""执行OCR识别"""
try:
# 准备请求数据
request_data = {
'type': 'recognize_image',
'image_path': self.current_image_path
}
# 获取当前用户数据(这里需要根据实际情况调整)
user_data = UserData(
user_id="current_user",
profile={},
meals=[],
feedback=[],
preferences={}
)
# 调用OCR模块
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
ocr_module = OCRCalorieRecognitionModule(self.app_core.config)
if not ocr_module.initialize():
raise Exception("OCR模块初始化失败")
result = ocr_module.process(request_data, user_data)
# 在主线程中更新UI
self.parent_window.after(0, self._on_recognition_complete, result)
except Exception as e:
self.parent_window.after(0, self._on_recognition_error, str(e))
def _on_recognition_complete(self, result):
"""识别完成回调"""
try:
# 停止进度条
self.progress_bar.stop()
self.recognize_button.config(state=tk.NORMAL)
if result.result.get('success', False):
self.current_recognition_result = result.result['result']
self._display_recognition_results()
self.status_label.config(text="识别完成")
else:
error_msg = result.result.get('error', '识别失败')
messagebox.showerror("识别失败", error_msg)
self.status_label.config(text="识别失败")
except Exception as e:
self._on_recognition_error(str(e))
def _on_recognition_error(self, error_msg: str):
"""识别错误回调"""
self.progress_bar.stop()
self.recognize_button.config(state=tk.NORMAL)
self.status_label.config(text="识别失败")
messagebox.showerror("识别错误", f"OCR识别过程中出现错误: {error_msg}")
def _display_recognition_results(self):
"""显示识别结果"""
if not self.current_recognition_result:
return
try:
# 清空现有结果
self._clear_results()
# 显示热量信息
calorie_infos = self.current_recognition_result.calorie_infos
for info in calorie_infos:
self.result_tree.insert('', tk.END, values=(
info.food_name,
f"{info.calories:.1f}" if info.calories else "未知",
f"{info.confidence:.2f}",
info.source
))
# 显示详细信息
self._update_detail_text()
# 显示建议
self._update_suggestion_text()
# 启用操作按钮
self.edit_button.config(state=tk.NORMAL)
self.confirm_button.config(state=tk.NORMAL)
except Exception as e:
messagebox.showerror("错误", f"显示识别结果失败: {str(e)}")
def _update_detail_text(self):
"""更新详细信息文本"""
if not self.current_recognition_result:
return
try:
detail_text = "=== OCR识别详细信息 ===\n\n"
# OCR结果
detail_text += "OCR识别结果:\n"
for ocr_result in self.current_recognition_result.ocr_results:
detail_text += f"- 方法: {ocr_result.method}\n"
detail_text += f" 置信度: {ocr_result.confidence:.2f}\n"
detail_text += f" 识别文本: {ocr_result.text[:100]}...\n\n"
# 处理时间
detail_text += f"处理时间: {self.current_recognition_result.processing_time:.2f}\n"
detail_text += f"整体置信度: {self.current_recognition_result.overall_confidence:.2f}\n"
# 热量信息
detail_text += "\n=== 热量信息 ===\n"
for info in self.current_recognition_result.calorie_infos:
detail_text += f"食物: {info.food_name}\n"
detail_text += f"热量: {info.calories} 卡路里\n"
detail_text += f"置信度: {info.confidence:.2f}\n"
detail_text += f"来源: {info.source}\n"
detail_text += f"原始文本: {info.raw_text}\n\n"
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(1.0, detail_text)
except Exception as e:
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(1.0, f"详细信息加载失败: {str(e)}")
def _update_suggestion_text(self):
"""更新建议文本"""
if not self.current_recognition_result:
return
try:
suggestions = self.current_recognition_result.suggestions
suggestion_text = "=== 建议 ===\n\n"
for i, suggestion in enumerate(suggestions, 1):
suggestion_text += f"{i}. {suggestion}\n"
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.insert(1.0, suggestion_text)
self.suggestion_text.config(state=tk.DISABLED)
except Exception as e:
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.insert(1.0, f"建议加载失败: {str(e)}")
self.suggestion_text.config(state=tk.DISABLED)
def _on_result_select(self, event):
"""结果选择事件"""
selection = self.result_tree.selection()
if selection:
self.edit_button.config(state=tk.NORMAL)
self.confirm_button.config(state=tk.NORMAL)
else:
self.edit_button.config(state=tk.DISABLED)
self.confirm_button.config(state=tk.DISABLED)
def _on_result_double_click(self, event):
"""结果双击事件"""
self._edit_result()
def _edit_result(self):
"""编辑识别结果"""
selection = self.result_tree.selection()
if not selection:
messagebox.showwarning("警告", "请先选择要编辑的结果")
return
try:
item = selection[0]
values = self.result_tree.item(item, 'values')
food_name = values[0]
calories = values[1]
# 创建编辑对话框
self._create_edit_dialog(food_name, calories, item)
except Exception as e:
messagebox.showerror("错误", f"编辑结果失败: {str(e)}")
def _create_edit_dialog(self, food_name: str, calories: str, item_id: str):
"""创建编辑对话框"""
dialog = tk.Toplevel(self.parent_window)
dialog.title("编辑识别结果")
dialog.geometry("400x300")
dialog.resizable(False, False)
# 居中显示
dialog.transient(self.parent_window)
dialog.grab_set()
# 创建表单
form_frame = ttk.Frame(dialog, padding=20)
form_frame.pack(fill=tk.BOTH, expand=True)
# 食物名称
ttk.Label(form_frame, text="食物名称:").pack(anchor=tk.W, pady=(0, 5))
food_name_var = tk.StringVar(value=food_name)
food_name_entry = ttk.Entry(form_frame, textvariable=food_name_var, width=40)
food_name_entry.pack(fill=tk.X, pady=(0, 15))
# 热量
ttk.Label(form_frame, text="热量(卡路里):").pack(anchor=tk.W, pady=(0, 5))
calories_var = tk.StringVar(value=calories)
calories_entry = ttk.Entry(form_frame, textvariable=calories_var, width=40)
calories_entry.pack(fill=tk.X, pady=(0, 15))
# 置信度
ttk.Label(form_frame, text="置信度:").pack(anchor=tk.W, pady=(0, 5))
confidence_var = tk.StringVar(value="0.95")
confidence_entry = ttk.Entry(form_frame, textvariable=confidence_var, width=40)
confidence_entry.pack(fill=tk.X, pady=(0, 20))
# 按钮
button_frame = ttk.Frame(form_frame)
button_frame.pack(fill=tk.X)
def save_changes():
try:
new_food_name = food_name_var.get().strip()
new_calories = calories_var.get().strip()
new_confidence = float(confidence_var.get())
if not new_food_name:
messagebox.showwarning("警告", "食物名称不能为空")
return
if new_calories and new_calories != "未知":
try:
float(new_calories)
except ValueError:
messagebox.showwarning("警告", "热量必须是数字")
return
# 更新表格
self.result_tree.item(item_id, values=(
new_food_name,
new_calories,
f"{new_confidence:.2f}",
"user_corrected"
))
# 保存用户修正
self.user_corrections[new_food_name] = {
'calories': float(new_calories) if new_calories and new_calories != "未知" else None,
'confidence': new_confidence,
'timestamp': datetime.now().isoformat()
}
dialog.destroy()
messagebox.showinfo("成功", "结果已更新")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def cancel_changes():
dialog.destroy()
ttk.Button(button_frame, text="保存", command=save_changes).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(button_frame, text="取消", command=cancel_changes).pack(side=tk.LEFT)
def _confirm_result(self):
"""确认识别结果"""
if not self.current_recognition_result:
messagebox.showwarning("警告", "没有可确认的结果")
return
try:
# 获取所有结果
results = []
for item in self.result_tree.get_children():
values = self.result_tree.item(item, 'values')
results.append({
'food_name': values[0],
'calories': float(values[1]) if values[1] != "未知" else None,
'confidence': float(values[2]),
'source': values[3]
})
if not results:
messagebox.showwarning("警告", "没有可确认的结果")
return
# 确认对话框
confirm_msg = "确认以下识别结果:\n\n"
for i, result in enumerate(results, 1):
confirm_msg += f"{i}. {result['food_name']}: {result['calories']} 卡路里\n"
confirm_msg += "\n是否确认这些结果?"
if messagebox.askyesno("确认结果", confirm_msg):
# 保存到餐食记录
self._save_to_meal_record(results)
messagebox.showinfo("成功", "结果已保存到餐食记录")
# 清空结果
self._clear_results()
except Exception as e:
messagebox.showerror("错误", f"确认结果失败: {str(e)}")
def _save_to_meal_record(self, results: List[Dict[str, Any]]):
"""保存到餐食记录"""
try:
# 这里需要调用应用核心的餐食记录功能
# 暂时保存到本地文件
meal_record = {
'timestamp': datetime.now().isoformat(),
'source': 'ocr_recognition',
'foods': results,
'total_calories': sum(r['calories'] for r in results if r['calories'])
}
# 保存到文件
record_file = Path("data/ocr_meal_records.json")
record_file.parent.mkdir(parents=True, exist_ok=True)
records = []
if record_file.exists():
with open(record_file, 'r', encoding='utf-8') as f:
records = json.load(f)
records.append(meal_record)
with open(record_file, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
except Exception as e:
self.logger.error(f"保存餐食记录失败: {e}")
raise
def _clear_results(self):
"""清空识别结果"""
self.result_tree.delete(*self.result_tree.get_children())
self.detail_text.delete(1.0, tk.END)
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.config(state=tk.DISABLED)
self.edit_button.config(state=tk.DISABLED)
self.confirm_button.config(state=tk.DISABLED)
self.current_recognition_result = None
if __name__ == "__main__":
# 测试GUI
root = tk.Tk()
root.title("OCR热量识别测试")
root.geometry("800x600")
# 模拟应用核心
class MockAppCore:
def __init__(self):
self.config = type('Config', (), {})()
app_core = MockAppCore()
# 创建GUI
ocr_gui = OCRCalorieGUI(root, app_core)
root.mainloop()

526
gui/quick_user_input.py Normal file
View File

@@ -0,0 +1,526 @@
"""
快速用户需求录入界面
优化用户录入流程,提高效率
"""
import tkinter as tk
from tkinter import ttk, messagebox
import customtkinter as ctk
from typing import Dict, List, Optional
from datetime import datetime
import json
class QuickUserInputDialog:
"""快速用户需求录入对话框"""
def __init__(self, parent, user_id: str):
self.parent = parent
self.user_id = user_id
self.input_data = {}
# 创建对话框
self.dialog = ctk.CTkToplevel(parent)
self.dialog.title("快速需求录入")
self.dialog.geometry("800x600")
self.dialog.transient(parent)
self.dialog.grab_set()
# 居中显示
self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
self._create_widgets()
def _create_widgets(self):
"""创建界面组件"""
# 主框架
main_frame = ctk.CTkScrollableFrame(self.dialog)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
main_frame,
text="🚀 快速需求录入",
font=ctk.CTkFont(size=24, weight="bold")
)
title_label.pack(pady=20)
# 步骤1基础信息
self._create_basic_info_section(main_frame)
# 步骤2饮食偏好
self._create_preferences_section(main_frame)
# 步骤3健康目标
self._create_health_goals_section(main_frame)
# 步骤4快速餐食记录
self._create_quick_meal_section(main_frame)
# 按钮区域
self._create_buttons(main_frame)
def _create_basic_info_section(self, parent):
"""创建基础信息区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="1⃣ 基础信息",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 信息网格
info_frame = ctk.CTkFrame(section_frame)
info_frame.pack(fill="x", padx=20, pady=10)
# 姓名
name_label = ctk.CTkLabel(info_frame, text="姓名:")
name_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.name_var = tk.StringVar()
name_entry = ctk.CTkEntry(info_frame, textvariable=self.name_var, width=200)
name_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 年龄范围
age_label = ctk.CTkLabel(info_frame, text="年龄范围:")
age_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.age_var = tk.StringVar(value="25-30岁")
age_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.age_var,
values=["18-24岁", "25-30岁", "31-35岁", "36-40岁", "41-45岁", "46-50岁", "51-55岁", "56-60岁", "60岁以上"]
)
age_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 性别
gender_label = ctk.CTkLabel(info_frame, text="性别:")
gender_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.gender_var = tk.StringVar(value="")
gender_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.gender_var,
values=["", ""]
)
gender_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 身高体重范围
height_label = ctk.CTkLabel(info_frame, text="身高范围:")
height_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.height_var = tk.StringVar(value="160-165cm")
height_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.height_var,
values=["150cm以下", "150-155cm", "155-160cm", "160-165cm", "165-170cm", "170-175cm", "175-180cm", "180cm以上"]
)
height_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
weight_label = ctk.CTkLabel(info_frame, text="体重范围:")
weight_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.weight_var = tk.StringVar(value="50-55kg")
weight_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.weight_var,
values=["40kg以下", "40-45kg", "45-50kg", "50-55kg", "55-60kg", "60-65kg", "65-70kg", "70-75kg", "75-80kg", "80kg以上"]
)
weight_menu.grid(row=2, column=1, sticky="w", padx=10, pady=5)
# 活动水平
activity_label = ctk.CTkLabel(info_frame, text="活动水平:")
activity_label.grid(row=2, column=2, sticky="w", padx=10, pady=5)
self.activity_var = tk.StringVar(value="中等")
activity_menu = ctk.CTkOptionMenu(
info_frame,
variable=self.activity_var,
values=["久坐", "轻度活动", "中等", "高度活动", "极度活动"]
)
activity_menu.grid(row=2, column=3, sticky="w", padx=10, pady=5)
def _create_preferences_section(self, parent):
"""创建饮食偏好区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="2⃣ 饮食偏好",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 偏好网格
pref_frame = ctk.CTkFrame(section_frame)
pref_frame.pack(fill="x", padx=20, pady=10)
# 口味偏好
taste_label = ctk.CTkLabel(pref_frame, text="主要口味偏好:")
taste_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.taste_var = tk.StringVar(value="均衡")
taste_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.taste_var,
values=["均衡", "偏甜", "偏咸", "偏辣", "偏酸", "偏清淡"]
)
taste_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 饮食类型
diet_label = ctk.CTkLabel(pref_frame, text="饮食类型:")
diet_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.diet_var = tk.StringVar(value="普通饮食")
diet_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.diet_var,
values=["普通饮食", "素食", "低脂饮食", "低糖饮食", "高蛋白饮食", "地中海饮食"]
)
diet_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
# 过敏食物
allergy_label = ctk.CTkLabel(pref_frame, text="过敏食物:")
allergy_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.allergy_var = tk.StringVar(value="")
allergy_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.allergy_var,
values=["", "花生", "海鲜", "牛奶", "鸡蛋", "坚果", "大豆", "小麦", "其他"]
)
allergy_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 不喜欢食物
dislike_label = ctk.CTkLabel(pref_frame, text="不喜欢食物:")
dislike_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.dislike_var = tk.StringVar(value="")
dislike_menu = ctk.CTkOptionMenu(
pref_frame,
variable=self.dislike_var,
values=["", "内脏", "辛辣", "油腻", "甜食", "酸味", "苦味", "其他"]
)
dislike_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
def _create_health_goals_section(self, parent):
"""创建健康目标区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="3⃣ 健康目标",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 目标选择
goals_frame = ctk.CTkFrame(section_frame)
goals_frame.pack(fill="x", padx=20, pady=10)
# 主要目标
main_goal_label = ctk.CTkLabel(goals_frame, text="主要目标:")
main_goal_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.main_goal_var = tk.StringVar(value="保持健康")
main_goal_menu = ctk.CTkOptionMenu(
goals_frame,
variable=self.main_goal_var,
values=["保持健康", "减重", "增重", "增肌", "改善消化", "提高免疫力", "控制血糖", "降低血压"]
)
main_goal_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 次要目标
sub_goal_label = ctk.CTkLabel(goals_frame, text="次要目标:")
sub_goal_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
self.sub_goal_var = tk.StringVar(value="")
sub_goal_menu = ctk.CTkOptionMenu(
goals_frame,
variable=self.sub_goal_var,
values=["", "改善睡眠", "提高精力", "美容养颜", "延缓衰老", "改善皮肤", "增强记忆", "缓解压力"]
)
sub_goal_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
def _create_quick_meal_section(self, parent):
"""创建快速餐食记录区域"""
section_frame = ctk.CTkFrame(parent)
section_frame.pack(fill="x", padx=10, pady=10)
# 标题
title = ctk.CTkLabel(
section_frame,
text="4⃣ 快速餐食记录",
font=ctk.CTkFont(size=18, weight="bold")
)
title.pack(pady=10)
# 餐食选择
meal_frame = ctk.CTkFrame(section_frame)
meal_frame.pack(fill="x", padx=20, pady=10)
# 餐次选择
meal_type_label = ctk.CTkLabel(meal_frame, text="餐次:")
meal_type_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.meal_type_var = tk.StringVar(value="午餐")
meal_type_menu = ctk.CTkOptionMenu(
meal_frame,
variable=self.meal_type_var,
values=["早餐", "午餐", "晚餐", "加餐"]
)
meal_type_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
# 快速食物选择
food_label = ctk.CTkLabel(meal_frame, text="快速选择食物:")
food_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.quick_food_var = tk.StringVar(value="米饭+鸡肉+蔬菜")
quick_food_menu = ctk.CTkOptionMenu(
meal_frame,
variable=self.quick_food_var,
values=[
"米饭+鸡肉+蔬菜",
"面条+鸡蛋+青菜",
"馒头+豆腐+白菜",
"粥+咸菜+鸡蛋",
"面包+牛奶+水果",
"饺子+汤",
"包子+豆浆",
"其他"
]
)
quick_food_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
# 分量选择
portion_label = ctk.CTkLabel(meal_frame, text="分量:")
portion_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
self.portion_var = tk.StringVar(value="正常")
portion_menu = ctk.CTkOptionMenu(
meal_frame,
variable=self.portion_var,
values=["少量", "正常", "较多", "很多"]
)
portion_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
# 满意度
satisfaction_label = ctk.CTkLabel(meal_frame, text="满意度:")
satisfaction_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.satisfaction_var = tk.IntVar(value=3)
satisfaction_slider = ctk.CTkSlider(
meal_frame,
from_=1,
to=5,
number_of_steps=4,
variable=self.satisfaction_var
)
satisfaction_slider.grid(row=2, column=1, columnspan=2, sticky="ew", padx=10, pady=5)
# 满意度标签
self.satisfaction_display = ctk.CTkLabel(meal_frame, text="3分 - 一般")
self.satisfaction_display.grid(row=2, column=3, sticky="w", padx=10, pady=5)
# 绑定滑块事件
satisfaction_slider.configure(command=self._on_satisfaction_changed)
def _create_buttons(self, parent):
"""创建按钮区域"""
button_frame = ctk.CTkFrame(parent)
button_frame.pack(fill="x", padx=10, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="💾 保存所有信息",
command=self._save_all_data,
width=200,
height=50,
font=ctk.CTkFont(size=16, weight="bold")
)
save_button.pack(side="left", padx=20, pady=10)
# 取消按钮
cancel_button = ctk.CTkButton(
button_frame,
text="❌ 取消",
command=self._cancel,
width=150,
height=50
)
cancel_button.pack(side="right", padx=20, pady=10)
def _on_satisfaction_changed(self, value):
"""满意度改变事件"""
score = int(float(value))
score_texts = {
1: "1分 - 很不满意",
2: "2分 - 不满意",
3: "3分 - 一般",
4: "4分 - 满意",
5: "5分 - 很满意"
}
self.satisfaction_display.configure(text=score_texts.get(score, "3分 - 一般"))
def _save_all_data(self):
"""保存所有数据"""
try:
# 收集所有数据
self.input_data = {
'basic_info': {
'name': self.name_var.get(),
'age_range': self.age_var.get(),
'gender': self.gender_var.get(),
'height_range': self.height_var.get(),
'weight_range': self.weight_var.get(),
'activity_level': self.activity_var.get()
},
'preferences': {
'taste': self.taste_var.get(),
'diet_type': self.diet_var.get(),
'allergies': self.allergy_var.get(),
'dislikes': self.dislike_var.get()
},
'health_goals': {
'main_goal': self.main_goal_var.get(),
'sub_goal': self.sub_goal_var.get()
},
'quick_meal': {
'meal_type': self.meal_type_var.get(),
'food_combo': self.quick_food_var.get(),
'portion': self.portion_var.get(),
'satisfaction': self.satisfaction_var.get()
}
}
# 保存到数据库
if self._save_to_database():
messagebox.showinfo("成功", "所有信息保存成功!")
self.dialog.destroy()
else:
messagebox.showerror("错误", "保存失败,请重试")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def _save_to_database(self) -> bool:
"""保存到数据库"""
try:
from modules.data_collection import collect_questionnaire_data, record_meal
# 保存基础信息
basic_data = self.input_data['basic_info']
age_mapping = {
"18-24岁": 21, "25-30岁": 27, "31-35岁": 33, "36-40岁": 38,
"41-45岁": 43, "46-50岁": 48, "51-55岁": 53, "56-60岁": 58, "60岁以上": 65
}
height_mapping = {
"150cm以下": 150, "150-155cm": 152, "155-160cm": 157, "160-165cm": 162,
"165-170cm": 167, "170-175cm": 172, "175-180cm": 177, "180cm以上": 180
}
weight_mapping = {
"40kg以下": 40, "40-45kg": 42, "45-50kg": 47, "50-55kg": 52,
"55-60kg": 57, "60-65kg": 62, "65-70kg": 67, "70-75kg": 72,
"75-80kg": 77, "80kg以上": 80
}
activity_mapping = {
"久坐": "sedentary", "轻度活动": "light", "中等": "moderate",
"高度活动": "high", "极度活动": "very_high"
}
basic_answers = {
'name': basic_data['name'],
'age': age_mapping.get(basic_data['age_range'], 25),
'gender': basic_data['gender'],
'height': height_mapping.get(basic_data['height_range'], 165),
'weight': weight_mapping.get(basic_data['weight_range'], 55),
'activity_level': activity_mapping.get(basic_data['activity_level'], 'moderate')
}
collect_questionnaire_data(self.user_id, 'basic', basic_answers)
# 保存口味偏好
preferences_data = self.input_data['preferences']
taste_answers = {
'taste_preference': preferences_data['taste'],
'diet_type': preferences_data['diet_type'],
'allergies': [preferences_data['allergies']] if preferences_data['allergies'] != "" else [],
'dislikes': [preferences_data['dislikes']] if preferences_data['dislikes'] != "" else []
}
collect_questionnaire_data(self.user_id, 'taste', taste_answers)
# 保存健康目标
health_data = self.input_data['health_goals']
health_answers = {
'main_goal': health_data['main_goal'],
'sub_goal': health_data['sub_goal']
}
collect_questionnaire_data(self.user_id, 'health', health_answers)
# 保存快速餐食记录
meal_data = self.input_data['quick_meal']
meal_type_mapping = {
"早餐": "breakfast", "午餐": "lunch", "晚餐": "dinner", "加餐": "snack"
}
# 解析食物组合
food_combo = meal_data['food_combo']
if "+" in food_combo:
foods = [food.strip() for food in food_combo.split("+")]
else:
foods = [food_combo]
# 估算分量
portion_mapping = {
"少量": "1小份", "正常": "1份", "较多": "1大份", "很多": "2份"
}
quantities = [portion_mapping.get(meal_data['portion'], "1份")] * len(foods)
meal_record = {
'date': datetime.now().strftime('%Y-%m-%d'),
'meal_type': meal_type_mapping.get(meal_data['meal_type'], 'lunch'),
'foods': foods,
'quantities': quantities,
'satisfaction_score': meal_data['satisfaction']
}
record_meal(self.user_id, meal_record)
return True
except Exception as e:
print(f"保存到数据库失败: {e}")
return False
def _cancel(self):
"""取消录入"""
self.dialog.destroy()
# 便捷函数
def show_quick_user_input_dialog(parent, user_id: str):
"""显示快速用户需求录入对话框"""
dialog = QuickUserInputDialog(parent, user_id)
parent.wait_window(dialog.dialog)
if __name__ == "__main__":
# 测试快速录入对话框
root = tk.Tk()
root.title("测试快速录入")
def test_dialog():
show_quick_user_input_dialog(root, "test_user")
test_button = tk.Button(root, text="测试快速录入", command=test_dialog)
test_button.pack(pady=20)
root.mainloop()

579
gui/smart_meal_record.py Normal file
View File

@@ -0,0 +1,579 @@
"""
简化的餐食记录界面
使用选择式输入,减少用户负担
"""
import tkinter as tk
from tkinter import ttk, messagebox
import customtkinter as ctk
from typing import Dict, List, Optional
from smart_food.smart_database import (
search_foods, get_food_categories, get_foods_by_category,
get_portion_options, estimate_calories, record_meal_smart
)
class SmartMealRecordDialog:
"""智能餐食记录对话框"""
def __init__(self, parent, user_id: str, meal_type: str = "lunch"):
self.parent = parent
self.user_id = user_id
self.meal_type = meal_type
self.selected_foods = []
# 创建对话框
self.dialog = ctk.CTkToplevel(parent)
self.dialog.title(f"记录{self._get_meal_name(meal_type)}")
self.dialog.geometry("600x700")
self.dialog.transient(parent)
self.dialog.grab_set()
# 居中显示
self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
self._create_widgets()
def _get_meal_name(self, meal_type: str) -> str:
"""获取餐次中文名称"""
meal_names = {
"breakfast": "早餐",
"lunch": "午餐",
"dinner": "晚餐"
}
return meal_names.get(meal_type, "餐食")
def _create_widgets(self):
"""创建界面组件"""
# 主框架
main_frame = ctk.CTkScrollableFrame(self.dialog)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
main_frame,
text=f"🍽️ 记录{self._get_meal_name(self.meal_type)}",
font=ctk.CTkFont(size=20, weight="bold")
)
title_label.pack(pady=10)
# 食物选择区域
self._create_food_selection(main_frame)
# 已选食物列表
self._create_selected_foods(main_frame)
# 满意度评分
self._create_satisfaction_rating(main_frame)
# 备注
self._create_notes_section(main_frame)
# 按钮区域
self._create_buttons(main_frame)
def _create_food_selection(self, parent):
"""创建食物选择区域"""
# 食物选择框架
food_frame = ctk.CTkFrame(parent)
food_frame.pack(fill="x", padx=10, pady=10)
# 标题
food_title = ctk.CTkLabel(
food_frame,
text="选择食物",
font=ctk.CTkFont(size=16, weight="bold")
)
food_title.pack(pady=10)
# 食物搜索
search_frame = ctk.CTkFrame(food_frame)
search_frame.pack(fill="x", padx=10, pady=5)
search_label = ctk.CTkLabel(search_frame, text="搜索食物:")
search_label.pack(anchor="w", padx=5, pady=2)
self.search_var = tk.StringVar()
self.search_entry = ctk.CTkEntry(search_frame, textvariable=self.search_var, placeholder_text="输入食物名称搜索...")
self.search_entry.pack(fill="x", padx=5, pady=2)
self.search_entry.bind("<KeyRelease>", self._on_search_changed)
# 搜索结果
self.search_results_frame = ctk.CTkFrame(food_frame)
self.search_results_frame.pack(fill="x", padx=10, pady=5)
self.search_results_label = ctk.CTkLabel(self.search_results_frame, text="搜索结果:")
self.search_results_label.pack(anchor="w", padx=5, pady=2)
self.search_results_menu = ctk.CTkOptionMenu(
self.search_results_frame,
values=[],
command=self._on_search_result_selected
)
self.search_results_menu.pack(fill="x", padx=5, pady=2)
# 分隔线
separator = ctk.CTkFrame(food_frame, height=2)
separator.pack(fill="x", padx=10, pady=5)
# 分类选择
category_frame = ctk.CTkFrame(food_frame)
category_frame.pack(fill="x", padx=10, pady=5)
category_label = ctk.CTkLabel(category_frame, text="食物分类:")
category_label.pack(side="left", padx=5)
self.category_var = tk.StringVar(value="主食")
self.category_menu = ctk.CTkOptionMenu(
category_frame,
variable=self.category_var,
values=get_food_categories(),
command=self._on_category_changed
)
self.category_menu.pack(side="left", padx=5)
# 食物选择
food_select_frame = ctk.CTkFrame(food_frame)
food_select_frame.pack(fill="x", padx=10, pady=5)
food_label = ctk.CTkLabel(food_select_frame, text="选择食物:")
food_label.pack(anchor="w", padx=5, pady=2)
self.food_var = tk.StringVar()
self.food_menu = ctk.CTkOptionMenu(
food_select_frame,
variable=self.food_var,
values=[]
)
self.food_menu.pack(fill="x", padx=5, pady=2)
# 分量选择
portion_frame = ctk.CTkFrame(food_frame)
portion_frame.pack(fill="x", padx=10, pady=5)
portion_label = ctk.CTkLabel(portion_frame, text="分量:")
portion_label.pack(anchor="w", padx=5, pady=2)
self.portion_var = tk.StringVar(value="适量")
self.portion_menu = ctk.CTkOptionMenu(
portion_frame,
variable=self.portion_var,
values=["适量"]
)
self.portion_menu.pack(fill="x", padx=5, pady=2)
# 添加按钮
add_button = ctk.CTkButton(
food_frame,
text="添加到餐食",
command=self._add_food,
width=150
)
add_button.pack(pady=10)
# AI分析按钮
ai_analyze_button = ctk.CTkButton(
food_frame,
text="AI分析食物",
command=self._analyze_food_with_ai,
width=150,
fg_color="green"
)
ai_analyze_button.pack(pady=5)
# 初始化食物列表
self._on_category_changed("主食")
def _create_selected_foods(self, parent):
"""创建已选食物列表"""
# 已选食物框架
selected_frame = ctk.CTkFrame(parent)
selected_frame.pack(fill="x", padx=10, pady=10)
# 标题
selected_title = ctk.CTkLabel(
selected_frame,
text="已选食物",
font=ctk.CTkFont(size=16, weight="bold")
)
selected_title.pack(pady=10)
# 食物列表
self.foods_listbox = tk.Listbox(selected_frame, height=6)
self.foods_listbox.pack(fill="x", padx=10, pady=5)
# 删除按钮
delete_button = ctk.CTkButton(
selected_frame,
text="删除选中",
command=self._remove_food,
width=150
)
delete_button.pack(pady=5)
def _create_satisfaction_rating(self, parent):
"""创建满意度评分"""
# 满意度框架
satisfaction_frame = ctk.CTkFrame(parent)
satisfaction_frame.pack(fill="x", padx=10, pady=10)
# 标题
satisfaction_title = ctk.CTkLabel(
satisfaction_frame,
text="满意度评分",
font=ctk.CTkFont(size=16, weight="bold")
)
satisfaction_title.pack(pady=10)
# 评分滑块
self.satisfaction_var = tk.IntVar(value=3)
satisfaction_slider = ctk.CTkSlider(
satisfaction_frame,
from_=1,
to=5,
number_of_steps=4,
variable=self.satisfaction_var
)
satisfaction_slider.pack(fill="x", padx=20, pady=10)
# 评分标签
self.satisfaction_label = ctk.CTkLabel(
satisfaction_frame,
text="3分 - 一般",
font=ctk.CTkFont(size=14)
)
self.satisfaction_label.pack(pady=5)
# 绑定滑块事件
satisfaction_slider.configure(command=self._on_satisfaction_changed)
def _create_notes_section(self, parent):
"""创建备注区域"""
# 备注框架
notes_frame = ctk.CTkFrame(parent)
notes_frame.pack(fill="x", padx=10, pady=10)
# 标题
notes_title = ctk.CTkLabel(
notes_frame,
text="备注 (可选)",
font=ctk.CTkFont(size=16, weight="bold")
)
notes_title.pack(pady=10)
# 备注输入
self.notes_text = ctk.CTkTextbox(notes_frame, height=60)
self.notes_text.pack(fill="x", padx=10, pady=5)
self.notes_text.insert("1.0", "可以记录一些感受或特殊说明...")
def _create_buttons(self, parent):
"""创建按钮区域"""
# 按钮框架
button_frame = ctk.CTkFrame(parent)
button_frame.pack(fill="x", padx=10, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="保存餐食记录",
command=self._save_meal,
width=150,
height=40
)
save_button.pack(side="left", padx=20, pady=10)
# 取消按钮
cancel_button = ctk.CTkButton(
button_frame,
text="取消",
command=self._cancel,
width=150,
height=40
)
cancel_button.pack(side="right", padx=20, pady=10)
def _on_search_changed(self, event=None):
"""搜索输入改变事件"""
query = self.search_var.get().strip()
if not query:
self.search_results_menu.configure(values=[])
return
try:
from smart_food.smart_database import search_foods
results = search_foods(query)
if results:
food_names = [result["name"] for result in results]
self.search_results_menu.configure(values=food_names)
self.search_results_label.configure(text=f"搜索结果 ({len(food_names)}个):")
else:
self.search_results_menu.configure(values=[])
self.search_results_label.configure(text="未找到匹配的食物")
except Exception as e:
self.search_results_menu.configure(values=[])
self.search_results_label.configure(text="搜索失败")
def _on_search_result_selected(self, food_name):
"""搜索结果选择事件"""
self.food_var.set(food_name)
self._on_food_changed(food_name)
# 自动选择对应的分类
try:
from smart_food.smart_database import get_food_categories, get_foods_by_category
categories = get_food_categories()
for category in categories:
foods_in_category = get_foods_by_category(category)
if food_name in foods_in_category:
self.category_var.set(category)
self._on_category_changed(category)
break
except Exception:
pass
def _analyze_food_with_ai(self):
"""使用AI分析食物"""
food_name = self.food_var.get()
portion = self.portion_var.get()
if not food_name:
messagebox.showwarning("警告", "请选择食物")
return
try:
from smart_food.smart_database import analyze_food_with_ai
# 显示分析进度
self._show_ai_analysis_dialog(food_name, portion)
except Exception as e:
messagebox.showerror("错误", f"AI分析失败: {str(e)}")
def _show_ai_analysis_dialog(self, food_name: str, portion: str):
"""显示AI分析对话框"""
# 创建分析对话框
analysis_dialog = ctk.CTkToplevel(self.dialog)
analysis_dialog.title(f"AI分析 - {food_name}")
analysis_dialog.geometry("500x600")
analysis_dialog.transient(self.dialog)
analysis_dialog.grab_set()
# 居中显示
analysis_dialog.geometry("+%d+%d" % (self.dialog.winfo_rootx() + 50, self.dialog.winfo_rooty() + 50))
# 创建滚动框架
scroll_frame = ctk.CTkScrollableFrame(analysis_dialog)
scroll_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
scroll_frame,
text=f"🤖 AI分析: {food_name}",
font=ctk.CTkFont(size=18, weight="bold")
)
title_label.pack(pady=10)
# 分析进度
progress_label = ctk.CTkLabel(scroll_frame, text="正在分析中...")
progress_label.pack(pady=10)
# 分析结果区域
result_text = ctk.CTkTextbox(scroll_frame, height=400, width=450)
result_text.pack(fill="both", expand=True, pady=10)
# 关闭按钮
close_button = ctk.CTkButton(
scroll_frame,
text="关闭",
command=analysis_dialog.destroy,
width=100
)
close_button.pack(pady=10)
# 在后台线程中执行AI分析
import threading
def analyze_thread():
try:
from smart_food.smart_database import analyze_food_with_ai
# 执行AI分析
result = analyze_food_with_ai(food_name, portion)
# 更新UI
analysis_dialog.after(0, lambda: self._update_ai_analysis_result(
analysis_dialog, result_text, progress_label, result
))
except Exception as e:
analysis_dialog.after(0, lambda: self._update_ai_analysis_error(
analysis_dialog, result_text, progress_label, str(e)
))
threading.Thread(target=analyze_thread, daemon=True).start()
def _update_ai_analysis_result(self, dialog, result_text, progress_label, result):
"""更新AI分析结果"""
progress_label.configure(text="分析完成")
if result.get('success'):
content = f"""
🍎 食物分析结果: {result.get('reasoning', 'AI分析')}
📊 营养成分:
- 热量: {result.get('calories', 0)} 卡路里
- 蛋白质: {result.get('protein', 0):.1f}
- 碳水化合物: {result.get('carbs', 0):.1f}
- 脂肪: {result.get('fat', 0):.1f}
- 纤维: {result.get('fiber', 0):.1f}
🏷️ 分类: {result.get('category', '其他')}
💡 健康建议:
"""
for tip in result.get('health_tips', []):
content += f"{tip}\n"
content += "\n👨‍🍳 制作建议:\n"
for suggestion in result.get('cooking_suggestions', []):
content += f"{suggestion}\n"
content += f"\n🎯 置信度: {result.get('confidence', 0.5):.1%}"
else:
content = "AI分析失败请稍后重试。"
result_text.delete("1.0", "end")
result_text.insert("1.0", content)
def _update_ai_analysis_error(self, dialog, result_text, progress_label, error_msg):
"""更新AI分析错误"""
progress_label.configure(text="分析失败")
result_text.delete("1.0", "end")
result_text.insert("1.0", f"AI分析失败: {error_msg}")
def _on_category_changed(self, category):
"""分类改变事件"""
foods = get_foods_by_category(category)
self.food_menu.configure(values=foods)
if foods:
self.food_var.set(foods[0])
self._on_food_changed(foods[0])
def _on_food_changed(self, food_name):
"""食物改变事件"""
portions = get_portion_options(food_name)
self.portion_menu.configure(values=portions)
if portions:
self.portion_var.set(portions[0])
def _add_food(self):
"""添加食物"""
food_name = self.food_var.get()
portion = self.portion_var.get()
if not food_name:
messagebox.showwarning("警告", "请选择食物")
return
# 估算热量
calories = estimate_calories(food_name, portion)
# 添加到列表
food_item = {
"name": food_name,
"portion": portion,
"calories": calories
}
self.selected_foods.append(food_item)
self._update_foods_list()
def _update_foods_list(self):
"""更新食物列表显示"""
self.foods_listbox.delete(0, tk.END)
total_calories = 0
for i, food_item in enumerate(self.selected_foods):
display_text = f"{food_item['name']} - {food_item['portion']} ({food_item['calories']}卡)"
self.foods_listbox.insert(tk.END, display_text)
total_calories += food_item['calories']
# 显示总热量
if self.selected_foods:
total_text = f"总热量: {total_calories}卡路里"
self.foods_listbox.insert(tk.END, "")
self.foods_listbox.insert(tk.END, total_text)
def _remove_food(self):
"""删除选中的食物"""
selection = self.foods_listbox.curselection()
if not selection:
messagebox.showwarning("警告", "请选择要删除的食物")
return
index = selection[0]
if index < len(self.selected_foods):
self.selected_foods.pop(index)
self._update_foods_list()
def _on_satisfaction_changed(self, value):
"""满意度改变事件"""
score = int(float(value))
score_texts = {
1: "1分 - 很不满意",
2: "2分 - 不满意",
3: "3分 - 一般",
4: "4分 - 满意",
5: "5分 - 很满意"
}
self.satisfaction_label.configure(text=score_texts.get(score, "3分 - 一般"))
def _save_meal(self):
"""保存餐食记录"""
if not self.selected_foods:
messagebox.showwarning("警告", "请至少添加一种食物")
return
try:
# 构建餐食数据
meal_data = {
"meal_type": self.meal_type,
"foods": self.selected_foods,
"satisfaction_score": self.satisfaction_var.get(),
"notes": self.notes_text.get("1.0", "end-1c").strip()
}
# 智能记录餐食
if record_meal_smart(self.user_id, meal_data):
messagebox.showinfo("成功", "餐食记录保存成功!")
self.dialog.destroy()
else:
messagebox.showerror("错误", "餐食记录保存失败")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def _cancel(self):
"""取消记录"""
self.dialog.destroy()
# 便捷函数
def show_smart_meal_record_dialog(parent, user_id: str, meal_type: str = "lunch"):
"""显示智能餐食记录对话框"""
dialog = SmartMealRecordDialog(parent, user_id, meal_type)
parent.wait_window(dialog.dialog)
if __name__ == "__main__":
# 测试智能餐食记录对话框
root = tk.Tk()
root.title("测试智能餐食记录")
def test_dialog():
show_smart_meal_record_dialog(root, "test_user", "lunch")
test_button = tk.Button(root, text="测试餐食记录", command=test_dialog)
test_button.pack(pady=20)
root.mainloop()

326
gui/styles.py Normal file
View File

@@ -0,0 +1,326 @@
"""
界面美化样式配置
提供统一的圆角设计和颜色主题
"""
import tkinter as tk
from tkinter import ttk
import customtkinter as ctk
class StyleConfig:
"""样式配置类"""
# 颜色主题
COLORS = {
'primary': '#3498db',
'primary_hover': '#2980b9',
'secondary': '#2ecc71',
'secondary_hover': '#27ae60',
'accent': '#e74c3c',
'accent_hover': '#c0392b',
'warning': '#f39c12',
'warning_hover': '#e67e22',
'info': '#9b59b6',
'info_hover': '#8e44ad',
'success': '#27ae60',
'success_hover': '#229954',
'danger': '#e74c3c',
'danger_hover': '#c0392b',
# 背景色
'bg_light': '#ffffff',
'bg_dark': '#2b2b2b',
'bg_card': '#f8f9fa',
'bg_card_dark': '#3b3b3b',
'bg_container': '#f0f0f0',
'bg_container_dark': '#1e1e1e',
# 文字色
'text_primary': '#2c3e50',
'text_primary_dark': '#ecf0f1',
'text_secondary': '#34495e',
'text_secondary_dark': '#bdc3c7',
'text_muted': '#7f8c8d',
'text_muted_dark': '#95a5a6',
# 边框色
'border': '#e0e0e0',
'border_dark': '#404040',
'border_light': '#f0f0f0',
'border_light_dark': '#555555',
}
# 圆角半径
CORNER_RADIUS = {
'small': 8,
'medium': 12,
'large': 15,
'xlarge': 20,
'xxlarge': 25,
}
# 字体配置
FONTS = {
'title': ('Arial', 22, 'bold'),
'subtitle': ('Arial', 18, 'bold'),
'heading': ('Arial', 16, 'bold'),
'body': ('Arial', 14),
'small': ('Arial', 12),
'tiny': ('Arial', 10),
}
# 间距配置
SPACING = {
'xs': 5,
'sm': 10,
'md': 15,
'lg': 20,
'xl': 25,
'xxl': 30,
}
def apply_rounded_theme():
"""应用圆角主题到CustomTkinter"""
# 设置全局主题
ctk.set_appearance_mode("light")
ctk.set_default_color_theme("blue")
# 注意CustomTkinter不支持类级别的configure方法
# 样式需要在创建组件时单独设置
print("✅ 圆角主题已应用")
def create_rounded_frame(parent, **kwargs):
"""创建圆角框架"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': (colors['bg_light'], colors['bg_dark']),
'border_width': 1,
'border_color': (colors['border'], colors['border_dark']),
}
default_kwargs.update(kwargs)
return ctk.CTkFrame(parent, **default_kwargs)
def create_rounded_button(parent, text, **kwargs):
"""创建圆角按钮"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': (colors['primary'], colors['primary_hover']),
'hover_color': (colors['primary_hover'], colors['primary']),
'text_color': ('#ffffff', '#ffffff'),
'font': StyleConfig.FONTS['body'],
}
default_kwargs.update(kwargs)
return ctk.CTkButton(parent, text=text, **default_kwargs)
def create_rounded_entry(parent, **kwargs):
"""创建圆角输入框"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': (colors['bg_card'], colors['bg_card_dark']),
'border_width': 1,
'border_color': (colors['border_light'], colors['border_light_dark']),
'font': StyleConfig.FONTS['body'],
'text_color': (colors['text_primary'], colors['text_primary_dark']),
}
default_kwargs.update(kwargs)
return ctk.CTkEntry(parent, **default_kwargs)
def create_rounded_label(parent, text, **kwargs):
"""创建圆角标签"""
colors = StyleConfig.COLORS
default_kwargs = {
'text_color': (colors['text_primary'], colors['text_primary_dark']),
'font': StyleConfig.FONTS['body'],
}
default_kwargs.update(kwargs)
return ctk.CTkLabel(parent, text=text, **default_kwargs)
def create_card_frame(parent, **kwargs):
"""创建卡片式框架"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
default_kwargs = {
'corner_radius': radius['large'],
'fg_color': (colors['bg_card'], colors['bg_card_dark']),
'border_width': 1,
'border_color': (colors['border'], colors['border_dark']),
}
default_kwargs.update(kwargs)
return ctk.CTkFrame(parent, **default_kwargs)
def create_accent_button(parent, text, color_type='primary', **kwargs):
"""创建强调色按钮"""
colors = StyleConfig.COLORS
radius = StyleConfig.CORNER_RADIUS
color_map = {
'primary': (colors['primary'], colors['primary_hover']),
'secondary': (colors['secondary'], colors['secondary_hover']),
'accent': (colors['accent'], colors['accent_hover']),
'warning': (colors['warning'], colors['warning_hover']),
'info': (colors['info'], colors['info_hover']),
'success': (colors['success'], colors['success_hover']),
'danger': (colors['danger'], colors['danger_hover']),
}
fg_color, hover_color = color_map.get(color_type, color_map['primary'])
default_kwargs = {
'corner_radius': radius['medium'],
'fg_color': fg_color,
'hover_color': hover_color,
'text_color': ('#ffffff', '#ffffff'),
'font': StyleConfig.FONTS['body'],
}
default_kwargs.update(kwargs)
return ctk.CTkButton(parent, text=text, **default_kwargs)
def apply_ttk_styles():
"""应用TTK样式"""
style = ttk.Style()
# 配置样式
style.configure('Rounded.TFrame',
relief='solid',
borderwidth=1,
background='#f8f9fa')
style.configure('Rounded.TLabelFrame',
relief='solid',
borderwidth=1,
background='#f8f9fa')
style.configure('Accent.TButton',
relief='flat',
borderwidth=0,
background='#3498db',
foreground='white',
font=('Arial', 12, 'bold'))
style.map('Accent.TButton',
background=[('active', '#2980b9'),
('pressed', '#1f618d')])
style.configure('Accent.TProgressbar',
background='#3498db',
troughcolor='#ecf0f1',
borderwidth=0,
lightcolor='#3498db',
darkcolor='#3498db')
def get_spacing(size='md'):
"""获取间距值"""
return StyleConfig.SPACING.get(size, StyleConfig.SPACING['md'])
def get_font(font_type='body'):
"""获取字体配置"""
return StyleConfig.FONTS.get(font_type, StyleConfig.FONTS['body'])
def get_color(color_name):
"""获取颜色值"""
return StyleConfig.COLORS.get(color_name, StyleConfig.COLORS['text_primary'])
def get_radius(size='medium'):
"""获取圆角半径"""
return StyleConfig.CORNER_RADIUS.get(size, StyleConfig.CORNER_RADIUS['medium'])
# 预定义的样式组合
STYLE_PRESETS = {
'card': {
'corner_radius': 20,
'fg_color': ('#ffffff', '#3b3b3b'),
'border_width': 1,
'border_color': ('#e0e0e0', '#404040'),
},
'button_primary': {
'corner_radius': 15,
'fg_color': ('#3498db', '#2980b9'),
'hover_color': ('#2980b9', '#1f618d'),
'text_color': ('#ffffff', '#ffffff'),
},
'button_secondary': {
'corner_radius': 15,
'fg_color': ('#2ecc71', '#27ae60'),
'hover_color': ('#27ae60', '#229954'),
'text_color': ('#ffffff', '#ffffff'),
},
'button_accent': {
'corner_radius': 15,
'fg_color': ('#e74c3c', '#c0392b'),
'hover_color': ('#c0392b', '#a93226'),
'text_color': ('#ffffff', '#ffffff'),
},
'input_field': {
'corner_radius': 12,
'fg_color': ('#f8f9fa', '#404040'),
'border_width': 1,
'border_color': ('#e0e0e0', '#555555'),
},
}
def apply_preset_style(widget, preset_name):
"""应用预设样式"""
if preset_name in STYLE_PRESETS:
style_config = STYLE_PRESETS[preset_name]
for key, value in style_config.items():
if hasattr(widget, key):
setattr(widget, key, value)
if __name__ == "__main__":
# 测试样式配置
root = tk.Tk()
root.title("样式测试")
root.geometry("400x300")
# 应用TTK样式
apply_ttk_styles()
# 创建测试框架
main_frame = ttk.Frame(root, style='Rounded.TFrame')
main_frame.pack(fill='both', expand=True, padx=20, pady=20)
# 创建测试按钮
test_button = ttk.Button(main_frame, text="测试按钮", style='Accent.TButton')
test_button.pack(pady=10)
# 创建测试标签框架
test_frame = ttk.LabelFrame(main_frame, text="测试框架", style='Rounded.TLabelFrame')
test_frame.pack(fill='both', expand=True, pady=10)
test_label = ttk.Label(test_frame, text="这是一个测试标签")
test_label.pack(pady=20)
root.mainloop()