none
VS2013でUTF-8のファイルを出力すると文字化けします RRS feed

  • 質問

  • Shift-JISの文字列をUnicodeに変換→UTF-8に変換後、WriteString関数で、ファイルに出力すると、

    ファイル表示したときに、英語、日本語全ての文字が文字化けします。

    他の質問などでは、日本語文字だけ文字化けする、などの問題が書かれてあったのですが、私の問題は別問題でしょうか。

    Visual Studioでは、UTF-8を扱うことはできるのでしょうか?

    ちなみに、Unicodeに変換した後の文字列をファイル出力して試したときも、

    ファイルでは文字化けしたので、Unicode,UTF-8両方ともうまくいっていません。

    現在、VS2013を使っています。

    ファイルを開くのは、CStdioFileの_topenを使っています。

    WriteString関数で書き込みを行い、そのときは、シリアライズして書き込みを行っています。

    変換した後の文字列を、ファイルではなくメッセージで表示した場合では、

    英語の文字列はうまく表示できます。日本語の文字列のみ、文字化けしています。

    なので、少なくとも、英語の文字の変換はできているのだと思います。

    コードは、以下のように作成しています。

    ・・・

    CString in_str="out.c";

    SetLocale(LC_ALL,"jpn");//日本語文字の変換に対応させる(ネットに書いてありましたが、いまいちこれもうまくいっているのかわかりません)

    CStdioFile hFile(_topen(in_str,_T("w,ccs=UTF-8))); //UTF-8形式で、ファイルを開く

    CArchive ar(&hFile,CArchive::store);

    Write FileHeader(ar.attribute);

    CString str="abcあいう"; //変換する前の文字列

    CString strA=ConvSJisTOUtf8(str);//Shift-JisのCString文字列を、UTF-8の文字列に変換(ネットによく載っている関数を参考にして作成しました。)

    ar.WriteString(strA);//シリアライズで、文字列をファイルに書き込み

    ar.Close();

    hFile.Close();

    }

    /***以下、作成した、Shift-Jis文字列を、UTF-8文字列に変換する関数**/

    CString ConvSJisToUtf8(CString in_str)

    {

       int iMAX_PATH=(int)in_str.GetLength+1;//文字列の長さ(終端文字のため、+1

       char *SJis =new char[iMAX_PATH];

       strcpy(SJis,in_str);//char型に文字列をコピー

       CString strUtf8;

       wchar_t* Uni=NULL;

       if(SJis!=NULL){

          const int iLenUni=MultiBytoToWideChar(CP_ACP,0,SJis,-1,NULL,0);//文字列の長さ取得

          if(iLenUni){

             Uni=new wchar_t[iLenUni+1]; //Unicode用のサイズ確保

             if(MultiByteToWidechar(CP_ACP,-,SJis,-1,Uni,iLenUni)>0){ //Unicode用の文字列に変換

                const int iLenUtf8 = WideCharToMultiByte(CP_UTF8,0,Uni,-1,NULL,0,NULL,NULL); //UTF-8の文字列の長さ取得

                if(iLenUtf8){

                   char* Utf8=new char[iLenUtf8+1]; //Utf-8用のサイズ確保

                   if(Utf8!=NULL){

                      if(WideCharToMultiByte(CP_UTF8,0,Uni,-1,(LPSTR)Utf8,iLenUtf8,NULL,NULL){ //Utf-8の文字列に変換

                          Utf8[iLenUtf8]=0; //終端文字を0にする

                          strUtf8=Utf8; //CString型にUTF-8に変換した文字列を格納

                          AfxMessageBox(strUtf8,MB_ICONINFORMATION); //メッセージで、文字列を表示してみる。

                     }else{

                           delete Uni;

                           delete Utf8;

                      }

               }else{

                     delete Uni;

               }

            }

         }

      }

     return strUtf8;

    }   

    この処理で、ファイルを表示すると、全文字列が中国語のような文字で表示されます。

    これが解決した後に、BOMなし、改行コードも考慮してファイル作成を行うコードに変更したいと考えています。

    助言よろしくお願いいたします。

    2014年7月6日 11:07

回答

  • CArchive の補足。

    CArchive はMFCのシリアライザーオブジェクトで、CDocument::Serialize() で利用することを想定して設計されたオブジェクトです。

    CDocument::Serialize() は、OnOpenDocument()/OnSaveDocument() から呼ばれる仮想メソッドで(ちなみに、OnXxxxDocument() も仮想メソッドです)、そのドキュメントクラスのNativeデータを効率よくバイナリフォーマットで保存することを前提として設計されたクラスとなっています。

    特殊なファイル形式(特定の画像ファイル形式や、任意のテキストファイル形式)で読み書きする場合は、Serialize() を利用するのではなく、OnOpenDocument() や OnSaveDocument() を自身の都合のよい形に書き換えて利用します。

    この形式の典型的な例が、CRichEditDoc クラスです(ちなみに、ワードパッドのサンプルは今もこれで作られています。現行のワードパッドはどうか知りませんが)。

    今回質問者さんが行おうとしていることが、ドキュメントクラスのデータ保存かどうかは分かりませんが、もしそうなら、OnOpenDocument() や OnSaveDocument() を独自に実装する形で用意する形に変更することをお勧めします。


    とっちゃん@わんくま同盟, Microsoft MVP for Visual C++ (Oct 2005-) http://blogs.wankuma.com/tocchann/

    • 回答としてマーク 星 睦美 2014年7月28日 6:57
    2014年7月7日 8:14
  • BOMが自動付加になるんですね。つけたくないので、Write関数を使用することにします。

    BOMが付くのは_tfopen()に依るものなのでCArchive::Write()関数を使っても解決しません。ただし、CStdioFileからCFileに戻されるとのことですので、_tfopen()も使わなくなり解消されます。
    CFileを使う場合、既に書いたようにCArchive::WriteString()は文字コード変換しないためそのまま使えます。ただし、コンパイル時にUNICODEの定義の有無によりUnicode(UTF-16)で書かれるかANSI(SHIFT_JIS)で書かれるか決定される不安定なものなので、確実にするためにはやはりCArichive::Write()が必要です。

    またUTF-8への変換方法ですがもうちょっと楽な方法がありましたので紹介しておきます。#include <atlconv.h>した上で、

    CArchive ar;
    CString str = "あいう";
    
    CW2A utf8(CT2W(str), CP_UTF8);
    ar.Write(utf8, strlen(utf8));

    ATL と MFC の文字列変換マクロを使うもので、CW2A()はUncideからANSIに変換します。この際、第2引数にコードページが指定可能で、これを利用することでANSIではなくUTF-8を得ることができます。
    # CW2A(CT2W(str), CP_UTF8)をCT2A(str, CP_UTF8)に省略してしまうとstrがANSIだった場合に文字コード変換が行われなくなります。

    • 編集済み 佐祐理 2014年7月7日 12:37 ANSIの考慮
    • 回答としてマーク 星 睦美 2014年7月28日 6:57
    2014年7月7日 12:23

すべての返信

  • _topen()の戻り値はintですが正しいですか? 引数を見る限り_tfopen()と思われますが。

    fopen()によるUnicodeサポート・文字コード変換は書き込まれた文字列を変換する機能であり、この場合で言えばWriteString()に渡された文字列が自動的に変換されるものです。ですので呼び出し元で変換する必要はありません。

    ドキュメントによると

    Unicode モードで書き込むように開かれたファイルには、自動的に BOM が書き込まれます。

    とのことなので、BOMなしにする場合は自前で文字コード変換した上で、WriteString()ではなくWrite()を使った方がいいでしょう。

    なお、CStringは実際には次の3つがあります。

    • CStringA、常にANSI(日本語OSではSHIFT_JIS)
    • CStringW、常にUnicode
    • CString、コンパイル時定数UNICODEが定義されている場合はCStringWでありUnicode、ない場合はCStringAでありANSI(SHIFT_JIS)

    (CStringAでなく)CStringをSHIFT_JISと見なしたり、ましてやUTF-8を格納するのはナンセンスです。
    このことを踏まえて、例えばstd::vector<char>に格納する場合、

    std::vector<char> ConvToUtf8(const CStringW& in_str){
    	auto length = WideCharToMultiByte(CP_UTF8, 0, in_str.GetString(), -1, nullptr, 0, nullptr, nullptr); // NUL終端込みの長さを返す
    	std::vector<char> utf8(length);
    	auto result = WideCharToMultiByte(CP_UTF8, 0, in_str.GetString(), -1, &utf8[0], length, nullptr, nullptr);
    	return utf8;
    }
    std::vector<char> ConvToUtf8(const CStringA& in_str){
    	return ConvToUtf8(CStringW(in_str));
    }
    

    こんな感じになるでしょうか。バイト列として扱うのでNUL終端しない方がいいようにも思いますがとりあえず。

    2014年7月6日 12:33
  • すみません、呼び出し関係がものすごくややこしいですね。

    • CArchive::WriteString()はCArchive::Write()を呼び出す
    • CArchive::Write()はCFile::Write()を呼び出す
    • CFile::Write()はWriteFile()で書き込む
    • CStdioFileはCFile::Write()をオーバーライドする
    • CStdioFile::Write()はfwrite()を呼び出す
    • fwrite()は_write()を呼び出す
    • _write()は改行コード変換・文字コード変換をした後にWriteFile()で書き込む

    まとめると

    • CArchiveにCFileを渡した場合は何も変換されず指定された通りのバイト列になる
    • CArchiveにCStdioFileを渡した場合は(WriteStringに限らず)全ての書き込みに対して(それが文字列かどうかに関わらず)改行コード変換・文字コード変換が行われる

    # …であってるかな (汗

    2014年7月6日 15:24
  • 佐祐理さんのご指摘どおりですが、
    もっと簡単に言ってしまうと、

    1.CStdioFileクラスは、それが引数などとして扱う文字コードが
     コンパイル時に指定したものに固定されてしまうため、
     動作時に文字コードを指定したい場合には使用できない。

    ということですね。
    また、細かい仕様上の難点があるため、

    2.文字列に対して、厳密な制御を行いたい場合は、
     一般にCStdioFileクラスを使ってはならない。

    という結論が導けます。

    2014年7月7日 1:36
  • 大筋ではその通りですが細かい部分でいくつか指摘があります。

    1.CStdioFileクラスは、それが引数などとして扱う文字コードが
     コンパイル時に指定したものに固定されてしまうため、
     動作時に文字コードを指定したい場合には使用できない。

    CArchive、CFile、CStdioFile、stdio.h、io.hの全てを理解しているという前提が付きますが、_setmode()により文字コードを動的に変更できます。理解していても苦難の道ですが…。

    2.文字列に対して、厳密な制御を行いたい場合は、
     一般にCStdioFileクラスを使ってはならない。

    追加でコメントしていますが、文字列に限定されません。私自身CArchiveを扱ったことがないためどのようなストリームになるのか把握していませんが、バイト列全体に対して改行コード変換・文字コード変換を行います。まともな出力が得られるとは思えません。
    # 同じロジックで読み込めば論理的には読めるはずですが、今回のように出力結果を意識するのであれば…。

    というわけで、CArchiveとCStdioFileは組み合わせるべきではありません。その前提でCArchiveとCStdioFileのどちらを使うのかは質問者さんが決定できます。

    2014年7月7日 2:13
  • CArchive の補足。

    CArchive はMFCのシリアライザーオブジェクトで、CDocument::Serialize() で利用することを想定して設計されたオブジェクトです。

    CDocument::Serialize() は、OnOpenDocument()/OnSaveDocument() から呼ばれる仮想メソッドで(ちなみに、OnXxxxDocument() も仮想メソッドです)、そのドキュメントクラスのNativeデータを効率よくバイナリフォーマットで保存することを前提として設計されたクラスとなっています。

    特殊なファイル形式(特定の画像ファイル形式や、任意のテキストファイル形式)で読み書きする場合は、Serialize() を利用するのではなく、OnOpenDocument() や OnSaveDocument() を自身の都合のよい形に書き換えて利用します。

    この形式の典型的な例が、CRichEditDoc クラスです(ちなみに、ワードパッドのサンプルは今もこれで作られています。現行のワードパッドはどうか知りませんが)。

    今回質問者さんが行おうとしていることが、ドキュメントクラスのデータ保存かどうかは分かりませんが、もしそうなら、OnOpenDocument() や OnSaveDocument() を独自に実装する形で用意する形に変更することをお勧めします。


    とっちゃん@わんくま同盟, Microsoft MVP for Visual C++ (Oct 2005-) http://blogs.wankuma.com/tocchann/

    • 回答としてマーク 星 睦美 2014年7月28日 6:57
    2014年7月7日 8:14
  • 佐祐理さん

    すみません、せっかく助言いただいたのに、返信が遅くて申し訳ありません。

    せっかく、助言いただいたのですが、PCが壊れて助言いただいたことが試せない状態です。。。

    何日かかるかわかりませんが、復旧次第、助言を参考にさせていただきます。

    助言に対して、、、

    _tfopenの間違いでした。失礼しました。

    BOMが自動付加になるんですね。つけたくないので、Write関数を使用することにします。

    CStringに、UTF-8の文字列を入れるのは、よくないんですね。。。そういえば、どこかでみた気がするのに、、、

    では、StringWから、charに変換して、char型で書き込みを行う、という形にしてみます。

    std::Vectorの関数はぜひ参考にさせていただきます。最近触り始めたばかりで、

    まだstd::vectorについて、理解不足なところがあるので、調べてみます。

    CArchiveは、今回私が変更する前から、処理として行われていた部分であり、あまり変えたくはない、というのが本音なので、

    CStdioFileをやめて、CFileに変更してみようと思います。(他の人が業務で使うファイルを作成するという部分なので、影響がどれだけあるかわからない部分を変更しづらい、という本音があります、、)


    • 編集済み Shopstaff 2014年7月7日 10:49
    2014年7月7日 10:49
  • 仲澤@失業者さん

    助言ありがとうございます。

    PCが壊れており、いますぐ試せないので、助言いただいたのに申し訳ないですが、、、

    CStdioFileについて理解不足でした。

    もともとのコードは、CFileクラスでしたので、戻してみようと思います。

    PCが復旧次第試してみます。

    2014年7月7日 10:52
  • とっちゃんさん

    助言ありがとうございます。

    今回は、VCを使って作成した、「グラフィック作成ツール」を用いて作成した画のファイルを読み込んで、そこからCソースファイルにグラフィック情報を書き出す、という処理をしています。

    とすると、OnOpenDocument、OnSaveDocumentを自分で変更しないといけないようですね。。。

    今までは、書き出したCソースファイルをLinuxで使用するのですが、その前にUTF-8にコマンドで変更していました。

    その手間が省きたいだけ、ということなのですが、その手間を省くために対して、大変な変更となるのかな、、、と感じております。。

    OnOpenDocument、OnSaveDocumentについて、無知なので、勉強してみます。

    2014年7月7日 11:02
  • BOMが自動付加になるんですね。つけたくないので、Write関数を使用することにします。

    BOMが付くのは_tfopen()に依るものなのでCArchive::Write()関数を使っても解決しません。ただし、CStdioFileからCFileに戻されるとのことですので、_tfopen()も使わなくなり解消されます。
    CFileを使う場合、既に書いたようにCArchive::WriteString()は文字コード変換しないためそのまま使えます。ただし、コンパイル時にUNICODEの定義の有無によりUnicode(UTF-16)で書かれるかANSI(SHIFT_JIS)で書かれるか決定される不安定なものなので、確実にするためにはやはりCArichive::Write()が必要です。

    またUTF-8への変換方法ですがもうちょっと楽な方法がありましたので紹介しておきます。#include <atlconv.h>した上で、

    CArchive ar;
    CString str = "あいう";
    
    CW2A utf8(CT2W(str), CP_UTF8);
    ar.Write(utf8, strlen(utf8));

    ATL と MFC の文字列変換マクロを使うもので、CW2A()はUncideからANSIに変換します。この際、第2引数にコードページが指定可能で、これを利用することでANSIではなくUTF-8を得ることができます。
    # CW2A(CT2W(str), CP_UTF8)をCT2A(str, CP_UTF8)に省略してしまうとstrがANSIだった場合に文字コード変換が行われなくなります。

    • 編集済み 佐祐理 2014年7月7日 12:37 ANSIの考慮
    • 回答としてマーク 星 睦美 2014年7月28日 6:57
    2014年7月7日 12:23
  • >「グラフィック作成ツール」を用いて作成した画のファイルを読み込んで、そこからCソースファイルにグラフィック情報を書き出す

    という形だと、ファイルの読み書きではなく、エクスポート機能ではありませんか?

    OnOpenDocument は、メニューのファイル-開く から呼ばれる部分に当たるもので、Nativeデータの読み込み(画像編集がメインなら、もとになる画像データ)を行う機能になります。

    OnSaveDocument は、保存(名前を付けて保存を含む)の部分ですね。

    MFCで、Doc/View 機構を使うのであれば、これら二つはドキュメントファイルを扱う上ではきちんと考慮しておかなければならない部分(必ず派生するわけではないが、重要な処理部分)ですが、今回の場合は直接的には影響ないかもしれません。


    とっちゃん@わんくま同盟, Microsoft MVP for Visual C++ (Oct 2005-) http://blogs.wankuma.com/tocchann/

    2014年7月8日 1:15
  • 佐祐理さん

    >CW2A()はUncideからANSIに変換します。この際、第2引数にコードページが指定可能で、これを利用することでANSIではなくUTF-8を得ることができます。

    読んだあと、自分でも調べてみました。便利で、私の今回の問題も解決できそうなマクロそうなので、ありがたいです。

    助言いただいたコードに差し替えて試してみます。(PC復旧した数日後に、、、)

    2014年7月8日 14:11
  • とっちゃんさん

    すみません、誤解を与えてしまいました。エクスポート機能でした。

    >MFCで、Doc/View 機構を使うのであれば、これら二つはドキュメントファイルを扱う上ではきちんと考慮しておかなければならない部分(必ず派生するわけではないが、重要な処理部分)ですが、今回の場合は直接的には影響ないかもしれません。

    なるほど、もしかしたら、OnSaveDocumentを独自実装しなくても済むかもしれないのですね。

    ただ、ほかの解決方法がうまくいかなければ、ここをいじってみることを考えてみます。

    2014年7月8日 14:15