65.9K
CodeProject 正在变化。 阅读更多。
Home

将 WebApp 添加到 Winforms 项目

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (5投票s)

2019 年 11 月 29 日

CPOL

3分钟阅读

viewsIcon

9593

使用 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 应用程序设置样式)
© . All rights reserved.