吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 698|回复: 7
收起左侧

[Python 原创] 基于python的消息提示(附多种模板+自定义)

[复制链接]
Serendisand 发表于 2026-7-31 13:13

消息提示中心

一个用 Python 写的小型桌面通知面板。它把几种常见、以及一些偏风格化的消息样式放在同一个窗口里,方便调标题、正文、图标和显示时间后直接预览。

下载:https://serendisand.lanzouu.com/iWCSw3zralah
密码:a15w
程序界面截图放在项目附件中,可与本说明一起查看。

能做什么

  • 发送 Windows Toast 通知:会从系统右下角出现,并保留在 Windows 通知中心。
  • 显示 Windows 原生对话框:传统的 MessageBox,带确定按钮。
  • 预览多种自绘消息样式:侧边吐司、灵动岛、ClickGUI、顶部横幅、聊天气泡、终端、信笺、胶囊、二次元角色卡片和聚光舞台等。
  • 设置消息标题、正文、信息等级、停留时间与显示位置。
  • 为自绘通知添加自己的小图标。
  • 开启调试模式后,将 Windows 通知和 PowerShell 的执行信息写入日志,方便排查问题。

运行环境

需要 Python 3.10 或更高版本。程序仅使用标准库,不需要 pip install

python main.py

在 Windows 下建议用命令提示符或 PowerShell 从项目目录启动。Windows Toast 通知会调用系统自带的 PowerShell 和通知 API。

使用方式

  1. 从左侧选择一个模板。
  2. 填写标题和消息内容。
  3. 根据需要选择信息、成功、警告或错误等级。
  4. 对悬浮模板,可选择显示位置和停留时长;部分模板会固定在符合其样式的位置,例如灵动岛固定在屏幕上方居中。
  5. 点击 测试发送 查看效果。

Windows 通知

“Windows 通知”会调用 Windows Toast API,通常从右下角出现,并可在通知中心查看。若没有看到横幅,请检查 Windows 的“请勿打扰 / 专注助手”设置;通知也可能被系统静默放入通知中心。

自定义图标

自绘消息模板支持添加图标。当前支持以下格式:

  • PNG
  • GIF
  • PPM
  • PGM

图标尺寸需要在 16×16 到 256×256 像素之间。程序会在显示时按比例缩小较大的图标。Windows 系统 Toast 和 Windows 原生对话框受系统 API 限制,仍使用系统提供的图标。

调试日志

底部勾选 调试模式 后再发送消息,程序会在项目目录生成或追加:

notification_debug.log

日志会包含所选模板、Windows Toast 的 PowerShell 返回信息,以及 Windows API 调用结果。遇到 Windows 通知没有显示的情况,可以把相关日志内容贴出来排查。

说明

这个项目适合做本地通知、界面效果预览或桌面小工具原型。请不要用它伪造系统安全、登录、支付等界面,或用来误导他人。

源码

"""本地消息提示中心:使用 Tkinter 和 Windows MessageBoxW 的通知模板面板。"""

from __future__ import annotations

import base64
import json
import os
import platform
import subprocess
import sys
import time
import tkinter as tk
import traceback
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from typing import Callable

MAX_ICON_SIZE = 256
MIN_ICON_SIZE = 16
SUPPORTED_ICON_TYPES = {".png", ".gif", ".ppm", ".pgm"}
LOG_FILE = Path(__file__).with_name("notification_debug.log")
TOAST_SCRIPT_NAME = "windows_toast.ps1"

def resource_path(filename: str) -> Path:
    root = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent))
    return root / filename

TOAST_SCRIPT_FILE = resource_path(TOAST_SCRIPT_NAME)

PALETTE = {
    "bg": "#eef8f5",
    "panel": "#ffffff",
    "panel_hover": "#e2f4ee",
    "surface": "#f5fbf8",
    "border": "#b8ddd2",
    "text": "#24463e",
    "muted": "#6b8d83",
    "accent": "#58bda5",
    "success": "#56bd91",
    "warning": "#e6b75c",
    "danger": "#df7885",
    "info": "#62aede",
    "cream": "#fffdf5",
    "ink": "#315d53",
}

LEVELS = {
    "信息": ("i", PALETTE["info"]),
    "成功": ("✓", PALETTE["success"]),
    "警告": ("!", PALETTE["warning"]),
    "错误": ("×", PALETTE["danger"]),
}

@dataclass(frozen=True)
class Template:
    key: str
    name: str
    subtitle: str
    symbol: str
    sender: Callable[[str, str], None]

