Lesson 14:検索と置換に挑戦(2)


 前回に引き続き、検索と置換の機能を作成します。
 今回は、全置換(Replace All)と、選択した範囲内の全置換(Replace in Selection)を作成します。それと、検索画面を再表示すると、内容がクリアされてしまうので、内容を保持するようにもします。

 まずは、検索画面に手を加えましょう。
 全置換を行うためにReplace Allボタンを作成し、更に内容の保持ができるようにします。
 Replace Allボタンは、普通にボタンを作成するだけです。注意するのは、Replace AllはFindよりも文字数が多いので、ボタンの幅をFindよりも広くする必要がある点です。当然、ウィンドウの幅も広げなければなりません。
 ボタンを押した際のメッセージはMSG_REPLACEALLとして、動作は、MSG_FINDメッセージと同様に、必要事項を付加してBTinyEditorAppクラスに発行します。

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

#ifndef FINDWINDOW 
#define FINDWINDOW 
//--------------------------------------------------------------------- 
#include <Be.h> 
//--------------------------------------------------------------------- 
#define FINDWINDOW_TITLE            "Find" 
#define FINDWINDOW_POSITION_LEFT    80 
#define FINDWINDOW_POSITION_TOP     80 
#define FINDWINDOW_POSITION_WIDTH   352 
#define FINDWINDOW_POSITION_HEIGHT  68 
#define FINDWINDOW_WINDOWSTYLE      (B_TITLED_WINDOW)
//--------------------------------------------------------------------- 
#define MSG_FIND       'mfnd'
#define MSG_REPLACEALL 'mrpa'
//--------------------------------------------------------------------- 
class BFindView : public BView 
{ 
public: 
    BFindView(BRect frame); 
}; 
//--------------------------------------------------------------------- 
class BFindWindow : public BWindow 
{ 
public:
    BFindWindow(BRect frame,const char *title);
    virtual void MessageReceived(BMessage *msg); 
}; 
//--------------------------------------------------------------------- 
#endif

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

//--------------------------------------------------------------------- 
#include "BFindWindow.h"
//--------------------------------------------------------------------- 
BFindWindow::BFindWindow(BRect frame,const char *title) 
    :BWindow(frame,title,FINDWINDOW_WINDOWSTYLE,0) 
{ 
    BFindView *findview=new BFindView(Bounds());
    AddChild(findview); 

    SetDefaultButton((BButton *)findview->FindView("btnfind"));
    findview->FindView("txtfind")->MakeFocus();
} 
//--------------------------------------------------------------------- 
void BFindWindow::MessageReceived(BMessage *msg) 
{ 
    switch(msg->what) 
    { 
        case MSG_FIND: 
        case MSG_REPLACEALL:
            {
                msg->AddString("findword",
                    ((BTextControl *)FindView("txtfind"))->Text());
                msg->AddString("replaceword",
                    ((BTextControl *)FindView("txtrepl"))->Text());
                msg->AddBool("ignorecase",
                    (((BCheckBox *)FindView("chkcase"))->Value()
                                                   ==B_CONTROL_ON));
                be_app->PostMessage(msg);

                PostMessage(B_QUIT_REQUESTED);
            } 
            break;        
        default: 
            BWindow::MessageReceived(msg); 
    } 
} 
//--------------------------------------------------------------------- 
BFindView::BFindView(BRect frame) 
    :BView(frame,"findview",B_FOLLOW_ALL,B_WILL_DRAW) 
{ 
    BTextControl *txtfind=new BTextControl(BRect(8,8,240,20),"txtfind",
                                           "Find","",NULL);
    BTextControl *txtrepl=new BTextControl(BRect(8,28,240,40),"txtrepl",
                                           "Replace","",NULL);
    BCheckBox *chkcase=new BCheckBox(BRect(8,48,240,60),"chkcase",
                                     "Ignore case",NULL);
    txtfind->SetDivider(40);
    AddChild(txtfind);
    txtrepl->SetDivider(40);
    AddChild(txtrepl);
    chkcase->SetValue(B_CONTROL_OFF);
    AddChild(chkcase);
    AddChild(new BButton(BRect(256,8,336,20),"btnfind","Find",
                         new BMessage(MSG_FIND)));
    AddChild(new BButton(BRect(256,38,336,58),"btnfind","Replace All",
                         new BMessage(MSG_REPLACEALL)));
} 
//--------------------------------------------------------------------- 

 内容の保持については、BTinyEditorAppクラスのメンバ変数にfindword,replaceword,ignorecaseが既に用意されていますので、あとはこれをBFindWindowを表示する際にセットしてやるだけです。
 BFindWindowを表示するには必ずBFindWindowクラスのコンストラクタを呼び出していますので、引数にfindword,replaceword,ignorecaseを受け取るための変数を用意しておき、更にBFindViewクラスのコンストラクタに引き継いで、そこで初期値としてセットするようにします。

//--------------------------------------------------------------------- 
BFindWindow::BFindWindow(BRect frame,const char *title,
                    const BString &findword,const BString &replaceword,
                    const bool ignorecase) 
    :BWindow(frame,title,FINDWINDOW_WINDOWSTYLE,0) 
{ 
    BFindView *findview=new BFindView(Bounds(),findword,replaceword,
                                      ignorecase);
    AddChild(findview); 

    SetDefaultButton((BButton *)findview->FindView("btnfind"));
    findview->FindView("txtfind")->MakeFocus();
} 
//--------------------------------------------------------------------- 


//--------------------------------------------------------------------- 
BFindView::BFindView(BRect frame,
                    const BString &findword,const BString &replaceword,
                    const bool ignorecase) 
    :BView(frame,"findview",B_FOLLOW_ALL,B_WILL_DRAW) 
{ 
    BTextControl *txtfind=new BTextControl(BRect(8,8,240,20),"txtfind",
                                           "Find",findword.String(),
                                           NULL);
    BTextControl *txtrepl=new BTextControl(BRect(8,28,240,40),"txtrepl",
                                           "Replace",
                                           replaceword.String(),NULL);
    BCheckBox *chkcase=new BCheckBox(BRect(8,48,240,60),"chkcase",
                                     "Ignore case",NULL);
    txtfind->SetDivider(40);
    AddChild(txtfind);
    txtrepl->SetDivider(40);
    AddChild(txtrepl);
    chkcase->SetValue(ignorecase?B_CONTROL_ON:B_CONTROL_OFF);
    AddChild(chkcase);
    AddChild(new BButton(BRect(256,8,336,20),"btnfind","Find",
                         new BMessage(MSG_FIND)));
    AddChild(new BButton(BRect(256,38,336,58),"btnfind","Replace All",
                         new BMessage(MSG_REPLACEALL)));
} 
//--------------------------------------------------------------------- 

 前回のプログラムではBTinyEditorAppクラスのreplaceword,ignorecaseを初期化していなかったので、BTinyEditorAppクラスのコンストラクタで初期化するようにします。

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

//--------------------------------------------------------------------- 
BTinyEditorApp::BTinyEditorApp() 
    :BApplication(APPLICATION_SIGNATURE) 
{ 
    EditorList=new BList(); 
    open_filepanel= new BFilePanel(); 
    findword="";
    replaceword="";
    ignorecase=false;
} 
//--------------------------------------------------------------------- 

 これで、Replace Allを作成するための準備が整いました。次にReplace in Selectionの準備をしておきましょう。
 Replace in Selectionはメニューから起動したいと思いますので、BEditorViewクラスのコンストラクタで、
    searchmenu->AddSeparatorItem(); 
    searchmenu->AddItem(new BMenuItem("Replace in Selection",
                                      new BMessage(MSG_REPLACEINSELECTION)));
として、MSG_REPLACEINSELECTIONメッセージを発行するようにしておきます。

 全置換を行うのと、選択範囲内を置換する事の違いは、置換する対象が文章全てか選択された文章だけかの差でしかありません。そこで、置換範囲を指定して置換する関数をBEditorWindowクラスに準備しておいて、全置換の場合は、全文章を対象に、選択範囲内を置換する場合は、その範囲を対象として、関数を呼び出すようにします。
 置換範囲を指定して置換する関数をReplaceWords、全置換をReplaceAll、選択範囲内置換をReplaceInSelectionとします。

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

