Lesson 8:メニューからのテキスト編集に挑戦


 いやぁ、仕事が忙しくて、なかなか先に進めないです。最近は疲れきってて、唯一の個人的な作業のできる時間(朝夕の通勤電車)も、寝てしまう事が多いもので・・・。
 今回からテキストエディタ作りに挑戦しましょう。今回は大まかな形とメニューからテキスト編集(コピー、カット、ペースト、アンドゥ、全選択)を行うところまでを行います。
 今回の作業では、特に新しいクラスは使用しません。今までに使用したクラスだけです。前回と前々回のちょっとした復習も兼ねてるんですね。

 いつものごとくBaseAppを雛型に新しいプロジェクトを作成します。プロジェクト名はBTinyEditorとでもしておきましょうか。プロジェクトを保存する際に、アプリケーションクラスをBTinyEditorApp、ファイル名BTinyEditor.cppとしておきます。また、MainWindowのクラスも、BAppMainViewをBEditorViewに、BAppMainWindowをBEditorWindowに変更しておきます。
今回からの数回はこのプロジェクトに手を加えていくことになります。
 プロジェクトのEditメニューのProject Settingsを選択し、設定画面を表示します。画面の左部分にリストが表示されていますので、その中からx86 ELF Projectを選択します。(ppcでは違う項目になっているはずですが、私はppcの環境を持っていませんので・・・。) File Name欄にBeAppと記述されていますので、これをBTinyEditorとします。これで、作成される実行ファイルの名称がBTinyEditorとなります。

 前々回にテキストビューを使用していますし、前回はメニューを使用しています。これらをポンポンと配置するように作ってしまっても良いのですが、ここは次回以降の事まで考えて、ちょっとだけ拡張しやすいように作ってしまいます。
 新しいファイルを作成して、BTextViewを継承するクラス、BMemoViewを作成します。BMemoViewにはコンストラクタとMessageReceived関数を用意しておきます。ファイル名はBMemoView.cppとBMemoView.hとします。

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

#ifndef BMEMOVIEW
#define BMEMOVIEW
//---------------------------------------------------------------------
#include <Be.h>
//---------------------------------------------------------------------
class BMemoView : public BTextView
{
public:
    BMemoView(BRect frame,const char *name,uint32 resizingMode);
    void MessageReceived(BMessage *msg);
};
//---------------------------------------------------------------------
#endif

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

//---------------------------------------------------------------------
#include "BMemoView.h"
//---------------------------------------------------------------------
BMemoView::BMemoView(BRect frame,const char *name,uint32 resizingMode)
    :BTextView(frame,name,BRect(0,0,frame.Width(),frame.Height()),
               resizingMode,B_WILL_DRAW)
{

}
//---------------------------------------------------------------------
void BMemoView::MessageReceived(BMessage *msg)
{
    switch(msg->what)
    {
        default:
            BTextView::MessageReceived(msg);
    }
}
//---------------------------------------------------------------------

 BTextViewのコンストラクタはBRect型の引数を二つ持っていていますが(二つ目の引数はBTextViewの中でのテキストの編集領域)、BMemoViewのコンストラクタではBRect型の引数は配置位置を指定するための引数だけにして、BMemoViewの領域いっぱいいっぱいを編集に使用するようにしています。
 MessageReceivedにはswitch文のdefaultで親(BTexctView)のMessageReceivedを呼ぶようにしておきます。
 このBMemoView.cppをプロジェクトに追加しておくのを忘れないでください。

 次にBEditorViewのコンストラクタで、メニューとBMemoViewの作成を行います。
 メニューの構成は、下記のようにします。
File Edit
Open Undo
Save
SaveAsCopy

Cut
Quit Paste
Select All
 FileメニューのOpen、Save、SaveAsは項目を準備しておくだけで、今回は使用しません。メニュー作成に使用するために、MSG_OPEN、MSG_SAVE、MSG_SAVEASのメッセージを定義しておきますが、それらをMessageRecievedで処理しません。QuitにはB_QUIT_REQUESTEDメッセージをセットしておきます。
 次にEditメニューの各項目ですが、ここにはテキストの基本的な操作を行うための機能を持たせます。その方法ですが、BeBookでBTextViewを調べてみると、BTextViewにはこれらの操作を行うためのメッセージが用意されているらしい事がわかります(正直に言うと、このメッセージの存在を知っていたから今回行うテキスト編集の機能を決めたんですけどね。)。B_UNDO、B_COPY、B_CUT、B_PASTE、B_SELECTALLがそれらのメッセージになります。
 それぞれのメッセージは今までと同じように「new BMessage(B_UNDO)」と作成してやれば良いようです。

 BMemoViewはメニューバーの領域を除いてBEditorView全体に配置します。今回はフォントのサイズを求めて、その倍をメニューバーの高さとしましたが、他にもっと良いやり方があるような気がします。(誰か教えてください。と書いておくものですね・・・。正しいやりかたの御教授いただきました。)
 SetDoesUndoでテキストのアンドゥを許可するようにしたら、コンパイルをかけて実行してみましょう。

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

#ifndef MAINWINDOW
#define MAINWINDOW
//---------------------------------------------------------------------
#include <Be.h>
#include "BMemoView.h"
//---------------------------------------------------------------------
#define MAINWINDOW_TITLE            "BTinyEditor"
#define MAINWINDOW_POSITION_LEFT    100
#define MAINWINDOW_POSITION_TOP     100
#define MAINWINDOW_POSITION_WIDTH   600
#define MAINWINDOW_POSITION_HEIGHT  400
#define MAINWINDOW_WINDOWSTYLE      (B_DOCUMENT_WINDOW)
//---------------------------------------------------------------------
#define MSG_OPEN    'mopn'
#define MSG_SAVE    'msav'
#define MSG_SAVEAS  'msva'
//---------------------------------------------------------------------
class BEditorView : public BView
{
public:
    BMemoView *memo;
    BEditorView(BRect frame);
};
//---------------------------------------------------------------------
class BEditorWindow : public BWindow
{
public:
    BEditorView *mainview;
    //-----------------------------------------------------------------
    BEditorWindow(BRect frame,const char *title);
    virtual void MessageReceived(BMessage *msg);
    //-----------------------------------------------------------------
    bool QuitRequested()
    {
        be_app->PostMessage(B_QUIT_REQUESTED);
        return true;
    };
};
//---------------------------------------------------------------------
#endif

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

//---------------------------------------------------------------------
#include "MainWindow.h"
//---------------------------------------------------------------------
BEditorWindow::BEditorWindow(BRect frame,const char *title)
    :BWindow(frame,title,MAINWINDOW_WINDOWSTYLE,0)
{
    mainview=new BEditorView(Bounds());    
    AddChild(mainview);
}
//---------------------------------------------------------------------
void BEditorWindow::MessageReceived(BMessage *msg)
{
    switch(msg->what)
    {
        case MSG_OPEN:
            break;
        case MSG_SAVE:
            break;
        case MSG_SAVEAS:
            break;
        default:
            BWindow::MessageReceived(msg);
    }
}
//---------------------------------------------------------------------
BEditorView::BEditorView(BRect frame)
    :BView(frame,"beditorview",B_FOLLOW_ALL,B_WILL_DRAW)
{
    BRect viewrect(Bounds());
    BMenuBar *mainmenu=new BMenuBar(BRect(0,0,viewrect.right,
                                          be_plain_font->Size()*2),
                                    "mainmenubar");
    AddChild(mainmenu);

    memo=new BMemoView(BRect(0,be_plain_font->Size()*2,
                 viewrect.right-B_V_SCROLL_BAR_WIDTH,
                 viewrect.bottom-B_H_SCROLL_BAR_HEIGHT),
               "memo",B_FOLLOW_ALL);
    BScrollView *scr=new BScrollView("memoscroll",memo, 
                     B_FOLLOW_ALL,0,true,true);
    AddChild(scr);

    BMenu *filemenu=new BMenu("File");
    filemenu->AddItem(new BMenuItem("Open...",new BMessage(MSG_OPEN)));
    filemenu->AddItem(new BMenuItem("Save",new BMessage(MSG_SAVE)));
    filemenu->AddItem(new BMenuItem("Save As...",new BMessage(MSG_SAVEAS)));
    filemenu->AddSeparatorItem();
    filemenu->AddItem(new BMenuItem("Quit",new BMessage(B_QUIT_REQUESTED)));
    mainmenu->AddItem(filemenu);

    BMenu *editmenu=new BMenu("Edit");
    editmenu->AddItem(new BMenuItem("Undo",new BMessage(B_UNDO)));
    editmenu->AddSeparatorItem();
    editmenu->AddItem(new BMenuItem("Copy",new BMessage(B_COPY)));
    editmenu->AddItem(new BMenuItem("Cut",new BMessage(B_CUT)));
    editmenu->AddItem(new BMenuItem("Paste",new BMessage(B_PASTE)));
    editmenu->AddItem(new BMenuItem("Select All",new BMessage(B_SELECT_ALL)));
    mainmenu->AddItem(editmenu);

    memo->SetDoesUndo(true);
}
//---------------------------------------------------------------------

