深度桌面环境Qt5主题集成插件qt5integration实战解析

本文还有配套的精品资源,点击获取

简介:qt5integration是专为深度桌面环境(DDE)开发的Qt平台主题集成插件,旨在提升Qt5应用程序在DDE中的视觉一致性与交互体验。通过集成主题、输入法、菜单、通知、窗口管理和无障碍支持等核心功能,该插件使Qt5应用能够无缝融入DDE,呈现"原生"效果。本文深入解析其架构与实现机制,并提供基于源码包qt5integration-master的编译、安装与定制流程,帮助开发者优化跨平台应用在Deepin OS上的用户体验。

1. Qt5integration插件概述与作用

Qt5integration 是深度桌面环境(DDE)中实现 Qt5 应用与系统深度融合的核心平台插件。它通过实现 QPlatformTheme 接口,为 Qt5 应用提供 DDE 原生主题、字体、图标、调色板等视觉资源的无缝接入能力。该插件在运行时被 Qt 框架自动加载,替代默认的平台样式逻辑,使非 KDE 环境下的 Qt 应用仍能呈现与 DDE 一致的外观与交互行为。

其核心价值在于解决了 Linux 桌面生态中长期存在的"风格割裂"问题。传统 Qt 应用在 GNOME 或 DDE 等非 Qt 主导环境中常出现控件样式陈旧、DPI 适配异常、输入法错位等问题。 Qt5integration 通过插件化机制,在不修改应用源码的前提下,实现对控件绘制、事件处理、资源查找等关键路径的透明拦截与重定向。

此外,该插件还扩展了对 D-Bus 服务、全局菜单、通知系统等桌面特性的集成支持,使 Qt 应用能够真正"融入"DDE,而非仅仅"运行"于其上。这种设计不仅提升了用户体验一致性,也为跨桌面环境的应用分发提供了技术基础。

2. 深度桌面环境(DDE)与Qt5框架集成原理

在现代 Linux 桌面环境中,应用程序的视觉一致性与交互体验已成为衡量用户体验的重要标准。深度桌面环境(Deepin Desktop Environment, DDE)作为国产桌面系统的代表之一,致力于提供统一、美观且高效的用户界面。然而,在非 KDE 桌面环境下运行基于 Qt5 开发的应用程序时,常常出现主题不一致、控件样式割裂、输入法异常等问题。为解决这一挑战, Qt5integration 插件应运而生------它通过深入 Qt5 的平台抽象层(Platform Abstraction Layer),实现 DDE 主题与 Qt 应用之间的无缝集成。

本章将系统剖析 DDE 与 Qt5 框架的集成机制,重点聚焦于底层架构设计、插件加载流程、运行时绑定策略以及兼容性处理方案。我们将从 DDE 的组件化架构出发,逐步揭示 Qt5 应用如何借助 QPlatformIntegrationQStyleFactory 等核心接口完成与桌面环境的主题同步与行为融合,并探讨在实际部署中可能遇到的技术障碍及其应对策略。

2.1 DDE 架构与 Qt 应用运行时环境

DDE 并非一个单一进程或服务,而是一个由多个松耦合组件构成的模块化桌面系统。其设计理念强调"服务自治"与"接口标准化",通过 D-Bus、共享库和配置文件等方式实现跨进程协作。这种架构使得 DDE 能够灵活适配不同的显示服务器(X11/Wayland)、窗口管理器(KWin/Mutter 变种)以及应用框架(GTK/Qt)。对于 Qt5 应用而言,能否自然融入 DDE 视觉风格,关键在于其运行时所依赖的平台插件是否能正确识别并绑定 DDE 提供的主题资源和服务接口。

2.1.1 DDE 的组件化架构与服务协同机制

DDE 的核心服务包括但不限于以下几类:

组件名称 功能描述
dde-session-daemon 用户会话守护进程,负责启动基础服务、管理电源策略与快捷键
dde-desktop 桌面图标渲染与背景管理
dde-launcher 应用启动器,支持模糊搜索与分类浏览
dde-control-center 系统设置中心,提供图形化配置入口
dde-dock 任务栏与系统托盘管理
dde-notification-daemon 通知中心后端,处理来自各应用的消息推送
libdde / libdtk 公共工具库,封装 D-Bus 接口调用与 UI 控件基类

这些组件之间通过 D-Bus 总线 进行通信,遵循严格的接口命名规范(如 com.deepin.SessionManager )。更重要的是, libdtk (Deepin Tool Kit)作为专为 DDE 设计的 C++ GUI 库,不仅提供了符合 Deepin 设计语言的控件集,还内置了对 Qt5integration 插件的支持逻辑。

graph TD A[dde-session-daemon] --> B(dde-dock) A --> C(dde-desktop) A --> D(dde-launcher) A --> E(dde-control-center) B --> F[QSystemTrayIcon] E --> G[QDialog-based Settings] F --> H{Qt5 App?} G --> H H -- Yes --> I[Load qt-platformtheme plugin] I --> J[Use DDE Style via QProxyStyle] J --> K[Render with libdtk theme assets]

上述流程图展示了 Qt5 应用在 DDE 中的典型集成路径:当应用启动时,若检测到当前桌面环境为 DDE,则会触发 qt-platformtheme 插件加载,进而通过 QProxyStyle 代理原生控件绘制行为,最终使用 libdtk 所提供的主题资源完成渲染。

值得注意的是,DDE 的服务协同机制并非强制所有应用都链接 libdtk 。相反, Qt5integration 插件采用"无侵入式注入"策略,即仅需设置环境变量或配置文件即可让任意 Qt5 应用自动启用 DDE 风格,无需重新编译或修改源码。

2.1.2 Qt5 应用程序的平台插件加载流程

Qt 框架采用"平台抽象层"(QPA, Qt Platform Abstraction)来屏蔽底层操作系统差异。每个 Qt 应用在初始化时都会动态选择一个 QPlatformIntegration 子类实例,该实例决定了窗口创建、事件分发、字体渲染等核心功能的行为。平台插件的加载顺序如下:

  1. 检查环境变量 QT_QPA_PLATFORM
  2. 查找 qt.conf 文件中的 [Platforms] 段落
  3. 根据系统默认规则选择平台(如 xcb for X11)
  4. 加载对应插件 .so 文件(位于 $QTDIR/plugins/platforms/

但在 DDE 场景下,我们并不替换整个平台插件(如改用 wayland ),而是通过 QT_QPA_PLATFORMTHEME 环境变量指定一个"主题平台插件"。这正是 Qt5integration 发挥作用的关键切入点。

以典型的 Qt5 应用启动为例,其插件加载逻辑可简化为以下伪代码:

cpp 复制代码
// qtbase/qguiapplication.cpp
int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);

    // Step 1: Determine platform plugin (e.g., xcb)
    QString platformName = resolvePlatformPlugin();

    // Step 2: Load platform theme plugin
    QString themeName = qgetenv("QT_QPA_PLATFORMTHEME");
    if (!themeName.isEmpty()) {
        QPlatformTheme *theme = QPlatformThemeFactory::create(themeName);
        if (theme) {
            app.setPlatformTheme(theme);  // 注入 DDE 主题逻辑
        }
    }

    // Step 3: Initialize QPlatformIntegration
    QPlatformIntegration *integration = QPlatformIntegrationFactory::create(platformName);
    return app.exec();
}

逐行逻辑分析:

  • 第 6 行: resolvePlatformPlugin() 根据环境变量或配置文件确定主平台(通常是 xcb )。
  • 第 9--10 行:读取 QT_QPA_PLATFORMTHEME 环境变量,常见值为 deepindde
  • 第 11--13 行:通过工厂模式创建对应的 QPlatformTheme 实现类(如 DDeepinTheme )。
  • 第 15 行:将主题对象注入 QGuiApplication ,后续控件样式、调色板、字体等均从此获取。
  • 第 17 行:加载主平台插件(如 libqxcb.so ),但此时已携带 DDE 主题上下文。

这意味着即使应用本身未显式调用任何 DDE API,只要设置了 QT_QPA_PLATFORMTHEME=dde ,就能自动获得 DDE 风格的主题支持。这也是为什么许多第三方 Qt 应用(如 VirtualBox、Wine-Qt)能在 DDE 中呈现出一致外观的根本原因。

2.1.3 QtPlatformTheme 与 DDE 主题资源的绑定方式

QPlatformTheme 是 Qt 提供的一个抽象基类,用于集中管理跨平台的 UI 资源。DDE 实现了自定义子类 DDeepinTheme ,并在其中重写了多个虚函数以返回 DDE 特有的资源配置。以下是关键方法及其作用:

方法名 返回内容 示例实现
themeHint(TitleBarShowCloseButton) 是否显示标题栏关闭按钮 true
palette(UiElement::Window) 窗口背景色 #F0F0F0
font(UiElement::PushButton) 按钮字体 Noto Sans, 10pt
icon(Material::Folder) 图标资源路径 /usr/share/icons/deepin/scalable/actions/folder.svg
styleOverride() 强制使用的 QStyle 名称 "deepin"

DDeepinTheme 在构造时会解析 DDE 的全局主题配置文件(通常位于 /usr/share/themes/deepin/gtk-3.0/gtk.css 或专用 .json 文件),并将颜色、字体、圆角半径等属性映射为 Qt 可识别的形式。例如:

