65.9K
CodeProject 正在变化。 阅读更多。
Home

Sciter/HTML/C#桌面应用教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (89投票s)

2016年2月15日

CPOL

31分钟阅读

viewsIcon

154358

downloadIcon

3261

使用Sciter引擎,用C#创建基于HTML/CSS/脚本的跨平台桌面应用程序!

引言

本文介绍了如何通过C#使用Sciter引擎创建基于HTML的桌面应用程序,使用了 SciterSharp 库(Sciter API的.NET绑定)。为了快速启动我们的应用程序,我们使用了一个Sciter Bootstrap模板,该模板有助于使我们的应用程序在Windows、Linux和OSX上进行编译。

Sciter是一个多平台HTML渲染引擎,用于创建桌面应用程序。Among the cool features, it has supports for the following(其中一些很酷的功能包括支持以下内容):

  • UI <-> 主机通信的本地集成,DOM操作,资源跟踪,所有这些都通过C#实现
  • 大量CSS3,并具有使用flex单位的出色布局系统
  • 通过TIScript语言进行脚本编写(JavaScript的扩展)
  • 用于前端DOM/样式操作、AJAX调用、JSON等的TIScript API

它也可以免费用于商业用途,尽管它不是开源的。它被分发为本地共享库(DLL),您可以在C/C++ SDK中找到它。SDK包含Windows、Linux和OSX的构建版本。

Sciter技术已被一些大型软件在现实世界中使用,证明了其潜力:ICQ客户端、Norton、Avast、Bitdefender、ESET杀毒软件。

示例桌面应用程序使用“Google API Client Library for .NET”来查询Google Fonts,以便向用户显示可用字体的列表以及下载每个字体的按钮。这将需要原生C#编码以及HTML和脚本的混合使用,以创建UI并在这两个层之间进行通信。这样,您将熟悉如何创建涉及少量原生C#编码、异步资源加载以及如何传递UI/原生层之间数据的应用程序。完整的源代码也可在 GitHub 上找到。

考虑使用Sciter而非CefSharp或Electron.io

在.NET应用程序中嵌入HTML内容已经有很多选项。CefSharp和Electron.io似乎是最受欢迎的,它们都使用Chromium作为底层引擎。既然Sciter不是一个流行的Web浏览器中使用的标准引擎,那么为什么还要选择它而不是一个广为人知的HTML引擎,如Gecko或Chromium,它们支持最新的HTML5技术呢?嗯,您需要自己决定什么对您和您的项目是合适的。我能做的是告诉您我为何采用Sciter,如何使用该技术,以及它的缺点。

Sciter特有功能集

Sciter不试图实现HTML5标准,但这并不意味着您以前的HTML/CSS知识就不能使用。以下是您可能需要考虑的关于差异的摘要

  • CSS包含所有基本属性(完整的CSS2.1)和一些重要的CSS3内容(border-radius, box-shadow, linear-gradient,请参阅 CSS属性支持表;此外,CSS布局是通过flex单位和“flow:”属性进行的,这与CSS3 flexbox非常接近。
  • HTML/CSS支持有一些细微之处,这是了解它们的最佳资源
  • 脚本是通过TIScript语言进行的;它不符合标准浏览器API,它有自己的API集,并且文档齐全;但由于它是JavaScript的扩展,Web开发人员会对此感到非常熟悉(请参阅比较表)。
  • 在HTML5功能中,Sciter具有以下等效功能:canvas绘图、SVG、WebSockets API、<video>、CSS动画/过渡、自定义字体(例如,您可以使用FontAwesome);其他HTML5功能您可能需要通过一些额外的原生编码来实现。

使用Sciter的优点

我选择Sciter而不是其他引擎的原因是,首先也是最重要的,Sciter是为创建桌面应用程序而量身定制的,而不是Web浏览器。我认为这种承诺带来了它多年来发展过程中的诸多好处(已经走了10年)。

我的Sciter使用建议是,当您需要良好的原生支持来构建HTML应用程序时:您可以灵活地选择您喜欢的编程语言(D、C#、C++、Python、Delphi、Go),并且它有一个成熟的原生API来操作您应用程序的每个方面。

标准浏览器引擎旨在访问从Web服务器远程提供的页面,在严格控制系统资源的沙盒中执行它们。Sciter旨在处理来自任何源的资源:资源加载是完全可定制的,它还提供了一个从BLOB打包数据加载资源的API。

在TIScript中,您可以处理系统资源(文件、套接字、IPC),并轻松地与原生层进行通信。Sciter没有很多安全限制和相关的开销,因为您正在制作桌面应用程序,所以假定您在处理受信任资源的执行。

Chromium对于中小型应用程序来说非常臃肿。Chromium与原生层的集成存在严重问题。例如,如本文所述,您无法将复杂的JavaScript对象传递到C#层,您需要先将其转换为JSON,然后将对象作为string发送回来。

  • Sciter不仅是跨平台的,支持Windows、Linux/GTK和OSX,它还为许多编程语言提供了绑定:C#、C++、D、Python、Delphi和Go(我是C#和D语言绑定的作者和维护者)。
  • 轻量级:引擎是一个您需要与产品一起发布的本地DLL
  • 高性能:引擎启动没有延迟,与一些臃肿的HTML引擎相比,内存消耗更低,并且绘图后端是GPU加速的(Windows上的Direct2D或Skia/OpenGL,Linux上的Cairo,OSX上的CoreGraphics)。
  • 出色的原生窗口集成:创建多个Sciter窗口实例,所有实例共享相同的TIScript VM,因此您可以共享数据;支持Aero-DWM窗口,WS_EX_LAYERED窗口(桌面透明);允许将原生窗口/HWND创建为子DOM元素。
  • 可定制的原生资源加载;可定制的调试输出消息
  • 用于DOM处理、DOM事件回调以及TIScript <-> 原生集成的原生API
    • CefSharp不公开DOM API;使用SciterSharp,您可以从C#查看/操作DOM!这对于调试非常有用,因为DOM操作通常是通过脚本进行的。
  • 从3.3.1.4版本开始,您可以使用它在DirectX窗口中渲染UI,参见此处和下图

使用Sciter的缺点

  • 它不是像WebForms或WPF那样的所见即所得环境,您需要编写HTML代码并在sciter.exe工具中预览最终的视觉效果(好吧,如果您认为HTML是一种优于所见即所得的良好方法,那么这确实不是缺点)。
  • 缺少HTML5功能和W3C标准:您不能简单地抓取像JQuery或Bootstrap这样的库并在Sciter中使用
  • 与Windows版本相比,Linux/GTK支持有许多HTML/CSS缺失的功能,而Windows版本更加成熟。

1. 开始

要开始,您必须清楚本文的目标是什么,以及涉及哪些内容

  • 我们将逐步创建一个桌面应用程序,该应用程序显示可用Google字体列表,并带有一个按钮,允许用户下载包含整个字体家族的.zip文件
  • 本文的整个过程将在Visual Studio 2015中完成,但最后我将展示如何在Linux中使用MonoDevelop进行编译。
  • 前端/UI完全由HTML/CSS和TIScript脚本实现。
  • 后端用C#实现,涉及
    • Google .NET API:用于查询Google Fonts
    • 异步处理Google响应并将返回的数据传递给UI层
    • 我们使用 SciterSharp 库及其Sciter引擎API来实现许多功能,从UI通信到资源加载。
  • 我们从一个预制的IDE项目开始,该项目是从Sciter Bootstrap页面下载的。
    • 我们选择一个多平台模板,以便轻松地为Windows、Linux和OSX进行编译。

因此,要开始,我们需要获取一些资源,请遵循以下3个步骤。

步骤1:下载Sciter Bootstrap包

由于我们将创建一个多平台桌面应用程序,您必须理解,支持不同的操作系统意味着要处理不同的API集。通常,您希望使用一个跨多个平台的统一API。但有时,这是不可能的,您必须诉诸于使用特定于操作系统的API。在C#中,您可以通过将平台特定的代码包围在#if/#endif条件编译块中来实现。在这种情况下,由于我们处理的是两个不同的操作系统及其各自的窗口系统:Win32 API和GTK+3 API,因此可能需要这样做。

幸运的是,您不必太担心这个问题,因为我们将从一个包含最少样板代码的模板开始。这不仅是代码,它还包含正确的SciterSharp依赖项配置,一个可以在VS和MonoDevelop中打开以编译应用程序的.sln文件,并且它包含两个项目(每个平台/IDE一个)。

因此,前往 Sciter Bootstrap下载页面 并按照以下步骤操作

步骤1:输入项目标题 - 对于本文,我们使用FontLister(标题名称必须符合C#标识符规则)。

步骤2:选择“Visual Studio + Xamarin项目”(第一个单选按钮)。

步骤3:点击“下载”按钮。

解压.zip文件内容,然后在Visual Studio 2015中打开.sln文件。按生成按钮,等待它下载SciterSharp NuGet包,然后运行应用程序。酷!您已经拥有一个Sciter多平台应用程序正在运行。这很容易,对吧?

如果出现编译错误,那是因为SciterSharp NuGet未正确下载。转到程序包管理器控制台,并尝试发出“Update-Packages -Reinstall”命令来修复它。

请注意,此解决方案包含三个项目。由于我们不需要在Windows上构建MONO/GTK和OSX项目(它们甚至不会运行),因此可以随意右键单击这些项目并选择“卸载项目”。

手动安装

如果您正在启动一个项目并且不想使用Sciter Bootstrap,您可以下载并自行安装SciterSharp。

但是,本文不支持这些方法,因为我们希望从Sciter Bootstrap模板代码开始,因为它已准备好跨平台,并且我们希望我们的应用程序在Windows、Linux和OSX上运行。

步骤2:安装NuGet包并获取Google API密钥

我们需要通过NuGet包安装额外的.NET库。您必须将包安装到两个项目(FontListerGTKFontListerWindows)中。您可以右键单击“FontLister”解决方案项并选择“为解决方案管理NuGet程序包”(请注意,FontListerGTK项目必须已加载)。

安装这些NuGet包

  • Google.Apis.Webfonts.v1
  • Newtonsoft.Json

您需要获取一个Google API密钥来查询Google Fonts。

  • 前往 Google Developers Console
  • 创建一个新项目
  • 在概览页面上,有一个输入框,上面写着“搜索所有100+个API”;搜索“fonts”。
  • 选择“Web Fonts Developer API”并点击蓝色的“启用API”按钮。
  • 现在转到凭据 / 新建凭据 / 服务器密钥。
  • 由于我们只会在本地从桌面应用程序查询Google Fonts,一个简单的服务器密钥就足够了。
  • 按创建,将弹出一个窗口显示API密钥,只需将其保存在某处。

步骤3:下载Sciter SDK

这里 获取Sciter SDK。SDK主要面向使用C/C++ API的开发者,但它也包含所有Sciter二进制文件和工具。我们从SDK中需要的是位于/bin目录中的sciter.exe工具。我建议您运行它并将图标固定到任务栏。我们将使用此工具做两件事:

  1. 您可以使用它来查看您编写的HTML代码的渲染结果;通常,您会编辑HTML代码,然后切换回sciter.exe并按F5刷新页面;也就是说,sciter.exe就像一个浏览器,用于Sciter HTML内容;它还具有等同于DOM检查和脚本调试的F12工具
  2. 它还使您能够访问SDK中随附的文档;点击左侧工具栏上的“?”按钮,将打开一个窗口,其中包含您用于脚本编写的所有TIScript API的描述,并教授您关于该语言的知识;您将经常查阅它,相信我。

2. 实战代码 -> 后端

在开始之前,我想提醒您在进行此类桌面应用程序开发时经常会遇到的一个挫败感来源。

在Web开发中,将工作分成两人是常见的做法:一个人负责UI/UX思考(前端),另一个人负责业务逻辑,后者根据UI事件驱动应用程序(后端)。

使用Sciter,就像在Web开发中一样,您应该坚持相同的开发流程。问题在于您将孤军奋战,不可避免地要同时做这两件事,这可能会非常令人沮丧。

在进行UI编码和思考时,通常情况下,只需一点CSS和HTML,您就能很快取得成果,并且可以看到视觉效果。后端通常需要更具挑战性的工作和过程化的思维方式。问题是这两种思维方式不兼容,当我同时进行这两种事情时,我的大脑会感到疼痛。因此,我非常鼓励您努力习惯将这些事情分开做,就像我将要展示的那样。

我们将从后端开始,在进行实际UI之前,我们将使用2种不同的约定定义与UI的连接点。在Sciter中,后端<->UI通信有3种约定,您可以在 此处 了解更多细节。

这是一个教程,我们将主要添加或调整Bootstrap模板中的默认代码。

窗口创建

我们的应用程序初始化在文件Src/App.cs中,您可以在其中找到窗口创建代码,这是不言而喻的。SciterWindow类是创建和处理用于托管Sciter HTML页面的OS原生窗口的独立方法。对于我们的FontLister,我更改了窗口的标题和大小,因此最终得到了

// Create the window
var wnd = new SciterWindow();
wnd.CreateMainWindow(800, 600);
wnd.CenterTopLevelWindow();
wnd.Title = "Font Lister";

仅在Windows上,您可以从SciterWindow类派生一个新C#类,以覆盖ProcessWindowMessage()虚拟方法,从而处理Win32消息。

理论上,仅凭一个SciterWindow实例,您就可以通过调用LoadHtml()来加载和显示HTML页面,例如

wnd.LoadHtml("<html><body>Hello World!</body></html>");
wnd.Show();

但我们想深入了解并控制HTML页面加载的方方面面,这就是为什么我们需要一个SciterHost实例来封装HTML加载过程。

页面托管解释

SciterSharp库中的SciterHost类是控制托管HTML页面的生命周期中许多方面的一个中心组件。本质上,它允许您跟踪Sciter引擎生成的以下通知:

  • SC_LOAD_DATASC_DATA_LOADED允许您操作资源加载,并在下一节中进行解释。
  • SC_POSTED_NOTIFICATION是在调用SciterHost.PostNotification()方法后收到的;它在多线程场景中很有用,因为此消息始终在UI线程中收到;SciterHost提供了一个InvokePost()方法,您可以从工作线程使用它在UI线程中执行给定的委托。
  • SC_ATTACH_BEHAVIOR:一个将原生行为(SciterEventHandler实例)附加到DOM元素的请求。
  • SC_ENGINE_DESTROYED:在引擎被销毁之前,在主窗口关闭之后生成。

SciterHost类有相应的可重写方法供您处理这些通知。

此外,SciterHost允许您通过AttachEvh()方法附加一个窗口级事件处理程序。此处理程序在将事件分派到目标DOM元素之前接收页面中的每个事件。

实际上,Bootstrap通过Host类扩展了SciterHost,因此与托管相关的代码位于一个单独的类/文件中(Src/Host.cs)。

// Prepares SciterHost and then loads the page
var host = this;
host.Setup(wnd);
host.AttachEvh(new HostEvh());
host.SetupPage("index.html");
wnd.Show();

请注意,在加载HTML页面之前,我们必须先调用Setup()SciterWindowSciterHost关联起来,并且可以选择使用AttachEvh()调用附加一个窗口级SciterEventHandler,以便接收即将通过SetupPage()加载的页面的事件。

从打包数据加载资源

通过SC_LOAD_DATASC_DATA_LOADED通知,您可以跟踪和定制HTML页面请求的每个资源(HTML页面本身、图像、脚本、CSS)的加载,或者您可以忽略它并保留默认加载机制。

注意,在Host.cs文件中的BaseHost类中,Bootstrap代码通过重写OnLoadData()方法使用了自定义加载策略。

protected override SciterXDef.LoadResult OnLoadData(SciterXDef.SCN_LOAD_DATA sld)
{
    if(sld.uri.StartsWith("archive://app/"))
    {
        // load resource from SciterArchive
        string path = sld.uri.Substring(14);
        byte[] data = _archive.Get(path);
        if(data!=null)
            _api.SciterDataReady(_wnd._hwnd, sld.uri, data, (uint) data.Length);
    }
    return SciterXDef.LoadResult.LOAD_OK;
}

Sciter资源通过标准URL机制进行跟踪。我们正在做的是,如果资源URL以archive://app/(即协议)开头,我们就从我们的SciterArchive实例加载资源,该实例以BLOB打包格式保存我们所有的资源。这个BLOB包含在文件ArchiveResource.cs中,并且每次我们通过项目中已配置的预构建命令构建项目时都会重新创建。

SciterArchive BLOB加载仅在发布模式下进行。在调试模式下,资源直接从文件系统加载。为什么?因为我们的发布版本将包含所有打包在.exe文件中的资源,从而使部署更容易,但我们的调试版本通过从文件系统加载资源会更快。

BaseHost() BaseHost类中的静态方法负责区分调试发布模式的加载策略。

static BaseHost()
{
#if DEBUG
    _rescwd = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).Replace('\\', '/');
#if OSX
    _rescwd += "/../../../../../res/";
#else
    _rescwd += "/../../res/";
#endif

    _rescwd = Path.GetFullPath(_rescwd).Replace('\\', '/');
    Debug.Assert(Directory.Exists(_rescwd));
#else
    _archive.Open(SciterAppResource.ArchiveResource.resources);
#endif
}

要加载初始HTML页面,它将页面URL指定为:file:///archive://app/,具体取决于是在DEBUG还是RELEASE模式下编译。初始页面的URL定义了用于解析相对URL的基URL,也就是说,每个相对URL(如链接、图像、框架、CSS或脚本导入)都将使用预置的基URL进行解析。

Google 1 - 加载Google字体列表并将其传递给UI层。 “应用程序事件”约定(主机 -> UI

为了处理Google Fonts,我们创建了一个专用的C#类。整个类代码都在文件Data/GAPI.cs中,您可以从完成的示例.zip中复制。

这个类需要解决的第一个任务是通过Google API请求可用Google字体列表,并将此列表传递给UI层。由于此操作需要时间,因为它从互联网获取数据,因此您不希望此处理发生在主UI线程中,而是发生在后台线程中。这是实现它的代码:

public static void Setup()
{
    new Thread(() =>
    {
        var service = new WebfontsService
                      (new Google.Apis.Services.BaseClientService.Initializer()
        {
            ApiKey = API_KEY
        });

        var request = service.Webfonts.List();
        request.Sort = WebfontsResource.ListRequest.SortEnum.Popularity;

        _fontlist = request.Execute().Items;// if you get an Exception here,
                                            // please disable your Firewall!
        Debug.Assert(_fontlist.Count > 0);
        ...

确保您已将Google API密钥设置为API_KEY变量。返回的Google Fonts列表存储在一个static变量“_fontlist”中,因为我们稍后将需要该列表。

现在我们需要将此列表传递给UI层,以便向用户显示字体列表。

        ...
        // converts the Webfont list to JSON string
        string json = JsonConvert.SerializeObject(_fontlist);

        // converts the JSON string to SciterValue
        SciterValue sv = SciterValue.FromJSONString(json);

        // calls UI layer TIScript function with the font data
        App.AppHost.InvokePost(() =>
        {
            App.AppHost.CallFunction("View_OnFontList", sv);
        });
    }).Start();
}

在这里,您会找到“应用程序事件”交互原理/约定的用法,您可以在 此处 阅读更多关于它的信息。它非常简单:应用程序原生层只需调用脚本函数即可传递数据给UI层(主机 -> UI通信)。

这是通过App.AppHost.CallFunction()方法实现的,该方法传递全局脚本函数的名称(或命名空间内的函数)以及任意数量的SciterValue参数。但是,请注意,此代码将在后台Thread中执行。这是危险的,可能会导致数据损坏,因为我不确定TIScript是否100%线程安全。

最安全的方法是切换回UI线程,然后从那里调用脚本。这就是为什么使用App.AppHost.InvokePost(() => { ... })方法将在UI线程上下文中执行Lambda参数。

SciterValue类用于在C#中表示/创建TIScript数据。为了从IList<WebFont>获取我们的SciterValue,我们进行了一个小技巧转换。我们首先将IList<WebFont>转换为JSON字符串,然后使用SciterValue.FromJSONString(json)将JSON解析为新的SciterValue。效果非常好!

Google 2 - 下载并将Google字体保存为.zip包。“POST请求”约定(UI -> 主机 -> UI

我们的GAPI C#类所做的第二个任务是下载给定的字体家族(可能包含多个.ttf文件),将其打包成.zip文件,并将其保存在指定文件夹中。

处理它的方法非常直接且不言自明,只需阅读源代码。它的签名是:

public static void DownloadFont(string family, string savefolder) { ... }

我们传递字体名称以及保存.zip文件的路径。我们使用WebClient.DownloadFile()调用下载远程字体文件,这需要时间,因此DownloadFont()方法不应从UI线程调用,而应从工作线程调用。

将文件下载到临时文件夹后,.NET有一个不错的API,可以将文件夹中的文件打包成.zip文件,这是通过调用以下方法实现的:

ZipFile.CreateFromDirectory(tmppath, savefolder + "/" + family + ".zip");

(您必须添加对System.IO.Compression.FileSystem程序集的引用才能使用此API。)

字体下载发生在用户点击我们HTML页面上的下载按钮时,所以我们需要一种方法来检测这个UI事件以触发字体下载。下载发生时,我们不应阻止UI,并且我们需要一种方法在下载完成后通知UI。为此,我们使用“POST请求”约定(UI -> 主机 -> UI)。本质上,在这种约定中,UI请求原生层提供资源,并附带一个回调脚本函数,主机应在资源可用时调用该函数,从而将结果传递回UI层。

执行此操作的代码位于Host.cs文件中。

protected override bool OnScriptCall
(SciterElement se, string name, SciterValue[] args, out SciterValue result)
{
    result = null;
    switch(name)
    {
        case "Host_DownloadFont":
            string savefolder = args[0].Get("");
            string family = args[1].Get("");
            SciterValue async_cbk = args[2];

            Task.Run(() =>
            {
                bool res;
                try
                {
                    GAPI.DownloadFont(family, savefolder);
                    res = true;
                }
                catch(Exception)
                {
                    res = false;
                }

                if(async_cbk.IsUndefined())
                    return;// no callback provided

                App.AppHost.InvokePost(() =>
                {
                    async_cbk.Call(new SciterValue(res));
                });
            });

            return true;
    }
    return false;
}

在UI/HTML脚本代码中,当按下下载按钮时,我们将调用view.Host_DownloadFont(folder, family, function() { ... }),最终会转到我们Host类的OnScriptCall() C#处理程序。

引用

在TIScript中,每当您调用“view”变量的一个方法,并且此函数方法未定义时,Sciter引擎会调用附加到SciterHost的OnScriptCall()事件处理程序,使其有机会处理不存在的函数名。如果您从OnScriptCall处理程序返回“true”,则表示您已处理了调用,脚本VM不会触发“View (View([object View])) has no method - Host_DownloadFont”异常并停止执行脚本。

当脚本调用view.Host_DownloadFont(folder, family, function(res) {...})时,我们以SciterValue数组的形式接收给定的参数。从这个数组中,我们手动将前两个项转换为string,方法是使用args[0].Get(""),因此我们得到了savefolder和family string变量(您可以对所有基本C#类型进行此类转换,Get()方法具有int Get(0)bool Get(true)等重载)。我们将第三个SciterValue arg(脚本回调)保存为原始SciterValue变量“async_cbk”。

然后,我们的处理程序启动一个Task,该任务在后台线程上运行GAPI.DownloadFont()过程。请注意,它可能会失败,因为它依赖于互联网连接,因此它被包含在try/catch块中。当DownloadFont()完成时,我们得到一个布尔结果,我们将将其作为下载是否成功的标志传递回UI层。

与“应用程序事件”约定一样,我们需要切换回UI线程来调用一个script函数,方法是使用App.AppHost.InvokePost()。然后,我们只需使用SciterValueCall()方法调用脚本回调,并将布尔结果作为SciterValue传递。

现在您知道了,SciterValue是一个用于处理任何TIScript数据的多功能类

后端测试

我们的后端一切就绪。从我们的App.Run()方法开始,在我们的Host类调用SetupPage()加载HTML页面后,我们必须调用:

..
FontLister.Data.GAPI.Setup();
..

GAPI类中的此方法创建一个线程,该线程查询Google API以获取字体列表,然后通过调用脚本全局函数“View_OnFontList”(请参阅Google 1部分)将列表传递给UI层。

因此,为了测试一切是否正常工作,只需在GAPI.cs文件第46行设置一个断点,该行读取:

..
App.AppHost.CallFunction("View_OnFontList", sv);// -> BREAKPOINT here
..

并开始调试我们的应用程序。它应该命中断点,否则检查任何早期异常。因此,Google API的测试实际上就是按F5来调试我们的应用程序。

我们现在需要测试和调试我们的代码,就像它将从UI层调用一样。因此,我们将直接从C#模拟UI请求,这样您就可以学习如何在没有前端的情况下进行测试。

在这第46行之后,添加以下第47行:

App.AppHost.CallFunction("View_OnFontList", sv);// line 46
App.AppHost.EvalScript("view.Host_DownloadFont
(\"D:/\", \"Open Sans\", function(res) {})");// line 47

应该看起来像这样:

这到底在做什么?view.Host_DownloadFont是我们HostEvh事件处理程序类在本地处理的一个函数(参见Google 2部分),因此当您从TIScript调用此函数时,它最终会被C#代码处理。

我们正在通过EvalScript()从C#模拟此脚本调用。我们传递一个TIScript代码作为参数,该代码将在我们的SciterWindow的全局命名空间中执行。

这将调用我们在Google 2部分定义的Host_DownloadFont处理程序。请注意,我们正在向Host_DownloadFont传递3个参数:

  • “D:/”
  • “Open Sans”
  • function(res) {}

分别是:要保存.zip的目录、字体家族以及一个空的返回函数。

好的,现在您可以调试并查看一切是否正常工作。在刚添加的第47行设置一个断点,并在Host.cs文件第33行设置一个断点。按F5,它应该首先命中第47行,再次按F5,然后它应该命中第33行。现在您可以单步执行代码,直到达到GAPI.DownloadFont()。在此调用之后,等待片刻,然后检查您的D:驱动器上是否创建了文件“Open Sans.zip。我们刚刚模拟并测试了当用户下载给定字体家族的.zip时会发生什么。删除第47行。

3. 实战代码 -> 前端

现在我们进入一个全新的开发领域,涉及纯粹的HTML/CSS以及TIScript语言的脚本编写。我可能会说它比后端容易,只要您对HTML和CSS有一点经验。即使您是Web技术的专家,阅读本节也很有价值,因为Sciter的HTML/CSS支持有一些细微之处,标准浏览器不支持。TIScript与标准JS有很多不同,但它仍然是一种类似JavaScript的语言。

就像我们对后端所做的那样,我们可以完全独立于后端来编写和测试前端,因此不需要一个工作的后端。

HTML

这是我们需要替换的index.html内容的所有HTML:

<html>
<head>
</head>

<body>
	<div .warning>Loading Google fonts..</div>
	<div #list />
</body>
</html>

它是标准的HTML,直到您注意到一些细微之处。注意我们是如何声明<div>元素的:

<div #list />

这里有两个Sciter特定的地方您必须知道。首先,注意我们没有结束</div>标签。如果元素不包含子元素,您可以完全省略闭合标签,无论它是什么HTML标签。在浏览器中,<div>总是需要闭合标签。

其次,我们不需要用id="XYZ"属性声明元素ID,如<div id="list" />,我们可以简单地说#THE_ID,就像在CSS中编写ID选择器一样。您也可以对class属性这样做,所以<div class="list list-colored" />可以写成<div .list.list-colored />。通过使用与CSS相同的语法编写元素ID或类,这是一种非常巧妙的方法。Sciter支持更多属性快捷方式,参见此处

此外,在Sciter中,您可以使用任何标签名,例如<list-of-details></list-of-details>。但您还需要设置其display CSS属性,因此为了使list-of-details可见,需要执行以下操作:

list-of-details
{
     display: block;// or inline, inline-block, ..
}

CSS

为了样式化我们的页面,我们需要以下CSS <style> 标签,您应该将其放在 <head> 标签内。

<style>
	body
	{
		margin: 0;
		padding: 10px;
		flow: vertical;
		background: #eee;
		font-family: system;
	}

	li
	{
		padding: 10px;
		margin-bottom: 10px;
		border: solid 1px #BBB;
		box-shadow: 0px 1px 1px #EEE;
		background: white;
	}

	h1 { margin: 0; margin-bottom: 10px; }

	b { font-size: 18px; }
	b.success { color: green; }
	b.error { color: red; }
</style>

这里没有什么特别需要说的,它只是标准的CSS。这里唯一Sciter特定的东西是“flow”属性,您可以在 此处 阅读更多关于它的信息。本质上,它是CSS3 Flexbox布局的等价物,但它有不同的命名和行为。

flex”属性决定了元素内部子元素的定位方式。“vertical”值表示将元素垂直布局,一个接一个地排列成行,无论它是内联元素还是块级元素。

TIScript

脚本语言应该是在原生函数/组件与UI事件/元素之间的粘合剂。此外,脚本的一个主要目的是更新DOM的状态,允许您为页面添加动态性。

TIScript将不同的线程模型分开。HTML、CSS和脚本(UI后面的代码)基于异步资源加载。应用程序核心原生层由同步过程组成,用于为异步UI提供支持。因此,原生层很方便,因为它不受UI及其基于事件的线程模型束缚。

对于我们的应用程序,您需要将以下所有脚本代码添加到<head>标签中:

<script type="text/tiscript">
	function View_OnFontList(data)
	{
		$(.warning).remove();

		for(var item in data)
		{
			var el_item = self#list.$append(<li><h1>{item.family}
                          </h1><button>Download Font Family</button></li>);
			el_item.family = item.family;
		}
	}

	self.on("click", "button", function() {
		var el_btn = this;
		var folder = view.selectFolder();
		if(folder)
		{
			el_btn.state.disabled = true;
			el_btn.text = "Downloading..";

			var family = el_btn.$p(li).family;
			view.Host_DownloadFont(folder, family, function(res) {
				if(res)
					el_btn.$after(<b .success>Download completed with success!</b>);
				else
					el_btn.$after(<b .error>Error downloading!</b>);
				el_btn.remove();
			});
		}
	});
</script>

请注意,TIScript代码必须放在<script type=”text/tiscript”>标签之间,而不仅仅是<script>标签。

我将不解释代码,因为它超出了本文的范围。您可以在 此处 了解更多关于TIScript的信息,并查看 与JS的比较表。但是,JS开发人员通过阅读代码就能轻松理解它。然而,请注意与普通JavaScript的以下区别:

  • 字符串文字只允许使用双引号:“My string”是有效的TIScript字符串,“My string”是无效语法;
  • 名称以$开头的函数/方法是 stringizer函数
    • 参数被字面解释为单个string,字符‘(‘ ‘)’本身充当引号的作用。
    • 它通常用于接收HTML参数的函数中,例如:$append(<div>)
    • 或者接收CSS选择器的函数:$(div > span)
    • 在这里,您会注意到存在一个与JQuery类似的全局函数$,用于选择DOM元素,但语法稍好一些,无需将字符串参数括在引号中,这不是很聪明吗!?
  • self#list表达式是$(#list)的简写语法,self始终是对页面根元素<html>元素的引用。

使用sciter.exe进行前端测试

在现代浏览器中,我们有F12工具,允许我们检查已加载页面的DOM,甚至调试正在运行的JavaScript代码。Sciter的等价物是sciter.exe工具,正如我之前所说的。

要测试我们的前端,通过从Windows资源管理器中拖放文件到sciter.exe中打开index.html文件。您将看到一个永远显示的“正在加载Google字体..”消息。由于我们没有后端,它永远不会消失。但由于脚本的存在,我们可以轻松地模拟这两个层之间流动的数据。

以下代码足以模拟View_OnFontList()的调用,否则该调用将由我们缺失的C#原生后端调用:

View_OnFontList([
    { family: "Helvetica" },
    { family: "Open Sans" },
    { family: "Consola" }
]);

将此代码添加到您的<script>标签的末尾,回到sciter.exe,然后按重新加载按钮。就是这样。它应该显示一个包含3个字体的列表。您可以通过点击“齿轮”按钮来检查DOM,这将显示以下窗口:

我们可以轻松地看到我们的DOM树,其中包含3个<li>元素,代表传递给View_OnFontList()数据的每个字体。

测试/调试Sciter应用程序 + 远程检查HTML

示例应用程序代码现在已完成,因此我们希望测试和调试/运行完整的可执行文件。

如有必要,请在尝试本节之前下载完整的示例应用程序。

在VS中调试时,请确保打开输出窗口,因为Sciter会将消息打印到stdout,这些消息可能包括简单的警告,但更重要的是TIScript执行错误和无法识别的CSS规则(根据我的经验,这些是最常见的错误)。

但是要使其在C#调试器中工作,请确保启用原生调试,这样您将在输出窗口中看到Sciter错误消息:项目属性/调试/选中“启用原生代码调试”

F5,我们的Google字体下载器应用程序应该可以顺利运行。

在普通浏览器中,我们可以检查我们实时运行的HTML页面的DOM(F12工具)。我们也可以在Sciter中通过inspector.exe工具远程检查我们正在运行的C#进程。

SciterSharp提供了一种简单的方法来做到这一点(您可能想阅读此处有关涉及机制的更多信息)。

首先,您需要运行位于Sciter SDK(/bin/64/inspector.exe)中的inspector.exe

然后,在C#中,您只需调用SciterHost类的DebugInspect()方法。

// Prepares SciterHost and then load the page
var host = new Host();
host.SetupWindow(wnd);
host.AttachEvh(new HostEvh());
host.SetupPage("index.html");
host.DebugInspect();// >-- call it after SetupPage(), dont forget to run inspector.exe
..

它将打开Sciter检查器。打开后,在您的程序中,您可以按CTRL + SHIFT + 单击任何元素以在DOM树中选择它,如下所示。

最棒的是,它在Linux上也有效,为此您需要将SDK中的“inspector64”与可执行二进制文件一起添加。

4. 在Linux上编译和运行

首先,您需要在Linux系统上安装Mono和MonoDevelop,请在此处 查看说明

然后,只需在MonoDevelop中打开解决方案,右键单击FontListerGTK项目,选择“设置为启动项目”,然后尝试构建。希望它能按预期编译。

现在,如果您尝试运行,它可能会抛出“TypeInitializationException”和一个内部“DllNotFoundException: sciter-gtk-64.so”。这是因为Mono找不到Sciter DLL(.so)。您需要将其安装到您的Linux环境中。最简单的方法是 下载此脚本(示例.zip已在根文件夹中包含此文件),然后只需执行:

sudo bash install-libsciter.sh

我在Linux上遇到的另一个问题是Google Webfonts API抛出异常。幸运的是,我在 此处 找到了一个解决方案,它帮了我大忙。

附录:Sciter技巧

全局配置

这些是一些全局配置,您可以从C#代码中为Sciter引擎设置

SciterX.API.SciterSetOption() API提供了许多选项。我有时会使用的一个选项是更改SCITER_SET_GFX_LAYER选项,它允许您设置GFX后端(Windows上默认是Direct2D)。为什么?因为在旧计算机上,引擎经常由于旧的显示硬件/驱动程序而崩溃,因此您可以切换到非加速GFX模式,将此选项设置为GFX_LAYER_WARP,它是基于CPU的图像渲染。

SciterX.API.SciterSetOption(IntPtr.Zero, SciterXDef.SCITER_RT_OPTIONS.SCITER_SET_GFX_LAYER,
                            new IntPtr((int) SciterXDef.GFX_LAYER.GFX_LAYER_WARP));
  • 另一个API允许您将CSS内容附加到引擎创建的所有HTML页面。请注意,许多脚本函数会创建新的HTML页面,例如view.msgbox(),并且您无法直接控制这些页面的样式,因此此API可能很有用。

要将所有页面的背景设置为银色,您可以调用:

string css = "html { background: silver; }";
byte[] bytes = Encoding.UTF8.GetBytes(css);
SciterX.API.SciterAppendMasterCSS(bytes, (uint) bytes.Length);

原生C# DOM操作

在本文中,我们没有使用它,但SciterSharp库有一个完整的API,可以通过SciterElement类来操作页面DOM。与SciterValue类似,SciterElement是一个用于任何DOM操作的多功能类:创建元素、遍历DOM树、更改元素状态/属性/CSS等等……

这是一个简单的示例,演示如何将<h1>标题附加到页面并将其CSS颜色设置为blue

// get the page <body>
var se_body = wnd.RootElement.SelectFirst("body");

// append a <h1> header to it
se_body.TransformHTML("<h1>Wow, this header was created natively!</h1>",
                        SciterXDom.SET_ELEMENT_HTML.SIH_INSERT_AT_START);

// set <h1> color to blue
se_body[0].SetStyle("color", "#00F");

TIScript:全局数据/通信视图

有时需要一种方法来共享不同HTML页面之间的数据,例如,不同的<frame>页面,甚至进行多个Sciter窗口之间的通信。这是可能的,因为同一进程中的Sciter窗口共享相同的TIScript VM。

您可以通过“View”类的属性来实现这一点。以下赋值:

View.CreateAlert = function() {
     self.$append(<div .alert />)
}

..使得View.CreateAlert()全局可用,因此您可以从任何页面调用它。

您也可以想共享数据

View.standardAlerts = {
     success: "You did it, I knew you could!"
     warning: "Hey, you were advised",
     error: "Huston, we have a problem!"
};

资源加载:通过“sciter:debug-peer.tis”将TIScript内容注入每个页面

这是一个非常有用的技巧。它将TIScript内容注入引擎加载的每个HTML页面。

首先,您需要确保在创建SciterWindow时,将SciterXDef.SCITER_CREATE_WINDOW_FLAGS.SW_ENABLE_DEBUG传递给CreateMainWindow()。请注意,如果您仅传递窗口大小来调用它,则默认情况下它将获取此标志,因此以下调用是OK的:

wnd.CreateMainWindow(800, 600);

SW_ENABLE_DEBUG允许您使用inspector.exe工具远程检查页面。为了做到这一点,引擎会将文件“sciter:debug-peer.tis”注入到所有加载的页面中。我们可以通过处理SC_LOAD_DATA通知来欺骗引擎,自定义“sciter:debug-peer.tis”文件的内容,使其加载我们给定的TIScript内容。为此,在您的SciterHost派生类中,向OnLoadData()重写方法添加以下内容:

if(sld.uri=="sciter:debug-peer.tis")
{
    string TIScript_content = "function Meow() { stdout.println(1234); }";
    byte[] buffer = Encoding.UTF8.GetBytes(TIScript_content);
    _api.SciterDataReady(_wnd._hwnd, sld.uri, buffer, (uint)buffer.Length);
}

这将使函数Meow()在所有页面中全局可用。请注意,您可以添加任何TIScript代码,因此您可以例如添加更改页面DOM的函数代码,或者使用self.url()检查加载的页面URL并执行某种自定义过程。

请注意,还有其他方法可以实现这一点,这些方法在此 进行了描述。

Sciter中可以做到的酷炫事情

可能性是无限的,这里我只提供一些链接,展示您可以用Sciter实现的功能

结语

这篇文章最终变得非常长,尽管它涵盖了理解C# Sciter桌面应用程序所涉及的所有基本要素。希望阅读愉快。

我的下一个主要目标是完成SciterSharp对OSX的支持,并提供一个关于如何在该平台上使用的教程。敬请关注!

© . All rights reserved.