ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

使用 Slint C++ Platform API 将界面嵌入原生 WIN32 应用:platform_native 示例深度解析

使用 Slint C++ Platform API 将界面嵌入原生 WIN32 应用:platform_native 示例深度解析 使用 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 UIwindowadapter_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.hslint::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.cppmain.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_ptrAppView销毁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 接入核心在attachToWindowvoid AppView::attachToWindow(WINDOW_HANDLE winId) { auto p std::make_uniqueMyPlatform(); p-the_window std::make_uniqueMyWindowAdapter(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 窗口创建之前完成且只能调用一次创建并显示 UIAppWindow是 CMake 构建时由 app-window.slint 自动生成的头文件中定义的组件类型AppWindow::create()创建组件实例show()触发窗口适配器的set_visible(true)。MyPlatform的完整实现极其精简只覆盖了唯一一个纯虚函数struct MyPlatform : public slint::platform::Platform { std::unique_ptrMyWindowAdapter the_window; std::unique_ptrslint::platform::WindowAdapter create_window_adapter() override { return std::move(the_window); } };从源码结构看这种“预先构造好 adapter 再让工厂直接返回”的模式是单窗口嵌入场景的惯用法如果应用需要多个 Slint 窗口只需在create_window_adapter()中按需创建新的WindowAdapter实例即可。五、WindowAdapter 的 Win32 实现windowadapter_win.hwindowadapter_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()返回持有的SkiaRendererSlint 核心用它绘制场景size()通过GetWindowRect查询真实窗口尺寸返回的是物理像素大小PhysicalSizeset_visible()映射到ShowWindowrequest_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 示例 UIapp-window.slintapp-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.happview.cpp 顶部#include app-window.h即来源于此其中的AppWindow类型与上述.slint中的export component AppWindow一一对应。6.2 构建配置CMakeLists.txtexamples/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时创建AppViewattachToWindow中构造MyPlatformMyWindowAdapter并调用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_PAINTupdate_timers_and_animations()后由SkiaRenderer绘制到 HWND动画未结束时持续request_redraw()退出原生按钮点击或WM_CLOSE时销毁AppViewPostQuitMessage(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),仅供参考
返回列表