用 LabVIEW 编写 Wizard 类型的应用程序 3 (LabVIEW 8.0)

五、SubPanel

    主VI太过复杂,是肯定会影响它的可读性和可维护性的。所以,对向导类型程序的进一步改进的重点,就是把主VI进一步模块化,不但是程序代码要模块化,界面也必须模块化。代码模块化相对比较简单,多利用子VI就是了。但是界面的模块化,在之前的LabVIEW中是非常困难的,因为 LabVIEW 没办法在运行时,把不同的 VI 的界面拼在一起。是 LabVIEW 7.1 和 8.0 的一些新功能最终解决了这个问题。

    对程序界面模块化,按一般的思路,第一步就是把每个页面划分成一个独立的模块。这似乎又回到了我们前文提到过的第一、二个阶段。但有所不同的是,旧版本 LabVIEW 功能不全,无法很好的管理被分为模块的页面,而新 LabVIEW 改进的对这方面的支持。
    在 LabVIEW 7.1 中出现了一个新的控件 – SubPanel(子面板)。当一个 VI 运行的时候,它的 SubPanel 控件中,可以显示另一个 VI 的前面板。我们可以利用这个新的控件,我们可以使用插件框架式程序架构来编写向导型的程序。图1是这种插件框架式程序结构的示意图。


图1:插件框架式的程序结构

    插件框架式程序的实现思路是,把向导的每个页面都分配到一个独立的VI上去,这个页面上所有的操作,都有这个页面所在的VI完成。图1左上部分的那些 VI 就是为每个页面编写的VI。这些 VI 都被当作插件,在主程序需要的时候被调用显示在主程序上。
    图1右下角的VI是主程序的 VI。它的界面上主要是一个 SubPanel 控件,这个控件用于显示页面VI的界面。主程序在每一步的时候,分别把对应这一步骤页面的VI的界面显示出来,这样就实现的向导功能。主程序的界面上还有一些公共控件,比如“上一步”“下一步”这样的按钮,这些按钮在所有步骤中都需要,所以可以放在主框架上,不需要再在每个页面中重复了。

    这样的插件框架式程序在运行时,主VI和插件VI是在同时运行的。
    主 VI 的运行流程大致如下:创建或注册程序运行时需要的各种事件 -> 初始化程序 -> 等待和处理事件,主要是管理插件。比如在用户按下“下一步”按钮后,主程序负责把当前的插件移出内存,把对应下一页的 VI 调入内存,运行,并显示界面。 -> 最后负责销毁创建的事件,关闭所有资源,退出。
    插件 VI 的主要程序结构和主 VI 一样,采用的是事件处理结构。它在运行起来以后执行的流程也和主 VI 类似: 创建或注册插件运行时需要的各种事件 -> 初始化程序 -> 等待和处理事件,主要是用户在界面上的操作,和一些后台程序,比如数据处理等等。 -> 销毁创建的事件,关闭插件。

    虽然 SubPanel 在 LabVIEW 7.1 中就出现了,但是我当时却并没有在我的程序里采用上述的设计方案。只是因为当时还有一个棘手的问题没有解决。这个问题就是 VI 太多了,不好管理。
    向导页面的多个插件 VI,他们的功能有很多共同之处。在以前,所有页面都在同一个主 VI 中的时候,那些相同的功能可以通过调用同一个子 VI 来完成。但是,把页面分割成独立 VI 之后,很多情况,我都不得不为每个页面做一整套子 VI,他们在每个页面上完成的功能都类似,但却不能使用同一个子 VI。
    以处理事件为例,我写了一套子 VI 处理页面 VI 的事件。但是由于不同的页面可能会同时在运行,每个页面都有自己的事件,如果调用同一套处理事件的子 VI,不同页面之间会相互干扰。
    另外,如果想创建一个新的页面,最方便的方法莫过于把一个已有页面的 VI、子 VI 全部复制一遍,然后在其基础上做改动。LabVIEW 以前是不允许出现同名 VI 的。把一个页面的 VI、子 VI 全部改名,还要保证调用链接不出现混乱,非常的不方便。所以上述的插件框架方案是我等到到 LabVIEW 8.0 出来以后才开始使用的。

六、Project Library

    LabVIEW 8.0 作为一大升级版本,拥有了很多新特性。其中之一是“Project Library(工程库)”。
    工程库是一组功能相关联的VI或其它文件的集合。一个工
程库是把一组功能相
关的VI,和其她文件按一定结构组合封装在一起,以便于代码的管理和发布。工程库的名字也是库中VI的名字空间(name space)。这个名字空间
与C+
+、C#等语言中的名字空间的概念类似。有了它
LabVIEW 就可以在一个程序中使用两个同文件名的 VI,当然,它们的名字空间不能相同,也就是它们存在于不同的工程库中。
    另外,工程库中的 VI 有操作安全设置, 每一个 VI 可以被设置为公有(Public,可以被库外的VI调用);或者私有(Private,只能被库的成员VI调用)。

    工程库给开发插件框架式的程序带来的很大的方便,特别是在 VI 文件的管理方面。
    新的设计思路是这样的:把所有的功能模块都封装在工程库内,比如说每个页面都有一个对应的工程库。专为这个页面使用的所有子 VI 都被加在它的工程库内。并且,不想被其它库使用的的子 VI 都要标记为私有。被这样组织起来的程序,虽然 VI 数量还是很多,但模块划分清楚,不会出现不希望出现的调用关系。安全性,可维护性就大大提高了。
    另外,可扩展性也得到了提高。如果需要添加一个新的页面,只需要把一个已有页面复制一份。复制出来的这一份,只要工程库的名字换个新的就行了。再也不需要一个一个的去改VI的名字了。
    图2是我的一个程序的工程管理窗口:


图2:采用工程库管理程序的VI

    但是,现在的程序结构还是有些令我不太满意的地方-重复的代码太多。不同页面之间,有很多类似的VI。就比如图2中的程序,每个页面都会用到事件处理的一些 VI,他们的代码在每个页面中都是相同的。但是,利用这个工程库组织起来的程序却不能把这些重复的VI提取出来,变成共用的子VI,因为在每个页面里,这些代码相同的VI,处理的数据是不同的。并且这些数据会保留在 VI 的局部或全局变量中,不同的页面如果共用一套子VI,会相互影响,出现数据混乱的。
    直到 LabVIEW 支持了面向对象的编程之后,我们才终于找到了一个完美的解决这一问题的方案。

阮奇桢

《我和 LabVIEW》
 用 LabVIEW 编写 Wizard 类型的应用程序 1 (LabVIEW 6.1 之前)
 用 LabVIEW 编写 Wizard 类型的应用程序 2 (LabVIEW 6.1 ~ 7.1)

广告

人类的后代会是啥样的

    科幻影片中未来世界中的人类,长得什么模样的都有。一般科普类的文章在预测这个问题的时候,就会以用进费退为原则,预测人类脑袋越来越大,四肢越来越短等等。我自己有时候也想,以后的人类会变成啥样?
    最近回顾动物的进化史,对这个问题又有了点新想法。

    生物进化不是一个线性的渐变过程,在动物的进化历史上,大多数时期,物种的种群都是比较稳定的,可能过个几百万年,几千万年都看不出一个物种有太大变化。但是有时候,某一新的功能或特性被进化出来,之后,这一生物界的平衡就会被立即打破。在之后,不长的时间内,生物进化会进入一个跳跃发展的时期,会有一大批物种消失,同时也出现一大批新的物种。
    我总结了一下,比较显著的几次快速进化的时期和他们的诱因如下:
    第一个新物种大量出现的时期,是由多细胞生物引起的。生物出现了超过25亿年了,前20多亿年都是只有单细胞生物。直到5亿年前,才进化出了多细胞生物。多细胞生物相对于之前的单细胞生物优势非常明显,于是它一出现,就立即压制了其它物种的发展,而其自身则毫无制约的繁衍、分化。没过多少年,海洋里就遍布了它的各式各样的子孙,地球的主宰从此变成了多细胞生物。这个时期被称作生物大爆炸时期,仿佛一夜之间,突然所有的生物都出现了。不过那个时候,植物和动物之间的接线还不那么明显。
    第二次大进化是由眼睛引起的。眼睛这东西太有用了,第一个进化出眼睛的动物由于优势明显,最终儿孙满堂。当年的瞎眼动物们,还能在竞争中保持不败,留有后裔到现在的屈指可数。
    再一次大进化应该是由速度引发的,其代表是新物种-鱼类。在鱼类出现之前,所有的动物都是慢吞吞挪动的。速度带来的优势导致脊椎动物的蓬勃发展。在之后的地球上,能够称霸一时的都是脊椎动物。

    下一次大进化时期,也许已经拉开序幕了,我认为它应该就是由智慧所带来的。人类是最初拥有智慧的生物,于是他也拥有了比其它物种大的多的竞争优势。现在,由于人类不断扩张所引起的物种大灭绝已经开始了。以后地球上甚至其他星球上的主要优势物种都会是人类不同类型的子孙,由现在的人类分化而来,现有其它物种根本没资本同人类争多霸权。或许过个几千万年,会出现各种各样的新物种:水里游的、地上爬的、天上飞的、打洞的、上树的、甚至戳在土里不能动的,然而他们都有一个共同的特征,就是拥有智慧,都是人类的后代。
