写出形似QML的C++代码

QML示例

一个简单的QML大概长这个样子:

ApplicationWindow {  
    // 属性赋值
    visible: true
    title: "Hello World"

    // 嵌套
    TextArea {
        id: textArea1
        readOnly: true
    }

    // 函数定义
    function makeViewToEntryPoint() {...}

    // 信号绑定
    Component.onCompleted: function() {...}
}
Copy

那么要怎么把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 = [&](){...};  
Copy

那这个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 = 1;
    ...
};

// 某个地方
class button {  
public:  
    property<int> x;
    property<int> y;
    ...
};
Copy

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;
}
Copy

接下来我们就都用button来举例子。

名字?

为了方便运行时的访问,以及后文的“继承”部分的实现,我们需要给每个klass取一个类名。所谓类名其实就是一个字符串,把它传给klass就好,比如:

klass<...> somevar("mybutton", [&](...){});  
Copy

……不过这样不行啊!要记得我们只能在大括号之前做手脚,这样做的话最后会多个括号的。所以,我们要换一种方式:

klass<button> somevar = klass_builder("mybutton") + [&](...){...};  
Copy

新搞出了一个东西,叫做klass_builder,专门记录klass的参数,以后参数再多也不怕啦。同时我还把创建对象的任务也交给了这个klass_builder,所以klass的模板参数也换掉了:

klass somevar = klass_builder<button>("mybutton") + [&](...){...};  
Copy

同时,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){};
};
Copy

对于最外层的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){...};  
Copy

注册

是时候讨论一下最开始说的“注册”的事情了。对于最外层的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(); // 进入消息循环
}
Copy

app的构造函数中,我们执行对象的初始化工作。上面已经提到,在初始化了一个对象之后,内部的klass们会自动注册到外部的klass中。因此初始化之后,还需要继续对当前klass的内部klass进行初始化,也就是创建完窗体再创建按钮了。

到这里,我们应该已经有一个基本能看框架了。我们可以用一个宏yz_object把lambda大括号之前的部分都包裹起来,需要用户填写的参数就当做宏的参数:

yz_object(window, main_form)  
{
    self.title = "Main Form";
    yz_object(button, button1)
    {
        self.x = 1;
        self.text = "button";
        ...
    };
};
Copy

感觉已经有几分味道了是吧?

“继承”?

在QML中,我们可以基于一个已有的部件构造一个新的自定义部件。如果我们也想要实现这样的功能,就需要添加进继承的功能。其实所谓“继承”,在这里就是把所有基类的“构造函数”(就是它们的那个lambda)都执行一遍。

OK,我们的klass还需要多一个参数,代表基类的名字:

klass somevar = klass_builder<button, remove_reference_t<decltype(self)>>("button", "mybutton", self) + [&](...){...};  
Copy

这里”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(0, "a", "a", 0); }  
yz_declare_with_members_end;  
Copy

如果各位看官有什么更好的方法,灰常欢迎讨论一下。

Demo

我只做了window、button和timer三个组件,属性封装的也少的可怜(没错,它只是个api wrapper),不过写个小小的演示程序应该还没什么问题。代码也不长,如下:

#include "yz/ui_begin.hpp"
// SpecialButton 的定义见上文
yz_define_with_members(button, SpecialButton)  
{
    self.text = "SpecialButton!";
    self.test = 1;

    // Button 里再来一个Button……
    yz_object(button, AnotherButton)
    {
        self.text = "AnotherButton";
    };
};

