每日清单

· · 科技·工程

你是否很苦恼不能准时完成任务?

你是否很苦恼每天还需要别人来提醒才可以想起来一些事情?

你是否还很苦恼于市面上的每日清单软件全是广告?

每日清单清爽版它来了!虽然是我给的思路欸哎写的,但是没有广告不就很好了吗(

本软件只有记录任务,删除任务,更改软件背景图片,到时提醒,开机自启的功能。代码还算好看,窗口中的任意功能都可以自定义。

在运行代码前,你需要安装以下库(在命令行运行):

pip install pystray Pillow

如果要打包成 .exe 文件的话,需要使用 PyInstaller 插件。

如果没有安装的话,打开命令行,输入下段代码以安装:

pip install pyinstaller

请配置好环境变量,并在存放 daily_list.py 的文件夹下打开命令行,输入以下代码,以打包。

如果你有喜欢的图标的话,可以把图标放在文件夹下面,并以 app_icon.ico 命名。如果没有的话,请将 --icon=app_icon.ico 这句删掉后再输入命令行。

pyinstaller --noconsole --onefile --name="每日清单" --icon=app_icon.ico daily_todo_completed.py

下面是项目代码。

import tkinter as tk
from tkinter import messagebox, filedialog, Menu, Scrollbar
import json
import os
import sys
import winreg
import threading
import ctypes # 用于系统API调用
from datetime import datetime

# 依赖库
import pystray
from pystray import MenuItem as item
from PIL import Image, ImageDraw, ImageTk, ImageOps 

# ================= 配置区域 =================
APP_TITLE = "Daily_List"
WINDOW_WIDTH = 450
WINDOW_HEIGHT = 720 # 稍微加高一点容纳滚动条
WINDOW_SIZE = f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}"
CARD_ALPHA = 180 

DATA_FILE = "todo_data_final.json"
ICON_FILE = "app_icon.ico" # 如果目录下有这个图标文件,会自动加载
DEFAULT_TIMES = ["11:30", "17:30"]

# 【新增】尝试开启 Windows 高DPI感知,解决模糊问题
try:
    ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
    try:
        ctypes.windll.user32.SetProcessDPIAware()
    except: pass
# ===========================================

