9.9 KiB
9.9 KiB
liboliveui.so — UI 层
依赖:
libolivecore.so,libolivecodec.so,libolivenode.so,liboliverender.so,liboliveaudio.so
外部依赖:Qt::Widgets, Qt::OpenGL, Qt::OpenGLWidgets, KDDockWidgets
包含源码:app/widget/,app/panel/,app/window/,app/dialog/,app/tool/,app/ui/
当前状态:单体 OBJECT 库的一部分
改造难度:⭐⭐⭐(中等,但代码量大)
1. 当前状态分析
UI 层是代码量最大的模块,但耦合方向是单向的:UI 依赖下层(node/render/codec),下层不依赖 UI。这使得 UI 层的拆分相对直接。
| 组件 | 说明 |
|---|---|
widget/ |
40+ 自定义 Qt Widget(节点视图、时间线、播放控制、颜色轮等) |
panel/ |
基于 KDDockWidgets 的可停靠面板包装 |
window/mainwindow/ |
主窗口 |
dialog/ |
模态对话框(导出、首选项、项目属性等) |
tool/ |
工具枚举 |
ui/ |
图标、光标、样式表、翻译资源 |
关键问题:
Core类(core.h/cpp)混合了业务逻辑和 UI 逻辑(StartGUI(),main_window_等)。RenderManager的RenderTicketWatcher使用 Qt 信号通知 UI。- 大量 UI 类直接包含
node/和render/的 C++ 头文件。
策略:
liboliveui.so的 C API 不需要非常完善,因为 UI 层大概率仍然与主进程一同编译(UI 是主进程的核心)。- 但为了保持架构一致性,仍定义 C API 用于:
- 第三方脚本/插件通过 C API 操作 UI(未来扩展)。
- 单元测试通过 C API 驱动 UI(自动化测试)。
- 主进程中的 UI 代码可以继续使用 C++ 直接包含下层头文件,不必全部改为 C API 调用。这是因为 UI 层在最顶层,不需要被其他模块依赖。
修正策略:liboliveui.so 的拆分重点在于:
- 将 UI 代码从单体 OBJECT 库移出,编译为独立的
liboliveui.so。 - 主进程显式加载
liboliveui.so。 - UI 层内部继续使用 C++ 直接调用下层(node/render 等),只在跨库边界处遵循 ABI 规则。
2. C API 设计(精简版)
UI 层的 C API 不需要覆盖所有 Widget,只需提供应用级入口和关键面板操作:
2.1 头文件:c_api/include/olive/ui_api.h
#ifndef OLIVE_UI_API_H
#define OLIVE_UI_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#include "node_api.h"
#define OLIVE_UI_API_VERSION 1
#ifdef OLIVE_BUILDING_UI
# define OLIVE_UI_API __attribute__((visibility("default")))
#else
# define OLIVE_UI_API
#endif
/* ========== 不透明类型 ========== */
typedef struct OliveApplication OliveApplication;
typedef struct OliveMainWindow OliveMainWindow;
typedef struct OliveViewerPanel OliveViewerPanel;
typedef struct OliveTimelinePanel OliveTimelinePanel;
typedef struct OliveNodeEditorPanel OliveNodeEditorPanel;
/* ========== API 版本 ========== */
OLIVE_UI_API int olive_ui_api_version(void);
/* ========== 应用生命周期 ========== */
OLIVE_UI_API OliveApplication* olive_ui_app_create(int argc, char** argv);
OLIVE_UI_API int olive_ui_app_exec(OliveApplication* app);
OLIVE_UI_API void olive_ui_app_quit(OliveApplication* app);
OLIVE_UI_API void olive_ui_app_destroy(OliveApplication* app);
/* ========== 主窗口 ========== */
OLIVE_UI_API OliveMainWindow* olive_ui_main_window_create(OliveApplication* app);
OLIVE_UI_API void olive_ui_main_window_destroy(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_main_window_show(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_main_window_set_fullscreen(OliveMainWindow* win, int fullscreen);
/* ========== 项目操作 ========== */
OLIVE_UI_API int olive_ui_open_project(OliveMainWindow* win, const char* filename);
OLIVE_UI_API int olive_ui_save_project(OliveMainWindow* win, const char* filename);
OLIVE_UI_API int olive_ui_import_footage(OliveMainWindow* win, const char** filenames, int count);
/* ========== 查看器(Viewer) ========== */
OLIVE_UI_API OliveViewerPanel* olive_ui_get_active_viewer(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_viewer_set_time(OliveViewerPanel* viewer, OliveRational time);
OLIVE_UI_API void olive_ui_viewer_play(OliveViewerPanel* viewer);
OLIVE_UI_API void olive_ui_viewer_pause(OliveViewerPanel* viewer);
OLIVE_UI_API void olive_ui_viewer_stop(OliveViewerPanel* viewer);
/* ========== 时间线 ========== */
OLIVE_UI_API OliveTimelinePanel* olive_ui_get_active_timeline(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_timeline_set_time(OliveTimelinePanel* timeline, OliveRational time);
OLIVE_UI_API void olive_ui_timeline_set_work_area(OliveTimelinePanel* timeline,
OliveRational in,
OliveRational out);
/* ========== 节点编辑器 ========== */
OLIVE_UI_API OliveNodeEditorPanel* olive_ui_get_node_editor(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_node_editor_set_graph(OliveNodeEditorPanel* editor,
OliveNodeGraph* graph);
/* ========== 导出对话框 ========== */
OLIVE_UI_API int olive_ui_show_export_dialog(OliveMainWindow* win,
OliveViewerOutput* viewer_output,
const char* default_filename);
/* ========== 状态栏消息 ========== */
OLIVE_UI_API void olive_ui_show_status_message(OliveMainWindow* win,
const char* message,
int timeout_ms);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_UI_API_H
3. CMake 改造
# 由于 UI 层代码分散在 widget/, panel/, window/, dialog/, tool/, ui/ 多个目录,
# 需要在 app/CMakeLists.txt 中统一聚合。
set(UI_INTERNAL_SOURCES
# widget/
widget/viewer/viewerwidget.cpp widget/viewer/viewerwidget.h
widget/timelinewidget/timelinewidget.cpp widget/timelinewidget/timelinewidget.h
widget/nodeview/nodeview.cpp widget/nodeview/nodeview.h
# ... 所有 widget 源文件
# panel/
panel/viewer/viewerpanel.cpp panel/viewer/viewerpanel.h
panel/timeline/timelinepanel.cpp panel/timeline/timelinepanel.h
panel/node/nodepanel.cpp panel/node/nodepanel.h
# ... 所有 panel 源文件
# window/
window/mainwindow/mainwindow.cpp window/mainwindow/mainwindow.h
# dialog/
dialog/export/exportdialog.cpp dialog/export/exportdialog.h
dialog/preferences/preferencesdialog.cpp dialog/preferences/preferencesdialog.h
# ... 所有 dialog 源文件
# tool/
tool/tool.cpp tool/tool.h
# ui/ 资源(.qrc 等)
# ...
)
set(UI_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/ui_api.cpp
)
add_library(oliveui SHARED
${UI_INTERNAL_SOURCES}
${UI_API_SOURCES}
)
target_compile_definitions(oliveui PRIVATE OLIVE_BUILDING_UI)
target_include_directories(oliveui
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/c_api/include
${CMAKE_SOURCE_DIR}/ext/KDDockWidgets/src
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(oliveui
PUBLIC
olivenode
oliverender
olivecodec
oliveaudio
olivecore
oliveplugin
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::OpenGL
Qt${QT_VERSION_MAJOR}::OpenGLWidgets
KDAB::kddockwidgets
)
set_target_properties(oliveui PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
install(TARGETS oliveui DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/ui_api.h DESTINATION include/olive)
4. 小步快跑实施步骤
Step 0: 解耦 Core 类(2 天)
- 分析
core.h/cpp中哪些属于 UI 逻辑(StartGUI(),main_window_,ImportFiles()等),哪些属于业务逻辑。 - 将 UI 相关逻辑迁移到
liboliveui.so内部的一个UiCore类中。 - 保留
Core类中纯业务逻辑(如FootageFileDialogFilter,CreateNewSequenceForProject)。
验收标准:core.h 不再包含 mainwindow.h 或 projectexplorer.h 等 UI 头文件。
Step 1: 聚合 UI 源码(1 天)
- 在
app/CMakeLists.txt中聚合所有 UI 相关源文件(widget/, panel/, window/, dialog/, tool/, ui/)。 - 确保
liboliveui.so可以编译。
验收标准:liboliveui.so 编译成功。
Step 2: 主进程加载 UI 库(1 天)
- 修改
main.cpp:先通过ModuleLoader加载liboliveui.so,然后调用olive_ui_app_create和olive_ui_main_window_create。 - 若动态加载失败,回退到静态链接模式。
验收标准:主程序启动时日志显示成功加载 ui 模块,并正常显示主窗口。
Step 3: C API 实现(按需,2–3 天)
- 实现
olive_ui_app_create/exec/quit/destroy。 - 实现
olive_ui_main_window_create/show。 - 实现项目操作:
open_project,save_project,import_footage。 - 其他 UI C API 根据测试/脚本需求逐步实现。
验收标准:可以通过一个外部测试程序加载 liboliveui.so 并打开主窗口。
5. 风险与回退
| 风险 | 对策 |
|---|---|
| UI 代码量巨大,聚合时容易遗漏源文件 | 编写脚本自动收集 widget/, panel/, window/, dialog/ 下的所有 .cpp/.h 文件,或在 CMake 中保持原有的 add_subdirectory 结构,只是最终输出为 SHARED 而非 OBJECT。 |
| KDDockWidgets 的符号跨动态库 | KDDockWidgets 以静态库形式链接进 liboliveui.so,其符号不外泄。确保 liboliveui.so 的 CXX_VISIBILITY_PRESET hidden。 |
| Qt 资源文件(.qrc)在动态库中的加载 | 将 .qrc 编译进 liboliveui.so,Qt 的资源系统在动态库中工作正常。确保 Q_INIT_RESOURCE() 在库加载时被调用。 |
Core 类的信号槽跨模块 |
Core 保留在主进程,UiCore 在 liboliveui.so 中。两者通过 C API 或 Qt 的跨进程信号(如果未来需要)通信。初期保持简单:主进程直接调用 UI 的 C API。 |