资讯动态

我的桌面便笺

发布时间:2026/10/13 10:18:52 来源:尧图企业网站定制
桌面便笺 使用说明 【基础操作】1. 拖拽标题栏移动便笺窗口。2. 窗口缩放鼠标放到便笺【四周任意边缘/四个角】拖动调整大小。3. 设置修改【当前这个便笺】位置与大小。4. 换色循环切换便笺背景颜色。5. ▬折叠一键折叠只保留顶部功能栏再次点击展开6. ▾更多菜单包含隐藏文本、加载、↩撤销、?帮助7. ↩ 撤销仅在文字修改后回退上一步文本操作8. 保存将全部便笺保存至json存档。9. 加载读取存档文件重建所有便笺。10. 新增便笺新建便笺自动右下偏移并轮换背景色。11. ✕ 关闭窗口只关闭GUI窗口不会删除存档。【图层说明】便笺窗口始终处于最底层可被任意其他窗口覆盖。点击便笺区域可交互但不会抢占前台焦点。【托盘菜单】• 新建便笺• 恢复便笺• 退出程序【退出前自动保存】【json存档】数据自动保存到用户目录 .sticky_notes.json# -*- coding: utf-8 -*- Sticky Notes - 桌面便笺 依赖pip install pystray pillow import json import os import threading import tkinter as tk from tkinter import messagebox import pystray from PIL import Image import ctypes # 全局常量 TIP_SHOW_MS 600 TOOLTIP_DELAY 400 DEFAULT_NOTE { rx: 0.832, ry: 0.0097, rw: 0.1617, rh: 0.26, content: Hello World, collapsed: False } # def set_window_to_bottom(window): 将 tkinter 窗口置于所有窗口的最底层 try: hwnd ctypes.windll.user32.GetParent(window.winfo_id()) HWND_BOTTOM 1 SWP_NOMOVE 0x0002 SWP_NOSIZE 0x0001 SWP_NOACTIVATE 0x0010 ctypes.windll.user32.SetWindowPos( hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE ) except Exception: pass class ToolTip: 简易悬浮提示文字 def __init__(self, widget, text): self.widget widget self.text text self.tip_win None self.id None self.widget.bind(Enter, self.on_enter) self.widget.bind(Leave, self.on_leave) def on_enter(self, event): self.id self.widget.after(TOOLTIP_DELAY, self.show) def on_leave(self, event): if self.id: self.widget.after_cancel(self.id) self.hide() def show(self): x, y self.widget.winfo_pointerxy() self.tip_win tk.Toplevel(self.widget) self.tip_win.wm_overrideredirect(True) self.tip_win.wm_geometry(f{x10}{y15}) label tk.Label(self.tip_win, textself.text, bg#222, fgwhite, font(Microsoft YaHei, 9), padx4, pady2) label.pack() self.tip_win.attributes(-topmost, True) def hide(self): if self.tip_win: self.tip_win.destroy() self.tip_win None class StickyNote: COLORS [#FFF9C4, #FFCCBC, #C8E6C9, #BBDEFB, #E1BEE7, #FFE0B2] TITLE_H 26 RESIZE_MARGIN 6 def __init__(self, app, rx0.1, ry0.1, rw0.15, rh0.25, color#FFF9C4, content, text_hiddenFalse, collapsedFalse): self.app app self.color color self.rx float(rx) self.ry float(ry) self.rw float(rw) self.rh float(rh) self.text_hidden text_hidden self.real_content content self.window None self.collapsed False # 先设为 False创建窗口后再根据参数折叠 self._initial_collapsed collapsed # 记录初始折叠状态 self._saved_w None self._saved_h None self._geo_dialog None self._resize_data None self.resize_direction None self._drag_data None self._text_modified_flag False self._orig_text_hash hash(content) self.create_window() self._refresh_text_visibility() # 【新增】如果加载时是折叠状态则执行折叠 if self._initial_collapsed: self.toggle_collapse() def create_window(self): 重建窗口 self.window tk.Toplevel(self.app.root) self.window.overrideredirect(True) self.window.configure(bgself.color) self.window.attributes(-topmost, False) self._apply_geometry() self._build_ui() self._bind_drag() self._bind_resize() self.window.update_idletasks() set_window_to_bottom(self.window) self.window.bind(FocusIn, self._on_focus_in) def _on_focus_in(self, event): 获得焦点时将窗口压到最底层 set_window_to_bottom(self.window) def _apply_geometry(self): sw self.window.winfo_screenwidth() sh self.window.winfo_screenheight() x int(sw * self.rx) y int(sh * self.ry) w max(int(sw * self.rw), 120) h max(int(sh * self.rh), 80) self.window.geometry(f{w}x{h}{x}{y}) def _refresh_text_visibility(self): if self.text_hidden: self.text.pack_forget() else: if not self.collapsed: self.text.pack(filltk.BOTH, expandTrue) def toggle_collapse(self): self.collapsed not self.collapsed if self.collapsed: self._saved_w self.window.winfo_width() self._saved_h self.window.winfo_height() self.text.pack_forget() self.window.geometry(f{self._saved_w}x{self.TITLE_H}{self.window.winfo_x()}{self.window.winfo_y()}) else: if self._saved_w is not None and self._saved_h is not None: self.window.geometry(f{self._saved_w}x{self._saved_h}{self.window.winfo_x()}{self.window.winfo_y()}) self._refresh_text_visibility() self._update_ratios_from_window() set_window_to_bottom(self.window) def _build_ui(self): self.title_bar tk.Frame(self.window, bgself.color, heightself.TITLE_H) self.title_bar.pack(sidetk.TOP, filltk.X) self.title_bar.pack_propagate(False) tk.Label(self.title_bar, text便笺, bgself.color, fg#555, anchorw, font(Microsoft YaHei, 9, bold)).pack(sidetk.LEFT, padx6) #(, self.delete_note, #c62828, 删除), main_btn_info [ (✕, self.hide_window, #D32F2F, 关闭窗口内容保留存档), (, self.manual_save, #00796B, 保存全部便笺), (, self.change_color, #F57C00, 切换背景颜色), (, self.app.new_note, #388E3C, 新建便笺), (▬, self.toggle_collapse, #607D8B, 折叠/展开便笺), ] self.more_menu tk.Menu(self.title_bar, tearoff0) self.more_menu.add_command(label? 帮助, commandself.app.show_help) self.more_menu.add_command(label 加载存档, commandself.app.manual_load) self.more_menu.add_command(label↩ 撤销, commandself.undo_text) self.more_menu.add_command(label 设置当前便笺大小, commandself.set_geometry_dialog) self.more_menu.add_command(label 显示/隐藏文本, commandself.toggle_hide_content) self.more_menu.add_command(label 删除, commandself.delete_note) self.more_menu.add_command(label✕ 关闭窗口, commandself.hide_window) def show_more_menu(event): self.more_menu.post(event.x_root, event.y_root) for txt, cmd, fg, tiptext in main_btn_info: b tk.Button(self.title_bar, texttxt, bd0, reliefflat, bgself.color, fgfg, activebackgroundself.color, activeforegroundfg, cursorhand2, font(Microsoft YaHei, 9, bold), commandcmd, highlightthickness0, takefocus0) b.pack(sidetk.RIGHT) b.bind(Enter, lambda e, wb: w.config(bg#dddddd)) b.bind(Leave, lambda e, wb: w.config(bgself.color)) ToolTip(b, tiptext) btn_more tk.Button(self.title_bar, text▾, bd0, reliefflat, bgself.color, fg#444, activebackgroundself.color, activeforeground#222, cursorhand2, font(Microsoft YaHei, 9, bold), highlightthickness0, takefocus0) btn_more.pack(sidetk.RIGHT) btn_more.bind(Button-1, show_more_menu) btn_more.bind(Enter, lambda e, wbtn_more: w.config(bg#dddddd)) btn_more.bind(Leave, lambda e, wbtn_more: w.config(bgself.color)) ToolTip(btn_more, 更多功能菜单) self.text tk.Text(self.window, bgself.color, bd0, wrapword, font(Microsoft YaHei, 10), padx8, pady6, insertbackground#333, highlightthickness0, undoTrue) self.text.insert(1.0, self.real_content) self.text.bind(Modified, self.on_text_modify) tk.Frame(self.window, bg#cccccc, height1).pack(sidetk.BOTTOM, filltk.X) def undo_text(self): if not self._text_modified_flag: return try: self.text.edit_undo() self.real_content self.text.get(1.0, end-1c) self._orig_text_hash hash(self.real_content) self._text_modified_flag False except tk.TclError: pass def on_text_modify(self, event): if self.text.edit_modified(): new_text self.text.get(1.0, end-1c) new_hash hash(new_text) if new_hash ! self._orig_text_hash: self._text_modified_flag True self.real_content new_text self.text.edit_modified(False) def toggle_hide_content(self): self.text_hidden not self.text_hidden self._refresh_text_visibility() def manual_save(self): self.real_content self.text.get(1.0, end-1c) self.app.save_notes() tip tk.Toplevel(self.window) tip.title(保存成功) tip.geometry(220x80) tip.attributes(-topmost, True) tip.update_idletasks() sw tip.winfo_screenwidth() sh tip.winfo_screenheight() ww tip.winfo_width() wh tip.winfo_height() x (sw - ww) // 2 y (sh - wh) // 2 tip.geometry(f{ww}x{wh}{x}{y}) tk.Label(tip, text便笺内容已保存, font(Microsoft YaHei, 10)).pack(expandTrue) tip.after(TIP_SHOW_MS, tip.destroy) def _bind_drag(self): self._drag_data None def on_press(event): self._drag_data { x_root: event.x_root, y_root: event.y_root, win_x: self.window.winfo_x(), win_y: self.window.winfo_y() } def on_move(event): if not self._drag_data: return dx event.x_root - self._drag_data[x_root] dy event.y_root - self._drag_data[y_root] new_x self._drag_data[win_x] dx new_y self._drag_data[win_y] dy self.window.geometry(f{new_x}{new_y}) def on_release(event): if self._drag_data: self._drag_data None self._update_ratios_from_window() set_window_to_bottom(self.window) self.title_bar.bind(ButtonPress-1, on_press) self.title_bar.bind(B1-Motion, on_move) self.title_bar.bind(ButtonRelease-1, on_release) def _bind_resize(self): def get_resize_dir(event): w self.window.winfo_width() h self.window.winfo_height() mx event.x my event.y dirs if mx w - self.RESIZE_MARGIN: dirs e if mx self.RESIZE_MARGIN: dirs w if my h - self.RESIZE_MARGIN: dirs s if my self.RESIZE_MARGIN: dirs n return dirs if dirs else None def motion(event): d get_resize_dir(event) if d in (se, sw, ne, nw): self.window.config(cursorsizing) elif d in (e, w): self.window.config(cursorsb_h_double_arrow) elif d in (s, n): self.window.config(cursorsb_v_double_arrow) else: self.window.config(cursor) def press(event): self.resize_direction get_resize_dir(event) if self.resize_direction: self._resize_data { x_root: event.x_root, y_root: event.y_root, win_x: self.window.winfo_x(), win_y: self.window.winfo_y(), w: self.window.winfo_width(), h: self.window.winfo_height() } def drag(event): if not self._resize_data or not self.resize_direction: return dx event.x_root - self._resize_data[x_root] dy event.y_root - self._resize_data[y_root] new_w self._resize_data[w] new_h self._resize_data[h] new_x self._resize_data[win_x] new_y self._resize_data[win_y] if e in self.resize_direction: new_w dx if w in self.resize_direction: new_w - dx new_x dx if s in self.resize_direction: new_h dy if n in self.resize_direction: new_h - dy new_y dy new_w max(new_w, 120) new_h max(new_h, 80) self.window.geometry(f{new_w}x{new_h}{new_x}{new_y}) def release(event): if self._resize_data: self._resize_data None self.resize_direction None self._update_ratios_from_window() set_window_to_bottom(self.window) self.window.bind(Motion, motion) self.window.bind(ButtonPress-1, press) self.window.bind(B1-Motion, drag) self.window.bind(ButtonRelease-1, release) def _update_ratios_from_window(self): try: sw self.window.winfo_screenwidth() sh self.window.winfo_screenheight() geo self.window.geometry().split() size geo[0].split(x) w, h int(size[0]), int(size[1]) x, y int(geo[1]), int(geo[2]) self.rx round(x / sw, 4) self.ry round(y / sh, 4) self.rw round(w / sw, 4) self.rh round(h / sh, 4) except Exception: pass def change_color(self): idx self.COLORS.index(self.color) if self.color in self.COLORS else -1 self.color self.COLORS[(idx 1) % len(self.COLORS)] widgets [self.window, self.title_bar, self.text] for w in widgets: try: w.configure(bgself.color) except tk.TclError: pass for child in self.title_bar.winfo_children(): try: child.configure(bgself.color, activebackgroundself.color) except tk.TclError: pass def set_geometry_dialog(self): if self.window is None or not self.window.winfo_exists(): return if self._geo_dialog is not None and self._geo_dialog.winfo_exists(): self._geo_dialog.lift() return win tk.Toplevel(self.window) self._geo_dialog win win.title(设置便笺位置与大小) win.geometry(320x260) win.attributes(-topmost, True) win.resizable(False, False) win.update_idletasks() sw win.winfo_screenwidth() sh win.winfo_screenheight() ww win.winfo_width() wh win.winfo_height() x (sw - ww) // 2 y (sh - wh) // 2 win.geometry(f{ww}x{wh}{x}{y}) tk.Label(win, text请输入相对于屏幕的比例值 (0.0 ~ 1.0):, font(Microsoft YaHei, 9)).pack(pady(10, 5)) entries {} fields [(X 坐标 (rx), rx), (Y 坐标 (ry), ry), (宽度 (rw), rw), (高度 (rh), rh)] frame tk.Frame(win) frame.pack(pady5) for i, (label, key) in enumerate(fields): tk.Label(frame, textlabel, font(Microsoft YaHei, 9)).grid(rowi, column0, stickye, padx5, pady3) val getattr(self, key) e tk.Entry(frame, width10, font(Microsoft YaHei, 9)) e.insert(0, str(val)) e.grid(rowi, column1, padx5, pady3) entries[key] e def apply(): try: nrx float(entries[rx].get()) nry float(entries[ry].get()) nrw float(entries[rw].get()) nrh float(entries[rh].get()) for v in (nrx, nry, nrw, nrh): if not (0.0 v 1.0): raise ValueError(值必须在 0 到 1 之间) self.rx, self.ry, self.rw, self.rh nrx, nry, nrw, nrh self._apply_geometry() if not self.collapsed: self._saved_w self.window.winfo_width() self._saved_h self.window.winfo_height() set_window_to_bottom(self.window) win.destroy() self._geo_dialog None except ValueError as e: messagebox.showwarning(输入错误, f请输入有效的数字 (0~1)\n{e}, parentwin) def on_close(): win.destroy() self._geo_dialog None win.protocol(WM_DELETE_WINDOW, on_close) tk.Button(win, text应用, commandapply, bg#1976D2, fgwhite, font(Microsoft YaHei, 10, bold), padx20, pady15).pack(pady15) def hide_window(self): if self in self.app.active_windows: self.app.active_windows.remove(self) try: self.window.destroy() except tk.TclError: pass self.window None def delete_note(self): if messagebox.askyesno(确认删除, 确定永久删除这条便笺删除后保存就会从存档移除, parentself.window): self.hide_window() if self in self.app.notes: self.app.notes.remove(self) def to_dict(self): return { rx: self.rx, ry: self.ry, rw: self.rw, rh: self.rh, color: self.color, content: self.real_content, text_hidden: self.text_hidden, collapsed: self.collapsed # 【新增】保存折叠状态 } class StickyNotesApp: DATA_FILE os.path.join(os.path.expanduser(~), .sticky_notes.json) def __init__(self): self.root tk.Tk() self.root.withdraw() self.notes [] self.active_windows [] self.tray_icon None self._help_win None self.restore_notes_from_tray() self.setup_tray() def create_note(self, rx0.1, ry0.1, rw0.15, rh0.25, color#FFF9C4, content, text_hiddenFalse, collapsedFalse): note StickyNote(self, rxrx, ryry, rwrw, rhrh, colorcolor, contentcontent, text_hiddentext_hidden, collapsedcollapsed) self.notes.append(note) self.active_windows.append(note) return note def new_note(self): if self.notes: last_note self.notes[-1] new_rx last_note.rx 0.02 new_ry last_note.ry 0.02 if new_rx last_note.rw 0.98: new_rx 0.05 if new_ry last_note.rh 0.90: new_ry 0.05 last_color_idx StickyNote.COLORS.index(last_note.color) if last_note.color in StickyNote.COLORS else -1 new_color StickyNote.COLORS[(last_color_idx 1) % len(StickyNote.COLORS)] self.create_note(rxnew_rx, rynew_ry, rwlast_note.rw, rhlast_note.rh, colornew_color, content, text_hiddenlast_note.text_hidden, collapsedFalse) else: self.create_note(**DEFAULT_NOTE) def restore_notes_from_tray(self): if not os.path.exists(self.DATA_FILE): self.create_note(**DEFAULT_NOTE) return try: for note in self.active_windows[:]: try: note.window.destroy() except: pass self.active_windows.clear() self.notes.clear() with open(self.DATA_FILE, r, encodingutf-8) as f: data json.load(f) for item in data: # 【兼容旧存档】如果没有 collapsed 字段默认为 False item.setdefault(collapsed, False) item.setdefault(text_hidden, False) self.create_note(**item) tip tk.Toplevel(self.root) tip.title(加载成功) tip.geometry(220x80) tip.attributes(-topmost, True) tip.update_idletasks() sw tip.winfo_screenwidth() sh tip.winfo_screenheight() ww tip.winfo_width() wh tip.winfo_height() x (sw - ww) // 2 y (sh - wh) // 2 tip.geometry(f{ww}x{wh}{x}{y}) tk.Label(tip, text便笺加载完成, font(Microsoft YaHei, 10)).pack(expandTrue) tip.after(TIP_SHOW_MS, tip.destroy) except Exception as e: err_msg f托盘恢复加载失败: {e} print(err_msg) messagebox.showerror(恢复便笺失败, err_msg) self.create_note(**DEFAULT_NOTE) def show_help(self): if self._help_win is not None and self._help_win.winfo_exists(): self._help_win.lift() return win tk.Toplevel(self.root) self._help_win win win.title(帮助) win.geometry(580x460) win.attributes(-topmost, True) win.resizable(False, False) win.update_idletasks() sw win.winfo_screenwidth() sh win.winfo_screenheight() ww win.winfo_width() wh win.winfo_height() x (sw - ww) // 2 y (sh - wh) // 2 win.geometry(f{ww}x{wh}{x}{y}) help_text ( 桌面便笺 使用说明 \n\n 【基础操作】\n 1. 拖拽标题栏移动便笺窗口。\n 2. 窗口缩放鼠标放到便笺【四周任意边缘/四个角】拖动调整大小。\n 3. 设置修改【当前这个便笺】位置与大小。\n 4. 换色循环切换便笺背景颜色。\n 5. ▬折叠一键折叠只保留顶部功能栏再次点击展开\n 6. ▾更多菜单包含隐藏文本、加载、↩撤销、?帮助\n 7. ↩ 撤销仅在文字修改后回退上一步文本操作\n 8. 保存将全部便笺保存至json存档。\n 9. 加载读取存档文件重建所有便笺。\n 10. 新增便笺新建便笺自动右下偏移并轮换背景色。\n 11. ✕ 关闭窗口只关闭GUI窗口不会删除存档。\n\n 【图层说明】\n 便笺窗口始终处于最底层可被任意其他窗口覆盖。\n 点击便笺区域可交互但不会抢占前台焦点。\n\n 【托盘菜单】\n • 新建便笺\n • 恢复便笺\n • 退出程序【退出前自动保存】\n\n 【json存档】\n 数据自动保存到用户目录 .sticky_notes.json\n 包含位置、大小、颜色、内容、隐藏状态、折叠状态 ) txt tk.Text(win, font(Microsoft YaHei, 10), padx5, pady5, wrapword, statenormal, bg#F5F5F5, bd0) txt.insert(1.0, help_text) txt.config(statedisabled) txt.pack(filltk.BOTH, expandTrue) def save_notes(self): for note in self.active_windows: try: note.real_content note.text.get(1.0, end-1c) except tk.TclError: pass data [n.to_dict() for n in self.notes] try: with open(self.DATA_FILE, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) except Exception as e: print(f保存失败: {e}) messagebox.showerror(保存失败, str(e)) def manual_load(self): if not os.path.exists(self.DATA_FILE): messagebox.showwarning(提示, 存档文件不存在) return try: for note in self.active_windows[:]: try: note.window.destroy() except: pass self.active_windows.clear() self.notes.clear() with open(self.DATA_FILE, r, encodingutf-8) as f: data json.load(f) for item in data: # 【兼容旧存档】 item.setdefault(collapsed, False) item.setdefault(text_hidden, False) self.create_note(**item) tip tk.Toplevel(self.root) tip.title(加载成功) tip.geometry(220x80) tip.attributes(-topmost, True) tip.update_idletasks() sw tip.winfo_screenwidth() sh tip.winfo_screenheight() ww tip.winfo_width() wh tip.winfo_height() x (sw - ww) // 2 y (sh - wh) // 2 tip.geometry(f{ww}x{wh}{x}{y}) tk.Label(tip, text便笺加载完成, font(Microsoft YaHei, 10)).pack(expandTrue) tip.after(TIP_SHOW_MS, tip.destroy) except Exception as e: err_msg f加载失败: {e} print(err_msg) messagebox.showerror(加载存档失败, err_msg) def quit_app(self): self.save_notes() for note in self.active_windows[:]: try: note.window.destroy() except Exception: pass self.active_windows.clear() try: if self.tray_icon is not None: self.tray_icon.stop() except Exception: pass self.tray_icon None try: self.root.destroy() except Exception: pass os._exit(0) def setup_tray(self): icon_img Image.new(RGB, (64, 64), color#FDD835) menu pystray.Menu( pystray.MenuItem(新建便笺, self.new_note), pystray.MenuItem(恢复便笺, self.restore_notes_from_tray), pystray.MenuItem(退出程序, self.quit_app) ) self.tray_icon pystray.Icon(sticky_notes, icon_img, 桌面便笺, menu) def tray_run(): self.tray_icon.run() threading.Thread(targettray_run, daemonTrue).start() def run(self): self.root.mainloop() if __name__ __main__: StickyNotesApp().run()version.txt# UTF-8 VSVersionInfo( ffiFixedFileInfo( filevers(1, 0, 0, 0), prodvers(1, 0, 0, 0), mask0x3f, flags0x0, OS0x40004, fileType0x1, subtype0x0, date(0, 0) ), kids[ StringFileInfo([ StringTable(u080404b0, [ StringStruct(uCompanyName, u公司), StringStruct(uFileDescription, u便笺), StringStruct(uFileVersion, u1.0.0.0), StringStruct(uInternalName, unotes), StringStruct(uLegalCopyright, uCopyright © 2026 ), StringStruct(uOriginalFilename, unotes.exe), StringStruct(uProductName, u我的便笺), StringStruct(uProductVersion, u1.0.0.0) ]) ]), VarFileInfo([VarStruct(uTranslation, [2052, 1200])]) ] )CMD命令行pyinstaller -F -i ICO.ico --noconsole --version-file version.txt PY.py要打包的文件要放在“用户目录”下打包好的文件在dist里。图标ICO可以在网站下载免费和高级图标支持 SVG、PNG、ICO 和 ICNS | Icon-Icons搜索“任务计划程序”创建基本任务“触发器”选择“登入时”“启用程序”选择已经打包好的程序。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