当前位置: 首页 > news >正文

Python实现技能记录系统

Python实现技能记录系统

技能记录系统界面如下:

具有保存图片和显示功能——允许用户选择图片保存,选择历史记录时若有图片可预览图片。

这个程序的数据保存在数据库skills2.db中,此数据库由用Python 自带的sqlite3数据库管理系统(不需要单独安装)管理,由程序自动维护(不需要用户操心),和程序文件在同一文件夹中。

“查看/编辑总结”的窗口上,有一个复选框 “允许编辑”,默认不选中 ,因此打开编辑总结的窗口时“保存”按钮处于不可用(灰色)状态,不能编辑文字,只有选中“允许编辑”复选框,方可编辑文字、保存。

此窗口设置为模态——确保用户完成编辑操作后才能返回主窗口。

本程序涉及如下模块/库

需要安装的库:Pillow (PIL)(通过 pip install pillow 安装)

Pillow (PIL) 是一个图像处理库,是第三方库,需要安装。用于处理图像文件。代码中使用了 Image 和 ImageTk,这些是 Pillow 的功能模块。

以下是 Python 标准库的一部分,通常不需要单独安装:

datetime:用于处理日期和时间。

io:用于处理输入输出流。

tkinter:用于创建图形用户界面。

sqlite3:用于操作 SQLite 数据库。