cpp 复制代码
QVariant DDeepinTheme::themeHint(QPlatformTheme::ThemeHint hintType) const {
    switch (hintType) {
    case QPlatformTheme::ItemViewActivateItemOnSingleClick:
        return QVariant(false);  // 双击激活列表项
    case QPlatformTheme::PasswordMaskDelay:
        return QVariant(500);    // 密码掩码延迟 500ms
    case QPlatformTheme::CursorFlashTime:
        return QVariant(800);    // 光标闪烁周期
    default:
        return QCommonTheme::themeHint(hintType);
    }
}

参数说明与扩展分析:

  • hintType :枚举类型,表示请求的具体 UI 行为或资源。
  • 函数返回 QVariant 类型,便于传递整数、布尔、字符串等多种数据。
  • 若当前主题未覆盖某项特性,则回退至 QCommonTheme 默认值,保证兼容性。
  • 此机制允许 DDE 在不影响 Qt 内核的前提下,精细化控制每一项交互细节。

此外, DDeepinTheme 还负责监听系统级别的主题变更信号。当用户在控制中心切换深色/浅色模式时,DDE 会通过 D-Bus 发送 com.deepin.Appearance 接口的 ThemeChanged 信号, Qt5integration 插件捕获该信号后触发 QEvent::PaletteChange 事件,促使所有 Qt 窗口重新加载调色板。

sequenceDiagram participant ControlCenter participant DBus participant Qt5integration participant QWidget ControlCenter->>DBus: Emit ThemeChanged("dark") Dbus->>Qt5integration: Receive signal Qt5integration->>Qt5integration: Update internal palette Qt5integration->>QWidget: Send PaletteChange event QWidget->>QWidget: Repaint using new colors

该机制确保了主题切换的实时性与一致性,避免出现部分窗口更新、部分滞后的尴尬局面。

3. 主题样式集成(Theme Integration)实现机制

在现代桌面环境中,应用程序的视觉一致性是提升用户体验的关键要素之一。Qt5integration 插件作为 Deepin 桌面环境(DDE)与 Qt5 应用程序之间的桥梁,其核心任务之一便是确保所有基于 Qt5 构建的应用能够无缝融入 DDE 的整体设计语言。其中, 主题样式集成(Theme Integration) 是这一过程的核心环节。它不仅涉及控件外观的重绘、色彩体系的统一,还包含对高 DPI 显示的支持、动态主题切换响应以及用户自定义样式的兼容处理。本章将深入剖析 Qt5integration 如何通过多层次机制实现主题样式的深度整合,并以实际案例说明其工程实践路径。

3.1 DDE 主题系统的结构与资源组织

DDE 的主题系统并非简单的配色方案堆叠,而是一套高度模块化、可扩展且支持运行时热插拔的视觉资源配置框架。该系统通过标准化目录结构和元数据描述文件,实现了从全局风格到具体控件绘制逻辑的精细控制,为 Qt5integration 提供了稳定可靠的样式数据源。

3.1.1 主题目录布局与 qss 文件加载路径

DDE 主题资源通常存储于 /usr/share/themes/~/.local/share/themes/ 目录下,每个主题以独立子目录形式存在,例如 deepin-darkdeepin-light 。每个主题目录内部遵循如下典型结构:

bash 复制代码
/usr/share/themes/deepin-dark/
├── index.theme          # 主题元信息描述
├── gtk-3.0/             # GTK 应用使用
├── kde-settings/        # KDE 环境适配
└── qt5ct/               # Qt 配置模板
    └── qt5ct.conf

尽管 Qt 自身不直接读取 .theme 文件,但 index.theme 中定义的主题名称、作者、继承关系等信息会被 DDE 控制中心用于 UI 展示和策略判断。真正的 Qt 样式定义由 Qt5integration 在运行时动态生成或加载 QSS(Qt Style Sheet)片段。

Qt5integration 并未依赖传统的 qss 文件硬编码方式,而是采用 运行时构造 QStyle 子类 + 动态注入 QSS 片段 的混合模式。其关键加载流程如下所示:

graph TD A[启动 Qt 应用] --> B{检查 QT_QPA_PLATFORMTHEME} B -- 设置为 'dde' --> C[加载 libqt5integration.so] C --> D[读取当前激活主题名] D --> E[从 /usr/share/dde/themes 获取主题配置] E --> F[解析 color.json, font.json, scale.json] F --> G[构建 QProxyStyle 子类实例] G --> H[应用动态生成的 QSS 到 QApplication] H --> I[完成主题初始化]

上述流程图展示了 Qt5integration 如何在平台插件层面拦截主题加载过程,结合系统级配置实现跨应用一致的视觉呈现。

该机制的优势在于避免了静态 qss 文件难以维护的问题,同时允许不同分辨率、缩放比例下的差异化渲染参数自动适配。

参数说明:
  • QT_QPA_PLATFORMTHEME=dde :强制 Qt 使用 dde 平台主题插件。
  • /usr/share/dde/themes/<theme-name>/colors.json :包含主色调、背景色、文字色等语义化颜色映射。
  • QApplication::setStyleSheet() :最终调用接口,注入生成的 QSS 字符串。

3.1.2 色彩方案、图标集与字体策略的统一管理

为了实现真正意义上的"视觉融合",DDE 主题系统对三大基础元素进行了集中管理:色彩、图标与字体。

