IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> QML与C++混合编程[官方文档汇编] -> 正文阅读

[C++知识库]QML与C++混合编程[官方文档汇编]

https://blog.csdn.net/weixin_40583088/article/details/104526578?spm=1001.2101.3001.6650.11&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-11.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-11.pc_relevant_default&utm_relevant_index=18

概述

QML架构实现的目的就是把UI设计与后台程序逻辑分离,实现快速的前端UI设计,提供简洁而功能丰富的动画效果。QML表象上更像Web开发中的前端脚手架,通过集成JavaScript的开发优势实现前端可视元素的编程控制。

QT整个架构的灵魂就是强大的元对象系统( meta object system),通过元对象系统提供的信号和槽机制实现QT开发的解决方案。QML作为QT解决方案的重要组成模块,必然继承强大的元对象系统( meta object system)。我们通过元对象系统( meta object system)可以通过C++代码轻松集成、扩展QML。这样我们就能轻松的把前端UI(QML)与后端C++(后端程序逻辑)分离。

QML模块中的类允许我们从C++加载和处理QML对象,并且QML引擎与QT的元对象系统完美的集成。使得C++代码可以直接从QML调用,也就是我们在QML里可以轻易的调用C++代码功能。QML的这种能力使得我们能够开发混合应用程序,该混合应用程序将QML、JavaScript和C++代码混合在一起实现。我们甚至可以使用Web的开发思维,使用JSON数据格式链接前端与后端进行开发。更确切的称之为MVC开发思维。

QML和C++的混合开发

QT给我们提供了以下几种方式实现QML和C++的混合开发:

通过QML和JavaScript实现前端UI,使用C++实现后台逻辑,将用户界面代码与应用程序逻辑代码分开。
通过QML引擎给出的接口环境,访问C++中的功能。例如:调用应用程序逻辑,使用从C++实现的数据模型或调用的第三方C++库中的某些方法。这种方式,可以让我们访问在C++中暴露的各种属性与方法。
访问Qt QML 或Qt Quick C++ API中的功能。这中方式只是调用引擎自带的C++功能。
从C++实现QML对象类型,在QML中像使用其他QML组件类型一样使用。以完整对象组件的方式。
要向QML提供一些C++数据或功能,必须从QObject派生类中获得。由于QML引擎与元对象系统的集成,任何QObject派生类的属性、方法和信号都可以从QML访问,如将C++类型的属性暴露为QML所描述的那样。一旦此类提供了所需的功能,就可以通过多种方式将其公开给QML:

类可以注册为可实例化的QML类型,这样它就可以像QML代码中的任何普通QML对象类型一样被实例化和使用。[C++类注册为组件服务于QML:这将完整的暴露给QML,以组件的形式。]

类可以注册为单例类型,这样类的单个实例可以从QML代码导入,从而允许从QML访问实例的属性、方法和信号。[C++类注册为组件服务于QML:这将完整的暴露给QML,以组件的形式。单例模式注册,C++类也必定是单例模式类,否则这种注册将无效。]

类的实例可以作为上下文属性或上下文对象嵌入到QML代码中,从而允许从QML访问实例的属性、方法和信号。[C++类里的方法和属性嵌入到QML上下文中,C++类的实例化将在QML引擎初始化之前初始化。C++类不会完全暴露给QML。只有特定声明的方法和属性。]

这些是从QML代码访问C++功能的最常用方法;对于更多的选项和细节,请参见下面的文档翻译。此外,除了从QML访问C++功能之外,QT QML模块还提供了反向操作的方法,并从C++代码中操纵QML对象。

C++代码可以集成到C++应用程序或C++插件中,这取决于它是否被作为独立的应用程序或库来分发。插件可以与QML模块集成,然后可以在其他应用程序中导入和使用QML代码;

C++与QML之间正确的集成方法选择

要快速确定哪种集成方法适合您的情况,可以使用以下流程图:
在这里插入图片描述


将C ++类的属性公开给QML


由于QML引擎与QT元对象系统的集成,QML可以很容易地从C++扩展。此集成允许从QML访问任何QObject派生类的属性、方法和信号:可以读取和修改属性,可以从JavaScript表达式调用方法,并根据需要自动为信号创建信号处理程序。另外,QObject派生类的枚举值可以从QML访问。

QML可以用C++代码中定义的功能轻松扩展。由于QML引擎与Qt元对象系统紧密集成,QObject派生类适当公开的任何功能都可以从QML代码访问。这使得C++数据和函数可以直接从QML访问,通常很少或没有修改。

QML引擎可以通过元对象系统对QObject实例进行自省。 这意味着任何QML代码都可以访问QObject派生类的实例的以下成员:

属性
方法(假设它们是公共插槽或用Q_INVOKABLE标记的方法)
信号
此外,如果枚举已经用Q_ENUMS声明,则可以使用。有关更多详细信息,请参见QML和C ++之间的数据类型转换。

通常,无论QObject派生类是否已注册到QML类型系统,都可以从QML访问这些类。但是,如果要以:需要引擎访问其他类型信息的方式使用类(例如,如果要将类本身用作方法参数或属性,或者要以这种方式使用其枚举类型之一),则可能需要注册该类。(暂时没有全部领会其意思,反正注册就好了。可能表达的意思就是,如果想深层访问类的内部数据、或方法,或是要把类实例当作一般组件使用,必须注册。)

还要注意,本文档介绍的许多重要概念在“用C ++编写QML扩展”教程中得到了演示与证明。

数据类型处理和所有权:
任何从C++传递到QML的数据,无论是作为属性值、方法参数还是返回值,还是信号参数值,都必须是QML引擎支持的类型。

默认情况下,引擎支持许多QT C++类型,并且可以从QML中自动转换它们。另外,用QML类型系统注册的C++类可以用作数据类型,如果它们的枚举可以适当地注册,它们也可以。请参阅QML和C++之间的数据类型转换,以获取更多信息。

