当前位置: 首页 > news >正文

嵌入式GUI开发:LISTVIEW控件从入门到精通,实现高效数据展示与排序

1. 项目概述:为什么嵌入式GUI离不开LISTVIEW控件

在嵌入式系统的人机交互界面开发中,我们经常需要展示结构化的数据,比如设备参数列表、通讯日志、文件目录或者传感器历史记录。面对这种多行多列的数据,一个简单的列表控件(LISTBOX)往往力不从心,因为它只能处理单列文本。这时,LISTVIEW(列表视图)控件就成为了我们的核心武器。它本质上是一个功能增强的表格,不仅能够清晰地展示多列数据,还集成了表头、排序、滚动和选择高亮等高级交互功能。对于资源受限但交互需求明确的嵌入式设备来说,直接绘制表格或使用多个控件拼接的方案在开发效率和内存占用上都不够理想,而像emWin这样的专业嵌入式GUI库提供的LISTVIEW控件,则提供了一个高度优化、功能集成的解决方案。

我接触过不少项目,从简单的工控HMI到复杂的医疗设备显示终端,LISTVIEW的使用频率非常高。很多新手开发者面对官方手册里几十个API函数可能会感到无从下手,或者仅仅停留在创建和显示数据的层面,未能挖掘其排序、动态更新、自定义绘制等高级特性,导致界面交互生硬,用户体验不佳。实际上,只要理解了它的设计哲学和几个关键函数,就能用极少的代码实现非常专业的数据展示界面。本文将基于SEGGER emWin V5.18的官方指南,结合我多年的实战经验,为你彻底拆解LISTVIEW控件,从创建、配置到实现复杂的排序功能,手把手带你掌握这个嵌入式GUI开发中的“瑞士军刀”。

2. LISTVIEW控件核心设计与工作原理拆解

要玩转LISTVIEW,不能只停留在调用API的层面,必须理解其内部的设计逻辑。你可以把它想象成一个由三个核心部分组成的精密仪器:数据模型(Model)视图(View)控制器(Controller),虽然emWin并未严格遵循MVC模式,但其设计思想是相通的。

2.1 数据存储与组织逻辑

LISTVIEW的数据存储方式非常“嵌入式”,它没有采用动态链表或复杂的数据结构,而是使用了一种高效、紧凑的数组式管理。当你调用LISTVIEW_AddRow时,实际上是向一个内部维护的二维字符串数组(或缓冲区)添加了一行数据。每一行的多个单元格(对应多列)文本指针,通过一个GUI_ConstString类型的数组(即const char*数组)一次性传入。这种设计的优势是内存访问效率高,开销可预测,非常适合实时性要求高的嵌入式环境。但这也带来了一个限制:你只能在控件为空(即没有任何行数据)时添加列LISTVIEW_AddColumn),一旦有了数据行,列结构就被“锁定”了。这是因为改变列数会打乱所有已有行数据的存储映射,在无动态内存管理的环境下处理起来非常复杂。理解这一点,就能避免在运行时动态增删列时遇到的陷阱。

2.2 视图渲染与HEADER的共生关系

LISTVIEW的视觉呈现依赖于其内置的HEADER(表头)控件。这个HEADER并非独立创建,而是在LISTVIEW_CreateEx时自动生成并与之绑定。你可以通过LISTVIEW_GetHeader函数获取其句柄,进而单独设置表头的颜色、字体、高度甚至响应其点击事件。表头是触发排序的关键。当排序功能启用后(LISTVIEW_EnableSort),点击某一列表头,LISTVIEW会向父窗口发送通知消息,并根据你为该列设置的比较函数(LISTVIEW_SetCompareFunc)对整个数据行进行重新排列。这种设计实现了视图与交互的分离,非常清晰。

2.3 选择状态与焦点管理

LISTVIEW对选中项的高亮显示逻辑比看起来要细致。它定义了三种核心状态,分别对应不同的背景色和文字颜色,这些颜色都可以通过LISTVIEW_SetBkColorLISTVIEW_SetTextColor进行自定义:

  • 未选中(UNSEL):项目的默认显示状态。
  • 选中但无焦点(SEL):项目被选中,但LISTVIEW控件本身并未获得输入焦点(例如,用户操作了其他控件)。此时通常用灰色背景提示“已选中但非当前操作对象”。
  • 选中且有焦点(SELFOCUS):项目被选中,且LISTVIEW控件拥有输入焦点。这是最显著的提示状态,通常用高对比色(如蓝色背景白色文字)显示。