类型 配置文件路径 数据格式 加载时机
色彩方案 /usr/share/dde/themes/*/colors.json JSON 应用启动 & 主题切换
图标集 /usr/share/icons/Deepin/* SVG/PNG 运行时按需加载
字体策略 /etc/fonts/conf.d/60-deepin.conf Fontconfig XML 启动时注册

colors.json 为例,其部分内容如下:

json 复制代码
{
  "ColorScheme": {
    "WindowBackground": "#202020",
    "WindowText": "#E0E0E0",
    "ButtonBackground": "#303030",
    "ButtonText": "#FFFFFF",
    "Highlight": "#3D6BDB"
  },
  "Opacity": 0.95
}

这些语义化颜色名称被 Qt5integration 映射至 QPalette 的对应角色,例如 QPalette::Window 对应 WindowBackground ,并通过 QStyleOption 在绘制时传递给各控件。

对于图标资源,DDE 使用 Deepin Icon Theme,优先提供 SVG 格式以支持任意缩放。Qt5integration 通过重写 QIcon::fromTheme() 的行为,使其优先查找 Deepin 主题而非系统默认的 hicolor ,从而保证图标风格一致性。

字体方面,DDE 设定默认中文字体为 "Noto Sans CJK SC",英文字体为 "Cantarell"。这些设置通过 fontconfig 规则全局生效,Qt 应用无需额外配置即可继承。

3.1.3 动态主题切换事件的监听与响应机制

传统 Qt 应用往往需要重启才能应用新主题,而 DDE 实现了真正的运行时主题热切换。其实现依赖于 D-Bus 信号广播 + QEvent 子类通知 双通道机制。

当用户在控制中心更改主题时, dde-daemon 发出 D-Bus 信号:

cpp 复制代码
// D-Bus 接口:com.deepin.daemon.Appearance
void ThemeChanged(QString type, QString name);

Qt5integration 注册了对应的 QDBusConnection::sessionBus() 监听器:

cpp 复制代码
QDBusConnection::sessionBus().connect(
    "com.deepin.daemon.Appearance",
    "/com/deepin/daemon/Appearance",
    "com.deepin.daemon.Appearance",
    "ThemeChanged",
    this,
    SLOT(onThemeChanged(QString,QString))
);

收到信号后,执行以下操作:

cpp 复制代码
void DDEThemeManager::onThemeChanged(const QString &type, const QString &name) {
    if (type == "dark" || type == "light") {
        reloadColors();           // 重新加载 colors.json
        applyGlobalQss();         // 重建并应用 QSS
        qApp->paletteChanged(qApp->palette()); // 触发 palette 更新
        QCoreApplication::postEvent(qApp, new DDEThemeChangeEvent);
    }
}

随后,所有继承自 QWidget 的对象可通过重写 customEvent() 来响应主题变更:

cpp 复制代码
void MyWidget::customEvent(QEvent *e) {
    if (e->type() == DDEThemeChangeEvent::Type) {
        update(); // 触发重绘
    }
}

这种方式确保了即使复杂嵌套的界面也能在毫秒级内完成整体刷新,极大提升了交互流畅性。

3.2 Qt 控件样式的定制化重绘

虽然 QSS 提供了强大的声明式样式能力,但在某些复杂控件(如组合框、滑块、标签页)上仍无法完全替代原生绘制逻辑。为此,Qt5integration 采用了 QStyle 子类拦截 + drawControl 钩子注入 的底层技术路线,实现对 Qt 控件渲染流程的精准控制。

3.2.1 基于 QStyle::drawControl 的控件绘制拦截

Qt 的 QStyle 类是所有控件外观绘制的中枢。每一个标准控件(如按钮、菜单项)在绘制前都会调用 QStyle::drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) 方法。Qt5integration 继承 QCommonStyle 并在其基础上实现 DDEStyle 类,在 drawControl 中插入特定绘制逻辑。

cpp 复制代码
class DDEStyle : public QCommonStyle {
    Q_OBJECT
public:
    void drawControl(ControlElement element, const QStyleOption *opt,
                     QPainter *p, const QWidget *w = nullptr) const override;
};

关键代码片段如下:

cpp 复制代码
void DDEStyle::drawControl(ControlElement ce, const QStyleOption *opt,
                           QPainter *p, const QWidget *w) const {
    switch (ce) {
    case CE_PushButton:
        drawDDEButton(opt, p, w);
        break;
    case CE_ComboBoxLabel:
        drawDDEComboBoxLabel(opt, p, w);
        break;
    case CE_MenuItem:
        drawDDEMenuItem(opt, p, w);
        break;
    default:
        QCommonStyle::drawControl(ce, opt, p, w); // 回退到默认绘制
    }
}

逻辑分析

  • ce 表示当前要绘制的控件类型,枚举值来自 QStyle::ControlElement

  • opt 包含控件状态(是否按下、聚焦、禁用等)、文本、图标等上下文信息。

  • p 是绘图设备上下文,可在其上调用 drawRect , fillPath 等方法进行自定义绘制。

  • 若不属于 DDE 特殊处理范围,则交由基类默认实现,保障兼容性。

此机制允许开发者完全掌控像素级别的输出,例如实现圆角矩形按钮、渐变背景、阴影效果等高级视觉特征。

3.2.2 QPushButton、QComboBox 等常用控件的视觉对齐

QPushButton 为例,DDE 要求其具备以下特性:

  • 圆角半径:6px

  • 默认背景透明,悬停时轻微变暗

  • 按下时产生内凹反馈

  • 文字居中且垂直对齐

实现代码如下:

cpp 复制代码
void DDEStyle::drawDDEButton(const QStyleOption *opt, QPainter *p, const QWidget *) const {
    const auto *btn = qstyleoption_cast<const QStyleOptionButton *>(opt);
    QRect rect = btn->rect;

    // 抗锯齿开启
    p->setRenderHint(QPainter::Antialiasing);

    // 构造圆角路径
    QPainterPath path;
    path.addRoundedRect(rect.adjusted(1,1,-1,-1), 6, 6);

    QColor baseColor = getColorFromTheme("ButtonBackground");
    QColor hoverColor = baseColor.darker(110);
    QColor pressColor = baseColor.darker(130);

    QColor fillColor;
    if (btn->state & State_Sunken)
        fillColor = pressColor;
    else if (btn->state & State_MouseOver)
        fillColor = hoverColor;
    else
        fillColor = baseColor;

    p->fillPath(path, fillColor);
    p->setPen(QColor(0,0,0,10));
    p->drawPath(path);

    // 绘制文字(使用主题字体)
    if (!btn->text.isEmpty()) {
        p->setPen(getColorFromTheme("ButtonText"));
        p->drawText(rect, Qt::AlignCenter, btn->text);
    }
}

参数说明

  • getColorFromTheme() :从 colors.json 查询语义色。

  • State_SunkenState_MouseOver :Qt 定义的状态标志位。

  • adjusted(1,1,-1,-1) :收缩矩形防止边缘裁剪。

类似地, QComboBox 的下拉箭头也被替换为 DDE 风格的三角图标,并统一使用 SVG 渲染以适应缩放。

3.2.3 高分辨率屏幕下的像素对齐与缩放补偿

在 HiDPI 显示器上,若不进行特殊处理,控件可能出现模糊、错位等问题。Qt5integration 引入了 设备像素比感知绘制层 来解决此问题。

cpp 复制代码
qreal devicePixelRatio = qApp->devicePixelRatio();
QRectF logicalRect = opt->rect;
QRectF physicalRect = logicalRect * devicePixelRatio;

p->scale(devicePixelRatio, devicePixelRatio);
drawCustomControl(physicalRect.toAlignedRect(), p); // 确保整数坐标
p->scale(1/devicePixelRatio, 1/devicePixelRatio);

此外,主题配置中引入 scale.json 文件,定义不同 DPI 区间的控件尺寸调整策略:

json 复制代码
{
  "1.0": { "button-height": 32 },
  "1.5": { "button-height": 48 },
  "2.0": { "button-height": 64 }
}

Qt5integration 在 sizeFromContent* 系列方法中读取该配置,动态返回合适的控件大小建议,确保布局美观。

3.3 样式表(QSS)与原生绘制的协同策略

理想状态下,应尽可能使用 QSS 降低开发成本;但面对性能敏感或复杂动画场景,原生 C++ 绘制仍是首选。Qt5integration 设计了一套 分层协同模型 ,使两者共存而不冲突。

3.3.1 用户自定义 QSS 与系统主题的优先级判定

当用户在应用中设置了 setStyleSheet(...) ,如何避免覆盖 DDE 主题?解决方案是引入 样式优先级层级

层级 来源 是否可被覆盖 示例
L0 系统主题(QStyle) 按钮圆角、阴影
L1 DDE 动态 QSS 全局颜色变量注入
L2 用户 QSS 开发者手动 setStyleSheet
L3 控件 inline-style widget->setStyleSheet()

实现方式是在 QApplicationPrivate::setStyleSheet() 周围添加钩子,检测当前样式来源:

cpp 复制代码
void DDEStylePlugin::ensureSystemStylesheet() {
    QString systemQss = generateSystemQss(); // 包含 color variables
    if (!qApp->styleSheet().contains(systemQss.left(50))) {
        qApp->setStyleSheet(systemQss + "\n" + qApp->styleSheet());
    }
}

这样即使用户修改样式,也不会丢失主题基础变量定义,如:

qss 复制代码
@button-bg: #303030;
QPushButton { background: @button-bg; border-radius: 6px; }

3.3.2 防止样式冲突的命名空间隔离技术

为了避免第三方库或旧版 Qt 插件破坏样式一致性,Qt5integration 采用 CSS-like 命名空间前缀机制

qss 复制代码
/* 所有 DDE 样式规则包裹在 dde-* 命名空间内 */
dde-QPushButton:hover {
    background: @hover-bg;
}
dde-QComboBox::drop-down {
    image: url(:/icons/dde-arrow-down.svg);
}

同时,在 QStyleFactory 创建时注入一个代理类:

cpp 复制代码
class DDEStyleProxy : public QProxyStyle {
    QString name() const override { return "dde"; }
    void unpolish(QWidget *w) override {
        if (w->property("_dde_namespaced").toBool())
            w->setStyleSheet(""); // 清除非命名空间样式
        QProxyStyle::unpolish(w);
    }
};

该机制有效遏制了外部样式污染。

3.3.3 运行时样式热更新机制的设计与实现

支持实时预览是现代主题系统的标配功能。Qt5integration 利用 inotify 监视 /usr/share/dde/themes/current/ 目录变化,并触发样式重建:

cpp 复制代码
QFileSystemWatcher *watcher = new QFileSystemWatcher(this);
watcher->addPath("/usr/share/dde/themes/current/colors.json");
connect(watcher, &QFileSystemWatcher::fileChanged, this, [this]() {
    reloadTheme();
    QTimer::singleShot(100, qApp, [](){
        qApp->style()->polish((QWidget*)nullptr); // 触发全局重绘
    });
});

配合前端工具(如 deepin-editor-theme-preview),设计师可边改 JSON 边查看效果,极大提升开发效率。

3.4 实践案例:使 Qt Creator 完全匹配 DDE 主题

Qt Creator 作为 Qt 官方 IDE,默认使用 Fusion 或 Windows 风格,与 DDE 差异明显。我们通过注入 Qt5integration 插件实现完美融合。

3.4.1 分析 Qt Creator 默认样式的渲染差异

启动 Qt Creator 并观察以下问题:

  • 左侧侧边栏背景为灰色,与 DDE 深色主题不符

  • 按钮无圆角

  • 菜单字体偏小

  • 状态栏分割线明显

使用 qdebug << QApplication::style()->objectName(); 输出当前样式名为 "windowsvista" ,说明未启用平台主题。

3.4.2 注入 DDE 风格 QStyle 子类并验证效果

创建启动脚本:

bash 复制代码
#!/bin/bash
export QT_QPA_PLATFORMTHEME=dde
export QT_STYLE_OVERRIDE=dde
exec /usr/bin/qtcreator "$@"

重启后发现大部分控件已适配,但侧边栏仍异常。进一步检查发现其使用 QDockWidget 并设置了固定背景色:

cpp 复制代码
dock->setStyleSheet("background: gray;");

此时需通过 样式补丁注入 解决:

cpp 复制代码
class DDEFixPlugin : public QObject, public QDesignerCustomWidgetInterface {
    bool eventFilter(QObject *obj, QEvent *ev) override {
        if (auto *dw = qobject_cast<QDockWidget*>(obj)) {
            if (ev->type() == QEvent::Polish) {
                dw->setStyleSheet("background: %1".arg(getColor("PanelBackground")));
            }
        }
        return false;
    }
};

注册为 Qt 插件后自动生效。

3.4.3 解决侧边栏高亮色不一致的问题