此外,数据所有权规则考虑到当数据从C++传递到QML时。当数据从C ++传输到QML时,数据所有权始终由C ++保留。 该规则的例外情况是从显式C ++方法调用返回QObject时:在这种情况下,除非通过调用QQmlEngine将对象的所有权显式设置为与C ++一起使用,否则QML引擎假定该对象的所有权: 使用QQmlEngine :: CppOwnership指定的setObjectOwnership()。(暂时没能全部理解。)

此外,QML引擎尊重Qt C ++对象的常规QObject父所有权语义,并且永远不会删除具有父对象的QObject实例。

公开属性:
可以使用Q_property()宏为任何QObject派生类指定属性。属性是具有关联的读取函数和可选的写入函数的类数据成员。

例如,下面是带有author属性的Message类。 如Q_PROPERTY宏调用所指定,此属性可通过author()方法读取,并且可通过setAuthor()方法写入:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
public:
    void setAuthor(const QString &a) {
        if (a != m_author) {
            m_author = a;
            emit authorChanged();
        }
    }
    QString author() const {
        return m_author;
    }
signals:
    void authorChanged();
private:
    QString m_author;
};

如果从C ++加载名为MyItem.qml的文件时将此类的实例设置为上下文属性,则:

int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);
 
    QQuickView view;
    Message msg;
    view.engine()->rootContext()->setContextProperty("msg", &msg);
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();
 
    return app.exec();
}

然后,可以从MyItem.qml读取author属性:

// MyItem.qml
import QtQuick 2.0
 
Text {
    width: 100; height: 100
    text: msg.author    // invokes Message::author() to get this value
 
    Component.onCompleted: {
        msg.author = "Jonah"  // invokes Message::setAuthor()
    }
}

为了最大程度地实现与QML的互操作性,任何可写属性都应具有一个关联的NOTIFY信号,只要该属性值发生更改,该信号便会发出。 这允许将属性与属性绑定一起使用,这是QML的基本功能,它通过在属性的任何依赖关系值发生变化时自动更新属性来强制属性之间的关系。

在上面的示例中,如Q_PROPERTY()宏调用中所指定,author属性的关联NOTIFY信号为authorChanged。 这意味着每当发出信号时(如作者在Message :: setAuthor()中进行更改时一样),这会通知QML引擎必须更新所有涉及author属性的绑定,并且引擎将更新文本。 通过再次调用Message :: author()设置属性。

如果author属性是可写的,但没有关联的NOTIFY信号,则将使用Message :: author()返回的初始值来初始化文本值,但以后对该属性进行的任何更改均不会更新该文本值。 另外,从QML绑定到该属性的任何尝试都将产生来自引擎的运行时警告。

注意:建议将NOTIFY信号命名为 Changed,其中是属性的名称。 由QML引擎生成的关联的属性更改信号处理程序将始终采用on Changed的形式,而不管相关C ++信号的名称如何,因此建议信号名称遵循此约定以避免任何混淆。

使用通知信号(NOTIFY)的注意事项:

为了防止循环或过度计算,开发人员应该确保只有在属性值实际更改时才会发出属性更改信号。此外,如果一个属性或一组属性很少使用,则允许对多个属性使用相同的通知信号。这样做时应小心,以确保性能不会受到影响。

通知信号(NOTIFY)的存在确实会产生少量开销。在某些情况下,属性的值在对象构造时设置,并且随后不会更改。最常见的情况是,当类型使用分组属性时,分组属性对象只分配一次,并且只在删除对象时释放。在这些情况下,常量属性(CONSTANT)可以添加到属性声明,而不是通知信号(NOTIFY)。

CONSTANT属性只能用于其值在类构造函数中已设置并最终确定的属性。 想要在绑定中使用的所有其他属性应改为具有NOTIFY信号。

对象的类型属性:其实就是C++类中包含的其他类实例。
如果对象类型已在QML类型系统中正确注册,则可以从QML访问对象的类型属性。

例如,消息类型可能具有MessageBody*类型的body属性:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MessageBody* body READ body WRITE setBody NOTIFY bodyChanged)
public:
    MessageBody* body() const;
    void setBody(MessageBody* body);
};
 
class MessageBody : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE text NOTIFY textChanged)
// ...
}

假设Message类型已在QML类型系统中注册,从而可以将其用作QML代码中的对象类型:

Message {
    // ...
}

如果MessageBody类型也已在类型系统中注册,则可以从QML代码中将MessageBody分配给Message的body属性:

Message {
    body: MessageBody {
        text: "Hello, world!"
    }
}

对象列表类型的属性:
包含QObject派生类型列表的属性也可以公开给QML。但是,出于这个目的,应该使用qqmlistproperty而不是QList作为属性类型。这是因为QList不是QObject派生的类型,因此无法通过Qt元对象系统提供必要的QML属性特性,例如修改列表时的信号通知。

例如,下面的MessageBoard类具有一个QQmlListProperty类型的messages属性,该属性存储Message实例的列表:

class MessageBoard : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<Message> messages READ messages)
public:
    QQmlListProperty<Message> messages();
 
private:
    static void append_message(QQmlListProperty<Message> *list, Message *msg);
 
    QList<Message *> m_messages;
};

MessageBoard :: messages()函数仅从其QList m_messages成员创建并返回QQmlListProperty,并根据QQmlListProperty构造函数的要求传递适当的列表修改函数:

QQmlListProperty<Message> MessageBoard::messages()
{
    return QQmlListProperty<Message>(this, 0, &MessageBoard::append_message);
}
 
void MessageBoard::append_message(QQmlListProperty<Message> *list, Message *msg)
{
    MessageBoard *msgBoard = qobject_cast<MessageBoard *>(list->object);
    if (msg)
        msgBoard->m_messages.append(msg);
}