《生活随笔》目录

LabVIEW 程序的内存优化 2 – 子 VI 的优化

1. 子 VI 参数的缓存重用

数据在子 VI 间传入传出,如果程序设计的好,可以做到缓存重用,使得数据在主 VI 和子 VI 中都不发生拷贝,提高程序的效率。

我们先来看一下图1所示的 VI。打开 Tool>>Profile>>Show Buffer Allocations 工具查看一下这个 VI 中内存分配的情况,会发现在代码的加法函数处有一个黑点。这个黑点说明程序在这里有分配了一块内存,这个内存是用来存储加法运算结果的。


图1:控件不与接线器相连时,加法处有内存分配

    为什么加法函数在这里不做缓存重用呢?利用其中一个加数的内存空间来保存计算结果。
当这个 VI 运行的时候,图2中,加数 Numeric 的数据是由 VI 前面板的控件提供的。如果用户不修改控件的值,每次 VI 运行,这个数值应该是保持不变的。如果加法函数在这里做缓存重用,加数或者说它对应的控件中的数据,就会在加法运算执行后被修改。这样程序就会出现逻辑上的错误。
所以把一个这样的控件联在 LabVIEW 的运算节点上,运算节点是不能重用控件的数据内存的。同样的道理,链接一个常量到运算节点上,节点同样不能做缓存重用。在子 VI 中,没有连到接线器上的输入控件就相当与一个常量。

但是,如果我们让 VI 上的控件与 VI 的接线器(Connector Pane)相连,情况就不一样了。入图2所示,把三个控件连到接线器上,程序中加法节点上那个黑点就消失了,不再为运算结果分配新的内存。


图2:控件不与接线器相连时,加法处有内存分配

    这是因为,当输入控件与接线器连接后,LabVIEW 就认为这个输入值应当是由子 VI 的调用者(父 VI)提供的:连到接线器上,逻辑上,这个输入控件就不再是常量,而是一个输入变量了。既然是输入变量,子 VI 不需要记住输入的数据共下次调用时使用,因此可以把新产生的数据放在输入参数所在的内存,做到缓存重用。

你可能在想,这个输入参数的内存不一定可以被修改吧,万一它的数据还要在父 VI 中被其它节点使用呢?
子 VI 是不需要考虑这点的,输入数据的数据被修改肯定是安全的,这一点是由父 VI 来保证的。如果输入数据不能被修改,父 VI 会把传入的数据拷贝一份再传到子 VI 中去。
比如图3中的程序,它所调用的子 VI 就是图2中那个 VI。由于与它的第一个输入参数相连的是一个常量,而常量的值是不能被改变的。所以 LabVIEW 要把这个常量的值复制一份,再传到子 VI 中去,以保证子 VI 中的运算节点可以做缓存重用。


图3:父 VI 中的数据拷贝

    如果图3中的父 VI,他也使用与接线器相连的输入控件为子 VI 提供输入参数,则 LabVIEW 会知道,父 VI 的这个数据是由再上一层 VI 提供的,这里也不需要需要做数据拷贝。这样,这个 VI 就也做到了缓存重用。设计合理,参数在传递多个深度后都不需要开辟新内存的。

    从上面的说明中,还可以发现一个问题。就是,有时候子 VI 的改动,会影响父 VI 的行为,比如是否为传入子 VI 的数据做个拷贝等等。有时候我们发现改动了一个子 VI,它的父 VI 也需要重新保存,就是由这个原因引起的。

2. 输入输出参数的排布

    在子 VI 的程序框图上,不论代码有多复杂,有多少嵌套的结构,控件终端最好按照这样的方式排布:所有输入参数(控制型控件的终端)都放在代码的最左端排成一列;所有的输出参数(显示型控件的终端)都放在代码。比如图4中的代码的风格就比较好。