正确处理这些状态,能让你的界面交互反馈更加专业,符合用户预期。特别是在含有键盘导航的设备上,焦点管理至关重要。

3. 从零到一:创建、配置与基础数据填充实战

理论清晰后,我们进入实战环节。我将通过一个“设备传感器监控列表”的实例,演示创建一个完整LISTVIEW的每一步。假设我们需要显示三列数据:传感器ID、实时数值和状态。

3.1 控件的创建与初始化

创建LISTVIEW主要有两种方式:LISTVIEW_CreateExLISTVIEW_CreateAttached。对于大多数独立窗口内的应用,我推荐使用LISTVIEW_CreateEx,因为它能提供最灵活的控制。

WM_HWIN hListView; GUI_COLOR aBkColor[4], aTextColor[3]; // 1. 创建LISTVIEW控件 hListView = LISTVIEW_CreateEx(10, 50, 300, 200, // x, y, width, height hParent, // 父窗口句柄 WM_CF_SHOW, // 创建后立即显示 0, // 扩展标志,保留 GUI_ID_LISTVIEW0); // 控件ID // 2. (关键步骤)在添加任何数据行之前,必须先定义列! LISTVIEW_AddColumn(hListView, 80, “传感器ID”, GUI_TA_LEFT | GUI_TA_VCENTER); LISTVIEW_AddColumn(hListView, 100, “实时数值”, GUI_TA_HCENTER | GUI_TA_VCENTER); LISTVIEW_AddColumn(hListView, 100, “状态”, GUI_TA_LEFT | GUI_TA_VCENTER);

注意LISTVIEW_AddColumnWidth参数可以设为0。如果设置为0,控件会根据表头文本的长度和默认水平间距自动计算列宽。这在表头文本长度固定且希望自适应时很有用,但我个人更倾向于显式指定宽度,以便精确控制布局。

3.2 填充数据与行操作

列定义好后,就可以添加数据行了。数据需要以指针数组的形式传入。

// 准备第一行数据 const GUI_ConstString aRow0[] = {“SENSOR_01”, “25.6”, “正常”}; // 准备第二行数据 const GUI_ConstString aRow1[] = {“SENSOR_02”, “-”, “离线”}; // 准备第三行数据 const GUI_ConstString aRow2[] = {“SENSOR_03”, “120.3”, “报警”}; // 添加行 LISTVIEW_AddRow(hListView, aRow0); LISTVIEW_AddRow(hListView, aRow1); LISTVIEW_AddRow(hListView, aRow2);

如果需要在中部插入或删除行,可以使用LISTVIEW_InsertRowLISTVIEW_DeleteRow。例如,在第二行后插入一个新行:

const GUI_ConstString aNewRow[] = {“SENSOR_NEW”, “0.0”, “初始化”}; LISTVIEW_InsertRow(hListView, 2, aNewRow); // 索引2表示在第三行前插入(0-based)

3.3 视觉样式深度定制

基础的列表显示出来了,但要让界面美观,必须进行视觉定制。

  • 设置行高与字体:默认行高由字体决定。如果觉得行距太紧凑,可以统一设置。

    // 设置统一行高为30像素 LISTVIEW_SetRowHeight(hListView, 30); // 更换字体 LISTVIEW_SetFont(hListView, &GUI_Font16_1);
  • 自定义颜色方案:这是区分专业与业余界面的关键。

    // 设置背景色:未选中/选中无焦点/选中有焦点/禁用状态 LISTVIEW_SetBkColor(hListView, LISTVIEW_CI_UNSEL, GUI_WHITE); LISTVIEW_SetBkColor(hListView, LISTVIEW_CI_SEL, GUI_GRAY); LISTVIEW_SetBkColor(hListView, LISTVIEW_CI_SELFOCUS, GUI_BLUE); LISTVIEW_SetBkColor(hListView, LISTVIEW_CI_DISABLED, GUI_LIGHTGRAY); // 设置文字颜色 LISTVIEW_SetTextColor(hListView, LISTVIEW_CI_UNSEL, GUI_BLACK); LISTVIEW_SetTextColor(hListView, LISTVIEW_CI_SEL, GUI_WHITE); LISTVIEW_SetTextColor(hListView, LISTVIEW_CI_SELFOCUS, GUI_WHITE);
  • 显示网格线与边框:网格线能让多列数据更易阅读。

    // 显示网格线 LISTVIEW_SetGridVis(hListView, 1); // 设置网格线颜色(默认为GUI_LIGHTGRAY) LISTVIEW_SetDefaultGridColor(GUI_DARKGRAY); // 设置单元格内文字距离左边框的距离,避免文字贴边 LISTVIEW_SetLBorder(hListView, 5);
  • 启用自动滚动条:当数据行或列总宽度超过控件可视区域时,自动添加滚动条是提升用户体验的必备功能。

    // 启用水平和垂直自动滚动条 LISTVIEW_SetAutoScrollV(hListView, 1); LISTVIEW_SetAutoScrollH(hListView, 1);

    这个功能非常实用,你无需手动计算内容尺寸再去创建和管理SCROLLBAR控件,LISTVIEW内部会自动处理。

