# 从 Qt 迁移

对于习惯 Qt 开发范式的程序员而言,LarkSDK 的开发范式会有一些区别。下面列出我们收集到的来自 Qt 程序员的一些常见问题,并相应给出 LarkSDK 中对应的写法或解决方案。

诚然,LarkSDK 当前提供的功能总体上无法与成熟的 Qt 相比,可能会在一定程度上缺失 Qt 具备的功能,对此主要有以下原因:

  • 在合迅智灵的研发规划之中,我们以完成框架设计、实现基本功能为优先,致力于优先实现涵盖大部分需求的通用功能,对于小部分扩展性质的、总体需求量不大(例如在所有来自用户的业务需求中,对于其中 90% 的业务需求,都不是必须用到的功能)的、不影响框架完备性的功能,我们会在后续规划中,动态根据用户的需求进行陆续实现;
  • LarkSDK 的架构设计与技术路线和 Qt 已存在较大差异,使得从底层架构的角度讲,LarkSDK 就不一定可以覆盖 Qt 所提供的部分功能特性。但考虑到用户习惯,我们会尽可能针对 Qt 的大部分常见功能进行覆盖,即使底层设计可能有所不同。若用户对某些功能需求较为强烈,合迅智灵将考虑优先实现覆盖。

# 一般问题

# 信号槽用法

Q: LarkSDK 中信号的声明、信号与槽的绑定、信号的发送等写法与 Qt 有何异同?槽函数是否需要特殊生命?是否支持类似 Qt 的 sender() 接口判定信号发送者对象?

LarkSDK 的信号槽应用方式类似 Boost 风格 (opens new window),不依赖代码预编译,通过信号对象进行槽连接、发送等操作;Qt 的信号槽依赖其元对象编译机制,使用时需借助其元对象编译工具对代码进行一定程度的预处理。

Qt 需要在代码中嵌入宏,配合 moc (opens new window) 工具生成预处理代码,将预处理代码嵌入用户代码之中,方可使用信号槽机制,其中用 signals 宏标定一个成员函数为信号,函数参数列表即作为信号所携带的参数列表,用 slot 宏标定一个成员函数为槽函数。

/** Qt Code */

#include <QObject>


class Counter : public QObject
{
    Q_OBJECT  // 宏,触发预编译

public:
    Counter() { m_value = 0; }
    int value() const { return m_value; }

public slots:  // 宏,声明成员函数为槽
    void setValue(int value);

signals:  // 宏,声明为信号
    void valueChanged(int newValue);

private:
    int m_value;

};

需要发送信号时,使用 emit 宏。

/** Qt Code */

void Counter::setValue(int value)
{
    m_value = value;
    emit valueChanged(value);  // 宏,发送信号
}

LarkSDK 直接在类中定义 LSignal 对象,表示该类将要发送的信号,其携带的参数清单通过模板参数定义;此外,槽函数无需进行特殊声明,技术上,LarkSDK 并无实际的的“槽函数”概念,一切函数,包括普通函数、成员方法和 Lambda 表达式均可接受信号连接。

/** LarkSDK Code */

#include <lobject.h>
#include <lsignal.h>


class Counter : public LObject
{

public:
    Counter() { m_value = 0; }
    int value() const { return m_value; }
    void setValue(int value);  // 普通成员函数,无需特殊的“槽”声明
    LSignal<int> valueChangedSignal;  // 信号对象

private:
    int m_value;

}

直接调用 LSignal 对象的 emit() 方法来发送信号。

/** LarkSDK Code */

void Counter::setValue(int value)
{
    m_value = value;
    valueChangedSignal.emit(value);  // 发送信号
}

Qt 通过静态接口 QObject::connect() 接口绑定信号与槽,调用时需同时提供发送者、接收者、信号与槽的信息。连接目标除了接收者的成员槽之外,还允许连接到任意函数,包括 Lambda 表达式。LarkSDK 则通过调用 LSignal 对象的 connect() 方法来绑定槽。同样允许连接到成员函数、任意函数和 Lambda 表达式。

/** Qt Code */

// 连接到成员函数槽
Counter a, b;
QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);

// 连接到普通函数
void someFunction(int value) { /* ... */ }
QObject::connect(&a, &Counter::valueChanged, someFunction);

// 连接到 Lambda 表达式
QObject::connect(&a, &Counter::valueChanged, [](int value) { /* ... */ });


/** LarkSDK Code */

// 连接到成员函数
Counter a, b;
a.valueChangedSignal.connect(&b, &Counter::setValue);

// 连接到普通函数
void someFunction(int value) { /* ... */ }
a.valueChangedSignal.connect(&someFunction);

// 连接到 Lambda 表达式
a.valueChangedSignal.connect([](int value) { /* ... */ });

与 Qt 信号必须是成员函数形式不同的是,LarkSDK 信号作为对象,可以在任何地方单独定义:

/** LarkSDK Code */

LSignal<value> signal;
signal.connect([](int value) { /* ... */ });

因研发规划优先级原因,目前 LarkSDK 信号尚不具备如下功能,功能将在后期将逐步实现:

  • 允许从槽函数中获取信号发送者对象,类似 Qt 的 QObject::sender() 接口;
  • 允许信号连接的 Lambda 表达式使用变量捕获。

关于 LarkSDK 信号槽的更多介绍请参见信号槽机制说明一文。

# 定时器用法

Q: LarkSDK 中使用定时器的方式与 Qt 有何异同?是否支持单次触发定时器?是否支持信号槽方式使用?

LarkSDK 中使用定时器的方式和 Qt 基本类似。均支持单次触发定时器、信号槽方式使用等功能。

Qt 中,QTimer 通过信号槽的方式将定时器的超时信号绑定到槽函数以实现定时触发:

/** Qt Code */

