欢迎光临散文网 会员登陆 & 注册

【炫丽】从0开始做一个WPF+Blazor对话小程序

2022-11-08 01:09 作者:沙漠尽头的狼  | 我要投稿

从一个WPF Hello World程序开始,逐渐引入Blazor,做个免费能看的对话小程序耍耍。

大家好,我是沙漠尽头的狼。

.NET是免费,跨平台,开源,用于构建所有应用的开发人员平台。

本文演示如何在WPF中使用Blazor开发漂亮的UI,为客户端开发注入新活力。

要使WPF支持Blazor,.NET版本必须是 6.0 或更高版本,本文所有示例使用的.NET 7.0,版本要求见链接,截图看如下文字:

1. WPF默认程序

本文从创建WPF Hello World开发:

使用WPF模板创建一个默认程序,取名【WPFBlazorChat】,项目组织结构如下:

运行项目,一个空白窗口:

接着往下看,我们添加Blazor支持,本小节代码在这WPF默认程序源码。

2. 添加Blazor支持

依然使用上面的工程,添加Blazor支持,此部分参考微软文档生成 Windows Presentation Foundation (WPF) Blazor 应用,本小节快速略过。

2.1 编辑工程文件

双击工程文件WPFBlazorChat.csproj,修改处如下:

  1. 在项目文件的顶部,将 SDK 更改为 Microsoft.NET.Sdk.Razor

  2. 添加节点<RootNameSpace>WPFBlazorChat</RootNameSpace>,将项目命名空间 WPFBlazorChat 设置为应用的根命名空间。

  3. 添加NugetMicrosoft.AspNetCore.Components.WebView.Wpf,版本看你选择的.NET版本而定。

2.2 添加_Imports.razor文件

_Imports.razor文件类似一个Global using文件,专门给Razor组件使用,放置一些用的比较多的全局的命名空间,精简代码。

内容如下,引入了一个命名空间Microsoft.AspNetCore.Components.Web,这是Razor常用命名空间,包含用于向 Blazor 框架提供有关浏览器事件的信息的类型。:

@using Microsoft.AspNetCore.Components.Web

HTML

Copy

2.3 添加wwwroot\index.html文件

VueReact一样,需要一个html文件承载Razor组件,页面内容类似:

  1. app.css文件在下面给出定义。

  2. <div id="app">Loading...</div>,这里是承载Razor组件的地方,后面所有加载的Razor组件都是在这里渲染出来的。

  3. 其他暂时不管。

2.4 添加wwwroot\css\app.css文件

页面的基本样式,通用的样式可放在这个文件:

2.5 添加一个Razor组件

加一个Razor的经典组件Counter.razorBlazorHello World程序就有这么一个组件,文件路径:/RazorViews/Counter.razor,之所以放RazorViews目录,是为了和WPF常用的Views目录区分,该组件内容如下:

一个按钮【快快点我】,点击@onclick="IncrementCount"使变量currentCount自增,同时页面显示此变量值,相信你能看懂。

2.6 Blazor与WPF窗体关联

这是两者产生关系的关键一步,打开窗体MainWindow.xaml,修改如下:

如上代码,要点如下:

  1. 添加上面引入的NugetMicrosoft.AspNetCore.Components.WebView.Wpf的命名空间,命名为blazor,主要是要使用BlazorWebView组件;

  2. BlazorWebView组件属性HostPage指定承载的html文件,Services指定razor组件的Ioc容器,看下面MainWindow()里标红的代码;

  3. RootComponentSelector="#app"属性指示Razor组件渲染的位置,看index.html中id为app的html元素,ComponentType指示需要在#app中渲染的Razor组件类型。

打开MainWindow.xaml.cs,修改如下:

在WPF里可以使用Prism等框架提供的UnityDryIocIoc容器实现视图与服务的注入;Razor组件这里,默认使用ASP.NET CoreIServiceCollection容器;如果WPF窗体与Razor组件需要共享数据,可以通过后面要说的Messager发送消息,也可以通过Ioc容器注入的方式实现,比如从WPF窗体中注入的数据(通过MainWindow构造函数注入),通过IServiceCollection容器再注入Razor组件使用,这里后面也有提到。

