WinForms WebBrowser 控件优化

本文将介绍如何通过编写一个小型 HTTP服务器来解决使用WinFormsWebBrowser控件时遇到的一些问题。通常,开发者会直接设置 WebBrowser.DocumentText 属性来显示 HTML,但这种方法存在一些局限性。通过调用 WebBrowser.Navigate 方法并让服务器提供 HTML 和支持文件,可以更接近真实网站的体验,从而让 WebBrowser 控件更加高效。

在开发一个 HTML 编辑器应用程序时,使用了WinForms的 WebBrowser 控件作为显示组件。HTML 内容会随着每次按键而动态变化,因此需要动态地将其发送到控件。最初,使用了 WebBrowser.DocumentText 属性,但这引发了各种小问题。

问题

首先,HTML 中的 和

C# + WebBrowserEx // 父目录 + Article // 基础目录 + WebBrowserEx.html // HTML 文件 + WebBrowserEx // 子目录 + demo.png // 图片文件 + src.zip // 压缩文件 + WebBrowserEx // 代码目录 + WebBrowserEx.sln // 解决方案文件 + ...

HTML 可能看起来像这样:

<img src='WebBrowserEx/demo.png'> <a href='WebBrowserEx/src.zip'>

为了解决这个问题,通过向 HTML 的 <head> 元素添加 <base> 标签来修改发送到控件的 HTML:

<base href='file:///C:/...path.../WebBrowserEx/Article/'>

这解决了图片的问题,但由于某些原因(可能是安全性),链接并没有解决。这是当时能找到的最好的折中方案。

另一个问题是锚点链接。HTML 通常包含一个“目录”列表,基本上是指向锚点的链接列表:

... <h2> 目录 </h2> <ul> <li> <a href='#Introduction'> 介绍 </a> </li> ... <h2> <a name='Introduction'> 介绍 </a> </h2> ...

由于需要 <base> 标签来显示图片,锚点链接被组合以包含 file:// 协议,给出的 URL 如下:

file:///C:/...path.../WebBrowserEx/Article/#Introduction

这些链接没有指向正确的位置,因此内部链接无法工作。

解决方案

总而言之,使用 WebBrowser.DocumentText 属性会导致关于哪些标签有效哪些无效的不可接受的妥协。既然接受了 WebBrowser 控件没有适当的HTTP服务器就不会满意,想知道编写一个服务器有多困难。结果证明这非常容易。

.NET Framework 自 2.0 版本以来就包含了困难的部分:实现HTTP/1.1 协议。相关类位于 System.Net 命名空间的 System.dll 中:

C# class HttpListener // 处理套接字级连接 class HttpListenerContext // 保存请求和响应对象 class HttpListenerRequest // 将请求解析为属性 class HttpListenerResponse // 将响应编译为有效的 HTTP Stream response.OutputStream // 以二进制形式保存响应实体

所要做的就是使用 URL 来返回 HTML 字符串,或者在磁盘上找到文件并将它们作为二进制写入 OutputStream。好吧,确实还需要设置一些头部属性,例如 ContentType,但真的:框架中编码的知识量惊人!

使用代码

如果要求与相符,只需要将 WebBrowser 实例替换为 WebBrowserEx 并调用 LoadHtml(string html, string baseDirectory) 而不是设置 DocumentText。就是这样。

基本服务器代码非常简单。如果要求略有不同,只需要更改获取二进制数据的位置。例如,如果图片存储在程序集中的资源中,可以从那里提供它们。不再有奇怪的 URI 和 Base64 编码!

完整源代码

通常不会在文章本身中提供完整的源代码,但由于总共只有 200 行:

