目錄
前言
技術原理概述
測試代碼和程序下載連接
本文出處鏈接: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.