Photino:使用 .NET Core 构建跨平台桌面应用的开源方案
完全开源的库,用于通过 .NET Core 构建跨平台桌面应用
引言
本文将指导您构建第一个 Photino 桌面应用程序(基于 .NET Core 构建),该应用程序将在所有三大平台(Linux、Mac、Windows)上运行。
为什么?
您一直梦想的未来终于到来:一次构建桌面应用,随处运行。
是的,这个未来确实涉及 HTML5(HTML、JavaScript、CSS),但没关系,我经验丰富的桌面开发者朋友。没关系,因为现在您拥有 .NET Core 框架的力量。
一次构建您的用户界面(使用 HTML5、JavaScript 和 CSS),同时利用 .NET Core 的所有强大功能来访问桌面 API 功能(读写文件、加密 API、通过 .NET Core 暴露的所有内容)。
背景
我为什么对跨平台应用感兴趣
我编写了一个密码生成器(Windows 商店链接[^] FOSS(完全开源软件),因此您可以在我的 Github 链接[^] 获取源代码。
如果您要编写一个人们会使用的密码生成器,它必须能在所有已知平台上运行,这样无论用户在哪里需要她的密码,它都可用。
原始版本使用 ElectronJS(Chrome 引擎)编写,同样能在所有主要平台上运行。现在 Photino 已经推出,我将把应用程序转换为 .NET Core,而且转换起来很容易。
官方 Photino 项目文档
顺便说一下,Photino 由 CODE Magazine 的好人们支持,您可以在 tryphotino.io 查看所有文档。另外,正如我所说,它全部是开源的,您可以在github获取所有代码。
这是一个我正在开发的 FileViewer
的快速示例。请记住,UI 是基于 HTML5、JavaScript 和 CSS 构建的,但它可以通过 .NET Core 调用本地“桌面”API——Directory.GetFiles()
等。
但是,为了了解 Photino 能为您做什么,让我们使用该库编写我们的第一个程序。
入门
更新说明:.NET 5.x 与 6.x
在将代码克隆到没有安装 .NET Core 的新系统后,当我安装 .NET Core 6.x 时,项目无法构建。
.NET Core 6.x 是新的标准,所以不得不安装一个旧版本会很麻烦。
与其这样做,您可以简单地更新 HelloPhotino.NET.csproj*
文件以引用 .net6.0。
*这个名字是 Photino 模板为您的项目提供的默认项目名称。 我应该改名的。 😖
只需在编辑器中打开 .csproj
文件并更改以下行
<TargetFramework>net5.0</TargetFramework>
只需将 5 更改为 6,然后您就可以构建了。
<TargetFramework>net6.0</TargetFramework>
您将需要
- .NET Core 5.0 或 6.0 SDK 已安装并准备就绪:在此处从 Microsoft 获取。
- Photino 项目模板 - 从命令行创建项目非常简单
- 代码编辑器:本文我使用的是 Visual Studio Code
我将从假定您已经安装了 .NET Core 5 或 6 开始。
您可以使用以下命令确定您拥有的版本
$ dotnet --version
安装 Photino 项目模板
打开命令行提示符并运行以下命令
$ dotnet new -i TryPhotino.VSCode.Project.Templates
这只会将项目模板列表添加到 dotnet new
命令可用列表中
您可以运行以下命令查看所有项目模板的列表(您将在列表中看到新的模板)
$ dotnet new -l // that's a lowercase L for list
您将看到一个看起来像这样的项目模板列表
创建我们的第一个项目
现在我们已经安装了 Photino 项目模板,我们可以转到开发目录(我将其命名为 dev/dotnet/photino/ 以包含我所有的 photino
项目),然后发出以下命令。
~/dev/dotnet/photino $ dotnet new photinoapp -o FirstOne
运行该命令将
- 在我的 photino 目录下方创建一个名为 FirstOne 的新目录(
-o
是输出) - 创建新的 .NET Core 项目(包括 .csproj 文件)和所有其他基本应用程序文件。
- 创建一个 wwwroot -- Photino 用于存储用户界面文件(HTML、JavaScript、CSS)的特殊文件夹
运行基本应用程序
创建样板项目后,您可以立即运行它。
只需进入新目录并运行
$ dotnet run // compiles & runs the app
应用程序将启动,并且屏幕中央会出现一个弹出对话框,以演示可以通过 JavaScript 执行操作。
点击 [关闭] 按钮,您就可以看到主界面。
点击 [调用 .NET] 按钮,您将看到以下内容
目前还不算太惊艳……
到目前为止没什么太惊艳的。让我们看看项目中包含的文件和代码,以便我们能大致了解到底发生了什么。之后,我们将通过 C# 进行一次“桌面 API”调用,这在 Web 应用程序中永远无法实现,以证明这个应用程序确实非常惊艳。
Program.cs:一切的起点
这是 Visual Studio Code 中的项目的一个大快照,显示了很多细节。
老牌的主入口点
在 Program.cs 文件中,右上角可以看到我们有我们熟悉的 Main()
方法。
魔法来了:它是如何工作的
这是一个实际的 C# .NET 程序。魔法在于它会自动加载 WebView2(Microsoft 文档)作为主 Form
界面,然后在此 WebView2
控件中加载您的目标 HTML。
如果我们向下滚动代码,您会看到 Main()
方法的最后一个调用是以下 Photino
库调用
.Load("wwwroot/index.html");
当然,正如您在左侧看到的,index.html 文件位于 wwwroot 文件夹中。
index.html 文件看起来如下
这只是简单的 HTML,但该文件构成了此应用程序的整个用户界面。这非常惊艳。
现在您可以梦想了
这意味着您现在可以将任何 HTML5(基于 Web)应用程序包装到 Photino 中,并将其变成一个桌面应用程序,该应用程序可以在任何 Mac、Linux 或 Windows 计算机上原生运行。
极致示例
作为一次实验,我创建了一个模板 Photino 项目,使用了我的基于 Web 的 C'YaPass 应用(密码生成器),放入了 HTML(index.html)、JavaScript 和 CSS 文件,并运行了 Photino 应用,得到了以下结果,没有任何代码更改。
该应用程序使用了 HTML5 Canvas
、localStorage
和各种其他 HTML 技术,但在任何桌面上都能完美运行。
但为什么?
该应用程序还通过 JavaScript 函数生成 SHA-256 哈希码(用作密码)。现在,有了 Photino,我可以删除 JavaScript,使用 .NET Core 加密库使一切更简洁。我之所以可以这样做,是因为我可以在 Photino
框架内,通过 C# 调用桌面 API。
让我们看看如何进行一个简单的 .NET API 调用。
通过 C# 调用桌面 API
为了证明这一点,我们确实需要通过 C# 调用 Desktop
API。
我们需要做什么
要完成这项工作,我们将
- 添加一个按钮来触发功能——当然,这个按钮将在 index.html 中创建
- 当按钮被点击时,我们需要向 Photino 窗口(C# 端)发送一条消息,请求调用相关的桌面 API。
- 将一条消息发送回用户界面(index.html)
- 在用户界面(index.html)中显示我们调用的结果
获取源代码
我将在本文的开头添加完成的代码,以便您可以轻松尝试。
FYI - 从模板中移除了代码
执行自动弹出功能的代码很烦人,所以我将其移除了。
步骤 1:添加一个按钮
为了保持简单,我将在(从项目模板中)现有按钮正下方添加一个新按钮
<button id="callApiButton" onclick="callApi()">Call API</button>
FYI - 是的,我知道很多人不喜欢将事件处理程序(onclick
)直接放在 HTML 元素上,但这对于我们的示例是简化的。
添加后,您可以运行并看到按钮存在,但什么也不做。
如果您正在跟随操作来运行应用程序,只需转到您的项目命令行并输入
$ dotnet run
现在,让我们让按钮起作用。
步骤 2:向 C# 端发送消息
我将添加一个新的 JavaScript 文件(api.js)并将其包含在 index.html 文件的顶部。api.js 文件将包含处理 callApi()
函数的代码。
我将从 index.html 中复制样板代码,该代码用于在第一个按钮被点击时向应用程序发送消息
window.external.sendMessage('Hi .NET! 🤖');
这是用于与 Photino 库交互以处理消息发送的 JavaScript 代码。
修改消息
模板项目发送的消息非常简单,因为它只是一个 string
。实际上,我们可能想要/需要发送某种包含以下内容的结构:
- 命令消息
- 一个或多个参数,将由 C# 端的目标函数使用。
JavaScript 对象和 JSON
我将创建一个 JavaScript 对象,然后使用 JSON.stringify
(创建完美的 JSON)将 string
发送到 C# 端,然后 C# 端将其反序列化并提取命令。
这是 api.js 的完整代码列表
function callApi(){
let message = {}; // create basic object
message.command = "getUserProfile";
message.parameters = "";
let sMessage = JSON.stringify(message);
console.log(sMessage);
window.external.sendMessage(sMessage);
}
在这种情况下,我没有使用任何其他参数,但仍然将它们传递进来。
此外,我没有必要创建一个单独的 sMessage
变量,但我这样做是为了让您能够查看我们正在传递的实际 string
(JSON)。
现在我们的按钮可以做些什么了
如果您正在跟随操作,请不要忘记在 index.html 的顶部添加我们新的 api.js 的引用。
设置好一切后,运行应用程序($ dotnet run
)并点击新按钮。
您将在控制台窗口中看到一些日志(来自 Photino.net),并且您将看到收到的消息在应用程序中弹出。
响应接收到的消息
但这还没有完成,因为我们希望它捕获 message.Command
并做出相应的响应(调用特定的桌面 API)。
将 JSON 解析为对象
要完成这项工作,我们需要更改 Program.cs 以将发送的 JSON 解析为相应的对象。我们必须在 C# 端进行这项工作。
首先,让我们创建一个简单的 DTO(数据传输对象)
我添加了一个名为 Model 的新文件夹(用于域模型对象),并创建了一个名为 WindowMessage.cs 的新 DTO 类文件。(您将在本文附带的最终代码中看到所有这些。)
这是简单的代码,现在可以让我们在代码中轻松使用 C# JSON 序列化器/反序列化器。
using System;
class WindowMessage{
public WindowMessage(String command, String parameters)
{
this.Command = command;
this.Parameters = parameters;
this.AllParameters = parameters.Split(',',StringSplitOptions.RemoveEmptyEntries);
}
public String Command{get;set;}
public String[] AllParameters{get;set;}
public String Parameters{get;set;}
}
传入的参数将是一个逗号分隔的 string
,然后该类会自动将其拆分并创建一个 String
数组,这些是我们可能想要使用的参数。
现在让我们使用这段代码。
在 Program.cs 中,主消息处理程序(来自项目模板)是一个简化的方法,看起来如下
.RegisterWebMessageReceivedHandler((object sender, string message) => {
var window = (PhotinoWindow)sender;
// The message argument is coming in from sendMessage.
// "window.external.sendMessage(message: string)"
string response = $"Received message: \"{message}\"";
// Send a message back the to JavaScript event handler.
// "window.external.receiveMessage(callback: Function)"
window.SendWebMessage(response);
})
您可以看到传入的消息只是一个 string
。
当然,在我们的新代码中,我们保证发送一个 WindowMessage
对象(通过 JSON)。
由于 C# 使 JSON 反序列化如此容易,我们可以添加以下代码将其反序列化为我们的 DTO(WindowMessage
)并处理 Command
值。
我在 Program.cs 的顶部添加了 using
语句
using System.Text.Json;
using System.Text.Json.Serialization;
现在我可以在 .RegisterWebMessageReceivedHandler()
调用顶部添加以下代码
WindowMessage wm = JsonSerializer.Deserialize<WindowMessage>(message);
这将把传入的 message String
解析到我们的目标 DTO。
根据 WindowMessage.Command 进行切换
现在,我们 .RegisterWebMessageRecievedHandler()
中的代码如下
.RegisterWebMessageReceivedHandler((object sender, string message) => {
var window = (PhotinoWindow)sender;
WindowMessage wm = JsonSerializer.Deserialize<WindowMessage>(message);
switch(wm.Command){
case "getUserProfile":{
window.SendWebMessage($"I got : {wm.Command}");
break;
}
default :{
// The message argument is coming in from sendMessage.
// "window.external.sendMessage(message: string)"
string response = $"Received message: \"{wm.Parameters}\"";
// Send a message back the to JavaScript event handler.
// "window.external.receiveMessage(callback: Function)"
window.SendWebMessage(response);
break;
}
}
})
我们只需将 JSON 反序列化为我们的 DTO,然后根据 wm.Command
值进行切换。
注意:我对原始 Button
JavaScript 做了一个更改,使其也能传递一个有效的 WindowMessage
对象,但您可以自己查看该代码。
当您点击新按钮时,运行情况如下。
我们现在可以成功运行各种 C# 代码,具体取决于我们的 WindowMessage
中的 Command
是什么。资深开发者:这一切如何回溯到原始的 Windows 消息循环(Windows API 编程)和消息处理,这难道不很有趣吗?
总结:通过环境变量获取用户配置文件
好吧,这本该是对 Photino 的快速介绍,所以让我们添加一个 .NET API 调用,然后就结束吧。
然而,为了正确地结束,我们还需要向您展示如何使用返回到用户界面端(HTML)的值。
在用户界面端(HTML)注册消息接收器
要获取返回值,我们需要在应用程序加载时在用户界面端注册一个消息接收器。
我们将做两件事
- 向 HTML 添加一个
onload
函数,该函数将运行初始化并设置消息接收器 - 将
initApi()
方法添加到 api.js。
这是应用程序启动时(HTML 加载时)将初始化的代码(在 api.js 中)。
function initApi(){
window.external.receiveMessage(response => {
response = JSON.parse(response);
switch (response.Command){
case "getUserProfile":{
alert(`user home is: ${response.Parameters}`);
document.querySelector("#output").innerHTML = `${response.Parameters}`;
break;
}
default:{
alert(response.Parameters);
break;
}
}
});
}
这段代码将在调用 Desktop
API 后获取一个响应(从 C# 端发送)。它将包含用户主目录的值(通过 C# 使用 Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
检索)。
一旦这段代码(JavaScript)收到值,它将使用 alert()
显示它,并使用 document.querySelector("#output").innerHTML
将其写入主 HTML。
这是最终的 C# 代码。
.RegisterWebMessageReceivedHandler((object sender, string message) => {
var window = (PhotinoWindow)sender;
WindowMessage wm = JsonSerializer.Deserialize<WindowMessage>(message);
switch(wm.Command){
case "getUserProfile":{
wm.Parameters = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
window.SendWebMessage(JsonSerializer.Serialize(wm));
break;
}
default :{
// The message argument is coming in from sendMessage.
// "window.external.sendMessage(message: string)"
wm.Parameters = $"Received message: \"{wm.Parameters}\"";
// Send a message back the to JavaScript event handler.
// "window.external.receiveMessage(callback: Function)"
window.SendWebMessage(JsonSerializer.Serialize(wm));
break;
}
}
})
这是我点击新按钮后的快照。
现在,您去尝试一下,做一些您自己的应用程序。
记住:构建并部署到任何操作系统
记住,您现在可以将此代码与构建并部署到任何操作系统,它将正常运行。太棒了!
您怎么看
这是构建桌面应用程序的新方式吗?我认为这是一个非常酷的构建可在任何平台上运行的用户界面的方法。我认为它很棒,我将继续进一步开发。
历史
- 2022 年 5 月 26 日:首次发布