// 创建定时器,连接超时信号到槽
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, []() { /* ... */ });

// 启动定时器
timer.start(1000);

// 设置定时器为单次触发并启动
timer.setSingleShot(true);
timer.start(1000);

// 也可以通过静态接口,直接启动一个单次触发定时器
QTimer::singleShot(1000, someSlot);

LarkSDK 中,既可以通过信号槽的方式使用,也可以给定时器指定一个 LObject 目标对象,超时发生时将触发目标对象的定时器事件处理函数,即 handleTimerEvent()

/** LarkSDK Code */

// 定时器的目标对象类型,重写其定时器事件处理函数
class TestTimerEventReceiver : public LObject
{
protected:
    void handleTimerEvent(LTimerEvent* e) override { /* ... */ }
};

// 创建定时器,指定一个目标对象
LTimer timer(&receiver);

// 也可以通过信号槽的方式使用
timer.timeoutSignal.connect([]() { /* ... */ });

// 启动定时器
timer.start(1000);

// 以单次触发的方式启动定时器
timer.startOnce(1000);

因研发规划优先级原因,目前 LarkSDK 尚不具备如下功能,功能将在后期将逐步实现:

  • 通过静态接口直接使用单次触发定时器。

# 组件与窗体

Q: Qt 的 QWidget 和 LarkSDK 的 LComponent 是对应的吗?

否。在组件与窗体的描述方法上,LarkSDK 和 Qt(准确说,Qt Widgets 模块)采用的是不同的语义。在 Qt 中,QWidget 在没有父对象的情况下表示窗体,如父对象为另一个 QWidget 则表示组件。

在 LarkSDK 中,LComponent 只表示组件,没有父对象的 LComponent 表示“孤立组件”,在一些特殊的情况下应用;同时用 LWindow 表示窗体。窗体有一个“根组件”的概念用于表示窗体之中处于最顶层的组件,默认该组件和窗体用户区保持同样大小。具体可参见跨平台窗体与组件系统架构一文。

# 界面设计方法

Q: 合迅智灵是否提供类似 Qt Designer 的,支持组件拖拽的低代码界面构建方案?

因研发规划优先级原因,目前合迅智灵尚未提供基于图形化拖拽与代码生成的界面构建方案。但已基本完成功能设计和。后续计划基于 VSCode 插件实现界面拖拽构建,集成到 LarkStudio 开发环境中。

# 组件布局方式

Q: LarkSDK 支持何种组件布局方式?是否支持类似 Qt QGridLayout 的网格式布局?在代码写法上和 Qt 有何异同?

不考虑无布局绝对定位,目前 LarkSDK 只支持弹性布局方式。理论上弹性布局可以完全覆盖 Qt 的盒子布局的所有功能。例如盒子布局中的弹簧对象可以用设置了增长因子的空白组件代替:

/** Qt Code */

QWidget container;
QHBoxLayout layout;
QLabel labelA("A");
QLabel labelB("B");
layout.addWidget(&labelA);
layout.addSpacing(20);  // 添加一个弹簧
layout.addWidget(&labelB);
container.setLayout(&layout);


/** LarkSDK Code */

LComponent container;
container.setLayout(LFlexLayout());
LLabel labelA("A", &container);
LComponent spacer(&container);
spacer.setFlexGrowth(1);  // 使用空白组件代替弹簧
LLabel labelB("B", &container);

代码效果大致如下:

Spacer

弹性布局思路取自 Web 前端开发中 CSS Flexbox (opens new window) 布局方式。因研发规划原因,团队优先实现弹性盒子布局中最重要和基础的部分功能,更多细节功能将在后续陆续实现。

LarkSDK 目前尚不支持 Qt 的网格布局和表单布局等功能,但将根据实际市场需求,规划在后续版本中实现。

# 组件属性动画

Q: LarkSDK 是否支持类似 Qt QPropertyAnimation 的组件属性动画效果?

因规划优先级原因,目前不支持,在后续版本中根据实际市场需求实现。

# 组件重绘机制

Q: LarkSDK 是否支持单个组件重绘?重绘方式与 Qt 有何区别?是否支持组件指定区域重绘?

调用 LComponent::repaint() 接口可以令单个组件重绘。重绘同时还将处理与自身相关的组件布局定位等问题,重绘操作本身和 Qt 区别不大。

目前暂不直接支持指定区域重绘,但该项功能已基本完成功能开发,正在内部测试中,将在后续版本中发布。

# 组件字体支持

Q: LarkSDK 对字体支持如何?组件是否支持设置不同字体?

LarkSDK 支持从系统读取字体,同时还随开发包提供一个默认字体以供使用。LarkSDK 也支持为组件设置不同字体。LarkSDK 通过 LFontDatabase 类来管理字体的读取与加载,通过 LFont 类完成字体检索匹配。

# 集成开发环境

Q: 合迅智灵是否提供集成开发环境(IDE)?开发流程是否依赖集成开发环境的使用?

合迅智灵目前基于 VSCode 插件提供集成开发环境,具备工程管理、项目创建、Demo、文档浏览等功能。但开发流程并不依赖开发环境的使用。LarkSDK 是一个单纯的 C++ 开发库,理论上可以使用任意开发环境进行配置后完成开发流程。

# 杂项问题

# 组件图标自适应

Q: 按钮等组件的图标不能按照组件大小自适应调整?

目前 LarkSDK 的组件图标不支持自适应,图标也暂不支持缩放。但该功能已纳入规划,将在后续版本中实现发布。

作为一个全新的 C++ 开发框架 LarkSDK 在规划功能路线的时候优先实现框架性质的功能,再逐步实现细节功能。

# 组件边框特效

Q: 按钮等组件的边框不支持设置圆角效果和颜色?

同上,该功能将在后续规划中逐步实现。