【炫丽】从 0 开始做一个 WPF+Blazor 对话小程序
大家好,我是沙漠尽头的狼。
.NET 是免费,跨平台,开源,用于构建所有应用的开发人员平台。
本文演示如何在 WPF 中使用 Blazor 开发漂亮的 UI,为客户端开发注入新活力。
注 要使 WPF 支持 Blazor,.NET 版本必须是 6.0 或更高版本,本文所有示例使用的.NET 7.0,版本要求见链接,截图看如下文字:
.NET 版本要求
WPF 默认程序本文从创建 WPF Hello World 开发:
使用 WPF 模板创建一个默认程序,取名【WPFBlazorChat】,项目组织结构如下:
空白 WPF 项目
运行项目,一个空白窗口:
WPF 项目空白窗口
接着往下看,我们添加 Blazor 支持,本小节代码在这 WPF 默认程序源码。
添加 Blazor 支持依然使用上面的工程,添加 Blazor 支持,此部分参考微软文档生成 Windows Presentation Foundation (WPF) Blazor 应用,本小节快速略过。
2.1 编辑工程文件双击工程文件 WPFBlazorChat.csproj,修改处如下:
工程文件修改对比
在项目文件的顶部,将 SDK 更改为 Microsoft.NET.Sdk.Razor。添加节点<RootNameSpace>WPFBlazorChat</RootNameSpace>,将项目命名空间 WPFBlazorChat 设置为应用的根命名空间。添加 Nuget 包 Microsoft.AspNetCore.Components.WebView.Wpf,版本看你选择的.NET 版本而定。2.2 添加_Imports.razor 文件_Imports.razor 文件类似一个 Global using 文件,专门给 Razor 组件使用,放置一些用的比较多的全局的命名空间,精简代码。
内容如下,引入了一个命名空间 Microsoft.AspNetCore.Components.Web,这是 Razor 常用命名空间,包含用于向 Blazor 框架提供有关浏览器事件的信息的类型。:
@using Microsoft.AspNetCore.Components.Web2.3 添加 wwwroot\index.html 文件和 Vue、React 一样,需要一个 html 文件承载 Razor 组件,页面内容类似:
<!DOCTYPE html><html lang="en">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>WPFBlazorChat</title><base href="/" /><link href="css/app.css" rel="stylesheet" /><link href="WpfBlazor.styles.css" rel="stylesheet" /></head>
<body><div id="app">Loading...</div>
<div id="blazor-error-ui">An unhandled error has occurred.<a href="" class="reload">Reload</a><a class="dismiss">🗙</a></div><script src="_framework/blazor.webview.js"></script></body>
</html>app.css 文件在下面给出定义。看<div id="app">Loading...</div>,这里是承载 Razor 组件的地方,后面所有加载的 Razor 组件都是在这里渲染出来的。其他暂时不管。2.4 添加 wwwroot\css\app.css 文件页面的基本样式,通用的样式可放在这个文件:
html, body {font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;}
h1:focus {outline: none;}
a, .btn-link {color: #0071c1;}
.btn-primary {color: #fff;background-color: #1b6ec2;border-color: #1861ac;}
.valid.modified:not([type=checkbox]) {outline: 1px solid #26b050;}
.invalid {outline: 1px solid red;}
.validation-message {color: red;}
#blazor-error-ui {background: lightyellow;bottom: 0;box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);display: none;left: 0;padding: 0.6rem 1.25rem 0.7rem 1.25rem;position: fixed;width: 100%;z-index: 1000;}
#blazor-error-ui .dismiss {cursor: pointer;position: absolute;right: 0.75rem;top: 0.5rem;}2.5 添加一个 Razor 组件加一个 Razor 的经典组件 Counter.razor,Blazor 的 Hello World 程序就有这么一个组件,文件路径:/RazorViews/Counter.razor,之所以放 RazorViews 目录,是为了和 WPF 常用的 Views 目录区分,该组件内容如下:
<h1>Counter</h1>
<p>好开心,你点我了,现在是:<span style="color: red;">@currentCount</span></p>
<button class="btn btn-primary" @onclick="IncrementCount">快快点我</button>
@code {private int currentCount = 0;
}一个按钮【快快点我】,点击 @onclick="IncrementCount"使变量 currentCount 自增,同时页面显示此变量值,相信你能看懂。
2.6 Blazor 与 WPF 窗体关联这是两者产生关系的关键一步,打开窗体 MainWindow.xaml,修改如下:
窗体 Xaml 修改
如上代码,要点如下:
添加上面引入的 Nuget 包 Microsoft.AspNetCore.Components.WebView.Wpf 的命名空间,命名为 blazor,主要是要使用 BlazorWebView 组件;BlazorWebView 组件属性 HostPage 指定承载的 html 文件,Services 指定 razor 组件的 Ioc 容器,看下面 MainWindow()里标红的代码;RootComponent 的 Selector="#app"属性指示 Razor 组件渲染的位置,看 index.html 中 id 为 app 的 html 元素,ComponentType 指示需要在 #app 中渲染的 Razor 组件类型。打开 MainWindow.xaml.cs,修改如下:
注入 Ioc 容器
在 WPF 里可以使用 Prism 等框架提供的 Unity、DryIoc 等 Ioc 容器实现视图与服务的注入;Razor 组件这里,默认使用 ASP.NET Core 的 IServiceCollection 容器;如果 WPF 窗体与 Razor 组件需要共享数据,可以通过后面要说的 Messager 发送消息,也可以通过 Ioc 容器注入的方式实现,比如从 WPF 窗体中注入的数据(通过 MainWindow 构造函数注入),通过 IServiceCollection 容器再注入 Razor 组件使用,这里后面也有提到。
WPF 与 Razor 组件之间通过 Ioc 数据传输
上面步骤做完后,运行程序:
WPF 集成 Blazor 的默认程序
OK,WPF 与 Blazor 集成成功,打完收工?
等等,还没完呢,本小节源码在这 WPF 中添加 Blazor,接着往下看。
自定义窗体 WPF 默认窗体
看上图,窗体边框是 WPF 默认的样式,有时会感觉比较丑,或者不丑,设计师有其他的窗体风格设计,往往我们要自定义窗体,本节分享部分 WPF 与 Blazor 的自定义窗体实现,更多定制化功能可能需要您自行研究。
3.1 WPF 自定义窗体一般实现是设置窗体的三个属性 WindowStyle="None" AllowsTransparency="True" Background="Transparent",即可隐藏默认窗体的边框,然后在内容区自己画标题栏、最小化、最大化、关闭按钮、客户区等。
MainWindow.xaml:隐藏 WPF 默认窗体边框
<Windowx:Class="WPFBlazorChat.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:razorViews="clr-namespace:WPFBlazorChat.RazorViews"Title="MainWindow"Width="800"Height="450"AllowsTransparency="True"Background="Transparent"WindowStyle="None"mc:Ignorable="d"><Grid><blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">blazor:BlazorWebView.RootComponents<blazor:RootComponent ComponentType="{x:Type razorViews:Counter}" Selector="#app" /></blazor:BlazorWebView.RootComponents></Grid></Window>上面的代码只是隐藏了 WPF 默认窗体的边框,运行程序如下:
隐藏 WPF 默认窗体边框
看上图,点击窗体中的按钮(其实是 Razor 组件的按钮),但未执行按钮点击事件,且窗体消失了,这是怎么回事?您可以尝试研究下为什么,我没有研究个所以然来,暂时加个背景处理 BlazorWebView 穿透的问题。
简单的 WPF 自定义窗体样式
我们加上自定义窗体的基本样式看看:
带基本样式的 WPF 自定义窗体
MainWindow.xaml 代码如下:
<Windowx:Class="WPFBlazorChat.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:razorViews="clr-namespace:WPFBlazorChat.RazorViews"Title="MainWindow"Width="800"Height="450"AllowsTransparency="True" Background="Transparent" WindowStyle="None"mc:Ignorable="d"><Window.Resources><Style TargetType="{x:Type Button}"><Setter Property="Width" Value="35" /><Setter Property="Height" Value="25" /><Setter Property="Margin" Value="2" /><Setter Property="Background" Value="Transparent" /><Setter Property="BorderThickness" Value="0" /><Setter Property="Foreground" Value="White" /></Style></Window.Resources><Border Background="#7160E8" CornerRadius="5"><Grid><Grid.RowDefinitions><RowDefinition Height="35" /><RowDefinition Height="*" /></Grid.RowDefinitions><BorderBackground="#7160E8" CornerRadius="5 5 0 0" MouseLeftButtonDown="MoveWindow_MouseLeftButtonDown"><Grid><TextBlockMargin="10,10,5,5"Foreground="White"Text="这里是窗体标题栏,左侧可放 Logo、标题,右侧放窗体操作按钮:最小化、最大化、关闭等" /><StackPanel HorizontalAlignment="Right" Orientation="Horizontal"><Button Click="MinimizeWindow_Click" Content="―" /><Button Click="MaximizeWindow_Click" Content="口" /><Button Click="CloseWindow_Click" Content="X" /></StackPanel></Grid></Border><blazor:BlazorWebView Grid.Row="1" HostPage="wwwroot\index.html" Services="{DynamicResource services}">blazor:BlazorWebView.RootComponents<blazor:RootComponent ComponentType="{x:Type razorViews:Counter}" Selector="#app" /></blazor:BlazorWebView.RootComponents></blazor:BlazorWebView></Grid></Border></Window>我们给整个窗体客户端区域加了一个背景 Border(您可以去掉 Border 背景色,点击界面按钮试试),然后又套了一个 Grid,用于放置自定义的标题栏(标题和窗体控制按钮)和 BlazorWebView(用于渲染 Razor 组件的浏览器组件),下面是窗体控制按钮的响应事件:
using Microsoft.Extensions.DependencyInjection;using System.Windows;
namespace WPFBlazorChat;
public partial class MainWindow : Window{public MainWindow(){InitializeComponent();var serviceCollection = new ServiceCollection();serviceCollection.AddWpfBlazorWebView();Resources.Add("services", serviceCollection.BuildServiceProvider());}
}代码简单,处理了窗体最小化、窗体最大化(还原)、关闭、标题栏双击窗体最大化(还原),上面的实现不是一个完美的自定义窗体实现,至少有这两个问题:
当您尝试最大化后,窗体铺满了整个操作系统桌面(连任务栏区域也占用了);窗体任务栏两个圆角未生效(红色矩形框选的部分),即窗体下面的两个圆角,站长未找到让 BlazorWebView 出现圆角的属性或其他方法;标题栏区域(绿色矩形框选的部分)是 WPF 控件,所以圆角显示正常。窗体圆角
在后面的 3.4 小节,站长使用一个第三库实现了窗体圆角问题,更多比较好的 WPF 自定义窗体实现可看这篇文章:WPF 三种自定义窗体的实现,本小节中示例源码在这 WPF 自定义窗体。
3.2 WPF 异形窗体异形窗体的需求,使用 WPF 实现是比较方便的,本来打算写写的,感觉偏离主题太远了,给篇文章自行看看吧:WPF 异形窗体演示,文中异形窗体效果如下:
WPF 异形窗体
下面介绍将窗体的标题栏也放 Razor 组件中实现的方式。
3.3 Blazor 实现自定义窗体效果上面使用了 WPF 制作自定义窗体,有没有这种需求,把菜单放置到标题栏?这个简单,WPF 能很好实现。
如果放 Tab 类控件呢?Tab Header 是在标题栏显示,TabItem 是在客户端区域,Tab Header 与 TabItem 风格统一,在一套代码里面实现和维护也方便,那么在 WPF+Blazor 混合开发的情况怎么实现呢?相信通过本节 Razor 组件实现标题栏的介绍,你能做出来。
MainWindow.xaml 恢复代码,只设置隐藏 WPF 默认窗体边框,并给 BlazorWebView 套一层背景:
WPF 透明窗体
后面的代码有参考 BlazorDesktopWPF-CustomTitleBar 开源项目实现。
我们把标题栏做到 Counter.razor 组件,即标题栏、客户区放一个组件里,当然你也可以分离,这里我们方便演示:
Counter.razor
@using WPFBlazorChat.Services
<div class="titlebar" @ondblclick="WindowService.Maximize" @onmouseup="WindowService.StopMove" @onmousedown="WindowService.StartMove"><button class="titlebar-btn" onclick="alert('js alert: navigation pressed');"><img src="svg/navigation.svg" /></button><div class="window-title">测试窗体标题</div><div style="flex-grow:1"></div><button class="titlebar-btn" onclick="alert('js alert: settings pressed');"><img src="svg/settings.svg" /></button><button class="titlebar-btn" @onclick="WindowService.Minimize"><img src="svg/minimize.svg" /></button><button class="titlebar-btn" @onclick="WindowService.Maximize">@if (WindowService.IsMaximized()){<img src="svg/restore.svg" />}else{<img src="svg/maximize.svg" />}</button><button class="titlebar-cbtn" @onclick="()=>WindowService.Close(false)"><img src="svg/dismiss.svg" /></button></div>
<p>好开心,你点我了,现在是:<span style="color: red;">@currentCount</span></p>
<button class="btn btn-primary" @onclick="IncrementCount">快快点我</button>
@code {private int currentCount = 0;
}下面给出代码简单说明:
第一个 div 充做窗体的标题栏区域,注册了双击事件调用窗体最大化(还原)方法、鼠标按下与释放调用窗体的移动开始与结束方法;在第一个 div 里,其中有 3 个按钮,即窗体的控制按钮,调用窗体最小化、最大化(还原)、关闭方法调用;另有两个按钮,演示单击调用 JavaScript 的 alert 方法弹出消息。WPF 透明窗体
运行效果如下:
WPF 透明窗体
实现这个效果,还有一些代码:
上面的代码调用了一些方法实现窗体操作最小化、关闭等,代码如下;因为是 Razor 组件,即 html 实现的界面,界面的 html 元素也定义了一些 css 样式,代码也一并给出。标题栏的按钮使用了一些 svg 图片,在仓库里,可自行获取。窗体拖动
首先添加 Nuget 包 Simplify.Windows.Forms,用于获取鼠标光标的位置:
<PackageReference Include="Simplify.Windows.Forms" Version="1.1.2" />添加窗体帮助类:Services\WindowService.cs
using System;using System.Linq;using System.Windows;using System.Windows.Forms;using System.Windows.Threading;using Application = System.Windows.Application;
namespace WPFBlazorChat.Services;
public class WindowService{private static bool _isMoving;private static double _startMouseX;private static double _startMouseY;private static double _startWindLeft;private static double _startWindTop;
}上面的代码用于窗体的最小化、最大化(还原)、关闭等实现,需要在 Razor 组件里正确的调用这些方法:
Counter.razor 组件的 OnInitialized 初始化生命周期方法里调用 WindowService.Init();,如上代码,这个方法开启定时器,定时调用 UpdateWindowPos 方法检查鼠标是否按下,如果按下,检查间隔内窗体的位置变化范围,然后修改窗体位置,从而实现窗体位置移动(移动窗体无法使用 WPF 的 DragMove 方法,您可以尝试使用看看它报什么错),移动窗体有更好的方法欢迎留言。
Razor 组件里窗体控制按钮的使用看上面的代码不难理解,不过多解释。
上面效果的样式文件修改如下,wwwroot\css\app.css:
/*BlazorDesktopWPF-CustomTitleBar - © Copyright 2021 - Jam-Es.comLicensed under the MIT License (MIT). See LICENSE in the repo root for license information.*/
html, body {font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;padding: 0;margin: 0;}
.valid.modified:not([type=checkbox]) {outline: 1px solid #26b050;}
.invalid {outline: 1px solid red;}
.validation-message {color: red;}
#blazor-error-ui {background: lightyellow;bottom: 0;box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);display: none;left: 0;padding: 0.6rem 1.25rem 0.7rem 1.25rem;position: fixed;width: 100%;z-index: 1000;}
#blazor-error-ui .dismiss {cursor: pointer;position: absolute;right: 0.75rem;top: 0.5rem;}
.page-container {display: flex;flex-direction: column;height: 100vh;}
.content-container {padding: 0px 20px 20px 20px;flex-grow: 1;overflow-y: scroll;}
.titlebar {width: 100%;height: 32px;min-height: 32px;background-color: #7160E8;display: flex;flex-direction: row;}
.titlebar-btn, .titlebar-cbtn {width: 46px;background-color: #7160E8;color: white;border: none;border-radius: 0;}
.titlebar-btn:hover {background-color: #5A5A5A;}
.titlebar-btn:focus, .titlebar-cbtn:focus {outline: 0;}
.titlebar-cbtn:hover {background-color: #E81123;}
.window-title {display: flex;flex-direction: column;justify-content: center;margin-left: 5px;color: white;}上面的一些代码即实现了由 Razor 组件实现窗体的标题显示、窗体的最小化、最大化(还原)、关闭、移动等操作,然而还是会有 3.1 结尾出现的问题,即窗体圆角和窗体最大化铺满操作系统桌面任务栏的问题,下面一小节我们尝试解决他。
小节总结:通过上面的代码,如果放 Tab 控件铺满整个窗体,是不是有思路了?
本小节源码在这 Razor 组件实现窗体标题栏功能
3.4 Blazor 与 WPF 比较完美的实现效果其实上面的代码可以当做学习,即使有不小瑕疵(哈哈),本小节我们还是使用第三包解决窗体圆角和最大化问题。
首先添加 Nuget 包 ModernWpfUI,该 WPF 控件库本站介绍链接开源 WPF 控件库:ModernWpf:
<PackageReference Include="ModernWpfUI" Version="0.9.7-preview.2" />然后打开 App.xaml,引用上面开源 WPF 控件的样式:
<Application x:Class="WPFBlazorChat.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:ui="http://schemas.modernwpf.com/2019"StartupUri="MainWindow.xaml"><Application.Resources><ResourceDictionary><ResourceDictionary.MergedDictionaries><ui:ThemeResources /><ui:XamlControlsResources /></ResourceDictionary.MergedDictionaries></ResourceDictionary></Application.Resources></Application>最后打开 MainWindow.xaml,修改如下(主要是引入的几个属性 ui:xxxxx):
<Window x:Class="WPFBlazorChat.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:ui="http://schemas.modernwpf.com/2019"xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"xmlns:razorViews="clr-namespace:WPFBlazorChat.RazorViews"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"ui:TitleBar.ExtendViewIntoTitleBar="True"ui:TitleBar.IsBackButtonVisible="False"ui:TitleBar.Style="{DynamicResource AppTitleBarStyle}"ui:WindowHelper.UseModernWindowStyle="True"><Border Background="#7160E8" CornerRadius="5"><blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">blazor:BlazorWebView.RootComponents<blazor:RootComponent Selector="#app" ComponentType="{x:Type razorViews:Counter}" /></blazor:BlazorWebView.RootComponents></blazor:BlazorWebView></Border></Window>就上面三处修改,我们运行看看:
WPF 与 Blazor 自定义窗体比较完美的解决
是不是和 3.3 效果一样?其实仔细看,窗体下面的圆角也有了:
窗体圆角
最终还是 WPF 解决了所有问题...
我笑了
具体怎么实现的窗体最大化未占操作系统的任务栏,以及窗体圆角问题的解决(竟然能让 BlazorWebView 部分透明了)可以查看该组件相关代码,本文不过多深究。
另外,WPF 熟手可能比较清楚,前面的代码还不能正常的拖动改变窗体大小(不知道你发现没,我当你没发现。),使用该库后也解决了:
窗体手动改变大小
本小节源码在这解决圆角和最大化问题,下面开始本文的下半部分了,好累,终于到这了。
我累了
添加第三方 Blazor 组件工欲善其事,必先利其器!
鉴于大部分同学前端基础可能不是太好,即使使用 Blazor 可以少用或者不用 JavaScript,但有那么一款漂亮、便捷的 Blazor 组件库,这不是如虎添翼吗?本文使用 Masa Blazor 做示例展示,如今 Blazor 组件库众多,选择自己喜欢的、顺手的就成:
Masa 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>节点添加如下资源:
<link href="_content/Masa.Blazor/css/masa-blazor.min.css" rel="stylesheet" />
<link href="https://cdn.masastack.com/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet"><link href="https://cdn.masastack.com/npm/materialicons/materialicons.css" rel="stylesheet"><link href="https://cdn.masastack.com/npm/fontawesome/v5.0.13/css/all.css" rel="stylesheet">
<script src="_content/BlazorComponent/js/blazor-component.js"></script>完整代码如下:
<!DOCTYPE html><html lang="en">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>WPFBlazorChat</title><base href="/" /><link href="css/app.css" rel="stylesheet" /><link href="WpfBlazor.styles.css" rel="stylesheet" />
</head>
<body><div id="app">Loading...</div>
<div id="blazor-error-ui">An unhandled error has occurred.<a href="" class="reload">Reload</a><a class="dismiss">🗙</a></div><script src="_framework/blazor.webview.js"></script></body>
</html>4.3 引入 Masa.Blazor 命名空间打开_Imports.razor 文件,修改如下:
@using Microsoft.AspNetCore.Components.Web@using Masa.Blazor@using BlazorComponent4.4 Razor 组件添加 Masa.Blazor 打开 MainWindow.xaml.cs,添加一行代码 serviceCollection.AddMasaBlazor();
Ioc 中添加 Masa Blazor
4.5 尝试 Masa.Blazor 案例上面 4 步的准备工作做好后,我们简单来使用下 Masa.Blazor 组件。
打开 Tab 组件链接:https://blazor.masastack.com/components/tabs,尝试这个 Demo:
Masa Blazor 的 Tab 组件案例
Demo 的代码我几乎不变的引入,打开 RazorViews\Counter.razor 文件,保留 3.4 节的标题栏,替换了客户区域内容,代码如下:
@using WPFBlazorChat.Services
</MApp>
@code {
}运行效果如下:
Masa Blazor 的 Tab 组件案例集成
是不是有那味儿了?再尝试把 Tab 移到标题栏,前面有提过的效果:
Tab 放标题栏
上面的效果,代码修改如下,删除了原标题栏代码,将窗体操作按钮放到了 MToolbar 里面,并使用 MToolbar 添加了双击事件、鼠标按下、释放事件实现窗体拖动:
<MApp>
</MApp>窗体操作按钮的背景色也做部分修改:
样式部分修改
其实上面的窗体效果还是有点瑕疵,注意到窗体右侧的竖直滚动条了吗?在没引入 Masa.Blazor 之前,右侧正常显示,引入后多了一个竖直滚动条:
引入 Masa.Blazor 后多了竖直滚动条
这个想去掉也简单,在 wwwroot\css\app.css 追加样式(当时也是折腾了好一会儿,最后在 Masa.Blazor 群里群友给出了解决方案,十分感谢):
问题解决过程
问题解决 css 代码:
::-webkit-scrollbar {width: 0px;}因为 Razor 组件是在 BlazorWebView 里渲染的,即 BlazorWebView 就是个小型的浏览器呀,上面的样式即把浏览器的滚动条宽度设置为 0,它不就没有了吗?现在效果如下,是不是舒服了?
根据后界面
添加 Masa.Blazor 就介绍到这里,本小节示例代码在这里 WPF 中使用 Masa.Blazor,下面讲解 WPF 与 Blazor 混合开发后多窗体消息通知问题。
多窗体消息通知一般 C/S 窗体之间通信使用委托、事件,而在 WPF 开发中,可以使用一些框架提供的抽象事件订阅\发布组件,比如 Prism 的事件聚集器 IEventAggregator,或 MvvmLight 的 Messager。在 B/S 开发中,进程内事件通知可能就使用 MediatR 组件居多了,不论是在 C/S 还是 B/S 开发,这些组件在一定程度上,各大程序模板可以通用的,更不用说分布式的消息队列 RabbitMQ 和 Kafka 是万能的进程间通信标准选择了。
上面是一些套话,站长根据 Prism 的事件聚集器和 MvvmLight 的 Messager 源码阅读,简单封装了一个 Messager,可以适用于一般的业务需求。
5.1 Messager 封装本来不想贴代码直接给源码链接的,想想代码也不多,直接上吧。
Message
消息抽象类,用于定义消息类型,具体的消息需要继承该类,比如后面的打开子窗体消息 OpenSecondViewMessage。
using System;
namespace WPFBlazorChat.Messages;
public abstract class Message{protected Message(object sender){this.Sender = sender ?? throw new ArgumentNullException(nameof(sender));}
}IMessenger
消息接口,只定义了三个接口:
Subscribe:消息订阅 Unsubscribe:取消消息订阅 Publish:消息发送 using System;
namespace WPFBlazorChat.Messages;
public interface IMessenger{void Subscribe<TMessage>(object recipient, Action<TMessage> action,ThreadOption threadOption = ThreadOption.PublisherThread) where TMessage : Message;
}
public enum ThreadOption{PublisherThread,BackgroundThread,UiThread}Messenger
消息的管理,消息中转等实现:
using System;using System.Collections.Generic;using System.Linq;using System.Threading;using System.Threading.Tasks;
namespace WPFBlazorChat.Messages;
public class Messenger : IMessenger{public static readonly Messenger Default = new Messenger();private readonly object registerLock = new object();
}
public class WeakActionAndToken{public object? Recipient { get; set; }
}有兴趣的看上面的代码,封装代码上面简单全部给上,后面的消息通知都是基于上面的三个类实现的,比较核心。
5.2 代码整理第 5 节涉及到多窗体及多 Razor 组件了,需要创建一些目录存放这些文件,方便分类管理。
整理后代码
A:放 Message,即一些消息通知类;
B:放 Razor 组件,如果需要与 Maui\Blazor Server(Wasm)等共享 Razor 组件,可以创建 Razor 类库存储;
C:放通用服务,这里只放了一个窗体管理静态类,实际情况可以放 Redis 服务、RabbitMQ 消息服务等;
D:放 WPF 视图,本示例 WPF 窗体只是一个壳,承载 BlazorWebView 使用;
5.3 示例及代码说明先看本示例效果,再给出相关代码说明:
消息通知示例
图中有三个操作:
点击主窗体 A 的【+】按钮,发送了 OpenSecondViewMessage 消息,打开子窗体 B;打开子窗体 B 后,再点击主窗体 A 的【桃心】按钮,发送了 SendRandomDataMessage 消息,子窗体 B 的第二个 TabItem Header 显示了消息传来的数字;点击子窗体 B 的【安卓】图标按钮,给主窗体 A 响应了消息 ReceivedResponseMessage,主窗体收到后弹出一个对话框。三个消息类定义如下:
public class OpenSecondViewMessage : Message{public OpenSecondViewMessage(object sender) : base(sender){}}
public class SendRandomDataMessage : Message{public SendRandomDataMessage(object sender, int number) : base(sender){Number = number;}
}
public class ReceivedResponseMessage : Message{public ReceivedResponseMessage(object sender) : base(sender){}}除了 SendRandomDataMessage 传递了一个业务 Number 属性,另两个消息只是起到通知作用(所以没有额外属性定义),实际开发时可能需要传递业务数据。
5.3.1 打开多窗体即上面的第一个操作:点击主窗体 A 的【+】按钮,发送了 OpenSecondViewMessage 消息,打开子窗体 B。
在 RazorViews\MainView.razor 中执行按钮点击,发送打开子窗体消息:
...<MCol><MButton class="mx-2" Fab Dark Color="indigo" OnClick="OpenNewSecondView"><MIcon>mdi-plus</MIcon></MButton></MCol>...
@code{...void OpenNewSecondView(){Messenger.Default.Publish(this, new OpenSecondViewMessage(this));}...}在 App.xaml.cs 里订阅打开子窗体消息:
public partial class App : Application{public App(){// 订阅打开子窗口消息,在主窗口点击【+】按钮 Messenger.Default.Subscribe<OpenSecondViewMessage>(this, msg =>{var chatWin = new SecondWindowView();chatWin.Show();}, ThreadOption.UiThread);}}实际开发可能情况更复杂,发送的消息 OpenSecondViewMessage 里带 WPF 窗体路由(定义的一套路径规则寻找窗体或 ViewModel),订阅的地方也可能不在主程序,在子模块的 Module 类里。
5.3.2 发送业务数据即第二个操作:打开子窗体 B 后,再点击主窗体 A 的【桃心】按钮,发送了 SendRandomDataMessage 消息,子窗体 B 的第二个 TabItem Header 显示了消息传来的数字。
在 RazorViews\MainView.razor 中执行按钮点击,发送业务消息(就当前时间的 Millisecond):...<MCol><MButton class="mx-2" Fab Small Dark Color="pink" OnClick="SendNumber"><MIcon>mdi-heart</MIcon></MButton></MCol>...
@code{...void SendNumber(){Messenger.Default.Publish(this, new SendRandomDataMessage(this, DateTime.Now.Millisecond));}...}在 RazorViews\SecondView.razor 的 OnInitialized()方法里订阅业务消息通知:@using WPFBlazorChat.Messages<MApp><MToolbar><MTabs BackgroundColor="primary" Grow Dark><MTab><MBadge Color="pink" Dot>Item One</MBadge></MTab><MTab><MBadge Color="green" Content="tagCount">Item Two</MBadge></MTab><MTab><MBadge Color="deep-purple accent-4" Icon="mi-masa">Item Three</MBadge></MTab></MTabs></MToolbar>
</MApp>
@code{private int tagCount = 6;
}注意看,上面收到消息时有两个方法要简单说一下,看 OnInitialized()里的代码:
InvokeAsync:将 Number 赋值给变量 tagCount 的代码是在 InvokeAsync 方法里执行的,这个和 WPF 里的 Dispatcher.Invoke 是一个意思,相当于接收数据是在子线程,而赋值这个操作会即时的绑定到<MBadge Color="green" Content="tagCount">上,就需要 UI 线程同步。StateHasChanged:相当于 WPF MVVM 里的 PropertyChanged 事件通知,通知 UI 这里有值变化了,请你刷新一下,我要看看最新值。上面的代码把子窗体消息回应也贴上了,即点击安卓图标按钮时发送了 ReceivedResponseMessage 消息,在主窗体 RazorViews\MainView.razor 里也订阅了这个消息,和上面的代码类似:
@code{...// 是否显示确认对话框 bool _showComfirmDialog;protected override void OnInitialized(){WindowService.Init();
...}在 OnInitialized()方法里订阅消息 ReceivedResponseMessage,收到后将变量_showComfirmDialog 置为 true,即上面对话框的属性 Visible 绑定的值,同理需要在 InvokeAsync()中处理数据接收,也需要调用 StateHasChanged 通知 UI 数据变化。
上面说了部分代码,可能讲的不太清楚,可以看本节示例源码:多窗体消息通知。
本文示例本来想写完整 Demo 说明的,发现上面把基本要点都拉了一遍,再粘贴一些重复代码有点没完没了了,有兴趣的拉源码 WPF 与 Blazor 混合开发 Demo 查看、运行,下面是项目代码结构:
Demo 代码结构
下面是最后的示例效果图,前面部分文章已经发过,再发一次,哈哈:
用户列表窗口
用户列表
打开子窗口
打开窗口
聊天窗口
聊天窗口
演示发送消息
Click Once 发布尝试上一篇文章链接:快速创建软件安装包-ClickOnce,本文示例 Click Once 安装页面:https://dotnet9.com/WPFBlazorChat
Q&A8.1 为啥要在 WPF 里使用 Blazor?吃饱了撑的?WPF 虽然相较 Winform 做出比较好看的 UI 相对容易一些,但比起 Blazor,或者直接说 html 开发界面,还是差了一点点,更何况 html 的资源更多一点,尝试一下为何不可?
8.2 WPF + Blazor 支持哪些操作系统最低支持 Windows 7 SP1 吧,有群友已经尝试在 Windows 7 正常运行成功,这是本文示例 Click Once 安装页面:https://dotnet9.com/WPFBlazorChat
8.3 Blazor 混合开发还支持哪些已有框架?Blazor 混合开发的话,除了 WPF,还有 MAUI(跨平台框架,支持平台包括 Windows\Mac\Linux\Android\iOS 等)、Winform(同 WPF,只能在 Windows 平台运行)等,建议阅读微软文档继续学习,本文只是个引子:
微软文档学习 Blazor
8.4 Blazor 组件库除了 Masa.Blazor 还有哪些?开源的 Blazor 组件有:Ant Design Blazor、Bootstrap Blazor、MudBlazor、Blazorise,以及微软自家的 FAST Blazor 等,当然还有不少开源的 Blazor 组件。
收费的 Blazor 组件:DevExpress、Telerik、Syncfusion 等
8.5 本文示例代码?文中各小节代码、最后的示例代码都给出了相应链接,您可返回查看。
版权声明: 本文为 InfoQ 作者【沙漠尽头的狼】的原创文章。
原文链接:【http://xie.infoq.cn/article/ba1ad0f93bd07d73b043d5585】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论