注意,QQmlListProperty的模板类类型(在本例中是消息)必须注册到QML类型系统。

分组属性:
任何只读对象类型属性都可以作为分组属性从QML代码中访问。这可用于公开一组相关属性,这些属性描述类型的一组属性。

例如,假设Message :: author属性的类型为MessageAuthor,而不是简单的字符串,其子属性为name和email:

class MessageAuthor : public QObject
{
    Q_PROPERTY(QString name READ name WRITE setName)
    Q_PROPERTY(QString email READ email WRITE setEmail)
public:
    ...
};
 
class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MessageAuthor* author READ author)
public:
    Message(QObject *parent)
        : QObject(parent), m_author(new MessageAuthor(this))
    {
    }
    MessageAuthor *author() const {
        return m_author;
    }
private:
    MessageAuthor *m_author;
};

可以使用QML中的分组属性语法来编写author属性,如下所示:

Message {
    author.name: "Alexandra"
    author.email: "alexandra@mail.com"
}

作为分组属性公开的类型不同于对象类型属性,因为分组属性是只读的,并且在构造时由父对象初始化为有效值。可以从QML修改分组属性的子属性,但分组属性对象本身不会更改,而对象类型属性可以随时从QML分配新的对象值。因此,分组属性对象的生命周期受到C++父实现的严格控制,而对象类型属性可以通过QML代码自由创建和销毁。

公开方法(包括Qt插槽):
QObject派生类型的任何方法都可以从QML代码访问,如果它是:

用Q_INVOKABLE()宏标记的公共方法

作为公共Qt插槽的方法

例如,下面的MessageBoard类具有已用Q_INVOKABLE宏标记的postMessage()方法以及作为公共插槽的refresh()方法:

class MessageBoard : public QObject
{
    Q_OBJECT
public:
    Q_INVOKABLE bool postMessage(const QString &msg) {
        qDebug() << "Called the C++ method with" << msg;
        return true;
    }
 
public slots:
    void refresh() {
        qDebug() << "Called the C++ slot";
    }
};

如果将MessageBoard的实例设置为文件MyItem.qml的上下文数据,则MyItem.qml可以调用以下方法中所示的两个方法:

//C++代码
int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);
 
    MessageBoard msgBoard;
    QQuickView view;
    view.engine()->rootContext()->setContextProperty("msgBoard", &msgBoard);
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();
 
    return app.exec();
}
 
//QML代码
// MyItem.qml
import QtQuick 2.0
 
Item {
    width: 100; height: 100
 
    MouseArea {
        anchors.fill: parent
        onClicked: {
            var result = msgBoard.postMessage("Hello from QML")
            console.log("Result of postMessage():", result)
            msgBoard.refresh();
        }
    }
}

如果C ++方法具有QObject *类型的参数,则可以使用对象ID或引用该对象的JavaScript var值从QML传递参数值。

QML支持重载的C ++函数的调用。 如果有多个具有相同名称但参数不同的C ++函数,则将根据提供的参数的数量和类型来调用正确的函数。

当从QML中的JavaScript表达式访问时,从C++方法返回的值将转换为JavaScript值。

公开信号(暴露信号:Signals):
可以从QML代码访问任何QObject派生类型的公共信号。

QML引擎会自动为QML使用的任何QObject派生类型的信号创建信号处理程序。 信号处理程序始终在上命名,其中是信号的名称,首字母大写。 信号传递的所有参数均可通过参数名称在信号处理程序中使用。

例如,假设MessageBoard类具有带有单个参数subject的newMessagePosted()信号:

class MessageBoard : public QObject
{
    Q_OBJECT
public:
   // ...
signals:
   void newMessagePosted(const QString &subject);
};

如果MessageBoard类型已在QML类型系统中注册,则在QML中声明的MessageBoard对象可以使用名为onNewMessagePosted的信号处理程序接收newMessagePosted()信号,并检查主题参数值:

MessageBoard {
    onNewMessagePosted: console.log("New message received:", subject)
}

与属性值和方法参数一样,信号参数必须具有QML引擎支持的类型。 请参见QML和C ++之间的数据类型转换。 (使用未注册的类型不会产生错误,但是无法从处理程序中访问参数值。)

类可能包含多个具有相同名称的信号,但是只有最终信号才能作为QML信号进行访问。 请注意,名称相同但参数不同的信号无法相互区分。


从C++中定义QML类型


可以在C ++中定义QML类型,然后在QML类型系统中注册。 这允许将C ++类实例化为QML对象类型,从而使自定义对象类型可以用C ++实现并集成到现有QML代码中。 C ++类也可能出于其他目的而注册:例如,可以将其注册为Singleton Type,以使单个类实例可以通过QML代码导入,或者可以注册,以启用非实例化的枚举值 类可从QML访问。

此外,Qt QML模块提供了定义与诸如附加属性和默认属性之类的QML概念集成的QML类型的机制。

使用C ++代码扩展QML时,可以在QML类型系统中注册C ++类,以使该类可用作QML代码中的数据类型。 尽管可以从QML访问任何QObject派生类的属性,方法和信号,如将C ++类型的属性暴露给QML中所讨论的那样,但直到将其注册到类型系统后,此类才能用作QML的数据类型。 另外,注册还可以提供其他功能,例如允许从QML将类用作可实例化的QML对象类型,或者允许从QML导入和使用该类的单例实例。

此外,Qt QML模块提供了用于实现QML特定功能的机制,例如C ++中的附加属性和默认属性。

(请注意,使用C ++编写QML扩展教程演示了本文档中涵盖的许多重要概念。)

向QML类型系统注册C ++类型:
可以将QObject派生的类注册到QML类型系统中,以使该类型可用作QML代码中的数据类型。

