嵌入式GUI开发实战:emWin LISTVIEW控件排序功能深度解析

AI助手已提取文章相关产品:

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控件可以看作是由几个逻辑部分组合而成的复合控件:

  1. 基础窗口 :作为容器,负责控件的区域管理、消息处理和绘制调度。
  2. HEADER部件 :这是LISTVIEW的灵魂之一。它自动位于控件顶部,负责显示列标题。用户点击表头的行为(如触发排序)正是通过HEADER来捕获和传递的。你可以通过 LISTVIEW_GetHeader() 获取其句柄,进而自定义表头的颜色、字体等属性。
  3. 内容区域 :用于绘制所有的数据行和列。它负责处理单元格的文本绘制、背景色渲染、网格线显示以及选中状态的高亮。
  4. 滚动条(可选) :当内容超出显示区域时,可以通过 LISTVIEW_SetAutoScrollV/H() 自动或手动添加滚动条。

这种设计带来了一个关键特性: LISTVIEW的数据模型是“按行组织”的 。当你使用 LISTVIEW_AddRow() 添加一行数据时,需要传入一个字符串数组( GUI_ConstString* ppText ),数组中的每个元素对应一列的内容。这意味着,你必须先通过 LISTVIEW_AddColumn() 定义好所有列的结构(宽度、标题、对齐方式),然后才能添加行数据。顺序不能颠倒,这是新手常踩的第一个坑。

2.2 排序机制深度解析

排序是LISTVIEW区别于简单列表的核心功能。其实现机制精巧且高效,理解它对于实现自定义排序逻辑至关重要。

排序的触发流程

  1. 启用排序 :首先,你需要调用 LISTVIEW_EnableSort(hObj) 来为整个控件开启排序功能。默认情况下,排序是关闭的。
  2. 设置比较函数 :这是排序的逻辑核心。你需要为 希望支持排序的每一列 单独设置一个比较函数,通过 LISTVIEW_SetCompareFunc(hObj, Column, fpCompare) 实现。 fpCompare 是一个函数指针,指向你定义的比较函数。如果不为某列设置比较函数,点击该列表头将不会触发排序。
  3. 用户交互与响应 :当用户点击某一列的标题时,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); // 新行将成为第一行

重要注意事项

  1. 数组大小 GUI_ConstString 数组中的元素数量 应该等于 列数。如果元素少于列数,多出的单元格将显示为空。如果多于列数,多出的部分会被忽略。
  2. 内存管理 GUI_ConstString 通常指向存储在常量区(如Flash)的字符串。如果你的数据是动态生成的,需要确保字符串在控件的生命周期内有效。emWin内部会存储这些指针,而不是拷贝字符串内容。因此,切勿使用局部变量数组的地址,除非你能保证其生命周期。
  3. 性能考量 :一次性添加大量行(如超过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 示例代码结构解析

该示例的核心流程如下:

  1. 创建窗口和LISTVIEW :在一个对话框上创建LISTVIEW。
  2. 添加列 :添加了“Text”和“Value”两列。
  3. 添加数据行 :添加了多行数据,其中“Value”列是数字字符串。
  4. 设置比较函数
    LISTVIEW_SetCompareFunc(hListView, 0, LISTVIEW_CompareText); // 第0列按文本排序
    LISTVIEW_SetCompareFunc(hListView, 1, LISTVIEW_CompareDec);  // 第1列按数值排序
    
  5. 启用排序 LISTVIEW_EnableSort(hListView);
  6. 初始排序 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 调试心得:使用模拟器与日志

  1. 善用emWin模拟器 :SEGGER提供了Windows平台的emWin模拟器。在开发初期,强烈建议在模拟器上完成LISTVIEW的所有逻辑和UI调试。模拟器支持源码级调试,可以单步跟踪到比较函数内部,查看变量值,效率远高于在目标板上下载调试。
  2. 添加调试信息 :在比较函数、消息回调中添加 printf 日志,输出 p0 p1 的值和比较结果。这对于验证自定义排序逻辑是否正确极其有效。
  3. 检查内存 :如果遇到控件创建失败或显示异常,首先检查堆栈大小和动态内存是否充足。emWin控件会消耗一定的RAM。
  4. 验证坐标与尺寸 :如果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,开始构建属于你自己的高效数据列表界面吧。

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值