获取 Windows 通知中心弹窗通知内容(含工具汉化)

news2024/12/19 20:14:28

目录

前言

技术原理概述

测试代码和程序下载连接


本文出处链接:https://blog.csdn.net/qq_59075481/article/details/136440280。

前言

从 Windows 8.1 开始,Windows 通知现在以 Toast 而非 Balloon 形式显示( Bollon 通知其实现在是应用通知的一个子集),并记录在通知中心中。到目前为止,为了检索通知的内容,您必须抓取窗口的句柄并尝试读取它的文本内容,或者其他类似的东西。

特别是在 Toast 可用之后,这样做变得很困难,但从 Windows 10 Anniversary Edition (10.0.14393.0) 版本开始, MS 实现了“通知监听器” API(UserNotificationListener),允许您以与获取 Android 通知相同的方式获取 Windows 通知。

Toast 通知

技术原理概述

参考 gpsnmeajp 的代码思路,

(原文翻译:https://blog.csdn.net/qq_59075481/article/details/136433878)

我们使用 UserNotificationListener 这个 WinRT API 来监视系统通知区域的弹窗。该 API 不仅可以拦截 Toast 通知(应用通知),而且可以拦截旧式的 Balloon 通知。

Balloon 通知

下面是异步获取消息的处理代码(await 异步关键字在 C++ 中没有,需要额外的自己构建处理模式,所以一般代码使用 C#):

首先,我们只关心前三个字段 id、title 和 body。title 是通知的标题,body 是通知的内容。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Windows.Forms;
using Windows.UI.Notifications.Management;
using Windows.Foundation.Metadata;
using Windows.UI.Notifications;

namespace NotificationListenerThrower
{
    class NotificationMessage
    {
        public uint id { get; set; }
        public string title { get; set; }
        public string body { get; set; }

        public NotificationMessage(uint id, string title, string body) {
            this.id = id;
            this.title = title != null ? title : "";
            this.body = body != null ? body : "";
        }
    }
}

获取消息的代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Windows.Forms;
using Windows.UI.Notifications.Management;
using Windows.Foundation.Metadata;
using Windows.UI.Notifications;


namespace NotificationListenerThrower
{
    class Notification
    {
        bool accessAllowed = false;
        UserNotificationListener userNotificationListener = null;

        public async Task<bool> Init()
        {
            if (!ApiInformation.IsTypePresent("Windows.UI.Notifications.Management.UserNotificationListener"))
            {
                accessAllowed = false;
                userNotificationListener = null;
                return false;
            }

            userNotificationListener = UserNotificationListener.Current;
            UserNotificationListenerAccessStatus accessStatus = await userNotificationListener.RequestAccessAsync();

            if (accessStatus != UserNotificationListenerAccessStatus.Allowed) {
                accessAllowed = false;
                userNotificationListener = null;
                return false;
            }
            accessAllowed = true;
            return true;
        }

        public async Task<List<NotificationMessage>> Get()
        {
            if (!accessAllowed) {
                return new List<NotificationMessage>();
            }

            List<NotificationMessage> list = new List<NotificationMessage>();

            IReadOnlyList<UserNotification> userNotifications = await userNotificationListener.GetNotificationsAsync(NotificationKinds.Toast);
            foreach (var n in userNotifications)
            {
                var notificationBinding = n.Notification.Visual.GetBinding(KnownNotificationBindings.ToastGeneric);
                if (notificationBinding != null)
                {
                    IReadOnlyList<AdaptiveNotificationText> textElements = notificationBinding.GetTextElements();
                    string titleText = textElements.FirstOrDefault()?.Text;
                    string bodyText = string.Join("\n", textElements.Skip(1).Select(t => t.Text));

                    list.Add(new NotificationMessage(n.Id, titleText, bodyText));
                }
            }
            return list;
        }
    }

}

gpsnmeajp 通过将功能写入 ListBox 和转发到 WebSocket 实现向远程客户端分发通知的信息。

NotificationListenerThrower 作为中间人获取 Windows 通知中心的消息内容,并通过 WebSocket 向客户端转发消息内容( 模式-> 外模式)。

软件组织逻辑

下面是该工具的 WebSocket 前/后端实现。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Windows.Forms;
using Windows.UI.Notifications.Management;
using Windows.Foundation.Metadata;
using Windows.UI.Notifications;


namespace NotificationListenerThrower
{
    class Websocket
    {
        HttpListener httpListener = null;
        Task httpListenerTask = null;

        List<WebSocket> WebSockets = new List<WebSocket>();

        bool localOnly = false;
        bool viewer = false;

        public void Open(string port, bool localOnly, bool viewer) {
            this.localOnly = localOnly;
            this.viewer = viewer;

            string host = localOnly ? "127.0.0.1" : "+";

            httpListener = new HttpListener();
            httpListener.Prefixes.Add("http://" + host + ":" + port + "/");
            httpListener.Start();

            //接続待受タスク
            httpListenerTask = new Task(async () => {
                try
                {
                    while (true)
                    {
                        HttpListenerContext context = await httpListener.GetContextAsync();
                        if (localOnly == true && context.Request.IsLocal == false)
                        {
                            context.Response.StatusCode = 400;
                            context.Response.Close(Encoding.UTF8.GetBytes("400 Bad request"), true);
                            continue;
                        }

                        if (!context.Request.IsWebSocketRequest)
                        {
                            if (viewer)
                            {
                                context.Response.StatusCode = 200;
                                context.Response.Close(Encoding.UTF8.GetBytes(html), true);
                            }
                            else {
                                context.Response.StatusCode = 404;
                                context.Response.Close(Encoding.UTF8.GetBytes("404 Not found"), true);
                            }
                            continue;
                        }

                        if (WebSockets.Count > 1024)
                        {
                            context.Response.StatusCode = 503;
                            context.Response.Close(Encoding.UTF8.GetBytes("503 Service Unavailable"), true);
                            continue;
                        }

                        HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
                        WebSocket webSocket = webSocketContext.WebSocket;

                        if (localOnly == true && webSocketContext.IsLocal == false)
                        {
                            webSocket.Abort();
                            continue;
                        }

                        WebSockets.Add(webSocket);
                    }
                }
                catch (HttpListenerException)
                {
                    //Do noting (Closed)
                }
            });
            httpListenerTask.Start();
        }

        public async Task Broadcast(string msg) {
            ArraySegment<byte> arraySegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes(msg));
            foreach (var ws in WebSockets)
            {
                try
                {
                    if (ws.State == WebSocketState.Open)
                    {
                        await ws.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
                    }
                    else
                    {
                        ws.Abort();
                    }
                }
                catch (WebSocketException)
                {
                    //Do noting (Closed)
                }
            }
            WebSockets.RemoveAll(ws => ws.State != WebSocketState.Open);
        }

        public void Close() {
            foreach (var ws in WebSockets)
            {
                ws.Abort();
                ws.Dispose();
            }
            WebSockets.Clear();

            try
            {
                httpListener?.Stop();
            }
            catch (Exception) { 
                //Do noting
            }
            httpListenerTask?.Wait();
            httpListenerTask?.Dispose();
        }

        public int GetConnected() {
            return WebSockets.Count;
        }


        string html = @"<html>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width,initial-scale=1'>
</head>
<body>
<input id='ip' value='ws://127.0.0.1:8000'></input>
<button onclick='connect();'>开始连接</button>
<button onclick='disconnect();'>取消连接</button>
<div id='ping'></div>
<div id='log'></div>
<script>
let socket = null;
let lastupdate = new Date();

window.onload = function() {
	document.getElementById('ip').value = 'ws://'+location.host;
    connect();
};
function connect(){
	try{
	if(socket != null){
		socket.close();
	}
	socket = new WebSocket(document.getElementById('ip').value);
	socket.addEventListener('error', function (event) {
	    document.getElementById('log').innerText = '连接失败';	
	});
	socket.addEventListener('open', function (event) {
	    document.getElementById('log').innerText = '持续连接中...';	
	});
	socket.addEventListener('message', function (event) {
        let packet = JSON.parse(event.data);
		if('ping' in packet){
            lastupdate = new Date();
		    document.getElementById('ping').innerText = 'ping: '+lastupdate;
		}else{
		    document.getElementById('log').innerText = packet.id +':'+packet.title+':'+packet.body +'\n'+ document.getElementById('log').innerText;
		}
	});
	socket.addEventListener('onclose', function (event) {
	    document.getElementById('log').innerText = document.getElementById('log').innerText +'\n' +'CLOSED';
		socket.close();
		socket = null;
	});
	}catch(e){
	    document.getElementById('log').innerHTML = e;	
	}
}
function disconnect(){
	socket.close();
	socket = null;
    document.getElementById('log').innerText = '正在连接';	
}
function timeout(){
    if(new Date().getTime() - lastupdate.getTime() > 3000){
        if(socket != null){
            document.getElementById('ping').innerText = 'ping: 超时! 正在重新连接...';
            disconnect();
            connect();
        }else{
            document.getElementById('ping').innerText = 'ping: 超时!';
        }
    }
}
setInterval(timeout,1000);

</script>
</body>
</html>";
    }
}

应用程序及后端代码如下。其中在 WatchTimer_Tick 方法内修复了使用默认参数调用 JsonSerializer 在序列化文本时,编码错误的问题。这使得中文文本可以正常显示在应用程序中。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Windows.Forms;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace NotificationListenerThrower
{
    public partial class Form1 : Form
    {
        class Setting {
            public string port { get; set; }
            public bool localonly { get; set; }
            public bool viewer{ get; set; }
        }

        Websocket websocket = new Websocket();
        Notification notification = new Notification();
        uint sent = 0;

        public Form1()
        {
            InitializeComponent();
        }

        private async void Form1_Load(object sender, EventArgs e)
        {
            if (!await notification.Init())
            {
                PresentTextBox.Text = "载入中";
                PresentTextBox.BackColor = Color.Red;
                return;
            }
            PresentTextBox.Text = "就绪";
            PresentTextBox.BackColor = Color.Green;

            open(load());
        }
        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            websocket.Close();
        }

        List<NotificationMessage> lastNotificationMessage = new List<NotificationMessage>();
        private async void WatchTimer_Tick(object sender, EventArgs e)
        {
            List<NotificationMessage> notificationMessage = await notification.Get();

            DetectedListBox.Items.Clear();
            foreach (var n in notificationMessage)
            {
                // 使用 UnsafeRelaxedJsonEscaping 编码器,
                // 它会在 JSON 字符串中对非 ASCII 字符进行逃逸处理,
                // 以确保正确的序列化。
                var options = new JsonSerializerOptions
                {
                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
                };

                string msg = JsonSerializer.Serialize(n, options);

                // 支持中文编码
                DetectedListBox.Items.Add(msg);

                if (lastNotificationMessage.Where(l => l.id == n.id).Count() == 0) {
                    // 新建
                    await websocket.Broadcast(msg);
                    sent = unchecked(sent + 1);
                }
            }
            lastNotificationMessage = notificationMessage;

            SendTextBox.Text = sent.ToString();
       }
        private async void PingTimer_Tick(object sender, EventArgs e)
        {
            ConnectedTextBox.Text = websocket.GetConnected().ToString();
            await websocket.Broadcast("{\"ping\":true}");
        }


        private void ApplyButton_Click(object sender, EventArgs e)
        {
            open(save());
        }

        private Setting load()
        {
            Setting setting;
            if (File.Exists("setting.json"))
            {
                string json = File.ReadAllText("setting.json");
                try
                {
                    setting = JsonSerializer.Deserialize<Setting>(json);
                    PortTextBox.Text = setting.port;
                    LocalOnlyCheckBox.Checked = setting.localonly;
                    ViewerCheckBox.Checked = setting.viewer;
                    return setting;
                }
                catch (JsonException)
                {
                    //Do noting (json error)
                }
            }
            setting = new Setting
            {
                port = PortTextBox.Text,
                localonly = LocalOnlyCheckBox.Checked,
                viewer = ViewerCheckBox.Checked
            };
            return setting;
        }
        private Setting save()
        {
            Setting setting = new Setting
            {
                port = PortTextBox.Text,
                localonly = LocalOnlyCheckBox.Checked,
                viewer = ViewerCheckBox.Checked
            };

            string json = JsonSerializer.Serialize(setting);
            File.WriteAllText("setting.json", json);
            return setting;
        }

        private void open(Setting setting)
        {
            AccessStatusTextBox.Text = "CLOSED";
            AccessStatusTextBox.BackColor = Color.Red;

            websocket.Close();

            try
            {
                websocket.Open(setting.port, setting.localonly, setting.viewer);
            }
            catch (HttpListenerException e) {
                MessageBox.Show(e.Message);
                return;
            }
            AccessStatusTextBox.Text = "OPEN";
            AccessStatusTextBox.BackColor = Color.Green;
        }
    }
}

这款软件的汉化界面如下图:

NotificationListenerThrower 工具界面

网页测试界面: 

网页端接收的消息

测试代码和程序下载连接

可以在 Github 上获取原版(不支持友好的中文输入输出)

https://github.com/gpsnmeajp/NotificationListenerThrower?tab=readme-ov-file

或者使用我修复并汉化后的版本:

1. NotificationListenerThrower_0.01_Repack(源代码):

        链接:https://wwz.lanzouo.com/iOESN1q7r1cf        密码:2ym3

2. NotificationListenerThrower-Net6.0_x64_10.0.19041.0(可执行文件):

        链接:https://wwz.lanzouo.com/iGFG11q7r21a        密码:5bcw


本文出处链接:https://blog.csdn.net/qq_59075481/article/details/136440280。

发布于:2024.03.03,更新于:2024.03.03.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1487943.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ARM总结and复习

安装交叉编译工具链 a. 为什么安装 因为arm公司的指令集在不断迭代升级&#xff0c;指令集日益增多,而架构是基于指令集研发的&#xff0c;所以架构不一样&#xff0c;指令集也不一样 eg:arm架构使用的是arm指令集 x86架构使用的是x86指令集 而我们日常开发环境中linux的架构…

QueryDet代码解析

配置文件 # 表示该配置文件是基于另一个配置文件BaseRetina.yaml进行扩展和覆盖的 _BASE_: "../BaseRetina.yaml" # 指定输出目录&#xff0c;训练过程中的日志、模型权重和评估结果将保存在该目录下。 OUTPUT_DIR: "work_dirs/visdrone_querydet" # 指定…