4. 核心进阶功能:实现灵活的数据排序

LISTVIEW最强大的功能之一就是内置排序。但很多开发者只是照搬例子,一旦遇到非字符串或需要复杂比较逻辑的数据就束手无策。我们来彻底搞懂它。

4.1 排序机制的三要素

要实现排序,必须理解并配置好以下三个要素,它们缺一不可:

  1. 比较函数(Compare Function):告诉LISTVIEW如何比较两行中指定列的数据大小。这是排序的逻辑核心。
  2. 排序启用(Enable Sort):通过LISTVIEW_EnableSort开启控件的排序能力。
  3. 排序触发(Set Sort):通过LISTVIEW_SetSort指定按哪一列排序,以及是升序还是降序。通常这个调用是由用户点击表头触发的。

4.2 使用内置比较函数

emWin提供了两个最常用的内置比较函数:

  • LISTVIEW_CompareText: 用于字符串类型的列,进行标准的C语言字典序比较(strcmp)。
  • LISTVIEW_CompareDec: 用于单元格文本内容是十进制整数的列。它会先将字符串转换为整数再比较。

假设我们的“实时数值”列是整数,需要为其设置排序:

// 为第二列(索引为1,数值列)设置十进制整数比较函数 LISTVIEW_SetCompareFunc(hListView, 1, LISTVIEW_CompareDec); // 启用整个LISTVIEW的排序功能 LISTVIEW_EnableSort(hListView);

现在,当用户点击“实时数值”表头时,我们需要在父窗口的消息回调中处理WM_NOTIFY_PARENT消息,并调用LISTVIEW_SetSort来执行排序。