//--------------------------------------------------------------------
void BEditorWindow::ReplaceAll(const BString &findword,
                    const BString &replaceword,const bool ignorecase)
{
    BMemoView *memo=(BMemoView *)FindView("memo");
    int32 old_start,old_finish;

    memo->GetSelection(&old_start,&old_finish);
    ReplaceWords(findword,replaceword,ignorecase,
                 0,strlen(memo->Text()));
    memo->Select(old_start,old_finish);
}
//--------------------------------------------------------------------
void BEditorWindow::ReplaceInSelection(const BString &findword,
                    const BString &replaceword,const bool ignorecase)
{
    BMemoView *memo=(BMemoView *)FindView("memo");
    int32 old_start,old_finish;

    memo->GetSelection(&old_start,&old_finish);

    ReplaceWords(findword,replaceword,ignorecase,
                 old_start,old_finish);
    memo->Select(old_finish,old_finish);
}
//--------------------------------------------------------------------
void BEditorWindow::ReplaceWords(const BString &findword,
                    const BString &replaceword,const bool ignorecase,
                    const int32 start,const int32 finish)
{
    BMemoView *memo=(BMemoView *)FindView("memo");
    int32 point;
    BString wkstr;
    int32 init_length,wkfinish;

    wkstr.SetTo(memo->Text());
    point=finish;
    wkfinish=finish;
    init_length=wkstr.Length();

    while(point!=B_ERROR && point>=start)
    {
        if(ignorecase)
            point=wkstr.IFindLast(findword,point);
        else
            point=wkstr.FindLast(findword,point);
    
        if(point==B_ERROR)
        {
            wkstr.SetTo(memo->Text());
            wkfinish-=init_length-wkstr.Length();

            if(ignorecase)
                point=wkstr.IFindFirst(findword);
            else
                point=wkstr.FindFirst(findword);
        }

        if(point!=B_ERROR && point>=start && point<=wkfinish)
        {
            memo->Select(point,point+findword.Length());
            ReplaceWord(replaceword);
        }
    }
}
//--------------------------------------------------------------------

 ReplaceWords関数では、指定された範囲を後ろからFindLast(又はIFindLast)で検索して置換します。それを指定範囲から外れるかみつからなくなるまで繰り返します。
 ここで後ろから順に検索しているのは、wkstrにコピーした内容と置換後の内容の食い違いから、次の検索を行った際の位置情報にズレがでることを防ぐためです。
 たとえば、「aaabbbcccdddaaabbbccc」という文章があり、その文章の「bbb」を「z」に置きかえるとします。
 前方から検索しながら置換したとすると、wkstrを検索した結果は、3となります。ここを置換すると「aaazcccdddaaabbbccc」となりますが、wkstrを置換したわけではないのでwkstrの内容は「aaabbbcccdddaaabbbccc」のままです。
 このまま次の「bbb」を検索すると、15となりますが、置換後の15から3文字は「bcc」の部分となってしまいます。
 これを後方から検索するようにすれば、「aaabbbcccdddaaazccc」、「aaazcccdddaaazccc」と変化していくので、wkstrと置換後の位置情報にズレがないことになります。
 それと、検索結果がB_ERRORとなった際にFindFirstで検索しなおしているのは、FindLastが先頭を検索してくれないための苦肉の策です。本来ならばこのような処理は不要です。FindLastのバグでしょうか。

 かなり簡単な内容で下が、今回はここまでです。
 それと、前回書くのを忘れていたのですが、BStringクラスはマルチバイト文字列を扱う事を苦手としているようで、検索等の関数で、IFindFirst等の大文字/小文字を区別しないような関数では、日本語がうまく検索されません。日本語を含んだ検索には、大文字/小文字を区別した関数を使用する必要があります。

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

次の項目へ

トップページへ戻る