Lesson 13:検索と置換に挑戦(1)


 テキストエディタに欠かす事のできない機能として、検索機能があります。今回と次回で、BTinyEditorに検索機能を付けてみましょう。

 と、その前に、今までに作成した部分に二箇所ほどバグをみつけてしまいましたので、そこを直してしまいましょう。
 現象は、ウィンドウが表示された際に、テキストビューにカーソルが移っていないことと、ウィンドウの大きさを変更しても、テキスト編集部分の領域の大きさが変更されない点です。
 まず、カーソルの件ですが、これは、BEditorWindowクラスのコンストラクタで、
mainview->MakeFocus(); 
としているところを、
((BMemoView *)FindView("memo"))->MakeFocus(); 
とすればOKです。
 BMemoViewにMakeFocus関数でフォーカスをセットしなければいけなかったのに、mainviewにセットしていたために、カーソルが移らなかったわけです。

 もう一点はもっと単純な間違いで、テキスト編集部分の領域の大きさを変更する処理そのものが抜けていました。BMemoViewクラスにFrameResized関数を作成して、ビューの大きさが変更されたら、テキスト編集部分の領域の大きさも変更するようにしなければならなかったのです。
 具体的には、
//--------------------------------------------------------------------
void BMemoView::FrameResized(float width, float height)
{
    SetTextRect(Frame());
}
//--------------------------------------------------------------------
となります。


 バグを修正したら、いよいよ検索処理の作成に入りましょう。
 MS-Windowsの検索は、一般的にはダイアログがモーダルで表示されます。BeOSの場合、ダイアログが表示されている間も、文章を編集できるのが一般的です。(検索だけではなく、ダイアログ全般に言える事ですが)
 文書編集を行うウィンドウと検索ダイアログが対になるわけではないので、検索を行うウィンドウは、必ず最前面に存在する文書編集を行うウィンドウとします。また、検索ダイアログには検索する文字列、置換する文字列、検索条件、検索ボタンのみを配置します。置換や連続しての検索はメニューで実施します。

 今回もメッセージが重要ですので、前回同様、メッセージ一覧と処理をまとめておきます。
発行元クラス処理クラスメッセージ処理
-BEditorWindowB_WINDOW_ACTIVATED  アクティブになったかを確認し、そうであれば自分自身のポインタを付加してMSG_ACTIVATEを発行する。
BEditorWindowBTinyEditorAppMSG_ACTIVATE  メッセージからアクティブになったウィンドウを取得し、そのウィンドウをリストの最後尾に移動する。
BEditorWindowBTinyEditorAppMSG_SHOWFIND BFindWindowの実体を作成して表示する。
BFindWindowBFindWindowMSG_FIND  検索文字列、置換文字列、検索条件を付加してメッセージをBTinyEditorAppに発行する。
BFindWindowBTinyEditorAppMSG_FIND  メッセージから検索文字列、置換文字列、検索条件を取得し、リストの最後尾のウィンドウを対象に検索を実施する。
BEditorWindowBTinyEditorAppMSG_FINDNEXT  MSG_FINDで取得済みの条件で検索を実施する。
BEditorWindowBTinyEditorAppMSG_REPLACEFINDNEXT  MSG_FINDで取得済みの条件で選択されている文字列を置換し、その後検索を実施する。
-BFindWindowB_QUIT_REQUESTED  BFindWindowをクローズする。
 大きく分けると、三つの処理の流れになります。
 一つ目は、アクティブウィンドウが変更されたときの処理。これは、B_WINDOW_ACTIVATEDメッセージ→BEditorWindowクラス→MSG_ACTIVATE→BEditorWindowクラスとなります。
 二つ目は、検索画面を表示する処理で、BEditorWindowクラス→MSG_SHOWFINDメッセージ→BTinyEditorAppクラスとなります。
 三つ目は、検索/置換処理ですが、これは開始点の違いから更に三つの流れに分かれます。
 検索ダイアログの検索(Find)ボタンからの処理では、BFindWindowクラス→MSG_FINDメッセージ→BFindWindowクラス→MSG_FINDメッセージ→BTinyEditorAppクラスとなります。
 メニューの「次を検索(Find Next)」では、BEditorWindowクラス→MSG_FINDNEXTメッセージ→BTinyEditorAppクラスとなり、「置換して次を検索(Replace & Find Next)」では、BEditorWindowクラス→MSG_REPLACEFINDNEXTメッセージ→BTinyEditorAppクラスとなります。

 今回も、ソースファイルは圧縮ファイルをダウンロードするか、このページの末尾にそれぞれのソースのリンクを作成しておきましたので、そちらをご覧ください。

 まずは、アクティブウィンドウが変更されたときの処理を作成します。ウィンドウのアクティブ状態下編抗されると、B_WINDOW_ACTIVATEDメッセージがウィンドウに通知されます。B_WINDOW_ACTIVATEDメッセージを処理する関数はWindowActivatedですので、BEditorWindowクラスのメンバにWindowActivated関数を追加します。

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