前端面试 跨域理解

2 实现 2-1 JSONP 实现 2-2 nginx 配置 2-2 vue 开发中 webpack自带跨域 2 -3 下载CORS 插件 或 chrome浏览器配置跨域 2-4 通过iframe 如&#xff1a;aaa.com 中读取bbb.com的localStorage 1)在aaa.com的页面中&#xff0c;在页面中嵌入一个src为bbb.com的iframe&#x…

Mybatis批量更新对象数据的两种方法

说明&#xff1a;遇到一次需要批量修改对象的场景。传递一个对象集合&#xff0c;需要根据对象ID批量修改数据库数据&#xff0c;使用的是MyBatis框架。查了一些资料&#xff0c;总结出两种实现方式。 创建Demo 首先&#xff0c;创建一个简单的Demo&#xff1b; &#xff08…

SOCKS55代理 VS Http代理,如何选择?

在使用IPFoxy全球代理时&#xff0c;选择 SOCKS55代理还是HTTP代理&#xff1f;IPFoxy代理可以SOCKS55、Http协议自主切换&#xff0c;但要怎么选择&#xff1f;为解决这个问题&#xff0c;得充分了解两种代理的工作原理和配置情况。 在这篇文章中&#xff0c;我们会简要介绍 …

java自动化之自动化框架项目(第五天-解析并执行初始化sql)