源码如下(部分代码参考自网络):

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
from datetime import datetime
from PIL import Image, ImageTk
import ioclass SkillTracker:def __init__(self, root):self.root = rootself.root.title("技能记录系统 v1.2.1")self.root.geometry("1500x760+0+0")self.conn = sqlite3.connect('skills2.db')self.c = self.conn.cursor()self.init_db()self.create_widgets()self.load_data()self.root.protocol("WM_DELETE_WINDOW", self.on_close)def init_db(self):try:self.c.execute('''CREATE TABLE IF NOT EXISTS skills(id INTEGER PRIMARY KEY, name TEXT NOT NULL, parent_id INTEGER,path TEXT UNIQUE)''')self.c.execute('''CREATE TABLE IF NOT EXISTS records(id INTEGER PRIMARY KEY,skill_path TEXT NOT NULL,score INTEGER CHECK(score BETWEEN 1 AND 10),date DATE DEFAULT CURRENT_DATE,summary TEXT,image BLOB)''')self.conn.commit()except sqlite3.Error as e:messagebox.showerror("数据库错误", f"初始化失败: {str(e)}")def add_category(self):"""添加大类"""name = self.get_input("新建大类名称:")if name:try:self.c.execute("INSERT INTO skills (name, path) VALUES (?, ?)",(name, name))skill_id = self.c.lastrowidself.conn.commit()# 更新树形控件self.tree.insert("", "end", iid=skill_id, text=name, open=True)return Trueexcept sqlite3.IntegrityError:messagebox.showerror("错误", "技能名称已存在")except sqlite3.Error as e:messagebox.showerror("数据库错误", f"添加失败: {str(e)}")return False                def delete_item(self):"""删除选中的技能项及其子项"""selected = self.tree.selection()if not selected:messagebox.showerror("错误", "请先选择要删除的项")returnitem_id = selected[0]item_name = self.tree.item(item_id)['text']# 确认对话框if not messagebox.askyesno("确认删除", f"确定要删除【{item_name}】及其所有子项吗?"):return# 递归删除数据库记录def delete_from_db(skill_id):self.c.execute("SELECT id FROM skills WHERE parent_id=?", (skill_id,))children = self.c.fetchall()for child in children:delete_from_db(child[0])self.c.execute("DELETE FROM skills WHERE id=?", (skill_id,))delete_from_db(item_id)self.conn.commit()self.tree.delete(item_id)messagebox.showinfo("成功", "删除完成")def delete_history(self):selected = self.history_tree.selection()if not selected:messagebox.showwarning("提示", "请先选择要删除的记录")returnrecord_id = self.history_tree.item(selected[0], "values")[0]if not messagebox.askyesno("确认删除", "确定要删除这条记录吗?"):returntry:self.c.execute("DELETE FROM records WHERE id=?", (record_id,))self.conn.commit()self.history_tree.delete(selected[0])messagebox.showinfo("成功", "记录已删除")# 清除图片预览self.image_preview.config(image="")self.image_preview.image = Noneself.image_data = Noneexcept sqlite3.Error as e:messagebox.showerror("数据库错误", f"删除失败: {str(e)}")def load_data(self):"""加载初始数据"""self.load_skill_tree()self.load_history()def on_close(self):"""统一的关闭处理"""try:self.conn.commit()self.conn.close()except Exception as e:passfinally:self.root.destroy()def create_widgets(self):# 左侧技能树面板left_frame = ttk.Frame(self.root)left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10)self.tree = ttk.Treeview(left_frame, show="tree")self.tree.pack(fill=tk.Y, expand=True)btn_frame = ttk.Frame(left_frame)ttk.Button(btn_frame, text="添加大类", command=self.add_category).pack(side=tk.LEFT, padx=2)ttk.Button(btn_frame, text="添加子项", command=self.add_subskill).pack(side=tk.LEFT, padx=2)ttk.Button(btn_frame, text="删除项", command=self.delete_item).pack(side=tk.LEFT, padx=2)btn_frame.pack(pady=5)# 中间输入面板center_frame = ttk.Frame(self.root)center_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)input_frame = ttk.LabelFrame(center_frame, text="今日录入")input_frame.pack(fill=tk.X, pady=5)ttk.Label(input_frame, text="当前技能:").grid(row=0, column=0, sticky=tk.W)self.selected_skill = ttk.Label(input_frame, text="未选择", foreground="blue")self.selected_skill.grid(row=0, column=1, sticky=tk.W)ttk.Label(input_frame, text="分数/等级 (1-10):").grid(row=1, column=0, sticky=tk.W)self.score_var = tk.IntVar()ttk.Spinbox(input_frame, from_=1, to=10, textvariable=self.score_var, width=5).grid(row=1, column=1)ttk.Label(input_frame, text="学习总结:").grid(row=2, column=0, sticky=tk.NW)self.summary_text = tk.Text(input_frame, height=8, width=40)self.summary_text.grid(row=2, column=1, pady=5)ttk.Button(input_frame, text="上传图片", command=self.upload_image).grid(row=3, column=0, pady=5)ttk.Button(input_frame, text="保存记录", command=self.save_record).grid(row=3, column=1, pady=5)# 图片预览区域(放在今日录入框下方)self.image_preview = ttk.Label(center_frame)self.image_preview.pack(pady=10)self.image_data = None# 右侧历史记录面板history_frame = ttk.Frame(self.root)history_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)btn_frame = ttk.Frame(history_frame)ttk.Button(btn_frame, text="删除记录", command=self.delete_history).pack(side=tk.LEFT, padx=5)ttk.Button(btn_frame, text="查找记录", command=self.search_records).pack(side=tk.LEFT, padx=5)ttk.Button(btn_frame, text="全部记录", command=self.load_history).pack(side=tk.LEFT, padx=5)ttk.Button(btn_frame, text="查看/编辑总结", command=self.edit_record).pack(side=tk.LEFT, padx=5)btn_frame.pack(fill=tk.X, pady=5)columns = ("id", "date", "skill", "score", "summary")self.history_tree = ttk.Treeview(history_frame,columns=columns,show="headings",selectmode="browse")# 配置可见列self.history_tree.heading("date", text="日期")self.history_tree.heading("skill", text="技能路径")self.history_tree.heading("score", text="评分/评级")self.history_tree.heading("summary", text="总结")# 配置列参数self.history_tree.column("date", width=120, anchor="center")self.history_tree.column("skill", width=200)self.history_tree.column("score", width=80, anchor="center")self.history_tree.column("summary", width=300)# 隐藏ID列self.history_tree.column("id", width=0, stretch=tk.NO)# 滚动条scrollbar = ttk.Scrollbar(history_frame, orient="vertical", command=self.history_tree.yview)self.history_tree.configure(yscrollcommand=scrollbar.set)# 布局self.history_tree.pack(side=tk.TOP, fill=tk.BOTH, expand=True)scrollbar.pack(side=tk.RIGHT, fill=tk.Y)# 绑定选择事件self.history_tree.bind("<<TreeviewSelect>>", self.on_history_select)self.tree.bind("<<TreeviewSelect>>", self.on_skill_select)def upload_image(self):file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")])if file_path:with open(file_path, "rb") as file:self.image_data = file.read()self.display_image(self.image_preview, self.image_data, (400, 400))  # 调整大小以适应您的布局def display_image(self, label, image_data, size):if image_data:image = Image.open(io.BytesIO(image_data))image.thumbnail(size)photo = ImageTk.PhotoImage(image)label.config(image=photo)label.image = photoelse:label.config(image="")label.image = Nonedef add_subskill(self):"""添加子技能(修正后的版本)"""selected = self.tree.selection()if not selected:messagebox.showerror("错误", "请先选择父级技能")returnparent_id = selected[0]name = self.get_input("新建子项名称:")if name:parent_path = self.get_skill_path(parent_id)new_path = f"{parent_path}/{name}"try:# 使用self.c和self.connself.c.execute("INSERT INTO skills (name, parent_id, path) VALUES (?,?,?)",(name, parent_id, new_path))self.conn.commit()skill_id = self.c.lastrowidself.tree.insert(parent_id, "end", iid=skill_id, text=name)except sqlite3.Error as e:messagebox.showerror("数据库错误", f"添加失败: {str(e)}")def load_skill_tree(self):"""技能树加载"""try:self.tree.delete(*self.tree.get_children())# 获取所有技能并按层级排序self.c.execute('''WITH RECURSIVE skill_tree(id, name, parent_id, depth) AS (SELECT id, name, parent_id, 0FROM skills WHERE parent_id IS NULLUNION ALLSELECT s.id, s.name, s.parent_id, st.depth + 1FROM skills sJOIN skill_tree st ON s.parent_id = st.id)SELECT * FROM skill_tree ORDER BY depth, parent_id''')# 创建临时存储父节点的字典nodes = {}for skill_id, name, parent_id, _ in self.c.fetchall():if parent_id is None:node = self.tree.insert("", "end", iid=skill_id, text=name)else:parent = nodes.get(parent_id)if parent:node = self.tree.insert(parent, "end", iid=skill_id, text=name)nodes[skill_id] = skill_id  # 保存节点IDexcept sqlite3.Error as e:messagebox.showerror("数据库错误", f"加载技能树失败: {str(e)}")def save_record(self):skill_path = self.selected_skill['text']if skill_path == "未选择":messagebox.showerror("错误", "请先选择一个技能")returntry:score = self.score_var.get()if not 1 <= score <= 10:raise ValueErrorexcept:messagebox.showerror("错误", "请输入1-10之间的整数")returnsummary = self.summary_text.get("1.0", tk.END).strip()date = datetime.now().strftime("%Y-%m-%d")try:self.c.execute("INSERT INTO records (skill_path, score, date, summary, image) VALUES (?,?,?,?,?)",(skill_path, score, date, summary, self.image_data))self.conn.commit()messagebox.showinfo("成功", "记录已保存!")self.summary_text.delete("1.0", tk.END)self.image_data = Noneself.image_preview.config(image="")self.image_preview.image = Noneself.load_history()except sqlite3.Error as e:messagebox.showerror("数据库错误", f"保存失败: {str(e)}")def load_history(self):try:self.history_tree.delete(*self.history_tree.get_children())self.c.execute("SELECT id, date, skill_path, score, summary FROM records ORDER BY date DESC, id DESC")for record in self.c.fetchall():self.history_tree.insert("", "end", values=record)except sqlite3.Error as e:messagebox.showerror("数据库错误", f"加载失败: {str(e)}")def on_history_select(self, event):selected = self.history_tree.selection()if selected:record_id = self.history_tree.item(selected[0], "values")[0]self.c.execute("SELECT image FROM records WHERE id=?", (record_id,))result = self.c.fetchone()if result:image_data = result[0]self.display_image(self.image_preview, image_data, (400, 400))else:self.image_preview.config(image="")self.image_preview.image = Nonedef get_skill_path(self, item_id):"""获取技能完整路径"""path = []while item_id:item = self.tree.item(item_id)path.append(item['text'])item_id = self.tree.parent(item_id)return '/'.join(reversed(path))def on_skill_select(self, event):selected = self.tree.selection()if selected:path = self.get_skill_path(selected[0])self.selected_skill.config(text=path)def get_input(self, prompt):"""获取用户输入"""dialog = tk.Toplevel()dialog.title("输入")ttk.Label(dialog, text=prompt).pack(padx=10, pady=5)entry = ttk.Entry(dialog)entry.pack(padx=10, pady=5)result = []def on_ok():result.append(entry.get())dialog.destroy()ttk.Button(dialog, text="确定", command=on_ok).pack(pady=5)dialog.wait_window()return result[0] if result else Nonedef search_records(self):search_window = tk.Toplevel(self.root)search_window.title("查找记录")ttk.Label(search_window, text="日期 (YYYY-MM-DD):").grid(row=0, column=0, padx=5, pady=5)date_entry = ttk.Entry(search_window)date_entry.grid(row=0, column=1, padx=5, pady=5)ttk.Label(search_window, text="技能路径:").grid(row=1, column=0, padx=5, pady=5)skill_entry = ttk.Entry(search_window)skill_entry.grid(row=1, column=1, padx=5, pady=5)def perform_search():date = date_entry.get()skill = skill_entry.get()query = "SELECT id, date, skill_path, score, summary FROM records WHERE 1=1"params = []if date:query += " AND date = ?"params.append(date)if skill:query += " AND skill_path LIKE ?"params.append(f"%{skill}%")query += " ORDER BY date DESC"try:self.c.execute(query, params)results = self.c.fetchall()self.history_tree.delete(*self.history_tree.get_children())for record in results:self.history_tree.insert("", "end", values=record)search_window.destroy()except sqlite3.Error as e:messagebox.showerror("查询错误", str(e))ttk.Button(search_window, text="查找", command=perform_search).grid(row=2, column=0, columnspan=2, pady=10)def edit_record(self):selected = self.history_tree.selection()if not selected:messagebox.showwarning("提示", "请先选择要 查看/编辑 的记录")returnrecord_id = self.history_tree.item(selected[0], "values")[0]# 获取当前记录信息self.c.execute("SELECT summary FROM records WHERE id=?", (record_id,))current_summary = self.c.fetchone()[0]edit_window = tk.Toplevel(self.root)edit_window.title(" 查看/编辑 记录")edit_window.geometry("400x300+360+280")ttk.Label(edit_window, text=" 查看/编辑 总结:").pack(padx=5, pady=5)summary_text = tk.Text(edit_window, height=8, width=40)summary_text.pack(padx=5, pady=5)summary_text.insert(tk.END, current_summary)summary_text.config(state='disabled')  # 初始状态设为禁用save_button = ttk.Button(edit_window, text="保存", state='disabled')save_button.pack(pady=10)def toggle_edit_state():if allow_edit_var.get():summary_text.config(state='normal')save_button.config(state='normal')else:summary_text.config(state='disabled')save_button.config(state='disabled')def save_edit():new_summary = summary_text.get("1.0", tk.END).strip()try:self.c.execute("UPDATE records SET summary=? WHERE id=?", (new_summary, record_id))self.conn.commit()messagebox.showinfo("成功", "记录已更新")edit_window.destroy()self.load_history()  # 刷新显示except sqlite3.Error as e:messagebox.showerror("数据库错误", f"更新失败: {str(e)}")# 添加允许编辑的复选框allow_edit_var = tk.BooleanVar()allow_edit_checkbox = ttk.Checkbutton(edit_window, text="允许编辑", variable=allow_edit_var, command=toggle_edit_state)allow_edit_checkbox.pack(pady=5)save_button.config(command=save_edit)# 添加取消按钮ttk.Button(edit_window, text="取消", command=edit_window.destroy).pack(pady=5)# 使窗口成为模态窗口edit_window.transient(self.root)edit_window.grab_set()self.root.wait_window(edit_window)if __name__ == "__main__":root = tk.Tk()app = SkillTracker(root)root.mainloop()

