尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MFC列表框控件完全指南:从数据绑定到事件处理的实战解析

MFC列表框控件完全指南:从数据绑定到事件处理的实战解析 如果你在开发一个桌面应用需要让用户从多个选项中选择你会用什么控件单选按钮复选框还是下拉列表这些都能用但当选项数量超过10个或者需要展示详细信息时你会发现它们都不够优雅。用户需要来回滚动、反复点击体验大打折扣。这时一个功能强大的列表框ListBox就该登场了。它不仅能清晰展示大量条目还支持多选、搜索、自定义渲染等高级功能。但很多开发者尤其是初学者对列表框的使用停留在“拖一个控件绑定数据”的层面结果就是程序运行起来后列表是空的或者选中了却不知道如何获取值调试起来一头雾水。这篇文章要解决的核心问题就是如何正确地“激活”一个列表框控件。这不仅仅是关联一个变量那么简单而是一个从界面设计到数据绑定再到事件处理的完整流程。很多人以为在属性面板里设置一下就行了实际上真正的“坑”往往藏在变量类型、数据更新机制和消息映射这些细节里。本文将基于经典的 MFCMicrosoft Foundation Classes框架但其中的设计思想和排查思路对 WPF、WinForms、Qt 乃至 Web 前端开发都有借鉴意义。读完本文你将彻底掌握列表框的核心属性哪些属性是必须设置的哪些能显著提升用户体验。变量关联的两种方式值变量与控件变量它们有何本质区别在什么场景下会“失灵”。动态操作列表框如何通过代码添加、删除、选中条目以及如何响应双击等事件。数据同步的陷阱为什么界面上选了代码里却读不到UpdateData这个函数到底该什么时候调用一套可复用的调试清单当列表框不显示、不响应、数据错乱时按照什么顺序排查。我们从一个最常见的失败案例开始。1. 为什么你的列表框总是“空”的从两个典型误区说起假设你在 Visual Studio 的对话框编辑器中拖入了一个ListBox控件ID 设为IDC_LIST_DATA。然后你通过类向导Class Wizard为它关联了一个控件变量m_listCtrl。满心欢喜地运行程序却发现列表框里空空如也。误区一以为关联变量后数据会自动出现。这是最大的误解。为列表框关联变量无论是值变量CString/int还是控件变量CListBox只是建立了一个编程层面访问该控件的通道。这个通道本身并不包含任何要显示的数据。数据需要你主动“喂”给列表框通常是在对话框初始化如OnInitDialog阶段。误区二混淆了“控件变量”和“值变量”。在 MFC 的类向导中为控件关联变量时有两种选择控件变量Control Variable类型是CListBox或对应控件的 MFC 类。它让你能直接调用该控件类的所有方法例如AddString,GetCurSel。值变量Value Variable类型通常是CString用于获取选中项的文本或int用于获取选中项的索引。它主要用于方便地通过UpdateData函数在控件和对话框成员变量之间交换数据。很多新手会同时为同一个控件创建两个变量或者错误地使用了值变量来尝试调用AddString方法这必然会导致编译错误或运行时逻辑错误。真正的第一步是理解关联变量是“连接”设置属性是“配置”而填充数据是“执行”。三者缺一不可。2. 核心概念列表框的两种变量与数据管理模型2.1 控件变量 (CListBox)你的操作手柄当你需要一个“手柄”来直接操纵列表框时就使用控件变量。通过它你可以调用 MFCCListBox类提供的丰富接口。// 假设 m_listData 是关联到 IDC_LIST_DATA 的 CListBox 类型控件变量 m_listData.AddString(_T(北京)); // 添加一项 m_listData.AddString(_T(上海)); int nIndex m_listData.GetCurSel(); // 获取当前选中项的索引 if (nIndex ! LB_ERR) { CString strCity; m_listData.GetText(nIndex, strCity); // 根据索引获取文本 AfxMessageBox(strCity); }关键点控件变量m_listData直接代表了屏幕上的那个列表框对象。你对它的操作会实时反映在界面上。2.2 值变量 (CString/int)数据的快捷通道值变量主要用于简化对话框数据交换DDX。当你的对话框上有“确定”按钮点击后需要收集所有控件的当前状态时值变量非常方便。// 在对话框类的头文件中声明 class CMyDialog : public CDialogEx { // ... public: CString m_strSelectedCity; // 值变量关联到 IDC_LIST_DATA CListBox m_listData; // 控件变量关联到同一个 IDC_LIST_DATA (通常二选一) }; // 在对话框的 DoDataExchange 函数中可以看到 MFC 自动生成的绑定代码 void CMyDialog::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_LIST_DATA, m_listData); // 控件变量绑定 DDX_LBString(pDX, IDC_LIST_DATA, m_strSelectedCity); // 值变量绑定 (获取选中项文本) // 或者使用 DDX_LBIndex 来绑定选中索引 // DDX_LBIndex(pDX, IDC_LIST_DATA, m_nSelectedIndex); }重要限制值变量m_strSelectedCity在通过UpdateData(TRUE)从控件获取数据时它获取的是当前选中项的文本。如果列表框允许多选LBS_MULTIPLESEL或者没有选中项这种绑定方式就可能无法满足需求。此时你必须使用控件变量m_listData来编程处理。2.3 数据从哪来理解列表框的数据源列表框自身不存储业务数据它只是一个“视图”。你的程序需要维护一个真实的数据源如一个std::vectorCityInfo然后在合适的时机初始化、数据变更后将这个数据源同步到列表框控件中。这就是典型的 MVCModel-View-Controller思想的简化体现。一个清晰的编程模型Model (模型)你的业务数据集合例如std::listCString m_dataList;。View (视图)屏幕上的ListBox控件通过CListBox m_listCtrl变量操作。同步在OnInitDialog中遍历m_dataList调用m_listCtrl.AddString将每个条目添加到视图。交互用户操作视图点击、双击通过控件变量m_listCtrl获取操作结果如选中索引再根据这个索引去m_dataList中查找对应的业务数据。3. 环境准备与属性设置让列表框按你的想法呈现在编写代码前我们需要在资源编辑器中正确设置列表框的属性。这些属性决定了它的外观和行为。3.1 必须设置的关键属性在属性窗口的 “Styles” 选项卡下属性名可选值说明与建议SelectionSingle / Multiple / Extended / NoneSingle默认单选。Multiple用单击切换多项选中。Extended支持 Shift/Ctrl 进行多选行为更符合现代习惯。根据需求选择。Owner drawNo / Fixed / Variable默认为No。如果你需要自定义每一项的绘制比如改变颜色、添加图标则需要选择Fixed等高或Variable不等高并重写OnDrawItem等函数。新手建议保持No。Has stringsTrue / False必须为True。这告诉列表框你添加的每一项都是一个字符串。如果设置为False你需要处理更复杂的自绘逻辑。SortTrue / False如果为True列表框会自动按字母顺序排序你添加的字符串。注意这可能会打乱你添加的顺序如果你的列表有内在顺序如优先级务必设为False。Vertical scrollTrue / False通常保持True显示垂直滚动条。No integral heightTrue / False默认为False。如果为True系统会精确采用你设定的高度可能导致最后一项显示不全。通常保持False让系统自动调整到能完整显示最后一项的高度。3.2 一个推荐的初始化配置流程在对话框编辑器中拖入ListBox控件。调整其大小和位置。右键点击控件选择 “Properties”。在 “General” 选项卡修改ID为一个有意义的名称如IDC_LIST_CITY。切换到 “Styles” 选项卡Selection:Single(假设单选)Owner draw:NoHas strings:True(确认已勾选)Sort:False(我们保持添加顺序)其他保持默认。保存资源文件。4. 核心流程拆解从关联变量到功能实现现在我们进入代码部分。假设我们要实现一个城市选择器。4.1 第一步关联控件变量在对话框编辑器中右键点击列表框控件选择 “Add Variable...”。在 “Add Member Variable Wizard” 对话框中Access: 选择public或protected视你的类设计而定。Variable Type: 选择CListBox。Variable Name: 输入m_listCity。Control ID: 确认是IDC_LIST_CITY。取消勾选 “Control variable” 下方的 “Value variable” 相关选项我们暂时只添加控件变量。点击 “Finish”。向导会在你的对话框类头文件中添加CListBox m_listCity;声明并在.cpp文件的DoDataExchange中生成DDX_Control绑定代码。4.2 第二步在对话框初始化时填充数据数据填充应在OnInitDialog函数中进行。确保在调用了基类的OnInitDialog()之后再添加你的代码。// 文件CitySelectorDlg.cpp BOOL CCitySelectorDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 1. 先调用基类初始化 // 2. 初始化你的业务数据模型 (这里简化为例) // 在实际项目中数据可能来自数据库、文件、网络等。 std::vectorCString cityNames {_T(北京), _T(上海), _T(广州), _T(深圳), _T(杭州), _T(南京)}; // 3. 将数据同步到列表框视图 for (const auto city : cityNames) { m_listCity.AddString(city); // 使用控件变量添加条目 } // 4. (可选) 设置默认选中项例如第一项 if (m_listCity.GetCount() 0) { m_listCity.SetCurSel(0); } return TRUE; // 除非你将焦点设置到某个控件否则返回 TRUE }4.3 第三步响应用户选择获取选中值用户选择了列表框中的一项你如何知道有两种主要方式方式一在某个按钮事件中获取例如“确定”按钮// “确定”按钮的点击事件处理函数 void CCitySelectorDlg::OnBnClickedOk() { // 获取当前选中项的索引 int nSelectedIndex m_listCity.GetCurSel(); if (nSelectedIndex ! LB_ERR) { // LB_ERR 表示没有选中项 CString strSelectedCity; m_listCity.GetText(nSelectedIndex, strSelectedCity); // 根据索引获取文本 // 现在你可以使用 strSelectedCity 了 AfxMessageBox(_T(你选择的城市是) strSelectedCity); // ... 其他业务逻辑 } else { AfxMessageBox(_T(请选择一个城市)); return; // 不关闭对话框 } CDialogEx::OnOK(); // 关闭对话框 }方式二实时响应选择变化添加 LBN_SELCHANGE 消息处理有时你希望用户一选择程序就立即做出反应比如在另一个区域显示详细信息。在对话框编辑器中右键点击列表框控件选择 “Add Event Handler”。在消息类型中选择LBN_SELCHANGE点击 “Add and Edit”。Visual Studio 会自动生成消息映射和函数框架。// 头文件中声明 afx_msg void OnLbnSelchangeListCity(); // 消息处理函数声明 // CPP 文件中的消息映射 BEGIN_MESSAGE_MAP(CCitySelectorDlg, CDialogEx) ON_LBN_SELCHANGE(IDC_LIST_CITY, CCitySelectorDlg::OnLbnSelchangeListCity) END_MESSAGE_MAP() // 消息处理函数实现 void CCitySelectorDlg::OnLbnSelchangeListCity() { int nSelectedIndex m_listCity.GetCurSel(); if (nSelectedIndex ! LB_ERR) { CString strSelectedCity; m_listCity.GetText(nSelectedIndex, strSelectedCity); // 立即更新界面其他部分例如在一个静态文本(Static Text)上显示 SetDlgItemText(IDC_STATIC_SELECTED_INFO, _T(当前选中) strSelectedCity); // 或者启用/禁用某些按钮 GetDlgItem(IDC_BUTTON_CONFIRM)-EnableWindow(TRUE); } }4.4 第四步处理多选列表框如果属性Selection设置为Multiple或Extended获取选中项的方法会有所不同。你需要使用GetSelItems。// 假设列表框支持多选获取所有选中项 int nSelectedCount m_listCity.GetSelCount(); // 获取选中项总数 if (nSelectedCount 0) { std::vectorint selectedIndices(nSelectedCount); m_listCity.GetSelItems(nSelectedCount, selectedIndices.data()); // 获取所有选中项的索引 CString strInfo _T(选中的城市); for (int idx : selectedIndices) { CString strCity; m_listCity.GetText(idx, strCity); strInfo strCity _T(; ); } AfxMessageBox(strInfo); }5. 完整示例一个简易城市管理对话框让我们整合以上步骤创建一个功能更完整的对话框。这个对话框展示城市列表支持添加、删除和选择城市。5.1 界面设计一个列表框IDC_LIST_CITY。一个编辑框IDC_EDIT_CITY_NAME用于输入新城市名。三个按钮IDC_BUTTON_ADD添加城市IDC_BUTTON_DELETE删除选中城市IDOK确定传统“确定”按钮一个静态文本IDC_STATIC_INFO用于显示信息。5.2 关键代码实现// 文件CityManagerDlg.h #pragma once #include vector #include string class CCityManagerDlg : public CDialogEx { DECLARE_DYNAMIC(CCityManagerDlg) public: CCityManagerDlg(CWnd* pParent nullptr); virtual ~CCityManagerDlg(); // 对话框数据 #ifdef AFX_DESIGN_TIME enum { IDD IDD_CITYMANAGER_DIALOG }; #endif protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 virtual BOOL OnInitDialog(); // 消息处理函数 afx_msg void OnBnClickedButtonAdd(); afx_msg void OnBnClickedButtonDelete(); afx_msg void OnLbnSelchangeListCity(); DECLARE_MESSAGE_MAP() private: // 控件变量 CListBox m_listCity; CEdit m_editCityName; CStatic m_staticInfo; // 业务数据模型 (与列表框同步) std::vectorCString m_cityData; // 同步函数将 m_cityData 刷新到列表框 void RefreshListBox(); };// 文件CityManagerDlg.cpp #include pch.h #include framework.h #include YourProject.h #include CityManagerDlg.h #include afxdialogex.h IMPLEMENT_DYNAMIC(CCityManagerDlg, CDialogEx) CCityManagerDlg::CCityManagerDlg(CWnd* pParent /*nullptr*/) : CDialogEx(IDD_CITYMANAGER_DIALOG, pParent) { // 初始化一些默认数据 m_cityData { _T(北京), _T(上海), _T(广州) }; } CCityManagerDlg::~CCityManagerDlg() {} void CCityManagerDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_LIST_CITY, m_listCity); DDX_Control(pDX, IDC_EDIT_CITY_NAME, m_editCityName); DDX_Control(pDX, IDC_STATIC_INFO, m_staticInfo); } BEGIN_MESSAGE_MAP(CCityManagerDlg, CDialogEx) ON_BN_CLICKED(IDC_BUTTON_ADD, CCityManagerDlg::OnBnClickedButtonAdd) ON_BN_CLICKED(IDC_BUTTON_DELETE, CCityManagerDlg::OnBnClickedButtonDelete) ON_LBN_SELCHANGE(IDC_LIST_CITY, CCityManagerDlg::OnLbnSelchangeListCity) END_MESSAGE_MAP() BOOL CCityManagerDlg::OnInitDialog() { CDialogEx::OnInitDialog(); RefreshListBox(); // 初始化时同步数据到列表框 return TRUE; } void CCityManagerDlg::RefreshListBox() { m_listCity.ResetContent(); // 清空列表框现有内容 for (const auto city : m_cityData) { m_listCity.AddString(city); } // 清空信息显示 m_staticInfo.SetWindowText(_T(就绪。)); } void CCityManagerDlg::OnBnClickedButtonAdd() { CString strNewCity; m_editCityName.GetWindowText(strNewCity); // 从编辑框获取文本 strNewCity.Trim(); // 去除首尾空格 if (strNewCity.IsEmpty()) { AfxMessageBox(_T(城市名不能为空)); m_editCityName.SetFocus(); return; } // 检查是否已存在 (可选) for (const auto city : m_cityData) { if (city.CompareNoCase(strNewCity) 0) { AfxMessageBox(_T(该城市已存在)); return; } } // 更新业务数据模型 m_cityData.push_back(strNewCity); // 同步到视图 RefreshListBox(); // 清空编辑框并聚焦 m_editCityName.SetWindowText(_T()); m_editCityName.SetFocus(); m_staticInfo.SetWindowText(_T(已添加城市) strNewCity); } void CCityManagerDlg::OnBnClickedButtonDelete() { int nSel m_listCity.GetCurSel(); if (nSel LB_ERR) { AfxMessageBox(_T(请先选择一个要删除的城市)); return; } CString strCityToDelete; m_listCity.GetText(nSel, strCityToDelete); // 从业务数据模型中删除 auto it std::find(m_cityData.begin(), m_cityData.end(), strCityToDelete); if (it ! m_cityData.end()) { m_cityData.erase(it); } // 同步到视图 RefreshListBox(); m_staticInfo.SetWindowText(_T(已删除城市) strCityToDelete); } void CCityManagerDlg::OnLbnSelchangeListCity() { int nSel m_listCity.GetCurSel(); if (nSel ! LB_ERR) { CString strSelectedCity; m_listCity.GetText(nSel, strSelectedCity); CString strInfo; strInfo.Format(_T(当前选中%s (索引%d)), strSelectedCity, nSel); m_staticInfo.SetWindowText(strInfo); // 可以同时更新编辑框方便修改 m_editCityName.SetWindowText(strSelectedCity); } }6. 运行结果与效果验证编译并运行上述程序你将看到一个对话框初始状态列表框中显示“北京”、“上海”、“广州”。选择项点击任一城市底部的信息栏会实时显示选中城市和索引。添加功能在编辑框输入“深圳”点击“添加”按钮。列表框中会新增“深圳”编辑框清空。删除功能在列表框中选中“上海”点击“删除”按钮。“上海”将从列表框中移除。数据同步验证反复添加、删除后关闭对话框并重新打开列表会恢复为初始的三个城市因为我们的数据模型m_cityData在构造函数中初始化。在实际项目中你会将m_cityData保存到文件或数据库实现持久化。如何验证绑定和逻辑正确断言与调试在OnInitDialog的RefreshListBox()后可以添加ASSERT(m_listCity.GetCount() 3);来验证初始条目数。检查变量在删除按钮的处理函数中设置断点观察nSel和strCityToDelete的值是否与你的点击一致。测试边界尝试删除时未选中任何项程序应弹出提示框而不是崩溃。7. 常见问题与排查思路当你遇到列表框相关问题时可以按以下清单排查问题现象可能原因排查方式解决方案列表框显示为空1. 未在OnInitDialog中调用AddString。2.OnInitDialog中代码未执行如对话框未正常初始化。3. 控件变量未正确关联DoDataExchange中无DDX_Control。1. 在OnInitDialog开始处设断点。2. 检查DoDataExchange函数。3. 检查类向导中变量是否成功添加。1. 确保在OnInitDialog中调用了填充数据的代码。2. 确认控件变量类型正确且已绑定。编译错误m_listCtrl未声明的标识符1. 头文件中未声明该变量。2. 变量名拼写错误。1. 检查对话框类头文件 (.h) 中是否有CListBox m_listCtrl;声明。2. 检查.cpp文件中DoDataExchange内的变量名是否一致。1. 通过类向导重新添加变量或手动补全声明和DDX_Control绑定。运行时点击列表框无反应1. 未添加LBN_SELCHANGE等消息处理函数。2. 消息映射宏 (ON_LBN_SELCHANGE) 缺失或 ID 不匹配。1. 在类视图检查是否有对应的消息处理函数。2. 检查.cpp文件中的BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间是否有对应的条目。1. 通过右键控件“添加事件处理程序”来添加。2. 手动检查并修正消息映射。GetCurSel()总是返回 -1 (LB_ERR)1. 确实没有选中项。2. 列表框属性Selection被设为None。3. 在多选模式下使用了GetCurSel它只用于单选。1. 运行程序观察是否有默认选中项。2. 检查控件属性。3. 判断当前是否为多选模式。1. 使用SetCurSel(0)设置默认选中。2. 修正控件属性。3. 多选时使用GetSelCount和GetSelItems。通过值变量 (UpdateData) 获取不到数据1. 在调用UpdateData(TRUE)前用户未做选择。2. 允许多选时值变量绑定方式不适用。3.DoDataExchange中值变量绑定代码被误删。1. 确保在调用UpdateData前界面已有选中项。2. 检查列表框是否允许多选。3. 检查DoDataExchange中的DDX_LBString或DDX_LBIndex。1. 对于不确定的选择优先使用控件变量方法 (GetCurSel)。2. 多选场景必须使用控件变量编程处理。3. 恢复或修正DoDataExchange中的绑定。添加/删除条目后界面未更新1. 只更新了业务数据模型 (m_cityData)未调用RefreshListBox同步到视图。2.RefreshListBox中ResetContent和AddString调用有误。1. 检查修改数据后是否调用了刷新列表框的函数。2. 在RefreshListBox中设断点看是否执行。1. 确保任何对数据模型的修改都伴随一次视图同步。2. 封装好RefreshListBox函数确保逻辑正确。自绘列表框显示异常1.Owner Draw属性设置后未重写OnDrawItem、OnMeasureItem等虚函数。2. 自绘逻辑计算错误如高度、宽度。1. 确认已重写必要的自绘函数。2. 在自绘函数中设置断点检查绘图参数。1. 对于复杂自绘建议先从 MFC 示例开始或使用第三方控件库。2. 仔细计算每一项的绘制区域。8. 最佳实践与工程建议掌握了基础操作后以下建议能让你的代码更健壮、更易维护分离数据与视图始终坚持本文提到的 Model-View 思路。业务数据 (std::vector,CStringArray等) 和控件 (CListBox) 分开管理。视图只是数据的投影。封装刷新逻辑像示例中那样将ResetContent和循环AddString封装成RefreshListBox或UpdateView函数。数据变更后调用此函数即可避免更新代码散落各处。慎用Sort属性除非你明确需要按字符串排序否则将其设为False。排序会打乱你添加的顺序这可能与你的业务逻辑如优先级、ID顺序冲突。为列表框项关联数据有时你需要在列表中显示友好文本如城市名但背后需要关联一个 ID如城市代码。可以使用CListBox::SetItemData和GetItemData。在AddString后获取该项的索引然后调用SetItemData关联一个DWORD_PTR数据。int nIndex m_listCity.AddString(_T(北京)); m_listCity.SetItemData(nIndex, (DWORD_PTR)110000); // 假设110000是北京的城市代码 // 获取时 int nSel m_listCity.GetCurSel(); if (nSel ! LB_ERR) { DWORD_PTR dwCityCode m_listCity.GetItemData(nSel); // 使用 dwCityCode }考虑使用虚拟列表LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT并设置SetItemCount当列表项数量极大如超过1000条时使用虚拟列表可以大幅提升性能。它只在需要显示时才请求数据但实现更复杂。线程安全如果你在后台线程中更新数据模型然后需要刷新列表框绝对不能直接从后台线程调用AddString等控件方法。必须通过PostMessage或SendMessage将消息抛到主UI线程由主线程执行界面更新。资源清理对于非常动态的列表如果关联了大量ItemData在对话框关闭或列表框重置前需要注意是否有需要手动释放的资源。9. 总结与扩展为控件关联变量并设置属性只是 MFC 或类似桌面框架编程的起点。列表框作为一个基础但强大的控件其核心在于理解“数据模型”与“视图控件”的分离与同步机制。本文通过一个城市管理器的完整案例拆解了从属性设置、变量关联、数据填充、事件响应到数据同步的全过程。关键在于控件变量 (CListBox)是你操作列表框的工具。值变量 (CString/int)是配合UpdateData进行数据交换的快捷方式但在动态交互中往往不够灵活。真正的数据应保存在你的业务模型成员变量、容器类中列表框只是它的一个显示窗口。任何对模型的修改都需要主动通知视图更新。当你熟练掌握了列表框这种 Model-View 的思维模式可以无缝迁移到列表控件 (CListCtrl)、树控件 (CTreeCtrl)乃至现代 UI 框架如 Qt 的QListView、WPF 的ListBox或 Web 前端的数据绑定中。它们的本质都是将数据与呈现分离通过一个控制器来同步。下一步你可以尝试实现带图标的列表框研究Owner Draw或使用CImageList。实现可编辑的列表项参考CListCtrl的编辑标签功能或自己处理LBN_DBLCLK消息弹出编辑框。探索更现代的替代方案如果你的项目允许了解CListCtrl报告视图、图标视图或直接使用CMFCListCtrl等 MFC 功能包控件它们提供了更丰富的功能。记住控件是死的数据和交互逻辑才是活的。把基础打牢复杂的界面交互不过是这些基础模式的组合与延伸。建议将本文中的示例代码运行一遍并尝试修改、扩展功能这是理解所有 UI 控件编程的最佳路径。
返回列表