目录
前言
技术原理概述
测试代码和程序下载连接
本文出处链接: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 通知。
技术原理概述
参考 gpsnmeajp 的代码思路,
(原文翻译:https://blog.csdn.net/qq_59075481/article/details/136433878)
我们使用 UserNotificationListener 这个 WinRT API 来监视系统通知区域的弹窗。该 API 不仅可以拦截 Toast 通知(应用通知),而且可以拦截旧式的 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;
}
}
}
这款软件的汉化界面如下图:
网页测试界面:
测试代码和程序下载连接
可以在 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.