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
 

最近的回复

  • 看了你的脚本,看来PowerShell是一种shell的方言,支持function和自定义的符号。和bash还不一样。
    我以前也写过Apple Script,看来大家都在shell语言上加上自己的特性啊。
    顺便了解到你们主要用Visual Studio开发Qt应用的。😁 😁

    read more
  • @wentaojia2014
    代码中的 {1} 是自动生成的,注意删掉

    read more
  • 脚本玩家路过一下😁
    我写了一段Powershell脚本,可以一键打开/关闭调试信息和代码优化。
    原理就是解析.sln文件(xml格式),找到关联的所有.vcproj文件(也是xml格式),并把其中的字段修改掉。

    看脚本

    # filename: DebugInfoSwitch.ps1 Add-Type -AssemblyName System.Xml.Linq # 获取绝对路径 function GetAbsolutePath { [OutputType([string])] param([string]$filePath) $aPath = Resolve-Path $filePath return Split-Path $aPath } # 解析sln文件,提取出project列表 function ParseSolution { [OutputType([string[]])] param([string]$solutionFile) $parttern = [regex]"^Project*" [string[]] $projs = @() Get-Content $solutionFile | Where-Object {$_ -match $parttern} | ForEach-Object { $items = $_.ToString().Split(',') if ($items[1].EndsWith("vcxproj`"")) { $projs += $items[1] } } return $projs } #Write-Host($debugGroup | Format-Table | Out-String ) function UpdateDebugInfo { param([System.Xml.Linq.XNamespace]$xNamespace,[System.Xml.Linq.XElement] $group, [bool]$isOpen) [System.Xml.Linq.XElement]$xClCompile = $group.Element($xNamespace + "ClCompile") [System.Xml.Linq.XElement]$xLink = $group.Element($xNamespace + "Link") [System.Xml.Linq.XElement]$format = $xClCompile.Element($xNamespace + "DebugInformationFormat") [System.Xml.Linq.XElement]$optimization = $xClCompile.Element($xNamespace + "Optimization") [System.Xml.Linq.XElement]$generate = $xLink.Element($xNamespace + "GenerateDebugInformation") if ($null -eq $format) { $format = [System.Xml.Linq.XElement]::new($xNamespace + "DebugInformationFormat") $xClCompile.Add($format) } if ($null -eq $optimization) { $optimization = [System.Xml.Linq.XElement]::new($xNamespace + "Optimization") $xClCompile.Add($optimization) } if ($null -eq $generate) { $generate = [System.Xml.Linq.XElement]::new($xNamespace + "GenerateDebugInformation") $xLink.Add($generate) } if ($isOpen) { $format.SetValue("ProgramDatabase"); $optimization.SetValue("Disabled"); $generate.SetValue("true"); } else { $format.SetValue("None"); $optimization.SetValue("MaxSpeed"); $generate.SetValue("false"); } } function SwitchDebugInfo { param([string]$projPath, [bool]$open, [bool]$isRelease) if (Test-Path($projPath)) { [System.Xml.Linq.XDocument]$xDoc = [System.Xml.Linq.XDocument]::Load($projPath) [System.Xml.Linq.XNamespace]$xNamespace = $xDoc.Root.GetDefaultNamespace(); [System.Xml.Linq.XElement]$group = $null $groups = $xDoc.Root.Elements($xNamespace + "ItemDefinitionGroup") foreach ($i in $groups) { if ($isRelease) { if ($i.Attribute("Condition").Value -match "^*Release*") { $group = $i; break; } } else { if ($i.Attribute("Condition").Value -match "^*Debug*") { $group = $i; break; } } } UpdateDebugInfo $xNamespace $group $open $xDoc.Save($projPath) } } #脚本入口函数 function Main { param([string]$sln, [bool]$open, [bool]$isRelease) Write-Host("sln file ", $sln) $slnPath= GetAbsolutePath $sln $projList = ParseSolution $sln $t = $projList.Split(' ') | ForEach-Object { $s = $_.ToString() if ($s) { $projPath=-Join($slnPath, '\', ($s.SubString(1, $s.Length - 2) )) SwitchDebugInfo $projPath $open $isRelease } } } function Usage { Write-Host ("Usage: DebugInfoSwitch.ps1 slnFilePath mode[D/d for Debug, R/r for Release] open[Y/y for open, N/n for close]") Write-Host ("Example for open Release mode debugInfo: DebugInfoSwitch.ps1 xxx.sln R y") Write-Host ("Example for close Debug mode debugInfo: DebugInfoSwitch.ps1 xxx.sln d N") } #检查参数 [string]$sln = "" [bool]$open = $true [bool]$isRelease = $false if ( $args.Count -ne 3) { Usage return } $sln = $args[0] if ($args[1] -eq "D" -or $args[1] -eq "d") { $isRelease = $false } elseif ($args[1] -eq "R" -or $args[1] -eq "r") { $isRelease = $true } else { Usage return } if ($args[2] -eq "Y" -or $args[2] -eq "y") { $open = $true } elseif ($args[2] -eq "N" -or $args[2] -eq "n") { $open = $false } else { Usage return } #调用主函数 Main $sln $open $isRelease

    用的时候,再写个bat脚本,调用这个ps1并传参数就行了。
    比如打开Release模式的调试信息、同时关掉优化开关

    @echo off powershell.exe -NoProfile -ExecutionPolicy Bypass -File DebugInfoSwitch.ps1 ./you/path/to.sln R N

    关闭调试信息、打开优化开关,则是传相反的参数

    @echo off powershell.exe -NoProfile -ExecutionPolicy Bypass -File DebugInfoSwitch.ps1 ./you/path/to.sln R Y

    read more
  • 我将USD在Linux中依赖的文件和脚本放在了我创建的QQ群里,我们的QQ群是“上海USD研究小组”。加入本小组,可以快速地在USD中上手解决编译问题,以及快速得到同行的响应。
    上海USD研究小组

    read more

关注我们

微博
QQ群