使用 Electron.NET 构建跨平台桌面应用





0/5 (0投票)
本文提供了使用 Electron.NET 开发和部署应用程序的逐步指南。
最近,我很荣幸在 NDC 奥斯陆在线活动中介绍了这个主题。这是我的博客形式的演讲,文章末尾附有源代码链接。
什么是 Electron?
Electron 是一个框架,支持使用 Chromium 渲染引擎和 Node.js 运行时等 Web 技术开发桌面应用程序。支持的操作系统包括 Windows、macOS 和 Linux。您很可能使用过以下至少一个使用 Electron 开发的应用程序:
- Visual Studio Code
- Slack
- Discord
- Skype
- GitHub Desktop
- Twitch
Electron 利用 HTML、CSS 和 JavaScript 等熟悉的标准。但是,如果您是一位习惯于使用 C# 的 .NET 开发人员怎么办?这就是 Electron.NET 发挥作用的地方。
什么是 Electron.NET?
Electron.NET 是一个包装器,它封装了一个带有嵌入式 ASP.NET Core 应用程序的“普通”Electron 应用程序。它是一个开源项目,允许 .NET 开发人员使用 C# 调用本机 Electron API。Electron.NET 由两个组件组成:
- 一个 NuGet 包,将 Electron API 添加到 ASP.NET Core 项目中
- 一个 .NET Core 命令行扩展,用于为 Windows、macOS 和 Linux 平台构建和启动应用程序。
Electron.NET 需要事先安装以下软件:
我依靠 Electron.NET 开发了 C1DataEngine Workbench,这是一个跨平台工具,支持创建和可视化由适用于 .NET Standard 的 ComponentOne DataEngine 库管理的数据分析工作区。我最初计划将其作为一个标准 Electron 应用程序,通过调用 .NET Core 全局工具的 shell 命令与库通信。但当我发现 Electron.NET 后,我能够消除 shell 命令并直接调用库。
创建一个 ASP.NET Core Web 应用程序
在本练习中,我使用的是在 Mac 上运行的 Visual Studio Code。首先,打开一个终端窗口并运行以下命令创建一个名为 Processes 的新项目。
mkdir Processes
cd Processes
dotnet new webapp
code .
当 Visual Studio Code 提示时,选择 是 加载项目所需的资产。按 F5 构建并运行应用程序,在 localhost:5001 上打开默认的 ASP.NET Core 欢迎页面。关闭页面,返回 VS Code 并停止调试。
Electron 化!
现在让我们将我们的样板 ASP.NET Core 项目转换为 Electron 应用程序。此步骤涉及向项目文件添加 NuGet 包,插入一些初始化代码,以及安装一个命令行工具来执行构建。首先,打开文件 Processes.csproj 并插入对托管在 nuget.org 上的 Electron.NET API 的包引用:
<ItemGroup>
<PackageReference Include="ElectronNET.API" Version="9.31.2" />
</ItemGroup>
保存文件,然后在 VS Code 提示时恢复包。此操作可让您立即访问 Intellisense,以便后续修改代码。
接下来,编辑 Program.cs 并为新添加的包插入一个 using 语句:
using ElectronNET.API;
找到静态方法 CreateHostBuilder
并在调用 UseStartup
之前插入以下两行:
webBuilder.UseElectron(args);
webBuilder.UseEnvironment("Development");
第一行是必需的。第二行在开发过程中很方便,因为它允许显示详细的错误消息。
编辑 Startup.cs 并插入以下 using 语句:
using ElectronNET.API;
using ElectronNET.API.Entities;
using System.Runtime.InteropServices;
找到 Configure
方法并将其主体末尾添加以下行:
if (HybridSupport.IsElectronActive)
{
CreateWindow();
}
最后,将以下方法添加到 Startup
类中以创建主 Electron 窗口:
private async void CreateWindow()
{
var window = await Electron.WindowManager.CreateWindowAsync();
window.OnClosed += () => {
Electron.App.Quit();
};
}
由于我们的应用程序由一个窗口组成,我们处理 OnClosed
事件以在用户关闭窗口(而不是从主菜单中选择“退出”)时终止应用程序。
安装命令行工具
除了您之前在项目文件中引用的运行时包,Electron.NET 还提供了一个命令行工具来执行构建和部署任务。在 VS Code 中,创建一个新的终端窗口并输入:
dotnet tool install ElectronNET.CLI -g
这个一次性步骤将安装一个 .NET Core 全局工具,该工具实现了一个名为 electronize 的命令。要查看系统上安装的工具/命令列表,请键入以下内容:
dotnet tool list -g
运行 Electron 应用程序
安装命令行工具后,在 VS Code 终端窗口中键入以下行:
electronize init
electronize start
第一行是一次性步骤,它会创建一个名为 electron.manifest.json 的清单文件并将其添加到您的项目中。第二行用于启动 Electron 应用程序(不要使用 F5,因为这只会浏览器中打开 ASP.NET Core 应用程序)。请注意,内容现在显示在应用程序窗口中,而不是浏览器中。
另请注意默认的 Electron 应用程序菜单。在 Mac 上,此菜单不是窗口本身的一部分,而是固定在屏幕顶部。
在撰写本文时,您创建项目时安装的 Bootstrap 模块中存在脚本错误。
要查看它,打开“视图”菜单并单击“切换开发人员工具”。
Uncaught TypeError: Cannot read property 'fn' of undefined
at util.js:55
at bootstrap.bundle.min.js:6
at bootstrap.bundle.min.js:6
幸运的是,有一个简单的解决方法。首先,打开“Electron”菜单并单击“退出 Electron”以关闭开发人员工具和应用程序窗口。在 VS Code 中,打开 Pages/Shared/_Layout.cshtml 并在 Bootstrap 脚本标签之前插入以下行:
<script>window.$ = window.jquery = module.exports;</script>
保存此更改,然后在终端窗口中键入 electronize start 以重新运行应用程序。打开开发人员工具并注意错误消息已消失。保持应用程序窗口打开以进行下一步。
调试 ASP.NET 代码
由于我们使用外部命令而不是 F5 启动了应用程序,因此我们需要将调试器附加到正在运行的 ASP.NET 进程。在应用程序窗口打开的情况下,转到 VS Code,打开 Pages/Index.cshtml.cs,并在 OnGet
处理程序上设置一个断点。单击活动栏上的“运行”,从下拉控件中选择 .NET Core Attach,然后单击相邻的图标以显示进程列表。在列表中输入应用程序名称 (Processes) 并选择剩余的项。(如果碰巧仍然显示多个进程,请选择 electronWebPort
值最大的那个。)
在应用程序窗口中,单击“视图”菜单上的“重新加载”,将命中断点。继续执行,关闭应用程序窗口,并注意调试器会自动断开连接。
自定义主页
为了说明 Electron.NET 的跨平台功能,让我们用活动系统进程列表替换默认主页内容。稍后,我们将构建一个 Linux 版本并观察该平台上的差异。
首先,打开 Pages/Index.cshtml.cs 并为进程 API 添加以下 using 语句:
using System.Diagnostics;
接下来,将以下属性声明添加到 IndexModel
类:
public List<Process> Processes { get; set; }
现在将以下行添加到 OnGet
处理程序的(最初为空的)主体中:
var items = Process.GetProcesses().Where(p => !String.IsNullOrEmpty(p.ProcessName));
Processes = items.ToList();
现在让我们修改相应的 Razor 页面标记以显示进程列表。
打开 Pages/Index.cshtml 并将整个原始 <div>
标签替换为以下行:
<div>
<table id="myTable" class="table table-sm table-striped">
<thead class="thead-dark">
<tr class="d-flex">
<th scope="col" class="col-sm-2">Id</th>
<th scope="col" class="col-sm-6">Process Name</th>
<th scope="col" class="col-sm-4 text-right">Physical Memory</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Processes)
{
<tr class="d-flex">
<td class="col-sm-2" scope="row">@item.Id</td>
<td class="col-sm-6">@try{@item.MainModule.ModuleName}catch{@item.ProcessName}</td>
<td class="col-sm-4 text-right">@item.WorkingSet64</td>
</tr>
}
</tbody>
</table>
</div>
此修改将显示一个命名进程表,其中包含 ID 号、进程名称和为进程分配的物理内存量的列。请注意内联 try/catch 块的使用。在某些平台上,进程名称可能会被截断,因此模块名称是首选,进程名称作为备用值。
Electron.NET 支持监视模式,它会监视您的更改并自动重建和重新启动您的应用程序。要调用监视模式,请运行以下命令:
electronize start /watch
现在保存您对项目的所有更改。应用程序重新启动后,修改后的主页应该看起来像这样:
添加详细视图
在典型的 ASP.NET Core 应用程序中,列表中的项目包含指向详细信息页面的链接,用户可以在其中更详细地查看项目或修改它。让我们为单个进程创建一个简单的视图。首先,在 Pages 文件夹中添加一个名为 View.cshtml 的新文件,并插入以下标记:
@page
@model ViewModel
@{
ViewData["Title"] = "Process view";
}
<div>
<dl class="row">
@foreach (var property in @Model.PropertyList.Select(name => typeof(System.Diagnostics.Process).GetProperty(name)))
{
<dt class="col-sm-4">
@property.Name
</dt>
<dd class="col-sm-8">
@property.GetValue(Model.Process)
</dd>
}
</dl>
</div>
<div>
<hr />
<form method="post">
<input type="submit" value="Kill Process" class="btn btn-danger" />
<a class="btn btn-light" asp-page="./Index">Back to List</a>
</form>
</div>
接下来,添加一个相应的名为 View.cshtml.cs 的代码隐藏文件,并插入以下代码:
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using ElectronNET.API;
using ElectronNET.API.Entities;
namespace Processes.Pages
{
public class ViewModel : PageModel
{
private readonly ILogger<ViewModel> _logger;
public ViewModel(ILogger<ViewModel> logger)
{
_logger = logger;
PropertyList = new string[] {
"Id",
"ProcessName",
"PriorityClass",
"WorkingSet64"
};
}
public Process Process { get; set; }
public string[] PropertyList { get; set; }
public void OnGet(int? id)
{
if (id.HasValue)
{
Process = Process.GetProcessById(id.Value);
}
if (Process == null)
{
NotFound();
}
}
}
}
字符串数组 PropertyList
定义了要在详细视图中显示的 Process
对象属性列表。我们没有在页面标记中硬编码这些字符串,而是使用反射在运行时派生属性名称和值。
要将详细视图链接到主页上的单个项目,请编辑 Pages/Index.cshtml 并替换表达式:
@item.Id
使用此锚标签:
<a asp-page="./View" asp-route-id="@item.Id">@item.Id</a>
像以前一样运行应用程序,并注意 Id
列包含导航到类似于此页面的超链接:
您可能已经注意到标记包含一个用于 HTTP POST 请求的提交按钮。让我们通过向 ViewModel
类添加以下方法来完成详细信息页面的实现:
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id.HasValue)
{
Process = Process.GetProcessById(id.Value);
}
if (Process == null)
{
return NotFound();
}
Process.Kill();
return RedirectToPage("Index");
}
虽然这无疑会完成任务,但最好给用户一个考虑并取消操作的机会。让我们用以下代码替换最后两行,该代码使用 Electron.NET 的 ShowMessageBoxAsync
API 向用户显示一个特定于平台的确认对话框:
const string msg = "Are you sure you want to kill this process?";
MessageBoxOptions options = new MessageBoxOptions(msg);
options.Type = MessageBoxType.question;
options.Buttons = new string[] {"No", "Yes"};
options.DefaultId = 1;
options.CancelId = 0;
MessageBoxResult result = await Electron.Dialog.ShowMessageBoxAsync(options);
if (result.Response == 1)
{
Process.Kill();
return RedirectToPage("Index");
}
return Page();
这样,如果用户取消,详细信息页面将保持当前状态。否则,应用程序在终止进程后重定向到主页。
自定义应用程序菜单
Electron.NET 提供了一个默认的应用程序菜单,如前所述。此菜单与 Electron 框架本身提供的默认菜单相同。不幸的是,无法调整默认菜单(Electron 的限制)。如果您想添加新命令或删除不需要的子菜单,您别无选择,只能从头开始指定整个菜单结构。由于 macOS 和其他平台之间的差异,此任务变得更加复杂。在 macOS 上,应用程序有自己的菜单,位于标准文件/编辑/视图菜单的左侧。
private void CreateMenu()
{
bool isMac = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
MenuItem[] menu = null;
MenuItem[] appMenu = new MenuItem[]
{
new MenuItem { Role = MenuRole.about },
new MenuItem { Type = MenuType.separator },
new MenuItem { Role = MenuRole.services },
new MenuItem { Type = MenuType.separator },
new MenuItem { Role = MenuRole.hide },
new MenuItem { Role = MenuRole.hideothers },
new MenuItem { Role = MenuRole.unhide },
new MenuItem { Type = MenuType.separator },
new MenuItem { Role = MenuRole.quit }
};
MenuItem[] fileMenu = new MenuItem[]
{
new MenuItem { Role = isMac ? MenuRole.close : MenuRole.quit }
};
MenuItem[] viewMenu = new MenuItem[]
{
new MenuItem { Role = MenuRole.reload },
new MenuItem { Role = MenuRole.forcereload },
new MenuItem { Role = MenuRole.toggledevtools },
new MenuItem { Type = MenuType.separator },
new MenuItem { Role = MenuRole.resetzoom },
new MenuItem { Role = MenuRole.zoomin },
new MenuItem { Role = MenuRole.zoomout },
new MenuItem { Type = MenuType.separator },
new MenuItem { Role = MenuRole.togglefullscreen }
};
if (isMac)
{
menu = new MenuItem[]
{
new MenuItem { Label = "Electron", Type = MenuType.submenu, Submenu = appMenu },
new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
};
}
else
{
menu = new MenuItem[]
{
new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
};
}
Electron.Menu.SetApplicationMenu(menu);
}
private async void CreateWindow()
{
CreateMenu(); // add this line
var window = await Electron.WindowManager.CreateWindowAsync();
window.OnClosed += () => {
Electron.App.Quit();
};
}
请注意使用预定义菜单角色来指定目标平台的适当操作和快捷键。此外,由于 Electron.NET 序列化传递给 SetApplicationMenu
的参数的方式,您需要使用数组初始化器构建整个菜单结构。您不能将 MenuItem 实例附加到空数组,然后将其传递给菜单 API。
现在,当您在 Mac 上运行应用程序时,菜单栏将有三个名为 Electron、文件和视图的子菜单。
Electron.NET 还支持本机系统文件对话框。让我们修改“文件”菜单定义以添加“另存为”命令,该命令会提示用户输入文件名,并将当前进程列表以逗号分隔格式输出到该文件。
MenuItem[] fileMenu = new MenuItem[]
{
new MenuItem { Label = "Save As...", Type = MenuType.normal, Click = async () => {
var mainWindow = Electron.WindowManager.BrowserWindows.First();
var options = new SaveDialogOptions() {
Filters = new FileFilter[] { new FileFilter{ Name = "CSV Files", Extensions = new string[] { "csv" } }
}};
string result = await Electron.Dialog.ShowSaveDialogAsync(mainWindow, options);
if (!string.IsNullOrEmpty(result))
{
string url = $"https://:{BridgeSettings.WebPort}/SaveAs?path={result}";
mainWindow.LoadURL(url);
}
}},
new MenuItem { Type = MenuType.separator },
new MenuItem { Role = isMac ? MenuRole.close : MenuRole.quit }
};
我们不指定预定义的菜单角色之一,而是将菜单类型设置为 normal 并提供一个异步 Click 处理程序。ShowSaveDialogAsync
API 使用指定的选项打开一个本机文件对话框,在这种情况下,是 .csv 扩展名文件的过滤器。如果用户不取消对话框,API 将返回所选文件的完整路径。此路径用作 SaveAs Razor 页面的参数,该页面包含一个输出逗号分隔文本的 OnGetAsync
处理程序:
public async Task<IActionResult> OnGetAsync(string path)
{
System.IO.StringWriter writer = new System.IO.StringWriter();
writer.WriteLine("Id,Process Name,Physical Memory");
var items = Process.GetProcesses().Where(p => !String.IsNullOrEmpty(p.ProcessName)).ToList();
items.ForEach(p => {
writer.Write(p.Id);
writer.Write(",");
writer.Write(p.MainModule.ModuleName);
writer.Write(",");
writer.WriteLine(p.WorkingSet64);
});
await System.IO.File.WriteAllTextAsync(path, writer.ToString());
return RedirectToPage("Index");
}
添加第三方控件
与任何常规 ASP.NET Core 网站一样,您可以将第三方控件添加到 Electron.NET 应用程序。让我们用 ComponentOne FlexChart 控件替换默认的隐私页面,该控件按物理内存使用量的降序绘制前十个进程。
首先,将以下包引用添加到 .csproj 文件中:
<PackageReference Include="C1.AspNetCore.Mvc" Version="3.0.20202.*" />
编辑 Pages/_ViewImports.cshtml 并附加相应的标签助手:
@addTagHelper *, C1.AspNetCore.Mvc
编辑 Pages/Shared/_Layout.cshtml 并在结束 </head>
标签之前插入以下行:
<c1-styles />
<c1-scripts>
<c1-basic-scripts />
</c1-scripts>
然后,在同一个文件中,将前两个 Privacy 替换为 Chart。
编辑 Startup.cs 并将 UseEndpoints
调用替换为以下内容:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
需要调用 MapControllerRoute
才能使 MVC 控件正确加载其资源。
最后,在 Pages 文件夹中创建一个名为 Chart.cshtml 的文件,并添加以下标记:
@page
@model ChartModel
@{
ViewData["Title"] = "Chart view";
}
<div>
<c1-flex-chart binding-x="Name" chart-type="Bar" legend-position="None">
<c1-items-source source-collection="@Model.Processes"></c1-items-source>
<c1-flex-chart-series binding="Memory" name="Physical Memory" />
<c1-flex-chart-axis c1-property="AxisX" position="None" />
<c1-flex-chart-axis c1-property="AxisY" reversed="true" />
</c1-flex-chart>
</div>
然后添加相应的代码隐藏文件 Chart.cshtml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespace Processes.Pages
{
public class ChartModel : PageModel
{
public List<object> Processes { get; set; }
private readonly ILogger<ChartModel> _logger;
public ChartModel(ILogger<ChartModel> logger)
{
_logger = logger;
}
public void OnGet()
{
var items = Process.GetProcesses()
.Where(p => !String.IsNullOrEmpty(p.ProcessName))
.OrderByDescending(p => p.WorkingSet64)
.Take(10);
Processes = items.Select(p => new {
Id = p.Id,
Name = p.ProcessName,
Memory = p.WorkingSet64
}).ToList<object>();
}
}
}
保存所有更改。请注意,构建过程将为 ComponentOne Studio Enterprise 创建 30 天试用许可证。单击菜单栏中的“图表”链接,您应该会看到一个类似于以下的条形图:
为其他平台构建
要为其他平台构建安装介质,请在终端窗口中运行以下命令:
electronize build /target xxx /PublishReadyToRun false
其中 xxx 是 win、linux、osx 之一。输出到 bin/Desktop 文件夹,例如:
- Processes Setup 1.0.0.exe (Windows)
- Processes-1.0.0.AppImage (Linux)
- Processes-1.0.0.dmg (OSX)
请注意,Windows 可执行文件是一个安装程序,而不是应用程序本身。此外,OSX 目标只能在 Mac 上构建,但 Windows/Linux 目标可以在任何平台上构建。要更改版本号、版权声明和其他属性,请在创建安装介质之前编辑 electron.manifest.json。
这是应用程序在 Linux 上运行时的样子:
结论和示例代码
Electron.NET 是一个开源工具,通过为 C# 开发人员提供为 Windows、Linux 和 macOS 交付跨平台桌面应用程序的途径,为 ASP.NET Core 增值。它还与 ComponentOne Studio Enterprise 中的 MVC 控件等第三方组件兼容。
本文描述的已完成项目的源代码可在 GitHub 上获取。