Qt Quick基础用法
1. 简介
- Qt 是一个跨平台的, 基于C++ 和 QML进行GUI开发的工具。
1.1 Qt Widgets 与 QML/Qt Quick
- Qt 4.7 发布时,引入了 QML,用于移动开发,其全面支持触摸操作、流畅的动画效果等。但在 Qt 5 中,QML 已经不再局限于移动开发,也可用于开发传统的桌面程序。
- 相比之下,Qt Widgets 更“老”、更成熟,而 QML/Qt Quick 则更“新”、更“现代”。
- 无论如何,Qt Widgets 和 QML/Qt Quick 都可以在多个平台上使用(Windows、Linux、OS X等)
- 对于传统的桌面程序来说,优先考虑使用 Qt Widgets,若要开发更“现代”的 UI 与高级应用,建议使用 Qt5.x + QML 2.x + QtQuick 2.x。
- 对于移动端开发来说,建议使用 QML,协同 JavaScript,简单快捷、渲染效果更佳、界面更炫酷。不建议使用 Qt Widgets,其显示效果、适应性都不好。
2. QML与QtQuick
- QML 是一种用户界面规范和标记语言,允许开发人员和设计师创建高性能、流畅的动画和视觉吸引人的应用程序。
- 用户界面规范:QML 提供了一种高度可读、声明性、类似 JSON 的语法,支持与动态属性绑定相结合的命令式 JavaScript 表达式。
- 标记语言:像 C++ 一样,QML 是一种语言,文件格式以 .qml 结尾。
- Qt Quick 是 QML 类型和功能的标准库,包括视觉类型、交互式类型、动画、模型和视图、粒子效果和着色效果。
- Qt Quick 使用 QML 作为声明语言,来设计以用户界面为中心的应用程序。
- QML 包含大量使用手机移动设备的功能模块,比如基本部件(QtQuick 模块)、GPS 定位、渲染特效、蓝牙、NFC、WebkKit 等等。
2.1 QtQuick 1.x VS QtQuick 2.x
- 全新的 Qt 版本
- QtQuick 1.x 基于 Qt4.x。
- QtQuick 2.x 随 Qt5.0 一起引入。
- 全新的绘图系统
- QtQuick 1.x 使用 QGraphicsView/QPainter API 来绘制场景。
- QtQuick 2.x 基于 Scene Graph,一个 OpenGL(ES)2.0 抽象层,对绘图进行了高度优化,效率更高。
- 全新的 QML 引擎
- Qt 4.x 中,QML 引擎基于JSC(JavaScriptCore - Webkit 的 JS 引擎)。
- Qt 5.0 中引入 V8(Google 的开源高性能 JavaScript 引擎,用 C++ 编写,用于 Chromium、Node.js 和多个其他嵌入应用程序)。
- Qt 5.2 中引入了 V4 JS 引擎,针对 QML 用例进行了优化,并且可以选择关闭 JIT(Just-In-Time)编译,以符合 iOS 和 WinRT 平台的限制。个头更小、反应更快、扩展性也非常好。
- 从 Qt 5.5 开始,加入了一个新模块 QtQuick3D,它提供使用 QML 语言创建 3D 应用程序/游戏的能力,其使用的是一个被命名为 FrameGraph 的新引擎,而非 Scene Graph(因为太 2D/2.4D)。
- 模块、属性和方法、类型和 API、C++ 代码(QtDeclarative 被移除了,替代的它是Qt QML 和 Qt Quick 模块)、QML 插件的更改。
3. 信号(Signal )与槽(Slot)
-
信号与槽(Signal & Slot)是 Qt 编程的基础,也是 Qt 的一大创新。因为有了信号与槽的编程机制,在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。
-
槽(Slot):就是对信号响应的函数。
- 槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何部分(public、private 或 protected),可以具有任何参数,也可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。
-
信号(Signal):就是对控件的点击事件或定时事件
- 就是在特定情况下被发射的事件,例如PushButton 最常见的信号就是鼠标单击时发射的 clicked() 信号,一个 ComboBox 最常见的信号是选择的列表项变化时发射的 CurrentIndexChanged() 信号。
-
信号与槽关联是用 QObject::connect() 静态函数实现的,其基本格式是:
- QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
- connect(ui->rBtnBlue,SIGNAL(clicked()),this,SLOT(setTextFontColor()));
- connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(addFun(int));
- 一个信号可以连接多个槽
- 当信号和槽函数带有参数时,在 connect()函数里,要写明参数的类型,但可以不写参数名称。
- 多个信号可以连接同一个槽
- 一个信号可以连接另外一个信号,例如:connect(spinNum, SIGNAL(valueChanged(int)), this, SIGNAL (refreshInfo(int));
- 严格的情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,会出现编译错误或运行错误。
- 在使用信号与槽的类中,必须在类的定义中加入宏 Q_OBJECT。
- 当一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。
- 信号与槽机制是 Qt GUI 编程的基础,使用信号与槽机制可以比较容易地将信号与响应代码关联起来。
- QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
-
在Qt c++中通过emit来发射信号,而在QML中直接将声明的信号当做函数来调用就可以触发了。
-
事件与信号:只要记住事件比信号更底层就可以了。事件由操作系统发出,信号由Qt的对象发出
-
QML兼具了UI界面文件和QtScript的特点,非常方便的将界面设计和与C++交互融化在了一起,这是Qt Quick最重要的特性,可以让我们以最便捷的方式去开发应用。
-
在QtWidgets中,信号与槽的连接方式使用的是QObject::connect()。相应的,在QtQuick中,signal对象也有一个connect()方法,用于将信号连接到一个或多个方法/信号。当信号连接到方法时,无论信号何时发出,该方法都将被自动调用。
有了这种机制,可以通过方法来接收信号,而无需使用信号处理器。也可以通过disconnect()来取消连接。
4. QML与C++混合编程
- Qt Quick能够使我们的界面生成非常绚丽的效果,但是,它本身也是有局限性的,对于一些业务逻辑和复杂算法,比如低阶的网络编程如 QTCPSocket ,多线程,又如 XML 文档处理类库 QXmlStreamReader / QXmlStreamWriter 等等,在 QML 中要么不可用,要么用起来不方便。所以呢,我们基于这种原因来混合编程。
4.1 原理和方法
- 简单来说,混合编程就是通过QML高效便捷的构建UI界面,而使用C ++来实现业务逻辑和复杂算法。
- Qt集成了QML引擎和Qt元对象系统,使得QML很容易从C ++中得到扩展,在一定的条件下,QML就可以访问QObject派生类的成员,例如:
- 信号 (signals)
- 槽函数 (slots)
- 枚举类型 (Q_ENUMS)
- 属性 (Q_PROPERTY)
- 成员函数 (Q_INVOKABLE )
- 实例代码如下:
class Mixing : public QObject
{
Q_OBJECT
Q_ENUMS(BALL_COLOR) // 定义可被 QML访问的枚举类型
// 定义可被QML访问的属性number
Q_PROPERTY(unsigned int number READ getNumber WRITE setNumber NOTIFY Numberchanged)
public:
explicit Mixing(QObject *parent = nullptr);
enum BALL_COLOR{
BALL_COLOR_YELLOW,
BALL_COLOR_BLUE,
BALL_COLOR_GREEN,
};
unsigned int getNumber() const;
void setNumber(const unsigned int &Number);
Q_INVOKABLE void stop(); // 定义可被QML调用的成员函数
signals: // 定义可被QML调用的信号
void colorChanged(const QColor & color);
void Numberchanged();
public slots: // 定义可被QML调用的槽函数
void start();
private:
unsigned int m_Number;
}
- 要想在QML中访问C++对象,必然要找到一种方法在两者之间建立联系,而Qt中提供了两种在 QML 环境中使用C ++对象的方式:
- 在C ++中实现一个类,注册到QML环境中,QML环境中使用该类型创建对象
- 在C ++中构造一个对象,将这个对象设置为QML的上下文属性,在QML环境中直接使用该属性
- 两种方式之间的区别是第一种可以使C ++类在QML中作为一个数据类型,例如函数参数类型或属性类型,也可以使用其枚举类型、单例等,功能更强大。
4.2 QML访问C++ 类 (QML=>C++)
- C++类要想被QML访问,首先必须满足两个条件:一是派生自QObject类或QObject类的子类,二是使用Q_OBJECT宏。
- QObject类是所有Qt对象的基类,作为Qt对象模型的核心,提供了信号与槽机制等很多重要特性。
- Q_OBJECT宏必须在private区(C++默认为private)声明,用来声明信号与槽,使用Qt元对象系统提供的内容,位置一般在语句块首行。
4.2.1 信号和槽
- 可以把 C++ 对象的信号连接到 QML 中定义的方法上,也可以把 QML 对象的信号连接到 C++ 对象的槽上,还可以直接调用 C++ 对象的槽或信号
- 槽必须被声明为public或protected,而且信号在 C++ 中使用时要用到emit关键字,但是在Qml中就是个普通的函数。
void Mixing::start()
{
qDebug() << "start";
emit colorChanged(Qt::blue); // 传递了一个颜色到QML中
}
4.2.2 把类注册到QML中
- 注册函数原型
template<typename T>
int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)
- 参数说明:
- 模板参数typename ,就是要实现的 C++ 类的类名。
- uri:指定一个唯一的包名,类似Java 中的那种,一是用来避免名字冲突,二是可以把多个相关类聚合到一个包中方便引用。比如我们常写这个语句 “import QtQuick.Controls 2.3” ,其中的 “QtQuick.Controls” 就是包名 uri ,而2.3则是版本,是versionMajor和versionMinor的组合。
- qmlName:是在QML中可以使用的类名
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include "mixing.h"
int main(int argc, char *argv[]) {
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<Mixing>("test.Mixing", 1, 0, "Mixing");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
- 上述示例代码把C++ Mixing类注册成为QML类型Mixing,主版本是1,次版本是0,包名是test.Mixing。注意:注册动作一定要放在 QML 上下文创建之前,否则的话,注册是没有用的。
- 在QML中导入Mixing类并使用它
import QtQuick 2.9
import QtQuick.Window 2.2
import test.Mixing 1.0
Window {
id:root
visible: true
width: 640
height: 480
title: qsTr("mixing")
MouseArea{
anchors.fill: parent
onClicked: {
mixing.start()
}
}
Mixing{ // 创建Mixing对象,并实现信号colorChanged的槽函数
id: mixing
onColorChanged: {
root.color = color
}
}
}
4.2.3 C++ 类的属性和成员函数
- C++中定义可被QML调用的成员函数(Q_INVOKABLE):
- 在定义一个类的成员函数时使用Q_INVOKABLE宏来修饰,但是注意的是在QML中访问的前提是public或protected成员函数,而且这个宏必须放在返回函数前面。
- C++中定义 可被QML访问的属性(Q_PROPERTY ):
- 定义属性则需要使用Q_PROPERTY 宏,通过它定义的属性,可以在 QML 中访问、修改,也可以在属性变化时发射特定的信号。要想使用 Q_PROPERTY 宏,定义的类必须是QObject的后裔,必须在类首使用Q_OBJECT宏。
class Mixing : public QObject
{
Q_OBJECT
Q_ENUMS(BALL_COLOR)
Q_PROPERTY(unsigned int number READ getNumber WRITE setNumber NOTIFY Numberchanged)
public:
explicit Mixing(QObject *parent = nullptr);
enum BALL_COLOR{
BALL_COLOR_YELLOW,
BALL_COLOR_BLUE,
BALL_COLOR_GREEN,
};
unsigned int getNumber() const;
void setNumber(const unsigned int &Number);
Q_INVOKABLE void stop();
signals:
void colorChanged(const QColor & color);
void Numberchanged();
public slots:
void start(BALL_COLOR ballColor);
private:
unsigned int m_Number;
}
- 通过Q_INVOKABLE修饰了stop函数。通过Q_PROPERTY修饰了名为number的属性,number通过Number函数读得数据,通过setNumber函数写入数据,触发信号是Numberchanged函数。
- 成员函数和属性的实现
unsigned int Mixing::getNumber() const{
return m_Number;
}
void Mixing::setNumber(const unsigned int &number){
if(number != m_Number) {
m_Number = number;
emit Numberchanged();
}
}
void Mixing::stop(){
qDebug() << "颜色改变啦!!!";
}
4.2.4 QML调用C++类的成员函数和属性
MouseArea{
anchors.fill: parent
acceptedButtons:Qt.LeftButton | Qt.RightButton;
onClicked: {
if(mouse.button === Qt.LeftButton)
{
mixing.start(Mixing.BALL_COLOR_BLUE)
}else if(mouse.button === Qt.RightButton){
mixing.start(Mixing.BALL_COLOR_GREEN)
}
}
onDoubleClicked: {
mixing.start(Mixing.BALL_COLOR_YELLOW)
mixing.number = 10;
}
}
Mixing{
id: mixing
onColorChanged: {
root.color = color
mixing.stop(color)
}
Component.onCompleted:
{
console.log("default ball number is", number)
}
onNumberChanged:
{
console.log("new ball number is", number)
}
}
4.3 QML上下文属性设置
- 目标:直接嵌入一些 C++ 数据给QML使用,需要使用QQmlContext类
- QQmlContext:
- 定义了QML引擎内的上下文,上下文允许将数据暴露给由QML引擎实例化的QML组件
- 每个QQmlContext包含一组属性,允许以名称将数据显式地绑定到上下文
- 通过调用QQmlContext::setContextProperty()来定义和更新上下文属性
// 简单的上下文属性,对应的值为QVariant类型
void QQmlContext::setContextProperty(const QString &name, const QVariant &value)
// 相对来说稍微复杂一些,QObject*对象类型。
void QQmlContext::setContextProperty(const QString &name, QObject *value)
4.3.1 设置简单的 上下文属性
- 实例
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickView>
#include <QQmlContext>
int main(int argc, char *argv[]){
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQuickView view;
view.rootContext()->setContextProperty("Data", QString("设置Qml上下文属性"));
view.setSource(QUrl(QStringLiteral("qrc:///main.qml")));
view.show();
return app.exec();
}
- 导入相关的类,设置了一个QString类型的属性,这个Data的值可以由加载QML组件的 C++ 程序直接设置,使用的就是setContextProperty()函数。然后,我们就可以直接在qml文件中直接使用Data了,不需要导入任何模块。
import QtQuick 2.9
import QtQuick.Window 2.2
Rectangle {
width: 640
height: 480
color: "lightgray"
Text {
id: text
anchors.centerIn: parent
text: Data
}
}
4.3.2 设置对象为上下文属性
- 设置对象为上下文属性
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickView>
#include <QQmlContext>
#include "mixing.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
Mixing mixing; // 从堆上分配了一个Mixing对象实例
QQuickView view;
view.rootContext()->setContextProperty("mixing", &mixing);
view.setSource(QUrl(QStringLiteral("qrc:///main.qml")));
view.show();
return app.exec();
}
- 在.qml文件中使用上下文属性中的对象
import QtQuick 2.9
import QtQuick.Window 2.2
//import test.Mixing 1.0
Rectangle {
id:root
visible: true
width: 640
height: 480
MouseArea{
anchors.fill: parent
acceptedButtons:Qt.LeftButton | Qt.RightButton;
onClicked: {
mixing.start()
mixing.number = 10
}
}
Connections{
target: mixing
onColorChanged: {
root.color = color
mixing.stop(color)
}
onNumberChanged:{
console.log("new ball number is", mixing.number) // 10
}
}
}
- 由于去掉了qmlRegisterType() 调用,所以在 main.qml中不能再访问Mixing类了,比如说不能通过类名来引用它定义的BALL_COLOR枚举类型了,否则会出现报错:“ReferenceError: Mixing is not defined”
- 除了枚举类型的值不可以调用之外,其他的属性,以及关联的信号和槽函数,以及用Q_INVOKABLE 宏修饰的方法都是可以继续使用的。
4.4 C++类访问QML(C++=>QML)
- C++ 访问QML的属性、函数和信号
- 在 C++ 中加载QML文件可以用QQmlComponent或QQuickView,然后就可以在 C++ 中访问QML对象了。QQuickView提供了一个显示用户界面的窗口,而QQmlComponent没有。
4.4.1 在C++中访问QML中的属性
- 在qml文件中对Window对象添加一个Rectangle,设置objectName为“rect”,这个值是为了在C++中能够找到这个Rectangle。
import QtQuick 2.9
import QtQuick.Window 2.2
import an.qt.Mixing 1.0
Window {
id:root
visible: true
width: 640
height: 480
Rectangle {
objectName: "rect"
anchors.fill: parent
}
MouseArea{
anchors.fill: parent
acceptedButtons:Qt.LeftButton | Qt.RightButton;
onClicked: {
mixing.start()
mixing.number = 10
qmlSignal("这是qml文件中的qml信号")
}
}
Mixing{
id:mixing
}
}
- 在main.cpp文件中加载QML文件并进行组件实例化后,就可以在 C++ 中访问、修改这个实例的属性值了。
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include "mixing.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<Mixing>("an.qt.Mixing", 1, 0, "Mixing");
//QQmlApplicationEngine engine;
//engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
//if (engine.rootObjects().isEmpty())
// return -1;
QQmlEngine engine;
QQmlComponent compontext(&engine, QUrl(QStringLiteral("qrc:///main.qml")));
QObject *object = compontext.create();
qDebug() << "width value is" << object->property("width").toInt();
object->setProperty("width", 320);
qDebug() << "height value is" << QQmlProperty::read(object, "height").toInt();
QQmlProperty::write(object, "height", 240);
QObject *rect = object->findChild<QObject*>("rect");
if(rect) {
rect->setProperty("color", "orange");
}
return app.exec();
}
- 使用QObject::property()/setProperty()来读取、修改width属性值。
- 使用QQmlProperty::read()/write()来读取、修改height属性值。
- 有时候,QML组件是一个复杂的树型结构,包含兄弟组件和孩子组件,我们可以使用QObject::findchild()/findchildren()来查找。如上面的代码中,我们查找名字为rect的孩子组件,如果找到的话就将其颜色设置为橘黄色。
4.4.2 在C++中访问QML中的函数与信号
- 在 C++ 中,使用 QMetaObject::invokeMethod() 可以调用QML中的函数,它 是个静态方法,其函数原型如下:
bool QMetaObject::invokeMethod(
QObject * obj, // 被调用对象的指针
const char * member, // 方法名字
Qt::ConnectionType type, // 连接类型
QGenericReturnArgument ret, // 用来接收返回值
QGenericArgument val0 = QGenericArgument( 0 ), // 传递给被调用方法的参数
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument())
- 它的返回值如果为true则表明调用成功,返回false,则说明参数不正确或被调用函数名错误。
- 对于要传递给被调用方法的参数,使用QGenericArgument来表示,原型如下:
QGenericArgument Q_ARG(Type, const Type & value)
- 对于返回类型来说,使用QGenericReturnArgument表示,你可以使用 Q_RETURN_ARG 宏来构造一个接收返回指的参数,原型如下:
QGenericReturnArgument Q_RETURN_ARG(Type, Type & value)
- 上述是方法的调用,信号又是如何传递到 C++ 中的呢?更简单了,使用使用 QObject::connect() 可以连接QML中的信号,connect()共有四个重载函数,它们都是静态函数。必须使用SIGNAL()宏来声明信号,SLOT()宏声明槽函数。
- 使用 QObject::disconnect() 可以解除信号与槽函数的连接。
- 在qml文件中添加一个信号和方法:
import QtQuick 2.9
import QtQuick.Window 2.2
import an.qt.Mixing 1.0
Window {
id:root
visible: true
width: 640
height: 480
signal qmlSignal(string message)
onQmlSignal: console.log("这是一个信号:",message)
function qmlFunction(parameter) {
console.log("这是一个方法:",parameter)
return "function from qml"
}
Rectangle {
objectName: "rect"
anchors.fill: parent
}
MouseArea{
anchors.fill: parent
acceptedButtons:Qt.LeftButton | Qt.RightButton;
onClicked: {
mixing.start()
mixing.number = 10
qmlSignal("这是qml文件中的qml信号")
}
}
Mixing{
id:mixing
}
}
- 信号:定义了qmlSignal信号,参数为message,当单击时,会输出message的值。用法很简单。
- 方法:首先方法的名字是qmlFunction,它的参数为parameter,当调用此方法时,会输出parameter的值,同时返回"function from qml"
- 修改main.cpp文件,调用信号和方法:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include "mixing.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<Mixing>("test.Mixing", 1, 0, "Mixing");
//QQmlApplicationEngine engine;
//engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
//if (engine.rootObjects().isEmpty())
// return -1;
QQmlEngine engine;
QQmlComponent compontext(&engine, QUrl(QStringLiteral("qrc:///main.qml")));
QObject *object = compontext.create();
qDebug() << "width value is" << object->property("width").toInt();
object->setProperty("width", 320);
qDebug() << "height value is" << QQmlProperty::read(object, "height").toInt();
QQmlProperty::write(object, "height", 240);
QObject *rect = object->findChild<QObject*>("rect");
if(rect) {
rect->setProperty("color", "orange");
}
QVariant returnedValue;
QVariant message = "Hello from c++";
QMetaObject::invokeMethod(object,"qmlFunction",
Q_RETURN_ARG(QVariant,returnedValue),
Q_ARG(QVariant,message));
qDebug() << "returnedValue is " << returnedValue.toString();
Mixing mixing;
QObject::connect(object, SIGNAL(qmlSignal(QString)),
&mixing, SLOT(cppSlot(QString)));
return app.exec();
}
5. 常用知识
5.1 引用外部动态库
- 在主程序的pro文件中,增加如下两行,把头文件和库文件引用进来。
INCLUDEPATH += /root/Downloads/testlib
LIBS += -L/root/Downloads/testlib -ltestlib
- INCLUDEPATH += 后面写so的头文件的路径
- LIBS += -L后面写lib文件的路径,-l后面是库的项目名称
- 注意:
- -l不要带上so,或者其他so.1
- -L后面直接写路径,不要有空格
- 建议头文件从原项目拷贝出来,放到主项目目录下,然后写相对连接来引用。
参考: