让fopen打开在不同的代码页下创建的文件

来源:互联网 发布:40万的suv 知乎2017 编辑:程序博客网 时间:2024/05/23 02:15

让fopen打开在不同的代码页下创建的文件


问题:
    英文Win2000下,应用程序操作一个文件名中带中文字符的文件时出错。

分析:
    文件是在中文Win2000下创建好之后,再拷贝到英文Win2000的,初步认为这涉及到代码页的问题!
    应用程序使用了一个第三方的工具包,提供的接口需要一个ANSI编码的文件名,而不是UNICODE的。
    进一步的分析发现,工具包是用fopen()来打开文件的。
    于是,问题变成了如何让fopen在不同的代码页之间也能正常工作?
   
    要解决问题,必须先弄清楚问题产生的具体原因。
   
    查看文档,Win2000的文件系统在保存文件时,实际记录了文件的两个名字:长文件名和短文件名。而长文件名是按UNICODE保存的,短文件名则是按ANSI保存的!
    显然上面的操作都是基于长文件名的。
    那么问题可以这样来理解:

    中文Win2000的代码页是936,在这个代码页下的字符的ANSI编码和UNICODE编码是一一对应的。
    文件的长文件名是UNICODE,而fopen()需要ANSI作为文件名参数,所以如果你想使用fopen(),必然需要UNICODE和ANSI之间的转换,而这个转换可能是你来做也可能是系统来做。比如你使用GetOpenFileNameA()来得到文件名,那么转换就不用你关心了;如果你非要用GetOpenFileNameW()的话,那么你的代码的下一行可能就是WideCharToMultiByte()了。总之,fopen的使用不会有什么问题。

    在文件迁移到英文Win2000后,问题就出现了。

    英文Win2000的代码页是437。一个中文字符的UNICODE映射到437是没有任何意义的,这称为unmappable character。一般情况下,unmappable character的转换结果会是一个'?'。所以转换之后,文件名中原有的信息就丢失了。这正是英文Win2000下fopen不能打开中文文件的原因。

解决:

    前面提到了,Win2000的文件系统是用ANSI编码来保存短文件名的。我们可以利用它来解决问题。

    怎么得到短文件名呢?有一个API是做这事的:

    DWORD GetShortPathName(LPCTSTR lpszLongPath, LPTSTR lpszShortPath, DWORD cchBuffer );

    GetShortPathName()需要一个长文件名(其实是PATH)作为参数,这就意味着我们只能使用GetShortPathNameW(),那么我们得到的短文件名也是UNICODE。那这不是存在和长文件名一样的问题吗???

    注意到一个事实:一个ANSI字符串,在不同的代码页有不同的含义。但是,在任意一个代码页下,将ANSI转为UNICODE,然后再从UNICODE转回ANSI,这个字符串保持不变,不会丢失任何信息!!!
   
    所以,我们可以放心地把得到的短文件名用WideCharToMultiByte()转为ANSI,再交给fopen使用。

      

示例代码:

 FILE * file = NULL;

 wchar_t wsShortName[1000] = {0,};

 OPENFILENAMEW ofn;
 wchar_t szFile[MAX_PATH];

 // Initialize OPENFILENAME
 ZeroMemory(&ofn, sizeof(ofn));
 ofn.lStructSize = sizeof(ofn);
 ofn.hwndOwner = NULL;
 ofn.lpstrFile = szFile;
 ofn.lpstrFile[0] = '/0';
 ofn.nMaxFile = sizeof(szFile);
 ofn.lpstrFilter = L"All/0*.*/0Text/0*.TXT/0";
 ofn.nFilterIndex = 1;
 ofn.lpstrFileTitle = NULL;
 ofn.nMaxFileTitle = 0;
 ofn.lpstrInitialDir = NULL;
 ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;


 if (GetOpenFileNameW(&ofn))
 {
  GetShortPathNameW(ofn.lpstrFile, wsShortName, 1000);
  char sShortName[1000] = {0};
  int length =WideCharToMultiByte(CP_ACP, 0, wsShortName, -1, sShortName, 1000, NULL, NULL);
  file = fopen(sShortName, "rb");
 }

 fclose(file);