static void _cbCallback(WM_MESSAGE * pMsg) { switch (pMsg->MsgId) { case WM_NOTIFY_PARENT: { int Id = WM_GetId(pMsg->hWinSrc); // 获取触发消息的控件ID int NCode = pMsg->Data.v; // 获取通知代码 if (Id == GUI_ID_LISTVIEW0) { if (NCode == WM_NOTIFICATION_RELEASED) { // 表头被点击释放 HEADER_Handle hHeader = LISTVIEW_GetHeader(pMsg->hWinSrc); int Column = HEADER_GetSel(hHeader); // 获取被点击的列索引 // 切换排序顺序:首次点击升序,再次点击降序。这里需要自己维护一个状态变量。 static int s_SortReverse = 0; s_SortReverse = !s_SortReverse; // 执行排序 LISTVIEW_SetSort(pMsg->hWinSrc, Column, s_SortReverse); } } } break; // ... 其他消息处理 } }

4.3 实现自定义比较函数(实战精华)

内置函数远不够用。例如,我们的“状态”列包含“正常”、“离线”、“报警”,我们希望按严重程度排序(报警>离线>正常)。或者“传感器ID”列是“SENSOR_XX”格式,我们希望按后面的数字排序。这就需要自定义比较函数。

自定义函数的原型是固定的:int MyCompare(const void * p0, const void * p1);p0p1是指向单元格字符串指针的指针。听起来绕,看例子就懂:

// 自定义函数:按“状态”列的严重程度排序 int _CompareByStatus(const void * p0, const void * p1) { // 1. 转换参数类型。p0实际是 char**,指向当前列两个待比较单元格的字符串地址。 const char ** ppStr0 = (const char **)p0; const char ** ppStr1 = (const char **)p1; // 2. 获取实际的字符串 const char * str0 = *ppStr0; const char * str1 = *ppStr1; // 3. 定义排序权重 int weight0, weight1; weight0 = _GetStatusWeight(str0); weight1 = _GetStatusWeight(str1); // 4. 返回比较结果。注意:返回值语义是 p1 vs p0。 // 若希望升序(权重小的在前),应返回 weight1 - weight0。 // 若希望降序(权重大的在前),应返回 weight0 - weight1。 return weight0 - weight1; // 这里按权重降序排列(报警在前) } // 辅助函数:将状态字符串映射为权重值 static int _GetStatusWeight(const char* status) { if (strcmp(status, “报警”) == 0) return 3; if (strcmp(status, “离线”) == 0) return 2; if (strcmp(status, “正常”) == 0) return 1; return 0; // 未知状态 } // 在初始化时,为“状态”列(索引2)设置自定义比较函数 LISTVIEW_SetCompareFunc(hListView, 2, _CompareByStatus);

关键技巧:自定义比较函数是LISTVIEW排序的灵魂。你可以在这里实现任何复杂的比较逻辑,比如解析日期字符串、比较浮点数、甚至联合多列数据进行综合排序(但这需要更复杂的数据结构支持)。务必注意内存安全和效率,因为排序过程中此函数会被频繁调用。

5. 高级技巧与避坑指南

掌握了基础创建和排序,你已经能解决80%的问题。剩下的20%则决定了界面的精致度和稳定性。以下是我在实际项目中总结的宝贵经验。

5.1 高效更新与性能优化

嵌入式设备资源紧张,频繁刷新整个列表可能导致界面卡顿。

  • 批量更新:如果需要修改多行多列数据,尽量避免在单次循环中连续调用LISTVIEW_SetItemText。更好的做法是,先准备好所有数据,然后调用WM_DisableWindow暂时禁用控件重绘,更新完成后再调用WM_EnableWindow并手动触发重绘(WM_InvalidateWindow)。这能有效减少绘图次数。
    WM_DisableWindow(hListView); for(int i=0; i<row_count; i++) { LISTVIEW_SetItemText(hListView, col, i, new_data[i]); } WM_EnableWindow(hListView); WM_InvalidateWindow(hListView);
  • 禁用非必要功能:如果列表数据量很大且不需要排序,在初始化完成后调用LISTVIEW_DisableSort(hListView)可以略微减少内部开销。

5.2 处理选中项与获取数据

获取用户选中的数据是核心交互。这里有一个巨大的坑:排序状态下的行索引。

  • LISTVIEW_GetSel(): 返回的是**当前显示视图(排序后)**中的选中行索引。
  • LISTVIEW_GetSelUnsorted(): 返回的是**原始数据(排序前)**中的选中行索引。

99%的情况下,你应该使用LISTVIEW_GetSelUnsorted()因为当你需要根据选中行索引去操作原始数据源(比如删除数据库中的对应记录)时,必须使用原始索引。LISTVIEW_GetSel()仅用于界面交互反馈。

int selectedIndex = LISTVIEW_GetSelUnsorted(hListView); if (selectedIndex >= 0) { char buffer[50]; // 获取原始数据中该行的第一列文本 LISTVIEW_GetItemText(hListView, 0, selectedIndex, buffer, sizeof(buffer)); // 现在可以对 buffer 中的 “SENSOR_XX” 进行后续处理了 }

设置选中项时同理,应使用LISTVIEW_SetSelUnsorted

5.3 为行附加用户数据(UserData)

每个列表行除了显示的文本,还可以关联一个32位的用户数据(U32类型)。这个功能极其有用,可以存储行的“身份证”,比如数据库中的主键ID、指向更多数据的指针(需谨慎转换)或任何状态标志。

// 假设每行数据对应一个设备句柄或ID U32 deviceIdArray[] = {0x1001, 0x1002, 0x1003}; for (int i = 0; i < 3; i++) { LISTVIEW_SetUserDataRow(hListView, i, deviceIdArray[i]); } // 当某行被选中时,可以快速获取其关联的ID int selectedRawIndex = LISTVIEW_GetSelUnsorted(hListView); if (selectedRawIndex >= 0) { U32 associatedId = LISTVIEW_GetUserDataRow(hListView, selectedRawIndex); // 使用 associatedId 进行后续逻辑处理 }

这避免了通过字符串去反向查找原始数据的低效操作。

5.4 常见问题排查速查表

问题现象可能原因解决方案
添加列失败,返回错误或无效在已有数据行(LISTVIEW_AddRow)之后调用LISTVIEW_AddColumn必须在添加任何行之前定义所有列。如果需动态改列,只能先删除所有行,再删列/加列,最后重新添加行数据。
点击表头排序无效或程序异常1. 未调用LISTVIEW_EnableSort
2. 未给目标列设置比较函数LISTVIEW_SetCompareFunc
3. 自定义比较函数实现有误,如指针解引用错误。
1. 确认已启用排序。
2. 确认已为需要排序的列设置了正确的比较函数。
3. 在自定义比较函数中增加调试输出,检查p0p1指向的字符串内容。
获取到的选中行索引不对,操作了错误数据在排序后使用了LISTVIEW_GetSel()返回的索引去操作原始数据数组。始终使用LISTVIEW_GetSelUnsorted()LISTVIEW_SetSelUnsorted()来关联原始数据。
列表显示区域出现空白或错位1. 列宽总和超过控件宽度,且未启用水平滚动。
2. 行高设置过小,与字体不匹配。
3. 单元格文本过长未换行,覆盖了相邻列。
1. 启用LISTVIEW_SetAutoScrollH或重新调整列宽。
2. 使用LISTVIEW_SetRowHeight设置合适的行高。
3. 考虑使用LISTVIEW_SetLBorder增加边距,或截断过长的文本。
滚动条不出现或行为异常1. 未启用自动滚动条功能。
2. 控件创建时高度/宽度计算错误,未留出滚动条空间。
3. 在动态增删数据后未触发重绘。
1. 确认调用了LISTVIEW_SetAutoScrollV/H(, 1)
2. 确保控件尺寸足够容纳数据行/列的基本显示。
3. 在数据更新后调用WM_InvalidateWindow
自定义绘制(如单元格背景色)无效调用LISTVIEW_SetItemBkColorLISTVIEW_SetItemTextColor的时机不对,可能在设置后被全局颜色覆盖。确保在设置完全局颜色(LISTVIEW_SetBkColor之后,再调用针对单元格的设置函数。单元格设置会覆盖全局设置。

5.5 一个综合实例:带排序和状态显示的监控列表

最后,我将上述所有知识点融合,给出一个更贴近真实项目的代码框架:

// 1. 定义数据结构 typedef struct { char id[20]; float value; char status[10]; U32 alarmCode; // 用户数据,用于存储报警代码 } SensorData_t; SensorData_t g_sensors[] = { /* 初始化数据 */ }; int g_sensorCount = ...; // 2. 创建并初始化LISTVIEW WM_HWIN CreateSensorListView(WM_HWIN hParent) { WM_HWIN hLV = LISTVIEW_CreateEx(...); LISTVIEW_AddColumn(hLV, ...); // ID, 数值, 状态 LISTVIEW_EnableSort(hLV); LISTVIEW_SetCompareFunc(hLV, 0, _CompareSensorId); // 自定义ID比较 LISTVIEW_SetCompareFunc(hLV, 1, _CompareFloatAsText); // 自定义浮点数比较 LISTVIEW_SetCompareFunc(hLV, 2, _CompareByStatus); LISTVIEW_SetAutoScrollV(hLV, 1); // 3. 填充数据并关联UserData for (int i = 0; i < g_sensorCount; i++) { char valStr[20]; sprintf(valStr, “%.2f”, g_sensors[i].value); const GUI_ConstString row[] = {g_sensors[i].id, valStr, g_sensors[i].status}; LISTVIEW_AddRow(hLV, row); LISTVIEW_SetUserDataRow(hLV, i, g_sensors[i].alarmCode); // 根据状态设置单元格颜色 if (strcmp(g_sensors[i].status, “报警”) == 0) { LISTVIEW_SetItemTextColor(hLV, 2, i, LISTVIEW_CI_UNSEL, GUI_RED); } } return hLV; } // 4. 在回调函数中处理排序和选择 static void _cbParentWindow(WM_MESSAGE *pMsg) { switch (pMsg->MsgId) { case WM_NOTIFY_PARENT: if (WM_GetId(pMsg->hWinSrc) == ID_LISTVIEW) { if (pMsg->Data.v == WM_NOTIFICATION_RELEASED) { // 处理表头点击排序 HEADER_Handle hHeader = LISTVIEW_GetHeader(pMsg->hWinSrc); int col = HEADER_GetSel(hHeader); // ... 切换排序顺序并执行 LISTVIEW_SetSort } if (pMsg->Data.v == WM_NOTIFICATION_SEL_CHANGED) { // 处理选中项变化 int sel = LISTVIEW_GetSelUnsorted(pMsg->hWinSrc); if (sel >= 0) { U32 alarmCode = LISTVIEW_GetUserDataRow(pMsg->hWinSrc, sel); // 根据 alarmCode 更新其他UI或执行操作 } } } break; } }

LISTVIEW控件的深度和灵活性远超一篇文档所能涵盖,但其核心脉络在于理解其“数据-视图-交互”的分离设计。从清晰的列定义开始,到高效的数据填充,再到利用比较函数和用户数据实现强大的排序与关联逻辑,每一步都体现了嵌入式GUI开发中对效率和可控性的追求。在实际项目中,多花时间设计好数据模型与LISTVIEW的交互方式,往往能节省后期大量的调试和重构时间。当你能够熟练运用LISTVIEW_SetCompareFunc实现任意复杂度的排序,并利用UserData优雅地关联业务数据时,这个控件就将从展示工具进化为你交互逻辑的核心枢纽。

http://www.jsqmd.com/news/1050930/

相关文章:

  • 嵌入式GUI开发利器:emWin MESSAGEBOX组件与GUIBuilder工具实战解析
  • 合肥职教高考升学率高的学校是哪个?推荐合肥理工学校! - 教育为先
  • emWin嵌入式GUI库入门指南:从项目结构到Hello World实战
  • 2026年东莞胶粘制品、EVA泡棉、保护膜、双面胶厂家名单:工业胶粘模切产品选购参考指南 - 海棠依旧大
  • 黄山学院对自主创业的学生有什么扶持政策?有没有创业补贴和孵化基地? - 寻茫精选
  • 合肥理工学校招生电话?2026年6月21号最新公布! - 教育为先
  • 大数据转大模型:把关键流程跑顺
  • 合肥口碑最好的中专是哪个?推荐合肥理工学校! - 教育为先
  • 2026青岛全屋定制推荐榜:5家值得信赖的选购指南 - 官方资讯
  • 8GB显存跑35B大模型:Qwen-A3B轻量化部署实战
  • SPT-AKI存档编辑器终极指南:如何快速解放你的塔科夫单机体验
  • 人文领域知识图谱实战包:支持实体检索、关系可视化与自然语言问答的完整Django+Neo4j系统
  • 2026 风电塔筒扰流条厂家推荐榜出炉|扰流条| 风电行业扰流条耐候性与牢固度双重达标实力口碑精选 —— 上海钟田橡塑制品有限公司 - 资讯速览
  • NLP文本标注:质量提升与工程实践指南
  • 基于双流网络的时序动作识别:从原理到击掌计数实战
  • 淮南师范学院入学后可以转专业到王牌专业吗?转专业的条件和难度大不大? - 寻茫精选
  • 2026年度AI搜索优化源头厂商全景评测:国内GEO市场避坑与选型指南 - 品牌报告
  • 【Netty源码解读和权威指南】第38篇:Netty SSL TLS安全传输——HTTPS背后的Netty实现
  • 安徽省职教高考升学率高的学校选哪家?优质升学名校推荐合肥理工学校 - 教育为先
  • 2026杭州GEO优化公司深度横评:源头技术赋能,企业避坑选型全指南 - 品牌报告
  • 淮南师范学院王牌专业在全国 / 省内排名第几?行业认可度高吗? - 寻茫精选
  • 3.4.5 索引的设计原则
  • 上海大宅定制装修品牌推荐:2026六大品牌按需匹配指南 - 资讯速览
  • 合肥高科经济技工学校怎么报名?报名地址、咨询电话、线上预报名渠道一览 - 教育为先
  • 2026青岛公认口碑好的全屋定制品牌门店选购指南 - 官方资讯
  • 夜间野生动物YOLO分割数据集:17000张红外多干扰场景图像
  • 怎么查看电动餐桌厂商的真实案例、如何挑选合适的电动餐桌公司做为合适的长期供应商 - 岳灵峰电动餐桌
  • 嵌入式GUI开发:emWin 2D图形库核心功能与性能优化实战
  • 基于Appium的微信小程序自动化测试实战指南
  • 告别限速!九大网盘直链解析下载神器完整指南