class FloatingNotice(tk.Toplevel):
    def __init__(
        self,
        app: "MessageCenter",
        title: str,
        content: str,
        *,
        style: str,
        duration: float,
        position: str,
        icon: tk.PhotoImage | None,
    ) -> None:
        super().__init__(app)
        self.app = app
        self.style = style
        self.duration = duration
        self.position = position
        self.icon = icon
        self.width = 380
        self.height = 126
        self._closing = False
        self._after_ids: set[str] = set()
        self.withdraw()
        self.overrideredirect(True)
        self.attributes("-topmost", True)
        self.attributes("-alpha", 0.0)
        self.configure(bg=PALETTE["bg"])

        symbol, color = LEVELS[app.level_var.get()]
        builders = {
            "toast": self._build_toast,
            "island": self._build_island,
            "clickgui": self._build_clickgui,
            "banner": self._build_banner,
            "bubble": self._build_bubble,
            "terminal": self._build_terminal,
            "letter": self._build_letter,
            "pill": self._build_pill,
            "chibi": self._build_chibi,
            "spotlight": self._build_spotlight,
        }
        builders[style](title, content, symbol, color)
        self.bind("<Button-1>", lambda _event: self.close())
        self._schedule_idle(self.animate_in)

    def _schedule(self, delay: int, callback: Callable[[], None]) -> None:
        after_id = ""

        def run() -> None:
            self._after_ids.discard(after_id)
            if self.winfo_exists():
                callback()

        after_id = self.after(delay, run)
        self._after_ids.add(after_id)

    def _schedule_idle(self, callback: Callable[[], None]) -> None:
        after_id = ""

        def run() -> None:
            self._after_ids.discard(after_id)
            if self.winfo_exists():
                callback()

        after_id = self.after_idle(run)
        self._after_ids.add(after_id)

    def _icon_or_symbol(self, parent: tk.Misc, symbol: str, color: str, bg: str, size: int = 36) -> None:
        if self.icon is not None:
            tk.Label(parent, image=self.icon, bg=bg, width=size, height=size).pack(side="left", padx=(14, 10), pady=14)
        else:
            tk.Label(parent, text=symbol, font=("TkDefaultFont", 16, "bold"), fg=bg, bg=color, width=3).pack(side="left", padx=(14, 10), pady=14)

    def _body(self, parent: tk.Misc, title: str, content: str, *, bg: str, title_fg: str, text_fg: str, wrap: int) -> None:
        body = tk.Frame(parent, bg=bg)
        body.pack(side="left", fill="both", expand=True, pady=14)
        tk.Label(body, text=title, font=("Segoe UI", 11, "bold"), fg=title_fg, bg=bg, anchor="w").pack(fill="x")
        tk.Label(body, text=content, font=("Segoe UI", 9), fg=text_fg, bg=bg, anchor="w", justify="left", wraplength=wrap).pack(fill="both", expand=True, pady=(4, 0))

    def _base_card(self, bg: str = PALETTE["surface"], border: str = PALETTE["border"]) -> tk.Frame:
        card = tk.Frame(self, bg=bg, highlightbackground=border, highlightthickness=1)
        card.pack(fill="both", expand=True)
        return card

    def _build_toast(self, title: str, content: str, symbol: str, color: str) -> None:
        card = self._base_card()
        tk.Frame(card, bg=color, width=5).pack(side="left", fill="y")
        self._icon_or_symbol(card, symbol, color, PALETTE["surface"])
        self._body(card, title, content, bg=PALETTE["surface"], title_fg=PALETTE["text"], text_fg=PALETTE["muted"], wrap=245)

    def _build_island(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = 430, 98
        card = self._base_card("#0a0810", "#493c5f")
        self._icon_or_symbol(card, symbol, color, "#0a0810")
        self._body(card, title, content, bg="#0a0810", title_fg=PALETTE["text"], text_fg="#c8bbd4", wrap=290)
        tk.Label(card, text="●", font=("Segoe UI", 8), fg=color, bg="#0a0810").pack(side="right", padx=20)

    def _build_clickgui(self, title: str, content: str, symbol: str, color: str) -> None:
        card = self._base_card("#20192c", "#80689d")
        header = tk.Frame(card, bg="#4b385e", height=34)
        header.pack(fill="x")
        header.pack_propagate(False)
        tk.Label(header, text=f"  {symbol}  {title}", font=("Consolas", 10, "bold"), fg=color, bg="#4b385e", anchor="w").pack(side="left", fill="both", expand=True)
        tk.Label(card, text=content, font=("Consolas", 9), fg="#f2e9f6", bg="#20192c", anchor="w", justify="left", wraplength=340).pack(fill="both", expand=True, padx=18, pady=14)

    def _build_banner(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = max(560, self.winfo_screenwidth() - 80), 72
        card = tk.Frame(self, bg=color)
        card.pack(fill="both", expand=True)
        self._icon_or_symbol(card, symbol, PALETTE["ink"], color)
        tk.Label(card, text=title, font=("Segoe UI", 11, "bold"), fg=PALETTE["ink"], bg=color).pack(side="left")
        tk.Label(card, text=f"  ·  {content}", font=("Segoe UI", 10), fg="#604766", bg=color, anchor="w").pack(side="left", fill="x", expand=True)

    def _build_bubble(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = 325, 116
        card = self._base_card(PALETTE["cream"], color)
        self._icon_or_symbol(card, symbol, color, PALETTE["cream"])
        self._body(card, title, content, bg=PALETTE["cream"], title_fg=PALETTE["ink"], text_fg="#806783", wrap=210)

    def _build_terminal(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = 425, 126
        card = self._base_card("#191421", "#8c72aa")
        tk.Label(card, text=f"  [{symbol}]  NOTICE", font=("Consolas", 10, "bold"), fg=color, bg="#30243f", anchor="w").pack(fill="x", ipady=8)
        tk.Label(card, text=f"> {title}\n  {content}", font=("Consolas", 10), fg="#f3dced", bg="#191421", anchor="w", justify="left", wraplength=380).pack(fill="both", expand=True, padx=16, pady=12)

    def _build_letter(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = 390, 150
        card = self._base_card("#fff9ef", "#d8b97c")
        tk.Label(card, text="✉", font=("TkDefaultFont", 24), fg="#c07a85", bg="#fff9ef").pack(anchor="w", padx=20, pady=(14, 0))
        tk.Label(card, text=title, font=("Segoe UI", 12, "bold"), fg="#553944", bg="#fff9ef", anchor="w").pack(fill="x", padx=20)
        tk.Label(card, text=content, font=("Segoe UI", 9), fg="#82636e", bg="#fff9ef", anchor="w", justify="left", wraplength=340).pack(fill="both", expand=True, padx=20, pady=(5, 14))

    def _build_pill(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = 460, 76
        card = self._base_card("#f9eafa", "#ed9ec6")
        self._icon_or_symbol(card, symbol, color, "#f9eafa")
        tk.Label(card, text=f"{title}  ", font=("Segoe UI", 10, "bold"), fg="#653e61", bg="#f9eafa").pack(side="left")
        tk.Label(card, text=content, font=("Segoe UI", 9), fg="#926e90", bg="#f9eafa", anchor="w", wraplength=220).pack(side="left", fill="x", expand=True)

    def _build_chibi(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = 365, 135
        card = self._base_card("#f2dff1", "#ff9bc7")
        portrait = tk.Label(card, text="✦\n◕‿◕", font=("TkDefaultFont", 13, "bold"), fg="#ffffff", bg="#ee86b7", width=6)
        portrait.pack(side="left", fill="y")
        self._body(card, title, content, bg="#f2dff1", title_fg="#633f65", text_fg="#815f83", wrap=235)

    def _build_spotlight(self, title: str, content: str, symbol: str, color: str) -> None:
        self.width, self.height = 460, 142
        card = self._base_card("#302041", color)
        tk.Label(card, text=symbol, font=("TkDefaultFont", 26, "bold"), fg=color, bg="#302041").pack(pady=(14, 0))
        tk.Label(card, text=title, font=("Segoe UI", 12, "bold"), fg=PALETTE["text"], bg="#302041").pack()
        tk.Label(card, text=content, font=("Segoe UI", 9), fg="#d5c7e0", bg="#302041", wraplength=400, justify="center").pack(padx=18, pady=(3, 12))

    def _target_geometry(self) -> tuple[int, int, int, int]:
        self.update_idletasks()
        sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
        margin = 22
        if self.style in {"island", "banner", "spotlight"}:
            target_x = (sw - self.width) // 2
            target_y = 8 if self.style != "spotlight" else max(30, (sh - self.height) // 3)
            return target_x, target_y, target_x, -self.height - 12
        if self.style in {"bubble", "letter", "pill"}:
            return sw - self.width - margin, sh - self.height - 76, sw + 20, sh - self.height - 76
        if self.position == "左上":
            return margin, margin, -self.width - 20, margin
        if self.position == "右上":
            return sw - self.width - margin, margin, sw + 20, margin
        if self.position == "左下":
            y = sh - self.height - 76
            return margin, y, -self.width - 20, y
        y = sh - self.height - 76
        return sw - self.width - margin, y, sw + 20, y

    def _exit_geometry(self) -> tuple[int, int]:
        target_x, target_y, _, _ = self._target_geometry()
        if self.style in {"island", "banner"}:
            return target_x, -self.height - 12
        if self.style == "clickgui":
            return target_x, min(self.winfo_screenheight() - 76, target_y + self.height + 28)
        if self.style == "spotlight":
            return target_x, self.winfo_screenheight() + 20
        if self.position.startswith("左") and self.style not in {"bubble", "letter", "pill"}:
            return -self.width - 20, target_y
        return self.winfo_screenwidth() + 20, target_y

    def animate_in(self) -> None:
        if self._closing:
            return
        target_x, target_y, start_x, start_y = self._target_geometry()
        self.geometry(f"{self.width}x{self.height}+{start_x}+{start_y}")
        self.deiconify()
        steps = 16

        def step(index: int = 0) -> None:
            if self._closing:
                return
            progress = min(1, index / steps)
            eased = 1 - (1 - progress) ** 3
            x = int(start_x + (target_x - start_x) * eased)
            y = int(start_y + (target_y - start_y) * eased)
            self.geometry(f"{self.width}x{self.height}+{x}+{y}")
            self.attributes("-alpha", min(0.98, 0.25 + 0.73 * eased))
            if index < steps:
                self._schedule(12, lambda: step(index + 1))
            else:
                self._schedule(int(self.duration * 1000), self.close)

        step()

    def close(self) -> None:
        if self._closing or not self.winfo_exists():
            return
        self._closing = True
        for after_id in self._after_ids.copy():
            self.after_cancel(after_id)
        self._after_ids.clear()
        start_x, start_y = self.winfo_x(), self.winfo_y()
        end_x, end_y = self._exit_geometry()
        steps = 14

        def animate_out(index: int = 0) -> None:
            if not self.winfo_exists():
                return
            progress = min(1, index / steps)
            eased = 1 - (1 - progress) ** 3
            x = int(start_x + (end_x - start_x) * eased)
            y = int(start_y + (end_y - start_y) * eased)
            self.geometry(f"{self.width}x{self.height}+{x}+{y}")
            self.attributes("-alpha", max(0.0, 0.98 * (1 - progress)))
            if index < steps:
                self._schedule(14, lambda: animate_out(index + 1))
            else:
                self.destroy()

        animate_out()

class MessageCenter(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.title("消息提示中心")
        self.geometry("1080x720")
        self.minsize(920, 620)
        self.configure(bg=PALETTE["bg"])
        self.option_add("*Font", "TkDefaultFont")
        self.style = ttk.Style(self)
        self.style.theme_use("clam")
        self._configure_ttk()

        self.selected_key = "native"
        self.level_var = tk.StringVar(value="信息")
        self.duration_var = tk.DoubleVar(value=4)
        self.position_var = tk.StringVar(value="右下")
        self.title_var = tk.StringVar(value="操作完成")
        self.status_var = tk.StringVar(value="准备就绪")
        self.debug_var = tk.BooleanVar(value=False)
        self.icon_path_var = tk.StringVar(value="未选择图标")
        self.icon_image: tk.PhotoImage | None = None
        self.template_buttons: dict[str, tk.Frame] = {}
        self.templates = [
            Template("native", "Windows 通知", "右下角系统 Toast 通知", "▣", self.send_windows_toast),
            Template("toast", "侧边吐司", "简洁的桌面通知卡片", "◐", self.send_toast),
            Template("island", "灵动岛", "屏幕上方居中悬浮", "●", self.send_island),
            Template("clickgui", "ClickGUI", "右侧滑入、向下收回", "▰", self.send_clickgui),
            Template("banner", "全宽横幅", "从顶部展开的提醒", "═", self.send_banner),
            Template("bubble", "聊天气泡", "明亮的聊天式提示", "✦", self.send_bubble),
            Template("terminal", "终端窗口", "复古命令行样式", ">_", self.send_terminal),
            Template("letter", "手写信笺", "暖色纸张与信件布局", "✉", self.send_letter),
            Template("pill", "糖果胶囊", "轻盈的圆润提示条", "♥", self.send_pill),
            Template("chibi", "二次元角色", "粉色角色卡片提示", "☆", self.send_chibi),
            Template("spotlight", "聚光舞台", "中央强调式消息", "✧", self.send_spotlight),
        ]
        self.template_by_key = {template.key: template for template in self.templates}
        self._build_ui()

    def _configure_ttk(self) -> None:
        self.style.configure("Dark.TCombobox", fieldbackground=PALETTE["surface"], background=PALETTE["surface"], foreground=PALETTE["text"], arrowcolor=PALETTE["text"], bordercolor=PALETTE["border"])
        self.style.map("Dark.TCombobox", fieldbackground=[("readonly", PALETTE["surface"])], selectbackground=[("readonly", PALETTE["surface"])], selectforeground=[("readonly", PALETTE["text"])])
        self.style.configure("Horizontal.TScale", background=PALETTE["panel"], troughcolor=PALETTE["border"], sliderthickness=15)

    def _label(self, parent: tk.Misc, text: str | None = None, *, muted: bool = False, **kwargs: object) -> tk.Label:
        options: dict[str, object] = {"fg": PALETTE["muted"] if muted else PALETTE["text"], "bg": parent.cget("bg"), **kwargs}
        if text is not None:
            options["text"] = text
        return tk.Label(parent, **options)

    def _build_ui(self) -> None:
        sidebar = tk.Frame(self, bg=PALETTE["panel"], width=310)
        sidebar.pack(side="left", fill="y")
        sidebar.pack_propagate(False)
        self._build_sidebar(sidebar)
        main = tk.Frame(self, bg=PALETTE["bg"])
        main.pack(side="left", fill="both", expand=True)
        self._build_main(main)

    def _build_sidebar(self, parent: tk.Frame) -> None:
        brand = tk.Frame(parent, bg=PALETTE["panel"])
        brand.pack(fill="x", padx=24, pady=(24, 16))
        tk.Label(brand, text="✦", font=("TkDefaultFont", 24), fg=PALETTE["accent"], bg=PALETTE["panel"]).pack(side="left", padx=(0, 10))
        text_box = tk.Frame(brand, bg=PALETTE["panel"])
        text_box.pack(side="left")
        self._label(text_box, "消息提示中心", font=("Segoe UI", 14, "bold")).pack(anchor="w")
        self._label(text_box, "NOTIFICATION STUDIO", muted=True, font=("Segoe UI", 8, "bold")).pack(anchor="w")
        self._label(parent, "提示模板  ·  向下滚动查看更多", muted=True, font=("Segoe UI", 9, "bold")).pack(anchor="w", padx=25, pady=(0, 8))

        canvas = tk.Canvas(parent, bg=PALETTE["panel"], highlightthickness=0, borderwidth=0)
        self.template_canvas = canvas
        scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview)
        cards = tk.Frame(canvas, bg=PALETTE["panel"])
        cards.bind("<Configure>", lambda _event: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas_window = canvas.create_window((0, 0), window=cards, anchor="nw")
        canvas.bind("<Configure>", lambda event: canvas.itemconfigure(canvas_window, width=event.width))
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side="left", fill="both", expand=True, padx=(8, 0), pady=(0, 18))
        scrollbar.pack(side="right", fill="y", pady=(0, 18))
        canvas.bind("<Enter>", self._enable_template_scroll)
        canvas.bind("<Leave>", self._disable_template_scroll)
        cards.bind("<Enter>", self._enable_template_scroll)
        cards.bind("<Leave>", self._disable_template_scroll)
        for template in self.templates:
            self._make_template_card(cards, template)

    def _enable_template_scroll(self, _event: tk.Event[tk.Misc]) -> None:
        self.bind_all("<MouseWheel>", self._scroll_template_list)

    def _disable_template_scroll(self, _event: tk.Event[tk.Misc]) -> None:
        self.unbind_all("<MouseWheel>")

    def _scroll_template_list(self, event: tk.Event[tk.Misc]) -> None:
        self.template_canvas.yview_scroll(-int(event.delta / 120), "units")

    def _make_template_card(self, parent: tk.Frame, template: Template) -> None:
        card = tk.Frame(parent, bg=PALETTE["panel"], cursor="hand2")
        card.pack(fill="x", padx=8, pady=3)
        icon = tk.Label(card, text=template.symbol, font=("TkDefaultFont", 15, "bold"), fg=PALETTE["accent"], bg=PALETTE["panel"], width=3)
        icon.pack(side="left", pady=9)
        body = tk.Frame(card, bg=PALETTE["panel"])
        body.pack(side="left", fill="x", expand=True, pady=8)
        self._label(body, template.name, font=("Segoe UI", 10, "bold")).pack(anchor="w")
        self._label(body, template.subtitle, muted=True, font=("Segoe UI", 8)).pack(anchor="w", pady=(2, 0))
        self.template_buttons[template.key] = card
        for widget in (card, icon, body, *body.winfo_children()):
            widget.bind("<Button-1>", lambda _event, key=template.key: self.select_template(key))
        self._paint_template_card(template.key)

    def _paint_template_card(self, key: str) -> None:
        card = self.template_buttons[key]
        active = key == self.selected_key
        bg = PALETTE["panel_hover"] if active else PALETTE["panel"]
        card.configure(bg=bg, highlightbackground=PALETTE["accent"] if active else bg, highlightthickness=1 if active else 0)
        self._paint_descendants(card, bg)

    def _paint_descendants(self, widget: tk.Widget, bg: str) -> None:
        try:
            widget.configure(bg=bg)
        except tk.TclError:
            pass
        for child in widget.winfo_children():
            self._paint_descendants(child, bg)

    def select_template(self, key: str) -> None:
        if key == self.selected_key:
            return
        previous = self.selected_key
        self.selected_key = key
        self._paint_template_card(previous)
        self._paint_template_card(key)
        self.status_var.set(f"已选择:{self.template_by_key[key].name}")

    def _build_main(self, parent: tk.Frame) -> None:
        header = tk.Frame(parent, bg=PALETTE["bg"])
        header.pack(fill="x", padx=38, pady=(28, 8))
        self._label(header, "创建一条消息", font=("Segoe UI", 22, "bold")).pack(anchor="w")
        self._label(header, "选择视觉模板、内容与可选图标后,立即发送测试。", muted=True).pack(anchor="w", pady=(3, 0))
        form = tk.Frame(parent, bg=PALETTE["panel"], highlightbackground=PALETTE["border"], highlightthickness=1)
        form.pack(fill="both", expand=True, padx=38, pady=18)
        form.columnconfigure(0, weight=1)
        form.columnconfigure(1, weight=1)
        self._build_content_fields(form)
        self._build_icon_picker(form)
        self._build_options(form)
        self._build_actions(parent)

    def _build_content_fields(self, parent: tk.Frame) -> None:
        self._label(parent, "标题", muted=True, font=("Segoe UI", 9, "bold")).grid(row=0, column=0, sticky="w", padx=25, pady=(20, 7))
        title_entry = tk.Entry(parent, textvariable=self.title_var, bg=PALETTE["surface"], fg=PALETTE["text"], insertbackground=PALETTE["text"], relief="flat", highlightthickness=1, highlightbackground=PALETTE["border"], highlightcolor=PALETTE["accent"], font=("Segoe UI", 11), borderwidth=10)
        title_entry.grid(row=1, column=0, columnspan=2, sticky="ew", padx=25)
        self._label(parent, "消息内容", muted=True, font=("Segoe UI", 9, "bold")).grid(row=2, column=0, sticky="w", padx=25, pady=(18, 7))
        self.content_text = tk.Text(parent, height=5, bg=PALETTE["surface"], fg=PALETTE["text"], insertbackground=PALETTE["text"], relief="flat", highlightthickness=1, highlightbackground=PALETTE["border"], highlightcolor=PALETTE["accent"], font=("Segoe UI", 10), padx=10, pady=9, wrap="word")
        self.content_text.insert("1.0", "你的消息已发送成功。")
        self.content_text.grid(row=3, column=0, columnspan=2, sticky="nsew", padx=25)

    def _build_icon_picker(self, parent: tk.Frame) -> None:
        box = tk.Frame(parent, bg=PALETTE["panel"])
        box.grid(row=4, column=0, columnspan=2, sticky="ew", padx=25, pady=(18, 0))
        self._label(box, "自定义图标(悬浮模板)", muted=True, font=("Segoe UI", 9, "bold")).pack(anchor="w")
        row = tk.Frame(box, bg=PALETTE["panel"])
        row.pack(fill="x", pady=(7, 0))
        self.icon_preview = tk.Label(row, text="无", font=("Segoe UI", 8), fg=PALETTE["muted"], bg=PALETTE["surface"], width=5, height=2)
        self.icon_preview.pack(side="left")
        self._label(row, None, textvariable=self.icon_path_var, muted=True, font=("Segoe UI", 9), anchor="w").pack(side="left", fill="x", expand=True, padx=10)
        tk.Button(row, text="选择图标", command=self.choose_icon, bg=PALETTE["surface"], fg=PALETTE["text"], activebackground=PALETTE["panel_hover"], activeforeground=PALETTE["text"], relief="flat", cursor="hand2", padx=12, pady=7).pack(side="right")
        tk.Button(row, text="清除", command=self.clear_icon, bg=PALETTE["panel"], fg=PALETTE["muted"], activebackground=PALETTE["panel_hover"], relief="flat", cursor="hand2", padx=8, pady=7).pack(side="right", padx=5)
        self._label(box, "仅支持 PNG / GIF / PPM / PGM,尺寸必须介于 16×16 与 256×256 像素。Windows 系统消息使用系统图标。", muted=True, font=("Segoe UI", 8), wraplength=600, justify="left").pack(anchor="w", pady=(5, 0))

    def _build_options(self, parent: tk.Frame) -> None:
        box = tk.Frame(parent, bg=PALETTE["panel"])
        box.grid(row=5, column=0, columnspan=2, sticky="ew", padx=25, pady=18)
        for index in range(3):
            box.columnconfigure(index, weight=1)
        for index, text in enumerate(("消息类型", "显示位置", "停留时间")):
            self._label(box, text, muted=True, font=("Segoe UI", 9, "bold")).grid(row=0, column=index, sticky="w", padx=0 if index == 0 else 12)
        ttk.Combobox(box, textvariable=self.level_var, values=list(LEVELS), state="readonly", style="Dark.TCombobox").grid(row=1, column=0, sticky="ew", pady=(8, 0))
        ttk.Combobox(box, textvariable=self.position_var, values=["右下", "右上", "左下", "左上"], state="readonly", style="Dark.TCombobox").grid(row=1, column=1, sticky="ew", padx=12, pady=(8, 0))
        duration = tk.Frame(box, bg=PALETTE["panel"])
        duration.grid(row=1, column=2, sticky="ew", padx=12, pady=(6, 0))
        ttk.Scale(duration, from_=1, to=10, variable=self.duration_var, orient="horizontal", style="Horizontal.TScale").pack(side="left", fill="x", expand=True)
        value = self._label(duration, "4 秒", font=("Segoe UI", 9, "bold"), width=5)
        value.pack(side="right", padx=(8, 0))
        self.duration_var.trace_add("write", lambda *_: value.configure(text=f"{self.duration_var.get():.0f} 秒"))

    def _build_actions(self, parent: tk.Frame) -> None:
        action = tk.Frame(parent, bg=PALETTE["bg"])
        action.pack(fill="x", padx=38, pady=(0, 22))
        self._label(action, None, textvariable=self.status_var, muted=True, font=("Segoe UI", 9)).pack(side="left", pady=12)
        controls = tk.Frame(action, bg=PALETTE["bg"])
        controls.pack(side="right", fill="x")
        tk.Checkbutton(controls, text="调试模式", variable=self.debug_var, command=self._on_debug_toggle, bg=PALETTE["bg"], fg=PALETTE["muted"], activebackground=PALETTE["bg"], activeforeground=PALETTE["text"], selectcolor=PALETTE["surface"], highlightthickness=0, font=("Segoe UI", 9), cursor="hand2").pack(side="left", padx=(0, 10))
        self.test_button = tk.Button(controls, text="◉  测试发送", command=self.send_selected, bg=PALETTE["accent"], fg="white", activebackground="#419d88", activeforeground="white", relief="flat", cursor="hand2", font=("Segoe UI", 10, "bold"), padx=14, pady=11)
        self.test_button.pack(side="right", fill="x", expand=True)
        self.reset_button = tk.Button(controls, text="重置", command=self.reset_form, bg=PALETTE["surface"], fg=PALETTE["text"], activebackground=PALETTE["panel_hover"], activeforeground=PALETTE["text"], relief="flat", cursor="hand2", font=("Segoe UI", 10), padx=18, pady=11)
        self.reset_button.pack(side="right", padx=10)
        parent.bind("<Configure>", self._resize_action_buttons, add="+")

    def _resize_action_buttons(self, event: tk.Event[tk.Misc]) -> None:
        compact = event.width < 570
        self.test_button.configure(text="发送" if compact else "◉  测试发送", padx=8 if compact else 14)
        self.reset_button.configure(text="↺" if compact else "重置", padx=10 if compact else 18)

    def _on_debug_toggle(self) -> None:
        state = "已开启" if self.debug_var.get() else "已关闭"
        self.status_var.set(f"调试模式{state}")
        self._log(f"调试模式{state}")

    def _log(self, message: str, *, exception: BaseException | None = None) -> None:
        if not self.debug_var.get():
            return
        lines = [f"[{datetime.now():%Y-%m-%d %H:%M:%S}] {message}"]
        if exception is not None:
            lines.extend(traceback.format_exception(type(exception), exception, exception.__traceback__))
        try:
            with LOG_FILE.open("a", encoding="utf-8") as log_file:
                log_file.write("\n".join(lines) + "\n")
        except OSError:
            pass

    def choose_icon(self) -> None:
        path = filedialog.askopenfilename(title="选择消息图标", filetypes=[("支持的图像", "*.png *.gif *.ppm *.pgm"), ("PNG", "*.png"), ("GIF", "*.gif")])
        if not path:
            return
        extension = os.path.splitext(path)[1].lower()
        if extension not in SUPPORTED_ICON_TYPES:
            messagebox.showwarning("不支持的图标", "请使用 PNG、GIF、PPM 或 PGM 格式的图标。", parent=self)
            return
        try:
            image = tk.PhotoImage(file=path)
            width, height = image.width(), image.height()
        except tk.TclError as exc:
            messagebox.showerror("图标加载失败", f"无法读取该图像:{exc}", parent=self)
            return
        if not (MIN_ICON_SIZE <= width <= MAX_ICON_SIZE and MIN_ICON_SIZE <= height <= MAX_ICON_SIZE):
            messagebox.showwarning("图标尺寸不符合要求", f"图标实际为 {width}×{height},允许范围是 {MIN_ICON_SIZE}×{MIN_ICON_SIZE} 至 {MAX_ICON_SIZE}×{MAX_ICON_SIZE} 像素。", parent=self)
            return
        scale = max(1, (max(width, height) + 47) // 48)
        self.icon_image = image.subsample(scale, scale)
        self.icon_preview.configure(image=self.icon_image, text="", width=48, height=48)
        self.icon_path_var.set(os.path.basename(path))
        self.status_var.set(f"已加载图标:{width}×{height}")

    def clear_icon(self) -> None:
        self.icon_image = None
        self.icon_preview.configure(image="", text="无", width=5, height=2)
        self.icon_path_var.set("未选择图标")
        self.status_var.set("已清除图标")

    def get_message(self) -> tuple[str, str]:
        return self.title_var.get().strip() or "消息提示", self.content_text.get("1.0", "end").strip() or "这是一条消息。"

    def send_selected(self) -> None:
        title, content = self.get_message()
        template = self.template_by_key[self.selected_key]
        self._log(f"发送请求:模板={template.key},标题={title!r},内容长度={len(content)}")
        try:
            template.sender(title, content)
            self.status_var.set(f"已发送:{template.name}({time.strftime('%H:%M:%S')})")
            self._log(f"发送完成:模板={template.key}")
        except Exception as exc:
            self.status_var.set("发送失败")
            self._log("发送异常", exception=exc)
            messagebox.showerror("发送失败", str(exc), parent=self)

    def _show_floating(self, title: str, content: str, style: str) -> None:
        FloatingNotice(self, title, content, style=style, duration=self.duration_var.get(), position=self.position_var.get(), icon=self.icon_image)

    def send_windows_toast(self, title: str, content: str) -> None:
        if platform.system() != "Windows":
            raise RuntimeError("Windows Toast 通知仅支持 Windows 系统。")
        payload = base64.b64encode(json.dumps({"title": title, "content": content}, ensure_ascii=False).encode("utf-8")).decode("ascii")
        command = [
            "powershell.exe",
            "-NoProfile",
            "-File",
            str(TOAST_SCRIPT_FILE),
            "-Payload",
            payload,
        ]
        self._log(f"启动 Windows Toast:脚本={TOAST_SCRIPT_FILE},命令={command!r}")
        try:
            completed = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", timeout=15, creationflags=subprocess.CREATE_NO_WINDOW)
        except (OSError, subprocess.TimeoutExpired) as exc:
            self._log("启动 Windows Toast 失败", exception=exc)
            raise RuntimeError(f"无法启动 Windows Toast:{exc}") from exc
        self._log(f"Windows Toast 退出:returncode={completed.returncode},stdout={completed.stdout!r},stderr={completed.stderr!r}")
        if completed.returncode != 0:
            detail = completed.stderr.strip() or completed.stdout.strip() or "PowerShell 未返回具体错误。"
            raise RuntimeError(f"Windows Toast 发送失败:{detail}")

    def send_toast(self, title: str, content: str) -> None: self._show_floating(title, content, "toast")
    def send_island(self, title: str, content: str) -> None: self._show_floating(title, content, "island")
    def send_clickgui(self, title: str, content: str) -> None: self._show_floating(title, content, "clickgui")
    def send_banner(self, title: str, content: str) -> None: self._show_floating(title, content, "banner")
    def send_bubble(self, title: str, content: str) -> None: self._show_floating(title, content, "bubble")
    def send_terminal(self, title: str, content: str) -> None: self._show_floating(title, content, "terminal")
    def send_letter(self, title: str, content: str) -> None: self._show_floating(title, content, "letter")
    def send_pill(self, title: str, content: str) -> None: self._show_floating(title, content, "pill")
    def send_chibi(self, title: str, content: str) -> None: self._show_floating(title, content, "chibi")
    def send_spotlight(self, title: str, content: str) -> None: self._show_floating(title, content, "spotlight")

    def reset_form(self) -> None:
        self.title_var.set("操作完成")
        self.content_text.delete("1.0", "end")
        self.content_text.insert("1.0", "你的消息已发送成功。")
        self.level_var.set("信息")
        self.duration_var.set(4)
        self.position_var.set("右下")
        self.clear_icon()
        self.status_var.set("已重置表单")

if __name__ == "__main__":
    MessageCenter().mainloop()
a37bd01d8e213b85cf849b2f8efa19df.png
ab163fa3765dabc042c75f303ca4e840.png
32e64e2fec0d542bf37ce11f281c94be.png
05290355e96a3df05adf3a1e6171e8a1.png

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
兮兮曦 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

iawyxkdn8 发表于 2026-7-31 13:53
蹲个坑,大多数人要的是把所有的信息集中起来查阅及回复,这个怎么用?是否发送成功,用这个是不是太闲了!
PulpF4on 发表于 2026-7-31 15:03
挺有意思的工具,虽然可能对我来说用到的机会小,可以学习下
wei1024 发表于 2026-7-31 15:21
开创者 发表于 2026-7-31 15:55
不错,就是实际使用,对新手来说还是比较麻烦,没直接问AI来的容易
 楼主| Serendisand 发表于 2026-7-31 16:27
iawyxkdn8 发表于 2026-7-31 13:53
蹲个坑,大多数人要的是把所有的信息集中起来查阅及回复,这个怎么用?是否发送成功,用这个是不是太闲了!

因为我开发这个程序的目的,不是为了让大家使用,而是为了让开发者能从中获取到灵感,我该怎么做一个消息通知?以及有我能参考的简易模板吗?有哪些样式和功能实现是我能使用的吗?
准确来说这并不是一款面向用户的程序
shockwave123 发表于 2026-7-31 17:56
谢谢分享,界面干净,可以用在自己程序中
 楼主| Serendisand 发表于 2026-7-31 18:02
开创者 发表于 2026-7-31 15:55
不错,就是实际使用,对新手来说还是比较麻烦,没直接问AI来的容易

也许我应该把他做成一个帖子,而不是一个完整的程序,这点我学习到了,谢谢你
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-12 12:48

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表