1. 项目概述:为什么嵌入式GUI开发绕不开LISTVIEW控件?
在嵌入式系统的人机交互界面开发中,数据展示是一个高频且核心的需求。无论是工业设备的参数监控表、医疗仪器的历史记录查询,还是消费电子产品的文件管理器,我们常常需要将结构化的数据清晰、直观地呈现给用户。这时候,一个功能强大的列表视图控件就显得至关重要。在众多嵌入式GUI解决方案中,SEGGER的emWin图形库以其高效、稳定和丰富的控件集著称,而其中的 LISTVIEW控件 ,正是处理这类表格化数据展示任务的“瑞士军刀”。
简单来说,你可以把LISTVIEW理解为一个功能增强版的表格。它不仅仅能显示多行多列的文本,更内置了表头、选择高亮、滚动浏览、以及我们今天要重点探讨的 排序功能 。与简单的列表控件不同,LISTVIEW的每一列都可以拥有独立的标题、宽度和对齐方式,其底层通过集成一个HEADER控件来管理这些列属性,这使得它在呈现复杂数据时结构更加清晰。对于资源受限的嵌入式设备而言,直接使用基础绘图函数来绘制一个带交互的表格是繁琐且低效的,而LISTVIEW控件将这些通用逻辑封装起来,提供了完整的API,让开发者能专注于业务逻辑,极大提升了开发效率和界面的专业度。
本文将从一个有经验的嵌入式GUI开发者的视角,手把手带你深入emWin的LISTVIEW控件。我不会仅仅罗列API手册,而是结合我实际项目中的使用经验,重点剖析如何从零创建一个LISTVIEW、如何进行各项关键配置,并攻克其最具价值的特性——多列数据排序。我们会以官方示例
WIDGET_SortedListview.c
为蓝本,但会注入大量手册上不会写的实操细节和避坑指南。无论你是刚刚接触emWin,还是希望更深入地掌握LISTVIEW的高级用法,这篇文章都将为你提供可直接复现的实践路径。
2. LISTVIEW控件核心设计与工作机制拆解
在动手写代码之前,理解LISTVIEW的内部设计和工作机制至关重要。这能帮助你在遇到问题时,更快地定位根源,而不是盲目地尝试各种API。
2.1 控件构成:不只是“表格”
一个LISTVIEW控件可以看作是由几个逻辑部分组合而成的复合控件:
- 基础窗口 :作为容器,负责控件的区域管理、消息处理和绘制调度。
-
HEADER部件
:这是LISTVIEW的灵魂之一。它自动位于控件顶部,负责显示列标题。用户点击表头的行为(如触发排序)正是通过HEADER来捕获和传递的。你可以通过
LISTVIEW_GetHeader()获取其句柄,进而自定义表头的颜色、字体等属性。 - 内容区域 :用于绘制所有的数据行和列。它负责处理单元格的文本绘制、背景色渲染、网格线显示以及选中状态的高亮。
-
滚动条(可选)
:当内容超出显示区域时,可以通过
LISTVIEW_SetAutoScrollV/H()自动或手动添加滚动条。
这种设计带来了一个关键特性:
LISTVIEW的数据模型是“按行组织”的
。当你使用
LISTVIEW_AddRow()
添加一行数据时,需要传入一个字符串数组(
GUI_ConstString* ppText
),数组中的每个元素对应一列的内容。这意味着,你必须先通过
LISTVIEW_AddColumn()
定义好所有列的结构(宽度、标题、对齐方式),然后才能添加行数据。顺序不能颠倒,这是新手常踩的第一个坑。
2.2 排序机制深度解析
排序是LISTVIEW区别于简单列表的核心功能。其实现机制精巧且高效,理解它对于实现自定义排序逻辑至关重要。
排序的触发流程 :
-
启用排序
:首先,你需要调用
LISTVIEW_EnableSort(hObj)来为整个控件开启排序功能。默认情况下,排序是关闭的。 -
设置比较函数
:这是排序的逻辑核心。你需要为
希望支持排序的每一列
单独设置一个比较函数,通过
LISTVIEW_SetCompareFunc(hObj, Column, fpCompare)实现。fpCompare是一个函数指针,指向你定义的比较函数。如果不为某列设置比较函数,点击该列表头将不会触发排序。 -
用户交互与响应
:当用户点击某一列的标题时,LISTVIEW控件内部会执行以下操作:
- 获取为该列设置的比较函数。
- 使用这个函数,对当前所有行的数据进行 重新排序 。
-
刷新显示,并更新选中项(因为行索引发生了变化,需要用
LISTVIEW_GetSelUnsorted来获取正确的数据索引)。
比较函数的设计
:
比较函数的原型是固定的:
int CompareFunc(const void *p0, const void *p1)
。
-
p0和p1:这两个指针指向的是 待比较的两个单元格的文本字符串地址 。注意,它们是指向字符串的指针,即const char**类型。这是理解如何编写比较函数的关键。 -
返回值约定
:
-
如果
p0对应的值 小于p1,返回 负值 (例如 -1)。 -
如果
p0对应的值 等于p1,返回 0 。 -
如果
p0对应的值 大于p1,返回 正值 (例如 1)。 -
重要
:这个“大小”关系决定了升序排列的顺序。返回负值会使
p0排在p1前面。
-
如果
emWin内置了两个常用的比较函数:
-
LISTVIEW_CompareText: 用于字符串的字典序比较。 -
LISTVIEW_CompareDec: 用于将单元格文本解析为十进制整数后进行数值比较。
对于更复杂的数据类型(如浮点数、日期时间),你需要编写自己的比较函数。例如,比较“2023-01-01”和“2022-12-31”这样的日期字符串,就需要在函数内部解析字符串并转换成可比较的数值。
2.3 选中状态与焦点管理
LISTVIEW的选中项高亮颜色会随着控件是否获得输入焦点而改变,这提供了良好的视觉反馈。其颜色由三个独立的配置控制:
-
LISTVIEW_CI_UNSEL: 未选中项的背景/文字色。 -
LISTVIEW_CI_SEL: 已选中项但控件 无焦点 时的背景/文字色。 -
LISTVIEW_CI_SELFOCUS: 已选中项且控件 有焦点 时的背景/文字色。
在嵌入式界面中,多个控件可能共享焦点。清晰地区分
LISTVIEW_CI_SEL
和
LISTVIEW_CI_SELFOCUS
(例如,无焦点时用灰色背景,有焦点时用蓝色背景),可以极大地提升用户体验,让用户明确知道当前键盘或触摸操作的对象是哪个控件。
3. 从零构建LISTVIEW:创建、配置与数据填充实战
理论说得再多,不如一行代码。让我们开始动手,创建一个具备完整功能的LISTVIEW。我将以创建一个显示“文件名、大小、修改日期”三列信息的简单文件列表为例。
3.1 创建控件:选择正确的API
emWin提供了多个创建函数,最推荐使用的是
LISTVIEW_CreateEx()
,因为它参数清晰,功能完整。
WM_HWIN hListView;
int x0 = 10, y0 = 50; // 起始坐标
int width = 300, height = 200; // 控件宽高
hListView = LISTVIEW_CreateEx(x0, y0, width, height, hParent, WM_CF_SHOW, 0, GUI_ID_LISTVIEW0);
if (hListView == 0) {
// 创建失败处理,通常是内存不足
_Error("LISTVIEW creation failed!");
}
参数解析与避坑 :
-
hParent: 父窗口句柄。设为0则创建为桌面窗口(顶级窗口)。在大多数有对话框或框架窗口的应用中,应传入父窗口句柄,以便进行消息传递和坐标管理。 -
WinFlags: 通常设为WM_CF_SHOW,让控件创建后立即可见。你也可以组合其他标志,如WM_CF_MEMDEV用于内存设备绘制(防闪烁)。 -
ExFlags: 保留位,设为0。 -
Id: 控件ID。在窗口回调函数中,可以通过此ID来识别是哪个控件发送的消息。务必为其分配一个唯一的ID。
实操心得 :在资源紧张的嵌入式设备上,创建控件后检查句柄是否为0是一个好习惯。如果创建失败,往往意味着没有足够的内存分配窗口对象或设备上下文。
3.2 定义列结构:构建表格的骨架
创建控件后,第一步是定义列。 必须在添加任何行数据之前完成 。
// 假设hListView已成功创建
LISTVIEW_AddColumn(hListView, 150, "文件名", GUI_TA_LEFT | GUI_TA_VCENTER);
LISTVIEW_AddColumn(hListView, 80, "大小(KB)", GUI_TA_RIGHT | GUI_TA_VCENTER);
LISTVIEW_AddColumn(hListView, 120, "修改日期", GUI_TA_LEFT | GUI_TA_VCENTER);
关键点解析 :
-
宽度
:第二个参数是像素宽度。你可以指定一个固定值,也可以传
0。传0时,控件会根据列标题文本的宽度和默认间距自动计算一个初始宽度,但这通常不精确,建议手动指定。 -
对齐方式
:
GUI_TA_LEFT | GUI_TA_VCENTER表示文本水平左对齐、垂直居中。这是最常用的组合,使表格看起来整齐。对于数值型的“大小”列,使用右对齐(GUI_TA_RIGHT)是行业惯例,便于数字比较。
3.3 填充行数据:让表格拥有灵魂
定义好列之后,就可以添加数据行了。数据需要以
GUI_ConstString
指针数组的形式提供。
// 定义第一行数据
static const GUI_ConstString _aFileData1[] = {
"system_log.txt",
"1024",
"2023-10-26 14:30",
};
// 定义第二行数据
static const GUI_ConstString _aFileData2[] = {
"config.ini",
"256",
"2023-10-25 09:15",
};
// 添加行到LISTVIEW
LISTVIEW_AddRow(hListView, _aFileData1);
LISTVIEW_AddRow(hListView, _aFileData2);
// 也可以动态插入行到指定位置,例如插入到索引0的位置(最前面)
static const GUI_ConstString _aFileDataNew[] = {
"readme.md",
"48",
"2023-10-27 10:00",
};
LISTVIEW_InsertRow(hListView, 0, _aFileDataNew); // 新行将成为第一行
重要注意事项 :
-
数组大小
:
GUI_ConstString数组中的元素数量 应该等于 列数。如果元素少于列数,多出的单元格将显示为空。如果多于列数,多出的部分会被忽略。 -
内存管理
:
GUI_ConstString通常指向存储在常量区(如Flash)的字符串。如果你的数据是动态生成的,需要确保字符串在控件的生命周期内有效。emWin内部会存储这些指针,而不是拷贝字符串内容。因此,切勿使用局部变量数组的地址,除非你能保证其生命周期。 - 性能考量 :一次性添加大量行(如超过100行)可能会引起界面卡顿。对于大数据集,考虑使用虚拟列表(如果emWin版本支持)或分页加载。
3.4 基础视觉定制:让界面更友好
默认的LISTVIEW可能看起来比较朴素,我们可以通过几个API快速美化它。
// 1. 设置字体(使用更清晰的字体)
LISTVIEW_SetFont(hListView, &GUI_Font16_ASCII); // 改为16像素字体
// 2. 设置行高(适应新字体,或留出更多空间)
LISTVIEW_SetRowHeight(hListView, 25); // 设置固定行高为25像素
// 如果注释掉上面这行,行高将自动适应字体高度。
// 3. 显示网格线,增强可读性
LISTVIEW_SetGridVis(hListView, 1); // 1为显示,0为隐藏
LISTVIEW_SetGridColor(hListView, GUI_GRAY_LIGHT); // 设置网格线颜色为浅灰色
// 4. 设置选中项颜色(区分有无焦点)
LISTVIEW_SetBkColor(hListView, LISTVIEW_CI_SELFOCUS, GUI_BLUE); // 有焦点时蓝色背景
LISTVIEW_SetTextColor(hListView, LISTVIEW_CI_SELFOCUS, GUI_WHITE); // 有焦点时白色文字
LISTVIEW_SetBkColor(hListView, LISTVIEW_CI_SEL, GUI_GRAY); // 无焦点时灰色背景
LISTVIEW_SetTextColor(hListView, LISTVIEW_CI_SEL, GUI_BLACK); // 无焦点时黑色文字
// 5. 启用自动滚动条(当内容超出时自动出现)
LISTVIEW_SetAutoScrollV(hListView, 1); // 启用垂直滚动条
LISTVIEW_SetAutoScrollH(hListView, 1); // 启用水平滚动条(如果列总宽超出控件宽度)
完成以上步骤,一个具备基本展示和交互功能的LISTVIEW就已经构建完成了。用户可以通过触摸或键盘方向键来浏览和选择列表项。接下来,我们将解锁它的高级技能——排序。
4. 实现高级排序功能:从内置函数到自定义逻辑
排序功能将静态的数据表格变成了一个交互式的数据管理工具。我们基于之前的文件列表,为“文件名”和“大小”两列添加排序能力。
4.1 启用全局排序功能
首先,需要告诉LISTVIEW控件:“我准备使用排序功能了”。
LISTVIEW_EnableSort(hListView);
就这么简单。这个调用是启用排序功能的开关。但仅仅这样,点击表头是不会有反应的,因为还没有告诉控件每一列具体该如何比较数据。
4.2 为各列配置比较函数
我们需要为“文件名”(第0列)和“大小”(第1列)设置比较函数。“修改日期”列我们暂时不处理。
// 为第0列(文件名)设置文本比较函数
LISTVIEW_SetCompareFunc(hListView, 0, LISTVIEW_CompareText);
// 为第1列(大小)设置十进制整数比较函数
LISTVIEW_SetCompareFunc(hListView, 1, LISTVIEW_CompareDec);
// 第2列(修改日期)未设置比较函数,点击该列表头无排序效果。
现在,运行程序,点击“文件名”或“大小”的表头,你会发现列表内容会按照相应的规则进行排序(升序)。再次点击同一列表头,排序顺序会在升序和降序之间切换。这个切换逻辑是控件内部自动处理的。
4.3 处理自定义数据类型:日期排序示例
内置的
LISTVIEW_CompareDec
只能处理纯整数。我们的“修改日期”是“YYYY-MM-DD HH:MM”格式的字符串,直接进行文本比较(
LISTVIEW_CompareText
)会得到错误的字典序结果(例如“2023-01-02”会排在“2023-02-01”后面,因为‘0’<‘2’)。我们需要编写自定义的比较函数。
假设日期字符串格式固定为“YYYY-MM-DD HH:MM”,我们可以编写如下函数:
/**
* 自定义比较函数:用于比较“YYYY-MM-DD HH:MM”格式的日期时间字符串。
* 注意:p0, p1 是指向字符串指针的指针 (const char**)。
*/
int _CompareDate(const void *p0, const void *p1) {
const char **ppsDate0 = (const char **)p0;
const char **ppsDate1 = (const char **)p1;
const char *sDate0 = *ppsDate0;
const char *sDate1 = *ppsDate1;
// 简易解析:直接比较字符串,因为格式固定“YYYY-MM-DD HH:MM”
// 这种格式的字典序恰好与时序一致,可以直接使用strcmp。
// 更严谨的做法是解析成年、月、日、时、分再比较。
return strcmp(sDate0, sDate1); // 升序:较早日期排前面
}
// 在初始化时,为第2列(修改日期)设置自定义比较函数
LISTVIEW_SetCompareFunc(hListView, 2, _CompareDate);
这里有一个至关重要的细节
:
p0
和
p1
的类型是
const void*
,但在LISTVIEW的上下文中,它们实际指向的是单元格文本的地址,即一个
const char*
的地址。所以我们需要先将其转换为
const char**
,再解引用得到真正的字符串指针
const char*
。这是很多开发者第一次编写比较函数时容易出错的地方。
对于更复杂的、非标准格式的日期,你需要在
_CompareDate
函数内部编写更复杂的解析逻辑,将字符串转换为便于比较的整数值(如自1970年以来的秒数)。
4.4 排序后的数据索引映射:
GetSel
vs
GetSelUnsorted
排序功能引入了一个关键问题: 显示的行索引和原始数据索引不再一致 。例如,原始添加的第3行数据,排序后可能显示在第1行。
-
LISTVIEW_GetSel(hObj): 返回的是 当前显示界面 上被选中行的索引(从0开始)。这个索引是排序后的“视觉索引”。 -
LISTVIEW_GetSelUnsorted(hObj): 返回的是 原始数据数组 中被选中行的索引。这个索引是排序前的“数据索引”。
何时使用哪个?
-
进行界面操作
:比如高亮某一行、滚动到某一行,使用
LISTVIEW_SetSel()和LISTVIEW_GetSel(),它们操作的是显示索引。 -
进行数据操作
:当你根据用户选择,需要获取该行对应的实际业务数据(例如,从你的文件信息结构体数组中获取数据)时,
必须使用
LISTVIEW_GetSelUnsorted()来获取正确的数据索引。
// 在窗口回调函数中,响应LISTVIEW的选择改变消息
case WM_NOTIFY_PARENT:
{
WM_MESSAGE* pMsg = (WM_MESSAGE*)pMsg;
int Id = WM_GetId(pMsg->hWinSrc); // 获取发送消息的控件ID
int NCode = pMsg->Data.v; // 通知代码
if (Id == GUI_ID_LISTVIEW0 && NCode == WM_NOTIFICATION_SEL_CHANGED) {
int SelSorted = LISTVIEW_GetSel(hListView); // 获取显示索引
int SelReal = LISTVIEW_GetSelUnsorted(hListView); // 获取真实数据索引
printf("选中了显示第 %d 行,对应原始数据第 %d 行。\n", SelSorted, SelReal);
// 使用 SelReal 来索引你自己的数据数组 fileDataArray[SelReal]
}
}
break;
忘记使用
GetSelUnsorted
是导致排序后数据操作错乱的常见原因,务必牢记。
5. 官方示例WIDGET_SortedListview.c深度剖析与扩展
官方示例
WIDGET_SortedListview.c
是一个极佳的学习模板,它演示了LISTVIEW排序的核心流程。我们来深入解读并补充一些它没明说但很重要的点。
5.1 示例代码结构解析
该示例的核心流程如下:
- 创建窗口和LISTVIEW :在一个对话框上创建LISTVIEW。
- 添加列 :添加了“Text”和“Value”两列。
- 添加数据行 :添加了多行数据,其中“Value”列是数字字符串。
-
设置比较函数
:
LISTVIEW_SetCompareFunc(hListView, 0, LISTVIEW_CompareText); // 第0列按文本排序 LISTVIEW_SetCompareFunc(hListView, 1, LISTVIEW_CompareDec); // 第1列按数值排序 -
启用排序
:
LISTVIEW_EnableSort(hListView); -
初始排序
:
LISTVIEW_SetSort(hListView, 1, 0);// 初始按第1列升序排列。
关键函数
LISTVIEW_SetSort()
用于
以编程方式
设置按哪一列排序以及排序方向,而不仅仅是响应用户点击。
-
Column: 要排序的列索引。 -
Reverse: 0为升序,1为降序。
5.2 扩展实践:动态数据更新与排序刷新
官方示例的数据是静态的。在实际项目中,数据常常是动态变化的。如何在添加、删除或修改行数据后,保持正确的排序显示?
场景 :在文件管理器中,用户新建了一个文件,需要将其添加到列表并自动排序。
// 假设已有排序好的LISTVIEW,当前按文件名升序排列。
void AddNewFileToList(LISTVIEW_Handle hListView, const char* name, const char* size, const char* date) {
const GUI_ConstString newRow[] = {name, size, date};
// 1. 添加新行(此时会添加到末尾,破坏排序视图)
LISTVIEW_AddRow(hListView, newRow);
// 2. 获取当前的排序状态
// 注意:emWin API没有直接获取当前排序列和方向的函数。
// 通常我们需要自己用一个变量来记录当前的排序状态。
// 假设我们用一个全局变量记录:g_SortColumn, g_SortReverse.
// 3. 重新应用排序,刷新视图
LISTVIEW_SetSort(hListView, g_SortColumn, g_SortReverse);
// 或者,如果希望触发用户最后一次点击的排序方式,可能需要更复杂的逻辑。
}
更优的做法 是,在添加数据前,暂时禁用排序,添加后再恢复。但这需要自己管理排序状态。
void AddNewFileToListOrdered(LISTVIEW_Handle hListView, int sortCol, int reverse, ...) {
// 临时禁用排序,防止每次AddRow都触发内部重排(如果数据量大可能影响性能)
LISTVIEW_DisableSort(hListView);
// 添加新行
LISTVIEW_AddRow(hListView, newRow);
// 重新启用并应用排序
LISTVIEW_EnableSort(hListView);
LISTVIEW_SetSort(hListView, sortCol, reverse);
}
5.3 性能优化技巧
当LISTVIEW行数非常多(比如超过500行)时,频繁的排序和刷新可能成为性能瓶颈。
-
批量操作
:在需要添加或删除多行数据时,先调用
WM_DisableWindow()或WM_DisableMemdev()(如果使用了内存设备)临时禁用窗口绘制,所有操作完成后再启用。这可以避免中间状态的频繁刷新。 -
虚拟列表
:对于海量数据(如数千行),考虑使用虚拟列表模式。emWin的LISTVIEW本身不支持真正的虚拟列表,但你可以通过只维护当前显示在视口内的少量行数据,结合滚动事件动态更新
LISTVIEW_SetItemText来实现类似效果。这是一个高级话题,需要对WM(窗口管理器)的消息循环有深入理解。 - 简化比较函数 :自定义比较函数应尽可能高效。避免在比较函数中进行复杂的字符串解析或内存分配。对于数值比较,可以先将字符串转换为数值并缓存起来。
6. 常见问题排查与调试技巧实录
即使理解了原理,在实际开发中仍会遇到各种问题。下面是我在项目中积累的一些常见问题及其解决方案。
6.1 问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 点击表头无排序反应 |
1. 未调用
LISTVIEW_EnableSort()
。
2. 未为对应列设置
LISTVIEW_SetCompareFunc()
。
3. 该列的比较函数指针设置错误(如NULL)。 |
1. 确保在添加数据后、显示前调用了
EnableSort
。
2. 检查
SetCompareFunc
的列索引是否正确。
3. 调试确认函数指针有效。 |
| 排序结果错乱 |
1. 自定义比较函数返回值逻辑错误(升/降序弄反)。
2. 比较函数未正确处理
p0
和
p1
参数(类型转换错误)。
3. 单元格数据格式不一致(如数值列中混入了非数字字符)。 |
1. 重温比较函数返回值规则:
p0
<
p1
时返回负值。
2. 确认将
const void*
正确转换为
const char**
并解引用。
3. 确保待排序列数据格式纯净,或在校验后处理。 |
| 获取到的选中行索引不对 |
在排序后,使用了
LISTVIEW_GetSel()
去索引原始数据数组。
|
必须使用
LISTVIEW_GetSelUnsorted()
来获取原始数据索引。
GetSel()
仅用于界面操作。
|
| 添加行数据后控件无显示或崩溃 |
1. 在添加列之前就添加了行。
2.
GUI_ConstString
数组指向了无效内存(如局部变量)。
3. 数组元素数量少于列数,导致访问越界。 |
1. 严格遵守先
AddColumn
,后
AddRow
的顺序。
2. 确保字符串存储于全局/静态区或堆中,生命周期长于控件。 3. 检查每行数据的数组大小是否与列数匹配。 |
| 网格线不显示 | 默认网格线是隐藏的。 |
调用
LISTVIEW_SetGridVis(hObj, 1)
启用。
|
| 滚动条不出现 |
1. 未启用自动滚动条。
2. 控件尺寸足够显示所有内容。 |
1. 调用
LISTVIEW_SetAutoScrollV/H(hObj, 1)
。
2. 确认内容确实超出了控件显示区域。 |
| 触摸或键盘选择无高亮 | 控件未获得焦点。 |
确保在对话框或父窗口初始化时,通过
WM_SetFocus()
或用户操作将焦点设置到LISTVIEW上。
|
6.2 调试心得:使用模拟器与日志
- 善用emWin模拟器 :SEGGER提供了Windows平台的emWin模拟器。在开发初期,强烈建议在模拟器上完成LISTVIEW的所有逻辑和UI调试。模拟器支持源码级调试,可以单步跟踪到比较函数内部,查看变量值,效率远高于在目标板上下载调试。
-
添加调试信息
:在比较函数、消息回调中添加
printf日志,输出p0、p1的值和比较结果。这对于验证自定义排序逻辑是否正确极其有效。 - 检查内存 :如果遇到控件创建失败或显示异常,首先检查堆栈大小和动态内存是否充足。emWin控件会消耗一定的RAM。
-
验证坐标与尺寸
:如果LISTVIEW完全不可见,检查其创建坐标
(x0, y0)是否在父窗口的客户区内,以及尺寸(xsize, ysize)是否大于0。
6.3 一个自定义排序的完整案例:混合数据类型排序
假设我们有一列“状态”,其值为字符串“High”, “Medium”, “Low”,我们希望按优先级(High > Medium > Low)排序,而非字母顺序。
// 定义优先级映射
static int _GetPriority(const char* status) {
if(strcmp(status, "High") == 0) return 3;
else if(strcmp(status, "Medium") == 0) return 2;
else if(strcmp(status, "Low") == 0) return 1;
else return 0;
}
// 自定义比较函数
int _CompareStatus(const void *p0, const void *p1) {
const char **ppsStatus0 = (const char **)p0;
const char **ppsStatus1 = (const char **)p1;
int prio0 = _GetPriority(*ppsStatus0);
int prio1 = _GetPriority(*ppsStatus1);
// 升序排列:优先级数字小的(Low)排前面
// 如果想降序(High排前面),则返回 prio1 - prio0
return prio0 - prio1;
}
// 应用比较函数到“状态”列(假设是第3列)
LISTVIEW_SetCompareFunc(hListView, 3, _CompareStatus);
这个案例展示了如何将业务逻辑嵌入到比较函数中,实现完全定制化的排序规则。
通过以上从原理到实践,从基础到高级,从正常流程到异常排查的全面解析,相信你已经对emWin的LISTVIEW控件有了深刻的理解。记住,强大的控件需要精细的操控。在嵌入式GUI开发中,对每一个细节的把握,正是打造出流畅、专业用户体验的关键。现在,就打开你的IDE,开始构建属于你自己的高效数据列表界面吧。
304

被折叠的 条评论
为什么被折叠?



