使用 Slint C++ Platform API 将界面嵌入原生 WIN32 应用:platform_native 示例深度解析
【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint
导读
examples/cpp/platform_native 是 Slint 官方提供的一个 C++ 平台集成示例,它演示了如何完全不依赖 Qt 等框架,仅用 Win32 API 编写应用外壳,并通过slint::platform::Platform与WindowAdapter把 Slint 界面“嵌入”到一个原生 Windows 窗口里。读完本文,你将掌握 Slint C++ Platform API 的核心抽象(Platform、WindowAdapter、AbstractRenderer)、消息循环与渲染的接线方式,以及把 Slint 界面集成进既有桌面程序的完整套路。
一、示例解决什么问题
Slint 是一个开源声明式 GUI 工具包,其 C++ 绑定默认自带基于 winit 的跨平台窗口后端。但在以下场景中,默认后端并不适用:
- 应用已经有自己基于 Win32 API 编写的窗口与消息循环,不希望引入第二套窗口系统;
- 应用是插件架构,Slint 界面只作为其中一个视图存在;
- 需要对窗口生命周期、输入事件、渲染时机做完全自主的控制。
platform_native示例就是为了回答这个问题而存在的:如何把 Slint 渲染进一个由 Win32 API 亲手创建的 HWND 之中。示例 README 明确说明了四个文件的分工:
| 文件 | 职责 |
|---|---|
| main.cpp | 使用原生 Win32 API 编写的应用外壳(消息循环 + 主窗口) |
| appview.h | 应用与 Slint 界面之间的抽象接口,其实现甚至可以做成插件 |
| appview.cpp | 接口的实现:注册自定义 Platform 并实例化 Slint UI |
| windowadapter_win.h | 基于 Win32 API 实现 SlintWindowAdapter的胶水代码 |
在 examples/cpp/README.md 的对比表中,platform_native被定位为:不需要 Qt、不支持运行时动态加载.slint文件、仅面向 Windows 平台。与之相对,platform_qt需要 Qt 但跨平台,qt_viewer则可通过slint::interpreter动态加载 UI。
二、核心抽象:Platform 与 WindowAdapter
在深入代码之前,先理解 Slint C++ Platform API 的两个核心类型,它们的定义都在 api/cpp/include/slint-platform.h:
slint::platform::Platform:一个“窗口适配器工厂”。应用必须自定义它的子类,实现纯虚函数create_window_adapter(),然后在创建任何 Slint 窗口之前调用slint::platform::set_platform()完成注册。API 文档对此的约束是:set_platform()必须在创建任何 Slint 句柄之前调用,且整个应用只能调用一次(见 slint-platform.h 中 Platform 类定义)。slint::platform::WindowAdapter:代表一个具体的 Slint 窗口。子类需要持有渲染器(SoftwareRenderer或SkiaRenderer),并实现renderer()、size()、set_visible()、request_redraw()等接口,把 Slint 的“窗口语义”翻译成目标平台的窗口操作。
此外,Platform还预留了一系列可覆盖的钩子:run_event_loop()/quit_event_loop()(自定义事件循环)、clipboard_text()/set_clipboard_text()(剪贴板读写)、run_in_event_loop()(跨线程投递任务)等。platform_native示例没有重写它们,而是复用 Win32 主线程消息循环并直接接管输入事件,这正是它展示的重点。
三、应用外壳:纯 Win32 的 main.cpp
main.cpp 是一个标准的 Win32 程序骨架,完全不含任何 Slint 相关代码(除了包含appview.h)。
3.1 窗口类注册与主窗口创建
WinMain中完成窗口类的注册与主窗口创建:
WNDCLASS c = { 0 }; c.lpfnWndProc = (WNDPROC)WindowProc; c.hInstance = hinst; c.hIcon = LoadIcon(0, IDI_APPLICATION); c.hCursor = LoadCursor(0, IDC_ARROW); c.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); c.lpszClassName = "MainWindow"; RegisterClass(&c); HWND h = CreateWindow("MainWindow", "WindowTitle", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, hinst, 0); ShowWindow(h, show);窗口背景色被设为白色画刷(WHITE_BRUSH),这是因为 Slint 内容会作为子窗口单独绘制,主窗口本身只是一个容器。
3.2 窗口过程:Slint 界面与原生控件共存
WindowProc展示了如何让原生 Win32 控件与 Slint 界面同时存在于一个窗口中:
WM_CREATE:先用CreateWindow创建一个原生 Win32 按钮(BUTTON类),随后创建AppView并调用app->attachToWindow(h)把 Slint 界面挂接到主窗口上;WM_SIZE:把主窗口去掉顶部 40 像素(原生按钮占用的区域)后,把剩余区域交给 Slint 界面:app->setGeometry(0, 40, width, height - 40);WM_COMMAND:原生按钮被点击时销毁AppView并PostQuitMessage(0)退出;WM_CLOSE:同样销毁AppView并退出。
注意这里app是一个std::unique_ptr<AppView>,销毁AppView即撤销自定义 Platform 与 WindowAdapter,这也是一种安全的退出清理方式。
3.3 消息循环
示例的消息循环使用PeekMessage轮询而非GetMessage阻塞:
while (1) { /* or while(running) */ MSG msg; while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) return (int)msg.wParam; TranslateMessage(&msg); DispatchMessage(&msg); } }这种“非阻塞轮询”写法在嵌入场景很典型:它保证在无消息时 CPU 仍可被占用(或通过GetMessage+ 定时器改造为阻塞式),为后续在空闲时驱动 Slint 动画/计时器留出机会。
四、AppView:应用与 UI 的接口层
appview.h 定义了一个极其轻量的接口,这也是 README 强调“接口实现甚至可以在插件中”的原因:
class AppView { MyWindowAdapter *myWindow = nullptr; public: AppView(); void attachToWindow(WINDOW_HANDLE winId); void setGeometry(int x, int y, int width, int height); };头文件通过WINDOW_HANDLE类型把平台相关的HWND隐藏起来(仅 Windows 下定义为HWND),上层应用只需知道“把一个窗口句柄交给 AppView,再告诉它几何尺寸即可”,完全不接触 Slint API——接口隔离做得非常干净。
appview.cpp 则完成真正的 Slint 接入,核心在attachToWindow:
void AppView::attachToWindow(WINDOW_HANDLE winId) { auto p = std::make_unique<MyPlatform>(); p->the_window = std::make_unique<MyWindowAdapter>(winId); myWindow = p->the_window.get(); slint::platform::set_platform(std::move(p)); // AppWindow is the auto-generated slint code static auto app = AppWindow::create(); app->show(); }这里有三个关键动作:
- 构造
MyPlatform并预先塞入一个MyWindowAdapter:由于create_window_adapter()的实现直接返回std::move(the_window),第一个 Slint 窗口就会复用到这个绑定到winId的适配器,从而实现“把 Slint 窗口嵌进指定 HWND”; - 调用
slint::platform::set_platform():注册自定义平台。按照 slint-platform.h 的说明,这一步必须在任何 Slint 窗口创建之前完成,且只能调用一次; - 创建并显示 UI:
AppWindow是 CMake 构建时由 app-window.slint 自动生成的头文件中定义的组件类型,AppWindow::create()创建组件实例,show()触发窗口适配器的set_visible(true)。
MyPlatform的完整实现极其精简,只覆盖了唯一一个纯虚函数:
struct MyPlatform : public slint::platform::Platform { std::unique_ptr<MyWindowAdapter> the_window; std::unique_ptr<slint::platform::WindowAdapter> create_window_adapter() override { return std::move(the_window); } };从源码结构看,这种“预先构造好 adapter 再让工厂直接返回”的模式是单窗口嵌入场景的惯用法;如果应用需要多个 Slint 窗口,只需在create_window_adapter()中按需创建新的WindowAdapter实例即可。
五、WindowAdapter 的 Win32 实现:windowadapter_win.h
windowadapter_win.h 是整个示例的胶水核心,它把一个WS_CHILDWINDOW子窗口变成 Slint 的宿主窗口,同时负责渲染与输入转发。
5.1 子窗口的创建与渲染器初始化
MyWindowAdapter的构造函数完成了三件事:
- 注册一个窗口类
Sample Window Class,其窗口过程指向静态成员MyWindowAdapter::windowProc; - 以传入的
winId(主窗口句柄)为父窗口,创建 600×300 的WS_CHILDWINDOW子窗口; - 构造 Skia 渲染器并把子窗口句柄交给它:
m_renderer.emplace(slint::platform::NativeWindowHandle::from_win32(hwnd, hInstance), slint::PhysicalSize({ 600, 300 }));NativeWindowHandle::from_win32(void *hwnd, void *hinstance)是 slint-platform.h 提供的构造方式,它把 HWND 包装成 SkiaRenderer 可直接绘制的原生句柄。最后通过SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)this)把this指针存进窗口,供windowProc回查。
5.2 覆盖 WindowAdapter 的四个接口
slint::platform::AbstractRenderer &renderer() override { return m_renderer.value(); } slint::PhysicalSize size() override { RECT r; GetWindowRect(hwnd, &r); return slint::PhysicalSize({ uint32_t(r.right - r.left), uint32_t(r.bottom - r.top) }); } void set_visible(bool visible) override { ShowWindow(hwnd, visible ? SW_SHOWNORMAL : SW_HIDE); } void request_redraw() override { InvalidateRect(hwnd, nullptr, false); }renderer()返回持有的SkiaRenderer,Slint 核心用它绘制场景;size()通过GetWindowRect查询真实窗口尺寸,返回的是物理像素大小(PhysicalSize);set_visible()映射到ShowWindow;request_redraw()触发WM_PAINT,从而驱动真正的渲染。
5.3 WM_PAINT:渲染与动画推进
渲染动作发生在windowProc的WM_PAINT分支:
case WM_PAINT: { PAINTSTRUCT ps; BeginPaint(hwnd, &ps); slint::platform::update_timers_and_animations(); self->render(); EndPaint(hwnd, &ps); return 0; }其中slint::platform::update_timers_and_animations()(定义见 slint-platform.h)负责推进 Slint 内部的定时器与动画时钟,而MyWindowAdapter::render()在绘制完成后检查window().has_active_animations(),若仍有进行中的动画则再次调用request_redraw(),形成“动画期间持续重绘”的自驱动循环。
5.4 输入事件:从 Win32 消息到 Slint 事件
mouse_event()负责把 Win32 鼠标消息翻译为 Slint 指针事件,例如:
case WM_LBUTTONDOWN: window().dispatch_pointer_press_event(LogicalPosition({ x, y }), PointerEventButton::Left); break; case WM_MOUSEMOVE: window().dispatch_pointer_move_event(LogicalPosition({ x, y })); break;坐标由LOWORD(lParam)/HIWORD(lParam)解出,事件类型通过dispatch_pointer_press_event、dispatch_pointer_release_event、dispatch_pointer_move_event分发到 Slint 场景(这些方法定义在 api/cpp/include/private/slint_window.h 附近)。示例完整覆盖了左/中/右键的按下与释放、WM_MOUSEMOVE移动,以及WM_XBUTTONDOWN时把 XBUTTON1/XBUTTON2 映射为PointerEventButton::Back/Forward的前进后退键事件。
尺寸变化则由WM_SIZE分支触发resize(),最终调用window().dispatch_resize_event()把新的逻辑尺寸通知给 Slint 布局系统。
六、UI 定义与构建配置
6.1 示例 UI:app-window.slint
app-window.slint 是一个简单的计数演示界面,同时用到了std-widgets.slint中的原生风格控件:
import {Button, AboutSlint} from "std-widgets.slint"; export component AppWindow inherits Window { property <int> count; preferred-width: 600px; preferred-height: 300px; VerticalLayout { AboutSlint { } Button { clicked => { count += 1; } text: "Press me"; } Text { text: count; } Rectangle { } } }它声明了一个count属性,点击按钮自增并由Text实时显示。这个.slint文件在构建时由 CMake 的slint_target_sources()编译为app-window.h(appview.cpp 顶部#include "app-window.h"即来源于此),其中的AppWindow类型与上述.slint中的export component AppWindow一一对应。
6.2 构建配置:CMakeLists.txt
examples/cpp/platform_native/CMakeLists.txt 只有 8 行,且整体包裹在if (WIN32)中:
if (WIN32) add_executable(platform_native WIN32 main.cpp appview.cpp) target_link_libraries(platform_native PRIVATE Slint::Slint) slint_target_sources(platform_native app-window.slint) endif(WIN32)要点:
WIN32关键字让链接器生成 GUI 子系统程序(入口为WinMain),避免弹出控制台窗口;Slint::Slint是 Slint 官方 CMake 包提供的目标,需要通过find_package(Slint)获得;slint_target_sources()指定.slint源文件,由 Slint 编译器在构建期生成 C++ 代码;- 整个示例仅面向 Windows,非 Windows 平台不会被构建。
构建方式可参考 examples/cpp/README.md 的通用流程:在示例目录下执行mkdir build && cd build && cmake .. && cmake --build .,并确保已正确安装/定位 Slint 的 CMake 包。
七、机制总结:事件、渲染与生命周期如何闭环
综合上述源码,可以把整个嵌入机制归纳为一条完整的数据通路:
- 注册:主窗口
WM_CREATE时创建AppView,attachToWindow中构造MyPlatform+MyWindowAdapter并调用set_platform(),随后AppWindow::create()+show(); - 布局:主窗口
WM_SIZE通过AppView::setGeometry()→SetWindowPos调整子窗口位置,子窗口自身WM_SIZE则通过dispatch_resize_event通知 Slint 布局系统; - 输入:子窗口收到鼠标消息后,
windowProc先调用update_timers_and_animations()推进动画时钟,再由mouse_event()翻译为dispatch_pointer_*_event交给 Slint 场景处理; - 渲染:
InvalidateRect触发WM_PAINT,update_timers_and_animations()后由SkiaRenderer绘制到 HWND;动画未结束时持续request_redraw(); - 退出:原生按钮点击或
WM_CLOSE时销毁AppView,PostQuitMessage(0)结束消息循环。
这套模式的价值在于:Win32 的消息循环、窗口过程、子窗口机制全部保持原生,Slint 只负责场景渲染与 UI 逻辑,两者通过Platform/WindowAdapter这两个薄接口解耦。如果后续想把界面逻辑做成插件,只需把AppView的实现(appview.cpp)编译进独立动态库并暴露同样的两个方法即可,这正是 README 中“接口实现甚至可以在插件中”的言下之意。
八、进一步探索
- 对照阅读 api/cpp/include/slint-platform.h,查看
Platform的run_event_loop、剪贴板、run_in_event_loop等未在本示例中使用的能力; - examples/cpp/platform_qt 展示了同一套 Platform API 与 Qt 窗口集成的做法,可横向对比不同宿主窗口的接线差异;
- api/cpp/include/slint-stm32.h 中还有一个面向嵌入式 STM32 的
Platform实现(含触摸事件转发与update_timers_and_animations()的用法),可帮助理解这套 API 的平台无关性; - 若需要在运行时动态加载 UI,可参考 examples/cpp/qt_viewer 对
slint::interpreter的使用方式。
【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考