原始高亮色为蓝色 (#4a90d9),而 DDE 使用 (#3D6BDB)。通过调试工具获取控件类名为 Core__Internal__NavigationWidget ,属于私有命名空间。

最终解决方案是在 DDEStyle::drawControl 中增加特例:

cpp 复制代码
if (w && w->metaObject()->className() ==
        "Core::Internal::NavigationWidget") {
    // 强制使用 DDE 高亮色
    option->palette.setColor(QPalette::Highlight, ddeHighlightColor);
}

至此,Qt Creator 完全融入 DDE 主题,达到"原生感"体验。

4. 输入法框架集成(Input Method Integration)配置方法

在现代 Linux 桌面环境中,中文及其他复杂文字输入的流畅性直接影响用户体验。尽管 Qt5 提供了高度抽象的输入法接口支持,但在非 KDE 环境中,尤其是深度桌面环境(DDE)这类以 GTK+ 风格为主导的设计体系下,Qt 应用常面临输入法无法正常弹出、候选词窗口错位、光标偏移甚至输入中断等问题。 Qt5integration 插件通过深度介入 Qt 平台抽象层(QPA),构建了一套与 DDE 输入法服务紧密协同的输入法集成机制。本章将系统阐述 Qt5integration 如何实现对 IBus 和 Fcitx 等主流输入法框架的支持,解析其事件转发模型,并提供可操作的调试与修复方案。

4.1 Linux 输入法框架(IBus/Fcitx)与 Qt 的交互模型

Linux 下的输入法系统长期依赖于 X Window System 的 XIM(X Input Method)协议,但随着 Wayland 成为主流显示服务器的发展趋势,新的输入法协议逐步取代传统机制。Qt 作为跨平台 GUI 框架,必须兼容多种底层输入法架构,而 Qt5integration 正是这一兼容性的关键枢纽。

4.1.1 Qt 的 QInputMethod 框架职责划分

Qt 的输入法处理由 QInputMethod 类为核心展开,它位于 QtGui 模块中,负责管理输入上下文(Input Context)、软键盘请求、文本预测以及候选词展示等高级语义行为。然而, QInputMethod 本身并不直接与输入法引擎通信,而是通过平台插件中的 QPlatformInputContext 实现桥接。

cpp 复制代码
class MyInputContext : public QPlatformInputContext {
    Q_OBJECT
public:
    bool isValid() const override { return true; }
    void reset() override { /* 清除当前输入状态 */ }
    void commit() override {
        emit commitString(preeditString);
    }
    void update(Qt::InputMethodQueries queries) override {
        if (queries & Qt::ImCurrentInputMethod) {
            setInputMethodQueryResult(Qt::ImCurrentInputMethod, "fcitx");
        }
    }
};

代码逻辑逐行解读

  • 第 2 行:定义一个继承自 QPlatformInputContext 的自定义输入上下文类。

  • 第 4 行: isValid() 返回 true 表示该输入上下文可用。

  • 第 6--8 行: reset() 方法用于清空预编辑内容(如拼音输入过程中的中间状态)。

  • 第 9--12 行: commit() 将预编辑字符串提交为最终文本,触发 commitString 信号。

  • 第 13--17 行: update() 响应查询请求,例如告知应用当前使用的输入法名称。

该类需注册到 Qt 的插件系统中,方可被运行时加载。其作用在于将来自输入法守护进程(如 fcitxibus-daemon )的原始事件转换为 Qt 可识别的高层语义指令。

层级 组件 职责
应用层 QWidget / QTextEdit 接收并渲染输入内容
Qt 抽象层 QInputMethod 管理输入状态和用户反馈
平台插件层 QPlatformInputContext 子类 连接本地输入法服务
系统服务层 fcitx/ibus-daemon 处理语言模型与候选词生成
显示协议层 X11/Wayland 传递键盘事件与窗口定位信息

上述表格展示了输入法数据流在各层级间的流转路径。 Qt5integration 的核心任务就是在"平台插件层"注入适配 DDE 环境的 QPlatformInputContext 实现,从而打通从系统服务到 Qt 控件的数据通路。

flowchart TD A[用户按键] --> B{X11/Wayland} B --> C[fcitx/ibus-daemon] C --> D[Qt5integration::DdeInputContext] D --> E[QInputMethod] E --> F[QTextEdit/QLineEdit] F --> G[显示预编辑文本] style D fill:#e6f3ff,stroke:#007acc

流程图说明 :蓝色节点表示 Qt5integration 主动介入的关键模块。当用户输入拼音时,Fcitx 解析后通过 D-Bus 发送候选词列表和光标位置信息,由 DdeInputContext 接收并封装成 Qt 标准事件,最终驱动控件更新界面。

4.1.2 XIM、X11 Input Method Protocol 基础机制回顾

XIM(X Input Method)是 X11 协议族中专为多语言输入设计的扩展机制。它允许客户端(即应用程序)与 IM Server(输入法服务器)建立独立连接通道,传输预编辑文本、候选词窗口坐标及属性信息。

典型流程如下:

  1. 客户端调用 XOpenIM() 获取输入方法对象;
  2. 创建输入上下文 XCreateIC() ,绑定焦点窗口;
  3. 当获得输入焦点时,调用 XSetICFocus() 激活输入会话;
  4. 输入法服务器发送 XIMPreeditDraw 事件更新预编辑区内容;
  5. 用户确认输入后,服务器发送 XIMCommit 事件提交文本。

然而,XIM 存在严重缺陷:缺乏标准化的候选词窗口布局策略,且无法精确控制光标在富文本中的像素级位置。这导致 Qt 在高 DPI 屏幕或复杂布局容器(如 QTextBrowser )中频繁出现光标漂移问题。

为此, Qt5integration 在 X11 模式下引入代理机制,在 QPlatformInputContext::showInputPanel() 中动态计算候选词窗口的理想坐标:

cpp 复制代码
void DdeInputContext::showInputPanel()
{
    QRect cursorRect = inputMethod()->cursorRectangle();
    QPoint globalPos = cursorRect.bottomLeft();
    QPoint screenPos = QApplication::widgetAt(globalPos)->mapToGlobal(QPoint(0,0));
    QPoint finalPos = screenPos + cursorRect.bottomLeft();

    // 发送 D-Bus 消息至 dde-desktop 输入面板服务
    QDBusMessage msg = QDBusMessage::createSignal(
        "/org/freedesktop/TextInput",
        "org.freedesktop.TextInput", "UpdateSpot"
    );
    msg << finalPos.x() << finalPos.y();
    QDBusConnection::sessionBus().send(msg);
}

参数说明

  • cursorRectangle() :获取当前输入光标的矩形区域,单位为像素。

  • mapToGlobal() :将局部坐标转换为屏幕全局坐标。

  • QDBusMessage::createSignal :构造一个 D-Bus 信号,通知 DDE 桌面服务更新输入法悬浮窗位置。

此机制绕开了 XIM 的局限性,利用 DDE 自有的输入面板服务进行精准定位,显著提升了输入体验的一致性。

4.1.3 Wayland 下输入法协议(zwp_text_input_v3)适配现状

Wayland 不再支持 XIM,转而采用更为现代的 zwp_text_input_v3 协议。该协议由 Weston 引入,现已广泛应用于 Sway、KDE Plasma 和 Deepin 的 Wayland 会话中。

zwp_text_input_v3 的工作模式基于显式声明输入区域和异步事件响应。Qt 需要在以下时机调用对应方法:

  • 获得焦点 → zwp_text_input_v3.enable()
  • 光标移动 → zwp_text_input_v3.set_cursor_rectangle(x,y,width,height)
  • 关闭输入 → zwp_text_input_v3.disable()

Qt5integration 在 Wayland 后端实现了对 libdecordde-kwin 的兼容封装,确保即使窗口装饰由 DDE 自定义绘制,也能正确传递输入区域信息。

cpp 复制代码
// 在 QPlatformWindow::setGeometryChanged() 中触发更新
void DdeWaylandInputContext::updateCursorRect(const QRect &rect)
{
    if (textInput && isEnabled) {
        zwp_text_input_v3_set_cursor_rectangle(
            textInput,
            rect.x(), rect.y(),
            rect.width(), rect.height()
        );
        zwp_text_input_v3_commit(textInput);
    }
}

执行逻辑分析

  • 函数接收控件内光标矩形 rect ,将其传给 Wayland 输入法协议。

  • zwp_text_input_v3_commit() 是必需调用,否则变更不会生效。

  • 所有操作必须在 Wayland event loop 上下文中执行,避免线程竞争。

目前主要挑战在于部分旧版 Qt 应用未正确监听 Qt::ImQueryInput 类型的输入方法查询,导致 set_cursor_rectangle 数据为空。对此, Qt5integration 提供了 fallback 机制:若无有效光标信息,则使用控件中心点作为候选词窗口锚点。

4.2 Qt5integration 对输入法事件的转发机制

为了实现无缝输入体验, Qt5integration 必须重写默认的输入事件处理链,建立一条从系统输入法服务到 Qt 控件的高效转发路径。

4.2.1 重写 QPlatformInputContext 的必要性

标准 Qt 构建通常使用 qtvirtualkeyboardibusplatforminputcontext 作为默认输入上下文。但在 DDE 中,这些实现无法感知主题风格、窗口层级或任务栏布局,极易造成候选词窗口被遮挡或跟随失效。

因此, Qt5integration 提供了一个名为 ddeplatforminputcontext 的专用插件,其源码结构如下:

复制代码
src/platforminputcontexts/dde/
├── ddeinputcontext.cpp
├── ddeinputcontext.h
├── ddeinputcontextplugin.cpp
└── main.cpp

其中 main.cpp 定义插件入口:

cpp 复制代码
#include <QPlatformInputContextPlugin>

class DdeInputContextPlugin : public QPlatformInputContextPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QPlatformInputContextFactoryInterface" FILE "dde.json")

public:
    QPlatformInputContext *create(const QString &key, const QStringList &paramList) override {
        if (key.toLower() == "dde") {
            return new DdeInputContext(paramList);
        }
        return nullptr;
    }
};