分析:
    文件是在中文Win2000下创建好之后,再拷贝到英文Win2000的,初步认为这涉及到代码页的问题!
    应用程序使用了一个第三方的工具包,提供的接口需要一个ANSI编码的文件名,而不是UNICODE的。
    进一步的分析发现,工具包是用fopen()来打开文件的。
    于是,问题变成了如何让fopen在不同的代码页之间也能正常工作?
   
    要解决问题,必须先弄清楚问题产生的具体原因。
   
    查看文档,Win2000的文件系统在保存文件时,实际记录了文件的两个名字:长文件名和短文件名。而长文件名是按UNICODE保存的,短文件名则是按ANSI保存的!
    显然上面的操作都是基于长文件名的。
    那么问题可以这样来理解:

    中文Win2000的代码页是936,在这个代码页下的字符的ANSI编码和UNICODE编码是一一对应的。
    文件的长文件名是UNICODE,而fopen()需要ANSI作为文件名参数,所以如果你想使用fopen(),必然需要UNICODE和ANSI之间的转换,而这个转换可能是你来做也可能是系统来做。比如你使用GetOpenFileNameA()来得到文件名,那么转换就不用你关心了;如果你非要用GetOpenFileNameW()的话,那么你的代码的下一行可能就是WideCharToMultiByte()了。总之,fopen的使用不会有什么问题。

    在文件迁移到英文Win2000后,问题就出现了。

    英文Win2000的代码页是437。一个中文字符的UNICODE映射到437是没有任何意义的,这称为unmappable character。一般情况下,unmappable character的转换结果会是一个'?'。所以转换之后,文件名中原有的信息就丢失了。这正是英文Win2000下fopen不能打开中文文件的原因。

解决:

    前面提到了,Win2000的文件系统是用ANSI编码来保存短文件名的。我们可以利用它来解决问题。

    怎么得到短文件名呢?有一个API是做这事的:

    DWORD GetShortPathName(LPCTSTR lpszLongPath, LPTSTR lpszShortPath, DWORD cchBuffer );

    GetShortPathName()需要一个长文件名(其实是PATH)作为参数,这就意味着我们只能使用GetShortPathNameW(),那么我们得到的短文件名也是UNICODE。那这不是存在和长文件名一样的问题吗???

    注意到一个事实:一个ANSI字符串,在不同的代码页有不同的含义。但是,在任意一个代码页下,将ANSI转为UNICODE,然后再从UNICODE转回ANSI,这个字符串保持不变,不会丢失任何信息!!!
   
    所以,我们可以放心地把得到的短文件名用WideCharToMultiByte()转为ANSI,再交给fopen使用。

      

示例代码:

 FILE * file = NULL;

 wchar_t wsShortName[1000] = {0,};

 OPENFILENAMEW ofn;
 wchar_t szFile[MAX_PATH];

 // Initialize OPENFILENAME
 ZeroMemory(&ofn, sizeof(ofn));
 ofn.lStructSize = sizeof(ofn);
 ofn.hwndOwner = NULL;
 ofn.lpstrFile = szFile;
 ofn.lpstrFile[0] = '/0';
 ofn.nMaxFile = sizeof(szFile);
 ofn.lpstrFilter = L"All/0*.*/0Text/0*.TXT/0";
 ofn.nFilterIndex = 1;
 ofn.lpstrFileTitle = NULL;
 ofn.nMaxFileTitle = 0;
 ofn.lpstrInitialDir = NULL;
 ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;


 if (GetOpenFileNameW(&ofn))
 {
  GetShortPathNameW(ofn.lpstrFile, wsShortName, 1000);
  char sShortName[1000] = {0};
  int length =WideCharToMultiByte(CP_ACP, 0, wsShortName, -1, sShortName, 1000, NULL, NULL);
  file = fopen(sShortName, "rb");
 }

 fclose(file);