1. 为什么给 LabVIEW 程序专门做虚拟键盘:从触摸屏工况说起
很多人第一次听到“LabVIEW 虚拟键盘”时,第一反应是:LabVIEW 前面板本身不是有字符串输入控件和数值输入控件吗,直接用鼠标点进去打字不就行了?这个想法在普通桌面电脑上完全没问题,但只要你把程序部署到工业触摸屏、一体化工控机或者某些需要用触控笔操作的便携设备上,问题马上就来了——物理键盘不存在,屏幕上弹出的是 Windows 自带的软键盘,又小又难点,还经常挡住你的操作区域。
我做这套 LabVIEW 虚拟键盘程序的初衷,就是给一套设备参数设定界面用的。那个项目要求操作员在现场不碰鼠标键盘,全靠触摸屏完成所有参数录入。数字量比如温度、压力、转速这些,需要一种干净利落的数字输入方式;文字量比如设备名称、操作员备注、工艺配方编号,又需要一套完整的字母输入方式。两种输入场景如果混用一个键盘,切换起来很别扭,布局上也很臃肿。所以我把它们拆成了两个独立的虚拟键盘:一个专门输数字,一个专门输字符串,再配一个示例程序把调用方式讲清楚。
数字键盘和字符串键盘在 LabVIEW 里的实现难点并不一样。数字键盘的难点在于“输入拼接”和“退格删除”的逻辑设计;字符串键盘的难点在于 26 个字母加功能键的布局管理、大小写切换。无论哪种键盘,核心都是“点击屏幕上的按钮,把对应字符送到显示框里”,这件事思路理顺了,剩下的就是重复劳动。
这篇文章面向两类读者:一类是刚接触 LabVIEW 但想做触摸屏界面的人,可以直接照搬示例程序的思路;另一类是已经在做设备软件、想优化人机交互体验的工程师,我后面会分享一些实测中踩过的坑和选型理由。
2. 数字键盘的实现核心:按钮矩阵、事件结构与输入拼接
2.1 数字键盘的按钮布局与功能键规划
数字键盘我参考了计算器和手机拨号盘的布局。主体是 3 列 4 行的数字区,放 1 到 9、0、小数点,再在最上面加一排功能按钮:退格、清空、确定、取消。这样一共 16 个按钮,分布在一个固定大小的前面板上,触摸操作时每个按钮的尺寸大约 40 像素 × 40 像素以上,手指头点起来不会误触。
这个设计有一个很容易被忽略的点:数字键盘上的“确定”和“取消”按钮,尽量不要做成和数字键一样的样式。我在项目里把它们做成了不同颜色的按钮,数字键是浅灰色底、黑色字,确定键是绿色底、白色字,取消键是红色底、白色字。触摸屏使用环境下,操作员经常不看旁边的提示文字,而是靠颜色快速定位功能键,这个细节能显著减少误操作。
布局做完之后,每个按钮的“机械动作”(Mechanical Action)要设置为“Latch When Released”,也就是按下并松开时值发生变化。这样一次点击只触发一次事件,不会出现按下去触发一次、松手又触发一次的双重响应。
2.2 事件结构里的按键处理逻辑
数字键盘的前面板做完后,后台逻辑用事件结构(Event Structure)驱动。每个数字按钮的“值改变”(Value Change)事件作为独立分支,分支里做的事情本质上只有一句话:把该按钮对应的字符追加到一个显示字符串里。
但这里有一个代码冗余问题:如果给 16 个按钮分别写事件分支,每个分支就是一行连接操作,代码结构确实简单,但后续要改键盘布局或者增加按键时,工作量会成倍增加。更推荐的做法是把数字键做成一个布尔按钮数组,用一个事件分支统一处理。
具体做法是这样:在前面板上放置一个 4 列 4 行的“按钮数组”控件,给每个元素设置标签文本(Label),标签就写该键对应的字符。然后在程序框图里放一个字符串常量数组,元素顺序和按钮数组的行列顺序一致,内容分别是"1"、"2"、"3"、"4"、"5"、"6"、"7"、"8"、"9"、"0"、"."以及功能键对应的特殊标记。事件结构里只写一个分支,处理这个按钮数组的“值改变”事件,用事件的索引输出(Index)拿到被点击按钮的行列号,再去字符串常量数组里查表取出对应字符。
数组化之后的逻辑看起来绕了一层,但实际写起来很轻松。你在事件数据节点里能看到一个“索引”端子,它是一个数组,第一个元素是行号,第二个元素是列号。把这个索引直接接到“索引数组”函数的索引端子上,从字符常量数组中取出目标字符。这样 10 个数字键加小数点共享一段逻辑,代码量从十几行压缩到几行。
2.3 输入拼接、退格删除与合理的数据上限
取到字符之后,要把它追加到显示框中。最直接的做法是读当前显示控件的值字符串,用“连接字符串”(Concatenate Strings)把旧值和新字符拼起来,再写回显示控件。这个“读-拼-写”循环在 LabVIEW 里没有任何延迟感,触摸屏上操作是很流畅的。
退格键的逻辑要稍微想一下。删除最后一个字符,可以用“字符串长度”(String Length)函数获得当前长度,然后用“删除部分字符串”(Delete From String)函数,设置偏移量为长度减 1、长度为 1,把最后一个字符截掉,剩下的写回显示控件。注意长度减 1 后如果变成负数,说明当前字符串已经为空,这时再做删除操作会触发错误。所以执行删除之前必须加一个“大于 0”的条件判断,字符串为空时直接忽略退格操作。
清空键就简单了,直接把显示控件写入空字符串即可。
还有一个值得提前考虑的设计:输入字符上限。工程中的设备参数一般不会超过 6 到 8 个字符,比如“250.0”或者“-18.5”。如果不加限制,操作员在触摸屏上乱点小数点,能点出一长串毫无意义的内容。我在数字键盘里限定最多 12 个字符,超出后忽略后续输入,同时把按钮颜色变灰提示操作员已经输入到上限了。这个限制实现起来很简单,在拼接字符之前先判断当前长度是否小于上限,不满足就直接放弃这一次拼接。
2.4 负数和小数点的特殊处理
数字键盘如果不支持负数,现场设置温度参数时会很痛苦,因为温度经常是零下。我的做法是在最上面一排功能键里加了一个负号键“-”,布局上放在清空键旁边。这个负号键比较特殊,它不需要出现在输入中间位置,正常数字输入场景里它只可能出现在开头。所以我在拼接负号时做了一个校验:只有当前字符串长度为 0 时才允许输入负号,否则直接忽略。
小数点的处理也有讲究:不允许出现两个小数点。每次点小数点之前,用“搜索替换字符串”(Search and Replace String)函数在当前字符串里查找“.”,如果已经存在就不再追加。这两条规则加起来,数字键盘的输入行为就和真实计算器非常接近了。
3. 字符串键盘的设计:QWERTY 布局、字符映射数组与大小写切换
3.1 为什么字符串键盘不适合逐键写事件分支
字符串键盘如果照搬数字键盘的逐键事件分支写法,26 个字母加退格、空格、大小写切换、清空、确定、取消,少说 32 个按钮,程序框图会变成一张密密麻麻的蜘蛛网。所以字符串键盘从设计第一天就必须走数组化路线。
按键布局上,我采用的是三排主字母区加一排功能键区。第一排是 QWERTYUIOP,第二排是 ASDFGHJKL,第三排是 ZXCVBNM,第四排放退格、空格、大小写切换、清空、确定和取消。这样布局符合大多数人使用电脑键盘形成的肌肉记忆,在触摸屏上找字母的速度会快很多。
这些按钮仍然用一个按钮数组控件承载,行数 4 行,列数按每排按钮数量不同而变化。这里有个 LabVIEW 的实现细节:按钮数组的列数并不要求每行相等,你可以自由调整前面板数组控件里每一行的元素个数,不会影响数组索引逻辑。也就是说,第一行放 10 个键,第二行放 9 个键,第三行放 7 个键,第四行放 6 个键,完全没有问题。
3.2 字符映射表与事件分支的写法
字符串键盘的事件分支比数字键盘要多一层考虑:按钮数组的标签文本不等于最终要拼接的字符,因为大小写切换会改变字母的实际输出。所以字符映射表要设计成“大小写两套”的结构。
我在程序框图中放置了一个二维字符串常量数组,第一行是小写字母表"qwertyuiopasdfghjklzxcvbnm",第二行是大写字母表"QWERTYUIOPASDFGHJKLZXCVBNM"。另设一个布尔变量作为大小写状态位,当事件结构响应按钮数组点击时,先根据当前大小写状态选中映射表的对应行,再结合按钮行列索引取出字符。
这个写法在程序框图里看起来是:索引数组函数从二维字符数组中取元素,输入给索引数组的两个索引端一维一行、一维一列。行索引来自大小写状态位的布尔转数值,列索引来自事件数据节点里的按钮列号。代码很短,但含义清晰。
第三排字母键(ZXCVBNM)下面的功能键区里,有些按钮不能走字符映射逻辑。处理方式是在字符映射表里给这些按钮对应的位置填上特殊标记,比如退格填“<”、空格填“_”、大小写切换填“^”、清空填“C”、确定填“OK”、取消填“X”。事件分支里取到字符后先判断它是不是这些特殊标记,是的话走各自的功能逻辑,否则才走“追加到字符串”的通用逻辑。
3.3 大小写切换的状态保持问题
大小写切换是我实际调试时花了最多时间的地方。它有两个细节:一是切换状态要借助移位寄存器(Shift Register)或者功能全局变量来保持,因为事件结构的分支结束后,局部变量会丢失状态;二是切换后按键上的文字显示也要跟着变,否则操作员不知道当前是在大写状态还是小写状态。
我的做法是在字符串键盘这个子 VI 的程序框图上放一个 While 循环,循环内部放事件结构,大小写状态位放在 While 循环的移位寄存器上。每次点击大小写切换键,移位寄存器里的布尔值取反,同时遍历按钮数组的所有元素,把它们的“标签文本”(Label Text)属性全部改为对应的大小写字母。这个遍历可以写在一个 For 循环里,循环次数等于按钮总数量,属性节点选择按钮数组的“标签文本”属性,按索引设置新的字符。
这个“切大小写同时改按钮文字”的细节,决定了这个键盘好不好用。如果只切换输出字符、不切换按钮显示,操作员完全不知道当前处于什么状态,点出来的字母和自己看到的不一致,体验会非常差。
3.4 字符串过滤与允许字符集限制
字符串键盘不是所有输入场景都需要 26 个字母全放开。比如输入工艺配方编号时,可能只需要字母和数字;输入操作员备注时,又需要允许空格和常用符号。我给字符串键盘加了一个“允许字符集”输入端子,接线端可以传入一个字符串,比如“ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-”,键盘在拼接字符前先检查当前字符是否在允许集合里,不在就忽略。默认不接线时,放开全部字符限制。
这个输入端子让我在后续项目里复用同一个字符串键盘时省了大量改代码的时间。
4. 子VI化设计:虚拟键盘如何与主程序之间传递数据
4.1 模态弹出与调用方式
虚拟键盘做成子 VI(SubVI)之后,主程序调用它就像调用一个普通函数。但这里有个关键:虚拟键盘子 VI 必须设置成“调用时显示前面板”和“调用时运行”,并且在关闭前保持前面板置顶,形成模态效果。
LabVIEW 里实现模态弹窗的常用方法,是在程序框图最开始调用前面板“运行方法”(FP.Run)里的“设为模态”(Set Modal)方法,这样虚拟键盘弹出后,主程序的界面暂时不可操作,操作员必须完成输入并点击确定或取消后,才能回到主界面。这个交互对设备参数设置场景非常重要,能避免操作员忘记关闭弹窗就去点主界面按钮,导致参数状态不一致。
不过我实测下来,Set Modal 这个方法在某些 LabVIEW 版本里需要在前台运行环境的上下文里调用,否则会报错。让子 VI 变成模态的方式还有一种不那么“官方”但更稳的替代方案:把虚拟键盘子 VI 放在主 VI 的同一个 While 循环里,通过一个“键盘可见”布尔变量控制前面板控件的可见属性。这个方案的好处是不会受模态方法版本兼容性影响,坏处是代码耦合度高一些。我最后的示例子程序用的还是 Set Modal 方式,毕竟干净,示例程序里会给你写上调用方法。
4.2 返回值设计:字符串从键盘到主程序的通道
虚拟键盘子 VI 需要对外暴露两个端口:一个字符串输出端口,返回当前输入的完整内容;一个布尔确认端口,告诉主程序操作员是点了确定还是取消。
这里有个 LabVIEW 子 VI 的细节:子 VI 的接线端(Connector Pane)默认是按照控件位置自动分配端子的。你要手动编辑连接板,把显示字符串控件的“值”输出端接到右下角的输出端子,把确定按钮的“值”输出端接到旁边的布尔输出端子。连接板如果不手动编辑,主程序里拖入子 VI 时往往会发现端子对不上。
模态交互的流程在主程序和子 VI 之间是这样走的:主程序调用子 VI,子 VI 前面板弹出,操作员在键盘上输入内容,点击确定或取消,子 VI 执行完毕,把字符串值和布尔值返回给主程序。所以子 VI 本身不依赖任何全局变量,数据传递干净,也方便被多个主程序复用。
4.3 用返回值还是用全局变量:我的选择依据
有些 LabVIEW 虚拟键盘示例程序喜欢用全局变量或功能全局变量来存储键盘输入值,主程序轮询读取。这种方式的优点是可以实时显示键盘输入内容,比如操作员打字时,主界面上的文本框同步更新。缺点也很明显:全局变量让程序的状态流动变得隐晦,一旦虚拟键盘和主程序的调用关系复杂了,排查问题时很难追踪数据到底被谁改了。
我的建议是优先用返回值。实时同步这个需求,其实可以通过子 VI 内部的事件结构把输入值持续更新到子 VI 前面板的显示控件上,操作员在子 VI 界面里就能看到自己输入了什么,不需要主程序实时参与。等点确定后一次性把最终值传回主程序,逻辑简单、行为可控。示例程序里我用的就是这种方式,全局变量方案只提一嘴,不建议直接用。
5. 示例程序的搭建步骤:主界面、键盘调用与整体验证
5.1 示例程序的主界面设计
示例程序的主界面我设计成了一个典型的设备参数设置面板。前面板上放两个主要控件:一个数值显示框,用于显示“目标温度”,旁边一个“修改温度”按钮,点击后弹出数字键盘;一个字符串输入显示框,用于显示“设备备注”,旁边一个“编辑备注”按钮,点击后弹出字符串键盘。界面最下方再放一个纯文本指示框,显示最近一次键盘回传的内容,方便观察调用是否成功。
这样设计的用意是:一个实际可操作的场景比单纯的控件展示更容易让读者理解虚拟键盘的调用方式。你拿到示例程序后,直接运行主 VI,点两个按钮,就能看到两个键盘分别弹出来,输入完成后数值和文字都回到主界面。
5.2 主 VI 程序框图的调用逻辑
主 VI 程序框图的核心是两个“调用子 VI”节点。第一个节点调用数字键盘子 VI,双击拖动输入端子传入一个初始字符串,输出端子把返回的字符串接到一个数值转换函数上,转换成双精度浮点数后写入数值显示框。这里有一个 LabVIEW 初学者容易犯的错误:数字键盘返回的是字符串,不能直接给数值显示控件赋值,需要经过“小数/字符串转换”(Fract/Exp String to Number)函数,否则程序类型检查直接报错。
第二个节点调用字符串键盘子 VI,输出字符串直接接到字符串显示控件的值接线端上。如果操作员点了取消,布尔确认端口返回假值,这时主程序应该丢弃返回值,保持原值不变。我用一个条件结构包裹了赋值逻辑,只有布尔为真时才更新控件值。
主 VI 内部还要做好一个细节:调用子 VI 之前,要把子 VI 的图标从“未编译”状态更新一下,否则初次运行会弹出“子 VI 需要编译”的提示,这在现场触摸屏上很掉价。做法是在项目浏览器里右键点击虚拟键盘子 VI,选择“编译”,再保存整个项目。
5.3 运行实测:整套流程的效果与耗时
我在一台触摸屏一体机上跑了完整流程。点击“修改温度”,数字键盘弹出耗时大约 0.2 秒,操作员依次点 8、5、小数点、5,显示框里实时出现“85.5”,点确定后键盘关闭,主界面的目标温度立刻变成 85.5。点击“编辑备注”,字符串键盘弹出,操作员输入“生产线A-02”,期间试了大小写切换键,按钮文字跟着切到大写,输入 A 和字母结尾的 2 都正常,点确定后备注框更新。
整个流程没有出现卡顿或事件丢失。连续快速点击按钮时,偶尔会出现一次按钮点击没有被识别的情况,这个我一开始以为是事件结构丢事件,后面排查下来才发现是按钮数组的机械动作设置问题,下面一节详细说。
6. 实测踩坑记录:按钮数组的机械动作、事件锁死与扩展建议
6.1 按钮数组元素的机械动作必须统一设置为 Latch
第一个坑就是刚才提到的快速连点时丢事件。问题的根源在按钮数组元素的机械动作属性。LabVIEW 的按钮默认机械动作是“Switch When Pressed”,这种模式下按下时状态改变,如果操作员按下的速度非常快,在程序还在处理上一个事件时,下一次按钮事件可能被合并掉。改用“Latch When Released”可以保证每次松开只产生一次状态变化,并且状态自动复原,不影响下一次点击。
需要特别注意的是,在按钮数组控件上设置机械动作时,要选中所有元素统一设置,不能只设置第一个元素。否则数组中不同位置的按钮可能行为不一致,表现症状就是某些按键能正常触发、某些按键偶尔失灵。我排查这个问题时花了不少时间,最后用操作者模式逐个查看属性才发现数组中还有两个按钮是默认机械动作。
6.2 事件结构里的超时分支与键盘关闭时机
虚拟键盘子 VI 的 While 循环里如果只放事件结构,不设置超时分支,那么当操作员长时间不点击任何按钮时,事件结构会一直空闲等待,这本身没问题。但如果你在子 VI 前面板上增加了“点击外部区域关闭键盘”的需求,就必须用超时分支定期检测点击位置。
我实际遇到的问题是:虚拟键盘弹出后,操作员点了主界面的空白区域,期望键盘关闭,但键盘没反应,因为事件结构根本不响应对前面板外部的点击。最简单的解决方法是设置事件结构超时时间为 200 毫秒,超时分支里检测一个“鼠标按下”事件或者检测前面板是否失去焦点,一旦满足就结束 While 循环、按取消流程返回。这个改进在触摸屏上特别好用,操作员手指点到键盘外面就认为是取消操作,学习成本几乎为零。
6.3 多个键盘实例共存时的资源占用
示例程序只有两个键盘,功能上完全够用。但如果你在一个大项目里同时放了数字键盘、字符串键盘,甚至再做一个小键盘专门输时间日期,就要考虑内存占用和前面板资源的加载问题。
LabVIEW 子 VI 默认是“预加载前面板”还是“按需加载”,取决于你调用时的配置。按需加载可以降低内存占用,但弹窗时第一次打开会稍慢一点。我的选择是:主 VI 启动时预加载这两个虚拟键盘子 VI,换取运行时弹窗响应速度。如果你对内存非常敏感,可以改成按需加载,实测耗时会从 0.2 秒变成 0.4 秒左右,对大多数设备场景来说完全可以接受。
6.4 虚拟键盘的扩展玩法:数字千分位、多语言字符与多功能键
最后分享几个我后来在别的项目里给虚拟键盘做的扩展。数字键盘增加千分位分隔符显示,比如输入 1234567,显示框实时显示 1,234,567,内部存储值仍然是纯数字字符串。实现方式是在显示控件里做“反向字符串处理”,每三位插一个逗号,但不影响最终数值转换。这个功能在做金额或大数量显示时非常实用。
字符串键盘可以扩展多语言字符集,比如带调号的字母,在字符映射表里增加第三行、第四行,用长按一个键弹出候选字符的实现也可以做,就是在按钮上同时放置一个“字符候选列表”的属性节点,点击时弹出下拉框。不过这个功能要看触摸屏设备是否支持下拉选择,有些老旧触摸屏控件对下拉框支持并不好。
更多时候,虚拟键盘的核心价值不在于按键多花哨,而在于“输入过程可控”。你可以随时在后面加上声音反馈、按键高亮、输入历史记录,这些功能基于本文的数组化事件结构都可以很自然地扩展,不需要推翻现有设计。
7. 一些小经验:把虚拟键盘从“能用”做到“好用”
整个虚拟键盘从写第一版到后面不断打磨,我最大的感受是:按键布局和基础逻辑只决定程序能不能跑通,真正决定操作员愿不愿意用的,是那些细节。比如按钮尺寸,在触摸屏上最好不小于 38 像素,太小了手指尖容易同时碰到两个键;比如按下去时按钮颜色变化要明显,让操作员有明确的“我被按到了”的反馈;比如键盘弹出位置,最好贴近主界面上对应的输入框,减少操作员视线来回移动的距离。
还有一点和 LabVIEW 程序本身无关,但和“好用的虚拟键盘”强相关:在你的触摸屏操作系统中,给虚拟键盘程序关闭系统自带的触摸键盘和物理键盘弹窗功能,否则每次操作员点输入框,屏幕上可能同时弹出两个键盘,看起来非常混乱。另外建议关掉屏幕上的鼠标指针显示,这样操作员会自然地把屏幕当成真正的触控设备,而不是用鼠标点按钮,整体的使用体验会好很多。
如果你打算把这套虚拟键盘用在正式设备项目上,建议自己把示例程序跑一遍,改一改按键颜色、按钮间距、最大输入长度这些参数,让它适配自己的屏幕尺寸和输入习惯。LabVIEW 这类工具就是这样,代码结构搭对了,后续调整成本是很低的。