1.实现目标 先将常用变量占位符替换为实际值&#xff0c;然后解析并执行初始化sql&#xff0c;保证用例中的数据可用&#xff0c;这样不用每次执行测试前修改测试数据。 2.添加pom依赖 连接操作数据库&#xff08;pom.xml文件中添加&#xff09; <!-- https://mvnreposi…

深度学习PyTorch 之 RNN-中文多分类【代码解析】

上篇文章给出了RNN-中文多分类的代码实现&#xff0c;本次主要是对RNN的架构进行一个详细的解析 1、主代码 class RNN(nn.Module):def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout):super().__init__()# 初始化函数…

白话transformer(一):注意力机制

前面我们分篇讲述了transformer的原理&#xff0c;但是对于很多刚接触transformer的人来说可能会有一点懵&#xff0c;所以我们接下来会分三篇文章用白话的形式在将transformer 讲一遍。 前文链接 Bert基础(一)–自注意力机制 Bert基础(二)–多头注意力 Bert基础(三)–位置编…

Linux安装JumpServer并结合内网穿透实现公网访问本地服务

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

全新攻击面管理平台

首页大屏 内测阶段&#xff0c;免费试用一个月 有兴趣体验的师傅&#xff0c;来长亭云图极速版群里找我 py

基于改进的YOLO算法在TT100K数据集上的交通标志目标检测