//--------------------------------------------------------------------
void BEditorWindow::WindowActivated(bool active)
{
    if(active)
    {
        BMessage *msg=new BMessage(MSG_ACTIVATE);
        msg->AddPointer("wnd",this);
        be_app->PostMessage(msg);
    }
}
//--------------------------------------------------------------------

 MSG_ACTIVATEメッセージにwndの名前で自分自身のポインタを付加してBTinyEditorAppクラスに発行しています。このメッセージはBTinyEditorAppクラスのMessageReceived関数で処理します。

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

    case MSG_ACTIVATE:
        { 
            BEditorWindow *targetwnd; 
                        
            msg->FindPointer("wnd",(void **)&targetwnd); 
                        
            if(targetwnd!=NULL) 
            { 
                EditorList->RemoveItem(targetwnd);
                EditorList->AddItem(targetwnd);
            }
        }
        break; 

 こうしておけば、EditorList->ItemAt(EditorList->CountItems()-1)と指定することで、最上面のウィンドウを取得できるようになります。

 MSG_ACTIVATE等の定義は、MainWindow.hに追加しておきます。

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

#define MSG_ACTIVATE  'mwac' 
#define MSG_SHOWFIND  'msfd'
#define MSG_FINDNEXT  'mfdn'
#define MSG_REPLACEFINDNEXT 'mraf'

 次に検索画面を表示、検索を実行する処理です。
 ですがその前に、検索関係のメニューを追加してしまいます。
 BEditorViewクラスのコンストラクタに次の処理を追加します。

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

    BMenu *searchmenu=new BMenu("Search");
    searchmenu->AddItem(new BMenuItem("Find",new BMessage(MSG_SHOWFIND)));
    searchmenu->AddSeparatorItem(); 
    searchmenu->AddItem(new BMenuItem("Find Next",new BMessage(MSG_FINDNEXT)));
    searchmenu->AddItem(new BMenuItem("Replace & Find Next",
                                      new BMessage(MSG_REPLACEFINDNEXT)));
    searchmenu->SetTargetForItems(be_app); 
    mainmenu->AddItem(searchmenu);     

 SearchメニューのメッセージはすべてBTinyEditorAppクラスですので、searchmenu->SetTargetForItems(be_app);としておきます。

 検索画面のクラスはBFindWindowという名前にしました。
 BFindWindowクラスには、表示部分のBFindViewクラスを作成して、そこにボタン等を配置するようにします。

/**** ファイル名 : 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   312 
#define FINDWINDOW_POSITION_HEIGHT  68 
#define FINDWINDOW_WINDOWSTYLE      (B_TITLED_WINDOW)
//--------------------------------------------------------------------- 
#define MSG_FIND      'mfnd'
//--------------------------------------------------------------------- 
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.cpp ****/

//--------------------------------------------------------------------- 
#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: 
            {
                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_ON);
    AddChild(chkcase);
    AddChild(new BButton(BRect(256,8,296,20),"btnfind","Find",
                         new BMessage(MSG_FIND)));
} 
//--------------------------------------------------------------------- 

 特に目新しいことは行っていませんので、プログラムの詳細については省略します。
 BFindWindowクラスは、Find、Replaceの二つの入力欄と、Ignore caseというチェックボックス、Findボタンを持ちます。
 Ignore caseは、検索文字列の大文字/小文字を区別して扱うかの条件を設定します。
 Findボタンを押すと、MSG_FINDメッセージに各コントロールの設定値を付加して、BTinyEditorAppクラスに発行します。

 MSG_FINDメッセージを受け取ったBTinyEditorAppクラスは、メッセージから情報を取得し、「Find Next」メニューや「Replace & Find Next」メニューのために、それらを保存しておきます。保存しておくために、メンバ変数に、
    BString findword;
    BString replaceword;
    bool ignorecase;    
