【Python开源】打造高颜值桌面便签工具:Tkinter实现智能提醒+任务管理(附完整源码)

📝【Python开源】打造高颜值桌面便签工具:Tkinter实现智能提醒+任务管理(附完整源码)

请添加图片描述

🌈 个人主页:创客白泽 - CSDN博客
🔥 系列专栏:🐍《Python开源项目实战》
💡 热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦

请添加图片描述
在这里插入图片描述

🌟 概述:为什么我们需要一个桌面便签工具?

在快节奏的现代生活中,高效的任务管理成为提升生产力的关键。市面上的待办事项应用虽然功能丰富,但往往存在以下痛点:

  1. 需要联网使用,隐私性差
  2. 界面复杂,启动缓慢
  3. 无法深度自定义功能

本文将带你用Python的Tkinter库开发一款轻量级桌面便签工具,具备以下亮点功能:

  • 📅 日期分组管理
  • ⏰ 智能提醒系统
  • ✅ 任务完成状态追踪
  • 🎨 简洁美观的UI设计
  • 💾 本地化数据存储

(开发环境:Python 3.8+,无需额外安装库)

🛠️ 核心功能实现详解

1. 整体架构设计

class TodoApp:
    def __init__(self, root):
        self.tasks_by_date = {}  # 按日期分组的任务字典
        self.group_states = {}   # 分组展开状态
        self.create_widgets()    # 初始化UI
        self.load_tasks()       # 加载本地任务
        self.check_alarms_on_startup()  # 启动时检查提醒
        self.update_time()      # 实时时钟

2. 智能提醒系统实现

核心算法通过比较当前时间与预设提醒时间实现精准提醒:

def set_alarm_timer(self, alarm_time, alarm_message, task_text, add_time, date):
    delay = (alarm_time - datetime.now()).total_seconds() * 1000
    self.root.after(int(delay), lambda: self.show_alarm(...))

def check_alarms_on_startup(self):
    # 启动时检查所有未完成任务的提醒时间
    for date in self.tasks_by_date:
        for task in self.tasks_by_date[date]:
            if task[4] != "无" and not task[1]:
                alarm_time = datetime.strptime(task[4], "%Y-%m-%d %H:%M:%S")
                if alarm_time <= datetime.now():
                    self.show_alarm(...)

3. 任务状态管理逻辑

当用户标记任务完成时,系统会进行时间校验:

def update_completion_time(self, var, time_label, task_text, add_time, date):
    if var.get():  # 如果标记为完成
        completion_time = datetime.now()
        # 检查提醒时间是否晚于完成时间
        if alarm_time > completion_time:
            var.set(False)  # 取消完成状态
            messagebox.showinfo("提示", "提醒时间晚于完成时间...")

4. 数据持久化方案

采用CSV格式存储任务数据,每行包含:

任务内容,完成状态,添加时间,完成时间,提醒时间,提醒信息

读写实现:

def load_tasks(self):
    if os.path.exists('tasks.csv'):
        with open('tasks.csv', 'r', encoding='utf-8') as file:
            reader = csv.reader(file)
            for row in reader:
                # 解析每行数据...

def save_tasks(self, silent=False):
    with open('tasks.csv', 'w', encoding='utf-8', newline='') as file:
        writer = csv.writer(file)
        for date in self.tasks_by_date:
            for task in self.tasks_by_date[date]:
                writer.writerow(list(task))

🎨 UI设计技巧与优化

1. 现代化界面布局

![界面结构图]

主界面结构:
- 顶部:标题+实时时钟
- 中部:输入框+添加按钮
- 底部:带滚动条的任务显示区域

2. 交互细节优化

# 平滑滚动实现
def _on_mousewheel(self, event):
    self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

# 分组折叠功能
def toggle_group(self, date, button):
    self.group_states[date] = not self.group_states.get(date, True)
    self.refresh_display()

3. 颜色方案

元素颜色值用途
背景色#f0f0f0主窗口背景
任务卡片#fafafa任务项背景
完成文本#2E7D32已完成任务文字
未完成文本#757575未完成任务文字

💡 功能扩展思路

  1. 云同步功能

    • 添加Dropbox/Google Drive API支持
    • 实现多设备同步
  2. 分类标签系统

   def add_tag(self, task_id, tag_name):
       # 为任务添加彩色标签
       pass
  1. 数据统计分析

    • 使用matplotlib生成任务完成率图表
    • 导出周报/月报
  2. 快捷键支持

  self.root.bind('<Control-n>', lambda e: self.add_task())
  self.root.bind('<Control-s>', lambda e: self.save_tasks())

