关于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>
运行后跑起来了,目前还有些问题(样式异常、并且有些报错等后续处理了更新文章)
如果要隐藏控制台把输出类型更改为windows应用程序即可
如果需要NaitiveAOT发布可以进一步参考官方Sample
photino.Blazor/Samples/Photino.Blazor.NativeAOT at master · tryphotino/photino.Blazor · GitHub
参考资料: