Sciter - 多平台嵌入式 HTML/CSS/脚本 UI 引擎
Sciter 嵌入基础知识,Sciter SDK 结构概览。
引言
Sciter 是一个用于渲染现代桌面应用程序 UI 的 HTML/CSS/脚本引擎。它是一个紧凑的、单一的 dll/dylib/so 文件(4-8 mb),引擎本身没有任何额外的依赖。它支持 Microsoft Windows (XP 及以上)、Apple OS X (v10.7 及以上) 和 Linux/GTK (GTK v3.0 及以上)。Sciter 在现代 Windows 版本上使用 Direct2D GPU 加速图形,在 XP 上使用 GDI+。在 OS X 上,它使用标准的 CoreGraphics 原始图形,而 Linux 版本则使用 Cairo。
背景
Sciter 自 2006 年以来已被 各公司 在生产中使用。目前,Sciter 驱动的 UI 在全球超过 1.2 亿台 PC 和 Mac 上运行。因此,您很可能已经在您的机器上拥有某个应用程序在后台使用了 Sciter。
Sciter 使用 HTML5 元素集,完全实现了 CSS level 2.1,以及 CSS level 3 中最受欢迎的功能。它还包含一些自定义 CSS 扩展,这些扩展是为了支持桌面 UI 情况而必需的。例如,flex 单位和各种布局管理器。Sciter 是 HTMLayout 的下一个主要版本,增加了硬件加速、多平台支持和 TIScript。
在本文中,我将介绍一个使用相同源代码并在所有支持的平台上运行的简单应用程序。
这个例子是 官方 Sciter SDK 分发版 的一部分,该分发版可在 此处 获得。源代码附在本文章中,您也可以在 sciter-sdk/demos/ulayered/ 文件夹中找到它。SDK 还包括此示例的每个平台的编译可执行文件,因此您可以立即进行尝试。
Sciter 时钟应用程序
我们的应用程序将是一个时钟,它将在所谓的“分层窗口”中渲染。所有现代操作系统都支持“alpha 感知”窗口,因此我们可以拥有任意形状的 UI:
源代码/项目布局
Sciter 时钟项目结构
- /builds/ - 包含 Visual Studio、XCode 和 Code::Blocks 项目文件。
- /res/ - 应用程序 UI 资源 - HTML、CSS、脚本和图像通常在这里。
- /res/default.htm - 主标记文件,也是唯一的标记文件。
- /res/analog-clock.tis - 脚本组件,即负责绘制时钟指针等的“行为”。
- /res/movable-view.tis - 脚本代码,允许通过鼠标拖动窗口在桌面表面上移动。
- /pack-resources.bat - 批处理文件,调用 sciter-sdk/bin/packfolder.exe 工具将 /res/ 文件夹的内容压缩到 resources.cpp 中。
- /resources.cpp - 包含从 /res/ 文件夹压缩到
unsigned char resources[] = { ... };
块中的资源。 - /ulayered.cpp - 定义 uimain() 函数的原生代码,该函数创建应用程序窗口。
原生代码
Sciter 时钟是一个相当简单的应用程序,因此其原生代码也同样简单。
ulayered.cpp
#include "sciter-x-window.hpp"
static RECT wrc = { 100, 100, 800, 800 };
class frame: public sciter::window {
public:
frame() : window( SW_MAIN | SW_ALPHA | SW_POPUP, wrc) {}
// define native functions exposed to the script:
BEGIN_FUNCTION_MAP
FUNCTION_0("architecture", architecture);
END_FUNCTION_MAP
int architecture() {
// this function is here just for the demo purposes,
// it shows native function callable from script as view.architecture();
#if defined(TARGET_32)
return 32;
#elif defined(TARGET_64)
return 64;
#endif
}
};
#include "resources.cpp" // packed /res/ folder
int uimain(std::function<int()> run ) {
sciter::archive::instance().open(aux::elements_of(resources)); // bind resources[] (defined in "resources.cpp") with the archive
frame *pwin = new frame(); // will be self destroyed on window close.
// note: this:://app URL is dedicated to the sciter::archive content associated with the application
pwin->load( WSTR("this://app/default.htm") );
pwin->expand();
return run();
}
它定义了一个 class frame
,它是 sciter::window
类的特化。我们的 frame 包含一个原生的 frame::architecture()
函数, intended to be called from script as view.architecture()
。 任何其他需要的原生函数都应包含在 BEGIN_FUNCTION_MAP/
END_FUNCTION_MAP
脚本<->原生绑定块中。
这里的主要关注点是 uimain()
函数。它做了三件主要的事情:
- 将打包的资源块与 sciter::archive 实例绑定。当引擎通过 SC_LOAD_DATA 通知请求资源时,sciter::archive 实例会提供资源。有关更多详细信息,请参阅
sciter-sdk/include/sciter-x-host-callback.h
文件中的sciter::host::on_load_data()
实现。您的应用程序可以以任何其他方式存储资源,sciter::archive 只是其中一种可能的方式。 - 创建窗口实例并指示它加载
this://app/default.htm
标记文件(参见 /res/default.htm)。 - 显示窗口并运行所谓的“消息循环”——即作为参数传递的
std::function<int()> run
。每个平台都有自己的定义“main”函数的方式,所以您需要在项目中包含其中一个:- sciter-sdk/include/sciter-win-main.cpp - 在 Windows 上
- sciter-sdk/include/sciter-osx-main.mm - 在 OS X 上
- sciter-sdk/include/sciter-gtk-main.cpp - 在 Linux 上
请注意,ulayered.cpp 在所有平台上都按原样使用。您唯一需要的平台相关文件是上述三个文件之一。如果您的其余代码使用通用的 C++ 库和 std、boost、POCO 等函数,那么 C++ 和 Sciter 允许您编写一次代码,它将在所有流行的桌面操作系统上运行。
UI 标记
我们的标记 (res/default.htm) 也相当简单:
<html>
<head>
<title>Sciter clock</title>
<style>
... see below ...
</style>
<script type="text/tiscript">
... see below ...
</script>
</head>
<body>
<header><span #platform /> <span #arch />bit time</header>
<footer>
<button #minimize>Minimize Window</button>
<button #close>Close Window</button>
</footer>
</body>
</html>
除了像 <button #minimize>
这样的东西之外,其他一切都一目了然,它是 <button id="minimize">
的简写形式。我扩展了 Sciter 中的 HTML 解析器以支持此功能以及其他常用的 UI 结构。
CSS,<style>...</style> 部分的内容。
通常,您会将样式包含在单独的文件中。为了简洁起见,在此示例中我将其内联。
html { background:transparent; } // the window is transparent
body
{
prototype: Clock url(analog-clock.tis); // will draw clock in between background and content layers
border-radius:50%;
border:3dip solid brown;
background:gold;
margin:*; // flex margins will move the body in the mddle of the root
size:300dip;
flow:vertical;
transform:scale(0.1); // initially it is small - collapsed to center
overflow:hidden;
font-size:10pt;
font-family: "Segoe UI", Tahoma, Helvetica, sans-serif;
}
body.shown
{
transform:scale(1);
transition: transform(back-out,600ms); // initial show - expanding animation
}
body.hidden
{
transform:scale(0.1);
transition: transform(linear,600ms); // closing animation
}
body > header { text-align:center; color:brown; margin-top:36dip; font-weight:bold; }
body > footer { flow:vertical; margin-top:*; margin-bottom:20dip; }
body > footer > button { display:block; background:transparent; margin:8dip *; border: 1px solid brown; border-radius:4dip; }
body > footer > button:hover { background-color:white; transition: background-color(linear,300ms); }
这基本上是标准的 CSS,除了两件事。首先,这个规则/属性:
body {
prototype: Clock url(analog-clock.tis);
}
告诉以下内容:“<body>
DOM 元素应该派生自(子类化)class Clock
,该类位于 analog-clock.tis
文件中”。Class Clock 组件(也称为“行为”)包含使用 图形原始对象 绘制时钟指针和标记的脚本代码。Sciter 提供了两种 CSS 机制用于声明式的元素到脚本绑定。有关详细说明,请参阅Sciter。通过 CSS 进行声明式行为分配:“prototype”和“aspect”属性文章。
其次,这个结构
body {
margin:*;
}
在 body 元素上定义了灵活的边距。本质上,它在 body 元素的侧面放置了 4 个“弹簧”,将其移动到容器(窗口)的中心。
您可以在我提交给 W3C/CSS WG 的 Flexible Flow 提案中阅读有关 Sciter 使用的 flex 单位和布局管理器。
脚本,<script type="text/tiscript">...</script> 部分的内容
这个应用程序的脚本也相对简单:
include "moveable-view.tis";
const body = $(body);
self.ready = function() // html loaded - DOM ready
{
view.caption = "Sciter Clock";
// positioning of the window in the middle of the screen:
var (sx,sy,sw,sh) = view.screenBox(#workarea,#rectw); // gettting screen/monitor size
var (w,h) = self.$(body).box(#dimension);
w += w/2; h += h/2; // to accomodate expanding animation
view.move( sx + (sw - w) / 2, sy + (sh - h) / 2, w, h);
body.timer(40ms, function() { body.attributes.addClass("shown") });
$(span#platform).text = System.PLATFORM;
$(span#arch).text = view.architecture(); // calling native function defined in ulayered.cpp
}
// <button #close> click handler
$(#close).onClick = function()
{
body.onAnimationEnd = function() { view.close(); };
body.attributes.removeClass("shown");
}
// <button #minimize> click handler
$(#minimize).onClick = function()
{
view.state = View.WINDOW_MINIMIZED;
}
// setup movable window handler:
movableView();
有两个按钮点击处理程序,以及 DOM 就绪处理程序,该处理程序在启动后 40 毫秒触发初始扩展动画 (body.timer(40ms, function() { ... });
)。
可能最有趣的脚本代码在 /res/analog-clock.tis 中(由 CSS 中的 prototype 语句包含)
class Clock: Behavior
{
function attached() {
this.paintForeground = this.drawclock; // attaching draw handler to paintForeground layer
this.timer(300ms,::this.refresh());
this.borderWidth = this.style["border-width"] || 3;
this.borderColor = this.style["border-color"] || color("brown");
}
function drawclock(gfx)
{
var (x,y,w,h) = this.box(#rectw);
var scale = w < h? w / 300.0: h / 300.0;
var now = new Date();
gfx.save();
gfx.translate(w/2.0,h/2.0);
gfx.scale(scale,scale);
gfx.rotate(-Math.PI/2);
gfx.lineColor(color(0,0,0));
gfx.lineWidth(8);
gfx.lineCap = Graphics.CAP_ROUND;
// Hour marks
gfx.save();
gfx.lineColor(color(0x32,0x5F,0xA2));
for (var i in 12) {
gfx.rotate(Math.PI/6);
gfx.line(137,0,144,0);
}
gfx.restore();
// Minute marks
gfx.save();
gfx.lineWidth(this.borderWidth);
gfx.lineColor(this.borderColor);
for (var i in 60) {
if ( i % 5 != 0)
gfx.line(143,0,146,0);
gfx.rotate(Math.PI/30);
}
gfx.restore();
var sec = now.second;
var min = now.minute;
var hr = now.hour;
hr = hr >= 12 ? hr-12 : hr;
// draw Hours hand
gfx.save();
gfx.rotate( hr*(Math.PI/6) + (Math.PI/360)*min + (Math.PI/21600)*sec )
gfx.lineWidth(14);
gfx.line(-20,0,70,0);
gfx.restore();
// draw Minutes hand
gfx.save();
gfx.rotate( (Math.PI/30)*min + (Math.PI/1800)*sec )
gfx.lineWidth(10);
gfx.line(-28,0,100,0);
gfx.restore();
// draw Seconds hand
gfx.save();
gfx.rotate(sec * Math.PI/30);
gfx.lineColor(color(0xD4,0,0));
gfx.fillColor(color(0xD4,0,0));
gfx.lineWidth(6);
gfx.line(-30,0,83,0);
gfx.ellipse(0,0,10);
gfx.noFill();
gfx.ellipse(95,0,10);
gfx.restore();
gfx.restore();
}
}
Sciter 支持所谓的即时绘图模式。如果您将 DOM 元素视为一个窗口 (HWND),那么即时模式绘图就是处理 WM_PAINT 窗口消息的代码。每次需要绘制某个表面区域时都会调用它。
上面的 function attached()
在 DOM 元素(此处为 <body>)被 class Clock
实例“子类化”时被调用。此语句
this.paintForeground = this.drawclock;
将 drawclock()
函数安装为该元素的“前景层”绘图处理程序,而这个
this.timer(300ms,::this.refresh());
导致元素每 300 毫秒刷新一次。因此,时钟表面每 300 毫秒重绘一次。 ::this.refresh()
是 TIScript 中的一个lambda 函数声明。
Sciter SDK 结构简要概览
公共 Sciter SDK 包含以下文件夹:
- bin、bin.osx 和 bin.gtk - 包含已编译 Sciter 引擎的文件夹:sciter32/64.dll (Windows)、sciter-osx-64.dylib (OS X)、sciter-gtk-64.so (Linux) 和 sciter.exe 变体——演示“浏览器”,内置 DOM 检查器、脚本调试器和 Sciter 文档浏览器(见上图)。还有来自 demo 文件夹的示例的编译版本。
- include - 定义公共 Sciter 引擎 API 的 C 和 C++ 头文件:窗口级别函数、DOM 访问方法和实用程序。
- demos、demos.osx、demos.gtk - 演示 Sciter 嵌入各种方面的演示项目/应用程序。
- doc - HTML 格式的文档,可以在常规浏览器和内置帮助查看器中查看。
- samples - HTML/CSS/脚本的现成代码片段和示例,演示了 Sciter 的各种功能。
公共 Sciter SDK 中包含的库和示例
- samples/+plus - 这是一个类似于 AngularJS 的数据绑定库。代码量小 (480 LOC),且不具侵入性的 Model-View-Whatever 库。
- samples/+lib - 类似于 underscore.js 的原始对象。
- samples/+promise - Promises/A+ 规范的实现。
- samples/+query - 基本 jQuery/Zepto 功能的移植。大多数基础 jQuery 功能都在 Sciter 中原生实现,因此这个库相当紧凑——700 LOC。
- samples/+lang - i18n(国际化)原始对象。
- samples/+vlist - 虚拟列表、网格库和示例。当您需要浏览大量记录时使用。+vlist 使用实时数据绑定机制。只需提供一个记录数组 [] 和一个类似于 AngularJS 的可重复模板。
- samples/animations - 动画框架库和演示,类似于 GreenSock.js 动画平台 (GSAP)。
- samples/animated-png - Animated PNG 演示。
- samples/animations-transitions-css - 基于 CSS 的过渡。历史上,Sciter 使用略有不同的 CSS 过渡定义语法,但功能集与 CSS3 Transitions 模块相似。
- samples/basics - 基本 CSS 示例,包括 CSS3 transform 属性。
- samples/communication - AJAX/JSON 客户端、WebSockets 和 DataSockets 双工内/内部网络通信。
- samples/css++ - 演示 Sciter 中引入的各种 CSS 扩展。
- samples/dialogs+windows - 演示 View.window、View.dialog 和 View.msgbox 功能:通过 HTML/CSS/脚本定义的桌面窗口。
- samples/drag-n-drop-manager - 拖放管理器。
- samples/effects.css++ - transition:blend 和 transition: slide-xxx 演示 - Sciter 特定的过渡扩展。
- samples/font-@-awesome - 使用 FontAwesome 集成的 CSS3 @font-face 功能演示。
- samples/forms - 演示 Sciter 扩展的 <input> 部件集,包括
<select type=tree>
、<input type=number>
、<input type=masked>
以及许多其他。 - samples/goodies - Sciter 附加功能,包括
behavior:file-icon
- 外壳图标渲染。 - samples/graphics - Graphics 类的使用——即时和缓冲绘图原始对象,包括“render-element-to-bitmap”和“dynamic-CSS-background-image”功能。Sciter 中的 Graphics 是浏览器中<canvas>功能的超集。
- samples/ideas/ - 一堆实现思路,包括:
- callout - 动态呼出
- carousel
- KiTE - 类似于 {{mustache}} 的模板引擎.
- lightbox-dialog -Lightbox 窗口内模态对话框。
- moveable-windows - Sciter 支持所谓的“浮动 DOM 元素”——在单独窗口中渲染的元素。该示例演示了此功能。
- tray-notifications - 通过 HTML/CSS 定义的系统托盘通知。
- virtual-list - 另一个虚拟列表,具有动量滚动,支持无限数量的可变高度项目(见右图)。
- samples/image-map - “人类的图像目录”和 DPI 感知图像。
- samples/image-transformations.css++ - 另一个 Sciter 特定的 CSS 功能:图像滤镜。
- samples/menu - 在 HTML 中定义的、由 CSS 样式化的真实菜单。
- samples/popup - “浮动”DOM 元素的另一个演示——弹出窗口。
- samples/replace-animator - 不同布局之间动画过渡的演示。
- samples/richtext - <richtext> 部件的演示——WYSIWYG HTML 编辑器。
- samples/scrollbars-n-scrolling - 滚动条和滚动方式样式。
- samples/selection - DOM 树上文本/范围选择的演示。
- samples/sqlite - SQLite 集成演示 (tiscript-sqlite.dll)。
- samples/svg - 演示 Sciter 中扩展的 SVG 支持。
- samples/tooltips++ - 提示/调用提示定义和样式的演示(见右图)。
- samples/video - Sciter 中的<video>播放。
- samples/xml - 使用基于 XML 分词器 的内置 XMLParser 处理 XML。
历史
文章:Sciter 的十年之路。
HTMLayout CodeProject 文章。概念上,Sciter 是 HTMLayout 的下一个主要版本。Sciter API 是 HTMLayout API 的超集,因此其中定义的嵌入原理也适用于 Sciter。
Sciter 论坛和讨论
- 主要 Sciter 讨论 (英语);
- RSDN.RU 上的 Sciter (俄语);
- Sciter 中文;
- 在这里 CodeProject 上。