Qt TaskTree C++ Classes
包含一个通用用途的 TaskTree 库。更多内容...
该模块处于技术预览阶段,内容可能会有变动。
该模块于 Qt 6.11 中首次引入。
命名空间
包含 TaskTree 模块中的所有类和全局函数 |
类
用于 For 和 When 结构的主体元素 | |
用于条件表达式的“else”元素 | |
用于条件表达式的“else if”元素 | |
可执行任务项的基类 | |
描述执行模式的组元素 | |
for循环元素 | |
子任务的无限循环 | |
用于“for”元素内部的无限迭代器 | |
表示用于构建声明式配方以描述如何执行和处理嵌套异步任务树的基本元素 | |
表示可作为任何“组”组成部分的基本元素 | |
用于条件表达式的“if”元素 | |
用作 For 元素内部迭代器的基类 | |
用于 For 元素内部的列表迭代器 | |
描述 QObject 子类及其信号的结构 | |
具有自定义限制的并行执行模式 | |
按需完成的异步任务 | |
用于声明自定义任务项并定义其初始化和完成处理程序的类模板 | |
一个提供 QCustomTask 中使用的默认任务适配器的类模板 | |
一个具有给定 Key 类型的映射任务树执行控制器 | |
QNetworkReply 和 QNetworkAccessManager 的封装类 | |
一个并行任务树执行控制器 | |
QProcess 的自定义删除器,由 QProcessTask 使用 | |
一个顺序任务树执行控制器 | |
单任务树执行控制器 | |
一个已启动且具有指定限值的 QBarrier | |
在其他任务之间同步执行自定义处理程序 | |
在适配自定义任务接口时使用的辅助类 | |
以声明式方式定义并运行异步任务树 | |
QTcpSocket 的封装类 | |
一个通过 QtConcurrent::run() 在单独线程中控制函数执行的类模板 | |
QThreadFunction 类模板的基类 | |
用于 For 元素内部的重复迭代器 | |
用于在运行中的任务树中进行自定义数据交换的类模板 | |
用于条件表达式的“then”元素 | |
用于 For 元素内部的条件迭代器 | |
一个将主体的执行延迟到屏障推进后的元素 |
详细说明
使用 TaskTree 库构建“配方”,描述需要执行的异步任务,并在 QTaskTree 中使用这些“配方”来执行它们。
这些“配方”是对将要创建和执行的任务类型的声明式描述,例如:QProcess 、QNetworkReplyWrapper 或QThreadFunction<ReturnType>,以及这些任务应按顺序还是并行运行。 在配方内部,您可以根据前一个任务是成功完成还是出现错误,定义不同的后续处理路径。此外,还可以在Group 元素中嵌套任务,每个Group 都可以根据其自身的执行模式或工作流策略来运行其任务。这些配方共同构成了任务树结构。
异步任务
异步任务是指任何可以启动,并在稍后以成功或错误状态完成的任务。“稍后”意味着在启动任务后,控制权将返回正在运行的事件循环。在任务完成之前,我们不会阻塞调用线程。要使用任务树,我们需要一个正在运行的事件循环。
异步任务的示例:
- QTimer::singleShot()
- QProcess
- QNetworkAccessManager +QNetworkReply =QNetworkReplyWrapper
- QtConcurrent::run()+QFutureWatcher<Result> =QThreadFunction<Result>
配方与任务树
为了更好地理解配方和任务树的概念,我们可以将其类比为游戏卡带和游戏机。当我们编写配方时,就像是在制作一张游戏卡带,因此我们只需详细描述当卡带被放入游戏机(任务树)并启动后,游戏机应该执行哪些操作。 配方本身只是对任务树的一种声明性描述,说明当配方被传递给任务树并启动任务树时,任务树应该做什么。如果没有任务树,配方本身不会自动执行任何操作——这就像没有播放器的游戏卡带一样。
以下是对配方和任务树职责的简要总结。
配方(卡带)描述:
- 运行中的任务树应通过 QCustomTask 动态创建哪些任务
- 按什么顺序
- 运行中的任务树应通过Storage 动态创建哪些数据结构
- 如何在任务开始前配置每个任务
- 任务完成时如何收集数据
- 执行模式(任务应顺序执行还是并行执行)
- 工作流策略
任务树(播放器):
- 读取配方并自动创建任务和数据结构
- 管理已创建任务和数据结构的生命周期
- 执行延续
- 根据已完成任务的结果和工作流策略选择不同的路径
- 提供基本的进度信息
自定义任务
由于配方是对任务树的描述,规定了在启动任务树时应创建哪些任务,因此我们无法直接在配方内部创建这些任务。相反,我们需要一种声明式的方法,指示任务树在稍后时间为我们创建并启动这些任务。 例如,如果希望任务树创建并启动QProcess ,我们可以通过在配方中放置QProcessTask 元素来描述该任务。QProcessTask 是QCustomTask<QProcess>的别名。每个任务类型都应提供其对应的QCustomTask<Type>,以便在配方中使用。
下表列出了若干可直接放入配方中的内置自定义任务:
| 自定义任务(用于配方) | 任务类(由运行中的任务树创建) | 简要说明 |
|---|---|---|
| QProcessTask | QProcess | 启动进程。 |
QThreadFunctionTask<ReturnType> | QThreadFunction<ReturnType> | 启动异步任务,在单独的线程中运行。 |
| QTaskTreeTask | QTaskTree | 启动嵌套任务树。 |
| QNetworkReplyWrapperTask | QNetworkReplyWrapper | 启动网络下载。 |
| QTcpSocketWrapperTask | QTcpSocketWrapper | 建立 TCP 连接。 |
有关如何调整特定任务以便在配方中使用的更多信息,请参阅QTaskInterface 和Task Adapters 。
示例配方
QTaskTree 包含一个顶级Group 元素(也称为“配方”),该元素可包含任意数量的各类任务,例如QProcessTask 、QNetworkReplyWrapperTask 或QThreadFunctionTask<ReturnType>:
const Group recipe {
QProcessTask(...),
QNetworkReplyWrapperTask(...),
QThreadFunctionTask<int>(...)
};
QTaskTree *taskTree = new QTaskTree(recipe);
connect(taskTree, &QTaskTree::done, ...); // finish handler
taskTree->start();上述配方由一个Group 类型的顶级元素组成,其中包含QProcessTask 、QNetworkReplyWrapperTask 和QThreadFunctionTask<int>类型的任务。调用taskTree->start() 后,任务将按链式顺序依次创建并运行,首先执行QProcess 。当QProcess 成功完成后,将启动QNetworkReplyWrapper 任务。最后,当网络任务成功完成后,将启动QThreadFunction<int>任务。
当最后一个正在运行的任务成功完成时,该任务树被视为已成功运行,并会发出QTaskTree::done()信号,同时发送DoneWith::Success 。当某个任务因错误而终止时,任务树的执行将停止,剩余任务将被跳过。任务树将以错误状态结束,并发出QTaskTree::done()信号,同时发送DoneWith::Error 。
组
Group 的父任务将其视为单个任务。与其他任务一样,该组可以被启动,并可能成功或出错地结束。Group 元素可以嵌套以创建树结构:
const Group recipe {
Group {
parallel,
QProcessTask(...),
QThreadFunctionTask<int>(...)
},
QNetworkReplyWrapperTask(...)
};上例与第一个示例的不同之处在于,顶级元素有一个子组,其中包含QProcessTask 和QThreadFunctionTask<int>。该子组是根节点中QNetworkReplyWrapperTask 的同级元素。子组中还包含一个额外的parallel 元素,该元素指示其Group 并行执行任务。
因此,当QTaskTree 启动上述配方时,QProcess 和QThreadFunction<int>会立即启动并并行运行。由于根组不包含parallel 元素,其直接子任务将按顺序运行。因此,QNetworkReplyWrapper 将在整个子组完成后启动。当该组的所有任务均已完成时,该组即被视为完成。任务完成的顺序并不重要。
因此,根据哪个任务耗时更长(QProcess 或QThreadFunction<int>),可能会出现以下情况:
| 情况 1 | 情况 2 |
|---|---|
| 根组启动 | 根组启动 |
| 子组开始 | 子组开始 |
| QProcess 开始 | QProcess 开始 |
| QThreadFunction<int> 开始 | QThreadFunction<int> 开始 |
| ... | ... |
| QProcess 结束 | QThreadFunction<int> 结束 |
| ... | ... |
| QThreadFunction<int> 结束 | QProcess 结束 |
| 子组结束 | 子组完成 |
| QNetworkReplyWrapper 开始 | QNetworkReplyWrapper 开始 |
| ... | ... |
| QNetworkReplyWrapper 完成 | QNetworkReplyWrapper 完成 |
| 根组完成 | 根组完成 |
不同场景之间的差异以粗体标出。三个点表示前一个事件与下一个事件之间经过了未指定时间(一个或多个任务仍在运行)。事件之间没有点表示它们是同步发生的。
所呈现的场景假设所有任务均运行成功。如果某项任务在执行过程中失败,则任务树将以错误结束。特别地,当QProcess 以错误结束,而QThreadFunction<int> 仍在执行时,QThreadFunction<int> 将被自动取消,子组将以错误结束,QNetworkReplyWrapper 将被跳过,且任务树将以错误结束。
任务处理程序
使用Task 处理程序来设置待执行的任务,并在任务成功或出错结束时读取其输出数据。
任务的开始处理程序
当任务对象创建后、启动之前,任务树会调用一个可选的用户自定义初始化处理程序。该初始化处理程序应始终获取关联任务类对象的引用:
const auto onSetup = [](QProcess &process) {
process.setProgram("sleep");
process.setArguments({"3"});
};
const Group root {
QProcessTask(onSetup)
};您可以在初始化处理程序中修改传入的 `QProcess `,以便任务树能根据您的配置启动进程。请勿在初始化处理程序中调用 `process.start(); `,因为任务树会在需要时自动调用该方法。初始化处理程序是可选的。若要使用,它必须作为任务构造函数的第一个参数。
可选地,设置处理程序可以返回一个 `SetupResult`。返回的 `SetupResult ` 将影响给定任务后续的启动行为。可能的取值包括:
| SetupResult 值 | 简要说明 |
|---|---|
| Continue | 任务将正常启动。这是当设置处理程序未返回SetupResult (即其返回类型为void )时的默认行为。 |
| StopWithSuccess | 任务不会被启动,并将向其父任务报告成功。 |
| StopWithError | 任务不会启动,并将向其父任务报告错误。 |
这在仅当满足某个条件时才需运行任务,且评估该条件所需的数据必须等到先前启动的任务完成后才能得知的情况下非常有用。通过这种方式,设置处理程序可以动态决定是正常启动相应的任务,还是跳过该任务并向其父任务报告成功或错误。有关任务间数据交换的更多信息,请参阅Storage 。
任务的完成处理程序
当正在运行的任务完成时,任务树会调用一个可选提供的完成处理程序。该处理程序应接收指向相关任务类对象的const 引用:
const autoonSetup= [](QProcess&process) {
process.setProgram("sleep");
process.setArguments({"3"});
};
const autoonDone= [](constQProcess&process,DoneWith result) {
if(result==DoneWith::Success)
qDebug() << "Success" << process.cleanedStdOut();
else
qDebug() << "Failure" << process.cleanedStdErr();
};
constGroup root {
QProcessTask(onSetup,onDone)
};“完成”处理程序可以从QProcess 收集输出数据,并将其存储以供后续处理,或执行其他操作。
注意:如果 任务初始化处理程序返回StopWithSuccess 或StopWithError ,则不会调用完成处理程序。
组处理程序
与任务处理程序类似,组处理程序允许您设置一个组,以便在整个组成功完成或出现错误时执行并应用更多操作。
组的开始处理程序
任务树在启动子任务之前会调用组开始处理程序。该组处理程序不接受任何参数:
const autoonSetup= []{
qDebug() << "Entering the group";
};
constGroup root {
onGroupSetup(onSetup),
QProcessTask(...)
};组初始化处理程序是可选的。要定义组初始化处理程序,请在组中添加一个 `onGroupSetup()` 元素。`onGroupSetup()` 的参数是一个用户处理程序。如果在一个组中添加了多个 `onGroupSetup()` 元素,运行时将触发包含错误消息的断言。
与任务的启动处理程序类似,组启动处理程序可返回SetupResult 。返回的SetupResult 值会影响整个组的启动行为。若未指定组启动处理程序,或其返回类型为void,则默认组操作为Continue ,此时所有任务均会正常启动。 否则,当启动处理程序返回StopWithSuccess 或StopWithError 时,任务将不会被启动(会被跳过),而组本身将根据返回值分别报告成功或错误。
const Group root {
onGroupSetup([] { qDebug() << "Root setup"; }),
Group {
onGroupSetup([] { qDebug() << "Group 1 setup"; return SetupResult::Continue; }),
QProcessTask(...) // Process 1
},
Group {
onGroupSetup([] { qDebug() << "Group 2 setup"; return SetupResult::StopWithSuccess; }),
QProcessTask(...) // Process 2
},
Group {
onGroupSetup([] { qDebug() << "Group 3 setup"; return SetupResult::StopWithError; }),
QProcessTask(...) // Process 3
},
QProcessTask(...) // Process 4
};在上例中,根组的所有子组都定义了各自的初始化处理程序。以下场景假设所有已启动的进程均成功结束:
| 场景 | 注释 |
|---|---|
| 根组启动 | 未返回 SetupResult,因此其任务被执行。 |
| 组 1 开始 | 返回 Continue,因此其任务被执行。 |
| 进程 1 启动 | |
| ... | ... |
| 进程 1 结束(成功) | |
| 组 1 结束(成功) | |
| 组 2 开始 | 返回 StopWithSuccess,因此跳过进程 2,组 2 报告成功。 |
| 组 2 完成(成功) | |
| 组 3 开始 | 返回 StopWithError,因此跳过流程 3,且第 3 组报告错误。 |
| 第 3 组完成(错误) | |
| 根组完成(错误) | 第 3 组作为根组的直接子组,因错误而结束,因此根组停止执行,跳过尚未启动的进程 4,并报告错误。 |
组的完成处理程序
Group 的完成处理程序会在其任务成功或失败执行后被调用。该组报告的最终值取决于其Workflow Policy 。处理程序可执行其他必要的操作。完成处理程序定义在组的onGroupDone()元素内。它可接受可选参数DoneWith ,用于指示执行结果是成功还是失败:
constGroup root {
onGroupSetup([]{qDebug()<< "Root setup"; }),
QProcessTask(...),
onGroupDone([](DoneWith result) {
if(result== DoneWith::Success)
qDebug() << "Root finished with success";
else
qDebug() << "Root finished with an error";
})
};组完成处理程序是可选的。如果在一个组中添加了多个onGroupDone(),运行时会触发一个断言,并显示一条错误消息。
注意:即使 组初始化处理程序返回StopWithSuccess 或StopWithError ,组的完成处理程序仍会被调用。此行为与任务完成处理程序的行为不同,未来可能会发生变化。
其他组元素
一个组可以包含其他描述处理流程的元素,例如execution mode 或workflow policy 。它还可以包含存储元素,这些元素负责收集和共享在组执行过程中收集的自定义公共数据。
执行模式
组中的执行模式元素指定了该组的直接子任务的启动方式。最常见的执行模式是sequential 和parallel 。此外,还可以通过ParallelLimit 元素指定并行运行的任务数量上限。
在所有执行模式下,组都会按照任务出现的顺序启动任务。
如果一个组的子任务本身也是一个组,则该子组将根据其自身的执行模式来运行其任务。
工作流策略
Group 中的工作流策略元素指定了当其任何直接子节点的任务完成时,该组应如何行为。有关可能策略的详细说明,请参阅WorkflowPolicy 。
如果一个组的子节点也是一个组,则该子组将根据其自身的流程策略来运行其任务。
存储
使用Storage 元素在任务之间交换信息。特别是在顺序执行模式下,当某个任务需要从另一个已经完成的任务获取数据后才能开始执行时。例如,一个通过从源读取数据并写入目标来复制数据的任务树可能如下所示:
staticQByteArray load(constQString&fileName) {...}
static voidsave(constQString&fileName, constQByteArray&array) {...}
staticGroup copyRecipe(constQString&source, constQString&destination)
{
structCopyStorage {// [1] 自定义任务间结构体
QByteArray content;// [2] 自定义任务间数据
};
// [3] 可由任务树管理的自定义任务间结构体实例
constStorage<CopyStorage>storage;
const autoonLoaderSetup= [source](QThreadFunction<QByteArray> &async) {
async.setThreadFunctionData(&load,source);
};
// [4] 运行时:任务树在调用处理程序之前,会从 [7] 激活该实例
const autoonLoaderDone= [storage](const QThreadFunction<QByteArray> &async) {
storage->content=async.result();// [5] 加载器将结果存储在存储器中
};
// [4] 运行时:任务树在调用处理程序之前激活来自 [7] 的实例
const autoonSaverSetup= [storage,destination](QThreadFunction<void> &async) {
constQByteArray content= storage->content;// [6] 保存器从存储中获取数据
async.setThreadFunctionData(&save,destination,content);
};
const autoonSaverDone= [](const QThreadFunction<void> &async) {
qDebug() << "Save done successfully";
};
constGroup root {
// [7] 运行时:当进入根节点时,任务树会创建一个 CopyStorage 实例
storage,
QThreadFunctionTask<QByteArray>(onLoaderSetup,onLoaderDone,CallDoneFlag::OnSuccess),
QThreadFunctionTask<void>(onSaverSetup,onSaverDone,CallDoneFlag::OnSuccess)
};
returnroot;
}
...
constQString source=...;
constQString destination=...;
QTaskTreetaskTree(copyRecipe(source,destination));
connect(&taskTree,&QTaskTree::done, &taskTree, [](DoneWith result) {
if(result==DoneWith::Success)
qDebug() << "The copying finished successfully.";
});
tasktree.start();在上例中,任务间数据由一个封装在CopyStorage 自定义结构体[1]中的QByteArray 内容变量[2]组成。如果加载器成功完成,它会将数据存储在CopyStorage::content 变量[5]中。随后,保存器使用该变量来配置保存任务[6]。
为了使任务树能够管理CopyStorage 结构体,会创建一个Storage<CopyStorage> 的实例 [3]。 如果将该对象的副本插入为该组的子项 [7],则当任务树进入该组时,会动态创建一个CopyStorage 结构体的实例。当任务树离开该组时,由于不再需要,现有的CopyStorage 结构体实例将被销毁。
如果多个持有共同的Storage<CopyStorage>实例副本的任务树同时运行(包括任务树在不同线程中运行的情况),则每个任务树都包含其自身的CopyStorage 结构体副本。
您可以通过存储对象从组内任何处理程序访问CopyStorage 。这包括该组中所有带有存储对象的子任务的所有处理程序。要在处理程序中访问自定义结构体,请将Storage<CopyStorage> 对象的副本传递给处理程序(例如,在lambda捕获中)[4]。
当任务树调用包含存储的子树中的处理程序时 [7],任务树会在Storage<CopyStorage> 对象内部激活其自身的CopyStorage 实例。因此,CopyStorage 结构体只能在处理程序主体内部访问。若要在Storage<CopyStorage> 内部访问当前活动的CopyStorage ,请使用Storage::operator->()、Storage::operator*() 或Storage::activeStorage() 方法。
以下列表总结了如何将 Storage 对象引入任务树:
- 定义包含自定义数据的自定义结构
MyStorage[1], [2] - 创建Storage<
MyStorage> 存储的实例 [3] - 将Storage<
MyStorage> 实例传递给处理程序 [4] - 在处理程序中访问
MyStorage实例 [5], [6] - 将Storage<
MyStorage>实例插入到一个组中 [7]
QTaskTree 类
QTaskTree 根据Group 根元素所描述的流程,执行异步任务的树形结构。
由于QTaskTree 本身也是一个异步任务,因此它可以作为另一个QTaskTree 的一部分。若要将嵌套的QTaskTree 放置在另一个QTaskTree 内部,请将QTaskTreeTask 元素插入到另一个Group 元素中。
QTaskTree 在运行时报告已完成任务的进度。当任务完成、被跳过或取消时,进度值会增加。当QTaskTree 完成并发出QTaskTree::done()信号时,进度的当前值等于最大进度值。最大进度等于树中异步任务的总数。 嵌套的QTaskTree 被计为单个任务,其子任务不计入顶级树中。组本身不被计为任务,但其中的任务会被计入。QSyncTask 任务不是异步的,因此不被计为任务。
若要为运行中的树设置额外的初始数据,请在树创建存储实例时,通过安装存储设置处理程序来修改这些实例:
Storage<CopyStorage> storage;
const Group root = ...; // storage placed inside root's group and inside handlers
QTaskTree taskTree(root);
auto initStorage = [](CopyStorage &storage) {
storage.content = "initial content";
};
taskTree.onStorageSetup(storage, initStorage);
taskTree.start();当运行中的任务树创建CopyStorage 实例时,在树内的任何处理程序被调用之前,任务树会调用initStorage 处理程序,以便为该特定taskTree 运行设置存储的初始数据。
同样地,若要从正在运行的任务树中收集一些额外的结果数据,可在树中的存储实例即将被销毁时从其读取数据。为此,请安装一个存储完成处理程序:
Storage<CopyStorage>storage;
constGroup root=...;// 存储位于 root 的组内,且位于处理程序中
QTaskTreetaskTree(root);
autocollectStorage= [](constCopyStorage&storage) {
qDebug() << "final content" << storage.content;
};
taskTree.onStorageDone(storage,collectStorage);
taskTree.start();当正在运行的任务树即将销毁一个CopyStorage 实例时,任务树会调用 collectStorage 处理程序,以便从存储中读取最终数据,这些数据专属于此次taskTree 的运行。
任务适配器
将新的 Task 类型纳入配方中非常简单。只需为QCustomTask 模板定义一个新的任务别名,并将您的Task 类型作为第一个模板参数传递即可,例如:
class Worker : public QObject
{
public:
void start() { ... }
signals:
void done(bool result);
};
using WorkerTask = QCustomTask<Worker>;只要满足以下条件,此方法即可生效:
- 您的任务继承自QObject 。
- 您的任务具有用于启动任务的公共 start() 方法。
- 任务完成时会发出 done(bool) 或 done(DoneResult) 信号。
如果您的任务不符合这些条件,您仍可通过提供包含自定义适配器的第二个模板参数,使任务适配 TaskTree 框架。假设我们希望将QTimer 适配到 TaskTree 中。Adapter 的代码可能如下所示:
class TimerAdapter
{
public:
void operator()(QTimer *task, QTaskInterface *iface) {
task->setSingleShot(true);
QObject::connect(task, &QTimer::timeout, iface, [iface] {
iface->reportDone(DoneResult::Success);
});
task->start();
}
};
using TimerTask = QCustomTask<QTimer, TimerAdapter>;现在,您可以在配方中开始使用TimerTask ,例如:
const autoonSetup= [](QTimer&task) {
task.setInterval(2000);
};
const autoonDone= [](constQTimer&task) {
qDebug() << "Timer triggered after" << task.interval() << "ms.";
};
constGroup recipe {
TimerTask(onSetup,onDone)
};注意: 实现运行中任务的类 应具有默认构造函数,且该类的对象应可自由销毁。应允许销毁运行中的任务,最好无需等待该任务完成(即运行中任务应具有安全的非阻塞析构函数)。 若要实现对具有阻塞式析构函数的任务进行非阻塞式销毁,可考虑使用 `QCustomTask ` 的可选模板参数 `Deleter `(即第三个模板参数)。
任务树运行器
任务树运行器负责管理用于执行给定配方(recipe)的底层QTaskTree 的生命周期。
下表总结了各种任务树运行器的区别:
| 类名 | 描述 |
|---|---|
| QSingleTaskTreeRunner | 管理单个任务树的执行。QSingleTaskTreeRunner::start() 方法会无条件地启动传入的配方,并重置任何可能正在运行的任务树。每次只能执行一个任务树。 |
| QSequentialTaskTreeRunner | 管理顺序任务树的执行。如果任务树运行器处于空闲状态,QSequentialTaskTreeRunner::enqueue() 方法会启动传入的配方;否则,该配方将被加入队列。当当前任务完成后,运行器会按顺序执行从队列中取出的配方。任何时候只能有一个任务树在执行。 |
| QParallelTaskTreeRunner | 管理并行任务树的执行。QParallelTaskTreeRunner::start() 方法会无条件地启动传入的配方,并保持任何可能正在运行的任务树并行执行。 |
| QMappedTaskTreeRunner | 管理映射的任务树执行。QMappedTaskTreeRunner::start() 方法会无条件地启动指定键对应的配方。如果已有键值相同的其他任务树正在运行,该任务树将被重置。键值不同的任务树不受影响,并将继续执行。 |
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.