を追加しておきます。
 検索処理自体は、BEditorWindowクラスに用意しておき、BTinyEditorAppクラスはMessageReceived関数でMSG_FINDメッセージを受け取ったら、検索対象のウィンドウの検索処理(FindWord)を呼び出すようにします。

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

    case MSG_FIND:
        {
            BEditorWindow *targetwnd=(BEditorWindow *)EditorList->
                                    ItemAt(EditorList->CountItems()-1);

            msg->FindString("findword",&findword);
            msg->FindString("replaceword",&replaceword);
            msg->FindBool("ignorecase",&ignorecase);
            if(targetwnd!=NULL)
            {
                targetwnd->Lock();
                targetwnd->Activate();
                targetwnd->FindWord(findword,ignorecase);
                targetwnd->Unlock();
            }
        }
        break;

 検索処理(FindWord関数)は、検索する文字列と、大文字/小文字を区別するかの条件を引数として持ちます。
 BTextViewの内容はText関数で取得できますが、BeOSにはBStringクラスという、便利な文字列操作を行うことのできるクラスが用意されていますので、内容をBStringにコピーして検索を行ってみました。

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

//--------------------------------------------------------------------
void BEditorWindow::FindWord(const BString &findword,const bool ignorecase)
{
    BMemoView *memo=(BMemoView *)FindView("memo");
    int32 start,finish,point;
    BString wkstr;

    memo->GetSelection(&start,&finish);
    wkstr.SetTo(memo->Text());

    if(ignorecase)
        point=wkstr.IFindFirst(findword,finish);
    else
        point=wkstr.FindFirst(findword,finish);
    
    if(point!=B_ERROR)
        memo->Select(point,point+findword.Length());
}
//--------------------------------------------------------------------

 GetSelection関数で選択状態を取得して、選択の終了位置を検索の開始位置とします。
 検索にはBStringクラスのFindFirstを使用しますが、これは大文字と小文字を区別して検索しますので、ignorecaseの値によっては、区別をしないIFindFirst関数を使用するようにします。
 検索が成功したら、Select関数で検索された文字列を選択します。

 残るは「Find Next」メニューと「Replace & Find Next」メニューの処理です。
 Find NextはMSG_FINDNEXTメッセージで通知されます。Replace & Find NextはMSG_REPLACEFINDNEXTメッセージです。この二つの処理は、Replace & Find Nextが置換処理を行う以外は、まるで同じ処理内容になりますので、MessageReceived関数の中で一緒に処理してしまいます。

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

    case MSG_FINDNEXT:
    case MSG_REPLACEFINDNEXT:
        {
            BEditorWindow *targetwnd=(BEditorWindow *)EditorList->
                                    ItemAt(EditorList->CountItems()-1);
            if(findword!="" && targetwnd!=NULL)
            {
                targetwnd->Lock();
                targetwnd->Activate();
                if(msg->what==MSG_REPLACEFINDNEXT)
                    targetwnd->ReplaceWord(replaceword);
                targetwnd->FindWord(findword,ignorecase);
                targetwnd->Unlock();
            }
        }
        break;

 MSG_FINDNEXTの処理としては、保存しておいた検索条件を使用する以外は、MSG_FINDの場合の処理と同じです。
 MSG_REPLACEFINDNEXTの場合は、FindWord関数の前に、ReplaceWord関数による置換処理が加わります。

 ReplaceWord関数は、選択されている文字列を指定された文字列に置き換えます。

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

//--------------------------------------------------------------------
void BEditorWindow::ReplaceWord(const char *replaceword)
{
    BMemoView *memo=(BMemoView *)FindView("memo");
    int32 start,finish;

    memo->GetSelection(&start,&finish);
    
    if(start<finish)
    {
        memo->Delete(start,finish);
        if(replaceword.Length()>0)
            memo->Insert(start,replaceword.String(),replaceword.Length());
        memo->Modified=true;

        memo->Select(start,start+replaceword.Length());
    }        
}
//--------------------------------------------------------------------

 検索機能は、凝りだすといくらでも機能が付けられる部分なのですが、それこそ、何回分も使うことになってしまいますので、次回、あと少しだけ機能を追加したら、検索については終わりにしたいと思います。

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

次の項目・・・作成中

次の項目へ

トップページへ戻る