.NET不够美,用webVIew2集成web页面组件
用.NET的Winform开发,发现界面居然很土很难看。和vue3下的element plus组件一比,简直没法看。想当初web开发刚出现的时候,界面也是又土又难看,没想到这么多年过去,桌面组件基本原地踏步,web组件确实如雨后春笋,越来越漂亮好看。
.Net也有一些专门美化过的组件,不过,基本都是收费的,而且美化后感觉依然不如开源的web组件。看来桌面组件这些年和web组件相比确实进步不大。web组件卷了十几年,其美观程度确实不可同日而语。所以桌面开发也出现了Electron这种怪胎。
引子
这一想法的缘起就是觉得桌面组件不好看。

别的不说,仅仅是一个button,就让看管了web组件button的人感觉有点low。这是element plus的button:

差距应该不是一点了。
虽然但是,还是尽量用.NET自己的组件比较好,搜了一下,大概有SunnyUI和hzhcontrols等几个开源项目可以用,但基本都是需要商用授权付费的,而且看上去也不是很好看的样子,遂起了嵌入web页面和web组件的念头,又好看又免费,正好还可以练习一下怎么在.NET中嵌入web页面。
试用结果
.NET中嵌入web页面也有很多种方法,包括:
- WebBrowser
- WebView
- WebView2
- Microsoft Edge WebView2*【强烈推荐】*
- CefSharp
- DotNetBrowser
- 。。。
类似的还有好多。不过,尝试后发现很多都已经过时了,有的还是IE时代的产品。繁琐的试用对比后,发现还是WebView2比较好用。
WebView2 项目得天独厚,有微软操作系统win10以及win11的加持,最起码,生成的项目文件是很小的,我这边是3.6M,相对于CefSharp项目动辄100M的大小来讲,大大降低分发的大小,所以还是值得深入研究一下的。
开发需要的条件
- 运行时
- WebView2 - Microsoft Edge Developer:https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section

通过控制面板,我们也能看到已经安装了这个运行时了。
没有的话,需要上边的那个地址下载安装。
具体地址: https://go.microsoft.com/fwlink/p/?LinkId=2124703
- Nuget包
需要引入以下Nuget包
Microsoft.Web.WebView2
复制
安装好之后,我这里默认是使用的WinFrom UI框架。
- 案例参考: https://github.com/MicrosoftEdge/WebView2Samples
新建项目 (winfrom 作为参考)

如果没有出现WebView2可以重启一下项目就会有了

同时,为了方便看,我们把Dock属性选为Fill 全填充就好了
这个时候,我们添加一下的基础环境代码,就可以让页面启动了。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Resize += new EventHandler(Form_Resize);
webView21.CoreWebView2InitializationCompleted += WebView21_CoreWebView2InitializationCompleted;
Initialize();
}
/// <summary>
/// 实现自适应页面缩放
/// </summary>
private void Form_Resize(object sender, EventArgs e)
{
webView21.Size = ClientSize - new Size(webView21.Location);
}
/// <summary>
/// webview 加载完毕
/// </summary>
private void WebView21_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
webView21.CoreWebView2.Navigate("https://www.baidu.com/");
}
/// <summary>
/// WebView2初始化
/// </summary>
async void Initialize()
{
var result = await CoreWebView2Environment.CreateAsync(null, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache"), null);
await webView21.EnsureCoreWebView2Async(result);
}
}
复制