图4:控件终端整齐的排列在程序框图左右两端

    这首先是为了保证程序有良好的可读性。我们在阅读 LabVIEW 代码的时候总是按照从左到右的顺序,所有的参数都排布在一起,我们就可以以数据线为线索,轻易的找的数据被读写的地方。其次,这种风格的 VI,在效率上也比较优化。

    对于一个输入参数(控制型控件的终端),如果把它放程序代码的最左侧,所有结构的外面,程序在运行这个子VI之前,就可以得到这个参数的确切值了。
但是,如果这个终端是在代码的某个结构中的,在某一结构的内部,那么LabVIEW必须在运行到这一结构内部的时候,才可以去读这个参数的值,否则可能会引起罗技上的错误。比如说,一个控制型控件的终端是在一个循环的内部,开始时它的值是x。在运行到第n次循环之前,这个终端对应的前面板上的控件被人改为一个新的数值y。那么逻辑上,在执行第n次循环之前,每次用到这个参数时,它的值要保持为x,而在第n次循环的时候,又要使用它的新值y。这样的数据所在的内存,LabVIEW 显然是不能将其重用的,否则下次循环再读它的时候,数据就不正确了。
如果这个终端是在所有结构之外,LabVIEW 则可以根据数据线的链接,明确的判断出在某一节点执行完之后,程序再也不需要用到这个参数的值了,那么 LabVIEW 就可以重用它所在的内存,以避免开辟新内存,拷贝数据等操作。这样就提高了程序的内存效率。

    对于一个输出参数(显示型控件的终端),如果它位于某个条件结构的内部,LabVIEW 就要考虑,程序有可能执行不到这个条件。LabVIEW 就会多添加一些代码来处理这种情况,当 VI 没有运行到这个条件时,要给输出参数准备一个默认值。
把这个终端移到所有结构之外,就可以省去这部分 LabVIEW 自动添加上去的工作和,稍微提高一点效率:)

3. 良好的数据流结构可以优化程序内存效率

    先看一个程序:


图5:程序中没有必要的数据线分枝

    图5 的程序只是一个演示,不必追究它到底实现了什么功能。图中的左半部分是主 VI,在这个 VI 中对输入的数组数据Array进行了两次操作:一次使用 subVI“My Search” ;另一次使用了数组排序函数。图5 的右半部分是 subVI“My Search”的程序框图。
需要注意的是,主 VI 上 Sort 1D Array 函数那里有个黑点(这个黑店靠近黄色方块的中心,这里看不太清楚,和图6对比一下,就可以发现了),说明这里做了一次内存分配。这是因为Array的数据被同时传递到了“My Search”和“Sort 1D Array”两个节点进行处理。这两个操作可能会同时进行,LabVIEW 为了安全(两个操作对数据的改动不能相互影响,不能同时对一块内存进行读写),就必须为这两个节点准备两份数据在两份内存中。所以在“My Search”和“Sort 1D Array”两个节点中,如果一个节点用了原来Array的内存,另一个节点就需要拷贝一份数据给自己用。
不过,如果看一下“My Search”的程序框图,它其实没有对Array数据进行任何改动,主VI完全没有比要给“Sort 1D Array”开辟一块新内存。我们只要对程序稍作改动,就可以对此进行优化。图6 是改进后的程序:


图6:符合数据流风格的主VI

    在改进后的程序中,Array 数据首先传入subVI“My Search”,然后又传出来,继续传给“Sort 1D Array”函数。这样子看上去好像数据要多到子VI中转一圈,但实际上,由于子VI中Array输入输出是缓存重用的,实际上相当于只是把数组数据的引用传给了子VI,效率是相当高的。而在主 VI 中,执行“Sort 1D Array”时,LabVIEW 知道输入数据现在是这个节点专用的,改了他也是安全的,于是也可以缓存重用。图六中,“Sort 1D Array”上的那个小黑点就消失了。

    图6 中的主 VI,它的优点首先是符合数据流的风格。一个主要的数据从左到右,流经每个节点。这样的程序非常容易阅读和理解。LabVIEW 也更容易对这样的代码进行优化,所以这样风格的程序通常效率也比较高。
