Posted on :: Updated on :: Tags: ,

LAF 是著名的像素画绘画软件 Aseprite 使用的库,使用的语言是 C++。在此之前,我听说过的 C++ 桌面应用程序库很少,所以今天就来试一试,编译一下所给的例子。我不熟悉 CMake,所以配置过程会记录得比较详细,方便自己以后查阅。

安装依赖

  • Skia。LAF 和 Aseprite 使用的版本是这个。我使用的是 Windows 系统,所以下载 Skia-Windows-Release-x64.zip,并解压到 D:\skia 中。
  • Ninja。可以去其官网下载。Windows 上可以用 scoop 安装:scoop install ninja

在 Visual Studio 中配置和编译

克隆 LAF 的仓库。注意,LAF 的仓库里包含子模块,克隆时要加上 --recursive 选项一并克隆:

git clone --recursive https://github.com/aseprite/laf

在 Windows 上写 C++ 项目,用 Visual Studio 会比较方便。用 Visual Studio 打开 laf 文件夹。选择「项目」->「laf 的 CMake 设置」,点击右上角的「编辑 JSON」打开 CMake 配置文件 CMakeSettings.json,在 x64-Debug 配置的 cmakeCommandArgs 字段中加上下面的内容,以指定 Skia 的位置:

-DLAF_BACKEND=skia  -DSKIA_DIR=D:\\skia  -DSKIA_LIBRARY_DIR=D:\\skia\\out\\Release-x64

保存 CMakeSettings.json,Visual Studio 会自动执行一次 CMake。输出中可以看到下面的信息:

[CMake] -- skia dir: D:/skia
[CMake] -- skia library: D:/skia/out/Release-x64/skia.lib
[CMake] -- skia library dir: D:/skia/out/Release-x64
[CMake] -- Configuring done (0.7s)
[CMake] -- Generating done (0.4s)
[CMake] -- Build files have been written to: D:/source/laf/out/build/x64-Debug
已提取 CMake 变量。
已提取源文件和标头。
已提取代码模型。
已提取工具链配置。
已提取包含路径。
CMake 生成完毕。

然后在「解决方案资源管理器」的顶部找到「房子图标」右边的「左下角带有 Visual Studio LOGO 的列表图标」,点击它,下面会出现「文件夹视图」和「CMake 目标视图」。默认的视图是「文件夹视图」,现在我们双击「CMake 目标视图」,就可以看到 VS 自动生成的所有 CMake 目标,里面大部分都是测试,今天要编译的是 laf-examples 目标。找到它,右键选择「生成 laf-examples」。如果顺利的话,你会看到 complextextlayout.cpp 中报了很多错误。我把其中的阿拉伯文删除后,仍然无法解决,只好打开 examples/CMakeLists.txt,注释掉 laf_add_example(complextextlayout GUI),放弃编译这个示例。

然后重新尝试生成。接下来迎接我们的是 13624 个 LNK2038 错误,其中大部分说的是「值 0 不匹配值 2」。经查阅发现是在 Debug 模式下使用了 Release 版本的静态库所致。aseprite/skia 的 Releases 页面中只提供了 Release 版本的静态库,如果要用 Debug 版本的话,还要自己用源代码编译一遍,今天就先不折腾了。再次打开 CMake 配置文件 CMakeSettings.json,把 x64-Debug 配置复制一份,然后把 name 字段改成 x64-ReleaseconfigurationType 字段改成 Release(等效于手动使用 CMake 时添加 -DCMAKE_BUILD_TYPE=Release 选项)。然后在「绿色三角形调试按钮」左边的下拉框中选择 x64-Release。重新尝试生成即可。

示例分析

examples 中提供了很多示例,包括悬浮窗、文件拖拽甚至着色器等示例,展示了 LAF 比较完善的功能。这次先从最基础的 hello_laf.cpp 开始,看看怎么用 LAF 创建一个桌面程序。

可以看到,程序的入口是 int app_main(int argc, char* argv[])。(需要提醒的是,函数签名中的 argcargv 参数,以及最后的 return 0 都是必须的,不要像平常一样因为偷懒省掉。)上来之后,先 make 一个 System 对象 systemSystem 类中提供了创建窗口等接口),然后用它设置应用模式为 GUI 模式,创建一个 400 × 300 大小的窗口 window。接下来设置标题、鼠标指针(非必须):

os::SystemRef system = os::make_system();
system->setAppMode(os::AppMode::GUI);

os::WindowRef window = system->makeWindow(400, 300);
window->setTitle("Hello World");
window->setCursor(os::NativeCursor::Arrow);

然后设置窗口大小变化时的回调函数,直接设置为窗口绘制的函数 draw_window 即可。finishLaunchingactivateApp 照抄即可,想了解的话可以参考代码注释中给出的说明。

system->handleWindowResize = draw_window;
system->finishLaunching();
system->activateApp();

接下来是主循环。创建一个 EventQueue 对象,然后在循环中等待事件(鼠标、键盘等)发生。注意在循环中使用的是 EventQueuegetEvent 方法,这个方法会一直等待到有事件发生,也就是说如果你什么也不做的话,之后的代码就不会执行。与之相对的是 queueEvent 方法,如果没有事情发生的话,就会得到一个 Event::None 类型的事件。(至于注释里面提到的 true 参数,我反正是没有看到有什么 true 参数,可能是改了代码忘记改注释了吧 (;_:),还真是如 README.md 中所说「API 尚未稳定」。)

这个示例使用的是懒惰更新策略,在主循环中维护了一个 redraw 变量,只在需要更新窗口内容时,将这个变量设置为 true,重新绘制一次窗口。

然后我们来看窗口绘制函数 draw_window。首先获取窗口的 surface 对象,接下来的绘制都是在这个 surface 上,用一个 Paint 对象进行。注意,和平常画画一样,绘制之前需要先把画板(窗口)清理干净:

p.color(gfx::rgba(0, 0, 0));
p.style(os::Paint::Fill);
surface->drawRect(rc, p);

否则窗口内容更新时,重新绘制的内容会叠在旧内容的上面。

绘制的操作都很容易理解,这里说一下 draw_text 函数,要绘制中文字符的话,不能直接传中文字符串,而是要先转换成 UTF-8 编码:

os::draw_text(surface, nullptr, base::to_utf8(L"你好世界"), gfx::Point(10, 20), &p, os::TextAlign::Left);

最后这三句也是先照抄即可:

if (window->isVisible())
    window->invalidateRegion(gfx::Region(rc));
else
    window->setVisible(true);
window->swapBuffers();