上面步骤做完后,运行程序:

OK,WPFBlazor集成成功,打完收工?

等等,还没完呢,本小节源码在这WPF中添加Blazor,接着往下看。

3. 自定义窗体

看上图,窗体边框是WPF默认的样式,有时会感觉比较丑,或者不丑,设计师有其他的窗体风格设计,往往我们要自定义窗体,本节分享部分WPF与Blazor的自定义窗体实现,更多定制化功能可能需要您自行研究。

3.1 WPF自定义窗体

一般实现是设置窗体的三个属性WindowStyle="None" AllowsTransparency="True" Background="Transparent",即可隐藏默认窗体的边框,然后在内容区自己画标题栏、最小化、最大化、关闭按钮、客户区等。

MainWindow.xaml:隐藏WPF默认窗体边框

上面的代码只是隐藏了WPF默认窗体的边框,运行程序如下:

看上图,点击窗体中的按钮(其实是Razor组件的按钮),但未执行按钮点击事件,且窗体消失了,这是怎么回事?您可以尝试研究下为什么,我没有研究个所以然来,暂时加个背景处理BlazorWebView穿透的问题。

简单的WPF自定义窗体样式

我们加上自定义窗体的基本样式看看:

MainWindow.xaml代码如下:

我们给整个窗体客户端区域加了一个背景Border(您可以去掉Border背景色,点击界面按钮试试),然后又套了一个Grid,用于放置自定义的标题栏(标题和窗体控制按钮)和BlazorWebView(用于渲染Razor组件的浏览器组件),下面是窗体控制按钮的响应事件:

代码简单,处理了窗体最小化、窗体最大化(还原)、关闭、标题栏双击窗体最大化(还原),上面的实现不是一个完美的自定义窗体实现,至少有这两个问题:

  • 当您尝试最大化后,窗体铺满了整个操作系统桌面(也霸占了任务栏区域);

  • 窗体任务栏两个圆角未生效(标题栏区域是WPF控件,所以正常),即窗体下面的两个圆角,站长未找到让BlazorWebView出现圆角的属性或其他方法。

在后面的3.4小节,站长使用一个第三库实现了窗体圆角问题,更多比较好的WPF自定义窗体实现可看这篇文章:WPF三种自定义窗体的实现,本小节中示例源码在这WPF自定义窗体。

3.2 WPF异形窗体

异形窗体的需求,使用WPF实现是比较方便的,本来打算写写的,感觉偏离主题太远了,给篇文章自行看看吧:WPF异形窗体演示,文中异形窗体效果如下:

下面介绍将窗体的标题栏也放Razor组件中实现的方式。

3.3 Blazor实现自定义窗体效果

上面使用了WPF制作自定义窗体,有没有这种需求,把菜单放置到标题栏?这个简单,WPF能很好实现。

如果放Tab类控件呢?Tab Header是在标题栏显示,TabItem是在客户端区域,Tab Header与TabItem风格统一,在一套代码里面实现和维护也方便,那么在WPF+Blazor混合开发的情景怎么实现呢?相信通过本节Razor实现标题栏的介绍,你能做出来。

MainWindow.xaml恢复代码,只设置隐藏WPF默认窗体边框,并给BlazorWebView套一层背景:

后面的代码有参考[BlazorDesktopWPF-CustomTitleBar](https://github.com/James231/BlazorDesktopWPF-CustomTitleBar)实现。

我们把标题栏做到Counter.razor组件,即标题栏、客户区放一个组件里,当然你也可以分离,这里我们方便演示:

Counter.razor

下面给出代码简单说明:

  1. 第一个div充做窗体的标题栏区域,注册了双击事件调用窗体最大化(还原)方法、鼠标按下与释放调用窗体的移动开始与结束方法;

  2. 在第一个div里,其中有3个按钮,即窗体的控制按钮,调用窗体最小化、最大化(还原)、关闭方法调用;

  3. 另有两个按钮,演示单击调用JavaScriptalert方法弹出消息。

运行效果如下:

实现这个效果,还有一些代码:

  1. 上面的代码调用了一些方法实现窗体操作最小化、关闭等,代码如下;

  2. 因为是Razor组件,即html实现的界面,界面的html元素也定义了一些css样式,代码也一并给出。

  3. 标题栏的按钮使用了一些svg图片,在仓库里,可自行获取。

窗体拖动

首先添加NugetSimplify.Windows.Forms,用于获取鼠标光标的位置:

<PackageReference Include="Simplify.Windows.Forms" Version="1.1.2" />

添加窗体帮助类:Services\WindowService.cs

上面的代码用于窗体的最小化、最大化(还原)、关闭等实现,需要在Razor组件里正确的调用这些方法:

  1. Counter.razor组件的OnInitialized初始化生命周期方法里调用WindowService.Init();,如上代码,这个方法开启定时器,定时调用UpdateWindowPos方法检查鼠标是否按下,如果按下,检查间隔内窗体的位置变化,然后修改窗体位置,从而实现窗体位置移动(移动窗体无法使用WPF的DragMove方法,您可以尝试使用看看它报什么错)。

  2. Razor组件里窗体控制按钮的使用看上面的代码不难理解,不过多解释。

上面效果的样式文件修改如下,wwwroot\css\app.css

上面的一些代码即实现了由Razor组件实现窗体的标题显示、窗体的最小化、最大化(还原)、关闭、移动等操作,然而还是会有3.1结尾出现的问题,即窗体圆角和窗体最大化铺满操作系统桌面任务栏的问题,下面一小节我们尝试解决他。

小节总结:通过上面的代码,如果放Tab控件铺满整个窗体,是不是有思路了?

本小节源码在这Razor组件实现窗体标题栏功能

3.4 Blazor与WPF比较完美的实现效果

其实上面的代码可以当做学习,即使有不小瑕疵(哈哈),本小节我们还是使用第三包解决窗体圆角和最大化问题。

首先添加NugetModernWpfUI,该WPF控件库本站介绍链接开源WPF控件库:ModernWpf:

<PackageReference Include="ModernWpfUI" Version="0.9.7-preview.2" />

然后打开App.xaml,引用上面开源WPF控件的样式:

最后打开MainWindow.xaml,修改如下:

就上面三处修改,我们运行看看:

是不是和3.3效果一样?其实仔细看,窗体下面的圆角也有了:

最终还是WPF解决了所有问题

具体怎么实现的窗体最大化未占操作系统的任务栏,以及窗体圆角问题的解决(竟然能让BlazorWebView部分透明了)可以查看该组件相关代码,本文不过多深究。

另外,WPF熟手可能比较清楚,前面的代码不能正常的拖动改变窗体大小(不知道你发现没,我当你没发现。),使用该库后也解决了:

本小节源码在这解决圆角和最大化问题,下面开始本文的下半部分了,好累,终于到这了。

4. 添加第三方Blazor组件

工欲善其事,必先利其器!

鉴于大部分同学前端基础可能不是太好,即使使用Blazor可以少用或者不用JavaScript,那么有那么一款漂亮、便捷的Blazor组件库,这不是如虎添翼吗?本文使用Masa Blazor做示例显示,如今Blazor组件库众多,选择自己喜欢的、顺手的就成:

站长前些日子介绍过MAUI使用Masa blazor组件库,本小节思路也是类似,且看我表演。

打开Masa Blazor文档站点:https://blazor.masastack.com/getting-started/installation,一起来往WPF中引入这款Blazor组件库吧。

4.1 引入Masa.Blazor包

打开工程文件WPFBlazorChat.csproj直接复制下面的包版本,或通过NuGet包管理器搜索Masa.Blazor安装

<PackageReference Include="Masa.Blazor" Version="0.6.0" />

4.2 添加Masa.Blazor带来的资源

打开wwwroot\index.html,在<head></head>节点添加如下资源:

完整代码如下:

4.3 引入Masa.Blazor命名空间

打开_Imports.razor文件,修改如下:

4.4 Razor组件添加Masa.Blazor

打开MainWindow.xaml.cs,添加一行代码 serviceCollection.AddMasaBlazor();


4.5 尝试Masa.Blazor案例

上面4步的准备工作做好后,我们简单来使用下Masa.Blazor组件。

打开Tab组件链接:https://blazor.masastack.com/components/tabs,尝试这个Demo:

Demo的代码我几乎不变的引入,打开RazorViews\Counter.razor文件,保留3.4节的标题栏,替换了客户区域内容,代码如下:

运行效果如下:

是不是有那味儿了?再尝试把Tab移到标题栏,前面有提过的效果:

上面的效果,代码修改如下,删除了原标题栏代码,将窗体操作按钮放到了MToolbar里面,并使用MToolbar添加了双击事件、鼠标按下、释放事件实现窗体拖动:

窗体操作按钮的背景色也做部分修改:

其实上面的窗体效果还是有点瑕疵,注意到窗体右侧的竖直滚动条了吗?在没引入Masa.Blazor之前都是没有的:

这个想去掉也简单,在`wwwroot\css\app.css`追加样式(当时也是折腾了好一会儿,最后在`Masa.Blazor`群里群友给出了解决方案,十分感谢):

问题解决`css`代码:

因为Razor组件是在BlazorWebView里渲染的,即BlazorWebView就是个小型的浏览器呀,上面的样式即把浏览器的滚动条宽度设置为0,它不就没有了吗?现在效果如下,是不是舒服了?

添加Masa.Blazor就介绍到这里,本小节示例代码在这里WPF中使用Masa.Blazor,下面讲解WPF与Blazor混合开发后多窗体消息通知问题。

5. 多窗体消息通知

一般C/S窗体之间通信使用委托、事件,而在WPF开发中,可以使用一些框架提供的抽象事件订阅\发布组件,比如Prism的事件聚集器IEventAggregator,或MvvmLightMessager。在B/S开发中,进程内事件通知可能就使用MediatR组件居多了,不论是在C/S还是B/S开发,这些组件在一定程度上,各大程序模板可以通用的,更不用说分布式的消息队列RabbitMQ 和 Kafka是万能的进程间通信标准选择了。

上面是一些套话,站长根据Prism的事件聚集器和MvvmLight的Messager源码阅读,简单封装了一个Messager,可以适用于一般的业务需求。

5.1 Messager封装

本来不想贴代码直接给源码链接的,想想代码也不多,直接上吧。

Message

消息抽象类,用于定义消息类型,具体的消息需要继承该类,比如后面的打开子窗体消息OpenSecondViewMessage

IMessenger

消息接口,只定义了三个接口:

  1. Subscribe:消息订阅

  2. Unsubscribe:取消消息订阅

  3. Publish:消息发送

Messenger

消息的管理,消息中转等实现:

有兴趣的看上面的代码,封装代码上面简单全部给上。

5.2 代码整理

第 5 节涉及到多窗体及多Razor组件了,需要创建一些目录存放这些文件,方便分类管理。

  1. A:放Message,即一些消息通知类;

  2. B:放Razor组件,如果需要与Maui\Blazor Server(Wasm)等共享Razor组件,可以创建Razor类库存储;

  3. C:放通用服务,这里只放了一个窗体管理静态类,实际情况可以放Redis服务、RabbitMQ消息服务等;

  4. D:放WPF视图,本示例WPF窗体只是一个壳,承载BlazorWebView使用;

5.3 示例及代码说明

先看本示例效果,再给出相关代码说明:

图中有三个操作:

  1. 点击主窗体A的【+】按钮,发送了OpenSecondViewMessage消息,打开子窗体B;

  2. 打开子窗体B后,再点击主窗体A的【桃心】按钮,发送了SendRandomDataMessage消息,子窗体B的第二个TabItem Header显示了消息传来的数字;

  3. 点击子窗体B的【安卓】图标按钮,给主窗体A响应了消息ReceivedResponseMessage,主窗体收到后弹出一个对话框。

三个消息类定义如下:

除了SendRandomDataMessage传递了一个业务Number属性,另两个消息只是起到通知作用,实际开发时可能需要传递业务数据。

打开多窗体

即上面的第一个操作:点击主窗体A的【+】按钮,发送了OpenSecondViewMessage消息,打开子窗体B。

RazorViews\MainView.razor中执行按钮点击,发送打开子窗体消息:

App.xaml.cs里订阅打开子窗体消息:

实际开发可能情况更复杂,发送的消息OpenSecondViewMessage里带WPF窗体路由(定义的一套路径规则寻找窗体或ViewModel),订阅的地方也可能不在主程序,在子模块的Module类里。

发送业务数据

即第二个操作:打开子窗体B后,再点击主窗体A的【桃心】按钮,发送了SendRandomDataMessage消息,子窗体B的第二个TabItem Header显示了消息传来的数字。

  1. RazorViews\MainView.razor中执行按钮点击,发送业务消息(就当前时间的Millisecond):

  1. RazorViews\SecondView.razorOnInitialized()方法里订阅业务消息通知:

注意看,上面收到消息时有两个方法要简单说一下,看OnInitialized()里的代码:

  • InvokeAsync:将Number赋值给变量tagCount的代码是在InvokeAsync方法里执行的,这个和WPF里的Dispatcher.Invoke是一个意思,相当于接收数据是在子线程,而赋值这个操作会即时的绑定到<MBadge Color="green" Content="tagCount">上,也需要UI线程同步。

  • StateHasChanged:相当于MVVM里的PropertyChanged事件通知,通知UI这里有值变化了,请你刷新一下,我要看看最新值。

上面的代码把子窗体消息回应也贴上了,即点击安卓图标按钮时发送了ReceivedResponseMessage消息,在主窗体RazorViews\MainView.razor里也订阅了这个消息,和上面的代码类似:

OnInitialized()方法里订阅消息ReceivedResponseMessage,收到后将变化_showComfirmDialog置为true,即上面对话框的属性Visible绑定的值,同理需要在InvokeAsync()中处理数据接收,也需要调用StateHasChanged通知UI数据变化。

上面说了部分代码,可能讲的不太清楚,可以看示例源码:多窗体消息通知。

6. 本文示例

本来想写完整Demo的,发现上面把基本要点都拉了一遍,再粘贴一些重复代码有点没完没了了,有兴趣的拉源码WPF与Blazor混合开发Demo,下面是项目代码结构大概:

下面是最后的示例效果图,前面部分文章已经发过,再发一次,哈哈:

用户列表窗口


打开子窗口


聊天窗口


演示发送消息

 

7. Click Once发布尝试

上一篇文章链接:快速创建软件安装包-ClickOnce

8. Q&A

8.1 为啥要在WPF里使用Blazor?吃饱了撑的?

WPF虽然相较Winform做出比较好看的UI相对容易一些,但比起Blazor,或者直接说html开发界面,还是差了一点点,更何况html的资源更多一点,尝试一下为何不可?

8.2 WPF + Blazor支持哪些操作系统

最低支持Windows 7 SP1吧,有群友已经尝试正常运行成功,这是本文示例Click Once安装页面:https://dotnet9.com/WPFBlazorChat

8.3 Blazor 混合开发还支持哪些已有框架?

Blazor混合开发的话,除了WPF,还有MAUI(跨平台框架,支持平台包括Windows\Mac\Linux\Android\iOS等)、Winform(同WPF,只能在Windows平台运行)等,建议阅读微软文档学习:

8.4 Blazor组件除了Masa.Blazor还有哪些?

  • 开源的Blazor组件:Ant Design Blazor、Bootstrap Blazor、MudBlazor、Blazorise,以及微软自家的FAST Blazor等,当然还有不少开源的Blazor组件。

  • 收费的Blazor组件:DevExpress、Telerik、Syncfusion等

8.5 本文示例代码?

文中各小节代码、最后的示例代码都给出了相应链接。


【炫丽】从0开始做一个WPF+Blazor对话小程序的评论 (共 条)

分享到微博请遵守国家法律