在新人课题中,我负责研究控制系统中多语言支持与单位转换的实现方法,经过一年的实践,现总结开发思路与经验如下:
首先进行需求分析:
嵌入式设备的多语言切换与单位转换功能主要面向不同地区用户的语言需求。考虑到机械设备在车间长期使用的特性,该功能实际使用频率较低。因此,设计时需要重点优化成本控制、内存占用和维护难度等关键指标。
从以下五个方面着手优化:
- 实现按需加载机制,避免系统启动时一次性加载全部文件资源;
- 在Gui.cpp主程序中完善函数初始化封装,增强代码可调试性;
- 优化语言切换机制:针对控件特性制定差异化的显示刷新策略,在保证低频切换的前提下提升高频翻译显示效率;
- 简化词库维护流程:考虑单机系统的特性,设计直观的词库编辑功能,便于后续修改和更新;
- 优化数据结构:采用哈希表存储方案替代传统线性查找,显著提升查询效率。
具体实现如下:
1.词库制作
我们开发了两种机型,支持24种语言和3种分类。以中文、英文和越南语为例,首先为每个单词分配唯一的Id
号作为索引。
为何不采用中文索引?原因在于HMI控制界面中,SubMenu菜单和普通文本可能出现中文名称相同的情况,而对应的英文则存在缩写与全称的区别。中文索引难以确保唯一性。
;像这种,根据不同控件分配不同的id号
;SUB
SUB00000 Home 首页 Trang
SUB00011 Back 返回 Quay lại
该步骤需统计系统所有字段,耗时最长。团队可协作完成,或开发工具辅助提取,目前暂未实施。
2.文件加载
1.加载文件后提取所需语言字段
# 构造结构体放置 id+所需语言字段
#define MAX_KEY_LENGTH 16
#define MAX_VALUE_LENGTH 256
# 取单个键值对
typedef struct lang_pair {
char_t key_ID[MAX_KEY_LENGTH];
char_t value_Lang[MAX_VALUE_LENGTH];
} lang_pair_t;
# 取整个所需词语和id
#define COUNTL 3000
typedef struct {
lang_pair_t pairs[COUNTL];
int32_t size;
} lang_db_t;
最初我在结构体中定义了所有语言种类的字段(如
char_t chinese[MAX_VALUE_LENGTH]
和char_t english[MAX_VALUE_LENGTH]
等二十多个),但这显著影响了开机速度。后来我优化为使用单一字段进行灵活取值。
文件加载的主逻辑依据接收到的语言变量确定
int32_t lang_ID = return_language_number();//获取当前语言类别
# lang_ID =0,1,2 代表英语 中文 越南语
用户通过下拉框(combobox)选择语言后,系统获取对应的lang_ID
值,并自动加载相应语言字段到数据表中。
#define LANG_MAX 3 #语言种类
static lang_db_t g_langs[LANG_MAX]; # 上面创建的存放字段和id的位置
static database_t lang_db[LANG_MAX];# 哈希数据库
# 文件加载主函数
int32_t lang_db_set(int32_t lang_ID){
lang_db[lang_ID] = database_create(COUNTL);//创建数据库
g_langs[lang_ID].size = 0;
# 文件路径加载 path用的全局变量
FILE *fptr = fopen(path, "r");;
if (fptr == NULL) {
//qDebug() << "Error opening file:" << path;
return -3;
}
char_t cfg[1024];
while (fgets(cfg, sizeof(cfg), fptr) != NULL) {
# 遇到注释行,跳过
if ((cfg[0] == ';') || (cfg[0] == '#')) {
continue;
}
# 文件行数大于数据库空间时,提示警告
if (g_langs[lang_ID].size >= COUNTL) {
fclose(fptr);
qWarning("LANG overflow: file 行数 > COUNTL (%d)", COUNTL);
return -2;
}
//分割数据
char* token = strtok(cfg," ");
if(!token){
continue;
}
lang_pair_t* p = &g_langs[lang_ID].pairs[g_langs[lang_ID].size++];
strcpy(p->key_ID,trim(token));
for(int i=0; i<=lang_ID; i++){
token =strtok(NULL," ");
if(!token){
break;
}
if(i == lang_ID){
strcpy(p->value_Lang,trim(token));
hash_t hash = Hash(p->key_ID,strlen(p->key_ID));
database_add_item2(lang_db[lang_ID],hash,p->value_Lang);
}
}
}
fclose(fptr);
return 0;
}
有以下两个关键点需要说明:
-
当前代码只设置了
database_create
却缺少database_delete
功能,随着切换次数的增加,内存占用会持续上升。这是因为在实际设备使用场景中,除了测试环境外,基本不会出现频繁切换的情况。 -
更核心的问题是:即使尝试在切换语言后执行
database_delete
操作,由于界面控件的刷新机制,系统仍会访问已删除的数据库。由于我无权修改系统底层架构,因此保留了当前方案。
trim()
函数用于清除字符串中的隐藏字符。在嵌入式开发中,跨平台解析文件读取的字符串时经常会出现问题。另外附上两个常用的字符串调试函数:
//去除字符串的隐藏字符
char* trim(char* str){
char *end;
//去除前导空白字符
while(isspace((unsigned char)*str)) str++;
if(*str == 0){ //空字符串
return str;
}
//去除尾部的空白字符
end = str + strlen(str) - 1;
while(end > str && isspace((unsigned char)*end)) end--;
//添加新的字符串终止符
*(end+1) = '\0';
return str;
}
//调试测定字符串的ASCII码
void printf_string(const char* str){
int length = strlen(str);
qDebug()<<"String length:"<<length;
for(int i=0;i<length;i++){
qDebug()<<"Character: '%c' =="<<str[i];
qDebug()<<"ASCII: %d\n =="<<(unsigned char)str[i];
}
}
// 移除字符串中的BOM字节序标记
char* remove_bom(char* str){
unsigned char bom[] = {0xEF, 0XBB, 0XBF};
if(strncmp((char*)str, (char*)bom, 3) == 0){
//跳过前三个字符
return str + 3;
}
return str;
}
根据传入的ID值,从哈希数据库中获取对应的翻译字段。
//获取翻译值
const char* lang_db_get_translation(const char* key){
hash_t hashget = Hash(key,strlen(key));
const char_t* path_get;
path_get = (char_t *)database_get_item2(lang_db[now_lang_ID], hashget);
if(path_get != NULL){
return path_get;
}else {
return key;
}
}
3.数据库初始化和调用接口
初始化主要涉及设置开机自启动功能,同时添加运行状态标志位,以便灵活控制功能开启与关闭。
static char_t path[64];
static int32_t now_lang_ID = 0;
int32_t lang_activate = 0;
//数据库初始化
void lang_db_init(const char_t* langFilePath){
strcpy(path,langFilePath);//完善文件加载路径
int32_t lang_ID = return_language_number();//获取当前语言类别
lang_db_set(lang_ID);//加载文件
now_lang_ID = lang_ID;
lang_activate = 999;//总功能运行与否标志位
}
在Gui.cpp处初始化:
lang_db_init("./externsion/lang_db.txt");
最初仅设计了一个通用接口,由各控件直接调用。但后续发现这种集中式管理方式存在诸多问题:工程配置时需逐个检查控件,维护成本极高。为此,我们调整了架构方案,将其拆分为两部分:首先在主逻辑文件中实现lang_apply
核心功能。
const char* lang_apply(char* text, char* trans){
int32_t trans_value = return_language_number();
if(trans_value != now_lang_ID){
lang_db_set(trans_value);
now_lang_ID = trans_value;
}
const char* trans_text = lang_db_get_translation(text);
strcpy(trans,trans_text);
return trans;
}
再创建独立的cpp文件来集中管理各类控件调用
// ctrlMutliLang.cpp
extern int32_t lang_activate;
//SubMenu控件调用接口
void lang_submenu(char_t* id, char_t* exText, char_t* trans_text, char_t* text){
if(!lang_activate) return;
lang_apply(id, trans_text);
if(strcmp(id,trans_text) == 0){
return ;
}else{
strcpy(text,exText);
strcat(text,trans_text);
}
return ;
}
//Text控件调用接口
void lang_text(char_t* id, char_t* trans_text, char_t* text){
if(!lang_activate) return;
lang_apply(id, trans_text);
if(strcmp(id, trans_text)==0){
return ;
}else{
strcpy(text,trans_text);
}
return ;
}
在不影响正常运行和其他功能的前提下,该设计确保系统在功能未启动或词库查询失败时仍能保持稳定,避免对团队开发进度造成干扰。
单位转换采用相同原理,以下仅展示核心处理代码片段:
//获取单位转换类型 0:压力 1:位置/速度 2:重量 3:温度 4:面积
int32_t get_units_type(char* para){
if(!para){
return -1;
}
hash_t hashget = Hash(para,strlen(para));
char_t* path_get;
path_get = (char_t *)database_get_item2(units_db,hashget);
if(path_get != NULL){
int32_t path_value = -3;
if(strcmp(path_get,"bar")==0) path_value = 0;
if(strcmp(path_get,"mm")==0) path_value = 1;
if(strcmp(path_get,"ton")==0) path_value = 2;
if(strcmp(path_get,"\xE2\x84\x83")==0) path_value = 3;//温度
if(strcmp(path_get,"mm\xC2\xB2")==0) path_value = 4;//mm
return path_value;
}else {
return -2;
}
}
//单位转换接口
fp32_t units_apply(char* para, fp32_t pdata, bool reverse){
if(unit_activate != 999) return pdata;
//获取单位转换类型 0:压力 1:位置/速度 2:重量 3:温度 4:面积
int32_t type = get_units_type(para);
if(type < 0){
return pdata;
}
//正转换:值不变,改变类型
switch (type) {
case 0: //压力
pdata = reverse ? (pdata / 0.0689) : (pdata * 0.0689);
break;
case 1: //位置
pdata = reverse ? (pdata / 25.4) : (pdata * 25.4);
break;
case 2: //重量
pdata = reverse ? (pdata / 0.907) : (pdata * 0.907);
break;
case 3: //温度
pdata = reverse ? ((pdata * 9 / 5) + 32.0) : ((pdata - 32.0) * 5 / 9);
break;
case 4: //面积
pdata = reverse ? (pdata / 25.4 / 25.4) : (pdata * 25.4 * 25.4);
break;
}
return pdata;
}
最后添加一个针对TEXT控件的单位文本切换功能,确保数值和单位的显示效果协调统一。
//单位文本切换
const char* get_units_ID(char* text){
if(strcmp(text,"mm")==0){
strcpy(text,"inch");
}
if(strcmp(text,"mm\xC2\xB2")==0){ //mm
strcpy(text,"inch\xC2\xB2");
}
if(strcmp(text,"bar")==0){
strcpy(text,"psi");
}
if(strcmp(text,"\xE2\x84\x83")==0){ //摄氏度
strcpy(text,"\xE2\x84\x89");//华氏
}
return text;
}
放张效果图