ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C# WinForm RichTextBox文本编辑器开发实战:从入门到高级功能实现

C# WinForm RichTextBox文本编辑器开发实战:从入门到高级功能实现 简介在桌面应用开发领域WinForm作为经典的.NET框架技术以其稳定性和快速开发能力在工控上位机、内部工具等场景中依然广泛应用。其事件驱动模型和丰富的控件库为开发者提供了高效的开发路径。RichTextBox作为WinForm中的富文本编辑控件内置了字体、颜色、段落等格式化功能是实现文本处理功能的核心组件。通过结合文件流操作、剪贴板交互和编码处理等技术可以构建出功能实用、性能可靠的文本编辑器。这种技术组合特别适合需要快速上马、对性能要求不高的工具开发例如日志查看、参数配置等场景。本文聚焦于利用WinForm和RichTextBox从零开始构建一个功能完备的编辑器并深入探讨了在工控等特定领域为何WinForm的轻量级和稳定性使其成为不可替代的选择同时分享了处理大文件、实现语法高亮等高级功能的工程实践。1. 项目概述为什么从WinForm和RichTextBox开始如果你刚接触C#桌面开发或者手头有一个需要快速上马、功能实用且对性能要求不高的文本处理工具需求那么用WinForm搭配RichTextBox控件来打造一个文本编辑器绝对是一个经典且高效的起点。这听起来可能有点“老派”毕竟现在WPF、WinUI甚至跨平台的Avalonia、MAUI听起来更时髦。但我想说的是在大量内部工具、工控上位机、数据录入和轻量级报表生成场景里WinForm的稳定、简单和快速开发特性让它依然拥有不可替代的生命力。而RichTextBox作为.NET Framework/WinForms里功能最强大的富文本编辑控件它内置了字体、颜色、段落对齐、项目符号等丰富的格式化功能相当于为你提供了一个现成的、功能强大的编辑内核。我选择这个组合核心原因就三个字快、稳、够用。快指的是从零到一搭建出可用的编辑器原型可能只需要一两个小时。稳指的是WinForm技术栈历经近二十年沉淀几乎不存在兼容性和诡异的运行时问题在Windows环境下极其可靠。够用意味着对于绝大多数非专业文字处理比如程序员看日志、运营编辑通知、工程师配置参数的需求RichTextBox提供的功能已经绰绰有余你不需要再去集成一个庞大的第三方编辑库。这个项目看似简单但深入下去你会涉及到WinForm的事件驱动模型、控件的深入定制、文件流的操作、剪贴板交互甚至是简单的插件化思想。它不仅是完成一个编辑器更是一次对经典桌面开发技术的系统性实践。接下来我会带你从零开始一步步构建一个功能完备的编辑器并分享那些官方文档里不会写的“踩坑”经验和性能优化技巧。2. 核心思路与整体架构设计2.1 技术选型背后的逻辑为什么不是WPF或Web看到网络热词里有“工控WPF为何替代不了WinForm”的讨论这正好点明了WinForm在特定领域的优势。WPF基于DirectX在UI动画、数据绑定和复杂视觉效果上更强但其学习曲线更陡峭运行时开销也更大。对于工控、医疗仪器等需要长时间稳定运行、对界面刷新效率和资源占用敏感的领域WinForm基于GDI的轻量级绘制和简单直接的事件处理机制反而成了优势。我们的文本编辑器项目核心是文本的增删改查和基础格式化不需要复杂的动画和模板WinForm的简单直接正好匹配。RichTextBox是System.Windows.Forms命名空间下的“老兵”。它直接封装了Windows系统的RichEdit控件这意味着它的文本渲染和编辑能力是与系统级组件绑定的稳定性和性能有保障。选择它而不是从头开始用Panel和Graphics去画文本是站在了巨人的肩膀上避免了底层文本布局、光标处理、输入法集成等一系列复杂问题。2.2 编辑器功能模块规划一个实用的文本编辑器远不止一个可以打字的框。我们需要系统性地规划功能模块核心编辑区RichTextBox控件本身负责所有文本的显示与交互。菜单栏提供“文件”、“编辑”、“格式”、“视图”、“帮助”等顶级功能入口。这是桌面应用的标准范式用户认知成本低。工具栏将最常用的功能如新建、打开、保存、剪切、复制、粘贴、字体加粗、斜体以图标按钮形式快速访问提升效率。状态栏实时显示当前编辑状态如光标位置行号、列号、总字符数、编码格式、是否只读等给予用户明确反馈。文件操作模块负责文件的打开、保存、另存为涉及编码检测与转换如ANSI, UTF-8, Unicode。编辑功能模块实现撤销、重做、查找、替换、全选等。格式功能模块控制选中文本的字体、字号、颜色、对齐方式左、中、右、项目符号列表等。视图与辅助功能如切换换行模式、显示状态栏、缩放文本等。架构上我们将采用典型的WinForm窗体应用结构。主窗体承载菜单、工具栏、状态栏和RichTextBox。通过为菜单项和工具栏按钮绑定事件处理程序来调用RichTextBox的相应方法或我们封装的业务逻辑。这里的关键是理解“事件驱动”——用户点击一个按钮触发一个Click事件我们在对应的事件处理方法里编写代码来改变RichTextBox的状态或内容。注意在规划初期就要考虑编码问题。RichTextBox默认使用系统的ANSI编码加载和保存文本这在处理中文或特殊字符时极易产生乱码。我们必须将文件操作部分的编码处理作为重中之重来设计。3. 开发环境搭建与基础窗体构建3.1 项目创建与必要配置打开Visual Studio2019或2022均可选择“创建新项目” - “Windows窗体应用(.NET Framework)”。我建议选择**.NET Framework 4.6.1或以上**版本它兼容性好并且支持后续我们可能用到的一些较新的API。项目名称可以定为“SimpleTextEditor”。创建成功后首先调整主窗体属性。将Form1重命名为更有意义的MainEditorForm。然后设置以下关键属性Text: 设置为“我的文本编辑器”这将显示在窗口标题栏。StartPosition: 设置为CenterScreen让窗口首次打开时位于屏幕中央。WindowState: 保持为Normal即可如果想最大化启动可设为Maximized。Size: 设置一个合适的初始大小例如800, 600。3.2 主界面控件的拖拽与布局WinForm开发的一大便利就是可视化的控件拖拽。我们从工具箱中将所需控件拖到窗体上MenuStrip拖到窗体顶部自动停靠。为其添加“文件(F)”、“编辑(E)”、“格式(O)”、“视图(V)”、“帮助(H)”等顶级菜单项。括号内的符号用于定义快捷键AltF。ToolStrip拖到MenuStrip下方。通过其“项”集合编辑器添加ToolStripButton并为其设置Image属性可以从VS内置图标库或自己准备、ToolTipText鼠标悬停提示。StatusStrip拖到窗体底部。添加多个ToolStripStatusLabel用于显示不同的状态信息例如一个显示“就绪”一个显示“行1列1”一个显示“字符数0”。RichTextBox这是主角。拖到窗体中央将其Dock属性设置为Fill使其填充菜单、工具栏和状态栏之外的剩余区域。将Name属性改为rtbContent遵循控件前缀命名习惯。建议将Font属性设置为“Consolas”或“宋体”等宽字体方便代码查看。布局完成后你的窗体设计器应该看起来像一个标准编辑器的雏形。此时运行程序你已经能在一个窗口里输入和编辑文本了只是所有功能都还没实现。实操心得在拖拽控件时善用“格式”菜单下的“对齐”、“使大小相同”、“水平间距”、“垂直间距”等功能可以快速让界面变得整齐。虽然WinForm不强调像素级完美但一个整洁的界面能极大提升用户体验和专业感。4. 核心功能实现从文件操作到文本编辑4.1 文件打开、保存与编码处理这是第一个“坑点”密集区。RichTextBox的LoadFile和SaveFile方法虽然方便但默认编码行为可能不符合预期。实现打开文件功能在“文件”菜单下创建“打开(O)”子项双击生成OpenToolStripMenuItem_Click事件处理方法。方法内首先使用OpenFileDialog让用户选择文件。private void OpenToolStripMenuItem_Click(object sender, EventArgs e) { using (OpenFileDialog ofd new OpenFileDialog()) { ofd.Filter 文本文件 (*.txt)|*.txt|富文本文件 (*.rtf)|*.rtf|所有文件 (*.*)|*.*; ofd.FilterIndex 1; ofd.RestoreDirectory true; if (ofd.ShowDialog() DialogResult.OK) { // 调用加载文件的方法 LoadFile(ofd.FileName); } } }实现LoadFile方法。这里不能直接用rtbContent.LoadFile因为需要处理编码。private void LoadFile(string filePath) { try { // 先尝试用UTF-8无BOM读取这是目前最通用的编码 string content File.ReadAllText(filePath, Encoding.UTF8); rtbContent.Text content; currentFilePath filePath; // 记录当前文件路径 UpdateTitle(); // 更新窗口标题显示文件名 UpdateStatusBar(文件加载成功。); } catch (DecoderFallbackException) { // 如果UTF-8失败尝试用系统默认编码通常是GBK/GB2312 try { string content File.ReadAllText(filePath, Encoding.Default); rtbContent.Text content; currentFilePath filePath; UpdateTitle(); UpdateStatusBar(文件加载成功使用系统默认编码。); } catch (Exception ex) { MessageBox.Show($无法打开文件{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } } catch (Exception ex) { MessageBox.Show($无法打开文件{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }关键技巧这里采用了一个简单的编码探测策略优先尝试UTF-8失败后再用系统默认编码对于中文Windows是GBK。对于更复杂的情况可以考虑使用StreamReader并设置detectEncodingFromByteOrderMarks为true来自动探测UTF-8 BOM或者集成像Ude这样的编码检测库。实现保存文件功能保存的逻辑类似但需要区分“保存”和“另存为”。private void SaveToolStripMenuItem_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(currentFilePath)) { // 如果文件从未保存过执行另存为逻辑 SaveAsToolStripMenuItem_Click(sender, e); } else { // 保存到当前路径 SaveToFile(currentFilePath); } } private void SaveAsToolStripMenuItem_Click(object sender, EventArgs e) { using (SaveFileDialog sfd new SaveFileDialog()) { sfd.Filter 文本文件 (*.txt)|*.txt|富文本文件 (*.rtf)|*.rtf; sfd.FilterIndex 1; sfd.RestoreDirectory true; if (sfd.ShowDialog() DialogResult.OK) { SaveToFile(sfd.FileName); currentFilePath sfd.FileName; UpdateTitle(); } } } private void SaveToFile(string filePath) { try { // 获取文件扩展名决定保存格式 string ext Path.GetExtension(filePath).ToLower(); if (ext .rtf) { // 保存为RTF格式保留所有富文本格式 rtbContent.SaveFile(filePath, RichTextBoxStreamType.RichText); } else { // 保存为纯文本默认使用UTF-8编码 File.WriteAllText(filePath, rtbContent.Text, Encoding.UTF8); } UpdateStatusBar(文件已保存。); } catch (Exception ex) { MessageBox.Show($保存文件失败{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }4.2 撤销、重做与剪贴板操作RichTextBox内置了撤销Undo和重做Redo栈我们只需要调用其方法即可。关键点在于如何管理菜单项或按钮的启用状态。绑定事件为“编辑”菜单下的“撤销”、“重做”、“剪切”、“复制”、“粘贴”、“全选”项生成点击事件。实现功能private void UndoToolStripMenuItem_Click(object sender, EventArgs e) { if (rtbContent.CanUndo) { rtbContent.Undo(); } } private void RedoToolStripMenuItem_Click(object sender, EventArgs e) { if (rtbContent.CanRedo) { rtbContent.Redo(); } } // 剪切、复制、粘贴、全选同理直接调用 rtbContent.Cut(), Copy(), Paste(), SelectAll()动态更新UI状态我们需要在文本变化或获得焦点时更新这些菜单项的Enabled属性。可以在rtbContent的TextChanged和SelectionChanged事件中处理。private void RtbContent_SelectionChanged(object sender, EventArgs e) { // 更新撤销/重做状态 undoToolStripMenuItem.Enabled rtbContent.CanUndo; redoToolStripMenuItem.Enabled rtbContent.CanRedo; // 更新剪切/复制状态有选中文本时才可用 bool hasSelection rtbContent.SelectionLength 0; cutToolStripMenuItem.Enabled hasSelection; copyToolStripMenuItem.Enabled hasSelection; // 粘贴状态取决于剪贴板是否有文本内容 pasteToolStripMenuItem.Enabled Clipboard.ContainsText(); }注意事项Clipboard.ContainsText()是一个简单的判断在实际应用中你可能需要更精细地处理剪贴板格式。此外直接启用/禁用菜单项比在点击时弹出错误提示用户体验更好。4.3 查找与替换功能的实现查找替换是编辑器的核心功能之一。我们需要创建一个新的对话框窗体FindReplaceDialog。创建对话框窗体添加两个TextBox用于输入查找和替换内容几个Button查找下一个、替换、全部替换、取消以及CheckBox用于控制“区分大小写”和“全字匹配”。实现查找逻辑核心是使用RichTextBox.Find方法。这个方法有多个重载我们需要根据选项选择合适的搜索起点和匹配方式。// 在 FindReplaceDialog 中 private void FindNext() { RichTextBox rtb ownerForm.rtbContent; // 假设通过构造函数传入了主窗体引用 StringComparison comparison chkMatchCase.Checked ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; int startIndex rtb.SelectionStart rtb.SelectionLength; // 从当前选中内容之后开始找 int index rtb.Text.IndexOf(txtFind.Text, startIndex, comparison); if (index 0) { rtb.Select(index, txtFind.Text.Length); rtb.ScrollToCaret(); // 滚动到选中位置 rtb.Focus(); } else { // 如果没找到可以从头开始找可选 MessageBox.Show(已搜索到文档末尾。, 查找, MessageBoxButtons.OK, MessageBoxIcon.Information); } }实现替换逻辑替换是在查找的基础上用SelectedText属性进行替换。private void Replace() { if (rtbContent.SelectionLength 0 string.Equals(rtbContent.SelectedText, txtFind.Text, chkMatchCase.Checked ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) { rtbContent.SelectedText txtReplace.Text; } FindNext(); // 替换后自动查找下一个 }全部替换循环执行查找和替换直到文档末尾。这里要注意性能对于大文档可能需要放在后台线程并显示进度。踩坑记录RichTextBox.Find方法对于“全字匹配”的支持不直接。你需要自己判断找到的文本前后是否是单词边界非字母数字字符。这增加了实现的复杂度但通过正则表达式可以更优雅地解决。5. 富文本格式与用户交互增强5.1 字体、颜色与段落格式控制RichTextBox提供了丰富的属性来控制选中文本的格式。我们通过工具栏按钮或格式菜单来调用。字体设置private void BoldToolStripButton_Click(object sender, EventArgs e) { // 获取当前选中文本的字体样式并切换加粗状态 Font currentFont rtbContent.SelectionFont; FontStyle newStyle; if (currentFont ! null currentFont.Bold) { newStyle currentFont.Style ~FontStyle.Bold; // 移除加粗 } else { newStyle (currentFont?.Style ?? FontStyle.Regular) | FontStyle.Bold; // 添加加粗 } rtbContent.SelectionFont new Font(currentFont ?? rtbContent.Font, newStyle); }斜体、下划线的实现逻辑类似。字体和字号则需要调用FontDialogprivate void FontToolStripMenuItem_Click(object sender, EventArgs e) { using (FontDialog fd new FontDialog()) { fd.Font rtbContent.SelectionFont ?? rtbContent.Font; if (fd.ShowDialog() DialogResult.OK) { rtbContent.SelectionFont fd.Font; } } }颜色设置使用ColorDialog来设置选中文本的颜色或背景色。private void ForeColorToolStripMenuItem_Click(object sender, EventArgs e) { using (ColorDialog cd new ColorDialog()) { cd.Color rtbContent.SelectionColor; if (cd.ShowDialog() DialogResult.OK) { rtbContent.SelectionColor cd.Color; } } }段落对齐直接设置SelectionAlignment属性。private void AlignLeftToolStripMenuItem_Click(object sender, EventArgs e) { rtbContent.SelectionAlignment HorizontalAlignment.Left; } // 居中和右对齐同理重要提示SelectionFont和SelectionColor在没有选中文本时表示的是当前光标插入点处的格式新输入的文本将应用此格式。在有选中文本时则改变选中部分的格式。这个特性需要向用户解释清楚。5.2 状态栏信息的实时更新状态栏是用户了解当前编辑状态的重要窗口。我们需要实时更新以下信息光标位置在rtbContent的SelectionChanged事件中计算。RichTextBox没有直接提供行号我们需要自己计算。一个常见的方法是使用GetLineFromCharIndex方法。private void UpdateStatusBar() { int index rtbContent.SelectionStart; int line rtbContent.GetLineFromCharIndex(index) 1; // 行号从1开始 int column index - rtbContent.GetFirstCharIndexFromLine(line - 1) 1; // 列号从1开始 lblPosition.Text $行{line}列{column}; lblCharCount.Text $字符数{rtbContent.Text.Length}; }字符数/字数同样在TextChanged事件中更新rtbContent.Text.Length。注意对于中文字数统计可能需要更复杂的逻辑如排除空格和标点。编辑状态如“已修改”标志。可以在TextChanged事件中设置一个bool isDirty true;并在保存后重置。在状态栏或窗口标题上提示用户。5.3 界面美化与用户体验优化虽然WinForm默认样式比较朴素但我们仍可以做一些优化图标与工具栏为工具栏按钮和菜单项设置清晰易懂的图标。可以使用Visual Studio自带的图像库或从专业的图标网站获取一套风格统一的图标。快捷键支持除了通过定义Alt快捷键还可以为菜单项设置ShortcutKeys属性如CtrlS对应保存CtrlZ对应撤销。确保快捷键符合用户习惯Windows标准。最近文件列表在“文件”菜单下动态生成一个最近打开的文件列表。这需要将文件路径保存到应用设置Properties.Settings.Default或一个配置文件中。文本缩放响应Ctrl鼠标滚轮事件调整RichTextBox的ZoomFactor属性实现缩放功能。private void RtbContent_MouseWheel(object sender, MouseEventArgs e) { if (Control.ModifierKeys Keys.Control) { // 限制缩放范围例如 0.2 到 5.0 float newZoom rtbContent.ZoomFactor (e.Delta 0 ? 0.1f : -0.1f); rtbContent.ZoomFactor Math.Max(0.2f, Math.Min(5.0f, newZoom)); e.Handled true; // 阻止默认滚动 } }拖放支持启用RichTextBox的AllowDrop属性并处理DragEnter和DragDrop事件允许用户将文本文件直接拖入编辑器打开。6. 高级功能拓展与性能考量6.1 语法高亮与自定义渲染RichTextBox本身不支持像代码编辑器那样的语法高亮。但我们可以通过一些“笨”办法实现基础高亮例如在文本改变后使用SelectionColor和SelectionFont属性遍历文本并着色关键词。但这种方法性能极差不适用于大文件。对于真正的语法高亮需求有两条路使用专业编辑控件如ScintillaNET封装了Scintilla编辑器组件它原生支持语法高亮、代码折叠、自动补全等。自己实现文本渲染放弃RichTextBox使用Panel或自定义控件在OnPaint事件中完全自己绘制文本。这条路非常复杂涉及文本测量、布局、光标处理、输入法等除非有极强的定制需求否则不推荐。对于我们的项目如果只是需要简单的关键词标记可以限定在文档较小或用户手动触发“格式化”时进行。6.2 大文件处理与性能优化当处理几MB甚至几十MB的文本文件时直接rtbContent.Text content会导致界面长时间卡死。解决方案是使用异步加载和虚拟化但RichTextBox本身不支持真正的虚拟化。异步加载示例private async void LoadLargeFileAsync(string filePath) { // 禁用控件显示加载提示 rtbContent.Enabled false; toolStripProgressBar1.Visible true; toolStripProgressBar1.Style ProgressBarStyle.Marquee; try { string content await Task.Run(() File.ReadAllText(filePath, Encoding.UTF8)); // 回到UI线程更新控件 rtbContent.Text content; currentFilePath filePath; UpdateTitle(); } catch (Exception ex) { MessageBox.Show($加载失败{ex.Message}); } finally { rtbContent.Enabled true; toolStripProgressBar1.Visible false; } }注意即使异步加载将超长字符串赋值给Text属性在UI线程渲染时仍可能卡顿。对于超大文件更好的做法是使用只读模式或者分页/分段加载但这需要更复杂的设计。6.3 插件化架构初探为了让编辑器具备扩展性可以引入简单的插件机制。例如定义一个插件接口public interface IEditorPlugin { string Name { get; } void Initialize(MainEditorForm editor); void Execute(); }在主窗体中维护一个插件列表在启动时从特定目录加载实现了该接口的DLL并动态创建菜单项或工具栏按钮来调用插件的Execute方法。这样就可以实现诸如“Markdown预览”、“JSON格式化”、“字数统计增强”等独立功能模块。7. 常见问题排查与调试技巧7.1 乱码问题终极解决方案乱码是文本编辑器最常见的问题。我们的LoadFile方法已经做了基础防护。但如果遇到更棘手的情况可以升级编码检测策略使用BOM探测StreamReader可以自动识别UTF-8、UTF-16等带有BOM的编码。using (var sr new StreamReader(filePath, Encoding.Default, detectEncodingFromByteOrderMarks: true)) { content sr.ReadToEnd(); Encoding detectedEncoding sr.CurrentEncoding; // 记录或使用检测到的编码 }集成第三方库对于无BOM的文件可以使用Ude一个C#端口或MLangWindows API进行更准确的编码猜测。提供编码选择菜单在“打开”对话框中或文件打开后在状态栏提供一个编码选择下拉框允许用户手动切换编码并重新加载。7.2 撤销/重做栈异常有时用户会反映撤销/重做行为不符合预期。这可能是因为某些操作如程序化地设置Text属性会清空撤销栈。rtbContent.Text “”和rtbContent.Clear()都会清空。如果需要在清空文本时保留撤销历史这很少见可以考虑使用rtbContent.SelectAll(); rtbContent.SelectedText “”;但这并不总是有效。通常明确告知用户“新建”或“打开”操作会重置撤销历史是更好的做法。7.3 内存泄漏与资源释放虽然.NET有垃圾回收但WinForm控件如果处理不当仍可能导致内存泄漏。最常见的原因是事件绑定未解除。如果你动态创建了控件如查找对话框并订阅了主窗体RichTextBox的事件在对话框关闭时务必取消订阅。或者确保对话框对象被正确释放使用using语句或手动调用Dispose。另一个资源大户是字体对象。在动态创建字体如new Font(...)后如果不再使用应调用Dispose()方法释放原生资源。不过分配给SelectionFont的字体RichTextBox会自行管理其生命周期。7.4 界面卡顿与响应迟缓除了之前提到的大文件加载问题界面卡顿还可能源于频繁的UI更新例如在TextChanged事件中执行复杂的计算或频繁更新状态栏。可以使用BeginUpdate/EndUpdateRichTextBox支持来暂停绘制或者使用Timer来延迟更新例如用户停止输入500ms后再更新字数统计。过多的自定义绘制如果重写了OnPaint来做一些装饰确保绘制逻辑高效只绘制需要更新的区域ClipRectangle。7.5 部署与兼容性问题项目开发完成后在Visual Studio中生成安装项目或使用ClickOnce发布相对简单。需要注意的是**.NET Framework版本的依赖**。确保目标机器安装了相应版本的.NET Framework运行时。如果使用了一些较新的API如异步文件操作要确认你选择的.NET Framework版本支持它们。对于“WinForm驱动佳博打印机”这类热词提到的硬件交互需求我们的编辑器可以作为文本输入前端通过调用系统打印对话框PrintDialog配合PrintDocument或特定的打印机SDK来实现打印功能。这超出了基础编辑器的范畴但说明了WinForm在连接传统硬件方面的实用性。最后这个基于WinForm和RichTextBox的文本编辑器项目就像一把瑞士军刀它简单、可靠、易于扩展。通过完成它你不仅学会了一个控件的使用更掌握了WinForm应用程序从界面布局、事件处理、文件IO、状态管理到性能优化的完整开发流程。当你下次需要为一个特定场景快速打造一个桌面工具时这套技术栈和设计思路依然是你最趁手的工具之一。本文还有配套的精品资源点击获取
返回列表