运行效果

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🏆 项目总结与收获

通过本项目的开发,我们掌握了:

  1. Tkinter高级组件的综合运用
  2. Python时间处理的最佳实践
  3. 状态管理与数据持久化方案
  4. 用户体验优化的具体方法

遇到的典型问题及解决方案:

  • 问题:滚动区域内容更新时闪烁
  • 解决:使用Canvas+Frame组合代替直接滚动Frame
  • 问题:提醒时间与完成状态冲突
  • 解决:添加时间逻辑校验

📥 完整源码获取

import tkinter as tk
from tkinter import messagebox
import csv
import os
from datetime import datetime

class TodoApp:
    def __init__(self, root):
        self.root = root
        self.root.title("桌面便签工具")
        self.root.geometry("390x600")
        self.root.configure(bg="#f0f0f0")

        self.tasks_by_date = {}
        self.group_states = {}

        self.create_widgets()
        self.load_tasks()
        self.check_alarms_on_startup()
        self.update_time()  # 启动时间更新

    def create_widgets(self):
        # 标题
        title_label = tk.Label(self.root, text="待办事项", font=("SimSun", 16, "bold"), bg="#f0f0f0", fg="#333333")
        title_label.pack(pady=10)

        # 输入框架
        input_frame = tk.Frame(self.root, bg="#f0f0f0")
        input_frame.pack(pady=5)

        # 当前时间显示
        self.time_label = tk.Label(input_frame, text="", font=("SimSun", 12), bg="#f0f0f0", fg="#333333")
        self.time_label.pack(pady=(0, 5))

        # 输入框
        self.entry = tk.Entry(input_frame, width=40, font=("SimSun", 12), bd=2, relief="flat")
        self.entry.pack(pady=(0, 5))  # 修改为垂直布局
        self.entry.focus_set()

        # 添加按钮
        add_button = tk.Button(input_frame, text="添加", command=self.add_task, 
                             font=("SimSun", 10), bg="#4CAF50", fg="white", bd=0, padx=10, pady=5)
        add_button.pack()  # 放在输入框下方

        # 任务显示框架
        self.task_frame = tk.Frame(self.root, bg="#ffffff", bd=1, relief="solid")
        self.task_frame.pack(pady=10, padx=10, fill=tk.BOTH, expand=True)

        self.canvas = tk.Canvas(self.task_frame, bg="#ffffff", highlightthickness=0)
        self.scrollbar = tk.Scrollbar(self.task_frame, orient="vertical", command=self.canvas.yview)
        self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.canvas.configure(yscrollcommand=self.scrollbar.set)

        self.inner_frame = tk.Frame(self.canvas, bg="#ffffff")
        self.canvas.create_window((0, 0), window=self.inner_frame, anchor="nw")

        self.inner_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
        self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)

    def update_time(self):
        """更新当前时间显示"""
        current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.time_label.config(text=f"当前时间: {current_time}")
        self.root.after(1000, self.update_time)  # 每秒更新一次

    def _on_mousewheel(self, event):
        self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")

    def toggle_group(self, date, button):
        is_expanded = self.group_states.get(date, True)
        self.group_states[date] = not is_expanded
        button.config(text=f"{'▼' if not is_expanded else '▶'} {date}")
        self.refresh_display()
        self.save_tasks(silent=True)

    def update_completion_time(self, var, time_label, task_text, add_time, date):
        alarm_time_str = "无"
        alarm_message = "无"
        for task in self.tasks_by_date[date]:
            if task[0] == task_text and task[2] == add_time:
                alarm_time_str = task[4]
                alarm_message = task[5]
                break

        if var.get():
            completion_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            if alarm_time_str != "无":
                try:
                    alarm_time = datetime.strptime(alarm_time_str, "%Y-%m-%d %H:%M:%S")
                    completion_datetime = datetime.strptime(completion_time, "%Y-%m-%d %H:%M:%S")
                    if alarm_time > completion_datetime:
                        var.set(False)
                        messagebox.showinfo("提示", f"任务 '{task_text}' 的提醒时间晚于完成时间,已取消完成状态并保留提醒")
                        completion_time = "未完成"
                        time_label.config(text=f"添加时间: {add_time}\n完成时间: {completion_time}", fg="#757575")
                        for i, (t_text, _, a_time, _, at, am) in enumerate(self.tasks_by_date[date]):
                            if t_text == task_text and a_time == add_time:
                                self.tasks_by_date[date][i] = (task_text, False, add_time, completion_time, at, am)
                                self.set_alarm_timer(alarm_time, alarm_message, task_text, add_time, date)
                                break
                        self.save_tasks(silent=True)
                        self.refresh_display()
                        return
                except ValueError:
                    pass

            time_label.config(text=f"添加时间: {add_time}\n完成时间: {completion_time}", fg="#2E7D32")
            for i, (t_text, _, a_time, _, at, am) in enumerate(self.tasks_by_date[date]):
                if t_text == task_text and a_time == add_time:
                    self.tasks_by_date[date][i] = (task_text, True, add_time, completion_time, at, am)
                    break
        else:
            time_label.config(text=f"添加时间: {add_time}\n完成时间: 未完成", fg="#757575")
            for i, (t_text, _, a_time, _, at, am) in enumerate(self.tasks_by_date[date]):
                if t_text == task_text and a_time == add_time:
                    self.tasks_by_date[date][i] = (task_text, False, add_time, "未完成", at, am)
                    if alarm_time_str != "无":
                        try:
                            alarm_time = datetime.strptime(alarm_time_str, "%Y-%m-%d %H:%M:%S")
                            if alarm_time > datetime.now():
                                self.set_alarm_timer(alarm_time, alarm_message, task_text, add_time, date)
                        except ValueError:
                            pass
                    break
        self.save_tasks(silent=True)
        self.refresh_display()

    def edit_task(self, task_text, add_time, date, label):
        edit_window = tk.Toplevel(self.root)
        edit_window.title("编辑任务")
        edit_window.geometry("300x100")
        edit_window.configure(bg="#f0f0f0")

        tk.Label(edit_window, text="任务内容:", bg="#f0f0f0").pack(pady=5)
        entry = tk.Entry(edit_window, width=40, font=("SimSun", 11))
        entry.pack(pady=5)
        entry.insert(0, task_text)

        def save_edit():
            new_text = entry.get().strip()
            if new_text:
                for i, (t_text, completed, a_time, c_time, at, am) in enumerate(self.tasks_by_date[date]):
                    if t_text == task_text and a_time == add_time:
                        self.tasks_by_date[date][i] = (new_text, completed, a_time, c_time, at, am)
                        break
                self.refresh_display()
                self.save_tasks(silent=True)
                edit_window.destroy()
            else:
                messagebox.showwarning("警告", "任务内容不能为空!")

        tk.Button(edit_window, text="保存", command=save_edit, bg="#4CAF50", fg="white", bd=0).pack(pady=5)

    # 删除功能2025.4.2已修改
    def delete_task(self, task_text, add_time, date):
        if messagebox.askyesno("确认", "确定删除此任务吗?"):
            for i, (t_text, _, a_time, _, _, _) in enumerate(self.tasks_by_date[date]):
                if t_text == task_text and a_time == add_time:
                    del self.tasks_by_date[date][i]
                    break
            if not self.tasks_by_date[date]:
                del self.tasks_by_date[date]
                del self.group_states[date]
            self.refresh_display()
            self.save_tasks(silent=True)

    def add_task(self):
        task_text = self.entry.get().strip()
        if task_text:
            add_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            date = add_time.split()[0]
            if date not in self.tasks_by_date:
                self.tasks_by_date[date] = []
                self.group_states[date] = True
            self.tasks_by_date[date].insert(0, (task_text, False, add_time, "未完成", "无", "无"))
            self.refresh_display()
            self.entry.delete(0, tk.END)
            self.save_tasks(silent=True)
        else:
            messagebox.showwarning("警告", "请输入任务内容!")

    def set_alarm(self, task_text, add_time, date):
        alarm_window = tk.Toplevel(self.root)
        alarm_window.title("设置提醒")
        alarm_window.geometry("400x300")
        alarm_window.configure(bg="#f0f0f0")

        tk.Label(alarm_window, text="提醒日期 (格式: YYYYMMDD, 留空默认当天):", bg="#f0f0f0").pack(pady=5)
        date_entry = tk.Entry(alarm_window, width=40, font=("SimSun", 11))
        date_entry.pack(pady=5)

        tk.Label(alarm_window, text="提醒时间 (格式: HHMMSS或HHMM, 无秒默认为00):", bg="#f0f0f0").pack(pady=5)
        time_entry = tk.Entry(alarm_window, width=40, font=("SimSun", 11))
        time_entry.pack(pady=5)

        tk.Label(alarm_window, text="提醒信息:", bg="#f0f0f0").pack(pady=5)
        message_entry = tk.Entry(alarm_window, width=40, font=("SimSun", 11))
        message_entry.pack(pady=5)

        def save_alarm():
            alarm_date_str = date_entry.get().strip()
            alarm_time_str = time_entry.get().strip()
            alarm_message = message_entry.get().strip()

            if not alarm_date_str:
                alarm_date_str = datetime.now().strftime("%Y%m%d")

            try:
                if len(alarm_time_str) == 4:
                    alarm_time_str += "00"
                alarm_datetime_str = f"{alarm_date_str} {alarm_time_str}"
                alarm_time = datetime.strptime(alarm_datetime_str, "%Y%m%d %H%M%S")
                if alarm_time < datetime.now():
                    messagebox.showwarning("警告", "提醒时间不能早于当前时间!")
                    return

                for i, (t_text, completed, a_time, c_time, _, _) in enumerate(self.tasks_by_date[date]):
                    if t_text == task_text and a_time == add_time:
                        if completed and c_time != "未完成":
                            completion_datetime = datetime.strptime(c_time, "%Y-%m-%d %H:%M:%S")
                            if alarm_time > completion_datetime:
                                completed = False
                                c_time = "未完成"
                                messagebox.showinfo("提示", f"任务 '{task_text}' 的提醒时间晚于完成时间,已取消完成状态")

                        self.tasks_by_date[date][i] = (task_text, completed, add_time, c_time, 
                                                    alarm_time.strftime("%Y-%m-%d %H:%M:%S"), alarm_message)
                        break

                if not completed:
                    self.set_alarm_timer(alarm_time, alarm_message, task_text, add_time, date)

                self.save_tasks(silent=True)
                self.refresh_display()
                alarm_window.destroy()
            except ValueError:
                messagebox.showwarning("警告", "时间格式不正确,请使用 YYYYMMDD HHMMSS 或 YYYYMMDD HHMM 格式!")

        tk.Button(alarm_window, text="保存", command=save_alarm, bg="#4CAF50", fg="white", bd=0).pack(pady=5)

    def set_alarm_timer(self, alarm_time, alarm_message, task_text, add_time, date):
        delay = (alarm_time - datetime.now()).total_seconds() * 1000
        self.root.after(int(delay), lambda: self.show_alarm(alarm_message, task_text, add_time, date))

    def show_alarm(self, alarm_message, task_text, add_time, date):
        for task in self.tasks_by_date[date]:
            if task[0] == task_text and task[2] == add_time and not task[1]:
                messagebox.showinfo("提醒", f"任务: {task_text}\n信息: {alarm_message}")
                break

    def check_alarms_on_startup(self):
        current_time = datetime.now()
        for date in self.tasks_by_date:
            for i, (task_text, completed, add_time, completion_time, alarm_time_str, alarm_message) in enumerate(self.tasks_by_date[date]):
                if alarm_time_str != "无":
                    try:
                        alarm_time = datetime.strptime(alarm_time_str, "%Y-%m-%d %H:%M:%S")
                        if completed and completion_time != "未完成":
                            completion_datetime = datetime.strptime(completion_time, "%Y-%m-%d %H:%M:%S")
                            if alarm_time > completion_datetime:
                                self.tasks_by_date[date][i] = (task_text, False, add_time, "未完成", alarm_time_str, alarm_message)
                                messagebox.showinfo("提示", f"任务 '{task_text}' 的提醒时间晚于完成时间,已取消完成状态")
                                if alarm_time <= current_time:
                                    self.show_alarm(alarm_message, task_text, add_time, date)
                                else:
                                    self.set_alarm_timer(alarm_time, alarm_message, task_text, add_time, date)
                        elif not completed:
                            if alarm_time <= current_time:
                                self.show_alarm(alarm_message, task_text, add_time, date)
                            else:
                                self.set_alarm_timer(alarm_time, alarm_message, task_text, add_time, date)
                    except ValueError:
                        continue
        self.refresh_display()
        self.save_tasks(silent=True)

    def create_task_frame(self, task_text, completed, add_time, completion_time, date):
        task_frame = tk.Frame(self.inner_frame, bg="#fafafa", bd=1, relief="solid", padx=8, pady=8)

        var = tk.BooleanVar(value=completed)
        checkbox = tk.Checkbutton(task_frame, variable=var, bg="#fafafa")
        checkbox.pack(side=tk.LEFT, padx=(0, 5))

        content_frame = tk.Frame(task_frame, bg="#fafafa")
        content_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)

        label = tk.Label(content_frame, text=task_text, font=("SimSun", 11, "bold"), 
                        bg="#fafafa", fg="#333333", wraplength=300, justify="left")
        label.pack(anchor="w")

        alarm_time = "无"
        alarm_message = "无"
        for t in self.tasks_by_date[date]:
            if t[0] == task_text and t[2] == add_time and len(t) >= 6:
                alarm_time = t[4]
                alarm_message = t[5]
                break

        time_text = f"添加时间: {add_time}\n完成时间: {completion_time if completed else '未完成'}"
        if alarm_time != "无" and not completed:
            time_text += f"\n提醒时间: {alarm_time}\n提醒信息: {alarm_message}"

        time_label = tk.Label(content_frame, text=time_text, 
                            font=("SimSun", 9), bg="#fafafa", 
                            fg="#2E7D32" if completed else "#757575",
                            height=4, justify="left")
        time_label.pack(anchor="w", pady=2)

        button_frame = tk.Frame(task_frame, bg="#fafafa")
        button_frame.pack(side=tk.RIGHT, padx=5)

        edit_button = tk.Button(button_frame, text="编辑", font=("SimSun", 11), bg="#2196F3", fg="white", bd=0,
                              command=lambda: self.edit_task(task_text, add_time, date, label))
        edit_button.pack(side=tk.TOP, pady=2)

        delete_button = tk.Button(button_frame, text="删除", font=("SimSun", 11), bg="#F44336", fg="white", bd=0,
                                command=lambda: self.delete_task(task_text, add_time, date))
        delete_button.pack(side=tk.TOP, pady=2)

        alarm_button = tk.Button(button_frame, text="提醒", font=("SimSun", 11), bg="#FF9800", fg="white", bd=0,
                                command=lambda: self.set_alarm(task_text, add_time, date))
        alarm_button.pack(side=tk.TOP, pady=2)

        var.trace("w", lambda *args: self.update_completion_time(var, time_label, task_text, add_time, date))
        return task_frame

    def refresh_display(self):
        for widget in self.inner_frame.winfo_children():
            widget.destroy()

        for date in sorted(self.tasks_by_date.keys(), reverse=True):
            group_frame = tk.Frame(self.inner_frame, bg="#e0e0e0")
            task_count = len(self.tasks_by_date[date])
            toggle_button = tk.Button(group_frame, text=f"{'▼' if self.group_states.get(date, True) else '▶'} {date} ({task_count} 项)", 
                                font=("SimSun", 10, "bold"), bg="#e0e0e0", fg="#333333", bd=0, 
                                command=lambda d=date, btn=group_frame: self.toggle_group(d, btn.winfo_children()[0]))
            toggle_button.pack(side=tk.LEFT, padx=5, pady=2)
            group_frame.pack(fill=tk.X, pady=5)

            if self.group_states.get(date, True):
                for i, (task_text, completed, add_time, completion_time, _, _) in enumerate(self.tasks_by_date[date]):
                    task_frame = self.create_task_frame(task_text, completed, add_time, completion_time, date)
                    task_frame.pack(fill=tk.X, pady=2 if i > 0 else (5, 2))

        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def load_tasks(self):
        if os.path.exists('tasks.csv'):
            with open('tasks.csv', 'r', encoding='utf-8', newline='') as file:
                reader = csv.reader(file)
                for row in reader:
                    if row and len(row) >= 3:
                        task_text = row[0]
                        completed = row[1] == 'True'
                        add_time = row[2]
                        completion_time = row[3] if len(row) > 3 else "未完成"
                        alarm_time = row[4] if len(row) > 4 else "无"
                        alarm_message = row[5] if len(row) > 5 else "无"
                        date = add_time.split()[0]
                        if date not in self.tasks_by_date:
                            self.tasks_by_date[date] = []
                            self.group_states[date] = True
                        self.tasks_by_date[date].append((task_text, completed, add_time, completion_time, 
                                                       alarm_time, alarm_message))

    def save_tasks(self, silent=False):
        with open('tasks.csv', 'w', encoding='utf-8', newline='') as file:
            writer = csv.writer(file)
            for date in sorted(self.tasks_by_date.keys(), reverse=True):
                for task in self.tasks_by_date[date]:
                    writer.writerow(list(task) if len(task) == 6 else list(task) + ["无", "无"])
        if not silent:
            messagebox.showinfo("成功", "任务已保存!")

    def run(self):
        self.root.mainloop()

if __name__ == "__main__":
    root = tk.Tk()
    app = TodoApp(root)
    app.run()

下一步学习建议

  1. 尝试添加语音提醒功能
  2. 学习PyQt5开发更复杂的界面
  3. 研究如何打包为exe可执行文件

Q&A环节
Q:如何修改界面主题色?
A:只需修改代码中的颜色常量,例如将"#f0f0f0"改为"#e6f7ff"可获得浅蓝色主题

版权声明:本代码采用MIT开源协议,欢迎自由使用和修改,但请保留原作者信息。

评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

创客白泽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值