lazarus鸿蒙开发12:使用鸿蒙原生API画图--鸿蒙侧关键代码
重点介绍:
entry\src\main\cpp\CMakeLists.txt
entry\src\main\cpp\native_module.cpp
entry\src\main\ets\pages\Index.ets
这三个文件共同实现了一个在鸿蒙系统上运行的跨语言图形渲染桥接方案。其核心架构为:ArkTS (UI层) -> C++ (NAPI桥接层) -> Pascal (核心渲染层)。
以下是这三个文件的功能与技术要点总结分析:
1. Index.ets (ArkTS UI 层 - 渲染载体与触发器)
功能: 提供用户界面,创建图形渲染的画布,并将系统分配的 Surface ID 传递给底层 C++。
技术要点:
- XComponent 机制: 使用
XComponent组件并指定type: XComponentType.SURFACE,在鸿蒙系统中独立创建一块用于图形渲染的表面。 - Surface ID 获取: 通过
XComponentController的getXComponentSurfaceId()方法,获取该画布在系统底层的唯一标识符(字符串格式的数字ID)。 - 跨语言调用: 在
onLoad生命周期中,引入编译好的 C++ 动态库libpascalbridge.so,并调用其导出的setSurfaceId方法,将 Surface ID 下发到 C++ 层,从而打通 UI 层与底层渲染链路。
2. native_module.cpp (C++ NAPI 桥接层 - 核心调度与显存操作)
功能: 作为中间层,接收 ArkTS 传来的 Surface ID 构建原生窗口,并调用 Pascal 层的绘制逻辑,将像素数据写入鸿蒙的原生缓冲区完成上屏。
技术要点:
- NAPI 注册与交互: 使用宏和
napi_module结构体将setSurfaceId函数注册为 NAPI 接口,供 ArkTS 调用;在函数内部通过napi_get_value_string_utf8和strtoull将 JS 字符串转换为 C++ 的uint64_t类型 Surface ID。 - NativeWindow 生命周期管理: 调用鸿蒙 NDK 接口
OH_NativeWindow_CreateNativeWindowFromSurfaceId将 Surface ID 还原为OHNativeWindow,这是后续图形操作的基础。 - 跨语言调用 Pascal: 通过
extern "C"声明了 Pascal 导出的DoDraw()和GetBitmapPixels(),实现了 C++ 与 Pascal 的 FFI(外部函数接口)互操作。 - 图形缓冲区与内存映射:
- 使用
OH_NativeWindow_LockBuffer申请并锁定一块显存缓冲区。 - 关键操作: 使用
mmap将底层BufferHandle的文件描述符(fd)映射到用户态内存空间,使得 CPU 可以直接操作显存。
- 使用
- 像素数据拷贝与对齐: 遍历 Pascal 生成的像素数据,使用
memcpy逐行拷贝到mmap映射出的内存中。代码中通过bufferHandle->width * 4计算步长处理了显存的对齐问题,并硬编码了 400x400 的渲染尺寸。 - 上屏刷新: 拷贝完成后调用
munmap解除映射,并调用OH_NativeWindow_UnlockAndFlushBuffer提交缓冲区,触发系统刷帧显示。
3. CMakeLists.txt (构建脚本 - 依赖管理与链接配置)
功能: 配置 C++ 桥接库的编译规则,链接鸿蒙系统库以及第三方预编译的 Pascal 动态库。
技术要点:
- 系统 NDK 库链接: 显式链接了鸿蒙图形与 NAPI 所需的核心库:
ace_napi.z(NAPI基础)、hilog_ndk.z(日志)、native_window(原生窗口) 以及dl(动态链接)。 - 第三方预构建库集成:
- 使用
install(FILES ...)命令,确保 Pascal 编译出的第三方.so文件(libOHOS_QT_Lazarus-x86_64.so)被正确拷贝到应用的libs目录下,以便打包进最终的 APP 中。 - 使用
target_link_directories指定第三方库的搜索路径,利用${OHOS_ARCH}变量实现了对arm64-v8a和x86_64多架构的自动适配。 - 使用
target_link_libraries将 Pascal 库链接到 C++ 桥接库中,使得 C++ 代码能找到 Pascal 的符号表。
- 使用
- 潜在隐患注意: 在
target_link_libraries中,Pascal 库名被硬编码为OHOS_QT_Lazarus-x86_64,这意味着在 ARM 架构真机(arm64-v8a)上编译时可能会找不到该库,理想情况下应使用类似${PASCAL_LIB_NAME}的变量根据架构动态替换。
整体技术链路总结
- ArkTS 通过
XComponent获取画布凭证,传给 C++。 - C++ 拿着凭证向系统换来显存操作权,然后叫 Pascal “开始画画并把画布数据给我”。
- Pascal 在内存中画完后,C++ 将这块内存数据通过
mmap和memcpy逐行搬运到系统的显存缓冲区中。 - C++ 通知系统刷新显存,画面最终显示在屏幕的
XComponent区域。
import pascalbridge from 'libpascalbridge.so'; // 导入 C++ 模块 @Entry @Component struct Index { private xComponentController: XComponentController = new XComponentController(); build() { Column() { XComponent({ id: 'xcomponentId', type: XComponentType.SURFACE, libraryname: 'pascalbridge', controller: this.xComponentController }) .onLoad(() => { // ★ 获取 XComponent 的 Surface ID (字符串形式) let surfaceId = this.xComponentController.getXComponentSurfaceId(); console.log("Lazarus ArkTS: surfaceId = " + surfaceId); // ★ 将 Surface ID 传递给 C++ 端 pascalbridge.setSurfaceId(surfaceId); }) .width('100%') .height('100%') } .width('100%') .height('100%') } }
native_module.cpp:
这个cpp最后生成:libpascalbridge.so
#include <native_window/external_window.h> #include <sys/mman.h> #include <unistd.h> #include <cstring> #include <cstdlib> #include "hilog/log.h" #include "napi/native_api.h" // 声明 Pascal 导出的函数 extern "C" { void DoDraw(); void* GetBitmapPixels(); } static OHNativeWindow* g_nativeWindow = nullptr; void RenderToScreen() { if (!g_nativeWindow) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0xFF00, "Lazarus", "Render Failed: g_nativeWindow is NULL!"); return; } DoDraw(); void* pixels = GetBitmapPixels(); if (!pixels) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0xFF00, "Lazarus", "Render Failed: GetBitmapPixels returned NULL!"); return; } OHNativeWindowBuffer* buffer = nullptr; Region::Rect rect = {0, 0, 400, 400}; Region region = {&rect, 1}; int ret = OH_NativeWindow_LockBuffer(g_nativeWindow, region, &buffer); if (ret != 0 || !buffer) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0xFF00, "Lazarus", "Render Failed: LockBuffer failed, ret=%{public}d", ret); return; } BufferHandle* bufferHandle = OH_NativeWindow_GetBufferHandleFromNative(buffer); if (!bufferHandle) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0xFF00, "Lazarus", "Render Failed: GetBufferHandle failed!"); return; } OH_LOG_Print(LOG_APP, LOG_INFO, 0xFF00, "Lazarus", "Buffer Info: w=%{public}d, h=%{public}d, stride=%{public}d", bufferHandle->width, bufferHandle->height, bufferHandle->stride); void* mappedAddr = mmap(nullptr, bufferHandle->size, PROT_READ | PROT_WRITE, MAP_SHARED, bufferHandle->fd, 0); if (mappedAddr == MAP_FAILED) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0xFF00, "Lazarus", "Render Failed: mmap failed!"); return; } uint32_t* srcLine = (uint32_t*)pixels; uint8_t* dstLine = (uint8_t*)mappedAddr; uint32_t stride = bufferHandle->width * 4; for (int y = 0; y < 400; y++) { memcpy(dstLine, srcLine, 400 * 4); srcLine += 400; dstLine += stride; } munmap(mappedAddr, bufferHandle->size); ret = OH_NativeWindow_UnlockAndFlushBuffer(g_nativeWindow); if (ret != 0) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0xFF00, "Lazarus", "Render Failed: UnlockAndFlushBuffer failed, ret=%{public}d", ret); } else { OH_LOG_Print(LOG_APP, LOG_INFO, 0xFF00, "Lazarus", "Render Success!"); } } // ============== 接收 SurfaceId 的 NAPI 函数 ============== static napi_value SetSurfaceId(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value args[1] = {nullptr}; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); size_t strSize = 0; napi_get_value_string_utf8(env, args[0], nullptr, 0, &strSize); char* surfaceIdStr = new char[strSize + 1]; napi_get_value_string_utf8(env, args[0], surfaceIdStr, strSize + 1, &strSize); OH_LOG_Print(LOG_APP, LOG_INFO, 0xFF00, "Lazarus", "Received SurfaceId string: %{public}s", surfaceIdStr); // 将字符串转为 uint64_t uint64_t surfaceId = strtoull(surfaceIdStr, nullptr, 10); delete[] surfaceIdStr; // ★ 根据头文件,创建 NativeWindow 的正确函数 ★ int32_t ret = OH_NativeWindow_CreateNativeWindowFromSurfaceId(surfaceId, &g_nativeWindow); if (ret != 0 || !g_nativeWindow) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0xFF00, "Lazarus", "CreateNativeWindowFromSurfaceId failed, ret=%{public}d", ret); } else { OH_LOG_Print(LOG_APP, LOG_INFO, 0xFF00, "Lazarus", "CreateNativeWindowFromSurfaceId Success!"); RenderToScreen(); } return nullptr; } extern "C" __attribute__((visibility("default"))) napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { { "setSurfaceId", nullptr, SetSurfaceId, nullptr, nullptr, nullptr, napi_default, nullptr } }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } static napi_module demoModule = { .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr, .nm_register_func = Init, .nm_modname = "pascalbridge", .nm_priv = ((void*)0), .reserved = { 0 }, }; extern "C" __attribute__((constructor)) void RegisterModule(void) { napi_module_register(&demoModule); }
CMakeLists.txt:
cmake_minimum_required(VERSION 3.5) project(pascalbridge) # === 让CMake能找到SDK自带的系统库 === if(DEFINED PACKAGE_FIND_FILE) include(${PACKAGE_FIND_FILE}) endif() add_library(pascalbridge SHARED native_module.cpp) # 设置预构建库路径 set(PREBUILT_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}) # 将预构建 so 安装到 libs 目录,使其被打包 install(FILES ${PREBUILT_LIB_DIR}/libOHOS_Lazarus.so DESTINATION libs/${OHOS_ARCH}) # === 1. 指定 Pascal 动态库的搜索路径 === # 假设您把 Pascal 编译的 .so 放在了 cpp/libs/arm64-v8a 和 cpp/libs/x86_64 下 # ${OHOS_ARCH} 会自动替换为 arm64-v8a 或 x86_64 target_link_directories(pascalbridge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}) # === 2. 链接所有需要的库 === # 注意:CMake规范中,链接库不需要加 lib 前缀和 .so 后缀 target_link_libraries(pascalbridge PUBLIC ace_napi.z dl hilog_ndk.z native_window OHOS_Lazarus # ★ 新增:链接你的 Pascal 动态库 )
lazarus生成的so要拷贝到以下2个libs目录,如果没有就创建相应的目录并将so拷贝过来
entry
└── libs/ --->打包hap需要的so
├── arm64-v8a/ <-- 真机架构
│ └── libOHOS_QT_Lazarus.so <-- Lazarus编译出的arm64版本
└── x86_64/ <-- 模拟器架构
└── libOHOS_QT_Lazarus.so <-- Lazarus编译出的x86_64版本
entry/src/main/cpp/ ---->链接时需要的so
├── CMakeLists.txt
├── native_module.cpp
└── libs/
├── arm64-v8a/ <-- 真机架构
│ └── libOHOS_QT_Lazarus.so <-- Lazarus编译出的arm64版本
└── x86_64/ <-- 模拟器架构
└── libOHOS_QT_Lazarus.so <-- Lazarus编译出的x86_64版本

浙公网安备 33010602011771号