using System; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Mime; using System.Text; using System.Threading; namespace Common { public class WebBrowserEx : System.Windows.Forms.WebBrowser { string _Html = String.Empty; string _BaseDirectory = String.Empty; public void LoadHtml(string html, string baseDirectory) { if (SynchronizationContext.Current != _UI) throw new ApplicationException("WebBrowserEx.LoadHtml must be called on the UI thread."); InitServer(); _Html = html; _BaseDirectory = baseDirectory; if (_Server == null) return; _Server.SetHtml(html, baseDirectory); Navigate(_Server.Url); } SynchronizationContext _UI = null; bool _TriedToStartServer = false; Server _Server = null; public WebBrowserEx() { _UI = SynchronizationContext.Current; } void InitServer() { if (_TriedToStartServer) return; _TriedToStartServer = true; _Server = new Server(); } public class Server { string _Html = String.Empty; string _BaseDirectory = String.Empty; public void SetHtml(string html, string baseDirectory) { _Html = html; _BaseDirectory = baseDirectory; } public Uri Url { get { return new Uri("http://" + "localhost" + ":" + _Port + "/"); } } HttpListener _Listener = null; int _Port = -1; public Server() { var rnd = new Random(); for (int i = 0; i < 100; i++) { int port = rnd.Next(49152, 65536); try { _Listener = new HttpListener(); _Listener.Prefixes.Add("http://localhost:" + port + "/"); _Listener.Start(); _Port = port; _Listener.BeginGetContext(ListenerCallback, null); return; } catch (Exception x) { _Listener.Close(); Debug.WriteLine("HttpListener.Start:\n" + x); } } throw new ApplicationException("Failed to start HttpListener"); } public void ListenerCallback(IAsyncResult ar) { _Listener.BeginGetContext(ListenerCallback, null); var context = _Listener.EndGetContext(ar); var request = context.Request; var response = context.Response; Debug.WriteLine("SERVER: " + _BaseDirectory + "" + request.Url); response.AddHeader("Cache-Control", "no-cache"); try { if (request.Url.AbsolutePath == "/") { response.ContentType = MediaTypeNames.Text.Html; response.ContentEncoding = Encoding.UTF8; var buffer = Encoding.UTF8.GetBytes(_Html); response.ContentLength64 = buffer.Length; using (var s = response.OutputStream) s.Write(buffer, 0, buffer.Length); return; } var filepath = Path.Combine(_BaseDirectory, request.Url.AbsolutePath.Substring(1)); Debug.WriteLine("--FILE: " + filepath); if (!File.Exists(filepath)) { response.StatusCode = (int)HttpStatusCode.NotFound; // 404 response.StatusDescription = response.StatusCode + " Not Found"; response.ContentType = MediaTypeNames.Text.Html; response.ContentEncoding = Encoding.UTF8; var buffer = Encoding.UTF8.GetBytes("404 Not Found"); response.ContentLength64 = buffer.Length; using (var s = response.OutputStream) s.Write(buffer, 0, buffer.Length); return; } byte[] entity = null; try { entity = File.ReadAllBytes(filepath); } catch (Exception x) { Debug.WriteLine("Exception reading file: " + filepath + "\n" + x); response.StatusCode = (int)HttpStatusCode.InternalServerError; // 500 response.StatusDescription = response.StatusCode + " Internal Server Error"; response.ContentType = MediaTypeNames.Text.Html; response.ContentEncoding = Encoding.UTF8; var buffer = Encoding.UTF8.GetBytes("500 Internal Server Error"); response.ContentLength64 = buffer.Length; using (var s = response.OutputStream) s.Write(buffer, 0, buffer.Length); return; } response.ContentLength64 = entity.Length; switch (Path.GetExtension(request.Url.AbsolutePath).ToLowerInvariant()) { // images case ".gif": response.ContentType = MediaTypeNames.Image.Gif; break; case ".jpg": case ".jpeg": response.ContentType = MediaTypeNames.Image.Jpeg; break; case ".tiff": response.ContentType = MediaTypeNames.Image.Tiff; break; case ".png": response.ContentType = "image/png"; break; // application case ".pdf": response.ContentType = MediaTypeNames.Application.Pdf; break; case ".zip": response.ContentType = MediaTypeNames.Application.Zip; break; // text case ".htm": case ".html": response.ContentType = MediaTypeNames.Text.Html; break; case ".txt": response.ContentType = MediaTypeNames.Text.Plain; break; case ".xml": response.ContentType = MediaTypeNames.Text.Xml; break; // let the user agent work it out default: response.ContentType = MediaTypeNames.Application.Octet; break; } using (var s = response.OutputStream) s.Write(entity, 0, entity.Length); } catch (Exception x) { Debug.WriteLine("Unexpected exception. Aborting...\n" + x); response.Abort(); } } } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485