该引擎允许实例化和非实例化类型的注册。 注册可实例化的类型可使C ++类用作QML对象类型的定义,从而允许将其用于QML代码的对象声明中以创建此类型的对象。 注册还为引擎提供了其他类型的元数据,使该类型(以及该类声明的任何枚举)可用作属性值,方法参数和返回值以及在QML和C ++之间交换的信号参数的数据类型。

注册非实例化类型也以这种方式将类注册为数据类型,但是该类型不能用作从QML实例化为QML对象类型。 例如,如果某个类型具有应暴露给QML的枚举但该类型本身不应实例化,则此功能很有用。

有关选择正确方法将C ++类型公开给QML的快速指南,请参阅在C ++和QML之间选择正确的集成方法。参考上面的导图。

注册实例化对象类型:
任何QObject派生的C ++类都可以注册为QML对象类型的定义。 一旦在QML类型系统中注册了一个类,就可以像声明QML代码中的任何其他对象类型一样声明和实例化该类。 一旦创建,就可以从QML中操纵类实例。 正如将C ++类型的属性暴露给QML所解释的那样,可以从QML代码访问任何QObject派生类的属性,方法和信号。

要将QObject派生的类注册为可实例化的QML对象类型,请调用qmlRegisterType()将该类作为QML类型注册到特定的类型名称空间中。 然后,客户端可以导入该名称空间以使用该类型。

例如,假设有一个具有author和creationDate属性的Message类:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
    Q_PROPERTY(QDateTime creationDate READ creationDate WRITE setCreationDate NOTIFY creationDateChanged)
public:
    // ...
};

可以通过使用适当的类型名称空间和版本号调用qmlRegisterType()来注册此类型。 例如,要使该类型在版本1.0的com.mycompany.messaging命名空间中可用:

qmlRegisterType<Message>("com.mycompany.messaging", 1, 0, "Message");

可以在QML的对象声明中使用该类型,并且可以按照以下示例读取和写入其属性:

import com.mycompany.messaging 1.0
 
Message {
    author: "Amelie"
    creationDate: new Date()
}

注册非实例类型:
有时,可能需要向QML类型系统注册QObject派生的类,而不是将其注册为可实例化的类型。 例如,如果是C ++类,就是这种情况:

是不应可实例化的接口类型
是不需要公开给QML的基类类型
声明一些枚举,这些枚举应可从QML访问,但其他枚举不应可实例化
是应通过单实例提供给QML的类型,不应从QML实例化
Qt QML模块提供了几种注册非实例类型的方法:

qmlRegisterType()(不带参数)注册了不可实例化且无法从QML引用的C ++类型。 这使引擎可以强制从QML实例化任何继承的类型。

qmlRegisterInterface()注册现有的Qt接口类型。 该类型不能从QML实例化,并且不能使用它声明QML属性。 但是,从QML使用这种类型的C ++属性将执行预期的接口强制转换。

qmlRegisterUncreatableType()注册一个命名的C ++类型,该类型不可实例化,但应标识为QML类型系统的类型。 如果应从QML访问类型的枚举或附加属性,但该类型本身不可实例化,则此方法很有用。

qmlRegisterSingletonType()注册一个可以从QML导入的单例类型,如下所述。

请注意,向QML类型系统注册的所有C ++类型都必须是QObject派生的,即使它们不是不可实例化的。

用单例类型注册单例对象:
单例类型使属性,信号和方法可以在命名空间中公开,而无需客户端手动实例化对象实例。 特别是QObject单例类型是一种提供功能或全局属性值的高效便捷的方法。

请注意,单例类型没有关联的QQmlContext,因为它们在引擎中的所有上下文之间共享。 QObject单例类型实例由QQmlEngine构造和拥有,并且在销毁引擎时将销毁。

可以以与任何其他QObject或实例化类型类似的方式与QObject单例类型进行交互,除了将仅存在一个(引擎构造并拥有)实例,并且必须通过类型名称而不是id进行引用。 可以绑定QObject单例类型的Q_PROPERTY,并且可以在信号处理程序表达式中使用QObject模块API的Q_INVOKABLE函数。 这使单例类型成为实现样式或主题的理想方式,并且它们也可以代替导入“ …pragma library”脚本来使用,以存储全局状态或提供全局功能。

一旦注册,就可以像导入QML的任何其他QObject实例一样导入和使用QObject单例类型。 下面的示例假定QObject单例类型已注册到版本为1.0的“ MyThemeModule”命名空间中,其中QObject具有QColor“ color” Q_PROPERTY:

import MyThemeModule 1.0 as Theme
 
Rectangle {
    color: Theme.color // binding.
}

QJSValue也可以作为单例类型公开,但是客户应注意,此类单例类型的属性无法绑定。

有关如何实现和注册新的单例类型以及如何使用现有的单例类型的更多信息,请参见qmlRegisterSingletonType()。

注意:QML中已注册类型的枚举值应以大写字母开头。

类型修订和版本:
许多类型注册功能要求为注册的类型指定版本。 类型修订和版本允许新属性或方法存在于新版本中,同时保持与先前版本的兼容性。

考虑以下两个QML文件:

// main.qml
import QtQuick 1.0
 
Item {
    id: root
    MyType {}
}
// MyType.qml
import MyTypes 1.0
 
CppType {
    value: root.x
}

其中CppType映射到C ++类CppType。

如果CppType的作者在其类型定义的新版本中将root属性添加到CppType,则root.x现在将解析为其他值,因为root也是顶级组件的ID。 作者可以指定可以从特定的次要版本获得新的根属性。 这允许在不破坏现有程序的情况下将新属性和功能添加到现有类型。

REVISION标记用于将根属性标记为该类型的修订版1中添加的。 也可以使用Q_REVISION(x)宏将诸如Q_INVOKABLE,信号和插槽之类的方法标记为修订版本:

class CppType : public BaseType
{
    Q_OBJECT
    Q_PROPERTY(int root READ root WRITE setRoot NOTIFY rootChanged REVISION 1)
signals:
    Q_REVISION(1) void rootChanged();
};

要将新的类修订注册到特定版本,请使用以下功能:

template<typename T, int metaObjectRevision>
int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)

要为MyTypes 1.1注册CppType版本1:

qmlRegisterType<CppType,1>("MyTypes", 1, 1, "CppType")

root仅在导入MyTypes 1.1版时可用。

出于相同的原因,在更高版本中引入的新类型应使用qmlRegisterType的次要版本参数。

该语言的功能允许在不破坏现有应用程序的情况下进行行为更改。 因此,QML模块的作者应始终记住记录次要版本之间的更改,并且QML模块的用户应在部署更新的import语句之前检查其应用程序是否仍正常运行。

您还可以使用qmlRegisterRevision()函数注册您的类型所依赖的基类的修订版:

template<typename T, int metaObjectRevision>
int qmlRegisterRevision(const char *uri, int versionMajor, int versionMinor)
 
template<typename T, int metaObjectRevision>
int qmlRegisterUncreatableType(const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString& reason)
 
template<typename T, typename E, int metaObjectRevision>
int qmlRegisterExtendedUncreatableType(const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString& reason)

例如,如果更改了BaseType并且现在具有修订版1,则可以指定您的类型使用新修订版:

qmlRegisterRevision<BaseType,1>("MyTypes", 1, 1);

当从其他作者提供的基类派生时,这很有用。 从Qt Quick模块扩展类时。

注意:QML引擎不支持对属性或分组和附加属性对象的信号的修订。

注册扩展对象:
将现有的类和技术集成到QML中时,通常需要对API进行调整,以使其更好地适应声明性环境。 尽管通常可以通过直接修改原始类来获得最佳结果,但是如果这不可能或由于其他一些原因而变得复杂,则扩展对象无需进行直接修改就可以实现有限的扩展可能性。

扩展对象将其他属性添加到现有类型。 扩展对象只能添加属性,不能添加信号或方法。 扩展类型定义允许程序员在注册类时提供其他类型,称为扩展类型。 从QML内部使用时,这些属性与原始目标类透明合并。 例如:

QLineEdit {
    leftMargin: 20
}

leftMargin属性是添加到现有C ++类型QLineEdit的新属性,而无需修改其源代码。

qmlRegisterExtendedType()函数用于注册扩展类型。 请注意,它有两种形式。

template<typename T, typename ExtendedT>
int qmlRegisterExtendedType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)
 
template<typename T, typename ExtendedT>
int qmlRegisterExtendedType()

应该使用此函数代替常规的qmlRegisterType()变体。 这些参数与相应的非扩展注册函数相同,除了ExtendedT参数是扩展对象的类型之外。

扩展类是常规QObject,其构造函数采用QObject指针。 但是,扩展类的创建被延迟,直到访问第一个扩展属性。 创建扩展类,并将目标对象作为父级传递。 访问原始属性时,将使用扩展对象上的相应属性。

扩展对象示例演示了扩展对象的用法。[https://doc.qt.io/qt-5/qtqml-referenceexamples-extended-example.html]

定义特定于QML的类型和属性:
提供附加属性:

在QML语言语法中,存在附加属性和附加信号处理程序的概念,它们是附加到对象的附加属性。 本质上,此类属性是通过附加类型实现和提供的,并且这些属性可以附加到其他类型的对象。 这与由对象类型本身(或对象的继承类型)提供的普通对象属性形成对比。

例如,下面的项目使用附加的属性和附加的处理程序:

import QtQuick 2.0
 
Item {
    width: 100; height: 100
 
    focus: true
    Keys.enabled: false
    Keys.onReturnPressed: console.log("Return key was pressed")
}

在这里,Item对象能够访问和设置Keys.enabled和Keys.onReturnPressed的值。 这允许Item对象访问这些额外的属性,作为对其自身现有属性的扩展。

实现附加对象的步骤:

在考虑上述例子时,涉及几个方面:

存在一个匿名附加对象类型的实例,该实例具有enable和returnPressed信号,该信号已附加到Item对象,以使其能够访问和设置这些属性。

Item对象是附加对象,附加对象类型的实例已附加到该对象。

Keys是附加类型,它为attachee提供一个命名限定符“Keys”,通过它可以访问附加对象类型的属性。

当QML引擎处理此代码时,它将创建附加对象类型的单个实例,并将此实例附加到Item对象,从而为它提供对实例的enabled和returnPressed属性的访问。

通过提供附加对象类型和附加类型的类,可以提供从C++提供附加对象的机制。对于附加的对象类型,提供一个QObject派生类,该类定义要使attachee对象可访问的属性。对于附加类型,提供一个QObject派生类:

实现具有以下签名的静态qmlAttachedProperties():

static <AttachedPropertiesType> *qmlAttachedProperties(QObject *object);

此方法应返回附加对象类型的实例。

QML引擎调用此方法,以便将附加对象类型的实例附加到由对象参数指定的attachee。这个方法实现通常(虽然不是严格要求)将返回的实例作为对象的父对象,以防止内存泄漏。

此方法最多由引擎为每个attachee对象实例调用一次,因为引擎缓存返回的实例指针以供后续附加的属性访问。因此,在销毁attachee对象之前,不能删除附件对象。

通过调用带有QML_HAS_ATTACHED_PROPERTIES标志的QML_DECLARE_TYPEINFO()宏,声明为附加类型。

实现附加对象:一个示例

例如,以前面示例中描述的消息类型为例:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
    Q_PROPERTY(QDateTime creationDate READ creationDate WRITE setCreationDate NOTIFY creationDateChanged)
public:
    // ...
};

