简介面向Visual Studio 2010下Windows桌面应用开发入门者该示例工程围绕EDIT控件提供了完整学习代码系统讲解控件基本创建与属性设置、事件处理、文本读写、输入限制、多行编辑、格式控制、光标与滚动条操作、错误提示以及涉及GDI的打印预览与自定义绘制等九类应用方式适合需要掌握MFC对话框开发或优化文本输入交互的读者。资源包约40.82MB共52个文件以C源码h/cpp/rc、工程配置sln/vcxproj和编译调试产生的文件obj、tlog、pdb、ilk为主同时包含可运行的exe及ReadMe说明方便对照代码梳理工程结构。目前已有609人学习下载。借助HelpEdit程序中GetDlgItemText、SetDlgItemText、EM_SETLIMITTEXT、EM_GETSEL等接口的实际调用边阅读源码边运行调试可以更快掌握EDIT控件各项方法及排错思路为实际项目灵活运用打下基础。 从你打算在界面上放一个输入框到真正让这个输入框按你的预期工作中间的细节往往比想象中多得多。Windows桌面开发里EDIT控件编辑框控件是最基础、使用频率也最高的交互组件之一但很多人在第一次接触时只是简单地把它拖到对话框上然后用GetWindowText和SetWindowText读写两下就完事了。这样用当然没问题但如果你的需求稍微复杂一点——比如限制输入格式、做输入法适配、处理回车键、实现多行文本的滚动定位——光靠这两三个函数就远远不够了。这篇文章打算把EDIT控件的使用方式完整地过一遍从控件的本质属性和样式说起再讲到消息机制、常用操作、以及实际开发中特别容易踩的坑。我会尽量说清楚每个操作背后的“为什么”而不是只丢给你一串API名字。无论你是刚接触Win32编程的初学者还是已经在用MFC但一直没时间深挖细节的人这篇文章应该都能帮上忙。1. 先搞清楚EDIT控件的本质它到底是个什么角色EDIT控件本质上是一个“轻量级文本编辑器”的封装。系统提供了一整套围绕它的消息和通知机制目的是让开发者不必自己处理光标闪烁、键盘输入、文本存储、选中状态、滚动显示这些底层的细节。你只需要告诉它“你要成为什么风格的编辑框”剩下的交互逻辑由控件自己完成。打个比方EDIT控件就像是一个装修好的单间公寓水电管道、墙面粉刷都做好了你要做的事情是决定这个房间是用来当书房还是卧室然后搬进去家具。如果你非要自己砸掉墙重新布管线——也就是从零做一个文本编辑控件——那工程量会大得惊人而且大概率做得不如系统自带的好。在Win32 SDK体系里EDIT控件对应的窗口类是EDIT在MFC里被封装为CEdit到了C# WinForms则是TextBox。但底层内核其实是同一套机制。理解这一点很重要你在MFC里写的代码最终发出去的消息和Win32 API的是一致的你在C#里设置的多行属性映射的也是同样的窗口样式。所以学EDIT控件本质上学的是一套跨语言的通用规则。它的核心能力包括接收键盘输入管理光标移动和插入位置支持选中文本并配合剪贴板进行剪切、复制、粘贴支持单行、多行、密码掩码等不同表现形态通过WM_CTLCOLOR和自绘机制改变外观通过父窗口通知消息向上层传递交互事件从数据流的角度看用户输入文本后EDIT控件内部会维护一块文本缓冲区并通知父窗口“内容已经变化了”。父窗口可以用消息去查询当前内容、修改内容、或者控制光标和选中状态。这套逻辑在任何一个图形界面框架里都是几乎相同的所以学会了EDIT控件的运作方式以后接触别的框架里的文本输入组件你会有一种“似曾相识”的感觉。2. 三步创建从Win32 API到MFC的对照EDIT控件的创建方式分成两类一类是直接代码创建另一类是在对话框资源里放置后通过框架机制关联。这里我分别说一下。2.1 纯Win32代码创建在Win32 API环境下你可以用CreateWindowEx创建EDIT控件HWND hEdit CreateWindowEx( WS_EX_CLIENTEDGE, LEDIT, NULL, WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL, 10, 10, 200, 25, hParent, (HMENU)IDC_EDIT_NAME, hInst, NULL );注意几个细节样式里的WS_CHILD和WS_VISIBLE是不可或缺的否则控件不显示。WS_TABSTOP让控件可以参与Tab键顺序切换焦点如果你设计表单却不加这个用户只能用鼠标点击切换输入框体验就比较糟糕。ES_AUTOHSCROLL是单行输入框的基础样式。不加它超出控件宽度的字会被截断直到你按方向键才能看到后面的内容。创建后父窗口会收到WM_COMMAND通知当控件是子窗口时通过HIWORD(wParam)判断是哪一种通知码通过LOWORD(wParam)判断是哪个控件的ID。MFC中使用CEdit则通常通过DDX_Control将对话框上的控件对象与资源ID绑定或者直接在运行时Create出来本质上做的也是同样的窗口创建只是包了一层更容易调用的C类接口。2.2 MFC中的创建与关联MFC里最省事的方式是在对话框模板上放置EDIT控件然后用DDX_Control、DDX_Text绑定变量void CMyDialog::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_EDIT_NAME, m_editName); DDX_Text(pDX, IDC_EDIT_NAME, m_strName); }这段代码有两个绑定DDX_Control把控件窗口句柄关联到m_editName这个成员变量上方便后续调用CEdit的各种方法DDX_Text则把控件内容关联到m_strName这个字符串变量上在UpdateData(TRUE/FALSE)时自动同步数据。很多初学者会忘记区分这两者一个负责“拿到控件的操作权”另一个负责“同步数据”。实际开发中常常两个都需要但各自承担的任务完全不同。2.3 C# WinForms的对照参考如果是C#环境创建TextBox其实就是一行代码TextBox textBox new TextBox(); textBox.Location new Point(10, 10); textBox.Size new Size(200, 25); this.Controls.Add(textBox); textBox.Multiline true;WinForms的TextBox.Multiline属性其实对应着原生EDIT控件的ES_MULTILINE样式PasswordChar对应ES_PASSWORDReadOnly对应ES_READONLY。搞清楚了原生样式的含义再去理解这些属性会变得非常顺你其实是在用不同的语言操作同一套机制。3. 样式与消息决定EDIT控件行为的核心机制EDIT控件的行为几乎完全由窗口样式决定。创建后虽然可以通过SetWindowLongPtr修改一部分样式但推荐的做法是在创建之前就确定好需求因为有些样式改动会出现不稳定的表现。3.1 常见样式位及其选择逻辑下表是我在实际开发中最常用到的样式位组合样式位作用适用场景备注ES_MULTILINE是否允许多行文本备注、日志显示配合WS_VSCROLL和ES_AUTOVSCROLL实现自动滚动ES_PASSWORD掩码显示密码输入配合EM_SETPASSWORDCHAR可以自定义掩码字符ES_READONLY只读模式数据分析结果、提示信息用户无法编辑但可以选中和复制ES_AUTOHSCROLL自动水平滚动单行输入不加则输入超出宽度截断ES_AUTOVSCROLL自动垂直滚动多行显示通常配合ES_MULTILINEES_WANTRETURN回车键作为换行符多行编辑器只在多行且无ES_MULTILINE冲突时配合WS_EX_ACCEPTFILES等使用ES_NUMBER只能输入数字数字框但无法拦截字母e、、-等符号需要额外过滤ES_UPPERCASE自动转为大写工号、车牌输入极少数场景用ES_NOHIDESEL失焦时保留选中状态高亮多选列表联动一般配合对话框默认按钮场景这里特别说一下ES_WANTRETURN它只对“多行编辑框”有意义。默认情况下单行EDIT控件在键盘上按下回车会直接把回车消息发给默认按钮比如对话框的“确定”按钮把对话框给提交了而多行EDIT控件按下回车默认是插入换行符。如果你做的是一个聊天输入框想要按回车发送、按Shift回车换行那你实际要处理的应该是键盘消息而不是依赖样式本身。3.2 核心消息一览EDIT控件最核心的消息可以分为几类文本操作、选中与光标、格式控制、裁剪板交互、底层回调。文本操作WM_SETTEXT设置整个控件文本内容WM_GETTEXT获取当前文本内容WM_GETTEXTLENGTH获取当前文本长度不包含终止符EM_REPLACESEL用新文本替换当前选中部分不改变未选中内容EM_GETSEL获取选中区域起始和结束位置EM_SETSEL设置选中区域或光标位置输入限制EM_LIMITTEXT限制最大字符长度。注意这个限制对WM_PASTE和WM_SETTEXT同样生效默认情况下单行EDIT控件的最大长度是32767个字符多行是更大的值但只要你自己设置了限制就以设置的为准。EM_SETCUEBANNER设置水印提示文字。这是比较实用的一个功能用于在空输入框里显示灰色提示。格式行为EM_SETPASSWORDCHAR设定密码掩码字符传0表示取消掩码。EM_SETREADONLY切换只读状态不需要重新创建窗口。EM_GETMODIFY和EM_SETMODIFY获取或设置修改标志位。这个标志位不会自动重置如果你做完一次数据保存想让它重新从“未修改”状态开始需要手动调用EM_SETMODIFY(FALSE)。滚动与光标EM_SCROLL和EM_SCROLLCARET滚动内容、确保光标所在行可见。EM_GETFIRSTVISIBLELINE获取第一行可见行号。EM_LINEFROMCHAR根据字符位置推算行号。EM_GETLINE获取指定行的内容。这里有一个特别容易踩的坑调用EM_GETLINE前必须把长度写入缓冲区的第一个WORD对ANSI版本或放到单独的计数参数里对不同封装否则函数可能返回0或者拿到空内容。3.3 通知消息父窗口如何知道用户动了控件父窗口通过WM_COMMAND接收控件通知。常见的通知码有EN_CHANGE文本内容已经改变。这个通知在用户每次键入或者是程序调用WM_SETTEXT以后都会触发。EN_UPDATE在控件将要重绘之前触发。和EN_CHANGE的区别在于此时控件内部文本已经更新但界面还没重绘。一般极少用但如果需要在文本变化后立刻调整控件尺寸或做状态处理可以在EN_UPDATE里做。EN_SETFOCUS/EN_KILLFOCUS控件获得或失去焦点。EN_ERRSPACE系统空间不足导致无法存储等极少遇到。EN_MAXTEXT文本被截断通常是超过了EM_LIMITTEXT限制。EN_VSCROLL/EN_HSCROLL用户点击滚动条。一个需要特别记住的经验EN_CHANGE在每次文本变化时都会触发但如果你的处理函数里又主动调用了SetWindowText去修改控件内容那会再次触发EN_CHANGE形成一种逻辑上的重入。如果不加保护标志很容易在联动刷新时出现意料之外的循环或者状态错乱。我的做法是统一用一个bool m_bUpdating标志位包裹所有程序主动修改控件内容的地方在通知处理函数里先判断这个标志如果为真就直接返回。4. 真正好用的技巧让EDIT控件符合“人类使用习惯”基本的读写是个人都会。但要让控件真正好用需要关心一些更贴近真实交互的细节。4.1 输入框水印提示Win32 EDIT控件原生支持水印。在XP SP2及以上的系统上可以向控件发送EM_SETCUEBANNER消息SendMessage(hEdit, EM_SETCUEBANNER, (WPARAM)TRUE, (LPARAM)L请输入用户名);第一个参数是是否立即显示水印TRUE就没有焦点的瞬间隐藏第二个参数是提示文本。如果你的程序需要在低版本系统兼容运行则要手动忽略这个消息改用WM_PAINT自绘水印但那个就麻烦多了。4.2 限制输入内容但不影响粘贴如果你想要“只允许输入数字”很多人会简单地把样式设为ES_NUMBER。但实测下来ES_NUMBER只限制了键盘输入无法阻止通过剪贴板粘贴进来的非法字符而且对于用户输入的某些字符比如中文输入法下的全角数字也未必能拦截干净。所以更稳妥的做法是在EN_CHANGE通知里做一次字符过滤void CMyDialog::OnEnChangeEditCode() { if (m_bUpdating) return; CString strText; m_editCode.GetWindowText(strText); // 只保留数字和英文字母 CString strFiltered; for (int i 0; i strText.GetLength(); i) { TCHAR ch strText[i]; if ((ch _T(0) ch _T(9)) || (ch _T(A) ch _T(Z)) || (ch _T(a) ch _T(z))) { strFiltered ch; } } if (strFiltered ! strText) { m_bUpdating TRUE; m_editCode.SetWindowText(strFiltered); m_editCode.SetSel(strFiltered.GetLength(), strFiltered.GetLength()); // 把光标移到最后 m_bUpdating FALSE; } }注意最后一定要把光标位置调整到最后否则用户输入到中间时一过滤字符光标就跳到开头体验非常差。这个细节很多教程不会提但实际用起来差别很大。4.3 多行EDIT控件的“追加日志”操作如果你用多行EDIT控件做日志输出框最忌讳的做法是每次都用SetWindowText把整段文本重新写一遍。日志会越来越长每次刷新整段文本不仅浪费性能还会导致滚动条跳到顶部用户根本没法正常阅读。正确的做法是void AppendLog(const CString strLine) { // 先选到文本末尾 int nLen m_editLog.GetWindowTextLength(); m_editLog.SetSel(nLen, nLen); // 用EM_REPLACESEL在末尾追加 m_editLog.ReplaceSel(strLine _T(\r\n)); }用SetSel把光标和选中位置都挪到末尾再用ReplaceSel追加这样文本内容可以持续增长而界面始终停留在最新一行。如果你的日志过长导致内存膨胀可以在追加前检查长度超过某个阈值后就清空重来if (m_editLog.GetWindowTextLength() MAX_LOG_LEN) { m_editLog.SetWindowText(_T()); }4.4 失焦后的选中状态保持Windows默认情况下EDIT控件在失去焦点后之前选中的文本会变成灰色的“非激活选中”状态这其实是很友好的交互方式。但如果你发现某些控件在失焦后选中的高亮完全消失了特别是在MFC对话框默认按钮场景下可以检查一下样式里是否误加了ES_NOHIDESEL。这个样式会让失焦后选中区域保持高亮但有时候也会造成视觉上的奇怪感受我个人的建议是能用默认就别加它。5. 实际开发中的常见坑与排查链路EDIT控件的坑说多不多但每一个都够你折腾好一阵。下面这几个是我踩过的也是身边同事经常来问的。5.1 EM_GETLINE永远返回空内容的真凶遇到“明明控件里有文字但用EM_GETLINE获取指定行却拿不到”的情况第一反应不要怀疑系统先检查自己有没有在调用前设置好缓冲区的第一个WORD。在纯Win32 API下EM_GETLINE要求你先把缓冲区长度写入缓冲区头两个字节这个消息才会正常执行。很多人在老的教材里看到代码会写wchar_t buf[256]; buf[0] 255; // 把最大长度写入第一个WORD SendMessage(hEdit, EM_GETLINE, nLine, (LPARAM)buf);在MFC的CEdit::GetLine里内部已经处理了这一步所以你不太会遇到这个坑但一旦你混用API就很容易翻车。更隐蔽的是在处理Unicode时缓冲区的第一个WORD是字符数还是字节数取决于你用的是EM_GETLINE还是EM_GETLINE的Unicode版本——而无论哪个版本长度都不包含字符串结束符。所以我的习惯是直接用m_edit.GetLine(nIndex, buf, nMaxLen)这种封装好接口尽量不直接裸用SendMessage减少心智负担。5.2 EN_CHANGE触发时机比你想的更早EN_CHANGE不是在用户完整的输入动作结束后才触发的而是在执行撤销、粘贴、删除、乃至输入法组合的每一步时都可能触发。如果你在窗口初始化时用了SetDlgItemText给EDIT填充数据那同样会触发EN_CHANGE。这个问题最常见的影响是你本来打算在用户编辑完后更新某个数据缓存结果初始化时就被触发了一次你又试图在EN_CHANGE里更新界面上的另一个控件内容然后那个控件又触发自己的EN_CHANGE一来二去就乱套了。我的排查思路非常简单在初始化时先设置一个m_bUpdating标志为TRUE。所有程序主动修改EDIT控件内容的地方统一用这个标志保护。EN_CHANGE处理函数开头第一行判断标志为真就return。退出修改后再恢复标志为FALSE。这个模式几乎可以解决所有和EN_CHANGE相关的重入问题。5.3 输入法光标定位错乱的元凶IME组合阶段涉及中文输入的EDIT控件还有一个高级坑输入法在组合拼音的阶段也会反复更新控件内的文本。某些输入法甚至在组合阶段会发送WM_IME_COMPOSITION消息如果你在EN_CHANGE里对文本做过过滤、截断、重排光标往往会和输入法自己的逻辑打架表现为“拼音打不出来”“候选框乱跳”“文本被反复截断”。一个稳妥的思路是在EN_CHANGE里过滤字符时先判断当前是否有输入法组合状态。你可以用ImmGetContext配合ImmGetOpenStatus判断输入法是否打开或在WM_IME_STARTCOMPOSITION/WM_IME_ENDCOMPOSITION消息里控制过滤开关。如果不想引入太多复杂逻辑最简单的做法是在组合期间跳过过滤只在EN_CHANGE里做长度截断不做字符白名单过滤这样至少不会破坏输入法的工作。5.4 对话框Tab顺序混乱导致焦点丢失这是个大坑。很多人创建了一堆EDIT控件没设置Tab顺序导致程序启动后用户按Tab键焦点不按预期顺序跳转甚至直接跳到“取消”按钮上把对话框关掉了。实际上Tab顺序取决于对话框中控件的创建顺序。在资源编辑器里可以按CtrlD调整Tab顺序在纯代码创建时要确保按照你期望的焦点顺序依次CreateWindowEx。这个细节在高密度表单界面中尤其重要因为用户习惯用键盘操作表单一旦Tab顺序是乱的这个软件的可用性瞬间下降一个等级。6. 进一步修饰让EDIT控件融入整体界面很多时候EDIT控件的默认样式足够使用但如果你对软件的视觉有更高期待有一些很实用的手段。6.1 背景色与文字颜色通过在父窗口处理WM_CTLCOLOREDIT消息可以给EDIT控件单独设置背景色和文字颜色HBRUSH CMyDialog::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); if (nCtlColor CTLCOLOR_EDIT pWnd-GetDlgCtrlID() IDC_EDIT_CODE) { pDC-SetTextColor(RGB(0, 0, 128)); pDC-SetBkColor(RGB(255, 255, 240)); return m_brEditBg; } return hbr; }需要注意的是这里返回的HBRUSH必须是一个稳定的画刷对象不要使用GetStockObject(NULL_BRUSH)之类的临时对象否则窗口重绘时会出现闪烁和背景异常。我的做法是把它设为对话框类的成员变量在构造函数里创建在析构函数里销毁。6.2 只读模式与选中复制只读的EDIT控件依然可以让用户选中和复制文本这是它比Static文本控件更适合展示日志信息的原因之一。但要注意只读EDIT控件在收到WM_SETTEXT时仍然可以正常更新内容所以用来做“运行结果展示”很合适。唯一的问题是只读控件默认会显示灰色背景导致和普通输入框的视觉差异太大。如果你希望只读框看起来和其他输入框一样可以用WM_CTLCOLOREDIT把背景色强制改成白色。6.3 自绘的边界与局限EDIT控件的自绘非常繁琐因为它不是简单的WM_DRAWITEM可以搞定的。很多成熟的界面库之所以把EDIT控件做得好看是因为它们要么是自己实现了完整的编辑器要么通过窗口子类化SubclassWindow接管了WM_PAINT、WM_NCPAINT、WM_ERASEBKGND等一系列消息。如果你只是想实现圆角边框或自定义边框颜色一个巧妙的取巧做法是不要直接把EDIT控件放在对话框上而是把它嵌在一个自绘的容器里让容器负责绘制边框EDIT自己设置为无边框样式去掉WS_BORDER或WS_EX_CLIENTEDGE。这样你拿到的视觉效果是一个自定义边框的输入框实现成本却比自绘EDIT本身低得多。6.4 自动调整控件高度适应字体创建EDIT控件时如果你指定了一个固定高度而系统和控件字体尺寸比较大文字可能显示不全或者垂直方向被裁掉。比较稳的做法是根据GetTextMetrics获取当前字体下字符的高度然后乘以1.3到1.5作为单行EDIT的高度多行EDIT则根据行数乘以行高再加一个固定边距。这虽然要写几行代码但比在资源编辑器里手测高度可靠得多。7. 我最后想补充的几个小经验写到这里我把这些年积累下来的EDIT控件使用心得再浓缩一下。这些不是官方文档上能直接读到的但它们确实提升了我自己代码的稳定性和可维护性。一开始就养成用变量保存控件句柄的习惯不要每次都GetDlgItem。特别在频繁交互的窗口里GetDlgItem本身有查找成本而且返回的句柄在控件销毁后可能失效。所有控件数据的读取和写入尽量集中在少数几个函数里不要散布在消息处理函数的各个角落。你无法预见之后维护代码的人会怎么改唯一的保护就是把数据流整理清晰。如果需要记录“用户是否修改过数据”以便关闭对话框时弹出保存提示记得用EM_SETMODIFY别自己维护一个布尔值。因为用户可能输入一个字符又撤销回来自己维护的标志位经常会误报而系统的修改标志在理论上能更准确地反映出“内容是否被实际改动过”。多数情况下SetSel配合ReplaceSel是实现文本插入和追加的最好办法。它不光性能好还能自动处理光标位置并且在撤销栈中留下正确的记录用户按CtrlZ时不会失控。在使用中文输入法的场景下如果EDIT控件要做字符长度限制建议用EM_LIMITTEXT来控制总长度而不要在输入法组合阶段用代码去截断字符串。这样既保护输入法又简化逻辑。EDIT控件看着不起眼但它在GUI程序里承担着最频繁的交互任务。读懂它的样式、消息和通知机制你其实就掌握了所有文本输入类控件的底层逻辑。希望这篇文章能帮你避开那些我踩过的坑做出来的软件在交互细节上更顺手一点。本文还有配套的精品资源点击获取