最开始想出的标题是《Declarative C++ GUI库》,但太标题党了。只写了两行代码,连Demo都算不上,怎么能叫库呢……后来想换掉“库”这个字,但始终找不到合适词来替换。最后还是起了个low一点的名字,贱名好养活啊!

这篇文章的目的是介绍如何用C++写出带有Declarative风格的代码。有一些GUI库需要额外的预处理过程(比如qt),还有一些也支持XML格式的GUI声明,但需要运行时Parse那个XML(比如wxWidgets)。能不能只用一个C++编译器、不要运行时Parse新语言来搞定这个问题?

直观地看上去,QML语法跟C++好像还有几分像,就选择QML进行借(chao)鉴(xi)吧。 最终的代码放在了 dontpanic92/yz ,代码与文章一同食用味道更佳。

目标

  1. 代码不应该需要额外程序进行处理
  2. 不要另做一套语法,然后运行时Parse
  3. 尽量保留类型

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++代码的更多相关文章

  1. 写出gradle风格的groovy代码

    写出gradle风格的groovy代码 我们先来看一段gradle中的代码: buildscript { repositories { jcenter() } dependencies { class ...

  2. [label][翻译][JavaScript-Translation]七个步骤让你写出更好的JavaScript代码

    7 steps to better JavaScript 原文链接: http://www.creativebloq.com/netmag/7-steps-better-javascript-5141 ...

  3. 让你用sublime写出最完美的python代码--windows环境

    至少很长一段时间内,我个人用的一直是pycharm,也感觉挺好用的,也没啥大毛病 但是pycharm确实有点笨重,啥功能都有,但是有很多可能这辈子我也不会用到,并且pycharm打开的速度确实不敢恭维 ...

  4. PyTorch最佳实践,怎样才能写出一手风格优美的代码

    [摘要] PyTorch是最优秀的深度学习框架之一,它简单优雅,非常适合入门.本文将介绍PyTorch的最佳实践和代码风格都是怎样的. 虽然这是一个非官方的 PyTorch 指南,但本文总结了一年多使 ...

  5. PAT 1002 写出这个数 (20)(代码)

    1002 写出这个数 (20)(20 分) 读入一个自然数n,计算其各位数字之和,用汉语拼音写出和的每一位数字. 输入格式:每个测试输入包含1个测试用例,即给出自然数n的值.这里保证n小于10^100 ...

  6. 如何写出高质量的JavaScript代码

    优秀的Stoyan Stefanov在他的新书中(<Javascript Patterns>)介绍了很多编写高质量代码的技巧,比如避免使用全局变量,使用单一的var关键字,循环式预存长度等 ...

  7. 如何写出高质量的Python代码--做好优化--改进算法点滴做起

    小伙伴你的程序还是停留在糊墙吗?优化代码可以显示程序员的素质欧! 普及一下基础了欧: 一层for简写:y = [1,2,3,4,5,6],[(i*2) for i in y ]       会输出  ...

  8. 推荐4款个人珍藏的IDEA插件!帮你写出不那么差的代码

    @ 目录 Codota:代码智能提示 代码智能补全 代码智能搜索 Alibaba Java Code Guidelines:阿里巴巴 Java 代码规范 手动配置检测规则 使用效果 CheckStyl ...

  9. 让我们一起写出更有效的CSharp代码吧,少年们!

    周末空闲,选读了一下一本很不错的C#语言使用的书,特此记载下便于对项目代码进行重构和优化时查看. Standing On Shoulders of Giants,附上思维导图,其中标记的颜色越深表示在 ...

随机推荐

  1. 解决Tomcat7“At least one JAR was scanned for TLDs yet contained no TLDs”问题

    解决Tomcat7“At least one JAR was scanned for TLDs yet contained no TLDs”问题 2013-12-05 21:58:00|  分类: t ...

  2. C#查找以某个字母开头另一字母结尾的字符串

    using System; using System.Text.RegularExpressions; namespace ConsoleApplication1 { class Program { ...

  3. 7、provider: SQL 网络接口, error: 26 - 定位指定的服务器/实例时出错

    在建立与服务器的连接时出错.在连接到 SQL Server 2005 时,在默认的设置下 SQL Server 不允许进行远程连接可能会导致此失败.(provider: SQL 网络接口, error ...

  4. Bool 类型变量的使用

    定义一个bool类型的变量,默认为FALSE的 private bool BHaveBeenTip=false; private void label5_Click(object sender, Ev ...

  5. SQL server2000更改数据库名称

    如果是SQL Server 2005可以直接右键重命名,但是SQL Server 2000中不能直接改,可以用sp_renamedb. 1.方法一(物理法): 把Old数据库改为New数据库 打开“企 ...

  6. 数据存储之Cookie和Web Storage。

    Cookie Cookie,有时也用其复数形式Cookies,指某些网站为了辨别用户身份.进行session跟踪而储存在用户本地终端上的数据(通常经过加密).接下来就谈谈cookie的一些利弊,coo ...

  7. akka实现的actor

    定义一个 Actor 类 要定义自己的Actor类,需要继承 Actor 并实现receive 方法. receive 方法需要定义一系列 case 语句(类型为 PartialFunction[An ...

  8. HSV与RGB颜色空间的转换

    一.本质上,H的取值范围:0~360   S的取值范围:0~1    V的取值范围:0~255                                     但是,当图像为32F型的时候,各 ...

  9. PHP 下option selected 无效

    针对于PHP 下的selected="selected" 赋值无效 <select autocomplete="off" ></select& ...

  10. junit单元测试(keeps the bar green to keeps the code clean)

    error是程序错误,failure是测试错误. junit概要: JUnit是由 Erich Gamma (设计模式的创始人)和 Kent Beck (敏捷开发的创始人之一)编写的一个回归测试框架( ...