假设有必要在将消息发布到消息板上时在消息上触发信号,并在消息板上的消息到期时进行跟踪。 由于没有必要直接将这些属性添加到Message,因为这些属性与留言板上下文更相关,所以可以将它们实现为Message对象上的附加属性,这些属性是通过“ MessageBoard”限定符提供的。 就前面描述的概念而言,此处涉及的各方是:

匿名附加对象类型的实例,它提供已发布的信号和过期的属性。此类型由下面的MessageBoardAttachedType实现
一个消息对象,它将是attachee
消息板类型,它将是消息对象用来访问附加属性的附加类型
下面是一个示例实现。首先,需要有一个附加的对象类型,该对象类型具有被附加者可以访问的必要属性和信号:

class MessageBoardAttachedType : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool expired READ expired WRITE setExpired NOTIFY expiredChanged)
public:
    MessageBoardAttachedType(QObject *parent);
    bool expired() const;
    void setExpired(bool expired);
signals:
    void published();
    void expiredChanged();
};

然后附加类型MessageBoard必须声明qmlAttachedProperties()方法,该方法返回由MessageBoardAttachedType实现的附加对象类型的实例。此外,消息板必须通过QML_DECLARE_TYPEINFO()宏声明为附加类型:

class MessageBoard : public QObject
{
    Q_OBJECT
public:
    static MessageBoardAttachedType *qmlAttachedProperties(QObject *object)
    {
        return new MessageBoardAttachedType(object);
    }
};
QML_DECLARE_TYPEINFO(MessageBoard, QML_HAS_ATTACHED_PROPERTIES)

现在,消息类型可以访问附加对象类型的属性和信号:

Message {
    author: "Amelie"
    creationDate: new Date()
 
    MessageBoard.expired: creationDate < new Date("January 01, 2015 10:45:00")
    MessageBoard.onPublished: console.log("Message by", author, "has been
published!")
}

此外,C++实现可以通过调用QMLATTHCHEDFRESTIOSObject()函数来访问已连接到任何对象的附加对象实例。

例如:

Message *msg = someMessageInstance();
MessageBoardAttachedType *attached =
        qobject_cast<MessageBoardAttachedType*>(qmlAttachedPropertiesObject<MessageBoard>(msg));
 
qDebug() << "Value of MessageBoard.expired:" << attached->expired();

属性修饰符类型

属性修饰符类型是一种特殊的QML对象类型。属性修饰符类型实例影响应用它的属性(QML对象实例的)。有两种不同的属性修饰符类型:

属性值写入侦听器
属性值源
属性值写入拦截器可用于筛选或修改写入属性的值。目前,唯一受支持的属性值写入拦截器是QtQuick导入提供的行为类型。

属性值源可用于随时间自动更新属性值。客户端可以定义自己的属性值源类型。QtQuick导入提供的各种属性动画类型是属性值源的示例。

可以通过“on”语法创建属性修饰符类型实例并将其应用于QML对象的属性,如下例所示:

import QtQuick 2.0
 
Item {
    width: 400
    height: 50
 
    Rectangle {
        width: 50
        height: 50
        color: "red"
 
        NumberAnimation on x {
            from: 0
            to: 350
            loops: Animation.Infinite
            duration: 2000
        }
    }
}

客户端可以注册自己的属性值源类型,但当前不能注册属性值写入侦听器。

属性值源

属性值源是QML类型,可以使用on语法随时间自动更新属性值。例如,QtQuick模块提供的各种属性动画类型都是属性值源的示例。

属性值源可以通过C++类实现QQMLPrimeTyValueSurCE并提供一个实现,该实现可以随着时间的推移将不同的值写入属性。当使用QML中的on语法将属性值源应用于属性时,引擎将为其提供对此属性的引用,以便可以更新属性值。

例如,假设有一个RandomNumberGenerator类可用作属性值源,因此当应用于QML属性时,它将每隔500毫秒将属性值更新为一个不同的随机数。此外,可以向该随机数生成器提供maxValue。这个类可以实现如下:

class RandomNumberGenerator : public QObject, public QQmlPropertyValueSource
{
    Q_OBJECT
    Q_INTERFACES(QQmlPropertyValueSource)
    Q_PROPERTY(int maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged);
public:
    RandomNumberGenerator(QObject *parent)
        : QObject(parent), m_maxValue(100)
    {
        QObject::connect(&m_timer, SIGNAL(timeout()), SLOT(updateProperty()));
        m_timer.start(500);
    }
 
    int maxValue() const;
    void setMaxValue(int maxValue);
 
    virtual void setTarget(const QQmlProperty &prop) { m_targetProperty = prop; }
 
signals:
    void maxValueChanged();
 
private slots:
    void updateProperty() {
        m_targetProperty.write(QRandomGenerator::global()->bounded(m_maxValue));
    }
 
private:
    QQmlProperty m_targetProperty;
    QTimer m_timer;
    int m_maxValue;
};

当QML引擎遇到使用RandomNumberGenerator作为属性值源时,它将调用RandomNumberGenerator::setTarget()为类型提供已应用值源的属性。当RandomNumberGenerator中的内部计时器每500毫秒触发一次时,它将向指定的属性写入一个新的数值。

RandomNumberGenerator类在QML类型系统中注册后,就可以从QML中将其用作属性值源。下面,它用于每500毫秒更改一个矩形的宽度:

import QtQuick 2.0
 
Item {
    width: 300; height: 300
 
    Rectangle {
        RandomNumberGenerator on width { maxValue: 300 }
 
        height: 100
        color: "red"
    }
}

在所有其他方面,属性值源都是常规的QML类型,可以有属性、信号方法等,但具有附加功能,可以使用on语法更改属性值。

当一个属性值源对象被分配给一个属性时,QML首先尝试正常地分配它,就像它是一个普通的QML类型一样。仅当此赋值失败时,引擎才会调用setTarget()方法。这允许在上下文中使用类型,而不仅仅是作为值源。

指定QML对象类型的默认属性

任何注册为可实例化QML对象类型的QObject派生类型都可以选择指定该类型的默认属性。默认属性是如果对象的子对象未指定给任何特定属性,则自动将其指定给的属性。

可以通过调用具有特定“DefaultProperty”值的类的Q_CLASSINFO()宏来设置默认属性。例如,下面的MessageBoard类将其messages属性指定为该类的默认属性:

class MessageBoard : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<Message> messages READ messages)
    Q_CLASSINFO("DefaultProperty", "messages")
