使用Photino.NET开发跨平台桌面应用
关于Hybrid桌面开发的方式其实有不少,偶然间看到Photino
相较于Winform+Webview2的方案 Photino可以跨平台,并且支持NaitiveAOT
相较于其他CEF方案(如electron,cet.net等)体积上小了很多

既然是Hybrid,那就支持主流的Web前端框架,这里以Blazor为例(官方Demo很全,除了Blazor还包含Vue,React,Anglar)
参考官方Demo的项目结构:tryphotino/photino.Blazor (github.com)
新建控制台应用:

安装nuget包 Photino.Blazor

编辑项目文件将项目SDK更改为 Microsoft.NET.Sdk.Razor
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Photino.Blazor" Version="2.6.0" />
</ItemGroup>
</Project>
更改program.cs
using Microsoft.Extensions.DependencyInjection;
using Photino.Blazor;
namespace PhotinoBlazor
{
internal class Program
{
[STAThread]
static void Main(string[] args)
{
var appBuilder = PhotinoBlazorAppBuilder.CreateDefault(args);
appBuilder.Services
.AddLogging();
// register root component and selector
appBuilder.RootComponents.Add<App>("app");
var app = appBuilder.Build();
// customize window
app.MainWindow
//需要有favicon.ico
//.SetIconFile("favicon.ico")
.SetTitle("Photino Blazor Sample");
AppDomain.CurrentDomain.UnhandledException += (sender, error) =>
{
app.MainWindow.ShowMessage("Fatal exception", error.ExceptionObject.ToString());
};
app.Run();
}
}
}
这里我们直接使用ant-design pro
Nutget安装 AntDesignPro所需要的包



Main中AddAntDeisgn
appBuilder.Services
.AddLogging()
.AddAntDesign();
安装项目模板:
>dotnet new -i AntDesign.Templates
新建Ant-Design Pro项目
>dotnet new antdesign --host wasm --full
将AntDeisignPro中的内容完整复制到Photino的项目中

最终项目结构如下:

处理几处报错(PhotinoBlazor中不需要WebAssembly引用)

_imports.razor
@using AntDesign @using AntDesign.Charts @using AntDesign.ProLayout @using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @using AntBlazorPro @using AntBlazorPro.Models @using AntBlazorPro.Services
App.razor
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(AntBlazorPro.BasicLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(AntBlazorPro.BasicLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
<AntContainer />
在Main中注入AntDesignPro示例中用到的Service
appBuilder.Services.AddScoped<IChartService, ChartService>();
appBuilder.Services.AddScoped<IProjectService, ProjectService>();
appBuilder.Services.AddScoped<IUserService, UserService>();
appBuilder.Services.AddScoped<IAccountService, AccountService>();
appBuilder.Services.AddScoped<IProfileService, ProfileService>();
修改index.html文件:
将
<div id="app">
更改为
<app id="app">
<script src="_framework/blazor.webassembly.js"></script>
更改为
<script src="_framework/blazor.webview.js"></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="keywords" content="antd,umi,umijs,ant design,脚手架,布局, Ant Design,项目,Pro,admin,控制台,主页,开箱即用,中后台,解决方案,组件库" />
<meta name="description" content="An out-of-box UI solution for enterprise applications as a React boilerplate." />
<meta name="description" content="开箱即用的中台前端/设计解决方案。" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>Ant Design Pro Blazor</title>
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<base href="/" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="_content/AntDesign.ProLayout/css/ant-design-pro-layout-blazor.css" rel="stylesheet" />
<link href="./css/site.css" rel="stylesheet" />
<link href="AntBlazorPro.styles.css" rel="stylesheet" />
</head>
<body>
<noscript>Out-of-the-box mid-stage front/design solution!</noscript>
<div id="app">
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
#app {
background-repeat: no-repeat;
background-size: 100% auto;
}
.page-loading-warp {
padding: 98px;
display: flex;
justify-content: center;
align-items: center;
}
.ant-spin {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
list-style: none;
-webkit-font-feature-settings: 'tnum';
font-feature-settings: 'tnum';
position: absolute;
display: none;
color: #1890ff;
text-align: center;
vertical-align: middle;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86), -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-dot {
position: relative;
display: inline-block;
font-size: 20px;
width: 20px;
height: 20px;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
-ms-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antSpinMove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antRotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-lg .ant-spin-dot {
font-size: 32px;
width: 32px;
height: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
</style>
<div style="
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 420px;
height: 100%;
">
<img src="./pro_icon.svg" alt="logo" width="256" />
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i>
</span>
</div>
</div>
<div style="display: flex; justify-content: center; align-items: center;">
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" width="32"
style="margin-right: 8px;" />
Ant Design Blazor <div class="loading-progress-text"></div>
</div>
</div>
</div>
<script type="text/javascript" src="https://unpkg.com/@antv/g2plot@2.4.17/dist/g2plot.min.js"></script>
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
<script src="_content/AntDesign.Charts/ant-design-charts-blazor.js"></script>
<script src="_framework/blazor.webview.js"></script>
</body>
</html>
编辑项目文件,添加
<ItemGroup>
<Content Update="wwwroot\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
运行后跑起来了,还有些问题(样式异常、并且有些报错)

样式问题直接将index.html中
<link href="AntBlazorPro.styles.css" rel="stylesheet" />
更改为程序集名称即可
<link href="PhotinoBlazor.styles.css" rel="stylesheet" />
报错可以找到SalesCard.Razor.cs 加上空值判断

private async Task OnTabChanged(string activeKey) { var data = await ChartService.GetSalesDataAsync(); if (activeKey == "1") if(_saleChart is not null) await _saleChart.ChangeData(data); else if (_visitChart is not null) await _visitChart.ChangeData(data); }
成功运行

如果要隐藏控制台把输出类型更改为windows应用程序即可
该源码已在github上发布 hejiajun107/PhotinoAntDesignBlazor: Photino Blazor sample with AntDesignPro (github.com)
如果需要NaitiveAOT发布可以进一步参考官方Sample
photino.Blazor/Samples/Photino.Blazor.NativeAOT at master · tryphotino/photino.Blazor · GitHub
参考资料:

浙公网安备 33010602011771号