class DailyTodoApp:
    def __init__(self, root):
        self.root = root
        self.root.title(APP_TITLE)
        self.root.geometry(WINDOW_SIZE)
        self.root.resizable(False, False)

        # 【新增】设置程序图标
        if os.path.exists(ICON_FILE):
            try: self.root.iconbitmap(ICON_FILE)
            except: pass

        # 数据初始化
        self.tasks = []
        self.reminder_times = []
        self.bg_image_path = ""
        self.auto_start_var = tk.BooleanVar()
        self.last_alert_signature = (None, None)

        self.root.protocol('WM_DELETE_WINDOW', self.minimize_to_tray)
        self.load_data()

        # Canvas 布局
        self.canvas = tk.Canvas(self.root, width=WINDOW_WIDTH, height=WINDOW_HEIGHT, bd=0, highlightthickness=0)
        self.canvas.pack(fill="both", expand=True)

        self.refresh_background_image()
        self.setup_widgets()

        self.refresh_task_list()
        self.refresh_time_list()
        self.check_reminder_loop()

    def refresh_background_image(self):
        if self.bg_image_path and os.path.exists(self.bg_image_path):
            try:
                base_img = Image.open(self.bg_image_path).convert("RGBA")
                base_img = ImageOps.fit(base_img, (WINDOW_WIDTH, WINDOW_HEIGHT), method=Image.Resampling.LANCZOS)
            except:
                base_img = Image.new("RGBA", (WINDOW_WIDTH, WINDOW_HEIGHT), "#333333")
        else:
            base_img = Image.new("RGBA", (WINDOW_WIDTH, WINDOW_HEIGHT), "#333333")

        # 创建半透明遮罩
        overlay = Image.new("RGBA", base_img.size, (255, 255, 255, 0))
        draw = ImageDraw.Draw(overlay)

        # 调整了卡片高度以适应滚动条
        draw.rectangle((20, 20, WINDOW_WIDTH-20, 390), fill=(255, 255, 255, CARD_ALPHA)) # 任务区
        draw.rectangle((20, 410, WINDOW_WIDTH-20, 700), fill=(255, 255, 255, CARD_ALPHA)) # 设置区

        combined_img = Image.alpha_composite(base_img, overlay)
        self.tk_image = ImageTk.PhotoImage(combined_img)
        self.canvas.delete("bg") 
        self.canvas.create_image(0, 0, image=self.tk_image, anchor="nw", tag="bg")
        self.canvas.tag_lower("bg")

    def setup_widgets(self):
        # 标题
        self.canvas.create_text(40, 50, text="待办任务", font=("微软雅黑", 14, "bold"), anchor="w", fill="#333")
        self.canvas.create_text(40, 440, text="提醒设置", font=("微软雅黑", 14, "bold"), anchor="w", fill="#333")

        # --- 任务输入 ---
        self.task_entry = tk.Entry(self.root, font=("微软雅黑", 10), bd=1, relief="solid")
        self.task_entry.bind('<Return>', lambda e: self.add_task())
        self.canvas.create_window(35, 80, window=self.task_entry, width=300, height=30, anchor="nw")

        add_btn = tk.Button(self.root, text="添加", bg="#0078d7", fg="white", relief="flat", command=self.add_task)
        self.canvas.create_window(345, 80, window=add_btn, width=60, height=30, anchor="nw")

        # --- 任务列表 + 滚动条 ---
        # 创建一个 Frame 来容纳 Listbox 和 Scrollbar,方便管理
        task_frame = tk.Frame(self.root, bg="white", bd=0)
        # 将 Frame 放到 Canvas 上
        self.canvas.create_window(35, 120, window=task_frame, width=370, height=210, anchor="nw")

        self.task_scrollbar = Scrollbar(task_frame)
        self.task_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.task_listbox = tk.Listbox(task_frame, font=("微软雅黑", 11), bg="#ffffff", bd=0, 
                                       highlightthickness=0, yscrollcommand=self.task_scrollbar.set)
        self.task_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.task_scrollbar.config(command=self.task_listbox.yview)

        # 绑定事件
        self.task_listbox.bind('<Double-1>', lambda event: self.delete_task())
        self.task_listbox.bind('<Button-3>', lambda event: self.show_context_menu(event, "task"))

        # 删除按钮
        del_task_btn = tk.Button(self.root, text="删除选中任务", font=("微软雅黑", 9), command=self.delete_task)
        self.canvas.create_window(320, 340, window=del_task_btn, anchor="nw")

        # --- 时间列表 + 滚动条 ---
        time_frame = tk.Frame(self.root, bg="white", bd=0)
        self.canvas.create_window(35, 480, window=time_frame, width=180, height=140, anchor="nw")

        self.time_scrollbar = Scrollbar(time_frame)
        self.time_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.time_listbox = tk.Listbox(time_frame, font=("Consolas", 11), bg="#ffffff", bd=0, 
                                       highlightthickness=0, yscrollcommand=self.time_scrollbar.set)
        self.time_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.time_scrollbar.config(command=self.time_listbox.yview)

        # 绑定事件
        self.time_listbox.bind('<Double-1>', lambda event: self.delete_time())
        self.time_listbox.bind('<Button-3>', lambda event: self.show_context_menu(event, "time"))

        # --- 设置操作 ---
        self.time_entry = tk.Entry(self.root, width=8, font=("Consolas", 11))
        self.canvas.create_window(240, 480, window=self.time_entry, anchor="nw")

        add_time_btn = tk.Button(self.root, text="添加时间", command=self.add_time, font=("微软雅黑", 9))
        self.canvas.create_window(330, 477, window=add_time_btn, anchor="nw")

        self.canvas.create_text(35, 630, text="* 双击或右键可删除时间", fill="#888", font=("微软雅黑", 8), anchor="nw")

        # --- 换背景 ---
        bg_btn = tk.Button(self.root, text="更换背景图", command=self.choose_bg_image, bg="#ff9800", fg="white", font=("微软雅黑", 9, "bold"))
        self.canvas.create_window(240, 530, window=bg_btn, width=150, anchor="nw")

        # --- 开机自启 ---
        self.auto_start_var.set(self.check_auto_start_status())
        chk = tk.Checkbutton(self.root, text="开机自启动", variable=self.auto_start_var, command=self.toggle_auto_start, bg="white") 
        self.canvas.create_window(320, 660, window=chk, anchor="nw")

        # 右键菜单
        self.context_menu = Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="删除", command=self.delete_from_menu)

    # ================= 逻辑处理 =================
    def show_context_menu(self, event, list_type):
        self.current_menu_target = list_type
        widget = event.widget
        try:
            widget.selection_clear(0, tk.END)
            widget.selection_set(widget.nearest(event.y))
            self.context_menu.post(event.x_root, event.y_root)
        except: pass

    def delete_from_menu(self):
        if self.current_menu_target == "task": self.delete_task()
        elif self.current_menu_target == "time": self.delete_time()

    def choose_bg_image(self):
        path = filedialog.askopenfilename(title="选择背景", filetypes=[("图片", "*.jpg;*.png;*.jpeg")])
        if path:
            self.bg_image_path = path
            self.save_data()
            self.refresh_background_image()

    def add_task(self):
        t = self.task_entry.get().strip()
        if t:
            self.tasks.append(t)
            self.task_entry.delete(0, tk.END)
            self.save_data()
            self.refresh_task_list()

    def delete_task(self):
        try:
            index = self.task_listbox.curselection()[0]
            del self.tasks[index]
            self.save_data()
            self.refresh_task_list()
        except IndexError: pass 

    def refresh_task_list(self):
        self.task_listbox.delete(0, tk.END)
        for i, t in enumerate(self.tasks):
            self.task_listbox.insert(tk.END, f" {i+1}. {t}")

    def add_time(self):
        t = self.time_entry.get().strip()
        try:
            ft = datetime.strptime(t, "%H:%M").strftime("%H:%M")
            if ft not in self.reminder_times:
                self.reminder_times.append(ft)
                self.reminder_times.sort()
                self.save_data()
                self.refresh_time_list()
            else: messagebox.showinfo("提示", "该时间已存在")
        except: messagebox.showerror("错误", "格式应为 HH:MM")

    def delete_time(self):
        try:
            index = self.time_listbox.curselection()[0]
            del self.reminder_times[index]
            self.save_data()
            self.refresh_time_list()
        except IndexError: pass

    def refresh_time_list(self):
        self.time_listbox.delete(0, tk.END)
        for t in self.reminder_times:
            self.time_listbox.insert(tk.END, f" ⏰ {t}")

    def save_data(self):
        data = {"tasks": self.tasks, "reminder_times": self.reminder_times, "bg_image_path": self.bg_image_path}
        try:
            with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=4)
        except: pass

    def load_data(self):
        if os.path.exists(DATA_FILE):
            try:
                with open(DATA_FILE, 'r', encoding='utf-8') as f:
                    d = json.load(f)
                    self.tasks = d.get("tasks", [])
                    self.reminder_times = d.get("reminder_times", DEFAULT_TIMES)
                    self.bg_image_path = d.get("bg_image_path", "")
            except: self.tasks, self.reminder_times = [], DEFAULT_TIMES
        else: self.tasks, self.reminder_times = [], DEFAULT_TIMES

    def check_reminder_loop(self):
        now = datetime.now()
        ct, cd = now.strftime("%H:%M"), now.strftime("%Y-%m-%d")
        if ct in self.reminder_times and self.tasks:
            if self.last_alert_signature != (cd, ct):
                self.last_alert_signature = (cd, ct)
                self.show_window()
                msg = "\n".join([f"{i+1}. {t}" for i, t in enumerate(self.tasks)])
                messagebox.showwarning("提醒", f"时间到 {ct}!\n未完成:\n{msg}")
        self.root.after(5000, self.check_reminder_loop)

    def minimize_to_tray(self):
        self.root.withdraw()
        threading.Thread(target=self.create_tray_icon, daemon=True).start()

    def create_tray_icon(self):
        img = Image.new('RGB', (64, 64), (0, 120, 215))
        d = ImageDraw.Draw(img)
        d.rectangle((20, 20, 44, 44), fill="white")
        # 如果有本地图标文件,优先使用本地图标
        if os.path.exists(ICON_FILE):
            try: img = Image.open(ICON_FILE)
            except: pass

        menu = (item('显示', self.show_window_from_tray), item('退出', self.quit_app))
        self.icon = pystray.Icon("todo", img, "每日清单", menu)
        self.icon.run()

    def show_window_from_tray(self, icon=None, item=None):
        self.icon.stop()
        self.root.after(0, self.show_window)

    def show_window(self):
        self.root.deiconify()
        self.root.lift()

    def quit_app(self, icon=None, item=None):
        self.icon.stop()
        self.root.quit()
        sys.exit()

    def get_exe_path(self):
        return sys.executable if getattr(sys, 'frozen', False) else os.path.abspath(__file__)

    def check_auto_start_status(self):
        try:
            key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_READ)
            winreg.QueryValueEx(key, APP_TITLE)
            winreg.CloseKey(key)
            return True
        except: return False

    def toggle_auto_start(self):
        try:
            key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_ALL_ACCESS)
            if self.auto_start_var.get():
                winreg.SetValueEx(key, APP_TITLE, 0, winreg.REG_SZ, self.get_exe_path())
            else:
                try: winreg.DeleteValue(key, APP_TITLE)
                except: pass
            winreg.CloseKey(key)
        except Exception as e:
            messagebox.showerror("错误", str(e))
            self.auto_start_var.set(not self.auto_start_var.get())

if __name__ == "__main__":
    root = tk.Tk()
    app = DailyTodoApp(root)
    root.mainloop()

本软件基于 Python 开发,使用 许可。该文章同时在 KobayashiKotona的回退博客 发布。