写出形似QML的C++代码
最开始想出的标题是《Declarative C++ GUI库》,但太标题党了。只写了两行代码,连Demo都算不上,怎么能叫库呢……后来想换掉“库”这个字,但始终找不到合适词来替换。最后还是起了个low一点的名字,贱名好养活啊!
这篇文章的目的是介绍如何只用C++写出带有Declarative风格的代码。有一些GUI库需要额外的预处理过程(比如qt),还有一些也支持XML格式的GUI声明,但需要运行时Parse那个XML(比如wxWidgets)。能不能只用一个C++编译器、不要运行时Parse新语言来搞定这个问题?
直观地看上去,QML语法跟C++好像还有几分像,就选择QML进行借(chao)鉴(xi)吧。 最终的代码放在了 dontpanic92/yz ,代码与文章一同食用味道更佳。
目标
- 代码不应该需要额外程序进行处理
- 不要另做一套语法,然后运行时Parse
- 尽量保留类型
QML示例
一个简单的QML大概长这个样子:
- ApplicationWindow {
- // 属性赋值
- visible: true
- title: "Hello World"
- // 嵌套
- TextArea {
- id: textArea1
- readOnly: true
- }
- // 函数定义
- function makeViewToEntryPoint() {...}
- // 信号绑定
- Component.onCompleted: function() {...}
- }
那么要怎么把C++写成这个样子呢?
思考
DSL
我的第一个想法(居然?)是做个Embedded-DSL。不过C++又不是Ruby……随便搜了一下,发现了一篇文章,也只是利用了重载运算符和运算符优先级,看上去限制比较大。最终还是放弃了这个想法。
嵌套类
从语法方面进行一下对比:QML声明一个对象的格式是类型+大括号,跟C++类声明其实有点类似,直接用类和嵌套类是比较自然的想法。QML中的嵌套层次关系表明的是父子关系——传给内部类一个外部类的this指针就好了。那外层的类如何知道内层定义了几个类、分别叫什么名字?反射看起来可以解决这个问题。
但是最后也放弃了这个想法,主要是考虑到:QML的大括号里面可以进行属性赋值,在类声明里要怎么搞?大概只能在构造函数里面了——不好不好;再就是构造函数估计也要单独在大括号里面占一行。后来也没有继续在这个方案上深入,主要去尝试了另外一个方案:lambda。
嵌套 lambda
lambda跟QML声明的语法也很像啊,就是脑袋大了一点。尾巴还是大括号,多出来的分号跟class的声明又一致,非常可以接受!大括号里面是函数内容,写点什么都行。而且其实脑袋大还是一个挺重要的特点:我们可以把所有的小动作都放在大括号之前,用一个宏都藏起来就好了。其实最开始我是不想用宏的,但最后发现,不用不行啊。
那像上面一样,我们怎么知道一个lambda里面嵌套了几个lambda呢?解决的办法是——靠初始化。我们可以定义一个类,它的构造函数接受一个lambda参数。在这个类的构造函数中,我们就可以做一些“注册”之类的事情了。
对于最外层的lambda,它们是全局变量,在主函数开始之前就“注册”好了;对于内部的lambda,只有在外层lambda执行时它们才会被“注册”。
好吧,嵌套的lambda,就决定是你了!
初始化的实现
lambda赋值的对象
根据目前的想法,我们需要把lambda赋值给一个新对象:
- Something somevar = [&](){...};
那这个Something是个什么东西呢?或者说,我们的lambda实际上定义了个啥?候选答案可以有2:类和对象。我们看QML好像确实是定义了一个对象的样子,但其实我们的lambda定义的是一个“类”,lambda就是这个类的“构造函数”。我们把自己的这个类叫做klass。然后在程序运行的时候,由klass负责构造出对象,并调用“构造函数”(就是这个lambda)。
属性们存在哪?
如果能在lambda里面使用this,那大概是极好的。但是this只存在于类里。对于内部的lambda来说,没办法再给它套上一个class了,那样的话最后就会有};};看起来非常奇怪。那只好从参数下手:我们传给lambda一个参数,里面存着对象的各种属性,这个参数就起名叫做self。在lambda里面,要访问自己的属性就需要加上self了。虽然跟QML差了一些,不过好在还不是什么大问题。比如,我们要定义一个button的话:
- klass<button> somevar = [&](button& self)
- {
- self.x = ;
- ...
- };
- // 某个地方
- class button {
- public:
- property<int> x;
- property<int> y;
- ...
- };
klass也要接受一个类型参数的原因是,somevar的目的是要在运行时创建对象的,具体somevar需要new一个window还是button它得知道呀。所以self的类型还需要传给somevar。
目前klass里面创建对象部分大概就这样:
- object* create() const override
- {
- T* p = new T(); // T就是button
- _constructor(*p); // _constructor就是那个lambda
- return p;
- }
接下来我们就都用button来举例子。
名字?
为了方便运行时的访问,以及后文的“继承”部分的实现,我们需要给每个klass取一个类名。所谓类名其实就是一个字符串,把它传给klass就好,比如:
- klass<...> somevar("mybutton", [&](...){});
……不过这样不行啊!要记得我们只能在大括号之前做手脚,这样做的话最后会多个括号的。所以,我们要换一种方式:
- klass<button> somevar = klass_builder("mybutton") + [&](...){...};
新搞出了一个东西,叫做klass_builder,专门记录klass的参数,以后参数再多也不怕啦。同时我还把创建对象的任务也交给了这个klass_builder,所以klass的模板参数也换掉了:
- klass somevar = klass_builder<button>("mybutton") + [&](...){...};
同时,klass的名字也是生成的对象的id。我们不准备允许在同一个“scope”(就是同一个lambda中)出现两个同样名称的klass,所以这些klass的名字用来充当id再好不过了。
父亲怎么办?
我们在lambda里面需要访问父亲。父亲在哪里?对于内层嵌套的lambda来说,事实上它们所能访问到的self就是它的父亲了。例如,在上面的button里面我们要再定义一个button:
- klass somevar = klass_builder<button>("mybutton") + [&](button& self) {
- // 在定义somevar2时的语境中的self就是somevar2的父亲了
- klass somevar2 = klass_builder<button>("mybutton2") + [&](button& self){};
- };
对于最外层的lambda来说,我们可以提前定义好一个self,它指向一个顶层的object,这样就统一了。
父亲要如何访问?用self.parent的话,如果我们不想丢掉parent的类型,就需要把parent作为模板参数加到button上。或者把parent当做参数传给lambda,然后把parent的类型加到klass_builder的模板参数上。这里选择了后者,就让那个button还是当年那个纯洁的button吧:
- klass somevar = klass_builder<button, remove_reference_t<decltype(self)>>("mybutton", &self) + [&](decltype(self) parent, button& self){...};
注册
是时候讨论一下最开始说的“注册”的事情了。对于最外层的lambda,它们是全局变量,注册时就注册在“最顶层”的klass中,我们用一个变量cls来代表这个“最顶层”的klass;内部嵌套的lambda就注册在外部的klass中,也就是它们的父亲。所以在程序的主函数还没执行的时候,最外层的klass就已经“注册”好了。
因此,对于klass来说,它们是有层次关系的,就像命名空间一样。最外层的klass注册在“最顶端”的类cls中,内部的klass注册在外部的klass中。
什么时候构造这些klass的对象?
主程序一开始,我们就来构造这些对象。我们搞出一个叫app的类,要(qiang)求(po)用户在main函数开始的时候初始化这个app。反正都需要一个东西来负责初始化、消息循环之类的工作,就是这个app了:
- class app
- {
- ...
- };
- int main() {
- app a;
- a.exec(); // 进入消息循环
- }
在app的构造函数中,我们执行对象的初始化工作。上面已经提到,在初始化了一个对象之后,内部的klass们会自动注册到外部的klass中。因此初始化之后,还需要继续对当前klass的内部klass进行初始化,也就是创建完窗体再创建按钮了。
到这里,我们应该已经有一个基本能看框架了。我们可以用一个宏yz_object把lambda大括号之前的部分都包裹起来,需要用户填写的参数就当做宏的参数:
- yz_object(window, main_form)
- {
- self.title = "Main Form";
- yz_object(button, button1)
- {
- self.x = ;
- self.text = "button";
- ...
- };
- };
感觉已经有几分味道了是吧?
“继承”?
在QML中,我们可以基于一个已有的部件构造一个新的自定义部件。如果我们也想要实现这样的功能,就需要添加进继承的功能。其实所谓“继承”,在这里就是把所有基类的“构造函数”(就是它们的那个lambda)都执行一遍。
OK,我们的klass还需要多一个参数,代表基类的名字:
- klass somevar = klass_builder<button, remove_reference_t<decltype(self)>>("button", "mybutton", self) + [&](...){...};
这里"button"就是基类的名字,"mybutton"就是我们这个button的类的名字。此外,我们还需要负责在千里之外把"button"基类定义好。
剩下的事情就交给klass_builder去做了。它会负责一层一层找到"mybutton"的所有祖先(当然在这里就只有一个"button"),依次调用它们的“构造函数”。
对于所有的“基类”来说,我们规定他们的的klass不能生成对象。原因之一在于,对于普通的klass来说,他们的parent是确定的;而对于这些“基类”来说,他们的parent其实只有在真正被“继承”的时候才会确定。我们可以用不同的klass_builder来处理这种区别。比如,基类的klass_builder不接受parent参数,不会创建对象等。
用户自定义属性(变量)怎么办?
如果这些变量只是在lambda内部(及其孩子中)使用,那么函数内部的static变量就可以了,他们会自动被lambda们以引用的形式捕捉。
难办的是:如果想要定义在类外部使用的变量要怎么办?如果不在意类型擦除的问题,用一个map就好了;如果想要保留类型信息,那么就只能在真正的C++类中进行定义,并把它们放在一个头文件中。用宏封装一下,大概如下:
- yz_declare_with_members_begin(button, SpecialButton)
- int test;
- void test_func() { MessageBoxA(, "a", "a", ); }
- yz_declare_with_members_end;
如果各位看官有什么更好的方法,灰常欢迎讨论一下。
Demo
我只做了window、button和timer三个组件,属性封装的也少的可怜(没错,它只是个api wrapper),不过写个小小的演示程序应该还没什么问题。代码也不长,如下:
- #include "yz/ui_begin.hpp"
- // SpecialButton 的定义见上文
- yz_define_with_members(button, SpecialButton)
- {
- self.text = "SpecialButton!";
- self.test = ;
- // Button 里再来一个Button……
- yz_object(button, AnotherButton)
- {
- self.text = "AnotherButton";
- };
- };
- yz_object(window, main_form)
- {
- self.title = "Main Form";
- // yz_property 就是 static
- yz_property auto test = [&]() { printf("aaaa\n"); };
- yz_object(SpecialButton, button1)
- {
- test();
- self.test = ;
- };
- yz_object(button, button2)
- {
- self.x = ;
- self.y = ;
- self.text = "button2";
- // 单击事件
- self.on_click += [&](){
- self.x = self.x + ;
- self.y = self.y + ;
- };
- };
- yz_object(timer, timer1)
- {
- // timer 的属性设计全部参(zhao)考(ban)QML
- self.interval = ;
- self.triggered_on_start = true;
- self.repeat = true;
- yz_property int direction = ;
- // 计时器事件
- self.on_timer += [&](){
- button& button1 = parent["button1"];
- if (button1.x > )
- direction = -;
- if (button1.x < )
- direction = ;
- button1.x = button1.x + * direction;
- };
- };
- };
- #include "yz/ui_end.hpp"
- int main()
- {
- yz::app app;
- yz::window* w = yz::ui["main_form"]; // ui就是所有顶层对象的父亲
- yz::SpecialButton* button = yz::ui["main_form"]["button1"];
- button->test_func();
- w->show();
- app.exec();
- return ;
- }
运行结果就是,首先控制台会输出几个a,然后SpecialButton的test_func被调用弹出一个小框框,接着显示主窗体:
上面的SpecialButton和AnotherButton重叠在一起,一同左右移动;button2点击后会向左下方移动。
后记
目前来看,这套东西还有几个比较明显的不足:
- 刚才提到的用户自定义变量的方法比较丑陋
- 编译时间较长
- 我的VS2013的Intellisense内心在崩溃,小红线不断啊!
其实现在觉得,倒还是做个DSL或者弄个预处理器比较痛快……
写出形似QML的C++代码的更多相关文章
- 写出gradle风格的groovy代码
写出gradle风格的groovy代码 我们先来看一段gradle中的代码: buildscript { repositories { jcenter() } dependencies { class ...
- [label][翻译][JavaScript-Translation]七个步骤让你写出更好的JavaScript代码
7 steps to better JavaScript 原文链接: http://www.creativebloq.com/netmag/7-steps-better-javascript-5141 ...
- 让你用sublime写出最完美的python代码--windows环境
至少很长一段时间内,我个人用的一直是pycharm,也感觉挺好用的,也没啥大毛病 但是pycharm确实有点笨重,啥功能都有,但是有很多可能这辈子我也不会用到,并且pycharm打开的速度确实不敢恭维 ...
- PyTorch最佳实践,怎样才能写出一手风格优美的代码
[摘要] PyTorch是最优秀的深度学习框架之一,它简单优雅,非常适合入门.本文将介绍PyTorch的最佳实践和代码风格都是怎样的. 虽然这是一个非官方的 PyTorch 指南,但本文总结了一年多使 ...
- PAT 1002 写出这个数 (20)(代码)
1002 写出这个数 (20)(20 分) 读入一个自然数n,计算其各位数字之和,用汉语拼音写出和的每一位数字. 输入格式:每个测试输入包含1个测试用例,即给出自然数n的值.这里保证n小于10^100 ...
- 如何写出高质量的JavaScript代码
优秀的Stoyan Stefanov在他的新书中(<Javascript Patterns>)介绍了很多编写高质量代码的技巧,比如避免使用全局变量,使用单一的var关键字,循环式预存长度等 ...
- 如何写出高质量的Python代码--做好优化--改进算法点滴做起
小伙伴你的程序还是停留在糊墙吗?优化代码可以显示程序员的素质欧! 普及一下基础了欧: 一层for简写:y = [1,2,3,4,5,6],[(i*2) for i in y ] 会输出 ...
- 推荐4款个人珍藏的IDEA插件!帮你写出不那么差的代码
@ 目录 Codota:代码智能提示 代码智能补全 代码智能搜索 Alibaba Java Code Guidelines:阿里巴巴 Java 代码规范 手动配置检测规则 使用效果 CheckStyl ...
- 让我们一起写出更有效的CSharp代码吧,少年们!
周末空闲,选读了一下一本很不错的C#语言使用的书,特此记载下便于对项目代码进行重构和优化时查看. Standing On Shoulders of Giants,附上思维导图,其中标记的颜色越深表示在 ...
随机推荐
- C#获取北京时间与设置系统时间
获取北京时间 public static DateTime GetBeijingTime() { DateTime dt; // 返回国际标准时间 // 只使用 timeServers 的 IP 地址 ...
- 自学C++第一天
常引用: 可以用在复制构造函数里面.
- Windows 10和Visual Studio 2015 能给.Net方向的开发从业者带来什么?
.Net 多年前我们选择了你,现在在当前的移动互联网热火朝天的时代,你能给我们什么样的惊喜?面对IOS和android的势头,windows的移动端能否实现三国鼎立? windows 10 号称统一各 ...
- 理解MySQL——索引与优化
转自:理解MySQL——索引与优化 写在前面:索引对查询的速度有着至关重要的影响,理解索引也是进行数据库性能调优的起点.考虑如下情况,假设数据库中一个表有10^6条记录,DBMS的页面大小为4K,并存 ...
- Hql 中 dao 层 以及daoimpl 层的代码,让mvc 模式更直观简洁
1.BaseDao接口: //使用BaseDao<T> 泛型 ,在service中注入的时候,只需要将T换为对应的bean即可 public interface BaseDao<T& ...
- Python 3.5 RuntimeError: can't start new thread
/*********************************************************************** * Python 3.5 RuntimeError: ...
- Dos学习笔记(3)attrib命令
今天和昨天一直在摸索这个命令觉得这个命令为什么改变不了文件夹的属性, 因为我试着用attrib +r /s 去修改子文件夹的时候发现没用,然后如果输入 attrib +r /d 又提示说/d需要和/s ...
- jquery常用选择器和常用方法
基本选择器 $(”#myDiv”) //匹配唯一的具有此id值的元素 $(”div”) //匹配指定名称的所有元素 $(”.myClass”) //匹配具有此class样式值的所有元素 $(”*”) ...
- 在laravel下關於blade模板的嘗試
Blade模板 關於模板繼承和分區段 @section和@yield的實驗 ①關於@section...@show嘗試 測試1 {{--appV2test.blade.php--}} <html ...
- [转]使用Maven添加依赖项时(Add Dependency)时,没有提示项目可用,并且在Console中,输出: Unable to update index for central|http://repo1.maven.org/maven2 。
使用Maven添加依赖项时(Add Dependency)时,没有提示项目可用,并且在Console中,输出: Unable to update index for central|http://re ...