☆2000年10月2日追記☆
 BMenuBarは高さを自動調整するそうですので、BMenuBarを作成する際に、be_plain_font->Size()*2といった姑息な方法で高さを指定する必要はないそうです。
 ただ、「AddChild(mainmenu);」の後で、「float height = mainmenu->Bounds().Height();」としても、正しい高さは取得できないそうです。
 BMenuBarの設定と、高さの取得は次のように行えばよいようです。
BMenuBar *mainmenu=new BMenuBar(BRect(0,0,0,0), "mainmenubar");
AddChild(mainmenu);
float height, width;
mainmenu->GetPreferredSize(&width, &height);
memo=new BMemoView(BRect(0,height + 1,
    viewrect.right-B_V_SCROLL_BAR_WIDTH,
    viewrect.bottom-B_H_SCROLL_BAR_HEIGHT),
    "memo",B_FOLLOW_ALL);
 GetPreferredSize関数でBMenuBarの幅と高さを取得し、BMemoViewの表示位置を決定しています。

 テキスト入力は普通にできますよね。次にメニューを試してみましょう。
 EditメニューのCopyやPasteを操作すると・・・、何も変化がありません。[Alt]+[C]や[Alt]+[V]等のショートカットキーを試してみると、これは正常に動いています。FileメニューのQuitを試すと、アプリケーションが終了します。どうも、B_COPYやB_PASTE等のメッセージの処理が正常に行えていないようです。
 なぜこのような動きになるのかを推測すると、B_COPY等のメッセージの送信先がおかしいのではないかと想像できます。
 これまで作ってきたアプリケーションは全てBWindowでメッセージを処理してきました(MessageRecievedやQuitRequestedです)。しかし、B_COPY等のメッセージはBTextViewが処理するメッセージです。当然、BTextViewに送らなければならないのではないでしょうか。(正確にはBTextViewを継承しているBMemoViewに送るのだとは思いますが)。
 そこで、まずはメニューからとショートカットキーでCopyを行い、B_COPYメッセージがどこに送信されるのかを確かめてみましょう。そのために、BEditorWindowとBMemoViewの両方のMessageRecievedにB_COPYを受けたら、BMemoViewのSetText関数を使用して、画面にどちらのクラスで受け取ったかを表示する処理を作っておきます。

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

//---------------------------------------------------------------------
void BEditorWindow::MessageReceived(BMessage *msg)
{
    switch(msg->what)
    {
        case B_COPY:
            ((BMemoView *)FindView("memo"))->SetText("BEditorWindow");
            break;
        case MSG_OPEN:
            break;
        case MSG_SAVE:
            break;
        case MSG_SAVEAS:
            break;
        default:
            BWindow::MessageReceived(msg);
    }
}
//---------------------------------------------------------------------

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

//---------------------------------------------------------------------
void BMemoView::MessageReceived(BMessage *msg)
{
    switch(msg->what)
    {
        case B_COPY:
            SetText("BMemoView");
            break;
        default:
            BTextView::MessageReceived(msg);
    }
}
//---------------------------------------------------------------------

 実際にこうして試してみると、メニューからはBEditorWindowに送られ、ショートカットキーではBMemoViewに送られている事が確認できました。
 後はメニューからメッセージを送信するときに、送信先をBMemoViewにする方法さえわかれば、メニューからテキスト編集する事ができるようになりそうです。
 またまたBeBookを見てみると、BMenuやBMenuItemのところにSetTargetForItemsだのSetTargetだのといった記述が見つかります。Targetなんて、いかにも怪しい名前を持ってますね。そこでSetTargetを調べてみると、やはりメッセージの送信先を設定する関数のようです。使い方はメッセージの送信先のBHandlerを指定するようです。BMemoViewはBTextViewを継承しています。BTextViewの継承元を調べていくと、BHandlerもありますので、BMemoViewを送信先にセットする事ができそうです。

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

//---------------------------------------------------------------------
BEditorView::BEditorView(BRect frame)
    :BView(frame,"beditorview",B_FOLLOW_ALL,B_WILL_DRAW)
{
    BRect viewrect(Bounds());
    BMenuBar *mainmenu=new BMenuBar(BRect(0,0,viewrect.right,
                                          be_plain_font->Size()*2),
                                    "mainmenubar");
    AddChild(mainmenu);

    memo=new BMemoView(BRect(0,be_plain_font->Size()*2,
                 viewrect.right-B_V_SCROLL_BAR_WIDTH,
                 viewrect.bottom-B_H_SCROLL_BAR_HEIGHT),
               "memo",B_FOLLOW_ALL);
    BScrollView *scr=new BScrollView("memoscroll",memo, 
                     B_FOLLOW_ALL,0,true,true);
    AddChild(scr);

    BMenu *filemenu=new BMenu("File");
    filemenu->AddItem(new BMenuItem("Open...",new BMessage(MSG_OPEN)));
    filemenu->AddItem(new BMenuItem("Save",new BMessage(MSG_SAVE)));
    filemenu->AddItem(new BMenuItem("Save As...",new BMessage(MSG_SAVEAS)));
    filemenu->AddSeparatorItem();
    filemenu->AddItem(new BMenuItem("Quit",new BMessage(B_QUIT_REQUESTED)));
    mainmenu->AddItem(filemenu);

    BMenu *editmenu=new BMenu("Edit");
    editmenu->AddItem(new BMenuItem("Undo",new BMessage(B_UNDO)));
    editmenu->ItemAt(0)->SetTarget(memo);
    editmenu->AddSeparatorItem();
    editmenu->AddItem(new BMenuItem("Copy",new BMessage(B_COPY)));
    editmenu->ItemAt(2)->SetTarget(memo);
    editmenu->AddItem(new BMenuItem("Cut",new BMessage(B_CUT)));
    editmenu->ItemAt(3)->SetTarget(memo);
    editmenu->AddItem(new BMenuItem("Paste",new BMessage(B_PASTE)));
    editmenu->ItemAt(4)->SetTarget(memo);
    editmenu->AddItem(new BMenuItem("Select All",new BMessage(B_SELECT_ALL)));
    editmenu->ItemAt(5)->SetTarget(memo);
    mainmenu->AddItem(editmenu);

    memo->SetDoesUndo(true);
}
//---------------------------------------------------------------------

 これでメニューを選択してみると・・・? BEditorWindowが受け取ってしまいました。
 それじゃ今度はSetTargetForItemsを・・・。やはりダメ。
 その後も、SetTargetItemを行うタイミングを変えてみたりと何度か試してみたんですが、どれもうまく行きませんでした。(うーん、どこが悪いんだろう・・・?)

 ここまできて手詰まりになってしまったので、今回はちょっと別の方法で逃げてしまうことにしました。
 ようはBMemoViewにB_COPY等のメッセージが送られてきたと認識させる事ができれば良いのですから、BEditorWindowで受け取ったB_COPY等のメッセージをBMemoViewに転送してしまえば良いのではないでしょうか。後はその方法ですが、これは今までにも散々使ってきた方法で大丈夫でしょう。
 今までいくつかのメッセージをMessageRecievedで処理してきましたが、MessageRecievedで処理を準備しておかなかったメッセージを受信したときはどうしていました? そう、継承元のクラスのMessageRecievedを呼び出して、そこで処理していましたよね。今回もこの方法が使えそうです。BEditorWindowのMessageRecievedでB_COPY等のメッセージを受け取ったらBMemoViewのMessageRecievedを呼び出して、そこで処理したらどうでしょう?
 BMemoViewの実体はBEditorWindowの中でmemoと定義していますから、BEditorWindowのMessageRecievedを
//---------------------------------------------------------------------
void BEditorWindow::MessageReceived(BMessage *msg)
{
    switch(msg->what)
    {
        case B_UNDO:
        case B_COPY:
        case B_CUT:
        case B_PASTE:
        case B_SELECT_ALL:
            ((BMemoView *)FindView("memo"))->MessageReceived(msg);
            break;
        case MSG_OPEN:
            break;
        case MSG_SAVE:
            break;
        case MSG_SAVEAS:
            break;
        default:
            BWindow::MessageReceived(msg);
    }
}
//---------------------------------------------------------------------
としてしまえば良いでしょう。
 ここで使っているmemoはメンバ変数のmemoではなく、BMemoViewを作成した時の名称の"memo"です。それをFindViewで探して、あとは取得したポインタをキャストして使用しています。

 これで実行してみると、今度はちゃんと、メニューからのCopyも、きちんとBMemoViewまで届きました。
☆2000年10月2日追記☆
 これについても情報をいただきました。
 SetTargetItemやSetTargetForItemsは、その対象がウィンドウにアタッチされてからでなければ意味をなさないとのことです。AddChild後にSetTargetItemを使用しなければなりません。

 もう一度実行して見て、一通りのテキスト編集が行えた事を確認して、今回はおしまいです。

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

次の項目へ

トップページへ戻る