交通标志是道路交通管理中的重要元素&#xff0c;准确高效地检测交通标志对于智能交通系统具有重要意义。本文基于改进的YOLO算法&#xff0c;结合TT100K数据集&#xff0c;实现了交通标志的目标检测。通过优化网络结构和训练策略&#xff0c;提升了交通标志检测的准确性和效率…

vue 安装各种问题

新下载了个项目模板&#xff0c;安装包就遇到了各种各样问题 电脑&#xff1a;mac 使用npm i 等命令一直安装项目&#xff0c;然后一直报错 2534 info run canvas2.11.2 install node_modules/canvas node-pre-gyp install --fallback-to-build --update-binary 2535 info r…

rocky使用yum安装msyql8.0

先查看一下源是否有mysql和mysql的版本 yum list mysql* 直接yum install mysql-server 会安装相关7个包 安装完毕后systemctl start mysqld启动mysql 然后mysql_secure_installation配置权限 mysql8的配置稍微有点不一样&#xff0c;按照英文提示来就行&#xff0c;不会的…

rocketmq+rocket-dashboard win10安装部署+注册为Windows服务

1.1 首先去官网下载zip包 选择自己需要的版本 下载 | RocketMQ 1.2 、下载后&#xff0c;解压到指定目录 1.3、配置RocketMQ环境变量 注意&#xff0c;看对应的版本需要jdk版本 1.4、启动mqnameserver 进入bin目录下&#xff0c;双击启动mqnamesrv.cmd 启动后&#xff0c;…

【推荐算法系列十八】:DSSM 召回算法

参考 推荐系统中 DSSM 双塔模型汇总&#xff08;二更&#xff09; DSSM 和 YouTubeDNN 都是比较经典的 U2I 模型。 U2I 召回 U2I 召回也就是 User-to-Item 召回&#xff0c;它基于用户的历史行为以及用户的一些个人信息&#xff0c;对系统中的候选物品进行筛选&#xff0c;挑…

蓝桥杯倒计时 41天 - 二分答案-最大通过数-妮妮的月饼工厂

最大通过数 思路&#xff1a;假设左边能通过 x 关&#xff0c;右边能通过 y 关&#xff0c;x∈[0,n]&#xff0c;通过二分&#xff0c;在前缀和中枚举右边通过的关卡数&#xff0c;保存 xy 的最大值。 #include<bits/stdc.h> using namespace std; typedef long long ll…

价格腰斩,腾讯云2024优惠活动云服务器62元一年,多配置报价

腾讯云服务器多少钱一年&#xff1f;62元一年起&#xff0c;2核2G3M配置&#xff0c;腾讯云2核4G5M轻量应用服务器218元一年、756元3年&#xff0c;4核16G12M服务器32元1个月、312元一年&#xff0c;8核32G22M服务器115元1个月、345元3个月&#xff0c;腾讯云服务器网txyfwq.co…

5、Linux-vi编辑器

目录 一、介绍 二、三种模式 1、命令模式&#xff08;默认&#xff09; 2、插入模式 3、末行模式 4、模式转换 三、基本操作 1、保存文件&#xff08;末行模式下&#xff09; 2、行号&#xff08;末行模式下&#xff09; 3、查找&#xff08;末行模式下&#xff09; …

Python web框架fastapi数据库操作ORM(二)增删改查逻辑实现方法

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;Fastapi 景天的主页&#xff1a;景天科技苑 上一章&#xff0c;我们讲到了fastapi数据库操作ORM的配置和查询操作&#xff0…

【Matlab深度学习】详解matlab深度学习进行时间序列预测

&#x1f517; 运行环境&#xff1a;Matlab &#x1f6a9; 撰写作者&#xff1a;左手の明天 &#x1f947; 精选专栏&#xff1a;《python》 &#x1f525; 推荐专栏&#xff1a;《算法研究》 &#x1f510;#### 防伪水印——左手の明天 ####&#x1f510; &#x1f497; 大家…