使用 Slint C++ Platform API 将界面嵌入原生 WIN32 应用:platform_native 示例深度解析
2026/9/13 9:10:56 网站建设 项目流程

使用 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::PlatformWindowAdapter把 Slint 界面“嵌入”到一个原生 Windows 窗口里。读完本文,你将掌握 Slint C++ Platform API 的核心抽象(PlatformWindowAdapterAbstractRenderer)、消息循环与渲染的接线方式,以及把 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 窗口。子类需要持有渲染器(SoftwareRendererSkiaRenderer),并实现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:原生按钮被点击时销毁AppViewPostQuitMessage(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(); }

这里有三个关键动作:

  1. 构造MyPlatform并预先塞入一个MyWindowAdapter:由于create_window_adapter()的实现直接返回std::move(the_window),第一个 Slint 窗口就会复用到这个绑定到winId的适配器,从而实现“把 Slint 窗口嵌进指定 HWND”;
  2. 调用slint::platform::set_platform():注册自定义平台。按照 slint-platform.h 的说明,这一步必须在任何 Slint 窗口创建之前完成,且只能调用一次;
  3. 创建并显示 UIAppWindow是 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的构造函数完成了三件事:

  1. 注册一个窗口类Sample Window Class,其窗口过程指向静态成员MyWindowAdapter::windowProc
  2. 以传入的winId(主窗口句柄)为父窗口,创建 600×300 的WS_CHILDWINDOW子窗口;
  3. 构造 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:渲染与动画推进

渲染动作发生在windowProcWM_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_eventdispatch_pointer_release_eventdispatch_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 包。

七、机制总结:事件、渲染与生命周期如何闭环

综合上述源码,可以把整个嵌入机制归纳为一条完整的数据通路:

  1. 注册:主窗口WM_CREATE时创建AppViewattachToWindow中构造MyPlatform+MyWindowAdapter并调用set_platform(),随后AppWindow::create()+show()
  2. 布局:主窗口WM_SIZE通过AppView::setGeometry()SetWindowPos调整子窗口位置,子窗口自身WM_SIZE则通过dispatch_resize_event通知 Slint 布局系统;
  3. 输入:子窗口收到鼠标消息后,windowProc先调用update_timers_and_animations()推进动画时钟,再由mouse_event()翻译为dispatch_pointer_*_event交给 Slint 场景处理;
  4. 渲染InvalidateRect触发WM_PAINTupdate_timers_and_animations()后由SkiaRenderer绘制到 HWND;动画未结束时持续request_redraw()
  5. 退出:原生按钮点击或WM_CLOSE时销毁AppViewPostQuitMessage(0)结束消息循环。

这套模式的价值在于:Win32 的消息循环、窗口过程、子窗口机制全部保持原生,Slint 只负责场景渲染与 UI 逻辑,两者通过Platform/WindowAdapter这两个薄接口解耦。如果后续想把界面逻辑做成插件,只需把AppView的实现(appview.cpp)编译进独立动态库并暴露同样的两个方法即可,这正是 README 中“接口实现甚至可以在插件中”的言下之意。

八、进一步探索

  • 对照阅读 api/cpp/include/slint-platform.h,查看Platformrun_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询