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 io
class SkillTracker:
def __init__(self, root):
self.root = root
self.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.lastrowid
self.conn.commit()
# 更新树形控件
self.tree.insert("", "end", iid=skill_id, text=name, open=True)
return True
except 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("错误", "请先选择要删除的项")
return
item_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("提示", "请先选择要删除的记录")
return
record_id = self.history_tree.item(selected[0], "values")[0]
if not messagebox.askyesno("确认删除", "确定要删除这条记录吗?"):
return
try:
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 = None
self.image_data = None
except 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:
pass
finally:
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 = photo
else:
label.config(image="")
label.image = None
def add_subskill(self):
"""添加子技能(修正后的版本)"""
selected = self.tree.selection()
if not selected:
messagebox.showerror("错误", "请先选择父级技能")
return
parent_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.conn
self.c.execute(
"INSERT INTO skills (name, parent_id, path) VALUES (?,?,?)",
(name, parent_id, new_path)
)
self.conn.commit()
skill_id = self.c.lastrowid
self.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, 0
FROM skills WHERE parent_id IS NULL
UNION ALL
SELECT s.id, s.name, s.parent_id, st.depth + 1
FROM skills s
JOIN 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 # 保存节点ID
except sqlite3.Error as e:
messagebox.showerror("数据库错误", f"加载技能树失败: {str(e)}")
def save_record(self):
skill_path = self.selected_skill['text']
if skill_path == "未选择":
messagebox.showerror("错误", "请先选择一个技能")
return
try:
score = self.score_var.get()
if not 1 <= score <= 10:
raise ValueError
except:
messagebox.showerror("错误", "请输入1-10之间的整数")
return
summary = 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 = None
self.image_preview.config(image="")
self.image_preview.image = None
self.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 = None
def 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 None
def 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("提示", "请先选择要 查看/编辑 的记录")
return
record_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()