使用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

参考资料:

tryphotino/photino.Blazor (github.com)

ant-design-blazor/ant-design-blazor: 🌈A set of enterprise-class UI components based on Ant Design and Blazor WebAssembly. (github.com)

ant-design-blazor/ant-design-pro-blazor: 👨🏻‍💻👩🏻‍💻 An out-of-box UI solution for enterprise applications as a Blazor boilerplate. (github.com)

posted @ 2023-05-13 10:21  何嘉俊  阅读(1968)  评论(3编辑  收藏  举报