ARTICLE DETAIL

资讯详情

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

MFC读取TXT文件逐行显示:编码转换与CListBox实战

MFC读取TXT文件逐行显示:编码转换与CListBox实战 简介面向C语言及其面向对象扩展的初学者以及在微软可视化开发环境中进行文本处理的开发人员这份资源提供了一套可直接运行的工程源码演示如何利用文件输入流对象、行读取函数和标准输出流对象按行读取TXT文件的内容并在控制台中显示出来。代码中包含了文件打开失败时的错误处理以及使用完毕后关闭文件、释放资源的示例有助于深入理解文件流机制、字符串处理和基本输入输出的完整流程。整个压缩包共包含三十八个文件主要文件类型包括头文件、源文件、目标文件、调试信息文件、可执行文件、资源文件以及工程配置文件包体大小仅为一点八七兆字节。工程内部附带完整的项目框架和编译中间文件可直接在VC6.0中打开运行也方便单独提取源码进行修改和扩展。已有二百五十六人学习使用适合作为课程设计、上机练习或文本预处理任务的参考实现并可在其基础上继续完善特定格式数据的读取与分析功能。 做VC/MFC开发的同学十有八九都遇到过“读取txt文件并按行显示”这种需求。别看这个功能不大但涉及文件操作、编码转换、控件刷新这些点写不好就是乱码、卡顿甚至崩溃。我最早做上位机工具的时候接过一个需求就是从设备导出的配置文件里逐行读取参数并显示在界面上当时踩了不少坑。这篇就把我从方案选型到完整源码再到排错经验的全过程整理出来希望能给正在做类似功能的你一点参考。先说清楚这篇文章能解决什么问题。如果你需要在Visual C环境MFC或Win32下实现一个功能用户点击按钮选择一个txt文件程序读取文件中每一行文本并逐行显示在界面上那么这篇非常合适。内容适合刚接触VC文件操作的初学者也适合想优化现有读文件代码的开发者。1. 需求拆解与技术选型txt逐行显示这件事为什么值得好好设计拿到需求不要急着写代码。先想清楚这个功能背后的几个关键点用什么方式读文件、用什么控件显示、要不要处理中文和各类编码、文件多大才算极端情况。1.1 三种主流读取文件方式的对比VC下读取txt文件常用的有三条路CFile、CStdioFile、标准C的ifstream。我做了一个对比表格方便你快速做技术决策。方式是否自动处理换行中文编码处理使用复杂度适用场景CFile不处理需手动解析需手动转码中等二进制或自定义格式文件CStdioFile按\r\n逐行读取依赖工程字符集低ANSI/Unicode编码的纯文本ifstream按\n逐行读取需配合wstring转换中等跨平台或纯C项目如果你用的是MFC对话框程序最省事的方案是CStdioFile因为它的ReadString接口天然支持按行读取配套CString类可以无缝对接MFC控件。CFile虽然灵活但你要自己维护缓冲区、手动查找换行符代码量立刻上去了。ifstream在标准C项目里没问题但在MFC中还要做string到CString的转换多少有点绕。1.2 按行显示用什么控件更合适界面部分大家通常会用CListBox或CEdit。我的建议是优先考虑CListBox因为“逐行显示”这个动作和ListBox的AddString操作是一一对应的来一行加一行逻辑非常直观。CEdit虽然也能通过追加字符串实现类似效果但行数多了之后滚动、查找、选中行这些操作都麻烦不少。还有一个容易忽略的点CListBox可以给每一行设置数据后续如果要做行号定位、关键字高亮、双击行取值会方便得多。如果你的需求只是简单预览CEdit也未尝不可但CListBox的扩展性明显更好所以下面的源码我以CListBox为例。2. 核心实现源码从按钮点击到逐行上屏明确了技术选型接下来就直接上代码。先搭一个最简单的MFC对话框工程再逐步填充读取和显示逻辑。2.1 界面控件搭建步骤打开Visual Studio新建一个基于对话框的MFC工程。在资源视图的对话框模板上放置以下控件一个Button控件ID设为IDC_BTN_OPENCaption改为“打开txt文件”一个Edit Control用于显示文件路径ID设为IDC_EDIT_PATH只读属性可勾选一个ListBox控件ID设为IDC_LIST_CONTENT用于逐行显示文本可选一个Static Text提示当前加载的行数ID设为IDC_STATIC_LINE_COUNT控件摆放好后双击“打开txt文件”按钮VS会自动生成按钮点击事件的处理函数框架。接下来把核心代码写进这个函数里。2.2 最简实现CStdioFile逐行读取并AddString下面的代码适合大多数常规场景尤其是txt文件是ANSI编码也就是记事本默认保存的GBK编码时可以直接跑通。void CYourDlg::OnBnClickedBtnOpen() { CFileDialog dlg(TRUE, _T(txt), NULL, OFN_FILEMUSTEXIST, _T(文本文件(*.txt)|*.txt|所有文件(*.*)|*.*||), this); if (dlg.DoModal() ! IDOK) return; CString strPath dlg.GetPathName(); SetDlgItemText(IDC_EDIT_PATH, strPath); CListBox* pList (CListBox*)GetDlgItem(IDC_LIST_CONTENT); pList-ResetContent(); CStdioFile file; if (!file.Open(strPath, CFile::modeRead | CFile::shareDenyNone)) { AfxMessageBox(_T(文件打开失败请检查文件是否被占用或存在)); return; } CString strLine; int nLineCount 0; while (file.ReadString(strLine)) { pList-AddString(strLine); nLineCount; // 这里可以加一个条件比如每1000行处理一次界面消息避免长时间卡死 if (nLineCount % 1000 0) { // 把控制权交给系统让界面能响应拖动和重绘 MSG msg; while (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } } } file.Close(); }这段代码逻辑不复杂用CFileDialog选取文件CStdioFile按行ReadString每读一行就AddString到ListBox。注意Open时我加了CFile::shareDenyNone这个参数允许其他程序同时访问该文件避免因为文件正被别处打开而导致读取失败。2.3 加强版源码自动识别UTF-8和ANSI编码但如果txt文件是UTF-8编码存储的常见于从网上下载的配置、日志或跨平台导出的数据上面那段代码就会出问题——中文全部变成乱码。原因是CStdioFile在Unicode工程下默认按当前系统的ANSI代码页去解释文件内容遇到UTF-8字节序列就错乱了。要解决这个问题不能只用CStdioFile得先判断文件编码再做字节流到CString的转换。下面这段加强版代码我实际用在了项目里可以同时处理带BOM的UTF-8、不带BOM的UTF-8和ANSI编码。BOOL ReadTextFileByLines(CString strPath, CListBox* pList, int* pOutLineCount) { CFile file; if (!file.Open(strPath, CFile::modeRead | CFile::shareDenyNone)) return FALSE; ULONGLONG nFileLen file.GetLength(); if (nFileLen 50 * 1024 * 1024) // 50MB以上提示 { AfxMessageBox(_T(文件超过50MB建议分片读取或优化显示策略)); file.Close(); return FALSE; } BYTE* pBuf new BYTE[(size_t)nFileLen 2]; memset(pBuf, 0, (size_t)nFileLen 2); file.Read(pBuf, (UINT)nFileLen); file.Close(); // 判断编码 UINT nCodePage CP_ACP; // 默认ANSI int nOffset 0; if (nFileLen 3 pBuf[0] 0xEF pBuf[1] 0xBB pBuf[2] 0xBF) { nCodePage CP_UTF8; nOffset 3; // 跳过BOM } else if (nFileLen 2 pBuf[0] 0xFF pBuf[1] 0xFE) { // UTF-16 LE这里不做展开需要的话要用MultiByteToWideChar配合转换 nCodePage CP_UTF8; // 简化处理实际项目中建议单独处理 nOffset 2; } CStringA strTextA((LPCSTR)(pBuf nOffset), (int)nFileLen - nOffset); CStringW strTextW; int nLen MultiByteToWideChar(nCodePage, 0, strTextA, -1, NULL, 0); MultiByteToWideChar(nCodePage, 0, strTextA, -1, strTextW.GetBuffer(nLen), nLen); strTextW.ReleaseBuffer(); delete[] pBuf; // 按行拆分 int nStart 0; int nPos 0; int nLineCount 0; CString strLine; while (nPos strTextW.GetLength()) { if (strTextW[nPos] L\r || strTextW[nPos] L\n) { if (nPos nStart || strTextW[nPos] L\n) { strLine strTextW.Mid(nStart, nPos - nStart); pList-AddString(strLine); nLineCount; } if (strTextW[nPos] L\r nPos 1 strTextW.GetLength() strTextW[nPos 1] L\n) nPos; nStart nPos 1; } nPos; } // 处理最后一行不是换行结尾的情况 if (nStart strTextW.GetLength()) { strLine strTextW.Mid(nStart); pList-AddString(strLine); nLineCount; } if (pOutLineCount) *pOutLineCount nLineCount; return TRUE; }这段代码把文件内容按字节读入内存先识别BOM头再用MultiByteToWideChar把ANSI或UTF-8字节序列转换成Unicode的CStringW最后自己按\r\n拆分。这种方式规避了CStdioFile对编码的限制写一次就能长期复用。关于拆分逻辑这里补充说明一下很多人写逐行读取时只按\n拆分结果在Windows下会将\r一并显示出来或者产生多一个空行。我在上面代码里同时判断\r和\n并跳过相邻的\r\n组合这样读出来的每一行是干干净净的不包含多余符号。3. 关键技术细节字符集、性能与界面刷新你以为代码能跑就结束了现实场景永远比想的复杂。这一节我把项目里经常翻车的三个细节单独拿出来说。3.1 工程字符集到底怎么设置这是所有中文乱码问题的根源。Visual Studio创建MFC工程时默认字符集可以选择“使用Unicode字符集”或“使用多字节字符集”。如果你用的是Unicode字符集现在绝大多数新工程都是那么CString内部是宽字符wchar_tCStdioFile读取ANSI文件时内部会按系统代码页先转为宽字符这个过程在中文系统下一般正常但读取UTF-8文件时就不行了因为CStdioFile根本不知道文件是UTF-8。我一般建议新工程都用Unicode字符集因为Windows API原生支持UnicodeMFC控件也基于Unicode适配性好。至于对不同编码txt文件的兼容就依靠我在2.3节写的编码识别逻辑由代码主动判断文件编码而不是依赖系统默认行为。使用Unicode字符集后写字符串字面量时别忘加_T()宏例如_T(打开文件)这样可以保证代码在UNICODE和MBCS两种字符集下都能编译通过。3.2 大文件读取时的性能和安全问题读取超大txt文件几十MB甚至上百MB有两个问题要提前预判。第一是内存占用一次性Read全部字节到BYTE数组再转换内存峰值大约是文件大小的四倍原始字节 ANSI转Unicode时的临时缓冲。所以我给文件大小设了一个50MB的阈值超过就提示用户。如果确实需要读大文件建议改用分块读取搭配进度条反馈。第二是界面响应问题。CListBox的AddString在行数很多时比如十几万行每次插入都会触发内部重算和重绘程序会显得非常卡。网上有人推荐在批量AddString前后用SetRedraw(FALSE)和SetRedraw(TRUE)包起来实测效果明显。pList-SetRedraw(FALSE); // 循环AddString pList-SetRedraw(TRUE); pList-Invalidate();但这种方法要注意SetRedraw(FALSE)之后ListBox不会立刻重绘用户期间拖拽也看不到内容所以要在耗时操作完成后再SetRedraw(TRUE)。如果文件太大最好分批刷新比如每加1000行就让界面重绘一次兼顾响应速度和用户体验。另外不要在UI线程里用Sleep或过于复杂的循环。上面的示例代码里用PeekMessage主动处理界面消息就是避免整个窗口进入“未响应”状态的一个技巧。3.3 中文乱码问题的三种场景逐一排查我整理了一张乱码排查表你可以对号入座。现象可能原因解决方案所有中文变成“锟斤拷”文件是UTF-8但按ANSI读取按2.3节用MultiByteToWideChar指定CP_UTF8转换中文变成两个字符间的“?”文件是ANSI但系统代码页与文件编码不一致确认文件实际编码用对应代码页转换每行末尾多个小黑点或方块\r被当成显示内容按\r\n拆分时过滤掉\r显示出来是正常中文但保存后又乱转码方向做反了读取时用的是源文件编码到UNICODE保存时要反向操作4. 常见问题与排查技巧实录不能只给代码我还是把实际操作中遇到的几个典型问题拿出来复盘一下这些都是搜索引擎不好直接搜到答案的细节。4.1 CStdioFile的ReadString读到最后会漏一行吗有朋友问我ReadString在文件最后一行没有换行符的情况下是不是会漏读我专门做过测试CStdioFile的ReadString能正确处理这种情况只要文件中有内容哪怕最后没有\r\nReadString也会正常返回该行内容。这个问题真正需要注意的是如果文件最后一行只有一个换行符那么ReadString可能返回一个空字符串这取决于你的逻辑是否要跳过空行。在2.3节的加强版代码里我用nPos nStart这个条件过滤了一部分空行比如连续两个\r\n之间没有任何字符的情况。但如果你需要保留空行信息请把这个条件去掉。是否过滤空行要根据业务来定比如读取配置文件时空行可以直接跳过但读取日志时空行也是有意义的。4.2 路径含空格或中文打不开CFileDialog返回的路径一般不会有问题但如果你用路径拼接或者从配置文件里读路径就容易踩坑。路径中含空格时Open如果失败可以先检查路径是否以空格开头或结尾这种情况偶尔会出现。推荐使用PathTrimRight和PathRemoveBlanks这类工具函数对路径做一次清理。另外就是CFile::shareDenyNone参数如果不加当目标txt文件正被Excel或记事本打开时Open会失败。加了shareDenyNone之后允许其他进程读取同一个文件这个参数在文件操作中非常好用建议根据自己的实际需要加上。4.3 行数太多导致ListBox直接卡死CListBox底层封装的是Windows标准ListBox控件行数超过十万以后性能会急剧下降。原因在于每次AddString都会触发LB_ADDSTRING消息控件内部要重新计算所有项的高度。这里有两个解决思路第一如果只是查看改用CRichEditCtrl配合流式插入另一个方案是CListView的虚拟列表模式只渲染看得见的行。第二如果坚持用CListBox可以限制最大显示行数。比如读取到10000行时就停止并提示“文件行数超过显示上限”。更优雅的方式是给ListBox添加一个开关当读取到超过5000行时弹窗询问用户是否继续并提示“继续显示可能导致界面卡顿”。这个交互设计虽然朴素但用户体验不错至少不会让程序看起来像死机了。4.4 每次打开新文件前要清空ListBox忘记清空是新手常见错误。如果用户连续打开多个txt文件不清空的话第二个文件的行会追加到第一个文件后面。正确做法是在读取之前调用pList-ResetContent()。如果你想保留第一份文件的内容可以先把ListBox里的内容备份到另一个容器中这就看业务需求了。我在2.2节的代码里就示范了ResetContent的用法。4.5 Debug和Release版本表现不一致个别情况下同一个txt文件在Debug版本下读取正常Release版本下却少了几行或出现字符错乱。这通常不是代码逻辑问题而是没有对缓冲区边界做严格处理。比如我写的加强版代码中读到BYTE数组后最后一位一定要补0否则当你的代码把pBuf当字符串处理时可能越界读取到不该读的数据。我在ReadTextFileByLines里做了pBuf大小2并memset清零的处理就是为了规避这类问题。建议你在自己的代码里也养成这种习惯凡是C风格缓冲区务必清零并预留结束符空间。5. 一点实操体会这几年代码写了又改txt逐行读取这种基础功能我至少封装过五六版。最大的体会是文件读取不难难的是“兼容各种情况”。你永远猜不到用户会拿一个UTF-8文件还是ANSI文件可能还是一个带有UTF-8 BOM的诡异文件。所以在项目里做这种通用IO功能时我会刻意把代码写得“防御性”强一点编码判断做在前、缓冲区边界留足、界面上给用户反馈。这些细节不会让你的代码看起来多炫但能省掉大把线上排障的时间。后续如果还有什么不明白的比如需要在逐行显示的同时支持行号、查找高亮或者要改造为后台线程异步读取欢迎在评论区留言我可以再整理一篇实用扩展出来。本文还有配套的精品资源点击获取
返回列表