Qml组件化编程5-Qml与C++交互



  • 简介

    本文是《Qml组件化编程》系列文章的第五篇,涛哥将教大家,Qml与C++的交互。

    Qml已经有很多功能,不过终归会有不够用或不适用的地方,需要通过与C++的交互进行功能扩展。

    这回涛哥尝试把所有Qml与C++交互相关的知识点都写出来,做一个透彻、全面的总结。


    顺便说一下,涛哥的TaoQuick项目正式开源了, 系列文章中的所有功能,包括动态换皮肤、切换多语言等等,都集成在了TaoQuick中,

    同时涛哥也在TaoQuick中使用了持续集成(CI)技术,目前已经能够自动编译、发布Windows和 Macos平台的软件包,可以在github的Release界面下载体验。

    互联网行业很流行的DevOps理念,在TaoQuick项目中得到了最佳的实践。

    (linux平台的发布工具linuxdeployqt暂时还有点问题,涛哥后续会搞定的)

    地址在这https://github.com/jaredtao/TaoQuick, 赶快去star吧。

    注:文章主要发布在涛哥的博客知乎专栏-涛哥的Qt进阶之路

    C++访问Qml

    c++访问Qml有两种方式: findChild和 QQmlComponent。

    findChild

    了解Qt的人都知道,Qt的很多对象是QObject的子类,这些QObject只要设置了parent,就是有父子关系的,会产生一棵 "对象树"。

    只要有了根节点,树上的任意节点都可以通过findChild的方式获取到。

    写个简单的TaoObject,来示意一下:

    class TaoObject 
    {
    public:
        //构造函数,传递parent进来
        TaoObject(TaoObject *parent = nullptr) : m_pParent(parent) 
        {}
        //析构函数,析构children。即子对象自动回收机制。
        ~TaoObject() 
        {
            for (auto *pObj : m_children) 
            {
                delete pObj;
            }
            m_chilrren.clear();
        }
        //获取name
        const QString &getName() const { return m_name;}
        //设置name
        void setName(const QString &name) { m_name = name;} 
        //查找子Object
        TaoObject * findChild(const QString &name) 
        {
            //先检查自己的名字,是否匹配目标名字
            if (m_name == name) 
            {
                return this;
            }
            //遍历子Object,查找
            for (auto pObj: m_children) 
            {
                //递归调用,深度优先的搜索
                auto resObj = pObj->findChild(name);
                if (resObj) 
                {
                    return resObj;
                }
            }
            return nullptr;
        }
    private:
        //存储名字
        QString m_name;
        //子对象列表
        std::vector<TaoObject *> m_children;
        //父对象指针
        TaoObject *m_pParent = nullptr;
    }
    
    

    Qml的基本元素,大多是继承于QQuickItem,而QQuickItem继承于QObject。

    所以Qml大多数对象都是QObject的子类,也是可以通过findChild的方式获取到对象指针。

    拿到了QObject,可以通过qobject_cast转换成具体的类型来使用,也可以直接用QObject的invok方法。

    例如有如下Qml代码:

    Item {
        id: root
        ...
        Rectangle {
            id: centerRect
            objectName: "centerRect"        //必不可少的objectName
            property bool canSee: visible   //自定义属性,可以被C++ invok访问
            signal sayHello()               //自定义信号1,可以被C++ invok调用
            signal sayHelloTo(name)         //自定义信号2,带参数。可以被C++ invok调用。参数的名字要起好,后面通过这个名字来使用参数
            function rotateToAngle(angle)   //自定义js函数,旋转至指定角度。可以被C++ invok调用。
            {
                rotation = angle
                return true;
            }
        }
        ...
    }
    
    

    那么在C++ 中访问的方式是:

    • 如果用QQuickView加载qml,就是
    QQuickView view;
    ...
    QObject *centerObj = view.rootObject()->findChild<QObject *>("centerRect");
    if (!centerObj) { return;}
    
    • 如果用QQmlEngine加载qml,就是
    QQmlEngine engine;
    ...
    QObject *centerObj = engine.rootObject()->findChild<QObject *>("centerRect");
    if (!centerObj) { return;}
    

    (QObject类型也可以换成QQuickItem 或者其它)

    拿到了对象指针,接下来就好办了

    访问其属性

    bool canSee = centerObj->property("canSee").toBool();
    

    发射其信号(其实就是函数调用)

    QObject::invokeMethod(centerObj, "sayHello");
    QObject::invokeMethod(centerObj, "sayHelloTo", Q_ARG(QString, "Tao"))
    

    调用其js函数,可以传参数过去,可以取得返回值

    bool ok;
    QObject::invokeMethod(centerObj, "rotateToAngle", Q_RETURN_ARG(bool, ok), Q_ARG(qreal 180));
    

    这里再补充一下, Qml中给自定义的信号写槽或连接到别的槽(Qml中的槽就是js函数):

    Item {
        id: root
        ...
        Rectangle {
            id: centerRect
            objectName: "centerRect" //必不可少的objectName
            signal sayHello()       //自定义信号1
            signal sayHelloTo(name) //自定义信号2,带参数。参数的名字要起好,后面通过这个名字来使用参数
    
            function rotateToAngle(angle) //自定义js函数,旋转至指定角度
            {
                rotation = angle
            }
    
            onSayHello: {               //信号1的槽
                console.log("hello")
            }
            onSayHelloTo: {             //信号2的槽,直接用信号定义时的参数名字name作为关键字访问参数
                console.log("hello", name)
            }
            Component.onCompleted: { 
                //信号2 连接到 root的函数。参数会自动匹配。
                sayHello.connect(root.rootSayHello)
            }
        }
        ...
    
        function rootSayHello(name) {
            console.log("root: hello", name)
        }
    }
    

    QQmlComponent

    C++中的QQmlComponent可以用来动态加载Qml文件,并可以创建多个实例,

    对应Qml中的Component。Qml中还有一个Loader,也可以动态加载并创建单个实例。

    (QQmlComponent这种方式不太多见,不过涛哥之前参与过开发一个框架,使用的就是QQmlComponent动态加载Qml,

    完全在c++中控制界面的加载,加载效率、内存占用上都比纯Qml优秀。)

    来看一个例子:

    // Circle.qml
    Rectangle {
        width: 300
        height: width
        radius: width / 2
        color: "red"
    }
    
    QQmlEngine engine;
    QQmlComponent component(&engine, QUrl::fromLocalFile("Circle.qml"));
    
    QObject *circleObject = component.create();
    QQuickItem *item = qobject_cast<QQuickItem*>(circleObject);
    int width = item->width();
    

    拿到对象指针,就和前面的一样了,这里不再赘述了。

    Qml访问C++

    Qml要访问C++的内容,需要先从C++把要访问的内容注册进Qml。

    先说说能用哪些:

    注册过后,Qml中可以访问的内容,包括 Q_INVOKABLE 修饰的函数、枚举、 QObject的属性 信号 槽

    Q_INVOKABLE 函数可以用在普通的结构体或者类中,但是这种用法不常见/不方便。常见的是在QObject的子类中,给非槽函数设置为Q_INVOKABLE

    枚举的注册Qt帮助文档很详细,而且5.10以后可以在qml中定义枚举了,这里涛哥就不展开了。

    QObject的属性 信号 槽,都是可以通过注册后,在qml中使用的。信号、槽都可以带参数,槽可以有返回值。

    class BrotherTao : public QObject 
    {
        Q_OBJECT //这个宏一定要写上。不写可能的后果是,moc生成失败,信号 槽实现不了,编译过不了。
        Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) //自定义属性,操作包括: 读、写和通知。Qml可以读写、获取通知
    public:
        ...
        //唱歌
        Q_INVOKABLE void sing();        //invok函数,可以被Qml调用。可以带参数和返回值
    public slots:
        //打游戏,参数为次数,返回值为得分。
        int playGame(int count);        //槽函数,可以被Qml调用。可以带参数和返回值
    
    signals:
        //肚子饿了。参数为想吃的东西。
        void hungry(const QString &foodName);                  //信号,可以被Qml接收。
        ...
    };
    

    这里要说的是,属性、函数参数、返回值的类型,都需要是Qml能识别的类型。

    Qt的常用类型已经在Qt内部注册好了,自定义的需要单独注册。

    再说说怎么用:

    注册分为两种:注册类型和注册实例。

    注册类并使用

    qmlRegisterTyle<BrotherTao>("BrotherTao",1, 0, "BrotherTao");
    
    import BrotherTao 1.0
    Item {
        BrotherTao {         //实例化一个对象
            id: tao
            onHungry: {     //给信号写个槽函数
                if (foodName === "蛋炒饭") {      //示意一下,不要在意吃啥。
                    console.log("涛哥要吃蛋炒饭")
                } else if (foodName === "水饺") {
                    console.log("涛哥要吃水饺")
                } 
            }
        }
        ...
        Button {
            onClicked: {
                //这个按钮按下的时候,涛哥开始唱歌
                tao.sing();
            }
        }
        Button {
            onClicked: {
                //这个按钮按下的时候,涛哥开始打游戏
                let score = tao.sing(3);
                console.log("涛哥打游戏次数", 3, "得分为", score)
            }
        }
    }
    

    注册实例并使用

    BrotherTao tao;     //C++中创建的实例
    
    //如果用QQ'u'ic'kView加载Qml
    QQuickView view;
    ...
    view.rootContext()->setContextProperty("tao", &tao);    //注意这个名字不要用大写字母开头,规则和Qml中的id不能用大写字母开头一样。
    
    //如果用QQmlEngine加载Qml
    QQmlEngine engine;
    ...
    engine..rootContext()->setContextProperty("tao", &tao); //注意这个名字不要用大写字母开头,规则和Qml中的id不能用大写字母开头一样。
    
    
    //这种不用再import了
    Item {
        Connections {        //通过connectins连接信号
            target: tao     //指定target
            onHungry: {     //给信号写个槽函数
                if (foodName === "蛋炒饭") {      //示意一下,不要在意吃啥。
                    console.log("涛哥要吃蛋炒饭")
                } else if (foodName === "水饺") {
                    console.log("涛哥要吃水饺")
                } 
            }
        }
        ...
        Button {
            onClicked: {
                //这个按钮按下的时候,涛哥开始唱歌
                tao.sing();
            }
        }
        Button {
            onClicked: {
                //这个按钮按下的时候,涛哥开始打游戏
                let score = tao.sing(3);
                console.log("涛哥打游戏次数", 3, "得分为", score)
            }
        }
    }
    

    文章出自涛哥的博客
    文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作 © 涛哥

    联系方式


    作者 涛哥
    开发理念 弘扬鲁班文化,传承工匠精神
    博客 https://jaredtao.github.io
    知乎 https://www.zhihu.com/people/wentao-jia
    邮箱 jared2020@163.com
    微信 xsd2410421
    QQ 759378563

    请放心联系我,乐于提供咨询服务,也可洽谈商务合作相关事宜。

    打赏

    weixin
    zhifubao


    如果觉得涛哥写的还不错,还请为涛哥打个赏,您的赞赏是涛哥持续创作的源泉。