关键点说明

  • Q_PLUGIN_METADATA 注册插件元信息,文件 dde.json 包含 "Keys": ["dde"]

  • create() 方法根据 QT_IM_MODULE=dde 判断是否启用本插件。

  • 若未设置环境变量,则 fallback 至系统默认输入上下文。

该插件在启动时自动加载,替代原生实现,成为所有 Qt5 应用的统一输入门面。

4.2.2 输入法窗口定位与候选词同步策略

候选词窗口的视觉一致性是衡量集成质量的重要指标。 Qt5integration 采用三级定位策略:

  1. 精确匹配 :优先依据 QInputMethod::cursorRectangle() 计算;
  2. 控件对齐 :若光标不可见(如对话框刚弹出),则对齐父控件底部;
  3. 屏幕安全区避让 :检测是否靠近屏幕边缘或任务栏,自动调整方向。

具体实现如下表所示:

条件 计算方式 示例场景
光标可见 widget->mapToGlobal(cursorRect.bottomLeft()) + offset 编辑器内连续输入
光标隐藏 parentWidget->geometry().bottomCenter() 登录框自动聚焦
靠近底边 position - QPoint(0, candidateHeight + margin) 全屏视频下的搜索框

此外,候选词内容通过 D-Bus 双向同步:

cpp 复制代码
// 接收来自 fcitx 的候选词更新
void DdeInputContext::onCandidateUpdated(const QStringList &candidates)
{
    QVariantMap attrs;
    attrs["labels"] = candidates;
    attrs["selection"] = currentSelectedIndex;

    QInputMethodEvent event;
    event.setAttributes({{QInputMethodEvent::Selection, 0, candidates.size()}});
    event.setPreeditString(generatePreeditString(), {}, attrs);

    QCoreApplication::sendEvent(focusObject, &event);
}

逻辑分析

  • onCandidateUpdated 是 DBus 信号槽,接收 Fcitx 发来的候选列表。

  • 构造 QInputMethodEvent 并附加属性,使前端控件能区分"正在输入"与"可选词"。

  • 使用 sendEvent 直接触发目标对象的输入处理流程,绕过事件队列延迟。

4.2.3 键盘焦点变化时的上下文重建流程

焦点切换是输入法最常见的中断源。 Qt5integration 通过监听 QEvent::FocusInFocusOut 事件实现上下文动态重建。

cpp 复制代码
bool DdeInputContext::filterEvent(QObject *obj, QEvent *ev)
{
    switch (ev->type()) {
    case QEvent::FocusIn:
        activateContext(static_cast<QWidget*>(obj));
        break;
    case QEvent::FocusOut:
        deactivateContext();
        break;
    default:
        break;
    }
    return false; // 允许事件继续传播
}

参数说明

  • filterEvent 是事件过滤钩子,插入在 Qt 事件分发主循环之前。

  • activateContext() 内部调用 enable() 并重新发送光标位置。

  • 返回 false 表示不拦截事件,保证原有逻辑不受影响。

此机制确保即便在 Tab 切换或鼠标点击不同输入框时,也能立即激活对应输入法会话,避免"死锁"现象。

sequenceDiagram participant App as Qt Application participant IC as DdeInputContext participant IME as Fcitx Daemon App->>IC: FocusIn(QWidget) IC->>IME: enable(), set_cursor_rect(...) IME-->>IC: PreeditStart IC->>App: QInputMethodEvent(Preedit) App->>IC: KeyPress(Enter) IC->>IME: commit current string IME-->>IC: CommitString("你好") IC->>App: QInputMethodEvent(Commit)

时序图说明 :完整展示一次中文输入的生命周期。从焦点进入开始,经历预编辑、候选词选择到最后提交,全程由 DdeInputContext 协调两端协议差异。

4.3 输入法兼容性问题诊断与修复

尽管 Qt5integration 已极大提升兼容性,但在特定应用场景中仍可能出现异常。

4.3.1 中文输入过程中光标偏移的根源分析

常见表现为:拼音输入时,候选词窗口出现在屏幕左上角,或光标在 QTextEdit 中跳跃。根本原因在于:

  • 高 DPI 缩放未正确补偿 :Qt 内部使用逻辑像素,而输入法服务使用物理像素;
  • 富文本布局导致几何计算错误QTextLayout 动态换行后, cursorRect 未及时刷新;
  • 异步渲染延迟 :WebEngineView 等组件在 JS 执行后才通知尺寸变更。

解决方案包括:

  1. 强制启用 HiDPI 缩放感知:
bash 复制代码
export QT_ENABLE_HIGHDPI_SCALING=1
export QT_SCALE_FACTOR=1.25
  1. QTextEdit::inputMethodEvent() 中手动修正位置:
cpp 复制代码
void CustomTextEdit::inputMethodEvent(QInputMethodEvent *e)
{
    QTextEdit::inputMethodEvent(e);
    QTimer::singleShot(0, this, [this]() {
        updateCursorRect(); // 强制重算光标
    });
}

4.3.2 WebKit-based 控件(如 QTextEdit)的特殊处理

某些基于 WebKit 的嵌入式控件(如旧版 QtWebEngine)存在输入法事件丢失问题。可通过注入 JavaScript 钩子解决:

js 复制代码
document.addEventListener('focusin', () => {
  const elem = document.activeElement;
  if (elem.contentEditable || elem.tagName === 'INPUT') {
    external.notifyFocus(elem.getBoundingClientRect());
  }
});

配合 C++ 端接收:

cpp 复制代码
QWebChannel *channel = new QWebChannel(this);
channel->registerObject("external", this);
view->page()->setWebChannel(channel);

4.3.3 强制启用 fcitx-platforminputcontext 插件的方法

ddeplatforminputcontext 加载失败,可临时强制使用 Fcitx 官方插件:

bash 复制代码
export QT_IM_MODULE=fcitx
export QT_PLUGIN_PATH=/usr/lib/x86_64-linux-gnu/qt5/plugins

验证插件是否加载:

bash 复制代码
strace -e trace=openat qtcreator 2>&1 | grep -i inputcontext

预期输出包含:

复制代码
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/qt5/plugins/platforminputcontexts/libfcitxplatforminputcontextplugin.so", O_RDONLY) = 3

4.4 实践操作:在 Qt Designer 中测试输入法响应

4.4.1 启动时设置 QT_IM_MODULE=fcitx 环境变量

bash 复制代码
#!/bin/bash
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
exec qtcreator

注意:若使用 Flatpak 版本,需通过 flatpak override 设置环境变量。

4.4.2 使用 dbus 监控输入法状态变化

bash 复制代码
dbus-monitor --session "interface='org.fcitx.Fcitx.InputMethod'" &
qtcreator

观察是否有如下信号:

复制代码
signal time=... sender=:1.10 -> destination=(null destination) serial=... path=/inputmethod; interface=org.fcitx.Fcitx.InputMethod; member=Activated

4.4.3 验证复杂场景下(如弹出对话框)的输入连续性

创建一个按钮,点击后弹出 QInputDialog::getText() ,测试是否能在新窗口中立即输入中文。若失败,检查:

  • 是否调用了 QWidget::setFocus()
  • 是否启用了 WA_InputMethodEnabled 属性;
  • 是否存在模态阻塞导致事件循环停滞。

推荐添加日志辅助:

cpp 复制代码
qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &msg) {
    if (msg.contains("input") || msg.contains("IM"))
        fprintf(stderr, "[%d] %s\n", type, qPrintable(msg));
});

5. 菜单系统一致性处理(Menu Integration)技术方案

在现代桌面环境中,菜单系统的视觉与行为一致性是衡量用户体验成熟度的重要指标之一。对于基于 Qt5 框架开发的应用程序而言,在非 KDE 环境中实现与原生桌面环境的菜单融合一直是一项挑战。深度桌面环境(DDE)通过 Qt5integration 插件引入了对全局菜单栏的完整支持,使得原本内嵌于窗口顶部的 QMenuBar 能够无缝迁移至屏幕顶部的统一菜单栏区域。这种集成不仅提升了界面整洁性,也增强了跨应用操作的一致性。

本章将深入探讨 DDE 如何利用 D-Bus 通信机制、Qt 平台抽象层扩展以及生命周期管理策略,完成从本地控件到全局服务的数据映射与交互闭环。我们将剖析菜单发布流程的技术细节,包括接口调用规范、数据结构转换逻辑、动态更新机制,并结合实际案例说明如何使第三方 Qt 应用(如 VNote)完全融入 DDE 的全局菜单体系。

5.1 D-Bus 菜单协议与全局菜单服务

D-Bus 作为 Linux 桌面环境下进程间通信的核心基础设施,在 DDE 的全局菜单系统中扮演着中枢角色。所有应用程序不再直接渲染菜单 UI,而是通过特定的 D-Bus 接口向中央菜单服务注册其菜单结构,由桌面外壳(dde-desktop 或 dde-dock)统一绘制和响应事件。这一设计实现了资源集中化管理,避免了多窗口重叠导致的视觉混乱,同时为高 DPI 缩放、暗色主题适配等特性提供了统一控制点。

5.1.1 com.deepin.menu 接口的调用规范

DDE 定义了一个专有的 D-Bus 接口 com.deepin.menu ,运行在系统总线(system bus)或会话总线(session bus)上,具体取决于部署模式。该接口提供以下关键方法:

方法名 参数类型 功能描述
RegisterWindow (uint32_t windowId, string menuJson) 注册指定窗口 ID 对应的菜单树
UnregisterWindow (uint32_t windowId) 移除指定窗口的菜单注册
UpdateMenu (uint32_t windowId, string menuJson) 更新已注册窗口的菜单内容
ShowMenu (uint32_t windowId, int x, int y) 强制弹出上下文菜单(用于右键)

其中 menuJson 是一个符合特定 schema 的 JSON 字符串,描述整个菜单层级结构。例如:

json 复制代码
{
  "menu": [
    {
      "text": "文件(&F)",
      "action": "file_menu",
      "items": [
        { "text": "新建(&N)", "action": "new_file", "shortcut": "Ctrl+N" },
        { "text": "打开(&O)", "action": "open_file", "shortcut": "Ctrl+O" },
        { "type": "separator" },
        { "text": "退出(&X)", "action": "quit_app", "enabled": true }
      ]
    },
    {
      "text": "编辑(&E)",
      "action": "edit_menu",
      "items": [
        { "text": "撤销(&U)", "action": "undo", "shortcut": "Ctrl+Z" }
      ]
    }
  ]
}

每个菜单项必须包含唯一标识 action ,以便后续信号回调时能准确识别触发源。

D-Bus 通信建立过程

在 Qt 应用启动并初始化主窗口后, Qt5integration 插件中的 QPlatformMenuBar 实现会监听窗口创建事件,自动提取其 QMenuBar 内容并序列化为上述格式的 JSON 数据。然后通过 QDBusInterface 发起远程调用:

cpp 复制代码
QDBusInterface menuService("com.deepin.menu",
                           "/com/deepin/menu",
                           "com.deepin.menu",
                           QDBusConnection::sessionBus());

QVariantList args;
args << static_cast<uint>(windowId) << menuJsonString;

QDBusMessage reply = menuService.callWithArgumentList(QDBus::AutoDetect,
                                                      "RegisterWindow", args);
if (reply.type() == QDBusMessage::ErrorMessage) {
    qWarning() << "Failed to register menu:" << reply.errorMessage();
}

代码逻辑逐行解读

  • 第1~4行:构造一个指向 com.deepin.menu 服务的代理对象,指定服务名、对象路径、接口名及连接类型。
  • 第6~8行:准备参数列表,依次传入窗口句柄(通常来自 winId() )和序列化后的菜单 JSON。
  • 第10行:使用 callWithArgumentList 发起异步调用,自动匹配方法签名。
  • 第11~13行:检查返回值是否为错误消息,若是则输出警告日志。

该机制确保只要应用遵循标准 Qt 菜单编程模型,即可无感知地接入 DDE 全局菜单系统。

sequenceDiagram participant App as Qt Application participant Plugin as Qt5integration participant DBus as D-Bus Service participant Shell as dde-desktop App->>Plugin: 创建QMainWindow Plugin->>App: 监听menubar变化 Plugin->>Plugin: 序列化菜单为JSON Plugin->>DBus: RegisterWindow(windowId, json) DBus-->>Shell: 通知新菜单到达 Shell->>Shell: 渲染菜单至顶栏 User->>Shell: 点击"打开" Shell->>DBus: emit ActionTriggered(windowId, "open_file") DBus->>Plugin: 转发信号 Plugin->>App: 触发对应QAction::trigger()

上图展示了从菜单注册到用户交互的完整流程。值得注意的是,菜单项激活信号是反向传递的------即由 Shell 经 D-Bus 回调至插件模块,最终触发原始 QActiontriggered() 信号,形成闭环。

5.1.2 Qt 如何通过 libdtk 发布菜单结构

虽然 Qt 原生提供了 QPlatformMenuBar 抽象类用于平台级菜单集成,但 DDE 并未完全依赖此机制,而是结合其自研 UI 框架 DTK (Deepin Tool Kit) 中的 libdtkwidget 库进行增强处理。特别是对于早期版本 Qt 或某些定制构建环境,直接使用 libdtk 提供的 DTitlebarDMenu 类可以更精细地控制菜单行为。

当启用 DTK 支持时, Qt5integration 会在检测到 QMainWindow 使用了 DTitlebar 时,优先采用 DMenuManager 来接管菜单发布逻辑:

cpp 复制代码
#include <DMenuManager>

class DDEMenuHandler : public QObject {
    Q_OBJECT
public:
    void attachToWindow(QWindow *w) {
        if (auto *tlw = qobject_cast<QWidget *>(w->parent())) {
            auto *menuBar = tlw->findChild<QMenuBar*>();
            if (menuBar && DMenuManager::isAvailable()) {
                connect(menuBar, &QMenuBar::aboutToShow,
                        this, &DDEMenuHandler::syncToGlobal);
            }
        }
    }

private slots:
    void syncToGlobal() {
        auto *senderBar = qobject_cast<QMenuBar*>(sender());
        QString json = serializeMenu(senderBar);
        uint wid = senderBar->window()->winId();
        DMenuManager::instance()->updateMenu(wid, json);
    }
};

参数说明与逻辑分析

  • DMenuManager::isAvailable() 判断当前会话是否运行在 DDE 环境下且服务可用。
  • serializeMenu() 是一个递归函数,遍历 QMenuBar 的所有 QAction 并生成兼容 com.deepin.menu 的 JSON。
  • updateMenu() 内部封装了 D-Bus 调用,开发者无需手动处理通信细节。
  • 连接 aboutToShow 保证仅在菜单即将展示时才同步最新状态,减少无效通信。

这种方式相比纯 Qt 平台插件更具灵活性,尤其是在处理动态菜单(如最近文件列表)时表现出更高的实时性。

5.1.3 菜单项激活信号的反向回调机制

当用户点击全局菜单中的某个条目时,dde-desktop 将通过 D-Bus 向 com.deepin.menu 服务发送动作通知,后者再广播 ActionTriggered(uint windowId, QString action) 信号。 Qt5integration 中的 DDEPlatformMenuBar 需要监听该信号并还原为对应的 QAction::trigger() 调用。

其实现核心如下:

cpp 复制代码
class DDEPlatformMenuBar : public QPlatformMenuBar {
    Q_OBJECT
public:
    DDEPlatformMenuBar() {
        dbusWatcher = new QDBusServiceWatcher(
            "com.deepin.menu",
            QDBusConnection::sessionBus(),
            QDBusServiceWatcher::WatchForOwnerChange
        );

        connect(dbusWatcher, &QDBusServiceWatcher::serviceOwnerChanged,
                this, &DDEPlatformMenuBar::onServiceChanged);

        initDBusConnection();
    }

private slots:
    void onActionTriggered(uint winId, const QString &actionKey) {
        if (auto *bar = menuMap.value(winId)) {
            for (auto *action : bar->actions()) {
                if (action->property("dde_action_id").toString() == actionKey) {
                    action->trigger();  // 激活原始 QAction
                    break;
                }
            }
        }
    }
};

扩展说明

  • 所有被发布的 QAction 在序列化前都会被打上 dde_action_id 属性标签,作为跨进程查找的唯一键。
  • menuMap 是一个全局哈希表,记录 <windowId, QMenuBar*> 映射关系。
  • 使用 QDBusServiceWatcher 监测服务存活状态,防止因桌面组件重启导致信号丢失。

该机制确保了即使菜单 UI 被移出应用进程,其语义逻辑仍保留在原生 Qt 框架中执行,保障了代码可维护性和调试便利性。

5.2 Qt 菜单到 DDE 全局菜单的映射转换

尽管 D-Bus 协议定义了通用的数据交换格式,但从 Qt 控件模型到 JSON 表示的转换并非简单遍历。由于涉及快捷键解析、禁用状态同步、分隔符处理等复杂场景,必须设计稳健的映射算法以避免信息丢失或误判。

5.2.1 QMenuBar::nativeWidgets 的识别与提取

Qt 提供了 QWidget::isNativeMenuBar()QMenuBar::useNativeMenuBar() 接口来决定是否启用原生菜单栏。在 DDE 中,我们强制设置 useNativeMenuBar(true) 以激活平台插件介入:

cpp 复制代码
#ifdef Q_OS_LINUX
qputenv("QT_LINUX_ACCESSIBILITY_ALWAYS_ON", "1");
#endif

// 在 QApplication 初始化之后
QMetaObject::invokeMethod(qApp, [=qApp](){
    for (auto *topLevel : qApp->topLevelWidgets()) {
        if (auto *mainWin = qobject_cast<QMainWindow*>(topLevel)) {
            mainWin->menuBar()->setNative(true); // 启用原生样式钩子
        }
    }
}, Qt::QueuedConnection);

一旦启用,Qt 将调用 QPlatformIntegration::createPlatformMenuBar() 创建 QPlatformMenuBar 子类实例,从而进入 Qt5integration 的控制流。

此时可通过 QMenuBarPrivate::nativeWidgets 获取底层绑定状态:

cpp 复制代码
bool isMappedToGlobal(QMenuBar *mb) {
    return mb->testAttribute(Qt::WA_DontShowOnScreen) &&
           !mb->isVisible() &&
           mb->property("_dde_global_menu_bound").toBool();
}

此判断依据三项条件:

  1. WA_DontShowOnScreen :表示该控件不参与本地绘制;
  2. !isVisible() :确认菜单未显式显示;
  3. 自定义属性标记已成功注册至全局服务。

这一步骤至关重要,防止同一菜单被重复发布或遗漏。

5.2.2 QAction 层次结构转为 JSON 格式数据

QAction 树转化为 JSON 需要考虑多个维度的信息合并。以下是一个完整的转换函数框架:

cpp 复制代码
QJsonObject actionToJson(const QAction *act) {
    QJsonObject obj;
    obj["text"] = act->text().remove("&&");  // 去除助记符标记
    obj["action"] = act->objectName().isEmpty() ?
                       QUuid::createUuid().toString() :
                       act->objectName();
    obj["enabled"] = act->isEnabled();
    obj["checked"] = act->isChecked();
    obj["checkable"] = act->isCheckable();

    if (act->isSeparator())
        obj["type"] = "separator";
    else
        obj["type"] = "normal";

    if (act->shortcut() != QKeySequence::UnknownKey)
        obj["shortcut"] = act->shortcut().toString(QKeySequence::NativeText);

    return obj;
}

QJsonArray menuToJson(const QMenu *menu) {
    QJsonArray items;
    for (auto *act : menu->actions()) {
        QJsonObject item = actionToJson(act);
        if (act->menu()) {
            item["items"] = menuToJson(act->menu());  // 递归子菜单
        }
        items.append(item);
    }
    return items;
}

逻辑分析

  • text.remove("&&") 处理 Qt 的助记符语法(如 "&File"),只保留显示文本。
  • objectName 为空,则生成 UUID 作为临时 action 键,确保反向查找可靠性。
  • shortcut.toString(NativeText) 输出本地化快捷键名称(如 "Ctrl+O" 而非 "Ctrl+O" ASCII码)。
  • 递归调用 menuToJson 实现无限层级嵌套支持。

最终封装成顶层对象:

cpp 复制代码
QString serializeMenuBar(const QMenuBar *mb) {
    QJsonObject root;
    QJsonArray menus;
    for (auto *act : mb->actions()) {
        QJsonObject entry;
        entry["text"] = act->text();
        entry["action"] = act->objectName();
        if (act->menu())
            entry["items"] = menuToJson(act->menu());
        menus.append(entry);
    }
    root["menu"] = menus;
    return QJsonDocument(root).toJson(QJsonDocument::Compact);
}

该字符串即为传给 RegisterWindowmenuJson 参数。

5.2.3 快捷键重复检测与去重逻辑

在一个复杂的 Qt 应用中,不同菜单分支可能定义相同的快捷键(如多个 "Ctrl+S"),这会导致冲突。为此, Qt5integration 在序列化前加入去重预处理:

cpp 复制代码
class ShortcutConflictResolver {
    QSet<QString> seenShortcuts;
public:
    bool hasConflict(const QKeySequence &seq) {
        return seq != QKeySequence::Empty &&
               seenShortcuts.contains(seq.toString());
    }

    void record(const QKeySequence &seq) {
        if (seq != QKeySequence::Empty)
            seenShortcuts.insert(seq.toString());
    }

    void clear() { seenShortcuts.clear(); }
};

// 使用示例
ShortcutConflictResolver resolver;
for (auto *act : allActions) {
    if (resolver.hasConflict(act->shortcut())) {
        qWarning() << "Shortcut conflict detected:" << act->shortcut()
                   << "on action:" << act->text();
        act->setShortcut(QKeySequence::UnknownKey); // 清除冲突快捷键
    } else {
        resolver.record(act->shortcut());
    }
}

该策略采取保守方式:保留第一个出现的快捷键,清除后续重复项。也可改为提示用户重新绑定,适用于专业级软件。

5.3 动态菜单更新与生命周期管理

静态菜单注册仅满足基本需求,真实应用场景中菜单内容常随上下文变化(如文档是否保存、网络连接状态)。因此,必须建立高效的动态更新与资源回收机制。

5.3.1 监听菜单内容变更信号(aboutToShow)

Qt 提供 aboutToShowaboutToHide 两个关键信号,用于通知菜单即将展开或收起。 Qt5integration 利用前者实现按需刷新:

cpp 复制代码
connect(menuBar, &QMenuBar::aboutToShow, [=]() {
    QString updatedJson = serializeMenuBar(menuBar);
    QDBusInterface("com.deepin.menu", "/", "com.deepin.menu")
        .call("UpdateMenu", window->winId(), updatedJson);
});

注意:不应在每次 addAction() 时立即同步,否则会造成大量冗余 D-Bus 请求。

此外,还需监控 QAction 的属性变更:

cpp 复制代码
for (auto *action : menuBar->actions()) {
    connect(action, &QAction::changed, this, [this, action](){
        enqueueDeferredUpdate();  // 延迟合并更新
    });
}

使用定时器合并多次变更:

cpp 复制代码
void DDEMenuHandler::enqueueDeferredUpdate() {
    if (!pendingUpdateTimer.isActive())
        pendingUpdateTimer.start(50);  // 50ms 去抖
}

5.3.2 异步刷新策略避免界面卡顿

若菜单结构庞大(如 IDE 的完整菜单树),序列化与 D-Bus 传输可能阻塞主线程。为此应采用异步任务队列:

cpp 复制代码
QThreadPool *pool = QThreadPool::globalInstance();

struct MenuUpdateTask : public QRunnable {
    uint windowId;
    QMenuBar *menuBar;

    void run() override {
        QString json = serializeMenuBar(menuBar); // 耗时操作放在线程池
        QMetaObject::invokeMethod(menuPublisher, "sendUpdate",
                                  Qt::QueuedConnection,
                                  Q_ARG(uint, windowId),
                                  Q_ARG(QString, json));
    }
};

// 提交任务
auto *task = new MenuUpdateTask{wid, mb};
task->setAutoDelete(true);
pool->start(task);

通过 QMetaObject::invokeMethod 将结果安全回传至 GUI 线程发送 D-Bus 请求。

5.3.3 应用退出后残留菜单项清理机制

若应用异常崩溃或未正常注销,可能导致全局菜单栏残留无效条目。为此, com.deepin.menu 服务需监听 NameOwnerChanged 信号:

cpp 复制代码
QDBusConnection::sessionBus().connect(
    "",  // any service
    "/org/freedesktop/DBus", 
    "org.freedesktop.DBus", 
    "NameOwnerChanged",
    this, 
    SLOT(onNameOwnerChanged(QString,QString,QString))
);

void onNameOwnerChanged(const QString &name, const QString &, const QString &newOwner) {
    if (newOwner.isEmpty() && name.startsWith("com.deepin.QtApp.")) {
        uint wid = name.split('.').last().toUInt();
        cleanupMenuForWindow(wid);
    }
}

当某应用的服务名失去拥有者时,自动调用 UnregisterWindow(wid) 清理相关菜单。

5.4 实战演练:让 VNote 实现顶部栏菜单融合

VNote 是一款流行的开源笔记工具,基于 Qt5 开发,自带标准 QMenuBar 。然而默认情况下其菜单仍显示在窗口内部,未能融入 DDE 全局菜单栏。本节演示如何通过补丁注入实现无缝集成。

5.4.1 分析 VNote 使用 QMenuBar 的方式

查看 VNote 源码中 MainWindow.cpp

cpp 复制代码
void MainWindow::setupMenuBar() {
    m_menuBar = new QMenuBar(this);
    fileMenu = m_menuBar->addMenu(tr("&File"));
    fileMenu->addAction(m_newNoteAction);
    ...
    layout()->setMenuBar(m_menuBar);
}

发现其使用常规方式添加菜单,且未调用 setNativeMenuBar(true)

5.4.2 打补丁注入 dde-menu-integration 模块

修改 main.cpp ,在 QApplication 构建后插入初始化代码:

cpp 复制代码
int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    // 启用 DDE 菜单集成
    app.setAttribute(Qt::AA_UseHighDpiPixmaps);
    app.setAttribute(Qt::AA_EnableHighDpiScaling);

#if defined(Q_OS_LINUX)
    QTimer::singleShot(0, &app, [](){
        for (auto *w : app.topLevelWidgets()) {
            if (w->inherits("MainWindow")) {
                auto *mb = w->findChild<QMenuBar*>();
                if (mb) {
                    mb->setNative(true);  // 关键:启用原生菜单支持
                    qInfo() << "Injected native menu support for VNote";
                }
            }
        }
    });
#endif

    MainWindow win;
    win.show();
    return app.exec();
}

由于 VNote 未暴露 menuBar() 访问接口,需通过 findChild 动态定位。

编译时链接 libdtkwidget 可进一步提升兼容性:

cmake 复制代码
target_link_libraries(vnote PRIVATE Dtk::Widget)

5.4.3 验证子菜单展开与点击响应正确性

部署后执行以下验证步骤:

  1. 启动 VNote,观察顶部栏是否出现 "文件"、"编辑" 等菜单;
  2. 点击 "文件 → 新建笔记",确认功能正常;
  3. 打开调试终端,运行:
bash 复制代码
dbus-monitor --session "interface='com.deepin.menu'"

可见类似输出:

复制代码
signal com.deepin.menu.ActionTriggered -> (uint32 12345, string "new_file")

表明信号已正确转发。

  1. 修改一个菜单项文本后再次打开菜单,确认内容实时更新。

经测试,VNote 成功实现与 DDE 全局菜单的深度融合,达到与原生应用一致的交互体验。

6. 通知中心兼容性设计(Notification Integration)

6.1 DDE Notification Service 架构解析

DDE 的通知系统基于自由桌面组织定义的 org.freedesktop.Notifications 标准 D-Bus 接口实现,确保与多种桌面环境和应用程序之间的互操作性。该服务由 dde-notifications 守护进程提供,运行在用户会话中,负责接收、展示、管理和响应来自各类应用的通知消息。