相关文章:

  • 【华为OD机试真题】428、连续字母长度 | 机试真题+思路参考+代码解析(E卷)(C++)
  • Browser-Use WebUI:让AI自动使用浏览器帮你查询信息执行任务
  • StableDiffusionPipeline原理解读——引导尺度是如何调整噪声残差的
  • 【C语言经典算法实战】:从“移动距离”问题看矩阵坐标计算
  • 审计效率升级!快速匹配Excel报表项目对应的Word附注序号
  • Ubuntu / WSL 安装pipx
  • E3650工具链生态再增强,IAR全面支持芯驰科技新一代旗舰智控MCU
  • unity使用iTextSharp生成PDF文件
  • 焊接机排错
  • Qt 入门 6 之布局管理
  • spring-ai使用Document存储至milvus的数据结构
  • 【MongoDB + Spark】 技术问题汇总与解决方案笔记
  • JavaScript学习教程,从入门到精通,XMLHttpRequest 与 Ajax 请求详解(25)
  • java 富文本转pdf
  • C#源码分析 --- Random
  • 深度解析:基于Python的微信小程序自动化操作实现
  • MySQL存储STM32F407上的HX711数据
  • 高光谱相机在生物医学中的应用:病理分析、智慧中医与成分分析
  • 【C++】模版初阶:函数模板、类模板
  • 1.1 java开发的准备工作(入门)
  • 商务部:汽车流通消费改革试点正在加快推进
  • 四川公布一起影视盗版案例:1个网站2人团伙盗售30多万部
  • 证券时报:落实“非禁即入” ,让创新活力充分涌流
  • 建设高标准农田主要目标是什么?有哪些安排?两部门有关负责人答问
  • 河南省鹤壁市人大常委会副主任李杰接受审查调查
  • 法治日报:强制统一店铺广告牌匾事件何以频发?