Qml组件化编程1-按钮的定制与封装



  • 简介

    本文是《Qml组件化编程》系列文章的第一篇,涛哥将教大家,如何在Qml中实现各种功能的按钮,

    同时也会教大家一些组件化编程的思想,如何将做好的功能封装成一个个组件,以便在工程中复用。

    写作背景

    作者“武威的涛哥”,从2015年开始参加工作,便入手了Qml,参与了很多大大小小的Qml项目,

    至今已有四年多实战经验。

    2019年,涛哥决定把自己掌握的很多知识都总结整理出来,以《Qml组件化编程》系列文章的形式分享给广大Qml

    爱好者和开发者。文章主要发布在涛哥的博客知乎专栏-涛哥的Qt进阶之路

    涛哥会坚持高质量和深入浅出的原则,将文章写好,让支持涛哥的读者能够受益匪浅。

    系列文章中涉及的源代码,绝大部分涛哥都会在github上开源。

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

    有时也难免会犯一些错误,希望看到的读者能够热心指出。有任何相关的问题,也欢迎与涛

    哥交流,向涛哥提出建议和意见。

    感谢大家!

    文章定位

    涛哥写的是进阶教程,教大家如何从新手成长为高手,要理解文章中的内容,需要有一点儿Qml基础。

    关于基础教程,网络上有很多,涛哥推荐以下几个质量比较高的:

    豆子的文章前75章关于Widget部分也很不错。

    • 还有一本安晓辉的书《QtQuick核心编程》

    Qt版本的选择

    Qt发布的版本有很多,一般稳妥的做法是使用LTS(长期支持版)版本的最后一个修正版本。

    当前已知的LTS最新修正版本是5.9.8和5.12.3 , 涛哥会优先使用这两个版本,后续有版本

    要求的地方,涛哥都会进行额外的说明。

    默认按钮

    开始进入正题了

    Qml中已经有了现成的按钮,在QtQuick.Controls模块中, 目前有两个版本:

    • QtQuick.Controls 1.x

    预览

    1.x的版本是通过style的方式进行定制

    预览

    • QtQuick.Controls 2.x

    预览

    2.x的版本则是通过修改Control属性的方式进行定制

    预览

    2.x版本的默认风格,还可以通过修改配置文件qtquickcontrols2.conf中的style来修改

    可参考Qt在线文档- style配置

    Qml版本混用

    这里顺便说一个经常有人问到的问题: 2.x版本中能否混用1.x版本的控件?

    答案是可以。只需要使用别名机制即可。(如果你看过Qml的源代码,就能轻易发现这个用法)

    // MixControls.qml
    import QtQuick 2.0                              
    import QtQuick.Controls 2.0                    //导入Controls 2.0模块
    import QtQuick.Controls 1.4 as QC14            //导入Controls 1.4模块,取别名QC14
    import QtQuick.Controls.Styles 1.4 as QCS14    //导入Controls.Styles 1.4模块,取别名QCS14
    
    Rectange {
        Button {        //默认使用2.0的Button
        }
    
        QC14.Button {   //通过别名使用1.4的Button
            style: QCS14.ButtonStyle {  //通过别名使用1.4的style
    
            }
        }
    }
    

    按钮的本质

    默认的按钮,很多时候并不能达到想要的效果,比如圆角、贴图片、渐变色的按钮等等,还需要做很多的定制化工作

    其实按钮的本质,就是一个可以点击的区域(MouseArea),附带颜色(Rectangle)、图片(Image)或文字(Text)。

    涛哥教大家造第一个轮子,一个带文字的按钮。

    // Taobutton.qml
    import QtQuick 2.0
    import QtQuick.Controls 2.0
    Item  {
        width: 800
        height: 600
        property color btnColor: "#009688"
        Rectangle {
            width: 140
            height: 40
            anchors.centerIn: parent
    
            color: btnArea.pressed ? Qt.darker(btnColor, 1.2) : (btnArea.containsMouse ? Qt.lighter(btnColor, 1.2) : btnColor)
            Text {
                id: btnText
                anchors.centerIn: parent
                text: qsTr("我是一个按钮")
            }
            MouseArea {
                id: btnArea
                anchors.fill: parent
                hoverEnabled: true  //打开悬浮
                onClicked: {
                    console.log(qsTr("按钮被按了"))
                }
            }
        }
    }
    
    

    使用qmlscene预览一下效果

    预览

    (gif录制工具不能鼠标悬浮,所以悬浮效果看不到,实际上是有悬浮效果的,可自行尝试)

    说明一下,qml中有个全局的对象Qt,它有一组调颜色的函数,Qt.lighter和Qt.darker,分别可以按系数来变浅和加深

    一个颜色值,这样只要有了一个颜色值,就能自动计算出深一点或者浅一点的颜色,可以减少很多配颜色的工作哦

    再看一下这个表达式,当鼠标按下去的时候,使用深一点的颜色,鼠标悬浮的时候使用浅一点的颜色,其它情况就用设定的颜色

        color: btnArea.pressed ? Qt.darker(btnColor, 1.2) : (btnArea.containsMouse ? Qt.lighter(btnColor, 1.2) : btnColor)
    

    这里的1.2是试出来的值,也可以使用其它值。

    按钮的演变

    如果要带个边框呢?只要给Rectange设置边框的宽度和大小就行了

        property color btnBorderColor: "orange"
        Rectangle {
            ...
            border.width: btnArea.containsMouse ? 2 : 0 //鼠标悬浮时有宽度,才能看到边框
            border.color: btnBorderColor
            ...
        }
    

    如果要圆角呢? 只要给Rectangle 设置radius就行了

        Rectangle{
            ...
            radius: 5
            ...
        }
    

    如果要圆形呢?只要给Rectangle设置宽度和高度相等(正方形),radius是宽度的一半

        Rectangle{
            ...
            width: 120
            height: width
            radius: width / 2
            ...
        }
    

    如果要背景色做成渐变的呢?

        Rectangle{
            id: gradientBtn
            ...
            property color btnColor: containsMouse ? Qt.darker("#009688") : "#009688"
            color: btnColor
            gradient: Gradient {
                GradientStop { position: 0 ; color: Qt.darker(gradientBtn.btnColor, 1.2) }
                GradientStop { position: 0.5 ; color:  Qt.darker(gradientBtn.btnColor, 1.4)  }
                GradientStop { position: 1 ; color:  Qt.darker(gradientBtn.btnColor, 1.6) }
            }
            ...
        }
    

    顺带提一下,5.12的Rectangle,有了新的渐变色 渐变色网站,大约有180种

    可以直接在Qml中使用。

    
        gradient: Gradient.NightFade //通过枚举使用
    
        gradient: "SugarLollipop"       //通过字符串名字使用
    

    预览

    (gif录制工具不能鼠标悬浮,所以悬浮效果看不到,实际上是有悬浮效果的,可自行尝试)

    组件化按钮

    为了能够复用我们的按钮,需要将它做成一个组件。

    这里以文本按钮为例:

    // TTextBtn.qml
    
    import QtQuick 2.9
    import QtQuick.Controls 2.0
    Rectangle {
        id: root
        property alias textItem: t      //导出Text实例,方便外部直接修改
        property alias text: t.text     //导出文本
        property alias textColor: t.color   //导出文本颜色
        property alias containsMouse: area.containsMouse    //导出鼠标悬浮
        property alias containsPress: area.containsPress    //导出鼠标按下
        signal clicked();               //自定义点击信号
        color: "transparent"
        Text {
            id: t
            //默认坐标居中
            anchors.centerIn: parent
            //默认文字对齐方式为水平和垂直居中
            verticalAlignment: Text.AlignVCenter
            horizontalAlignment: Text.AlignHCenter
            //默认宽度为parent的宽度,这样字太长超出范围时自动显示省略号
            width: parent.width
        }
    
        MouseArea {
            id: area
            anchors.fill: parent;
            hoverEnabled: parent.enabled;
            onClicked: root.clicked();  //点击时触发自定义点击信号
            cursorShape: Qt.PointingHandCursor  //悬浮或点击时的鼠标样式
        }
    }
    
    

    Qml组件,先以最简单的方式理解,就是放在一个单独的Qml文件中,声明一些属性导出,由使用者去实例化并设置属性

    比如这样:

    预览

    组件化的好处,包括容易复用(如上图,多个实例都不一样)、可以统一修改(后面会有动态换皮肤的方案,依赖于组件化)、

    便于维护和扩展等。后续涛哥还会讲如何做多层抽象的组件、单例组件、如何引用插件中的组件等。

    组件化图片按钮

    带图片的按钮,只需要把Rectangle换成Image即可。

    为了应对各种按钮状态,涛哥做了以下的属性扩展

    // TImageBtn.qml
    
    import QtQuick 2.9
    import QtQuick.Controls 2.0
    Item {
        id: root
        property url normalUrl      //常规状态下的图片路径
        property url hoveredUrl     //悬浮
        property url pressedUrl     //按下
        property url disabledUrl    //禁用
    
        property alias imageItem: img           //直接别名导出Image实例,外面可以修改其任意属性
        property alias imageUrl: img.source     //别名导出图片路径
    
        property alias imageWidth: img.width
        property alias imageHeight: img.height
        property alias imageAnchors: img.anchors
        property alias containsMouse: area.containsMouse
        property alias containsPress: area.containsPress
        //点击信号
        signal clicked();
        Image {
            id: img
            anchors.fill: parent
            //默认按鼠标状态选取不同的图片
            source: root.enabled ? (containsPress ? pressedUrl : (containsMouse ? hoveredUrl : normalUrl)) : disabledUrl
        }
    
        MouseArea {
            id: area
            anchors.fill: parent;
            hoverEnabled: parent.enabled;
            onClicked: root.clicked();
            cursorShape: Qt.PointingHandCursor
            preventStealing: true
        }
    }
    

    组件化图文按钮

    有了前面的文字按钮和图片按钮,我们可以做一个图片和文字都有的按钮。

    图片和文字同时显示,那么就有了布局问题。图片在左?还是在右?在上?还是在下?

    涛哥这里用了点技巧,封装了一个同时支持四种布局的图文按钮

    // TImgTextBtn.qml
    
    import QtQuick 2.9
    import QtQuick.Controls 2.0
    Rectangle {
        id: root
        property url normalUrl
        property url hoveredUrl
        property url pressedUrl
        property url disabledUrl
    
        property alias imageItem: img
        property alias imageUrl: img.source
        property alias imageWidth: img.width
        property alias imageHeight: img.height
    
        property alias textItem: t
        property alias text: t.text
        property alias textColor: t.color
        property alias containsMouse: area.containsMouse
        property alias containsPress: area.containsPress
        signal clicked();
    
        //5.10以前的版本,Qml中没有枚举,用int属性代替枚举
        property int layoutType: layoutImageLeft    //布局类型,默认图片在左,外部可修改
        readonly property int layoutImageLeft: 0    //图片在左 只读属性,代替枚举
        readonly property int layoutImageRight: 1   //图片在右 只读属性,代替枚举
        readonly property int layoutImageUp: 2      //图片在上 只读属性,代替枚举
        readonly property int layoutImageDown: 3    //图片在下 只读属性,代替枚举
        color: "transparent"
        Image {
            id: img
            source: root.enabled ? (containsPress ? pressedUrl : (containsMouse ? hoveredUrl : normalUrl)) : disabledUrl
        }
        Text {
            id: t
            //默认文字对齐方式为水平和垂直居中
            verticalAlignment: Text.AlignVCenter
            horizontalAlignment: Text.AlignHCenter
        }
        //按布局类型 处理布局
        Component.onCompleted: {
            switch (layoutType) {
            case layoutImageLeft:
                img.anchors.verticalCenter = root.verticalCenter
                t.anchors.verticalCenter = root.verticalCenter
                img.anchors.left = root.left
                t.anchors.left = img.right
                t.anchors.leftMargin = 6
                break;
            case layoutImageRight:
                img.anchors.verticalCenter = root.verticalCenter
                t.anchors.verticalCenter = root.verticalCenter
                t.anchors.left = root.left
                img.anchors.left = t.right
                img.anchors.leftMargin = 6
                break
            case layoutImageUp:
                img.anchors.horizontalCenter = root.horizontalCenter
                t.anchors.horizontalCenter = root.horizontalCenter
                img.anchors.top = root.top
                t.anchors.top = img.bottom
                t.anchors.topMargin = 6
                break
            case layoutImageDown:
                img.anchors.horizontalCenter = root.horizontalCenter
                t.anchors.horizontalCenter = root.horizontalCenter
                t.anchors.top = root.top
                img.anchors.top = t.bottom
                img.anchors.topMargin = 6
                break;
            }
        }
        MouseArea {
            id: area
            anchors.fill: parent;
            hoverEnabled: parent.enabled;
            onClicked: root.clicked();
            cursorShape: Qt.PointingHandCursor
        }
    }
    

    最后,我们来看看效果吧

    预览

    转载声明

    文章出自涛哥的博客 -- 点击这里查看涛哥的博客
    本作品采用 知识共享署名-非商业性使用-相同方式共享 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群