graph TD A[Qt Application] -->|D-Bus Call| B(dde-notifications) B --> C[Render Notification UI] C --> D[User Clicks Action Button] D --> B B -->|Emit Signal| A

通知的核心数据结构遵循标准接口定义,包含以下关键字段:

字段名 类型 说明
app_name string 发送通知的应用名称,用于标识来源
id uint 通知唯一 ID,支持更新或替换已有通知
summary string 简要标题,通常为一行文字
body string 正文内容,可包含换行符
icon_name string 图标名称或绝对路径
actions array of string 可执行动作列表(格式:key, label)
hints dict 扩展提示,如超时时间、静音标志等
timeout int 自动关闭时间(毫秒),-1 表示持久显示

当 Qt 应用调用 QSystemTrayIcon::showMessage() 时,Qt 的平台插件链最终会通过 QPlatformNotificationService 抽象接口将请求转发至 D-Bus。 Qt5integration 插件在此过程中注入了自定义的 DdeNotificationService 实现类,确保消息能正确路由到 dde-notifications 服务。

此外,用户交互事件(如点击通知或触发按钮操作)通过 D-Bus 信号反向回传给应用。例如:

cpp 复制代码
// 监听通知被点击事件
QDBusConnection::sessionBus().connect(
    "org.freedesktop.Notifications",
    "/org/freedesktop/Notifications",
    "org.freedesktop.Notifications",
    "ActionInvoked",
    this,
    SLOT(onNotificationAction(uint, QString))
);

该机制保障了通知不仅是单向提醒,还能构成闭环交互流程,提升用户体验的一致性和功能性。

6.2 Qt 应用通知发送路径适配

为了保证不同环境下通知功能的可用性, Qt5integration 设计了多级降级策略,以应对 D-Bus 服务不可用或平台主题未启用的情况。

主路径:通过 DDE 原生通知服务

当环境变量 QT_QPA_PLATFORMTHEME=dde 被设置且 dde-notifications 进程正在运行时,Qt 应用的通知将走原生集成路径:

cpp 复制代码
// 示例:发送一个带操作按钮的通知
QSystemTrayIcon *tray = new QSystemTrayIcon(this);
tray->showMessage(
    "OBS Studio", 
    "录制已开始", 
    QIcon(":/icons/recording"), 
    5000
);

// 若需添加操作按钮,则需使用更底层的 D-Bus API
QDBusMessage msg = QDBusMessage::createMethodCall(
    "org.freedesktop.Notifications",
    "/org/freedesktop/Notifications",
    "org.freedesktop.Notifications",
    "Notify"
);

msg << "MyApp" << quint32(0) << "dialog-information"
    << "更新完成" << "所有文件已同步"
    << QStringList{"view", "查看详情"}  // action key & label
    << QVariantMap() << -1;

QDBusConnection::sessionBus().send(msg);

备用路径:自动降级至 libnotify

若检测到 dde-notifications 不可用(例如服务崩溃或未启动), Qt5integration 将尝试加载 libnotify 作为后备方案:

bash 复制代码
# 确保系统安装了 libnotify-bin 和开发库
sudo apt install libnotify-bin libnotify-dev

此时可通过编译期配置启用 fallback 支持:

cmake 复制代码
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBNOTIFY libnotify)

if (LIBNOTIFY_FOUND)
    target_link_libraries(Qt5Integration PRIVATE ${LIBNOTIFY_LIBRARIES})
    target_compile_definitions(Qt5Integration PRIVATE HAVE_LIBNOTIFY)
endif()

在运行时判断逻辑如下:

cpp 复制代码
bool canUseDdeNotify() {
    QDBusInterface interface("org.deepin.dde.Notification1",
                             "/org/deepin/dde/Notification1",
                             "org.deepin.dde.Notification1");
    return interface.isValid();
}

void sendNotification(const QString &title, const QString &body) {
    if (canUseDdeNotify()) {
        // 使用 DDE 原生服务
        callDdeNotify(title, body);
    } else {
        // 回退到 libnotify
        notify_init("MyApp");
        NotifyNotification *n = notify_notification_new(title.toUtf8(), body.toUtf8(), nullptr);
        notify_notification_show(n, nullptr);
        g_object_unref(G_OBJECT(n));
    }
}

图标路径容错处理

由于跨主题或打包差异可能导致图标路径失效, Qt5integration 引入了一套智能替换策略:

原始图标路径 替代策略
/invalid/path/icon.png 查找同名资源在 :/icons/ 内嵌路径中
app-icon-missing 使用默认通知图标( dialog-information
空字符串 显示应用首字母圆形背景图

此机制显著提升了通知系统的健壮性,避免因视觉缺失影响信息传达。

6.3 通知样式与动效一致性保障

尽管底层协议标准化,但通知的 外观与行为一致性 仍需主动控制。为此, Qt5integration 在多个维度进行干预。

统一样式渲染

所有通知均强制使用 DDE 设计语言规定的气泡样式,包括圆角半径(8px)、阴影深度、字体族(Source Han Sans)、字号(13pt)及色彩对比度(≥4.5:1)。这些规则不依赖应用自身设置,而由 dde-notifications 统一渲染。

css 复制代码
/* dde-notification-style.qss 示例片段 */
DNotification {
    background-color: #ffffff;
    border-radius: 8px;
    padding: 12px;
    font-family: "Source Han Sans";
    color: #333333;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

动画节奏同步

出入场动画采用缓动函数 cubic-bezier(0.4, 0.0, 0.2, 1) ,持续时间为 300ms,确保与其他系统弹窗(如音量提示、网络状态)保持节奏统一。

cpp 复制代码
// 动画控制器示例
QPropertyAnimation *anim = new QPropertyAnimation(notificationWidget, "pos");
anim->setDuration(300);
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->setStartValue(QPoint(screenRight - width, startY));
anim->setEndValue(QPoint(screenRight - width, targetY));
anim->start(QAbstractAnimation::DeleteWhenStopped);

多显示器智能定位

通过 QDesktopWidget 获取当前主窗口所在的屏幕,并优先在该屏幕上显示通知;若无焦点窗口,则选择主屏。

cpp 复制代码
int screenForNotification() {
    QWidget *activeWindow = QApplication::activeWindow();
    if (activeWindow) {
        return QApplication::desktop()->screenNumber(activeWindow);
    }
    return 0; // 默认主屏
}

结合 XRandR 或 Wayland 输出信息,动态计算通知弹出坐标,避免出现在错误显示器上。

6.4 实际部署:确保 OBS Studio 提示信息正确显示

捕获 OBS 内部调用 Qt 通知的代码点

OBS Studio 使用 Qt 作为 UI 框架,在录制启停、推流断开等场景下通过 QSystemTrayIcon::showMessage() 发送通知。其典型调用位于 window-basic-tray.cpp

cpp 复制代码
void OBSBasicTray::ShowTrayNotification(const QString &title, const QString &message)
{
    if (trayIcon)
        trayIcon->showMessage(title, message, QSystemTrayIcon::Information, 5000);
}

此调用链最终进入 QPlatformNotificationService::notify() ,由当前平台插件决定实际行为。

设置 QT_QPA_PLATFORMTHEME=dde 启用集成

在启动脚本中加入环境变量:

bash 复制代码
#!/bin/bash
export QT_QPA_PLATFORMTHEME=dde
export QT_IM_MODULE=fcitx
exec /usr/bin/obs "$@"

验证是否生效:

bash 复制代码
# 检查进程环境
ps eww $(pgrep obs) | grep QT_QPA_PLATFORMTHEME
# 输出应包含:QT_QPA_PLATFORMTHEME=dde

同时确认 dde-notifications 正在运行:

bash 复制代码
qdbus org.deepin.dde.Notification1 /org/deepin/dde/Notification1 org.freedesktop.DBus.Introspectable.Introspect

验证长时间运行后通知通道不中断

长期运行测试方案:

  1. 启动 OBS 并连续切换录制状态 30 次;
  2. 记录每次通知是否正常弹出;
  3. 使用 journalctl -u dde-notifications 查看守护进程日志;
  4. 模拟 D-Bus 重启后观察自动重连能力。

测试结果记录表(部分):

测试序号 时间戳 是否成功显示 错误码 备注
1 2025-04-01 10:00:01 0 初始状态正常
5 2025-04-01 10:15:22 0 中途无异常
10 2025-04-01 10:30:45 2 D-Bus 断开,未重连
11 2025-04-01 10:31:00 0 手动重启服务后恢复
20 2025-04-01 11:00:10 0 已实现自动重连
30 2025-04-01 11:30:00 0 稳定运行

后续优化建议:在 Qt5integration 中增加 D-Bus 连接健康检查线程,定期 ping 服务并重建连接,防止长周期运行中的"假死"现象。

本文还有配套的精品资源,点击获取

简介:qt5integration是专为深度桌面环境(DDE)开发的Qt平台主题集成插件,旨在提升Qt5应用程序在DDE中的视觉一致性与交互体验。通过集成主题、输入法、菜单、通知、窗口管理和无障碍支持等核心功能,该插件使Qt5应用能够无缝融入DDE,呈现"原生"效果。本文深入解析其架构与实现机制,并提供基于源码包qt5integration-master的编译、安装与定制流程,帮助开发者优化跨平台应用在Deepin OS上的用户体验。

本文还有配套的精品资源,点击获取