public:
    QQmlListProperty<Message> messages();
 
private:
    QList<Message *> m_messages;
};

这样,如果未将MessageBoard对象的子对象分配给特定属性,则可以将其自动分配给messages属性。例如:

MessageBoard {
    Message { author: "Naomi" }
    Message { author: "Clancy" }
}

如果未将消息设置为默认属性,则必须将任何消息对象显式分配给消息属性,如下所示:

MessageBoard {
    messages: [
        Message { author: "Naomi" },
        Message { author: "Clancy" }
    ]
}

(顺便说一下,Item::data属性是它的默认属性。添加到此数据属性的任何项对象也将添加到Item::children的列表中,因此使用默认属性可以为项声明可视子项,而无需将它们显式分配给children属性。)

使用Qt Quick模块定义视觉项目

使用Qt-Quick模块构建用户界面时,要可视化呈现的所有QML对象都必须派生自项类型,因为它是Qt-Quick中所有可视化对象的基类型。此项目类型由QQuestIt+C++类实现,QQuestC+++类由QT快速模块提供。因此,当需要在C++中实现可视类型,可以将其集成到基于QML的用户界面中时,应该对该类进行子类化。

有关详细信息,请参阅QQuickItem文档。此外,用C++教程编写QML扩展说明了QQuiTube为基础的可视化项目如何在C++中实现,并集成到QT基于快速的用户界面中。

接收对象初始化通知

对于某些自定义QML对象类型,在创建对象并设置其所有属性之前延迟特定数据的初始化可能是有益的。例如,如果初始化代价高昂,或者在初始化所有属性值之前不应执行初始化,则可能会出现这种情况。

Qt QML模块提供了QQmlParserStatus子类以实现这些目的。它定义了在组件实例化的不同阶段调用的许多虚拟方法。为了接收这些通知,C++类应该继承QQMLPARSSTATION,并且还使用QyIdFACESSUMER()宏通知QT元系统。

例如:

class MyQmlType : public QObject, public QQmlParserStatus
{
    Q_OBJECT
    Q_INTERFACES(QQmlParserStatus)
public:
    virtual void componentComplete()
    {
        // Perform some initialization here now that the object is fully created
    }
};

用C++编写QML扩展


QT C++与QML扩展教程。

QT QML模块提供了一组API,用于通过C++扩展来扩展QML。您可以编写扩展来添加自己的QML类型、扩展现有QT类型,或者调用不可从普通QML代码访问的C/C++函数。

本教程展示如何使用C++编写QML扩展,其中包括核心QML特征,包括属性、信号和绑定。它还展示了如何通过插件部署扩展。

本教程中涉及的许多主题在集成QML和C++及其文档子主题方面有进一步的详细记录。特别是,您可能会感兴趣的子主题,将C++类的属性暴露为QML,并从C++定义QML类型。

运行教程示例

本教程中的代码作为一个示例项目提供,其中的子项目与每个教程章节关联。在Qt Creator中,打开欢迎模式并从示例中选择教程。在编辑模式下,展开扩展qml项目,右键单击要运行的子项目(章节),然后选择运行。

第1章:创建新类型

扩展qml/chapter1基础

扩展QML时的常见任务是提供一种新的QML类型,该类型支持一些自定义功能,而不是内置的Qt Quick类型所提供的功能。 例如,可以执行此操作以实现特定的数据模型,或为类型提供自定义的绘画功能,或访问无法通过内置QML功能访问的系统功能(如网络编程)。

在本教程中,我们将展示如何使用Qt Quick模块中的C ++类扩展QML。 最终结果将是一个简单的饼图显示,该显示由几种自定义QML类型实现,这些QML类型通过诸如绑定和信号之类的QML功能连接在一起,并通过插件提供给QML运行时。

首先,让我们创建一个名为“PieChart”的新QML类型,它有两个属性:名称和颜色。我们将在名为“Charts”的可导入类型命名空间中提供它,版本为1.0。

我们希望这种PieChart类型可以从QML中使用,如下所示:

import Charts 1.0
 
PieChart {
    width: 100; height: 100
    name: "A simple pie chart"
    color: "red"
}

要做到这一点,我们需要一个C++类来封装这个PyCurpe类型及其两个属性。由于QML广泛使用Qt的元对象系统,这个新类必须:

从QObject继承
使用Q_PROPERTY宏声明其属性
这是我们的PieChart类,在PieChart.h中定义:

#include <QtQuick/QQuickPaintedItem>
#include <QColor>
 
class PieChart : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName)
    Q_PROPERTY(QColor color READ color WRITE setColor)
public:
    PieChart(QQuickItem *parent = 0);
 
    QString name() const;
    void setName(const QString &name);
 
    QColor color() const;
    void setColor(const QColor &color);
 
    void paint(QPainter *painter);
 
private:
    QString m_name;
    QColor m_color;
};

该类继承自QQuickPaintedItem,因为我们希望在使用QPainter API执行绘图操作时重写QQuickPaintedItem::paint()。如果类只是表示某种数据类型,而不是实际需要显示的项,那么它可以简单地从QObject继承。或者,如果我们想扩展现有的基于QObject的类的功能,它可以从该类继承。或者,如果我们想创建一个不需要使用QPainter API执行绘图操作的可视化项,我们可以将QQuickItem子类化。

