65.9K
CodeProject 正在变化。 阅读更多。
Home

CTextFileDocument

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (80投票s)

2004 年 8 月 12 日

Ms-RL

10分钟阅读

viewsIcon

574523

downloadIcon

10528

CTextFileDocument 允许您以不同的编码(支持 ASCII、UTF-8、Unicode 16 小端/大端)写入和读取文本文件。

Sample Image - textfilesample.gif

目录

引言

从前,文本文件只是一个简单的文件。但现在事情变得没那么简单了。换行符可以以三种不同的方式写入。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();
}

正如您所见,您可以使用 charwchar_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 字符串(例如 wstringCString(如果定义了 _UNICODE)),字符串将使用您选择的代码页进行转换。将 Unicode 字符串写入 ASCII 文件时也是如此(但方向相反)。

请记住,如果您将 ASCII 文件读取/写入非 Unicode 字符串,则不会进行转换。我稍后将展示如何进行从一个代码页到另一个代码页的转换。

当您将 Unicode 字符串转换为多字节字符串时,可能会发生某些字符无法转换的情况。这些字符默认会替换为问号('?'),但您可以通过调用 SetUknownChar() 来更改此设置。如果您想知道是否发生了这种情况,请调用 IsDataLost()

一些 Windows API

CTextFileDocument 使用 Windows 中的一些 API 来转换字符串:MultiByteToWideCharWideCharToMultiByte。使用这些函数时,必须定义多字节字符串的代码页。默认情况下,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 中找到 ConvertCharToWstringConvertWcharToString

//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 日 - 初始版本。
© . All rights reserved.