导读本文是 Qt 学习笔记系列的第二篇。在实际开发中我们很少在一个窗口里堆砌所有控件更常见的做法是将功能相关的控件封装成自定义控件。本文通过Slider与SpinBox联动的案例讲解自定义控件的封装思路以及 Qt 对象树机制如何帮助我们自动管理内存和查找控件。一、为什么要封装自定义控件上一篇文章里我们直接在窗口中new按钮和输入框用setGeometry()手动摆位置。控件少的时候没问题但一旦界面复杂起来所有逻辑都挤在一个类里会非常混乱。更好的做法是把一组功能相关的控件封装成一个独立的类对外只暴露必要的接口。比如滑动条 数字框这个组合在很多界面都会用到封装一次就能反复复用。下面我们用**Slider与SpinBox联动** 这个经典案例来演示完整过程。二、先搭框架创建控件并设置属性首先创建一个新的QWidget子类BoxAndSlider。在构造函数中把两个基础控件创建出来BoxAndSlider::BoxAndSlider(QWidget*parent):QWidget(parent),ui(newUi::BoxAndSlider){ui-setupUi(this);// 创建 SpinBox数字选择框和 Slider滑动条mySpinBoxnewQSpinBox(this);mySlidernewQSlider(this);// 设置 Slider 为水平方向并指定初始位置和大小mySlider-setOrientation(Qt::Horizontal);mySlider-setGeometry(70,30,100,15);mySpinBox-setGeometry(0,25,40,25);}创建控件时传了this作为父对象传了this之后Qt 会把这个控件挂到当前对象的对象树上父对象销毁时子对象会自动跟着销毁不用我们手动delete。关于对象树后面第二节会详细讲。三、实现联动让两个控件互相影响联动的逻辑很简单拖动Slider-SpinBox数字跟着变修改SpinBox-Slider位置跟着变用connect把双方的valueChanged信号连到对方的设置值操作上就行了。3.1Slider-SpinBox直接连接// Slider 值改变 - 更新 SpinBox 的值connect(mySlider,QSlider::valueChanged,this,BoxAndSlider::changeBoxValue);槽函数就一行voidBoxAndSlider::changeBoxValue(intx){mySpinBox-setValue(x);}3.2SpinBox-Slider需要处理信号重载反过来的连接有个小问题要注意。QSpinBox::valueChanged有两个重载版本// 重载1值改变时触发参数是 intvoidvalueChanged(intval);// 重载2文本改变时触发参数是 QStringvoidvalueChanged(constQStringtext);如果直接写QSpinBox::valueChanged编译器不知道你要连的是哪一个会报错。我们用函数指针来明确指定// 用函数指针明确指定要的是 int 参数的那个版本void(QSpinBox::*valueChangedPointer)(int)QSpinBox::valueChanged;connect(mySpinBox,valueChangedPointer,this,BoxAndSlider::changeSliderPosition);Qt 信号有重载时怎么处理三种方案函数指针本文用的方法C11 lambda 表达式Qt 提供的QOverloadint::of(QSpinBox::valueChanged)模板类四、封装的意义外部调用有多简单经过上面的步骤BoxAndSlider已经是一个完整的自定义控件了。外部使用的时候根本不需要知道内部有一个SpinBox和一个Slider// 外部只需要创建一个 BoxAndSlider内部的联动已经自动工作BoxAndSlider*myWidgetnewBoxAndSlider(this);这就体现了封装的核心价值把复杂的内部逻辑藏起来对外只暴露简单的接口。五、对象树Qt 的自动内存管家前面创建控件时反复出现new QSpinBox(this)这个this就是把新建的控件挂到对象树上。对象树是 Qt 非常重要的机制理解它能避免很多内存问题。5.1 对象树是什么当你在创建QObject包括所有控件时指定了父对象Qt 会自动在内部维护一棵树。比如CustomWidget父窗口 ├── QLabellabel ├── QLabellabel_2 └── BoxAndSlider自定义控件 ├── QSpinBox └── QSlider每个控件都知道自己的父节点是谁也知道自己的所有子节点是谁。5.2 对象树帮我们做了什么功能说明自动内存管理父对象销毁时所有子对象自动销毁不用手动 delete结构清晰父子层级一目了然代码更好维护坐标管理子控件的坐标相对于父控件方便计算布局节点查找可以按名称、按类型快速找到目标控件其中自动内存管理是最重要的。很多初学者new了控件却忘记delete造成内存泄漏。只要在创建时指定了父对象对象树就会在父对象析构时自动清理所有子对象。5.3 遍历和查找节点对象树提供了很方便的API来访问树上的节点。遍历所有子对象// 打印当前窗口的所有子控件名称foreach(QObject*child,this-children()){qDebug()child-objectName();}按类型查找// 找出所有 QLabel 类型的子控件批量修改文字foreach(QLabel*child,this-findChildrenQLabel*()){child-setText(修改文字);}findChildrenT()非常实用——假设你有一个表单窗口里面有 20 个输入框需要在清空时一起重置逐个手动调用setText()显然很累用一行findChildrenQLineEdit*()就能全部搞定。注意如果控件是根节点没有父对象调用parent()会返回nullptr访问前要做空指针检查否则程序会崩溃。六、总结本文核心知识点知识点关键内容自定义控件封装将多个基础控件组合成复合控件提高复用性函数指针解决信号重载时connect的歧义问题对象树new 控件(this)时自动挂载父对象销毁时子对象自动释放children()获取所有子对象用于遍历findChildrenT()按类型查找子对象用于批量操作