Qml组件化编程2-可拖动组件和定制窗体



  • 简介

    本文是《Qml组件化编程》系列文章的第二篇,涛哥将教大家,如何在Qml中实现可拖动组件,通过拖动

    改变组件的大小和位置;以及实现定制窗体(无边框和标题栏), 并把拖动组件应用在顶层窗体。

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

    拖动组件

    拖动改变坐标

    拖动改变坐标的原理很简单,鼠标移动的时候改变目标Item的坐标即可。

    说话的功夫,涛哥就造了个轮子出来

    (其实是太常用了,涛哥已经写了很多遍)

    import QtQuick 2.9
    import QtQuick.Controls 2.5
    Item {
       width: 800
       height: 600
    
       Rectangle {
           id: moveItem
    
           //注意拖动目标不要使用锚布局或者Layout,而是使用相对坐标
           x: 100
           y: 100
           width: 300
           height: 200
    
           color: "lightblue"
           MouseArea {
               anchors.fill: parent
               property real lastX: 0
               property real lastY: 0
               onPressed: {
                   //鼠标按下时,记录鼠标初始位置
                   lastX = mouseX
                   lastY = mouseY
               }
               onPositionChanged: {
                   if (pressed) { 
                       //鼠标按住的前提下,坐标改变时,计算偏移量,应用到目标item的坐标上即可
                       moveItem.x += mouseX - lastX
                       moveItem.y += mouseY - lastY
                   }
               }
           }
       }
    }
    

    预览

    上面例子中的MouseArea是拖动区域,Rectangle是要拖动的目标Item。

    为了实现高度的可复用性,涛哥将MouseArea独立封装成一个组件,并提供一个control属性,

    让外部使用组件实例的时候指定要拖动的目标。

    // TMoveArea.qml
    
    import QtQuick 2.9
    
    MouseArea {
        id: root
    
        property real lastX: 0
        property real lastY: 0
        property bool mask: false       //有时候外面需要屏蔽拖动,导出一个mask属性, 默认false。
        property var control: parent   //导出一个control属性,指定要拖动的目标, 默认就用parent好了。注意目标要有x和y属性并且可修改
    
        onPressed: {
            lastX = mouseX;
            lastY = mouseY;
        }
        onContainsMouseChanged: { //修改一下鼠标样式,以示区别
            if (containsMouse) {
                cursorShape = Qt.SizeAllCursor;
            } else {
                cursorShape = Qt.ArrowCursor;
            }
        }
        onPositionChanged: {
            if (!mask && pressed && control)
            {
                control.x +=mouseX - lastX
                control.y +=mouseY - lastY
            }
        }
    }
    
    

    TMoveArea组件的用法

    Item {
        anchors.fill: parent
    
        Rectangle {
            x: 100
            y: 200
            width: 400
            height: 300
            color: "darkred"
            //实例化一个MoveArea
            TMoveArea {
                //指定control为parent。 其实默认就是parent,写出来示意一下
                control: parent
                anchors.fill: parent
            }
        }
    }
    
    

    一般来说,将

      property var control: parent
    

    中的var换成确切的类型比如Item会更好一些,Qml底层引擎处理var会慢一些,但是这样就限制了

    目标必须是Item或者其子类。var是把双刃剑,有利有弊。涛哥后面要拖动的目标还包括QQuickView

    这种类型,所以这里用var就好了。

    拖动改变大小

    拖动改变大小,原理参考下面这张示意图:

    预览

    就是在要拖动的目标Item的8个位置分别放一个拖动组件,并在拖动时计算相应的坐标和大小变化即可。

    涛哥先是把TMoveArea改造成了TDragRect

    // TDragRect.qml
    import QtQuick 2.9
    import QtQuick.Controls 2.0
    Item {
        id: root
        property alias containsMouse: mouseArea.containsMouse
        signal posChange(int xOffset, int yOffset)
        implicitWidth: 12   //这里隐式的宽为12
        implicitHeight: 12  //这里隐式的高为12
        property int posType: Qt.ArrowCursor
    
        //5.10之前, qml是不能定义枚举的,用只读的int属性代替一下。
        readonly property int posLeftTop: Qt.SizeFDiagCursor
        readonly property int posLeft: Qt.SizeHorCursor
        readonly property int posLeftBottom: Qt.SizeBDiagCursor
        readonly property int posTop: Qt.SizeVerCursor
        readonly property int posBottom: Qt.SizeVerCursor
        readonly property int posRightTop: Qt.SizeBDiagCursor
        readonly property int posRight: Qt.SizeHorCursor
        readonly property int posRightBottom: Qt.SizeFDiagCursor
        MouseArea {
            id: mouseArea
            anchors.fill: parent
            hoverEnabled: true
            property int lastX: 0
            property int lastY: 0
            onContainsMouseChanged: {
                if (containsMouse) {
                    cursorShape = posType;
                } else {
                    cursorShape = Qt.ArrowCursor;
                }
            }
            onPressedChanged: {
                if (containsPress) {
                    lastX = mouseX;
                    lastY = mouseY;
                }
            }
            onPositionChanged: {
                if (pressed) {
                    posChange(mouseX - lastX, mouseY - lastY)
                }
            }
        }
    }
    
    

    就是把前面的鼠标拖动时的处理逻辑,换成了带参数的信号发送出去,由外面决定怎么用这两个坐标

    同时也定义了一组枚举,用来表示拖动区域的位置。位置不同,则鼠标样式不同。

    之后涛哥写了一个叫TResizeBorder的组件,里面实例化了8个TDragRect组件,分别放在前面示意图

    所示的位置,并实现了不同的处理逻辑。

    (后来涛哥把上下左右四个中心点换成了四个边)

    // TResizeBorder.qml
    import QtQuick 2.7
    
    Rectangle {
        id: root
        color: "transparent"
        border.width: 4
        border.color: "black"
        width: parent.width
        height: parent.height
        property var control: parent
        TDragRect {
            posType: posLeftTop
            onPosChange: {
                //不要简化这个判断条件,至少让以后维护的人能看懂。化简过后我自己都看不懂了。
                if (control.x + xOffset < control.x + control.width)
                    control.x += xOffset;
                if (control.y + yOffset < control.y + control.height)
                    control.y += yOffset;
                if (control.width - xOffset > 0)
                    control.width-= xOffset;
                if (control.height -yOffset > 0)
                    control.height -= yOffset;
            }
        }
        TDragRect {
            posType: posMidTop
            x: (parent.width - width) / 2
            onPosChange: {
                if (control.y + yOffset < control.y + control.height)
                    control.y += yOffset;
                if (control.height - yOffset > 0)
                    control.height -= yOffset;
            }
        }
        TDragRect {
            posType: posRightTop
            x: parent.width - width
            onPosChange: {
                //向左拖动时,xOffset为负数
                if (control.width + xOffset > 0)
                    control.width += xOffset;
                if (control.height - yOffset > 0)
                    control.height -= yOffset;
                if (control.y + yOffset < control.y + control.height)
                    control.y += yOffset;
            }
        }
        TDragRect {
            posType: posLeftMid
            y: (parent.height - height) / 2
            onPosChange: {
                if (control.x + xOffset < control.x + control.width)
                    control.x += xOffset;
                if (control.width - xOffset > 0)
                    control.width-= xOffset;
            }
        }
        TDragRect {
            posType: posRightMid
            x: parent.width - width
            y: (parent.height - height) / 2
            onPosChange: {
                if (control.width + xOffset > 0)
                    control.width += xOffset;
            }
        }
        TDragRect {
            posType: posLeftBottom
            y: parent.height - height
            onPosChange: {
                if (control.x + xOffset < control.x + control.width)
                    control.x += xOffset;
                if (control.width - xOffset > 0)
                    control.width-= xOffset;
                if (control.height + yOffset > 0)
                    control.height += yOffset;
            }
        }
        TDragRect {
            posType: posMidBottom
            x: (parent.width - width) / 2
            y: parent.height - height
            onPosChange: {
                if (control.height + yOffset > 0)
                    control.height += yOffset;
            }
        }
        TDragRect {
            posType: posRightBottom
            x: parent.width - width
            y: parent.height - height
            onPosChange: {
                if (control.width + xOffset > 0)
                    control.width += xOffset;
                if (control.height + yOffset > 0)
                    control.height += yOffset;
            }
        }
    }
    
    

    注意组件的顶层,使用的是透明的Rectangle,这样做的目的是,外面可以给这个组件设置

    不同的颜色、边框等。无论哪种UI框架,透明处理都是需要一定的性能消耗的,所以在不需要显示

    出来的情况下,组件顶层最好还是用Item替代。

    融合

    我们来实例化一个能拖动改变大小和位置的Item

    Item {
        width: 800
        height: 600
        Rectangle {
            x: 300
            y: 200
            width: 120
            height: 80
            color: "darkred"
            TMoveArea {
                anchors.fill: parent
                control: parent     //默认就是parent,可以不写。这里写出来示意一下。
            }
            TResizeBorder {
                control: parent     //默认就是parent,可以不写。这里写出来示意一下。
                anchors.fill: parent
    
            }
        }
    }
    

    预览

    用起来还是挺方便的,直接在目标Item里面实例化一个TResizeBorder组件,指定control即可。

    这里同时实例化了TMoveArea和TResizeBorder两个组件,作为目标Item的child,就把两种功能 融合起来了。

    注意前后顺序,如果反过来写则TMoveArea会把ResizeBorder遮盖住。(Qml是有z轴的,以后的文章涛哥再讲)

    多级组件和Qml应用的框架结构

    回过头来看一下,先是封装了两个组件:TMoveArea和TDragRect,之后又封装了一个组件:TResizeBorder,

    而这个TResizeBorder里面使用了多个TDragRect组件,显然是有层级结构在里面的。

    涛哥把TMoveArea和TDragRect这样的最基础的组件叫做一级组件,那么TResizeBorder就是一个二级组件。

    涛哥大量的实战经验后,总结出了这样一种Qml应用框架结构:

    一级和二级组件可以单独做成一个插件(或者叫Qml通用库)。
    
    实际的Qml项目,在这些基础上,做一些功能性或者业务性的组件,即三级组件。
    
    由这些三级组件组成一堆的页面(Page)。
    
    最终的main.qml中,只剩下Page的布局。
    

    示意图如下:

    预览

    自定义窗口

    自定义窗口,这里以QQuickView

    无边框

    去掉边框,需要在C++中设置flag为Qt::FramelessWindowHint

    同时我们注册view到qml上下文环境,给后面的功能来使用。

        ...
        QQuickView view;
        view.setFlag(Qt::FramelessWindowHint);
        view.rootContext()->setContextProperty("view", &view);
        ...
    

    可拖动窗口

    将我们前面做的两种拖动框放在main.qml中,填满顶层Item,并指定control为view。

    //main.qml
    
    import QtQuick 2.0
    
    #import TaoQuick 1.0      //这里是做成插件的情况下,引用了插件
    #import "qrc:/Tao/Qml"    //没有做插件的情况下,只要引用qml文件的资源路径即可
    
    Item {
        //标题栏
        TitlePage {
            id: titleRect
            width: root.width
            height: 60
            ...
            //标题栏区域,实例化一个可以拖动位置的组件
            TMoveArea {
                height: parent.height
                anchors {
                    left: parent.left
                    right: parent.right
                    rightMargin: 170 //留一点右边距,给最小化、最大化、关闭等按钮用
                }
                //指定拖动目标为view
                control: view
            }
            ...
        }
        //实例化一个拖动改大小的组件
        TResizeBorder {
            //指定拖动目标为view
            control: view
            anchors.fill: parent
        }
        ...
    }
    
    

    自定义标题栏

    标题栏的关键就是实现右侧的三个按钮,如果你看了《Qml组件化编程1-按钮的定制与封装》

    这都没有什么难度了。涛哥这里用图片按钮的方式实现。

    注意最大化按钮在最大化状态下变成标准化按钮。

    最小化:view.showMinimized()

    最大化:view.showMaximized()

    标准化:view.showNormal()

    关闭: view.close()

    这里给出关键代码

    Item{
        ...
        property bool isMaxed: false
        Row {
            id: controlButtons
            height: 20
            anchors.verticalCenter: parent.verticalCenter
            anchors.right: parent.right
            anchors.rightMargin: 12
            spacing: 10
            TImageBtn {
                width: 20
                height: 20
                imageUrl: containsMouse ? "qrc:/Image/Window/minimal_white.png" : "qrc:/Image/Window/minimal_gray.png"
                onClicked: {
                    view.showMinimized()
                }
            }
            TImageBtn {
                width: 20
                height: 20
                visible: !isMaxed
                imageUrl: containsMouse ? "qrc:/Image/Window/max_white.png" : "qrc:/Image/Window/max_gray.png"
                onClicked: {
                    view.showMaximized()
                    isMaxed = true
                }
            }
            TImageBtn {
                width: 20
                height: 20
                visible: isMaxed
                imageUrl: containsMouse ? "qrc:/Image/Window/normal_white.png" : "qrc:/Image/Window/normal_gray.png"
                onClicked: {
                    view.showNormal()
                    isMaxed = false
                }
            }
            TImageBtn {
                width: 20
                height: 20
                imageUrl: containsMouse ? "qrc:/Image/Window/close_white.png" : "qrc:/Image/Window/close_gray.png"
                onClicked: {
                    view.close()
                }
            }
        }
    }
    

    效果

    最后,我们来看一下效果吧

    预览

    转载声明

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