这个页面可以自由的拉伸,十分的方便。
Web组件调用C#代码
最后还有一个大问题,就是web组件和C#代码的相互调用问题。毕竟web页面里面运行的是JS,winform的程序是C#,在我的例子里,需要点击web按钮组件的时候调用C#的程序。具体做法如下:
-
用vite+vue3+element plus, 新建一个web项目
package.json
{ "name": "vitetest", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vue-tsc && vite build", "preview": "vite preview" }, "dependencies": { "element-plus": "^2.4.2", "vue": "^3.3.8" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.0", "typescript": "^5.2.2", "unplugin-auto-import": "^0.16.7", "unplugin-vue-components": "^0.25.2", "vite": "^5.0.0", "vue-tsc": "^1.8.22" } } -
App.vue中加入三个按钮, 编译运行
App.vue
<script> import { defineComponent } from 'vue' import { ElButton } from 'element-plus' export default defineComponent({ components: { ElButton, //ElConfigProvider }, setup() { const clickme = async () => { alert("aaaa") window.chrome.webview.hostObjects.webBrowserObj.ClickMe1(); // alert("bbb") // var hostObject = window.chrome.webview.hostObjects.customWebView2HostObject; // var result = await hostObject.TestCallFromJS(); // alert(result) }; const clickme2 = async () => { alert("bbbb") window.chrome.webview.hostObjects.webBrowserObj.ClickMe2(); // alert("bbb") // var hostObject = window.chrome.webview.hostObjects.customWebView2HostObject; // var result = await hostObject.TestCallFromJS(); // alert(result) }; const clickme3 = async () => { alert("cccc") window.chrome.webview.hostObjects.webBrowserObj.ClickMe3(); // alert("bbb") // var hostObject = window.chrome.webview.hostObjects.customWebView2HostObject; // var result = await hostObject.TestCallFromJS(); // alert(result) }; return { clickme, clickme2, clickme3, }; // return { // zIndex: 3000, // size: 'large', // } }, }) </script> <template> <el-button type="primary" @click="clickme()" class="btn">MD5单磁盘扫描</el-button> <el-button type="success" @click="clickme2()" class="btn">单磁盘重复排查</el-button> <el-button type="danger" @click="clickme3()" class="btn">去扫描另一台新机器</el-button> </template> <style scoped> body { margin: 0; display: flex; place-items: center; min-width: 320px; } #app { max-width: 1280px; margin: 0 auto; padding: 0rem; padding-top: 0rem; padding-right: 2rem; padding-bottom: 0rem; padding-left: 2rem; text-align: center; } .btn{ height: 50px; font-size:medium; margin-left: 20px; margin-right: 20px; } </style>
最后,运行pnpm run build进行打包。
将打包好的dist目录拷贝到.net项目的根目录下。
-
visual studio中打开.net项目,在form中加入webView2组件

在form的构造函数中加入以下部分:
public FormMain() { InitializeComponent(); InitializeAsync(); /* string filePath = "app48.ico"; // 替换为你的.ico文件路径 try { formIconBytes = File.ReadAllBytes(filePath); } catch (IOException e) { } this.Icon = new Icon(new MemoryStream(formIconBytes)); */ } async void InitializeAsync() { await webView21.EnsureCoreWebView2Async(null); } private void webView21_CoreWebView2InitializationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e) { if ((webView21 == null) || (webView21.CoreWebView2 == null)) Console.WriteLine("not ready"); //webView21.NavigateToString(File.ReadAllText("index.html")); else { webView21.CoreWebView2.AddHostObjectToScript("webBrowserObj", new ScriptCallbackObject()); //虚拟映射 webView21.CoreWebView2.SetVirtualHostNameToFolderMapping("html.sam", "./dist", CoreWebView2HostResourceAccessKind.Allow); //导航 webView21.CoreWebView2.Navigate("https://html.sam/index.html"); webView21.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("var webBrowserObj= window.chrome.webview.hostObjects.webBrowserObj;"); //MessageBox.Show("done"); } //webView21.NavigateToString(File.ReadAllText("index.html")); }同时新增类ScriptCallbackObject.cs
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using WinFormsApp1; namespace FindDup4Disk { [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] /// <summary> /// 网页调用C#方法 /// </summary> public class ScriptCallbackObject { public string UserName { get; set; } = "我是C#属性"; public void ClickMe1() { FormMain currentForm = (FormMain)Form.ActiveForm; // 使用currentForm进行操作 currentForm.button1_Click(null,null); } public void ClickMe2() { FormMain currentForm = (FormMain)Form.ActiveForm; // 使用currentForm进行操作 currentForm.button5_Click(null, null); } public void ClickMe3() { FormMain currentForm = (FormMain)Form.ActiveForm; // 使用currentForm进行操作 currentForm.button2_Click(null, null); } public void ShowMessageArg(string arg) { MessageBox.Show("【网页调用C#】:" + arg); } public string GetData(string arg) { return "【网页调用C#获取数据】;" + arg; } [System.Runtime.CompilerServices.IndexerName("Items")] public string this[int index] { get { return m_dictionary[index]; } set { m_dictionary[index] = value; } } private Dictionary<int, string> m_dictionary = new Dictionary<int, string>(); } } -
编译运行.net程序,成功调用

结论
虽然有点小麻烦,但总算是成功的。虽然没有尝试用C#调用JS,不过应该也是可以的。
最后,实验的成果就是这个.NET6 Runtime的小工具,免费开源:
https://github.com/shenyaojun/FindDup4Disk
https://gitee.com/shenyao/find-dup4-disk