1. 项目概述与核心价值
在嵌入式GUI开发领域,emWin以其高效、稳定和功能全面而著称,是许多资源受限的MCU项目的首选图形库。它提供了一套丰富的控件(Widgets),将复杂的图形渲染和用户交互逻辑封装成易于调用的API,让开发者能够像搭积木一样构建复杂的用户界面。今天,我们聚焦于两个看似基础但至关重要的控件:TEXT文本控件和TREEVIEW树形视图控件。TEXT控件负责静态或动态文本的显示,是界面信息传递的基石;而TREEVIEW控件则用于展示层次化数据,如文件系统、设备菜单或配置选项,是构建复杂导航和设置界面的利器。
很多开发者拿到官方手册(如UM03001)后,面对密密麻麻的API列表和参数说明,常常感到无从下手。手册提供了“是什么”(函数原型)和“做什么”(功能描述),但很少深入讲解“为什么这么用”以及“实际用起来有哪些坑”。本文的目的,就是结合我多年在STM32、NXP等平台使用emWin的经验,为你拆解这两个控件的核心API,不仅告诉你每个函数怎么调用,更会剖析其背后的设计逻辑、分享实战中的配置技巧和避坑指南。无论你是正在为工业HMI设计状态显示界面,还是在为智能家居中控屏构建设备树菜单,理解并熟练运用这两个控件,都将是你项目成功的关键一步。
2. TEXT控件:嵌入式界面的信息基石
TEXT控件是emWin中最基础的控件之一,它的核心任务就是显示一段文本。别看它简单,在实际项目中,文本的显示效果直接影响到用户体验和专业感。字体、颜色、对齐方式、背景处理,每一个细节都值得推敲。
2.1 核心API详解与设计逻辑
emWin提供了多种创建TEXT控件的方式,从早期的TEXT_Create到更推荐的TEXT_CreateEx,其演进体现了库设计上对灵活性和易用性的平衡。
1. 创建控件:TEXT_CreateEx是首选官方手册标注TEXT_Create和TEXT_CreateAsChild为“Obsolete”(过时),这并非空穴来风。TEXT_CreateEx函数提供了更清晰的参数分离和更强大的功能。
TEXT_Handle TEXT_CreateEx(int x0, int y0, int xSize, int ySize, WM_HWIN hParent, int WinFlags, int ExFlags, int Id, const char * pText);我们来拆解关键参数:
hParent:父窗口句柄。这里有个重要技巧:如果传入0,控件将成为桌面(顶级窗口)的子窗口。但在实际的多窗口应用中,强烈建议始终指定明确的父窗口。这关系到窗口管理器的消息传递、裁剪区域计算和内存管理。一个没有父窗口的控件,在窗口关闭或移动时可能引发难以调试的显示异常。WinFlags:窗口创建标志。WM_CF_SHOW是最常用的,创建后立即显示。但在一些动态界面中,你可能先创建隐藏控件(不传此标志),等所有属性配置完毕后再调用WM_ShowWindow()显示,这样可以避免界面闪烁。ExFlags:文本对齐标志。这是TEXT_CreateEx的精华所在。它使用TEXT_CF_*系列的宏进行位或(|)组合,同时指定水平和垂直对齐。
> 注意:对齐的参照系是控件矩形区域,而非屏幕或父窗口。例如,TEXT_CF_HCENTER | TEXT_CF_VCENTER会让文本在控件创建时定义的(xSize, ySize)矩形区域内居中。
2. 动态文本设置:TEXT_SetText的返回值玄机设置文本最常用的函数是TEXT_SetText。它的原型是:
int TEXT_SetText(TEXT_Handle hObj, const char * s);手册提到返回值是0成功,1失败。但在实际使用中,我几乎从未见过它返回1。失败通常只发生在极端情况,如传入的句柄hObj完全无效(不是TEXT控件或已被删除)。更常见的“问题”是文本没有按预期显示,这往往不是本函数的问题,而是字体、颜色或控件尺寸设置不当。因此,在调试时,不要过度依赖这个返回值,而应检查其他属性。
3. 外观控制三要素:字体、颜色与自动换行
- 字体 (
TEXT_SetFont):emWin支持等宽和比例字体。为TEXT控件设置字体时,必须确保该字体已通过GUI_UC_SetEncodeUTF8()等函数正确初始化并加入库。否则,控件可能显示为空白或乱码。对于多语言项目,这是一个关键检查点。 - 颜色 (
TEXT_SetTextColor,TEXT_SetBkColor):文本颜色设置很直观。背景颜色TEXT_SetBkColor有一个特殊值GUI_INVALID_COLOR,表示透明背景。透明背景的渲染速度会比纯色背景慢,因为需要混合计算。如果控件区域背景是静态的,设置为实际背景色能获得更好的性能。 - 自动换行 (
TEXT_SetWrapMode):当文本长度超过控件宽度时,换行模式决定如何断开。GUI_WRAPMODE_NONE:不换行,超出的部分被裁剪。适用于标签、标题等短文本。GUI_WRAPMODE_WORD:按单词换行。这是最人性化的方式,保证单词完整性。GUI_WRAPMODE_CHAR:按字符换行。适用于中文等无空格分隔的语言。
> 实操心得:换行与控件高度的计算。如果你启用了WORD或CHAR换行,必须注意控件的高度ySize是否足够。你可以先调用TEXT_SetText设置文本,然后通过TEXT_GetNumLines()获取实际行数,再根据行高(字体高度+行间距)动态调整控件高度,这是一个创建自适应文本显示区域的常用技巧。
2.2 实战应用:构建一个动态更新的状态信息框
假设我们要在工业设备屏幕上创建一个显示实时温度和历史状态日志的区域。这个区域需要滚动显示新消息,并且不同级别的消息(正常、警告、错误)用不同颜色显示。
步骤1:创建与基础配置我们首先创建一个足够大的TEXT控件作为日志显示框。为了支持滚动和颜色变化,我们实际上需要更复杂的方案(如LISTVIEW或LISTBOX),但用TEXT模拟可以理解其原理。
// 假设在对话框回调函数中创建 TEXT_Handle hTextStatus; GUI_COLOR textColorNormal = GUI_GREEN; GUI_COLOR textColorWarning = GUI_YELLOW; GUI_COLOR textColorError = GUI_RED; // 创建文本控件,左对齐、顶部对齐,初始文本为空 hTextStatus = TEXT_CreateEx(10, 50, 300, 150, hParent, WM_CF_SHOW, TEXT_CF_LEFT | TEXT_CF_TOP, GUI_ID_TEXT0, // 自定义ID,用于消息识别 “”); // 设置等宽字体,方便对齐 TEXT_SetFont(hTextStatus, &GUI_Font8x16); // 初始背景设为浅灰色 TEXT_SetBkColor(hTextStatus, GUI_GRAY_2F); // 启用按单词换行 TEXT_SetWrapMode(hTextStatus, GUI_WRAPMODE_WORD);步骤2:实现动态追加文本函数TEXT控件本身没有“追加”文本的API。我们需要先获取现有文本,拼接新字符串,再重新设置。
void TEXT_AppendString(TEXT_Handle hText, const char* newLine, GUI_COLOR color) { char buffer[512]; // 确保缓冲区足够大 int currentLen; // 1. 获取当前文本 currentLen = TEXT_GetText(hText, buffer, sizeof(buffer)-1); buffer[currentLen] = ‘\0‘; // TEXT_GetText 保证以0结尾,但显式处理更安全 // 2. 拼接新行。简单处理,实际项目需考虑缓冲区溢出! strcat(buffer, newLine); strcat(buffer, “\n“); // 添加换行符 // 3. 设置新文本 TEXT_SetText(hText, buffer); // 4. 改变颜色(这里简化处理,实际可记录颜色状态或使用多控件) // TEXT_SetTextColor(hText, color); // 注意:上述代码会改变全部文本颜色。如需部分着色,需用多个TEXT控件或OwnerDraw。 }> 踩坑记录:缓冲区溢出与性能。上面的示例代码存在明显的缓冲区溢出风险。在嵌入式系统中,必须严格管理内存。更安全的做法是使用环形缓冲区或链表来管理日志行,并只显示最近N行。此外,频繁调用TEXT_GetText和TEXT_SetText(特别是文本很长时)会导致CPU占用率高。优化方案是:对于高速更新的状态,考虑使用单独的TEXT控件只显示最新一行;对于历史日志,使用LISTBOX或MULTIEDIT控件更为合适,它们原生支持行追加和滚动。
步骤3:处理用户交互(通知代码)虽然TEXT控件默认不接收焦点,但它能产生通知。例如,我们可以让用户点击某条日志(如果支持)来查看详情。这需要在父窗口的WM_NOTIFY_PARENT消息中处理。
case WM_NOTIFY_PARENT: Id = WM_GetId(pMsg->hWinSrc); // 获取触发控件的ID NCode = pMsg->Data.v; // 获取通知代码 switch (Id) { case GUI_ID_TEXT0: // 我们的状态文本控件ID switch (NCode) { case WM_NOTIFICATION_CLICKED: // 用户点击了文本区域 // 可以在这里弹出对话框显示更详细的信息 break; case WM_NOTIFICATION_RELEASED: // 点击释放 break; } break; } break;3. TREEVIEW控件:层次化数据管理的利器
TREEVIEW控件是展示树形结构数据的标准组件,比如文件浏览器、设备树、分类设置菜单等。它的核心概念是项(Item),每个项可以是节点(Node)或叶(Leaf)。节点可以展开/折叠以显示/隐藏其子项。
3.1 核心概念与API设计哲学
理解TREEVIEW,首先要厘清几个关键概念,这直接关系到API的正确调用:
- 句柄的双重性:TREEVIEW控件本身有一个
TREEVIEW_Handle,而树中的每一个项(Item)都有一个独立的TREEVIEW_ITEM_Handle。很多函数都需要同时指定控件句柄和项句柄。 - 项的创建与附着分离:
TREEVIEW_ITEM_Create仅仅是在内存中创建了一个项对象,它此时是“游离”的,并未显示。必须通过TREEVIEW_InsertItem或TREEVIEW_AttachItem将其插入到树控件中,它才会被显示和管理。这种设计允许你预先构建复杂的树结构,再一次性附加到控件上,提高初始化效率。 - 坐标系统与滚动:TREEVIEW的内容区域可能远大于其显示窗口。
TREEVIEW_SetAutoScrollV/H可以自动管理滚动条。一个重要细节:自动滚动条只在内容超出时出现。如果你需要始终显示滚动条(哪怕内容很少),则需要手动创建SCROLLBAR控件并通过回调函数关联。
3.2 构建一棵完整的树:从创建到渲染
让我们一步步构建一个模拟设备设置菜单的树。
步骤1:创建TREEVIEW控件
TREEVIEW_Handle hTreeView; int TreeFlags = 0; // 组合创建标志:启用行选择模式、自动垂直滚动条 TreeFlags |= TREEVIEW_CF_ROWSELECT; // 整行高亮,更易操作 TreeFlags |= TREEVIEW_CF_AUTOSCROLLBAR_V; // 内容过长时自动出现滚动条 hTreeView = TREEVIEW_CreateEx(10, 10, 200, 250, hParent, WM_CF_SHOW | WM_CF_MEMDEV, // 使用内存设备防闪烁 TreeFlags, GUI_ID_TREEVIEW0, “”); // 注意,TREEVIEW_CreateEx没有直接的初始文本参数> 注意:WM_CF_MEMDEV标志在界面复杂或更新频繁时能有效减少闪烁,但会消耗额外的RAM作为内存设备。需根据芯片资源权衡。
步骤2:设置视觉属性(字体、颜色、图片)在插入任何项之前,先配置好控件整体的外观。
// 设置字体 TREEVIEW_SetFont(hTreeView, &GUI_Font13_1); // 设置颜色:未选中项白底黑字,选中项蓝底白字 TREEVIEW_SetBkColor(hTreeView, TREEVIEW_CI_UNSEL, GUI_WHITE); TREEVIEW_SetTextColor(hTreeView, TREEVIEW_CI_UNSEL, GUI_BLACK); TREEVIEW_SetBkColor(hTreeView, TREEVIEW_CI_SEL, GUI_BLUE); TREEVIEW_SetTextColor(hTreeView, TREEVIEW_CI_SEL, GUI_WHITE); // 设置连接线颜色 TREEVIEW_SetLineColor(hTreeView, TREEVIEW_CI_UNSEL, GUI_DARKGRAY); // 自定义节点开/合和叶子图标(需要先定义好GUI_BITMAP资源) extern GUI_BITMAP bmFolderClosed, bmFolderOpen, bmFile; TREEVIEW_SetImage(hTreeView, TREEVIEW_BI_CLOSED, &bmFolderClosed); TREEVIEW_SetImage(hTreeView, TREEVIEW_BI_OPEN, &bmFolderOpen); TREEVIEW_SetImage(hTreeView, TREEVIEW_BI_LEAF, &bmFile); // 也可以自定义+/-号图标,若不设置则使用库默认步骤3:创建并插入树项这是最核心的步骤。我们构建一个“系统设置”树。
TREEVIEW_ITEM_Handle hItemRoot, hItemComm, hItemNet, hItemWiFi, hItemEth; // 1. 创建根节点 “系统设置” hItemRoot = TREEVIEW_ITEM_Create(TREEVIEW_ITEM_TYPE_NODE, “系统设置”, 0); // 作为第一个项插入,Position用TREEVIEW_INSERT_FIRST_CHILD,hItemPrev为0 TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_NODE, 0, TREEVIEW_INSERT_FIRST_CHILD, “系统设置”); // 注意:TREEVIEW_InsertItem内部会创建项,并返回其句柄。 // 这里为了演示,我们直接用其返回值。上面两行等价,但通常用一行完成创建和插入。 hItemRoot = TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_NODE, 0, TREEVIEW_INSERT_FIRST_CHILD, “系统设置”); // 2. 在根节点下插入子节点 “通信设置” hItemComm = TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_NODE, hItemRoot, TREEVIEW_INSERT_FIRST_CHILD, “通信设置”); // 3. 在“通信设置”下插入两个子节点 hItemNet = TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_NODE, hItemComm, TREEVIEW_INSERT_FIRST_CHILD, “网络设置”); hItemWiFi = TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_LEAF, hItemNet, TREEVIEW_INSERT_FIRST_CHILD, “Wi-Fi”); // 在“Wi-Fi”后面插入“以太网”,使用TREEVIEW_INSERT_BELOW hItemEth = TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_LEAF, hItemWiFi, TREEVIEW_INSERT_BELOW, “以太网”); // 4. 在“通信设置”下插入另一个叶子节点 “蓝牙” TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_LEAF, hItemNet, TREEVIEW_INSERT_BELOW, “蓝牙”); // 注意:此时hItemNet是“网络设置”节点,新项插在它的“下面”,即与“Wi-Fi”/“以太网”同级?不对! // 这里意图是在“通信设置”下,与“网络设置”同级。所以hItemPrev应为hItemNet,Position为TREEVIEW_INSERT_BELOW。 // 但hItemNet是hItemComm的子节点,INSERT_BELOW会插在hItemNet的后面,但仍在hItemComm下,且缩进级别与hItemNet相同。 // 要插入到hItemComm下,作为第二个子项(在“网络设置”后面),正确的做法是: TREEVIEW_ITEM_Handle hItemBt; hItemBt = TREEVIEW_InsertItem(hTreeView, TREEVIEW_ITEM_TYPE_LEAF, hItemComm, TREEVIEW_INSERT_BELOW, “蓝牙”); // 这样,“蓝牙”就和“网络设置”是兄弟关系,都是“通信设置”的子项。> 核心技巧:理解hItemPrev和Position参数。这是TREEVIEW插入逻辑的难点。
TREEVIEW_INSERT_FIRST_CHILD:将新项作为hItemPrev项的第一个子项插入。hItemPrev必须是一个节点。TREEVIEW_INSERT_BELOW:将新项插入到hItemPrev项的下方,并保持相同的缩进层级。hItemPrev可以是任何项。TREEVIEW_INSERT_ABOVE:将新项插入到hItemPrev项的上方,并保持相同的缩进层级。
构建树时,从根节点开始,深度优先是一种清晰的思路。先创建父节点,然后立即创建其子节点,再回来创建父节点的兄弟节点。
步骤4:设置初始状态与获取用户选择
// 默认展开“系统设置”和“通信设置”节点 TREEVIEW_ITEM_Expand(hItemRoot); TREEVIEW_ITEM_Expand(hItemComm); // 默认选中“Wi-Fi”项 TREEVIEW_SetSel(hTreeView, hItemWiFi); // 并滚动到选中项,确保其在可视区域内 TREEVIEW_ScrollToSel(hTreeView);3.3 高级功能与性能优化
1. 自定义绘制(Owner Draw)当默认的项渲染(图片+文字)不满足需求时,可以使用TREEVIEW_SetOwnerDraw设置一个回调函数,完全接管每一项的绘制。这在需要显示额外信息(如状态图标、进度条)时非常有用。回调函数中,你可以通过TREEVIEW_ITEM_GetUserData获取项关联的32位用户数据,根据数据内容动态绘制。
2. 动态增删与更新对于需要频繁更新的树(如实时刷新的文件列表),直接删除重建整个树会导致界面闪烁。优化策略是:
- 批量操作:在开始大量更新前,调用
WM_DisableWindow()临时禁用控件重绘,所有操作完成后再调用WM_EnableWindow()并手动WM_InvalidateWindow()触发一次重绘。 - 增量更新:使用
TREEVIEW_GetItem配合TREEVIEW_GET_FIRST_CHILD、TREEVIEW_GET_NEXT_SIBLING等标志遍历树,只更新发生变化的项。 - 谨慎使用
TREEVIEW_ITEM_Delete:删除一个节点会自动删除其所有子节点。确保你不再需要这些子项的数据。
3. 内存与项管理每个TREEVIEW_ITEM都占用内存。在资源紧张的MCU上,对于可能很大的树(如全文件系统),考虑“懒加载”(Lazy Loading):只创建和显示顶层或已展开的节点,当用户展开一个节点时,再动态创建并插入其子项。这需要配合WM_NOTIFY_PARENT消息中的WM_NOTIFICATION_SEL_CHANGED或自定义展开/折叠通知来实现。
4. 联动与实战:TEXT与TREEVIEW的综合应用案例
让我们设计一个简单的设备管理器界面:左侧是TREEVIEW控件展示设备分类(传感器、执行器、网络),右侧是一个TEXT控件,用于显示当前选中设备项的详细信息。
设计思路:
- 创建对话框,左侧放置TREEVIEW,右侧放置TEXT。
- 初始化TREEVIEW,构建设备树。
- 为TREEVIEW的每个项设置唯一的
UserData(例如,一个指向设备信息结构体的指针或ID)。 - 在对话框的
WM_NOTIFY_PARENT消息处理中,监听TREEVIEW的WM_NOTIFICATION_SEL_CHANGED通知。 - 当选择改变时,通过
TREEVIEW_GetSel获取当前选中项的句柄,进而用TREEVIEW_ITEM_GetUserData获取设备ID。 - 根据设备ID,查询或组装详细的设备状态字符串。
- 调用
TEXT_SetText更新右侧TEXT控件的内容。
关键代码片段:
// 假设在对话框初始化中... // 创建TREEVIEW和TEXT控件 hTree = TREEVIEW_CreateEx(...); hTextInfo = TEXT_CreateEx(...); // 插入设备项,并设置UserData为设备类型ID hItemSensor = TREEVIEW_InsertItem(hTree, TREEVIEW_ITEM_TYPE_NODE, ... , “传感器”); TREEVIEW_ITEM_SetUserData(hItemSensor, DEVICE_TYPE_SENSOR); hItemTemp = TREEVIEW_InsertItem(hTree, TREEVIEW_ITEM_TYPE_LEAF, hItemSensor, ... , “温度传感器”); TREEVIEW_ITEM_SetUserData(hItemTemp, SENSOR_ID_TEMP); // 在WM_NOTIFY_PARENT消息处理中 case WM_NOTIFY_PARENT: Id = WM_GetId(pMsg->hWinSrc); NCode = pMsg->Data.v; if (Id == GUI_ID_TREEVIEW0) { switch (NCode) { case WM_NOTIFICATION_SEL_CHANGED: { TREEVIEW_ITEM_Handle hSel; U32 devId; char infoBuffer[128]; hSel = TREEVIEW_GetSel(hTree); if (hSel) { devId = TREEVIEW_ITEM_GetUserData(hSel); // 根据devId生成详细信息 GenerateDeviceInfoString(devId, infoBuffer, sizeof(infoBuffer)); // 更新TEXT控件 TEXT_SetText(hTextInfo, infoBuffer); } break; } } } break;> 避坑指南:消息传递与重入问题。在WM_NOTIFICATION_SEL_CHANGED通知的处理函数中,避免直接进行耗时操作(如复杂的字符串格式化、从低速外设读取数据)。这会导致GUI主线程阻塞,界面卡顿。正确的做法是:在此处仅设置一个标志或发送一个自定义用户消息(WM_USER),然后在主消息循环或一个低优先级的定时器回调中去执行实际的数据获取和更新操作。这保证了GUI的响应性。
5. 常见问题排查与调试技巧实录
在实际项目中使用TEXT和TREEVIEW控件,你肯定会遇到一些“诡异”的情况。下面是我踩过的一些坑和解决方法。
问题1:TEXT控件设置了文本,但屏幕上什么都不显示。
- 检查点1:字体。这是最常见的原因。确认
TEXT_SetFont使用的字体指针有效,且该字体已链接到项目中。尝试换用emWin内置的&GUI_Font13_1等字体测试。 - 检查点2:颜色。文本颜色是否与背景颜色相同?比如白色文本配白色背景。设置一个对比度强的颜色测试。
- 检查点3:控件尺寸和位置。控件是否被其他窗口或控件覆盖?控件的
(xSize, ySize)是否为0?是否在父窗口的可视区域外?使用WM_BringToTop()或临时设置一个显眼的背景色来调试。 - 检查点4:内存设备与重绘。如果使用了内存设备(
WM_CF_MEMDEV),确保在父窗口的WM_PAINT消息中正确调用了GUI_MEMDEV_*系列函数进行绘制。
问题2:TREEVIEW控件内容显示不全,或者滚动条行为异常。
- 检查点1:自动滚动条标志。创建时是否设置了
TREEVIEW_CF_AUTOSCROLLBAR_V或_H?内容必须超出控件范围,滚动条才会出现。 - 检查点2:项的高度计算。TREEVIEW的每一项高度由字体高度和图片高度共同决定。如果你使用了自定义的大图片,但未调整
TREEVIEW_SetIndent或TREEVIEW_SetTextIndent,可能导致项的高度计算错误,部分内容被裁剪。调试方法:临时关闭自定义图片,使用默认图片看是否正常。 - 检查点3:WM_PAINT消息阻塞。如果父窗口或TREEVIEW自身的
WM_PAINT消息处理中有耗时操作,会导致滚动时界面卡顿、部分区域刷新不及时。确保绘图回调函数快速返回。
问题3:TREEVIEW项点击无反应,选择状态不更新。
- 检查点1:输入焦点。TREEVIEW默认可以接收焦点并通过键盘操作。确保没有其他控件(如EDIT)一直霸占焦点。可以通过
WM_SetFocus()手动设置焦点到TREEVIEW。 - 检查点2:通知回调。确认父窗口正确处理了
WM_NOTIFY_PARENT消息,并且针对TREEVIEW的ID和WM_NOTIFICATION_CLICKED/WM_NOTIFICATION_SEL_CHANGED通知码进行了处理。 - 检查点3:控件使能状态。检查是否意外调用了
WM_DisableWindow()禁用了TREEVIEW控件。
问题4:动态修改TREEVIEW项文本后,显示出现乱码或残留。
- 原因与解决:直接修改
TREEVIEW_ITEM_SetText传入的字符串指针指向的内容是危险的。emWin内部可能会存储该指针或进行拷贝。最安全的做法是,每次更新文本都传入一个全新的、有效的字符串常量或静态缓冲区地址。确保在文本生命周期内,该地址的内容不会被意外修改或释放。
调试利器:emWin模拟器与调试输出。在移植到硬件之前,务必在PC上的emWin模拟器中充分测试。模拟器可以快速验证逻辑,并且emWin库通常带有调试版本,可以在输出窗口打印警告和错误信息(如无效句柄、内存分配失败)。善用这些信息,能极大缩短定位问题的时间。
最后,关于性能,记住一个原则:减少无效的重绘。频繁调用TEXT_SetText或TREEVIEW_InsertItem会导致整个控件甚至窗口区域重绘。在需要连续更新时,考虑使用WM_DisableWindow/WM_EnableWindow包裹更新序列,或者将多次更新合并为一次。对于TREEVIEW,如果只是更新某项的文本或状态,使用TREEVIEW_ITEM_SetText和TREEVIEW_ITEM_SetImage比删除再插入项要高效得多。