Lesson 15:日本語って厄介だなぁ

 今回は今までとはちょっと変わった題材でいきます。それは日本語コードの扱いです。
 ですが、その前に、前回追加した全置換(Replace All)に、無限ループに突入してしまうバグがありましたので、それをなおしてしまいます。
 置換文字列に検索文字列と同じ内容を含んだ文字列を指定して置換した場合に、置換後に置換した文字列に含まれる検索文字列を検出してしまいます。そこで更にその文字列を置換するので、また検索文字列が出てきてしまい・・・、と無限ループになってしまいます。
 それを回避するには、検索結果の範囲が置換しても良い位置(前回置換した位置よりも前方)にあるかを判断してから置換するようにすればよいのです。それにはwkfinish変数に置換しても良い位置の最後尾を設定しておき、置換する際に、検索結果と比較します。
 実は、前回のプログラムでも、検索結果と比較は行っていたのですが、wkfinish変数に置換しても良い位置の最後尾を設定するのを忘れていました。
 もう、どこを直せばよいかわかりますよね? わからないようでしたら、今回のソースと、前回のソースのReplaceWords関数を見比べてみてください。

 さて、今回の話に戻ります。
 文字コードといえば、やはりASCIIコードでしょう。ですが、これは一文字を表すのに1バイトしか使用しないため、文字種の多い日本語を表すことはできません。そこで、JIS、SJIS、EUCといった日本語を複数バイトで表す日本独自の文字コードが昔から使われてきました。
 BeOSも日本語表示が可能ですが、BeOSはUnicode(UTF8)という文字コードを使用しています。Unicodeは日本独自ではなく、多国語対応した文字コードです。かなり乱暴な面のある文字コードなのですが、私自身、このコードについてはあまり理解していませんので、興味のある方は、ご自分でお調べください。
 BeOSを使用している方のほとんどは、Windowsを併用していることでしょう。LinuxやFreeBSD等のUNIXライクなOSを使っているかもしれません。 Windowsは日本語表記にはSJISが標準的に使用されています。UNIXならEUCですね。(Linux等では、Windowsとのことを考えて、SJISを使用している方も多いようです。PPCの方はMacOSでしょうけど、MacOSの文字コードって何なんでしょう?)
 こうなると、複数のOSをまたいでのファイルのやりとりには文字コードの変換が必須になります。一番簡単な方法としては、日本語表示可能なブラウザでファイルを読み込むことです。ですが、この方法は使い勝手が良くない上に、問題も色々とあります。ここはやはり、エディタに文字コードを変換する機能が欲しいですね。
 幸いにもBeOSにはUTF8との変換をおこなう関数が用意されていますので、BTinyEditorで実現するには、ファイルの読み込み時と保存時に、それを使用するようにするだけでよさそうです。

 各文字コードからUTF8に変換するconvert_to_utf8関数の宣言をみてみると、status_t convert_to_utf8(uint32 SourceEncoding, const char *source, int32 *sourceLength, char *destination, int32 *destinationLength, int32 *state, char substitute = B_SUBSTITUTE) となっています。
 source、sourceLengthは変換元の文字列とその長さ、destinationとdestinationLengthは変換結果の格納先とその大きさを指定するようです。問題はSourceEncodingで、これは変換元の文字コードを指定するようなのですが、文字列の文字コードを調べるための関数は存在しないのか、BeBookをみても探し出すことができませんでした。
 無いならば作るだけのことなのですが、運良くというか、都合よくというか、以前、遊びで作った文字コード変換ルーチンに文字コード判定のルーチンがありましたので、それを流用することにしました。(あまりまじめに作ったわけではないので、正しく機能するかは不安ですが・・・)

/**** ファイル名 : cccheck.h ****/

#ifndef CCCHECKH
#define CCCHECKH
//--------------------------------------------------------------------- 
#include <Be.h>
#include <string.h>
//--------------------------------------------------------------------- 
const uint32 ccBCType[7]={B_UNICODE_CONVERSION,B_UNICODE_CONVERSION,
                          B_JIS_CONVERSION,B_SJIS_CONVERSION,
                          B_EUC_CONVERSION,B_SJIS_CONVERSION,
                          B_UNICODE_CONVERSION};

enum CCODETYPE {ccBINARY,ccASCII,ccJIS,ccSJIS,ccEUC,ccSJISorEUC,ccOther};
enum CRETURNCODE {rcLF,rcCRLF};

void CheckCharCodeType(const unsigned char *sce,enum CCODETYPE &code,
                       enum CRETURNCODE &ret,const long checklen=1024);