Log in to reply
 

走马观花

最近的回复

  • Qt for MCUs

    搭建Qt for MCUs PC端开发环境。qt for mcus提供了一个完整的图形框架和工具包,包含了在MCUs上设计、开发和部署gui所需的一切。它允许您在裸机或实时操作系统上运行应用程序。

    先决条件

    开发主机环境支持仅限于Windows 10

    MSVC compiler v19.16 (Visual Studio 2017 15.9.9 or newer) x64

    CMake v3.13 or newer (you can install it using the Qt Online installer) x64

    使用Qt联机安装程序安装Qt for MCUs,该安装程序可通过Qt帐户下载

    安装Qt 5.14和Qt Creator 4.11 or higher

    安装链接

    › Qt: https://account.qt.io/downloads
    › CMake: https://cmake.org/download/
    › Python 2.7 32-bit: https://www.python.org/downloads/release/python-2716/
    › Arm GCC: https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnutoolchain/gnu-rm/downloads
    › J-Link Software Pack: https://www.segger.com/downloads/jlink/JLink_Windows.exe
    › J-Link OpenSDA Firmware: https://www.segger.com/downloads/jlink/OpenSDA_MIMXRT1050-EVKHyperflash
    › STM32CubeProgrammer: https://www.st.com/en/development-tools/stm32cubeprog.html
    › STM32 ST-LINK Utility: https://www.st.com/en/development-tools/stsw-link004.html​​​​​​​

    Qt Creator设置 启用Qt Creator插件 选择“帮助>关于插件”,然后从列表中选择“MCU支持(实验性)”插件,重新启动Qt Creator以应用更改
    替代文字 为MCU创建Qt工具包

    选择工具>选项>设备>MCU

    选择Qt for MCUs-Desktop 32bpp作为目标

    如果尚未设置,请提供Qt for MCUs安装目录的路径。

    单击Apply应用。

    替代文字

    替代文字
    替代文字

    注意:

    编译器要选X64,Qt版本要选64bit,CMake Tool选x64

    打开恒温器项目demo

    选择文件>打开文件或项目。。。

    打开CMakefiles.txt文件来自thermo文件夹的文件。

    选择Qt作为MCU-桌面32bpp套件。

    单击“配置项目”以完成。

    替代文字

    问题

    开发主机环境支持仅限于Windows 10

    C++编译失败,文本大字体.pixelSize.

    文本类型无法正确呈现需要复杂文本布局的unicode序列。对复杂文本使用StaticText

    read more
  • H

    hi 有问题请教你,方便加个联系方式吗

    read more
  • boost.asio是一个很棒的网络库,这回儿我也开始系统地学习起来了。想想当年接触boost,也有八年多了。这次开始接触boost,觉得既熟悉又陌生。熟悉的是小写字母+下划线的命名方式、晦涩的模板、很慢的编译速度以及较大的程序体积,陌生的是asio的各种概念:io服务、接收器、套接字等等:我之前对网络编程不是非常了解。

    于是根据我的理解,参考《Boost.Asio C++网络编程》实现了这样一个简单的客户端和服务端通信的例子,例子非常简单,还不完善,但是幸运的是,可以在本机上互通了。
    下面是客户端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void writeHandler( const boost::system::error_code& ec, size_t bytesTransferred ) { if ( ec ) { cout << "Write data error, code: " << ec.value( ) << "transferred: " << bytesTransferred << endl; } else { cout << "OK! " << bytesTransferred << "bytes written. " << endl; } } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::socket sock( service ); ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; sock.connect( ep, ec ); if ( ec ) { cout << "Connect error, code: " << ec.value( ) << ", We will exit." << endl; return ec.value( ); } else { char buf[1024] = "Hello world!"; sock.async_write_some( buffer( buf ), writeHandler ); sock.close( ); } return service.run( ); }

    下面是服务端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void acceptHandle( const boost::system::error_code& code ) { cout << "Accepted." << endl; } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; ip::tcp::socket sock( service ); ip::tcp::acceptor acceptor( service, ep ); acceptor.async_accept( sock, acceptHandle ); if ( ec ) { cout << "There is an error in server. code: " << ec.value( ) << endl; } return service.run( );// 阻塞运行 }

    运行结果是这样的:
    78448d7b-b3ae-42fc-9e2e-4dd2fbdac2c2-image.png

    我对boost.asio中几个概念的理解:

    io_service,这就是一个类似事件循环的东西,它为io设备提供服务,故名。不管是套接字、文件还是串口设备,都要使用它的服务。它的run()函数相当于启动了一个事件循环。一旦有消息了,即进行响应。这也是实现异步编程的重要基础。 socket,这个类则是套接字,可以处理TCP或者是UDP请求。有同步以及异步的处理方式,也有带异常以及不带异常的处理方式。 acceptor,接收器,仅仅是服务端使用。相当于其余框架中的listener,作接收用的。

    比较浅显,如果有不当之处,敬请指正。

    read more
  • 843143141.jpg
    闲下来了,我又开始大规模地学习了。
    最近开始学习内存模型和无锁结构。因为这个是和操作系统密切相关的,懂得这些对于编写C++服务端应用程序
    有着非常好的帮助。之前我对内存模型以及无锁结构几乎没有什么了解,我就询问群里的大佬看看有没有可以参考的资料。
    大佬很高兴,并且推荐了我一本名为《Memory Model》的电子书。这本电子书虽然页数不多,但是从起源到发展,
    从源码到汇编,都给我们详细地介绍了。看了一遍,不是非常理解,但是依然尝试将自己的理解写下来,以便日后翻阅。
    首先因为多核处理器成为主流,多线程的程序已经非常常见,因此我们不可避免地要处理多线程程序的同步问题。
    然后,因为编译器默认都对源码进行了优化,在单核处理器中这通常不是什么问题,但是在多核处理器中,就会因为编译器
    对其进行了乱序处理而导致程序出现问题。由此深入地探讨内存模型。
    内存模型主要分为:
    载-载 顺序(load-load order)
    载-存 顺序(load-store order)
    存-载 顺序(store-load order)
    存-存 顺序(store-store order)
    依赖载入顺序(dependent loads order)

    通过内存栅栏(memory barrier)能够避免编译器对指令的乱序。Linux中有

    READ_ONCE( x, value ) WRITE_ONCE( x )

    避免这些读写被编译器乱序或者是优化掉。

    这里谈到volatile关键字。在另外一篇博客上说,volatile具有“易变性、不可优化性、顺序性”。简单说,由于
    被volatile声明的变量,指令须从内存读取,并且不能被编译器乱序以及优化。在Java(语言扩展)和MSVC(系统兼容)上,
    还附带了Accquire()和Release()语义,因此可部分用于多线程环境。但多数情况下,还是慎用volatile,
    因为不同架构的处理器,它的内存模型是千变万化的,不能一而概之。

    至于C++11,它提供了std::atomic<T>这个模板类,相当于提供了很多方式来实现不同内存模型的原子操作。
    它的load()和store()方法,第二个参数有以下几个选项:

    std::memory_order_relaxed std::memory_order_seq_cst std::memory_order_acq_rel std::memory_order_acquire std::memory_order_release std::memory_order_consume

    我们最常用来实现RCpc(Release Consistency、Processor Consistency)是使用

    std::memory_order_acquire std::memory_order_release

    这两对。

    作为例子,在实现自旋锁时使用std::atomic<T>是这样的:

    struct SpinLock2 { void lock( ) { for ( ; ; ) { while ( lock_.load( std::memory_order_relaxed ) ); if ( !lock_.exchange( true, std::memory_order_acquire ) ) break; } } void unlock( ) { lock_.store( false, std::memory_order_release ); } std::atomic<bool> lock_ = { false }; };

    read more

关注我们

微博
QQ群