有的时候,利用 LabVIEW 的自动多线程特性,书写并行代码,对程序效率有利。比如,程序中某一部分的代码需要较长时间的计算或者读写时间的情况。但是并不是任何时候并行执行都好。并行书写的程序不易理解,容易出错,多线程运行也会带来额外的开销。像图5、图6中的程序,数据量较大,但是并没有比较耗时的运算操作,或数据读写操作,这样的程序,串行运算比并行效率更高。

https://lv.qizhen.xyz/optimization_memory

LabVIEW 中的泛型容器

https://lv.qizhen.xyz/oop_generic

Google 网站里有个 Google 实验室,有不少 Google 的产品最初就是放在这个实验室里的。现在 NI 也有 NI 实验室了。NI 实验室公布出来的项目一般是 NI 工程师利用额外时间做的一些调查研究。这些项目不是公司的正式产品,但是它们的设计很有创新或者是比较有应用潜力。与其让这些项目被埋没了,不如先看看用户对这些项目的反应,如果相当一部分用户觉得某个项目非常有帮助,或许它就值得我们为其增加投资,把它作成正式产品了。
我这里给大家介绍其中的一个项目:“LabVIEW Generic Container Map”。因为这个项目是我设计的,所以对它了解比较多一些。当时,我们打算提出这个项目的时候,主要有两个目的:第一是帮助用户编写有复杂数据结构的应用程序;第二是推进 LabVIEW 向通用编程语言方向做改进。

C++ 的程序员基本都很喜欢 STL 这个模板库。程序中常会使用数组、队列、字符串等等数据类型和结构,如果自己设计实现这些数据结构和相关的操作,是相当耗费精力的。好在 STL 实现了这些数据结构,和它们常用的操作方法。借用 STL 提供的功能,编程时很多细节方面不需要再去考虑了,这就让工作简化了许多。尝到 STL 甜头的程序员,在编写程序的时候,已经很难离开 STL 了。
STL 中非常重要的一个部分就是容器。容器用于存放数据,程序通过调用容器的结构函数保存数据到容器或者访问容器中的数据。容器也分为不同的类型,如链表、队列等。它们在数据的组织方式上,或存取方式上有所分别,以适用不同的需求。STL 中的容器和方法都是泛型的或者说是数据类型无关的,就是说这些容器可以保存和操作任何类型的数据。
其它一些常用的编程语言,如 Java、C# 也都有类似的泛型容器以方便程序员使用。

LabVIEW 的主要方针是简化工程师们编写程序的难度,以前用 LabVIEW 编写的程序大多是工业领域流程控制类型的。这种类型的程序用不到太复杂的数据结构和算法,因此,LabVIEW 中对我们在计算机课程中学到的那些经典数据结构以及算法的支持并不多。
但是在我自己用 LabVIEW 多了之后,用它比用 C++ 要顺手,任何类型的程序都喜欢使用 LabVIEW 来编写,包括一些通常用途的程序。这时候,LabVIEW 缺乏对基本数据类型支持的缺点就格外突出了。于是我和周围几个同事就想到应该在这些方面对 LabVIEW 做一些补充,做一些比较规范的泛型容器和算法,一方面方便自己,也许还可以提供给别的用户。

由于这不是正式项目,我们能投入的资源很有限,不可能一开始就做得很全面。作为开始,我们选择了 Map 容器和它最常用的几个方法。首先选择 Map 一是因为它比较常用,二是其它容器中,有些在概念上和 LabVIEW 中已有的一些函数比较接近,如果选则他们,可能会引起用户的误解。
LabVIEW 中的 Array 操作与 STL 中的 vector 是非常相似的,功能齐全,不需要考虑底层操作如内存管理等。STL 中的 deque, queue, stack 等,与 LabVIEW 中的“队列”(Queue)操作比较类似。但是 LabVIEW 中的队列存在的目的不是为了作容器,而是用于在多线程程序中通讯。在“生产者/消费者”程序模式中,经常使用队列在不同的线程中传递数据或消息。因为 LabVIEW 中队列操作主要用于不同线程间的通讯,因此它的函数并没有采用 LabVIEW 的主要传参方式-传数据,而是采用了传引用的方式。

