☰
VC读取TXT按行显示源码:编码、换行与大文件流式读取
2026/10/24 4:20:35 网站建设 项目流程

简介:面向VC++初学者和需要批量处理文本文件的开发者,这份源码工程演示在MFC环境中使用ifstream按行读取TXT并逐行输出显示,涉及文件打开与关闭、getline读取、字符串保存、打开失败判断和控制台打印等环节。工程可直接编译运行,也适合作为课程实验、文本预览小工具或二次开发的原型参考,代码结构清晰,便于逐步跟踪调试。压缩包共38个文件,约1.87MB,包含5个cpp与6个h等核心代码,rc、ico、bmp等界面资源,dsp、dsw工程配置,txt示例数据,以及obj、pdb、exe等编译调试文件,便于在VC++6.0中打开工程、梳理文档/视图结构并验证运行效果。已有257人学习,读者可从中理解MFC框架下文件流与界面显示的配合方式,并据此扩展路径选择、编码兼容、异常提示和按格式解析等实际需求,适合需要可运行示例来查漏补缺的初中级开发者。

1. 从一个小需求说起:为什么“读文本按行显示”在 VC 里没那么简单

前阵子同事扔过来一个两百多兆的日志文件,让我用 VC 做个界面,能一行行翻着看。我当时心想这不就是十几行代码的事,随手翻了一份 vc 读取 txt 文件 按行显示 源码 贴进工程,结果中文全变问号,滚到末尾少了一行,文件一换大界面直接白屏,三个坑一次踩全。这类需求看着入门,真做扎实要处理编码判定、换行符差异、大文件流式读取和控件重绘四件事。这份源码适合两类人:刚接触 MFC、想搞明白 CStdioFile 和文件流到底怎么用的是第一类;老工程维护者是第二类,手上有个必须跑在旧框架里的读文本功能,需要一份能直接改的底稿。

2. 工程骨架与技术选型:一个对话框工程怎么把“读文件”和“显示”拆开

2.1 目录结构:读取、编码、UI 三层分开

拿到一份“读取 txt 并显示”的源码,第一眼别看代码,看目录。绝大多数能拿来改的版本都是单文档或对话框工程,核心逻辑就那么几个文件。常见做法是按职责切三层:文件读取、编码转换、界面显示,这样后面要换控件或加功能时不用推倒重来。

TxtViewer/ ├─ TxtViewer.sln 解决方案 ├─ TxtViewer/ │ ├─ TxtViewer.h / .cpp 应用对象,注册窗口类 │ ├─ TxtViewerDlg.h / .cpp 主对话框,控件消息都在这里 │ ├─ TextFileReader.h / .cpp 读取封装:打开、逐行、分块、进度回调 │ ├─ EncodingUtil.h / .cpp 编码探测(BOM / UTF-8 校验)与宽窄转换 │ ├─ resource.h 控件 ID 定义 │ └─ TxtViewer.rc 对话框资源,列表控件和按钮布局

TextFileReader是对外的唯一入口,对外只暴露两件事:给一个路径,回调里给我一行文本;给一个进度回调,告诉我读到百分之几。EncodingUtil不碰文件,只接收一段char*加长度,返回CStringW。主对话框负责把回调拿到的东西塞进控件。这么拆的好处很直接,后面想把 ListBox 换成虚拟列表、或者想把读取搬到工作线程,改动范围都锁在一个文件里。

我见过不少流传的版本把这三层写在一个OnButtonRead()里,七八十行一气呵成。功能是能跑,但用户点第二次按钮就崩,因为CFile对象没关、列表没清、上次的缓冲区还在。分层不是为了好看,是为了出错的时候你知道该去哪个文件里找。

2.2 字符集设置决定了你后面所有代码的写法

这一步是分水岭。工程属性 → 常规 → 字符集,选“使用 Unicode 字符集”还是“使用多字节字符集”,直接决定CString是CStringW还是CStringA,也决定_T("...")展开成宽串还是窄串。

较新的 VS 版本建 MFC 工程默认给 Unicode,这是好事,但很多老源码是在多字节时代写的,直接拖进来会看到一大片const char*转LPCTSTR的编译错误。两种处理方式:一是把工程改回多字节字符集图个省事,二是保留 Unicode 并在边界处做一次转换。我一般选后者,因为 Windows 的窗口消息、控件接口原生就是宽的,改成多字节等于跟系统对着干,后面接别的库还要再翻一次。

// 统一在读取边界做转换,内部一律用宽字符 CStringW ToWide(const char* pData, int nBytes, UINT nCodePage) { if (pData == nullptr || nBytes <= 0) return CStringW(); // 先问需要多少宽字符,Multibyte 到 Wide 不会自动补 '\0' int nWide = ::MultiByteToWideChar(nCodePage, 0, pData, nBytes, nullptr, 0); if (nWide <= 0) return CStringW(); CStringW str; ::MultiByteToWideChar(nCodePage, 0, pData, nBytes, str.GetBuffer(nWide), nWide); str.ReleaseBuffer(nWide); // 长度按实际转换结果算,不靠 0 结尾 return str; }

参数上nCodePage传CP_ACP表示跟随系统当前代码页(中文环境就是 936),传CP_UTF8表示按 UTF-8 解。第三个参数pData配第四个参数nBytes使用时,源串不需要以 0 结尾,这对分块读取特别关键——你手上往往就是一截没有终止符的缓冲区。GetBuffer(nWide)会额外留一个终止位,ReleaseBuffer(nWide)必须传实际字符数,传-1会让它去找 0,缓冲区里没 0 就越界了。这个细节翻车过一次就记住了。

2.3 三种读文件方式的取舍

市面上的源码基本绕不开这三种读法,先把差异摆出来,后面看代码就不会懵。

读法核心接口优点短板适用场景
MFC 封装CStdioFile::ReadString一行一句,代码最短编码支持窄,UTF-8 无能为力小文件、本地代码页
CRT 流_tfopen_s+_fgetts可用ccs参数指定编码BOM 残留、宽窄模式混用易错中等文件、编码已知
Win32 字节流CreateFile+ReadFile完全掌控字节,可分块半行拼接、编码转换全自己写大文件、编码不定

CStdioFile是很多人第一份源码里出现的写法,胜在直观。它在 Unicode 配置下会按当前 C locale 去解字节,中文 Windows 默认 936,所以读 GBK 文件正常,读 UTF-8 文件就是一堆问号。这不是 bug,是它压根没打算管多编码。

_tfopen_s的ccs是 MSVC CRT 的扩展参数,写_T("r, ccs=UTF-8")就能让 CRT 在读取时直接做转换,省掉手工调MultiByteToWideChar。代价是 BOM 不会被吞掉,UTF-8 BOM 文件的第一行开头会多出三个不可见字符,显示出来就是行首莫名多个方块。

CreateFile+ReadFile是唯一能扛住超大文件的路径,因为你可以自己定块大小、自己决定什么时候转换。代价是所有细节归你:跨块的半行、被切断的多字节字符、末尾没有换行符的最后一行。

提示:如果是维护老工程且文件基本是 GBK,CStdioFile完全够用,别为了“先进”去上分块读取,反而增加出错面。

2.4 打开方式:共享读、只读、文本模式与二进制模式

CFile::Open的第二个参数是一组标志位或起来的,两个最容易忽略:shareDenyNone和typeText。

不加shareDenyNone,默认是独占打开。日志文件经常正被别的进程写着,你这边一点按钮就弹“文件被占用”。加上它,允许别人同时读写,你只读自己那份。

typeText和typeBinary的差别在换行处理。文本模式下,Windows 的\r\n会被 CRT 折叠成一个\n,你ReadString拿到的行尾是干净的;二进制模式下原样给你,行尾会残留一个\r,直接塞进控件看起来没区别,但一做字符串比较、一导出就露馅。

CStdioFile file; // shareDenyNone:允许其他进程同时打开;typeText:读取时折叠 CRLF if (!file.Open(strPath, CFile::modeRead | CFile::typeText | CFile::shareDenyNone)) { AfxMessageBox(_T("文件打开失败,检查路径或权限")); return; }

反过来,如果你走的是CreateFile那套字节流,就完全没有文本模式这回事了,\r得自己在拼行时剥掉。这也是后面分块代码里那句if (len > 0 && pending[start + len - 1] == '\r') --len;的由来。

3. 核心代码拆解:一行文本从磁盘到列表控件要经过几道手

3.1 最短路径:CStdioFile 逐行读 + ListBox 批量刷新

先给能立刻跑起来的最小版本,适合几千行以内的文件。这段代码很多源码包里都有,但十份里有八份漏了SetRedraw。

void CTextDlg::OnBnClickedBtnRead() { CString strPath = _T("D:\\data\\sample.txt"); CStdioFile file; if (!file.Open(strPath, CFile::modeRead | CFile::typeText | CFile::shareDenyNone)) return; CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LINES); pList->ResetContent(); // 先清空,否则点第二次会追加重复内容 pList->SetRedraw(FALSE); // 关键:批量插入期间关闭重绘 CString line; int nLineNo = 0; while (file.ReadString(line)) { ++nLineNo; CString strItem; strItem.Format(_T("%06d %s"), nLineNo, (LPCTSTR)line); pList->AddString(strItem); } pList->SetRedraw(TRUE); // 恢复重绘并主动刷新一次 pList->Invalidate(); file.Close(); }

ReadString(CString&)每次读一行,返回BOOL表示是否读到内容。SetRedraw(FALSE)到SetRedraw(TRUE)之间控件不响应绘制消息,插入一万行也只是内存操作,最后统一刷一次屏。不加这两句,每AddString一次就触发一次重排重绘,行数一多界面直接假死,用户以为程序崩了。

ResetContent()容易被忘。点第一次正常,点第二次列表变成两倍长,这是新手源码里最高频的翻车点。

3.2 编码探测:BOM、UTF-8 合法性校验与 CP_UTF8 转换

要同时应付 GBK 和 UTF-8 两种文件,就得在打开后先看一眼头几个字节,再决定用哪个代码页。

enum class TextEncoding { Ansi, Utf8, Utf8Bom, Utf16LE, Utf16BE, Unknown }; // 只在完整数据或文件头部调用,不要喂被块边界截断的片段 static bool IsValidUtf8(const BYTE* p, size_t n) { size_t i = 0; while (i < n) { BYTE c = p[i]; size_t extra = 0; if (c < 0x80) extra = 0; // 单字节 ASCII else if ((c & 0xE0) == 0xC0) extra = 1; // 2 字节序列 else if ((c & 0xF0) == 0xE0) extra = 2; // 3 字节序列 else if ((c & 0xF8) == 0xF0) extra = 3; // 4 字节序列 else return false; // 孤立的 0x80~0xBF 或 0xF8+ if (i + extra >= n) return false; // 序列被截断 for (size_t k = 1; k <= extra; ++k) if ((p[i + k] & 0xC0) != 0x80) return false; // 后续字节必须是 10xxxxxx i += extra + 1; } return true; } static TextEncoding DetectEncoding(const BYTE* p, UINT n) { if (n >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) return TextEncoding::Utf8Bom; if (n >= 2 && p[0] == 0xFF && p[1] == 0xFE) return TextEncoding::Utf16LE; if (n >= 2 && p[0] == 0xFE && p[1] == 0xFF) return TextEncoding::Utf16BE; // 没有 BOM 时按 UTF-8 语法校验;纯 ASCII 也会被判成 UTF-8,这是对的 return IsValidUtf8(p, n) ? TextEncoding::Utf8 : TextEncoding::Ansi; }

这个判定顺序有讲究。BOM 一定先看,EF BB BF是 UTF-8 BOM,FF FE和FE FF分别是 UTF-16 的小端和大端,四者互斥。没有 BOM 才走语法校验,因为 GBK 的双字节里第一个字节范围是 0x81 到 0xFE,很容易凑出看似合法的 UTF-8 序列,所以校验必须严格到每一个后续字节都符合10xxxxxx才算数。

IsValidUtf8有个使用前提写在注释里了:不要拿被块边界截断的片段去校验。一个 3 字节汉字被切成 2 字节,函数会返回 false,本来 UTF-8 的文件就被误判成 GBK。稳妥做法是校验完整文件,或者只校验开头 8KB 并且对齐到字符边界。

判定完之后,转换就一句ToWide(pData, nBytes, CP_UTF8)或CP_ACP。UTF-16 的两种直接memcpy到CStringW即可,大端需要先做一次字节序交换,这段代码量不大,源码包里一般在EncodingUtil.cpp最下面。

3.3 大文件流式读取:256KB 分块 + 按字节拼行

到这一步就得上ReadFile了。核心思路一句话:按字节拼行,凑齐一整行再解码。这样即使块边界正好切在一个汉字的三个字节中间,也不会影响结果,因为拼的时候拼的是原始字节,字节齐全了才转字符。

bool ReadLinesStreaming(LPCTSTR path, std::function<void(const CStringW&)> onLine, std::function<void(int)> onProgress) { HANDLE h = ::CreateFile(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (h == INVALID_HANDLE_VALUE) return false; LARGE_INTEGER li{}; ::GetFileSizeEx(h, &li); const ULONGLONG ullTotal = (ULONGLONG)li.QuadPart; const DWORD kChunk = 256 * 1024; // 256KB,机械盘和固态都合适的折中值 std::vector<char> buf(kChunk); std::string pending; // 上一块没凑齐整行的残余字节 ULONGLONG ullOffset = 0; while (ullOffset < ullTotal) { DWORD dwWant = (DWORD)min<ULONGLONG>(kChunk, ullTotal - ullOffset); DWORD dwGot = 0; if (!::ReadFile(h, buf.data(), dwWant, &dwGot, nullptr) || dwGot == 0) break; ullOffset += dwGot; pending.append(buf.data(), dwGot); // 只拼字节,不做任何解码 size_t nStart = 0, nPos; while ((nPos = pending.find('\n', nStart)) != std::string::npos) { size_t nLen = nPos - nStart; // 剥掉 CRLF 里的 '\r',兼容 Windows 与 Unix 两种换行 if (nLen > 0 && pending[nStart + nLen - 1] == '\r') --nLen; onLine(ToWide(pending.data() + nStart, (int)nLen, CP_UTF8)); nStart = nPos + 1; } pending.erase(0, nStart); // 剩下的半行留到下一块继续拼 if (onProgress) onProgress((int)(ullOffset * 100 / (ullTotal ? ullTotal : 1))); // 防御:整份文件没有任何换行符时会撑爆内存 if (pending.size() > 16 * 1024 * 1024) break; } // 文件末尾没有换行符时,pending 里还剩最后一行 if (!pending.empty()) { if (!pending.empty() && pending.back() == '\r') pending.pop_back(); onLine(ToWide(pending.data(), (int)pending.size(), CP_UTF8)); } ::CloseHandle(h); return true; }

几个参数值得说。kChunk取 256KB 是个经验折中,太小系统调用次数多,太大单次等待时间长、进度条跳得粗。pending用std::string而不是CString,因为这里存的是裸字节,CString在某些构造下会按当前代码页做转换,反而把字节搞坏。ullOffset用ULONGLONG而不是DWORD,超过 4GB 的文件用 32 位会直接溢出成负数。

最后那个pending的收尾不能省。绝大部分文本编辑器保存时会补一个换行,但不是所有,尤其程序生成的日志。少了这三行,文件最后一行就永远不显示,而且用户很难察觉,因为没有报错。

24 * 1024 * 1024那条防御是针对“整个文件一行”的极端情况,比如某些被压成一行的 JSON。没有它,pending会一路涨到文件大小,直接吃光内存。

调用方大约长这样,注意进度回调和列表刷新都在主线程,真正的读取如果要放到工作线程,界面更新得用PostMessage回主线程,别在子线程里碰控件。

ReadLinesStreaming(_T("D:\\data\\big.log"), [this](const CStringW& line) { m_lines.push_back(line); // 先存内存,后面交给虚拟列表 }, [this](int nPercent) { m_progress.SetPos(nPercent); }); m_list.SetItemCountEx((int)m_lines.size(), LVSICF_NOSCROLL);

3.4 显示层:虚拟列表与自绘的取舍

数据读进来了,怎么显示是另一回事。CListBox::AddString在几千行内没问题,到了十万行级别,光是控件内部的数据结构维护就够呛,百万行基本必死。

CListCtrl的LVS_OWNERDATA虚拟模式是标准解法:控件自己只保存总行数,每次重绘时问你要当前可见的那几十行文本。

// 虚拟列表只请求可见行,十万行和一百行在渲染上没有区别 void CTextListCtrl::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult) { NMLVDISPINFO* pInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR); if (pInfo->item.mask & LVIF_TEXT) { int nRow = pInfo->item.iItem; if (nRow >= 0 && nRow < (int)m_lines.size()) { _tcsncpy_s(pInfo->item.pszText, pInfo->item.cchTextMax, (LPCTSTR)m_lines[nRow], _TRUNCATE); } } *pResult = 0; }

pInfo->item.pszText和cchTextMax由控件提供,你只管往里写,用_tcsncpy_s加_TRUNCATE保证不越界。m_lines是std::vector<CString>,索引就是行号,随机访问 O(1)。

虚拟模式的代价是:滚动条、选中、排序、搜索都得自己维护。这也是为什么很多“读取 txt 显示”的源码在小文件场景下老老实实用 ListBox,因为改成虚拟列表之后功能代码量翻倍,收益只有超大文件才体现。

CEdit或CRichEditCtrl也能显示,好处是能直接选中复制、能设不同颜色,坏处是它们内部按字符存储,几十兆文本一次性SetWindowText会卡好几秒。真要用,就得分页——一次只喂 2000 行,翻页时再换。

4. 编译与运行:把这份源码跑起来要动的几个开关

4.1 工程属性里的三处必改项

拿到源码直接 F5,十有八九报一堆错。先看工程属性这三处。

第一处,常规 → 字符集。源码里大量裸char*就选多字节,大量CString配_T()就选 Unicode。选错了症状是满屏的“无法从 const char* 转换为 LPCTSTR”。

第二处,C/C++ → 语言 → C++ 语言标准。3.3 节那段代码用了std::function和std::vector,至少要 C++11,老工程默认可能是更早的标准,编译会报“未定义标识符 std::function”。改成 C++14 或 C++17。

第三处,链接器 → 系统 → 子系统。MFC 对话框工程必须是 Windows 子系统,配错了会出现一个黑框跟窗口一起弹出来。

# 命令行编译时的手动指定,等价于上面三处设置 cl /EHsc /std:c++17 /D_UNICODE /DUNICODE TxtViewer.cpp TextFileReader.cpp ^ /link /SUBSYSTEM:WINDOWS

/D_UNICODE和/DUNICODE一起加,前者影响 Windows API 的宏展开,后者影响 CRT 的宽字符函数映射,缺一个就会出现一半宽一半窄的诡异编译错误。

4.2 从空工程接上源码的完整步骤

如果源码包只给了几个 cpp,没给完整工程,按下面走一遍。

新建 MFC 应用 → 基于对话框 → 项目名随意 → 完成。资源视图里打开主对话框,拖两个控件上去:一个 List Control,ID 改成IDC_LIST_LINES;一个 Button,ID 用IDC_BTN_READ。双击按钮生成OnBnClickedBtnRead,把 3.1 节那段代码贴进去。

List Control 记得把 View 属性改成 Report,再加一列,否则加进去的行不显示。

// 初始化时补上列头,否则 Report 模式下只能看到一片空白 m_list.InsertColumn(0, _T("行号"), LVCFMT_LEFT, 80); m_list.InsertColumn(1, _T("内容"), LVCFMT_LEFT, 1200);

然后把TextFileReader.cpp和EncodingUtil.cpp通过“添加现有项”加进工程。源文件里的路径改成你自己的测试文件路径。整个流程十分钟以内能跑通。

4.3 用三份样本文件验证读取结果

验证别只拿一份 UTF-8 文件跑一遍就完事,那套不出问题。至少造三份,覆盖编码和换行符的交叉情况。

# 造三份测试样本,覆盖编码、换行符、末尾有无换行三种变量 lines = ["第一行 中文测试", "第二行 ascii only", "第三行 符号 ,。!"] # 1. GBK + CRLF + 末行无换行 with open("sample_gbk.txt", "w", encoding="gbk", newline="\r\n") as f: f.write("\r\n".join(lines)) # 2. UTF-8 带 BOM + CRLF + 末行有换行 with open("sample_utf8bom.txt", "w", encoding="utf-8-sig", newline="\r\n") as f: f.write("\r\n".join(lines) + "\r\n") # 3. UTF-8 无 BOM + LF with open("sample_utf8_nobom.txt", "w", encoding="utf-8", newline="\n") as f: f.write("\n".join(lines) + "\n")

三份文件的行数都应该是 3。第一份测 GBK 转换和末行丢失,第二份测 BOM 是不是被吞掉了,第三份测 LF 换行符能不能正确断行。三份都显示 3 行中文、没有方块、没有多出来的空行,读取层才算及格。

我就吃过只测 UTF-8 的亏,上线后用户拿了一份 GBK 的导出文件过来,满屏问号,现场改代码的滋味不好受。

5. 常见问题排查:乱码、丢行、白屏、卡顿的现场记录

5.1 编码与换行相关的四个坑

中文变问号或方块。现象是界面能显示行,但每行中文都是???或者一串方块。原因在于字节被按错误代码页解码了:UTF-8 文件被当成 936 解,或者反过来。解决是打开文件后先跑一遍DetectEncoding,用判定结果去选CP_UTF8还是CP_ACP,别硬编码一个代码页吃遍天下。判断不出来的场合,宁可给个下拉框让用户手动选。

第一行开头多出三个看不见的字符。现象是首行内容前面有个小方块,复制出去看是\ufeff。原因是 UTF-8 BOM 被当成正文了。解决是在探测到EF BB BF后,把数据指针往后挪 3 个字节再开始解析,或者在转换完的宽串上判断首字符是不是0xFEFF,是就Delete(0)。

最后一行不显示。现象是文件末尾明明有内容,列表里就是少一行。原因是读取循环只认\n,文件末尾那个没有换行符的尾巴被留在了残余缓冲区里没处理。解决是循环结束后补一次判断,pending非空就再输出一行。这个坑在程序生成的日志文件上几乎必现,人工编辑保存的文件反而少。

每行末尾多一个不可见字符。现象是显示正常,但按内容做精确比较、或者导回文件后发现行尾多个\r。原因是用二进制方式打开文件时没做 CRLF 折叠。解决要么改用typeText,要么在拼行时手动剥掉行尾的\r,也就是 3.3 节里那句--nLen。两种方式选一种,别混着来。

5.2 性能与资源相关的三个坑

文件一大就白屏假死。现象是点按钮后窗口没响应,标题栏显示“未响应”,过一会儿啪一下全部出来。原因是逐行AddString每次触发重绘,主线程被绘制消息占满。解决是SetRedraw(FALSE)包住整个插入过程,或者直接换成虚拟列表;再彻底一点,把读取放到工作线程,主线程只负责收结果。

滚动到中间突然卡顿。现象是前几百行很流畅,越往后滚越慢。原因是 ListBox 内部按索引查找的开销随项数增长,或者每行字符串都很长、控件宽度撑得过大。解决是换虚拟列表,同时给列表设一个合理的列宽上限,超长行做截断显示,完整内容放到双击弹窗里看。

打开大文件直接崩。现象是几十兆的文件运行时被系统终止,或者抛bad_alloc。原因是用CFile::Read一次性读到内存,或者CString拼接时反复重分配。解决是改成分块读取,pending加大小上限,std::vector用reserve预留容量。32 位进程的用户态地址空间只有 2GB 左右,一个几百兆的文件加上转换后的宽字符副本,翻四倍就爆了。

6. 二次开发改造:从“能看”到“能查、能筛、能导出”

跑通之后,真实需求基本都会往上加东西。下面这张表是我做过几次之后总结的优先级,按投入产出排。

改造点做法工作量风险
关键字搜索保留原始行文本,另存一个vector<int>命中索引,列表只渲染索引半天低,不动读取层
编码手动切换界面加下拉框,改nCodePage参数后重读两小时低
导出为 UTF-8遍历m_lines,WideCharToMultiByte(CP_UTF8)后写入两小时中,注意 BOM 要不要写
分页浏览一次只喂 2000 行给 Edit 控件,翻页换数据一天中,索引换算易错
工作线程读取读取放子线程,PostMessage回主线程更新进度一天中高,控件跨线程必崩

搜索这块展开说一下。最朴素的做法是每次输入都遍历全部行做Find,十万行以内感觉不出来,百万行就开始卡键盘。改成保留一份命中索引,输入变化时只重建索引、不动文本,列表通过虚拟模式按索引取行,输入响应能压到毫秒级。

// 只重建索引,不碰原始文本,虚拟列表按索引取行 void CTextDlg::RebuildFilter(const CString& strKey) { m_vecFilter.clear(); if (strKey.IsEmpty()) { for (int i = 0; i < (int)m_lines.size(); ++i) m_vecFilter.push_back(i); } else { for (int i = 0; i < (int)m_lines.size(); ++i) { if (m_lines[i].Find(strKey) >= 0) // 需要忽略大小写就换 FindNoCase m_vecFilter.push_back(i); } } m_list.SetItemCountEx((int)m_vecFilter.size(), LVSICF_NOSCROLL); m_list.Invalidate(); }

m_vecFilter存的是原始行号,这样导出时还能按原顺序还原,不会因为过滤丢掉上下文。注意SetItemCountEx之后一定要Invalidate,否则控件不知道数量变了,滚动条还是旧的。

另一个常被忽略的是导出时的换行符。用户拿这个文件去 Linux 环境用,你导出 CRLF 就会多出一堆^M。我一般给个选项,默认跟随源文件的换行风格,读取时顺手记一下第一行是\r\n还是\n。

从那以后我每次接文本文件相关的活,都先拿三份样本——GBK 带 CRLF 且末行无换行、UTF-8 带 BOM、UTF-8 无 BOM 用 LF——从头跑一遍再动业务代码,省下来的返工时间远比造样本的十分钟值。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询