//--------------------------------------------------------------------- 
#endif

 文字コードを判断するCheckCharCodeType関数は、四つの引数を持ちます。sceは調べる文字列のポインタ。code、retには結果の格納先。checklenは最大何文字まで調べるかを指定します。
 簡単にロジックの説明しますが、sceに指定された文字列を1文字ずつ調べていき、その文字が存在してもかまわない文字コードの種類を判断していきます。途中でその文字コードに存在してはいけない文字が現れたときは、バイナリとして返します。
 CCODETYPE、CRETURNCODEの列挙型は、それぞれ文字コードの種類と、改行コードの種類を列挙しています。
 ccBCTypeは、CCODETYPE型の変数をインデックスにして参照すれば、それに対応したconvert_to_utf8関数のSourceEncodingに渡す値を求めることができます。

 文字コード変換を行う必要があるのは、読み込みと保存時ですから、BEditorWindowクラスのOpenFile関数、SaveFile関数に変換ロジックを組み込みます。
 まずは読み込み時に文字コードを判断、UTF8へ変換し、保存時はUTF8から元の文字コードへ戻すようにしてみます。

 今まではファイルの読み込みにBTextViewクラスのSetText関数を使用していました。文字コードを変換するには、読み込みと変換用のバッファを準備しておき、そこで変換作業を行ってからBTextViewクラスに反映したほうがシンプルな構造になります。
 BEditorWindowクラスに、enum CCODETYPE fcode;とenum CRETURNCODE fret;を追加しておき、CheckCharCodeType関数で得た値を保存しておくために使用します。コンストラクタで初期化するときは、それぞれ、ccASCII、rcLFを設定しておきます。(このプログラムでは、JIS,SJIS,EUC以外はUTF8として扱います。)

/**** ファイル名 : MainWindow.cpp ****/

//--------------------------------------------------------------------- 
void BEditorWindow::OpenFile(BEntry *entry) 
{ 
    if(entry->Exists()) 
    { 
        BFile fil; 
        off_t size; 
        BString str; 
        unsigned char *buf; 

        try 
        { 
            fil.SetTo(entry,B_READ_ONLY | B_FAIL_IF_EXISTS); 
            fil.GetSize(&size); 

            buf=(unsigned char *)malloc(size); 
            fil.ReadAt(0,buf,size); 
            CheckCharCodeType(buf,fcode,fret); 

            if(ccBCType[fcode]!=B_UNICODE_CONVERSION) 
            { 
                int32 buflen,buf2len,state=0; 
                str.SetTo('\0',size*2); 
                char *buf2=str.LockBuffer(size*2); 
                buflen=size; 
                buf2len=size*2; 

                convert_to_utf8(ccBCType[fcode],(char *)buf,
                                &buflen,buf2,&buf2len,&state); 
                str.UnlockBuffer(); 
            } 
            else 
                str.SetTo((char *)buf); 

            if(fret==rcCRLF) 
                str.ReplaceAll("\r\n","\n"); 

            mainview->memo->SetText(str.String()); 

            free(buf); 
            fil.Unset(); 
             
            SetFileName(entry); 
        } 
        catch(...) 
        { 
            BAlert *altmsg = new BAlert("Error", 
                                        "File read failed!!","OK", 
                                        NULL,NULL,B_WIDTH_AS_USUAL, 
                                        B_WARNING_ALERT); 
            altmsg->Go(); 
        } 
    } 
} 
//--------------------------------------------------------------------- 

 bufに必要な大きさの領域を確保し、そこにファイルの内容を読み込みます。CheckCharCodeType関数で使用している文字コードを調べ、UTF8以外のコードであれば、convert_to_utf8関数を使って、文字コードの変換を行います。(変換先の領域にBStringクラスを使っているので注意してください。)
 また、改行コードがrcCRLFであるならば、BStringクラスのReplaceAll関数で、\r\nを\nで置き換えます。

 次は保存時の処理ですが、こちらはfcode、fretの値をみて、改行コード変換や文字コード変換を行ってfcode、fretに格納されている文字コードにしてから保存するようにするだけです。

/**** ファイル名 : MainWindow.cpp ****/

