emWin进阶控件:LISTWHEEL与MENU的API详解与实战应用

emWin进阶控件:LISTWHEEL与MENU的API详解与实战应用

1. 项目概述

在嵌入式GUI开发领域,emWin以其高效、稳定和功能全面而著称,是许多资源受限的MCU项目的首选图形库。今天,我们不谈那些基础的按钮和文本框,而是深入两个在构建现代化、交互性强的用户界面时至关重要的“进阶”控件:LISTWHEEL(列表滚轮)和MENU(菜单)。如果你正在为你的嵌入式设备设计一个日期时间选择器、一个可滚动的参数列表,或者一个带有多级子菜单的系统设置界面,那么这两个控件将是你工具箱里的利器。

LISTWHEEL控件,顾名思义,模拟了物理滚轮或触摸屏上常见的“惯性滚动”列表。它不同于传统的LISTBOX(列表框),后者通常依赖滚动条或方向键进行逐项选择。LISTWHEEL允许用户通过触摸或指针设备(PID)在列表上滑动,列表会随之流畅滚动,并在释放后带有减速动画,最终“吸附”到某个选项上,交互体验非常自然。而MENU控件则是构建复杂导航结构的基石,无论是横置在屏幕顶部的导航栏,还是点击后弹出的垂直下拉菜单,甚至是多级嵌套的树形菜单,它都能胜任。

掌握这两个控件的API,意味着你能在有限的嵌入式资源上,创造出不输于移动应用的流畅交互体验。接下来,我将结合官方手册和多年的一线开发经验,为你拆解这两个控件的核心API、使用技巧以及那些手册里不会明说的“坑”。

2. LISTWHEEL控件:打造流畅的滚轮选择器

LISTWHEEL控件的核心魅力在于其动态的交互逻辑。它内部维护着一个虚拟的、可循环的列表,用户的操作直接转化为列表的位移和速度,最终通过一个“吸附点”(Snap Position)来确定选中的项。理解这个机制,是灵活运用其API的关键。

2.1 核心机制与配置选项解析

在开始调用API之前,我们必须先理解几个影响LISTWHEEL“手感”和外观的核心配置,这些通常在创建控件前通过宏定义进行全局设置,也可以在运行时动态调整。

减速系数(Deceleration):这是控制“手感”最重要的参数。当用户滑动后释放,列表不会立刻停止,而是会继续滚动一段距离并逐渐减速至停止。LISTWHEEL_DECELERATION_DEFAULT默认值为15。这个值越大,减速越快,滚动的距离越短,感觉越“涩”;值越小,减速越慢,滚动的距离越长,感觉越“滑”。在触摸屏设备上,我通常需要根据屏幕尺寸和项目密度进行微调,值在10到30之间比较常见。设置函数为LISTWHEEL_SetDeceleration

定时器周期(Timer Period):控件内部使用一个定时器来更新滚动动画。LISTWHEEL_TIMER_PERIOD_DEFAULT默认是25毫秒。这意味着动画的帧间隔约为40帧/秒。降低这个值(如设为10ms)会让动画更新更频繁,看起来更平滑,但会消耗更多的CPU资源;提高这个值会节省资源,但可能导致动画卡顿。在性能紧张的系统中需要权衡。通过LISTWHEEL_SetTimerPeriod函数设置。

视觉样式默认值:包括字体、颜色、对齐方式等。例如,LISTWHEEL_FONT_DEFAULT定义了默认字体,LISTWHEEL_TEXTCOLOR0_DEFAULTLISTWHEEL_TEXTCOLOR1_DEFAULT分别定义了未选中和选中项的文字颜色。合理设置这些默认值,可以保持整个应用UI风格的一致性。

2.2 创建与初始化:从零构建一个LISTWHEEL

创建LISTWHEEL控件主要有两种方式:直接创建和间接创建。对于大多数应用,直接使用LISTWHEEL_CreateEx函数就足够了。

// 准备要显示的字符串数组,注意最后一个元素必须是NULL static const GUI_CONST_STORAGE char * _apWeekdays[] = { “星期一”, “星期二”, “星期三”, “星期四”, “星期五”, “星期六”, “星期日”, NULL // 结束标志,至关重要! }; // 创建LISTWHEEL控件 hListWheel = LISTWHEEL_CreateEx(50, // x坐标 100, // y坐标 200, // 宽度 150, // 高度 hParent, // 父窗口句柄,可为0(桌面) WM_CF_SHOW, // 创建后立即显示 0, // 扩展标志,保留 GUI_ID_LISTWHEEL0, // 控件ID _apWeekdays); // 初始文本数组

这里有几个关键点需要注意:

  1. 字符串数组必须以NULL结尾:这是emWin中许多以数组指针作为参数的函数的通用约定,用于标识数组结束。忘记添加NULL会导致内存访问越界,是常见的崩溃原因。
  2. 控件尺寸:宽度需要能容纳下最长的字符串,否则超出的部分会被裁剪。高度决定了同时可见的项数。通常,控件高度是行高的整数倍,视觉上会更协调。
  3. 控件IDGUI_ID_LISTWHEEL0是预定义的ID。当控件触发通知消息(如WM_NOTIFICATION_SEL_CHANGED)时,这个ID会包含在消息中,方便父窗口回调函数识别是哪个控件产生的事件。

创建完成后,我们通常需要进一步配置。例如,设置一个居中的吸附位置,让选中的项总是停在控件中央,这能极大提升用户体验:

// 获取控件高度 int height = WM_GetWindowSizeY(hListWheel); // 设置吸附点为控件垂直中心 LISTWHEEL_SetSnapPosition(hListWheel, height / 2);

2.3 动态操作与数据管理

静态列表往往不够用,我们需要动态增删改列表项。

添加项:使用LISTWHEEL_AddString。这里有一个性能上的考量:如果需要在初始化后一次性添加大量项目,直接调用此函数多次是低效的,因为每次添加都可能触发重绘。更好的做法是,先创建一个临时的字符串指针数组,填充所有数据,然后使用LISTWHEEL_SetText一次性设置整个列表内容。

删除与更新:LISTWHEEL没有提供直接的删除单个项目的API。如果需要删除或修改中间项,标准的做法是:

  1. 使用LISTWHEEL_GetNumItems获取当前项数。
  2. 遍历所有项(LISTWHEEL_GetItemText),将需要的项复制到一个新的临时数组中。
  3. 使用LISTWHEEL_SetText用新数组重新设置整个列表。

获取与设置选中项:这是交互的核心。LISTWHEEL_GetSel返回当前选中项的索引(从0开始)。LISTWHEEL_SetSel可以编程式地设置选中项,并会触发滚动动画移动到该项。而LISTWHEEL_SetPos则是直接“跳转”到指定项,没有动画。在需要响应外部事件(如从串口接收到一个预设值)快速更新显示时,用SetPos;在响应用户界面操作时,用SetSel以获得更好的反馈。

2.4 高级定制:所有者绘制(Owner Draw)

默认的LISTWHEEL只显示文本。但很多时候,我们需要更丰富的表现力,比如在每一项旁边显示一个图标,或者用不同的颜色和背景区分状态。这时就需要用到所有者绘制(Owner Draw)功能。

通过LISTWHEEL_SetOwnerDraw函数,你可以注册一个自定义的回调函数。当控件需要绘制每一项、计算项的大小或绘制背景/覆盖层时,都会调用这个函数。