yz_object(window, main_form)  
{
    self.title = "Main Form";
    // yz_property 就是 static
    yz_property int direction = 1;
    yz_property auto test = [&]() { printf("aaaan"); };

    yz_object(SpecialButton, button1)
    {
        test();
        self.test = 100;
    };

    yz_object(button, button2)
    {
        self.x = 200;
        self.y = 100;
        self.text = "button2";

        // 单击事件
        self.on_click += [&](){
            self.x = self.x + 50; 
            self.y = self.y + 50;
        };
    };

    yz_object(timer, timer1)
    {
        // timer 的属性设计全部参(zhao)考(ban)QML
        self.interval = 100;
        self.triggered_on_start = true;
        self.repeat = true;
        yz_property int direction = 1;

        // 计时器事件
        self.on_timer += [&](){
            button& button1 = parent["button1"]; 
            if (button1.x > 400)
                direction = -1;

            if (button1.x < 0)
                direction = 1;

            button1.x = button1.x + 50 * 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 0;
}
Copy

运行结果就是,首先控制台会输出几个a,然后SpecialButtontest_func被调用弹出一个小框框,接着主界面显示:

上面的SpecialButton和AnotherButton重叠在一起,一同左右移动;button2点击后会向左下方移动。

后记

目前来看,这套东西还有几个比较明显的不足:

  • 刚才提到的用户自定义变量的方法比较丑陋
  • 编译时间较长
  • 我的VS2013的Intellisense内心在崩溃,小红线不断啊!

其实现在觉得,倒还是做个DSL或者弄个预处理器比较痛快……

原文链接:http://dontpanic.blog/declarative-cpp-gui/

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏鸿的学习笔记

Python写的Python解释器(六)

目前可以确认Python虚拟机是一个堆栈机器。它通过指令来控制执行顺序,推入和弹出堆栈的值。在上面的例子中,最后一条指令是RETURN_VALUE,它对应于代码...

691
来自专栏北京马哥教育

十分钟完成Bash 脚本进阶!列举Bash经典用法及其案例

前言:在linux中,Bash脚本是很基础的知识,大家可能一听脚本感觉很高大上,像小编当初刚开始学一样,感觉会写脚本的都是大神。虽然复杂的脚本是很烧脑,但是,当...

1483
来自专栏逸鹏说道

C# 温故而知新:Stream篇(五)上

MemoryStream 目录: 1 简单介绍一下MemoryStream 2 MemoryStream和FileStream的区别 3 通过部分源码深入了解下...

4055
来自专栏菜鸟致敬

记一次两小时的js编程学习

1.弱类型语言 2.解释型语言 3.客户端语言 对于有学习Java、C以及Python一类的人来说,最熟悉的莫过于这些都是强类型语言。它们严格的遵守自身的规定,...

862
来自专栏JavaEdge

Netty 源码深度解析(九) - 编码概述1 抽象类 MessageToByteEncoder2 抽象类 MessageToMessageEncoder一个java对象最后是如何转变成字节流,写到s

编码器实现了ChannelOutboundHandler,并将出站数据从 一种格式转换为另一种格式,和我们方才学习的解码器的功能正好相反。Netty 提供...

2761
来自专栏java达人

方法参数过多怎么办

我们在编程或阅读前人的代码时,经常会看到多个参数的方法,有的甚至达到二十个,看得人眼花缭乱,不便于阅读和维护,而且参数很容易混淆,如两个参数类型同为short型...

2858
来自专栏锦小年的博客

python学习笔记7.4-内建模块base64

有时候,我们用noepad++或者记事本打开图片或者程序等文件的时候会显示大量的乱码,主要原因是这些文件编码的时候并不是字符串编码的。如果我们想把这些文件正常显...

2199
来自专栏Java学习网

Java面试题系列之基础部分(六)——每天学5个问题

Java基础部分学习的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io的语法,虚拟机方面的语法,这些都是最基...

2625
来自专栏咸鱼不闲

fastjson 重复引用和循环引用问题

数据传输使用json格式再方便不过了。 fastjson 由阿里巴巴那伙人使用Java语言编写,号称最快的JSON库 前两天遇到一个问题 后台的数据转化为jso...

2784
来自专栏java达人

Java中的堆和栈的区别

当一个人开始学习Java或者其他编程语言的时候,会接触到堆和栈,由于一开始没有明确清晰的说明解释,很多人会产生很多疑问,什么是堆,什么是栈,堆和栈有什么区别?更...

2316

扫码关注云+社区

领取腾讯云代金券