将 WebApp 添加到 Winforms 项目






4.85/5 (5投票s)
使用 httpListener、WebSockets 和 JavaScript 为 Windows Forms 程序添加远程控制
引言
这是一篇关于如何将托管网页添加到 Windows Winforms 桌面程序的指南,该网页允许通过在另一台设备上查看的网页控制程序。
本文着眼于托管/提供 Web 文件、从网页向 Windows Form 发送命令,以及从 winform 向网页发送数据更新。
网页使用 HTML/JavaScript,并通过 Web 套接字连接使用 XML 封装的数据,在 WebApp 和 Windows 程序之间进行双向通信。
WebSocket 连接通过打开 http 请求并请求升级到 websocket 来工作。这允许在可以建立 Web 连接的任何地方打开连接。与发送请求的数据然后关闭连接的 http 请求不同,WebSocket 连接保持打开状态以进行双向通信,直到连接被明确关闭。
背景
我最初开发这个是为了与 SFXPlayer 一起使用,SFXPlayer 是一个为戏剧用途开发的 Windows 音效播放器。Web 应用程序为 Windows Forms 程序添加了一个简单的远程控制。它显示了提示说明、要播放的音轨以及播放/停止/快进/快退按钮。
Web 应用程序
index.html 包含 Web 应用程序布局,sfx.js 包含用于打开 websocket 连接和处理与主程序通信的 JavaScript。
<!DOCTYPE html>
<html>
<head>
<title id="Title">SFX Player</title>
<script src="sfx.js"></script>
</head>
<body>
<table style="width:100%">
<tr>
<td id="PrevMainText">Cell 1</td>
<td id="MainText">Cell 2</td>
</tr>
<tr>
<td colspan="2" id="TrackName"></td>
</tr>
</table>
<button onclick="sfxws.sendCommand('previous')">Previous</button>
<button onclick="sfxws.sendCommand('stop')">Stop</button>
<button onclick="sfxws.sendCommand('play')">Go</button>
<button onclick="sfxws.sendCommand('next')">Next</button>
</body>
</html>
除了一个简单的表格外,我没有添加任何样式或布局控制。带有 id
(包括 title
)的节点可以从 winforms 程序更新。四个按钮分别向 winforms 程序发送一个命令。
sfx.js 文件包含实现必要 websocket 代码的 JavaScript。它在页面加载后创建 websocket 连接
function init() {
sfxws = new SFXWebSocket();
}
document.addEventListener('DOMContentLoaded', init);
连接被打开到原始 http 地址
var ws = new WebSocket("ws://" + location.hostname + ":3030", "ws-SFX-protocol");
消息接收事件期望 XML,并遍历所有节点,并使用节点的内容更新 ID 与节点名称匹配的 DOM 节点。
ws.onmessage = function (evt) {
var received_msg = evt.data;
//console.log("Message received:\n" + received_msg);
BuildXMLFromString(received_msg);
//document.getElementById("PrevMainText").innerHTML =
//xmlDoc.getElementsByTagName("PrevMainText")[0].childNodes[0].nodeValue;
var DisplaySettings = xmlDoc.getElementsByTagName("DisplaySettings")[0].childNodes;
if (DisplaySettings != null) {
for (i = 0; i < DisplaySettings.length; i++) {
if (DisplaySettings[i].nodeType == Node.ELEMENT_NODE) {
if (DisplaySettings[i + 1].nodeType == Node.TEXT_NODE) {
var field = document.getElementById(DisplaySettings[i].nodeName);
if (field != null) {
field.innerHTML = DisplaySettings[i].textContent;
} else {
console.log("Unable to locate id=" +
DisplaySettings[i].nodeName +
". New value = " + DisplaySettings[i].textContent);
}
}
}
}
}
};
从 winforms 程序发送的 XML 示例,用于更新网页标题
<DisplaySettings>
<Title>New Title Test</Title>
</DisplaySettings>
Web 服务器
Web 服务器的源文件可在 此处找到 (WebApp.cs)
使用 HttpListener
类构建一个简单的 Web 服务器。如果请求是针对 websocket 连接的 (context.Request.IsWebSocketRequest
),则代码在 ProcessWebSocketRequest()
中处理此请求。
其他请求被假定为针对文件,这些文件被查找并发送或标记为错误。
服务器通过从主窗体的 Load
事件处理程序调用 WebApp.Start();
来启动。
维护一个 Web 套接字连接列表,以便可以处理多个并发连接
private static List<WebSocket> webSockets = new List<WebSocket>();
任何显示更新都将发送到所有连接
LastMessage = Encoding.UTF8.GetBytes(e.SerializeToXmlString());
foreach (WebSocket ws in webSockets) {
await ws.SendAsync(new ArraySegment<byte>(LastMessage, 0, LastMessage.Length),
WebSocketMessageType.Text, true, CancellationToken.None);
}
这里的一个怪癖是,由于 C# 字符串是 Unicode 的,XML 序列化程序添加了它们是 UTF-16 的标头,但我们将其转换为 UTF-8 来发送它们。最好更改文本以反映这一点。
来自 Web 应用程序的消息被手动编码为 XML 代码片段,形式如下
<command>play</command>
winforms 程序将其接收为 string
,将其转换为 XMLDocument
并对 command 节点进行操作。
string strXML = Encoding.UTF8.GetString(receiveBuffer, 0, receiveResult.Count);
//Debug.WriteLine(strXML);
XmlDocument xml = new XmlDocument();
xml.LoadXml(strXML);
var nodes = xml.SelectNodes("command");
switch (command){
case "play":
Program.mainForm.PlayNextCue();
break;
由于 Web 套接字处理程序在与窗体不同的线程中运行,因此更新必须使用 Invoke
。我发现按钮对于 InvokeRequired
不返回 true
,所以我使用了我的 CueList
对象。
private delegate void SafeCommandDelegate();
internal void PlayNextCue() {
if (CueList.InvokeRequired) {
var d = new SafeCommandDelegate(PlayNextCue);
CueList.Invoke(d);
} else {
bnPlayNext_Click(null, null);
}
}
关注点
打开端口(本例中为 3030)以提供 Web 文件很困难。当我找出实际相关的步骤时,我会在这里发布它们。
历史
- 2019 年 11 月 29 日:第一版(实际
SFXPlayer
程序仍然需要为 Web 应用程序设置样式)