//--------------------------------------------------------------------- 
void BEditorWindow::SaveFile(BEntry *entry) 
{ 
    BFile fil; 
    off_t wsize; 
    off_t tsize; 
    BString str; 

    try 
    { 
        fil.SetTo(entry,B_READ_WRITE | B_CREATE_FILE); 

        str.SetTo(mainview->memo->Text()); 
            
        if(fret==rcCRLF) 
            str.ReplaceAll("\n","\r\n"); 

        if(ccBCType[fcode]==B_UNICODE_CONVERSION) 
        { 
            tsize=str.Length(); 
            wsize=fil.WriteAt(0,str.String(),tsize); 
        } 
        else 
        { 
            int32 buflen,buf2len,state=0; 

            buf2len=str.Length(); 
            buflen=buf2len*2; 
            char *buf=(char *)malloc(buflen); 
            
            convert_from_utf8(ccBCType[fcode],str.String(),
                &buf2len,buf,&buflen,&state); 

            tsize=buflen; 
            wsize=fil.WriteAt(0,buf,tsize); 
            free(buf); 
        } 
       
        if(tsize==wsize) 
        { 
            SetFileName(entry); 
            
            if(save_to_quit) 
                PostMessage(new BMessage(B_QUIT_REQUESTED)); 
        } 
    } 
    catch(...) 
    { 
        BAlert *altmsg = new BAlert("Error", "File write failed!!","OK", 
                        NULL,NULL,B_WIDTH_AS_USUAL, 
                        B_WARNING_ALERT); 
        altmsg->Go(); 
    } 
} 
//--------------------------------------------------------------------- 

 OpenFile関数のほぼ逆の事をやっていますので、特に難しい部分はないですよね。

 これでSJISやEUCのファイルを読み込んでも、普通に編集作業をして、保存時にも元の文字コードに戻す事が出来るようになりました。実際にUTF8以外のファイルを読み込んでみてください。(SJISなら簡単に用意できるでしょうから)

 一応の文字コード変換が出来るようになりましたが、そうせなら、任意の文字コードで保存できるようになっていたほうが便利です。そこで、文字コードを選択するためのメニューを追加しておいて、そのメニューでfcode、fretを変更できるようにしましょう。
 BEditorViewクラスのコンストラクタで、Fileメニューを作成している部分にCharacter Codeを追加します。

    BMenu *filemenu=new BMenu("File"); 
    filemenu->AddItem(new BMenuItem("Open...",new BMessage(MSG_OPEN))); 
    filemenu->ItemAt(0)->SetTarget(be_app); 
    filemenu->AddItem(new BMenuItem("New Text",new BMessage(MSG_ADDEDITOR))); 
    filemenu->ItemAt(1)->SetTarget(be_app); 
    filemenu->AddItem(new BMenuItem("Save",new BMessage(MSG_SAVE))); 
    filemenu->ItemAt(2)->Message()->AddBool("to_quit",false); 
    filemenu->AddItem(new BMenuItem("Save As...",new BMessage(MSG_SAVEAS))); 
    filemenu->ItemAt(3)->Message()->AddBool("to_quit",false); 

    savemenu=new BMenu("Character Code");
    savemenu->AddItem(new BMenuItem("UTF8",new BMessage(MSG_CCODECHANGE)));
    savemenu->ItemAt(0)->Message()->AddInt32("code",ccASCII);
    savemenu->AddItem(new BMenuItem("SJIS",new BMessage(MSG_CCODECHANGE)));
    savemenu->ItemAt(1)->Message()->AddInt32("code",ccSJIS);
    savemenu->AddItem(new BMenuItem("JIS",new BMessage(MSG_CCODECHANGE)));
    savemenu->ItemAt(2)->Message()->AddInt32("code",ccJIS);
    savemenu->AddItem(new BMenuItem("EUC",new BMessage(MSG_CCODECHANGE)));
    savemenu->ItemAt(3)->Message()->AddInt32("code",ccEUC);
    savemenu->AddSeparatorItem(); 
    savemenu->AddItem(new BMenuItem("LF",new BMessage(MSG_CRETCHANGE)));
    savemenu->ItemAt(5)->Message()->AddInt32("ret",rcLF);
    savemenu->AddItem(new BMenuItem("CR/LF",new BMessage(MSG_CRETCHANGE)));
    savemenu->ItemAt(6)->Message()->AddInt32("ret",rcCRLF);
    filemenu->AddItem(savemenu);

    filemenu->AddSeparatorItem(); 
    filemenu->AddItem(new BMenuItem("Close",new BMessage(B_QUIT_REQUESTED))); 
    filemenu->AddSeparatorItem(); 
    filemenu->AddItem(new BMenuItem("Quit",new BMessage(MSG_QUIT))); 
    filemenu->ItemAt(8)->SetTarget(be_app); 
    mainmenu->AddItem(filemenu); 

 savemenuは、BEditorViewクラスのメンバとして追加しておきます。MSG_CCODECHANGEとMSG_CRETCHANGEも、もちろん宣言しておきますよ。
 MSG_CCODECHANGEとMSG_CRETCHANGEには、code、retの名前で選択内容を付加しておきます。

 それと、今選択されているコードにチェックを付けるように、SetCCodeToMenu関数を作っておきます。

