实现自动生成变量的Setter&Getter(Qt版)
这玩意儿是啥?
Qt提供了非常简易方便的信号槽机制,基于Qt的信号槽,我们能够快速实现一个观察者模式。在观察者模式中,被观察的对象(假设为Model)内有相应的属性数据,观察者们(假设为Controller_(1-n))通过注册槽函数监视着被观察对象的变化,当被观察的对象发生变化时,会扔出信号,观察者们在槽函数中进行响应。
在这个过程中,被观察的对象需要有相应的属性数据,并提供相应的Set接口来改变属性数据,属性数据发生改变后,会自动扔出属性被改变的信号,同时被观察的对象还可以提供Get接口来直接获取属性数据。
我们打算实现的,就是简化代码,用少量的代码来生成:属性数据、Set&Get接口函数、调用Set接口设置属性数据后自动扔出信号。
这玩意儿解决什么问题?
解决的问题
对于被观察的对象,它们内部的所有属性数据都有共同的地方:需要Set&Get函数、需要属性被改变时扔出去的信号、在调用Set函数设置属性数据后扔出信号。如果我们纯手写这些东西,也能达到最终的目的,但是我们需要手写和维护许多的Set&Get函数以及信号,当属性数据变得很多的时候,手写和维护的工作量都会变得很庞大,一不小心就容易出错,而且会有很多重复的代码。作为一名合格的超懒程序员,希望能把这些东西做成自动生成的,将重复代码降到最低,提升代码整体的可维护性。
防止回环
在进入实现之前,我们得先解决一个小问题。
看个简单的图,Controller_(2-n)绑定了Model中的一个信号,在一个属性数据被设置后会扔出这个信号,Controller_1通过Model提供的Set接口设置了这个属性数据,Controller_(2-n)响应Model中这个属性数据的改变。
1 | graph LR |
如果Controller_1也绑定了这个信号,这个由Controller_1调用Set接口改变Model中的属性数据而引发的信号,Controller_1自己也会收到,但是Controller_1很可能只希望在别的Controller对Model的这个属性数据进行操作时才响应,但是此时Controller_1无法分辨出这个属性数据是由它自己改变的还是由其他Controller改变的。 (一个例子是,Controller_1对接了相应的窗口,窗口中有一个输入框,允许用户改变这个属性数据,其他Controller也对接了其他的窗口,也有同样的输入框能改变这个属性数据,当用户在Controller_1相关的窗口的输入框内改变了这个属性数据后,Controller_1会调用Set函数改变Model中的属性数据,其他窗口接收到信号后更新自己输入框内的数据保持同步显示,此时Controller_1也会收到这个信号,但是用户的操作是在Controller_1相关的窗口上的,Controller_1其实可以不用去更新窗口的数据,由于没法判断是由谁触发的改变,Controller_1只能更新窗口的数据。)
1 | graph LR |
我们可以在Set函数中给一个调用源,来标识触发改变的源对象,我们可以直接使用调用Set函数的Controller的类实例地址来作为调用源的标识符,因为地址是唯一的。 (这样上面例子中的Controller_1就能在槽函数中通过标识符确定信号是由自己调用Set函数而触发的,Controller_1可以不用更新窗口中的数据了。)
这玩意儿咋实现?
来个特例的实现
假设我们的Model中有一个颜色属性数据Color,有RGBA共4个int型的分量,那么我们针对Color,在Model中就要有如下的代码。
1 | class Model : public QObject { |
由特例到通用化
我们期望上面的代码能够通用化,这样才能自动去生成。
在这条路子上,C++超级强大的模板也施展不了手脚了(是我太菜T_T),Qt的继承自QObject的类不能再声明一个继承自QObject的内部类[可参考下方的示例代码],没有类没有函数,模板凉了。
1 | class Model : public QObject { |
没关系,我们还有一个终极利器宏定义。当下的许多IDE也都支持了宏展开后的函数的提示和联想功能。
在特例中,我们能发现除了属性数据名称、属性数据值名称、属性数据值类型是不同的外,其他的大部分对于不同的属性数据其实是一致的,那么这些一致的部分就是我们需要自动生成的部分。定义宏名及宏参数列表,宏参数列表依次就是属性数据名称、属性数据值依次的名称,属性数据值依次的类型。
1 |
这里我们把生成过程拆成了两个宏,一个是SETGET,一个是SIGNAL,这么做的原因是Qt的信号槽机制依赖于Qt自己的moc预编译器,moc预编译器在正常的C++编译器编译之前会预编译我们写的代码,moc预编译器只有在源码中找到signals关键字才处理信号函数,如果我们把signals写在了宏中,moc预编译器的处理早于C++的编译器,此时宏还没有被展开和替换,moc预编译器不会在宏中寻找signals关键字,只会在找到关键字后在处理中遇到宏才进行展开,如果我们不把Set&Get函数与信号分开,都写在宏中,signals关键字只能放在宏内,这就会导致moc预编译器不生成Qt的信号函数,最终导致编译出错。
1 |
写好生成宏后,我们的属性数据就能十分快速地用宏来生成相应的Set&Get函数代码、信号及属性数据值的声明了。我们只需要在Model类声明中写上下面简单的几行,就能在Model类中声明propColorR、propColorB、propColorG、propColorA变量,生成SetPropsColor、SetColorR、SetColorG、SetColorB、SetColorA、GetPropsColor、GetColorR、GetColorG、GetColorB、GetColorA函数,生成ColorChanged信号,并在Set函数中扔出ColorChanged信号。舒服~
1 | class Model : public QObject { |
最后,我们再提供一个生成changer的宏方便生成changer。
1 |
前面也说到调用源的标识符,changer就是这个标识符,用来标识触发改变的源对象,它通常是Controller的类实例地址,那么我们在Controller内调用Set的地方,直接用宏包装this指针来生成这个标识符吧。
1 | model.SetColorR(PROPERTY_GENERATOR_MAKE_CHANGER(this), 255); |
把宏写短一点儿
这个宏写得简直又臭又长,又臭又长的宏确实能降低撞宏的概率,但是在实际项目中打这么长的宏手也很累啊。赶紧简化简化:
1 |
|
嘿,这样只要没有定义PROPERTY_GENERATOR_FORCE_FULL_MACRO宏指定要强制使用全名,我们就可以用简化的宏PG_MAKE_CHANGER、PG_SETGET_x和PG_SIGNAL_x了。
完整代码
源码仅实现了一个属性数据最多支持4个属性值,4个属性值一般能满足绝大多数的场景。加上调用源的标识符,信号中携带的参数达到了5个,过长的参数列表不仅影响代码质量,也会影响信号槽的性能,当然对于现代硬件条件,普通的应用这点影响几乎可以忽略。如果有需要,可以参照代码补充支持更多的属性值。
1 | //////////////////////////////////////////////////////////////////////////////// |
协议
本文以上内容遵循CC BY-ND 4.0协议,署名-禁止演绎。
本文中所提供的源代码遵循MIT开源协议。 代码托管于:https://github.com/KondeU/QtPropGen
代码仓中含有演示示例代码。
转载请注明出处:https://tis.ac.cn/blog/kongdeyou/实现自动生成变量的settergetter_qt版/
署名作者:kongdeyou
后记
2020年12月29日 周二 南京大雪纷飞,一天的光景大地已是白皑皑的一片。