Python实现技能记录系统

Python实现技能记录系统

来自网络,有改进。

技能记录系统界面如下:

Python实现技能记录系统_第1张图片

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

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

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

Python实现技能记录系统_第2张图片

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

本程序涉及如下模块/库

需要安装的库: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("<>", self.on_history_select)
        self.tree.bind("<>", 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()

你可能感兴趣的:(Python学习,编程实践系列,python,开发语言)