PieChart类使用Q_PROPERTY宏定义了两个属性name和color,并重写QQuickPaintedItem::paint()。cpp中的类实现只是根据需要设置并返回m_name和m_color值,并实现paint()以绘制简单的饼图。它还关闭QGraphicsItem::ItemHasNoContents标志以启用绘制:

PieChart::PieChart(QQuickItem *parent)
    : QQuickPaintedItem(parent)
{
}
...
void PieChart::paint(QPainter *painter)
{
    QPen pen(m_color, 2);
    painter->setPen(pen);
    painter->setRenderHints(QPainter::Antialiasing, true);
    painter->drawPie(boundingRect().adjusted(1, 1, -1, -1), 90 * 16, 290 * 16);
}

既然我们已经定义了PieChart类型,我们将从QML中使用它。app.qml文件创建饼图项,并使用标准qml文本项显示饼图的详细信息:

import Charts 1.0
import QtQuick 2.0
 
Item {
    width: 300; height: 200
 
    PieChart {
        id: aPieChart
        anchors.centerIn: parent
        width: 100; height: 100
        name: "A simple pie chart"
        color: "red"
    }
 
    Text {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 }
        text: aPieChart.name
    }
}

请注意,尽管颜色在QML中指定为字符串,但它会自动转换为PieChart color属性的QColor对象。为各种其他基本类型提供了自动转换;例如,“640x480”之类的字符串可以自动转换为QSize值。

我们还将创建一个C++应用程序,使用QQueVIEW运行并显示App.QML。应用程序必须使用qmlRegisterType()函数注册PieChart类型,以允许从QML使用它。如果不注册类型,app.qml将无法创建PieChart。

下面是application main.cpp:

#include "piechart.h"
#include <QtQuick/QQuickView>
#include <QGuiApplication>
 
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
 
    qmlRegisterType<PieChart>("Charts", 1, 0, "PieChart");
 
    QQuickView view;
    view.setResizeMode(QQuickView::SizeRootObjectToView);
    view.setSource(QUrl("qrc:///app.qml"));
    view.show();
    return app.exec();
}

对qmlRegisterType()的此调用将PieChart类型注册为类型为“ PieChart”的类型名称空间,该名称空间的名称为“ Charts”,版本为1.0。

最后,我们编写一个.pro项目文件,其中包括文件和声明性库:

QT += qml quick
 
HEADERS += piechart.h
SOURCES += piechart.cpp \
           main.cpp
 
RESOURCES += chapter1-basics.qrc
 
DESTPATH = $$[QT_INSTALL_EXAMPLES]/qml/tutorials/extending-qml/chapter1-basics
target.path = $$DESTPATH
 
qml.files = *.qml
qml.path = $$DESTPATH
 
INSTALLS += target qml

现在我们可以构建并运行应用程序:
在这里插入图片描述
注意:您可能会看到一个警告表达式。。。取决于不可通知的属性:PieChart::name。发生这种情况是因为我们向writeable name属性添加了绑定,但尚未为其定义通知信号。因此,如果名称值更改,QML引擎将无法更新绑定。这将在以下章节中讨论。

本章将引用以下文件中的源代码:

第2章:C++方法与信号的连接

扩展qml/chapter2方法

假设我们希望PieChart有一个“clearChart()”方法来擦除图表,然后发出一个“chartCleared”信号。我们的app.qml可以调用clearChart()并接收如下chartCleared()信号:

import Charts 1.0
import QtQuick 2.0
 
Item {
    width: 300; height: 200
 
    PieChart {
        id: aPieChart
        anchors.centerIn: parent
        width: 100; height: 100
        color: "red"
 
        onChartCleared: console.log("The chart has been cleared")
    }
 
    MouseArea {
        anchors.fill: parent
        onClicked: aPieChart.clearChart()
    }
 
    Text {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 }
        text: "Click anywhere to clear the chart"
    }
}

为此,我们将CultCARTHER()方法和CARTCAREAREDE()信号添加到C++类:

class PieChart : public QQuickPaintedItem
{
    ...
public:
    ...
    Q_INVOKABLE void clearChart();
 
signals:
    void chartCleared();
    ...
};

Q_INVOKABLE的使用使clearChart()方法可用于Qt元对象系统,进而可用于QML。 请注意,它可以被声明为Qt插槽,而不是使用Q_INVOKABLE,因为也可以从QML调用该插槽。 这两种方法都是有效的。

clearChart()方法只需将颜色更改为Qt::transparent,重新绘制图表,然后发出chartCleared()信号:

void PieChart::clearChart()
{
    setColor(QColor(Qt::transparent));
    update();
 
    emit chartCleared();
}

现在,当我们运行应用程序并单击窗口时,饼图消失,应用程序输出:

qml: The chart has been cleared

本章将引用以下文件中的源代码:

第3章:添加属性绑定

扩展qml/chapter3绑定

属性绑定是QML的一个强大功能,它允许自动同步不同类型的值。当属性值更改时,它使用信号通知和更新其他类型的值。

让我们为颜色属性启用属性绑定。这意味着如果我们有这样的代码:

import Charts 1.0
import QtQuick 2.0
 
Item {
    width: 300; height: 200
 
    Row {
        anchors.centerIn: parent
        spacing: 20
 
        PieChart {
            id: chartA
            width: 100; height: 100
            color: "red"
        }
 
        PieChart {
            id: chartB
            width: 100; height: 100
            color: chartA.color
        }
    }
 
    MouseArea {
        anchors.fill: parent
        onClicked: { chartA.color = "blue" }
    }
 
    Text {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 }
        text: "Click anywhere to change the chart color"
    }
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-03 15:50:34  更:2022-03-03 15:51:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:38:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码