/**** ファイル名 : MainWindow.cpp ****/

//--------------------------------------------------------------------
void BEditorWindow::SetCCodeToMenu(void)
{
    const int  MenuIdxCode[7]={0,0,2,1,3,1,0}; //Menu Index
    const int  MenuIdxRet[2]={5,6}; //Menu Index

    for(int i=0;i<mainview->savemenu->CountItems();i++)
        mainview->savemenu->ItemAt(i)->SetMarked(false);

    mainview->savemenu->ItemAt(MenuIdxCode[fcode])->SetMarked(true);
    mainview->savemenu->ItemAt(MenuIdxRet[fret])->SetMarked(true);
}
//--------------------------------------------------------------------

 MenuIdxCode,MenuIdxRetはそれぞれfcode、fretからチェックを付けるメニューのインデックスを求めるために使用しています。

 この関数を呼び出すのは、メニューが選択されたときと、ファイルの読み込みを行ったとき、それと初期処理ですね。
 ファイルを読み込む処理では、成功した場合は必ずSetFileName関数を呼び出しますので、そこでSetCCodeToMenu関数を呼び出せば良いでしょう。初期処理としては、BEditorWindowクラスのコンストラクタでfcode、fretの初期化が終わったら呼び出すようにします。
 メニューが選択されたときの処理は、BEditorWindowクラスのMessageReceived関数にMSG_CCODECHANGEメッセージと、MSG_CRETCHANGEメッセージの処理として追加します。

        case MSG_CCODECHANGE:
            {
                int32 i;
                msg->FindInt32("code",&i);
                fcode=(enum CCODETYPE)i;
                SetCCodeToMenu();
            }
            break;
        case MSG_CRETCHANGE:
            {
                int32 i;
                msg->FindInt32("ret",&i);
                fret=(enum CRETURNCODE)i;
                SetCCodeToMenu();
            }
            break;

 メッセージから値を取り出して、fcode、fretに格納し、SetCCodeToMenu関数を呼び出します。

 最後にCheckCharCodeType関数がどのようにして文字コードを判断しているかを少しだけ説明します。
 それぞれの文字コードには、それぞれに特徴があります。JISコードならば、漢字コードは必ずKIコード、KOコードと呼ばれる3バイトのコードに挟まれています。SJISやEUCは漢字コードの1バイト目は必ず0x80以降(最上位ビットが1)の値です。また、漢字コードの1バイト目と2バイト目には、それぞれ漢字として使用される値の範囲が決まっています。
 そこでccBINARY,ccASCII,ccJIS,ccSJIS,ccEUC,ccSJISorEUC,ccOtherの7つの状態を作り、ccOtherを初期状態として状態遷移しつつ、文字列の先頭から順に調べていきます。
 JISはKIコードが存在するかで判断して、更にJISコードの1バイト目として有効なコードであれば、それに続くコードがJISコードの2バイト目として有効かどうかを調べていきます。
 SJISとEUCは、1バイト目のコードも2バイト目のコードも重なってしまう部分が多いので、そのコードがSJISなのかEUCなのかの判断が付かない場合があります。そこで、SJISでしか使用しないコード、EUCでしか使用しないコード、SJIS,EUCともに使用するコードの三つの範囲で漢字コードの1バイト目、2バイト目をチェックして文字コードの判断をします。
 UTF8はというと、私がUTF8を理解していませんので、JISでもSJISでもEUCでもないコードであれば、すべてUTF8として扱うようにしてしまいました。(アバウトですね。)
 それと、調べた範囲内でSJISかEUCかの判断が付かなかった場合は、SJISとして扱っています。それと、3バイト以上で構成されているEUCには対応していません。(ほんと、いいかげんだなぁ)
 文字コードの判断をしている際に、一緒に改行コードも調べていますが、こちらは単純で、0x0a,0x0dの並びのコードが存在すれば、rcCRLFとしています。

 今回作ったCheckCharCodeType関数は、簡易文字コード判断でしかありませんので、きちんとしたコードを調べるには使えないと思います。無いよりはまし程度です。練習用に作っているエディタには、この程度でも十分ですよね。

ソースリスト
圧縮ファイル
R5 Intel環境で確認
BTinyEditor20000628.zip
ソースファイル BTinyEditor.cpp
BTinyEditor.h
BFindWindow.cpp
BFindWindow.h
BMemoView.cpp
BMemoView.h
cccheck.cpp
cccheck.h
main.cpp
MainWindow.cpp
MainWindow.h

次の項目へ

トップページへ戻る