我们实现的这个 Map(这个按字面翻译比较别扭,中文可能翻译成“字典”还比较合理)泛型容器功能与 C++ STL 中的 Map 是类似的,它主要用于程序经常需要按某一关键字查询数据的情况。
Map 已经包括了编写查询程序时常用的操作,比如把数据放到容器中、查找一个数据、删除、清空容器等。
我们的 LabVIEW Generic Container Map 内部的数据是按照平衡二叉树的方式组织存储的,它的查询复杂度比一般线性数据结构的要低。这样,在数据量很大的情况下,使用 Map 的程序效率明显高于使用数组的程序。
Map 采用的是符合 LabVIEW 风格的传数据方式,把整个 Map 中的数据在不同函数间传递。

C++ 是支持泛型编程的。简单地说,泛型编程可以这样理解,就是程序员可以实现一个方法,这个方法能够应用在任何合法的数据类型上。比如,前面提到的 STL,它有个“比较”方法,你可以用它来比较整数,也可以比较字符串,或者使用户定义的类的实例等等。
支持泛型编程,程序员就可一抽象出与数据类型无关的算法,从而使代码具有更好的可重用性。
目前,用户还不能在 LabVIEW 上实现泛型编程。使用 Polymorphic VI 可以使一个方法支持某几种特定的数据类型,但不是任何数据类型。真正能做到数据无关的函数,比如说“equal”函数,都是 LabVIEW 自带的,用户无法写出这样一个函数或 VI。
我是非常想推动 LabVIEW 有朝一日也实现泛型编程的。这个 Map 容器也是在 LabVIEW 泛型编程方面做的一次探索。这个 Map 容器不是 LabVIEW 自带的东西,它是完全使用 G 代码实现的。但是它同时也是一个泛型容器,支持对任何数据类型的操作。

下图是使用 Map 编程的一个例子:

缓存重用结构

一、缓存重用

    在《LabVIEW 程序的内存优化》一文中有一个利用移位寄存器来降低 VI 内存的例子。
    下面这个 VI 大约会占用了2.7M的内存空间


图1: 对数组进行数值运算的顺序执行程序

    给它加上一个移位寄存器,如下图所示,内存占用就降低到只有不到400k了。


图4: 利用移位寄存器实现缓存重用

    这其实是利用了移位寄存器两端接线端指向的是同一块内存这一特性,主动的告诉 LabVIEW 这段代码上的每个加法节点的输入输出数据可以使用同一块内存。避免的 LabVIEW 分配不必要的数据缓存。
    但是代码还是不够完美,本来不需要循环,却非得摆上一个只执行一次的循环结构。感觉上总是有些别扭。
    这个问题终于在 LabVIEW 8.5 中被解决了。LabVIEW 8.5 中多出了一个结构——缓存重用结构,专门用于告诉 LabVIEW 在某段代码上为输入输出数据做缓存重用。上面这个程序用新的缓存重用结构来写就是这样的:


图3:利用缓存重用结构实现缓存重用

二、使用缓存重用结构

    缓存重用结构与其它结构不在同一个函数选板上。这是缓存重用结构不是一个功能性、或改变程序流程的结构。它的使用不会改变代码的功能,仅仅会改变代码的效率。
    要使用缓存重用结构,需要打开函数选板的 Programming->Application Control->Memory Control。第一个选项就是他了。


图4:缓存重用结构在函数选板上的位置

    缓存重用结构为了方便使用,并不是简单的作为循环加移位寄存器的替换,它还有一些可选的边框节点,帮助编程者处理不同的数据类型。
    刚刚被拖到程序框图上的是一个光滑的黄色方框,要使用它的缓存重用功能还要为打算从用的内存,根据它的数据类型选择相应的边框节点。在黄色的边框上点击鼠标右键,弹出菜单的最后几项就是可供选择的边框节点类型。如图5所示。
    每种边框节点都是成对出现的,一个在输入端,另一个在输出端。


图5:添加边框节点

三、边框节点

1. 数组元素索引和替换节点

    这对节点用于改变数组中某个元素的值。输入的数组数据连到缓存重用结构左面的数据索引节点上,结构内得到的数据,就是需要处理的元素的数值。

<正在写>

一年的 LabVIEW 写作成绩

我开始决定整理和写作 LabVIEW 的相关知识,恰好是一年之前。今天回头查看了一下自己到底写了多少东西。
杂七杂八的随笔不算,我把《我和LabVIEW》系列的文章都拷贝到了word文档里。总共有大约45篇文章,5万多字。加上图片也有一百多页纸了。
和我一年前的估计差不多。说明我还是比较勤劳的:)