static int _MyOwnerDraw(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) { LISTWHEEL_Handle hObj = pDrawItemInfo->hWin; int ItemIndex = pDrawItemInfo->ItemIndex; const char* pText; char buffer[50]; switch (pDrawItemInfo->Cmd) { case WIDGET_ITEM_GET_YSIZE: // 告诉控件每一项需要多高。比如,我们想要比字体默认高度更高的行。 return GUI_GetFontSizeY(GUI_GetFont()) + 4; // 字体高度加4像素 case WIDGET_ITEM_DRAW: // 这是核心的绘制命令 // pDrawItemInfo->Rect 定义了该项的绘制区域 // pDrawItemInfo->SelFlags 指示该项是否被选中 // 1. 绘制背景 if (pDrawItemInfo->SelFlags & WIDGET_ITEM_SEL_SELECTED) { GUI_SetColor(GUI_BLUE); GUI_FillRectEx(&(pDrawItemInfo->Rect)); // 选中项蓝色背景 } else { GUI_SetColor(GUI_WHITE); GUI_FillRectEx(&(pDrawItemInfo->Rect)); // 未选中项白色背景 } // 2. 获取该项文本 LISTWHEEL_GetItemText(hObj, ItemIndex, buffer, sizeof(buffer)); pText = buffer; // 3. 绘制文本(可以自定义位置、颜色等) GUI_SetColor(GUI_BLACK); GUI_SetTextAlign(GUI_TA_LEFT | GUI_TA_VCENTER); // 在矩形区域内垂直居中绘制文本,并留出左边距画图标 GUI_DispStringInRect(pText, &(pDrawItemInfo->Rect), GUI_TA_LEFT | GUI_TA_VCENTER); // 4. 例如,在左边绘制一个小图标(假设有图标资源) // GUI_DrawBitmap(&_bmIcon, pDrawItemInfo->Rect.x0 + 2, pDrawItemInfo->Rect.y0 + 2); break; case WIDGET_DRAW_OVERLAY: // 在所有项绘制完成后,再绘制覆盖层。常用于绘制固定的指示线。 // 例如,在吸附位置画两条红色横线,形成一个“瞄准框” GUI_SetColor(GUI_RED); int snapY = LISTWHEEL_GetSnapPosition(hObj); GUI_DrawHLine(snapY - 1, pDrawItemInfo->Rect.x0, pDrawItemInfo->Rect.x1); GUI_DrawHLine(snapY + 1, pDrawItemInfo->Rect.x0, pDrawItemInfo->Rect.x1); break; default: // 对于不处理的命令,调用默认的绘制函数,确保基础功能正常 return LISTWHEEL_OwnerDraw(pDrawItemInfo); } return 0; } // 在初始化控件后,设置所有者绘制函数 LISTWHEEL_SetOwnerDraw(hListWheel, _MyOwnerDraw);

所有者绘制功能非常强大,它把最终的视觉表现控制权完全交给了开发者。但能力越大,责任也越大,你需要仔细处理每一项的绘制逻辑,并确保性能。

2.5 事件处理与用户交互

LISTWHEEL控件会向父窗口发送通知消息。最常用的是WM_NOTIFICATION_SEL_CHANGED,当滚动停止,一项被“吸附”选中时触发。在你的窗口回调函数中,可以这样处理:

static void _cbCallback(WM_MESSAGE * pMsg) { switch (pMsg->MsgId) { case WM_NOTIFY_PARENT: { WM_NOTIFY_PARENT_INFO * pInfo = (WM_NOTIFY_PARENT_INFO *)pMsg->Data.p; int Id = WM_GetId(pMsg->hWinSrc); // 获取触发控件的ID int NCode = pInfo->NotificationCode; // 获取通知代码 if (Id == GUI_ID_LISTWHEEL0) { switch (NCode) { case WM_NOTIFICATION_SEL_CHANGED: { // 获取当前选中的索引 int sel = LISTWHEEL_GetSel(pMsg->hWinSrc); // 根据sel执行你的业务逻辑,例如更新其他UI显示 printf(“选中了第 %d 项\n”, sel); break; } case WM_NOTIFICATION_CLICKED: // 控件被点击(按下) break; case WM_NOTIFICATION_RELEASED: // 控件被释放 break; } } break; } // ... 处理其他消息 } }

3. MENU控件:构建层级导航系统

MENU控件用于创建各种菜单,从简单的水平工具栏到复杂的多级弹出式菜单。它的核心数据结构是MENU_ITEM_DATA,每个菜单项都通过这个结构体来定义。

3.1 菜单项数据结构与创建

MENU_ITEM_DATA结构体包含四个关键成员:

  • pText: 指向菜单项显示文本的字符串指针。
  • Id: 菜单项的唯一标识符。当菜单项被选中时,这个ID会通过WM_MENU消息传递给处理函数。强烈建议为所有菜单项(包括不同子菜单中的项)分配唯一的ID,这能极大简化事件处理逻辑。
  • Flags: 标志位,可以组合使用。常用的有:
    • MENU_IF_DISABLED: 禁用该项(灰色显示,不可选)。
    • MENU_IF_SEPARATOR: 该项是一个分隔符(一条横线),用于对菜单项进行分组。
  • hSubmenu: 如果该项是一个子菜单,这里填入子菜单的句柄(MENU_Handle)。如果只是普通命令项,则设为0。

创建菜单通常是自底向上的。先创建最末级的子菜单,然后逐级向上构建。

static MENU_Handle _CreateSubMenu(void) { MENU_Handle hSubMenu; MENU_ITEM_DATA Item; // 创建子菜单(垂直方向) hSubMenu = MENU_CreateEx(0, 0, 0, 0, WM_UNATTACHED, WM_CF_SHOW, MENU_CF_VERTICAL, 0); if (hSubMenu) { // 添加子菜单项 Item.pText = “子项 A”; Item.Id = ID_SUB_ITEM_A; Item.Flags = 0; Item.hSubmenu = 0; MENU_AddItem(hSubMenu, &Item); Item.pText = “子项 B”; Item.Id = ID_SUB_ITEM_B; Item.Flags = 0; Item.hSubmenu = 0; MENU_AddItem(hSubMenu, &Item); // 添加一个分隔符 Item.pText = NULL; // 分隔符文本为空 Item.Id = 0; // ID无关紧要 Item.Flags = MENU_IF_SEPARATOR; Item.hSubmenu = 0; MENU_AddItem(hSubMenu, &Item); Item.pText = “子项 C”; Item.Id = ID_SUB_ITEM_C; Item.Flags = MENU_IF_DISABLED; // 禁用的项 Item.hSubmenu = 0; MENU_AddItem(hSubMenu, &Item); } return hSubMenu; } static MENU_Handle _CreateMainMenu(void) { MENU_Handle hMainMenu; MENU_Handle hSubMenu; MENU_ITEM_DATA Item; // 创建主菜单(水平方向) hMainMenu = MENU_CreateEx(0, 0, LCD_GetXSize(), 30, hDesktop, WM_CF_SHOW, MENU_CF_HORIZONTAL, GUI_ID_MENU0); // 创建文件子菜单 hSubMenu = _CreateFileSubMenu(); // 假设这个函数创建了“文件”子菜单 Item.pText = “文件(F)”; Item.Id = ID_MENU_FILE; Item.Flags = 0; Item.hSubmenu = hSubMenu; // 关联子菜单! MENU_AddItem(hMainMenu, &Item); // 创建编辑子菜单 hSubMenu = _CreateEditSubMenu(); Item.pText = “编辑(E)”; Item.Id = ID_MENU_EDIT; Item.Flags = 0; Item.hSubmenu = hSubMenu; MENU_AddItem(hMainMenu, &Item); // 添加一个普通命令项(无子菜单) Item.pText = “帮助(H)”; Item.Id = ID_MENU_HELP; Item.Flags = 0; Item.hSubmenu = 0; MENU_AddItem(hMainMenu, &Item); return hMainMenu; }

注意MENU_CreateEx的参数xSizeySize。如果设置为0,菜单会根据其内容自动调整大小。对于水平主菜单,我们通常设置一个固定的高度(如30像素),宽度设为0让其自适应。对于弹出式子菜单,通常宽高都设为0,让其根据最长的菜单项文本自动计算尺寸。

3.2 菜单消息处理

MENU控件通过发送WM_MENU消息与其“所有者”(Owner)窗口通信。所有者窗口可以通过MENU_SetOwner指定,默认为其父窗口。消息数据是一个指向MENU_MSG_DATA结构的指针,其中MsgTypeItemId是关键。

void _HandleMenuMessage(WM_MESSAGE * pMsg) { MENU_MSG_DATA * pData; if (pMsg->MsgId == WM_MENU) { pData = (MENU_MSG_DATA *)pMsg->Data.p; switch (pData->MsgType) { case MENU_ON_INITMENU: // 菜单即将显示。这是一个绝佳的时机来动态更新菜单状态! // 例如,根据当前应用状态,启用或禁用某些菜单项。 if (g_bFileOpened) { MENU_EnableItem(pMsg->hWinSrc, ID_MENU_SAVE); } else { MENU_DisableItem(pMsg->hWinSrc, ID_MENU_SAVE); } break; case MENU_ON_ITEMSELECT: // 用户最终选择了一个菜单项(非子菜单项) switch (pData->ItemId) { case ID_MENU_NEW: _OnMenuNew(); break; case ID_MENU_OPEN: _OnMenuOpen(); break; case ID_MENU_HELP: _OnMenuHelp(); break; // ... 处理其他ID } break; case MENU_ON_ITEMACTIVATE: // 鼠标或键盘焦点移动到了一个菜单项上(高亮显示时) // 可以用于实现状态栏提示 _UpdateStatusBarHint(pData->ItemId); break; } } }

MENU_ON_INITMENU消息非常有用,它允许你在菜单每次弹出前,根据运行时上下文动态修改菜单项(如禁用/启用、修改文本),实现上下文敏感菜单。

3.3 弹出式菜单(Popup Menu)

除了附着在窗口上的菜单栏,MENU控件另一个重要用途是创建弹出式菜单。这需要使用MENU_Popup函数。

static void _ShowPopupMenu(int x, int y) { MENU_Handle hPopup; MENU_ITEM_DATA Item; // 创建一个垂直菜单作为弹出菜单 hPopup = MENU_CreateEx(0, 0, 0, 0, WM_UNATTACHED, WM_CF_SHOW, MENU_CF_VERTICAL, 0); if (!hPopup) return; // 设置弹出菜单的所有者,用于接收WM_MENU消息 MENU_SetOwner(hPopup, hMainWindow); // 添加弹出菜单项 Item.pText = “复制”; Item.Id = ID_POPUP_COPY; Item.Flags = 0; Item.hSubmenu = 0; MENU_AddItem(hPopup, &Item); Item.pText = “粘贴”; Item.Id = ID_POPUP_PASTE; Item.Flags = g_bClipboardEmpty ? MENU_IF_DISABLED : 0; // 根据剪贴板状态禁用 Item.hSubmenu = 0; MENU_AddItem(hPopup, &Item); // 在指定坐标弹出菜单 // 注意:坐标是相对于hDestWin参数指定的窗口。这里我们用桌面窗口。 MENU_Popup(hPopup, WM_HBKWIN, x, y, 0, 0, 0); // 重要:MENU_Popup是异步的,菜单显示后函数立即返回。 // 菜单窗口的生命周期需要管理。通常,在收到该弹出菜单的MENU_ON_ITEMSELECT消息后, // 或者在其他地方检测到菜单应关闭时(如点击别处),需要手动删除菜单窗口(WM_DeleteWindow)。 // 一种常见做法是在所有者窗口的WM_MENU消息处理中,判断ItemId来自弹出菜单,然后删除它。 }

弹出菜单的坐标(x, y)通常是鼠标点击或长按事件的位置。MENU_Popup会接管后续的用户交互,并在选择完成或点击外部后自动关闭菜单(但不会自动删除窗口对象)。你需要在适当的时机(例如在对应WM_MENU消息处理完后)调用WM_DeleteWindow(hPopup)来释放资源。

3.4 视觉样式与自定义

与LISTWHEEL类似,MENU也支持丰富的样式定制。

颜色设置:通过MENU_SetBkColorMENU_SetTextColor,可以分别设置不同状态下的背景色和文字颜色。ColorIndex参数用于指定状态:

  • MENU_CI_ENABLED: 启用未选中
  • MENU_CI_SELECTED: 启用且选中(高亮)
  • MENU_CI_DISABLED: 禁用
  • MENU_CI_DISABLED_SEL: 禁用但被选中(某些场景下)
  • MENU_CI_ACTIVE_SUBMENU: 激活的子菜单项背景色

边框设置MENU_SetBorderSize可以调整菜单项文本与边缘的间距,让布局更美观。

字体设置MENU_SetFont可以更改菜单字体。

默认值:通过MENU_SetDefaultBkColorMENU_SetDefaultFont等函数设置的默认值,会影响之后创建的所有MENU控件,是实现全局主题统一的高效方法。

4. 实战经验与避坑指南

经过多个项目的打磨,我总结了一些关于LISTWHEEL和MENU控件的实战经验和常见问题。

4.1 LISTWHEEL的“手感”调优

LISTWHEEL的滚动体验是门艺术。除了之前提到的Deceleration(减速系数),LISTWHEEL_SetVelocity函数也很有用。你可以通过程序模拟一个初始速度,让列表自动滚动起来。Velocity参数的单位是像素/定时器周期。正值向下滚动,负值向上滚动。这个功能可以用来实现“快速滚动”或“自动浏览”。

另一个关键是LISTWHEEL_SetLineHeight。默认行高由字体决定。但在使用OwnerDraw绘制复杂内容(如图标+文字)时,你需要手动设置一个更大的行高,否则内容会被裁剪。计算好内容所需高度,并留出一些边距。

常见坑点

  • 内存泄漏:使用LISTWHEEL_SetTextMENU_AddItem时,传入的字符串数组必须是持久存在的(全局变量、静态变量或动态分配的内存)。如果传入局部变量的地址,函数返回后该内存失效,会导致显示乱码或崩溃。
  • 消息循环阻塞:LISTWHEEL的滚动动画依赖于emWin的定时器消息(WM_TIMER)。如果你的主任务或回调函数中有长时间的阻塞操作(如GUI_Delay或忙等待),动画会卡住。务必保持消息循环的畅通。
  • 触摸校准:LISTWHEEL对触摸滑动非常敏感。如果设备的触摸屏校准不准,滑动操作可能会不跟手或误触发。确保触摸驱动和校准参数正确。

4.2 MENU的焦点与键盘导航

MENU控件支持完整的键盘导航(方向键、Enter、Esc)。但这需要窗口管理器将键盘输入焦点正确地设置到菜单控件上。通常,当你点击一个水平菜单项弹出子菜单时,焦点会自动转移。但如果你通过编程方式(如按下一个硬件按钮)打开菜单,可能需要手动调用WM_SetFocus来设置焦点。

在嵌套子菜单时,要特别注意MENU_SetOwner的调用。通常,所有子菜单应该将所有者设置为同一个顶层窗口(如主窗口),这样所有的WM_MENU消息都会发往同一个回调函数集中处理,逻辑更清晰。

动态菜单更新:在MENU_ON_INITMENU消息中更新菜单状态是标准做法。但注意,不要在此消息中执行耗时操作,否则会明显延迟菜单的弹出。对于需要从外部设备(如SD卡)读取数据来构建菜单项的情况,建议在后台线程准备好数据,在此消息中仅进行快速的启用/禁用或文本替换操作。

4.3 性能优化技巧

  1. 避免频繁重绘:无论是LISTWHEEL还是MENU,频繁调用WM_InvalidateWindow或直接修改控件属性触发重绘,在低性能MCU上都会导致界面卡顿。对于LISTWHEEL,批量更新内容使用SetText而非多次AddString。对于MENU,在MENU_ON_INITMENU中一次性完成所有状态更新。
  2. 使用内存设备(Memory Device):如果LISTWHEEL的滚动区域较大,或者MENU菜单项绘制复杂(如带渐变背景),开启内存设备(WM_SetCreateFlags(WM_CF_MEMDEV))可以极大地消除闪烁,提升滚动和弹出动画的平滑度。这是emWin中提升视觉流畅度的“王牌”功能。
  3. 精简OwnerDraw回调:OwnerDraw回调函数会在每次重绘时被频繁调用。确保其中的代码尽可能高效。避免在回调中进行复杂的计算或资源加载(如从文件系统解码图片)。应该将这些数据提前准备好(如解码为位图资源存储在RAM或Flash中)。

4.4 调试与问题排查

  • 控件不显示:首先检查创建函数(CreateEx)的返回值是否为0(失败)。常见原因是父窗口句柄无效,或者内存不足。确保在调用任何GUI函数之前,已经正确初始化了emWin库(GUI_Init)。
  • 触摸/点击无反应:检查控件是否被其他窗口覆盖(Z序问题)。确保控件的窗口回调函数正确传递了消息给默认处理函数(如LISTWHEEL_CallbackMENU_Callback)。对于MENU,检查MENU_SetOwner设置是否正确,以及所有者窗口是否处理了WM_MENU消息。
  • 文本显示乱码:99%的情况是字符串编码问题。emWin内部使用ASCII或UTF-8(取决于配置)。确保你的字符串常量编码与库配置一致。对于中文等宽字符,必须启用UTF-8支持并配置相应的字体。
  • 内存增长:反复创建和删除菜单(特别是弹出菜单)而不调用WM_DeleteWindow会导致内存泄漏。使用emWin提供的调试工具(如GUI_GetNumUsedBytes())定期检查内存使用情况。

最后,emWin的官方示例代码(Sample文件夹)是无价的宝藏。WIDGET_ListWheel.cApplication\Reversi.c(内含菜单使用)是两个极佳的学习起点。不要只看手册,一定要实际运行、修改这些例子,观察效果,这是掌握这两个强大控件最快的方式。