CTextFileDocument
CTextFileDocument 允许您以不同的编码(支持 ASCII、UTF-8、Unicode 16 小端/大端)写入和读取文本文件。
目录
引言
从前,文本文件只是一个简单的文件。但现在事情变得没那么简单了。换行符可以以三种不同的方式写入。Windows/DOS 使用字符 13 和 10,Macintosh 只使用字符 13,Unix 使用字符 10。为什么会这样一直让我感到困惑。不同的字符集使读取和写入文本文件变得更加困难,所以我很高兴我们有 Unicode 可以替代。但正如您可能知道的,用 Unicode 编写文本可以有很多种方式……
文本编码
我编写 CTextFileDocument
是因为我认为使用 Unicode 字符写入和读取文件过于复杂。我还希望该类能够处理普通的 8 位文件。从 1.20 版开始,读取/写入 ASCII 文件时支持不同的代码页。
该类可以读取和写入的编码是
CTextFileBase::ASCII |
简单的 8 位文件(支持不同的代码页)。 |
CTextFileBase::UTF_8 |
Unicode 编码的 8 位文件。一个字符可以用一、二或三个字节写入。 |
CTextFileBase::UNI16_BE |
Unicode,大端序。每个字符用两个字节写入。先写入最高有效字节。 |
CTextFileBase::UNI16_LE |
Unicode,小端序。每个字符用两个字节写入。先写入最低有效字节。 |
我在此页面上使用的大部分代码都是用于 Windows/MFC 的,但这些代码也应该可以在其他平台上运行。唯一的重大区别是代码页仅在 Windows 中受支持。在其他平台上,您应该使用 setlocale
来指定要使用哪个代码页。在 Windows 上不一定需要使用 MFC。
结构
CTextFileDocument
由三个类组成
CTextFileBase |
这是其他两个类的基类。 |
CTextFileWrite |
使用此来写入文件。 |
CTextFileRead |
使用此来读取文件。 |
基类中有一些有用的成员函数
class CTextFileBase { public: CTextFileBase(); ~CTextFileBase(); //Is the file open? int IsOpen(); //Close the file void Close(); //Return the encoding of the file (ASCII, UNI16_BE, UNI16_LE or UTF_8); TEXTENCODING GetEncoding() const; //Set which character that should be used when converting //Unicode->multi byte and an unknown character is found ('?' is default) void SetUnknownChar(const char unknown); //Returns true if data was lost //(happens when converting Unicode->multi byte string and an unmappable //characters is found). bool IsDataLost() const; //Reset the data lost flag void ResetDataLostFlag(); //Set codepage to use when working with none-Unicode strings void SetCodePage(const UINT codepage); //Get codepage to use when working with none-Unicode strings UINT GetCodePage() const; //Convert char* to wstring static void ConvertCharToWstring(const char* from, wstring &to, UINT codepage=CP_ACP); //Convert wchar_t* to string static void ConvertWcharToString(const wchar_t* from, string &to, UINT codepage=CP_ACP, bool* datalost=NULL, char unknownchar=0); }
前五个函数是最重要的,我希望它们的作用显而易见。其余的在处理不同的代码页时是必需的。
写入文件
写入文件非常简单。公共函数是
class CTextFileWrite : public CTextFileBase { public: CTextFileWrite(const FILENAMECHAR* filename, TEXTENCODING type=ASCII); CTextFileWrite(CFile* file, TEXTENCODING type=ASCII); //Write routines void Write(const char* text); void Write(const wchar_t* text); void Write(const string& text); void Write(const wstring& text); CTextFileWrite& operator << (const char wc); CTextFileWrite& operator << (const char* text); CTextFileWrite& operator << (const string& text); CTextFileWrite& operator << (const wchar_t wc); CTextFileWrite& operator << (const wchar_t* text); CTextFileWrite& operator << (const wstring& text); //Write new line (two characters, 13 and 10) void WriteEndl(); }
正如您所见,您可以使用 char
或 wchar_t
来写入文本(CString
也没问题)。示例
//Create file. Use UTF-8 to encode the file CTextFileWrite myfile(_T("samplefile.txt"), CTextFileWrite::UTF_8 ); ASSERT(myfile.IsOpen()); //Write some text myfile << "Using 8 bit characters as input"; myfile.WriteEndl(); myfile << L"Using 16-bit characters. The following character is alfa: \x03b1"; myfile.WriteEndl(); CString temp = _T("Using CString."); myfile << temp;
很简单,不是吗? :-).
读取文件
读取文件并不复杂。公共成员函数是
class CTextFileRead : public CTextFileBase { public: CTextFileRead(const FILENAMECHAR* filename); CTextFileRead(CFile* file); //Reading functions. Returns false if eof. bool ReadLine(string& line); bool ReadLine(wstring& line); bool ReadLine(CString& line); bool Read(string& all, const string newline="\r\n"); bool Read(wstring& all, const wstring newline=L"\r\n"); bool Read(CString& all, const CString newline=_T("\r\n")); //End of file? bool Eof() const; }
ReadLine
函数只是读取一行。示例 1
CTextFileRead myfile(_T("samplefile.txt")); ASSERT(myfile.IsOpen()); CString encoding; if(myfile.GetEncoding() == CTextFileRead::ASCII) encoding = _T("ASCII"); else if(myfile.GetEncoding() == CTextFileRead::UNI16_BE) encoding = _T("UNI16_BE"); else if(myfile.GetEncoding() == CTextFileRead::UNI16_LE) encoding = _T("UNI16_LE"); else if(myfile.GetEncoding() == CTextFileRead::UTF_8) encoding = _T("UTF_8"); MessageBox( CString(_T("Text encoding: ")) + encoding ); while(!myfile.Eof()) { CString line; myfile.ReadLine(line); MessageBox( line ); }
如果您想读取整个文件,请改用 Read
函数。示例 2
CTextFileRead myfile(_T("samplefile.txt"));
ASSERT(myfile.IsOpen());
CString alltext;
myfile.Read(alltext);
MessageBox( alltext );
文档/视图
如果您正在使用文档/视图,您可能希望在 Serialize
函数中保存和读取文件。这样做的一个问题是您不能关闭 CArchive
对象。如果您这样做,将会收到一个 ASSERT
错误。因此,您应该使用指定文件名的构造函数,而是使用使用 CFile
指针的构造函数。这样做时,文件在对象删除时不会关闭。下面的示例源自 CEditView
,并且不是使用仅读取 ASCII 文件的原始代码,而是可以读取 Unicode 文件
void CTextFileDemo2Doc::Serialize(CArchive& ar) { if(ar.IsStoring()) { #ifndef _UNICODE //Save in ASCII if not unicode version CTextFileWrite file(ar.GetFile(), CTextFileWrite::ASCII); #else //Save in UTF-8 in unicode version CTextFileWrite file(ar.GetFile(), CTextFileWrite::UTF_8); #endif CString allText; ((CEditView*)m_viewList.GetHead())->GetWindowText(allText); file << allText; } else { CTextFileRead file(ar.GetFile()); //Read text CString allText; file.Read(allText); //Data may be lost when the file is read. This happens when the //file is using Unicode, but your program doesn't. if(file.IsDataLost()) MessageBox( AfxGetMainWnd()->m_hWnd, _T("Data was lost when the file was read!"), NULL, MB_ICONWARNING|MB_OK); //Set text BOOL bResult = ::SetWindowText(((CEditView*)m_viewList.GetHead())->GetSafeHwnd(), allText); // make sure that SetWindowText was successful if (!bResult || ((CEditView*)m_viewList.GetHead())->GetWindowTextLength() < (int)allText.GetLength()) AfxThrowMemoryException(); } }
就是这样!
代码页/字符集
我希望到目前为止看到的大部分代码都相当直接易用。当您想处理不同的代码页(或“字符集”,我不明白其中的区别)时,会稍微复杂一些。
在 Unicode 之前,如何表示世界上某些地区使用的字符(a-z 不够)是一个问题。例如,我们这些住在瑞典的人喜欢字符“å”。“å”可以在 代码页 437 中找到。在那里,它的 ASCII 码是 134。但是,“å”也存在于 代码页 1252 中,但那里的 ASCII 码是 229!听起来复杂吗?等等,情况变得更糟!
在一些其他国家,会使用更复杂的字符,比如在韩国。这里的 ASCII 表不足以容纳所有字符,因此为了能够表示所有字符,有必要使用两个字节来表示某些字符。 代码页 949 包含许多多字节字符,例如这个:이 (代码:C0CC=U+C774)(如果您看不到该字符,请不要担心)。该字符由两个字节(192 和 204)表示。如果您使用代码页 949 打开一个使用此字符的 ASCII 文件,在记事本中,您将正确看到该字符。但如果您做同样的事情,但使用代码页 1252,您将看到两个字符(“ÀÌ”)。
显然,处理所有不同的代码页非常困难,这就是 Unicode 被发明出来的原因。在 Unicode 中,理念是只使用一种字符集,并且每个字符的大小都相同(不再需要多字节解决方案)。
所以 Unicode 很棒,但我们仍然需要处理使用不同代码页的文件。如果您定义了要使用哪个代码页,CTextFileDocument
就会为您处理这些(如果您不定义,它将使用系统使用的代码页,这通常效果很好)。
如果您将 ASCII 文件读取到 Unicode 字符串(例如 wstring
或 CString
(如果定义了 _UNICODE
)),字符串将使用您选择的代码页进行转换。将 Unicode 字符串写入 ASCII 文件时也是如此(但方向相反)。
请记住,如果您将 ASCII 文件读取/写入非 Unicode 字符串,则不会进行转换。我稍后将展示如何进行从一个代码页到另一个代码页的转换。
当您将 Unicode 字符串转换为多字节字符串时,可能会发生某些字符无法转换的情况。这些字符默认会替换为问号('?'),但您可以通过调用 SetUknownChar()
来更改此设置。如果您想知道是否发生了这种情况,请调用 IsDataLost()
。
一些 Windows API
CTextFileDocument
使用 Windows 中的一些 API 来转换字符串:MultiByteToWideChar
和 WideCharToMultiByte
。使用这些函数时,必须定义多字节字符串的代码页。默认情况下,CTextFileDocument
使用 CP_ACP
,这意味着将使用系统默认代码页。如果您想使用其他代码页,请调用 SetCodePage
。
设置代码页时,您必须确保代码页存在。通过调用 IsValidCodePage
来做到这一点。
要查看您的系统正在使用哪个代码页,请调用 GetACP()
。
要查看您的系统正在使用的所有代码页,您可以这样做
void ListCodePages() { EnumSystemCodePages(&EnumCodePagesProc, CP_SUPPORTED); } BOOL CALLBACK EnumCodePagesProc(LPTSTR lpCodePageString) { cout << "Code-page: " << lpCodePageString << endl; return TRUE; }
示例 1
好了,关于代码页的讨论够多了,这里有一个例子。下面的代码将一个 ASCII 文件(代码页为 437)读取到一个 Unicode 字符串。然后创建一个新的 ASCII 文件,并使用代码页 1252 写入该字符串。
如果您想将字符串从一个代码页转换为另一个代码页,应该这样做。将多字节字符串转换为 Unicode 字符串,然后再将 Unicode 字符串转换为多字节字符串。如果您不想将字符串写入文件,可以在 CTextFileBase
中找到 ConvertCharToWstring
和 ConvertWcharToString
。
//Make file reader. Read the file "ascii-437.txt" CTextFileRead reader("ascii-437.txt"); //Define which code-page to use when we read the file //437 are very often used in DOS. reader.SetCodePage(437); //Read everything to a Unicode-string wstring alltext; reader.Read(alltext); //Close file reader.Close(); //Now we create a new ASCII-file CTextFileWrite writer("ascii-1252.txt", CTextFileBase::ASCII); //Set which code-page to use. //1252 is very often used in Windows writer.SetCodePage(1252); //Do the writing... writer << alltext; //Was data lost when the Unicode-string was converted to //code-page 1252? if(writer.IsDataLost()) { //Do something... } //Close the file writer.Close();
示例 1b
正如我之前所说,CTextFileDocument
应该可以在 Windows 以外的平台中使用。如果您这样做,必须知道代码页的处理方式略有不同。不是调用 SetCodePage
,而是应该调用 setlocale
来定义要使用哪个代码页。下面的代码执行的操作与上一个示例相同,但可以在所有平台上运行(我希望 ;-))
//Make file reader. Read the file "ascii-437.txt" CTextFileRead reader("ascii-437.txt"); //Define which code-page to use when we read the file //437 are very often used in DOS. //NOTE: Make sure setlocale doesn't return an empty //string. If it do, you have probably tried to use //an code-page that your system doesn't support cout << setlocale(LC_ALL, ".437") << endl; //Read everything to a Unicode-string wstring alltext; reader.Read(alltext); //Close file reader.Close(); //Now we create a new ASCII-file CTextFileWrite writer("ascii-1252.txt", CTextFileBase::ASCII); //Set which code-page to use. //1252 is very often used in Windows cout << setlocale(LC_ALL, ".1252") << endl; //Do the writing... writer << alltext; //Was data lost when the Unicode-string was converted to //code-page 1252? if(writer.IsDataLost()) { //Do something... } //Close the file writer.Close();
关于代码
CTextFileDocument
最初是为使用 MFC 而编写的,但现在它更加平台无关。为了实现这一点,代码中包含了一些 #define
。最重要的一个是 PEK_TX_TECHLEVEL
,它定义了要使用哪些功能。但您不必考虑这一点,代码应该会自动正确定义它。下表解释了差异
PEK_TX_TECHLEVEL
= 0如果您在非 Windows 平台运行,将使用此项。此项在内部使用
fstream
来读取和写入文件。如果您想更改代码页,应调用setlocal
。PEK_TX_TECHLEVEL
= 1如果您不使用 MFC,则在 Windows 上使用此项。此项直接调用 Windows API 来读取和写入文件。如果某个内容无法读取/写入,将抛出
CTextFileException
。支持代码页。支持文件名的 Unicode。PEK_TX_TECHLEVEL
= 2如果您使用 MFC,则使用此项。此项在内部使用
CFile
来读取和写入文件。如果数据无法读取/写入,CFile
将抛出异常。支持代码页。支持文件名的 Unicode。支持CString
。
链接
关注点
即使这些类相当简单,它们对我来说也很有用。它们拥有我想要的所有功能,所以我并不缺少任何重要的东西。但是,如果它支持更多编码,比如 UTF-32,那就更好了。也许将来我会添加这个。性能相当不错,但如果您知道有什么方法可以提高速度,请告诉我 :-)。
也许可以提高性能的一个方法是增加 BUFFSIZE
(在 CTextFileBase
中定义)的值。另一件事是改进 CTextFileRead::GuessCharacterCount
中的代码。这应该返回文件中的字符数。目前,这只在您使用 MFC 时起作用,否则它将返回 1 MB。GuessCharacterCount
只在调用 Read
时使用,因此在调用 ReadLine
时不使用。
wchar_t
有多少字节?这取决于编译器,我认为这将来可能会给我带来一些问题。在 Windows 中,wchar_t
是两个字节,但我认为在 Unix 中使用四个字节。目前这不是一个问题,但如果我添加对 UTF-32(每个字符四个字节)的支持,可能会出现一些问题。
为什么 IsOpen()
不是一个 const
函数?我认为它应该是,但这是不可能的。原因在于 fstream::is_open()
不是 const
(嗯,在我的 VC6 中是,但在标准 C++ 中不是)。为什么是这样,对我来说是个谜。
这些类假定文件在文件的开头字节中有“字节顺序标记”(BOM)。这些字节告诉您使用了哪种编码。“大端序”文件的前两个字节是 0xFF 和 0xFE;如果您制作一个“小端序”文件,顺序则相反。如果编码是 UTF-8,前三个字节是 0xEF、0xBB 和 0xBF。如果没有找到 BOM,文件将被视为 ASCII 文件。
您可能会想,为什么我称这些类为 CTextFileDocument
。简单原因是因为 CTextFile 这个名字已经被占用了……在我想要上传这篇文章的几分钟前发现这一点真是令人沮丧 :-).
最后,感谢所有评论、发现 bug(并修复 bug)的人。这些类得到了极大的改进,感谢你们。
历史
- 2005 年 5 月 21 日 - 版本 1.22。
- 在读取所有内容之前读取一行可能会添加额外的换行符,已修复。
- 一个成员变量未始终初始化,可能导致读取单行时出现问题,已修复。
- 读取单行时使用更智能/更简单的算法。
- 2005 年 4 月 10 日 - 版本 1.21。如果无法在 techlevel 1 中打开文件,
IsOpen
返回了错误的结果。已修复。 - 2005 年 1 月 15 日 - 版本 1.20
- 修复:修复了将多字节字符串转换为 Unicode 以及反之的一些问题。
- 改进了转换例程。现在可以定义要使用哪个代码页。
- 现在可以设置在无法将 Unicode 字符转换为多字节字符时使用的字符。
- 现在可以查看转换过程中数据是否丢失。
- 对其他平台的支持更好,在 Windows 中不再需要使用 MFC。
- 修复:读取非常小的文件(1 字节)失败。
- 2004 年 12 月 26 日 - 版本 1.13
- 修复 1:如果文件第一行为空,则忽略该行。
- 修复 2:将多字节字符转换为宽字符以及反之出现问题。
- 2004 年 10 月 17 日 - 版本 1.12。打开文件失败时存在一个小的内存泄漏,已修复。
- 2004 年 8 月 28 日 - 版本 1.11。
WriteEndl()
在写入 ASCII 文件时未正确工作。已修复。 - 2004 年 8 月 13 日 - 版本 1.1。抱歉如此快速的更新。我重写了部分代码,因此它比以前的版本快了很多。
- 2004 年 8 月 12 日 - 初始版本。