第73回 プログラミングについて『テキストエディタに変身』

今回からは前回までのプログラムを簡単なテキストエディタに変身させることを目的にお話していきます。
まず前回のソースコード。
#include <afxwin.h>
#include "resource.h"
class MyApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
class MyWnd : public CFrameWnd
{
public:
MyWnd();
afx_msg void OnOpen();
DECLARE_MESSAGE_MAP()
};
class MyApp MyApp;
BOOL MyApp::InitInstance()
{
m_pMainWnd = new class MyWnd;
m_pMainWnd->SetIcon(LoadIcon(ID_MYAPP_ICON),TRUE);
return TRUE;
}
MyWnd::MyWnd()
{
Create(NULL,"",WS_OVERLAPPEDWINDOW | WS_VISIBLE ,rectDefault,NULL,
"MYAPP_MENU");
}
BEGIN_MESSAGE_MAP(MyWnd,CFrameWnd)
ON_COMMAND(IDM_OPEN,OnOpen)
END_MESSAGE_MAP()
afx_msg void MyWnd::OnOpen()
{
MessageBox("OnOpen was called !","Open file",MB_OK);
}
前回の後半でお話した様に、簡単なテキストエディタにするために CWnd クラスから派生した CEdit クラスを使用します。このクラスは私が JUM (テキストエディタ)を作成し始めたときに最初に利用しようとしたクラスでした。キーのカスタマイズや多くの編集機能を盛り込んだのですが、最初の頃はWindows3.1上で動作する16ビット版のもので、Windows3.1ではこのクラスに使用するテキストデータ用のメモリーがローカルメモリーと言って1つのアプリケーションで64Kバイトしか使えず、機能を盛り込めば盛り込む程、そのメモリーが別の目的で使用されるためにだんだんとテキスト用のメモリーが小さくなってしまい、最後には20Kぐらいしか使えなくなってしまいました。そこで16ビット版は諦めて、32ビット版に移植することにしました。
32ビット版になっても、ローカルメモリーを使用することは同じなのですが、このクラス専用に64Kバイトを使用できるのです。1行が80バイトとすると800行のテキストを扱えることになり、プログラムを書くならば2000行程度は扱えるだろうと考えていた訳です。そこでこのクラスを使用することを前提にしてそのまま開発を進めていたのですが、バックグランドの色と文字の色を変えられない、カーソルも変えられないなどの制約があり、どうしても速度の向上が望めないということが分かって最終的にはすべてを自分で作ることにしたという経緯があったのです。
高機能なテキストエディタを作るのは CEdit クラスを使用するには無理がありますが、簡易的なテキストエディタであれば開発期間を考えればおつりがくるかも知れません。
それでは実際に変身させていきましょう。
まずこれまでのプログラムに CEdit クラスのオブジェクトを組み込むことにしましょう。MyWnd クラスに組み込む CEdit クラスのポインタを保持するデータメンバーを追加します。ここではプライベート属性としましたがパブリック属性でも構いません。
class MyWnd : public CFrameWnd
{
private:
CEdit *edit_box;
public:
MyWnd();
afx_msg void OnOpen();
DECLARE_MESSAGE_MAP()
};
次に MyWnd のコンストラクタで実際に CEdit クラスのオブジェクトを作成します。
MyWnd::MyWnd()
{
RECT rect;
edit_box=NULL;
Create(NULL,"",WS_OVERLAPPEDWINDOW | WS_VISIBLE ,rectDefault,NULL,
"MYAPP_MENU");
GetClientRect(&rect);
edit_box=new CEdit();
edit_box->Create(
WS_CHILD | WS_VISIBLE | WS_DLGFRAME | WS_HSCROLL | WS_VSCROLL
| ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL,
rect,this,ID_EDIT_BOX);
::SetFocus(edit_box->m_hWnd);
}
この MyWnd::MyWnd の追加部分はかなり難しくなってしまいましたので詳しく説明します。まず CEdit クラスのオブジェクトは、
edit_box=new CEdit();
で生成されます。このクラスは2段階に分けて生成するようになっています。1段階目がこの行で、2段階目が、
edit_box->Create(
WS_CHILD | WS_VISIBLE | WS_DLGFRAME | WS_HSCROLL | WS_VSCROLL
| ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL,
rect,this,ID_EDIT_BOX);
なのですが、なんだか難しいことになってしまいました。第1引数は CEdit のオブジェクトのウインドウのスタイルの指定です。その内容は、
WS_CHILD
子ウインドウである。
WS_VISIBLE
初期状態で可視。
WS_DLGFRAME
ダイアログの枠を付ける。
WS_HSCROLL
横スクロールバーを付ける。
WS_VSCROLL
縦スクロールバーを付ける。
ES_MULTILINE
複数行。
ES_AUTOHSCROLL
横方向に自動的にスクロールする。
ES_AUTOVSCROLL
縦方向に自動的にスクロールする。
というものです。これらの定数は論理和にして1つの引数にします。第2引数はウインドウの位置とサイズを指定します。この引数は、
GetClientRect(&rect);
で取得した親ウインドウのクライアント領域をウインドウの位置をサイズにしています。またこの引数は参照によって渡すことになっていますので、& は付けずにそのままrect として指定します。第3引数は親ウインドウのポインタですのでここでは MyWndとするので this とします。this はオブジェクトそのもののポインタです。
第4引数は CEdit のオブジェクトのID番号で、何らかのメッセージが発生したときにはこの番号のメッセージが親ウインドウ(MyWnd)に通知されることになります。この定数は resource.h に自分でテキストエディタを使用して定義しても構いませんし、Visual C++で myapp.rc を開き、『表示』をクリックして『シンボルブラウザ』を指定し、新規として登録しても構いません。
次の行がちょっと見慣れないと思います。
::SetFocus(edit_box->m_hWnd);
キー入力を行なう際には、キーを入力したいウインドウにフォーカスがなければなりません。そのため初期状態でエディットボックスにフォーカスを合わせるために、CWndクラスやそれから派生したクラスには SetFocus という自分自身にフォーカスを合わせるメンバー関数があるのですが、CEdit は生成されたときには自分自身にフォーカスを合わすことはしませんので、別の方法が必要になります。そこで、クラスライブラリではなく、Windowsの関数 SetFucos を使用することになります。もし、
SetFocus(edit_box->m_hWnd);
とすると、CWnd のメンバー関数を呼び出そうとしてコンパイラがエラーを起こしますので、スコープを解決する演算子 :: を使用してグローバルな SetFocus を使用することを教えてやらなければなりません。またこの関数の引数にはエディットボックスのハンドル番号を指定しなければなりませんので、CWnd のデータメンバーの m_hWnd を渡す様にします。
それではこのプログラムをコンパイル、リンクして実行してみましょう。スクロールバーが新たに追加され、左上にはカーソルが点滅しているはずです。試しにキー入力を行なうと、嬉しいように表示されます。これで簡易テキストエディタの第一歩を踏み出したことになります。
ところが、嬉しくないことはウインドウのサイズをマウスで変更してもエディットボックスのサイズが変わってくれないのです。そこで、ウインドウのサイズが変わったときにエディットボックスのサイズも変更される機能を追加しましょう。
MyWnd クラスに、
afx_msg void OnSize(UINT nType,int cx,int cy);
を追加し、次のようにします。
class MyWnd : public CFrameWnd
{
private:
CEdit *edit_box;
public:
MyWnd();
afx_msg void OnOpen();
afx_msg void OnSize(UINT nType,int cx,int cy);
DECLARE_MESSAGE_MAP()
};
次に、MyWnd のメッセージマップに ON_WM_SIZE() を追加し次のようにします。
BEGIN_MESSAGE_MAP(MyWnd,CFrameWnd)
ON_COMMAND(IDM_OPEN,OnOpen)
ON_WM_SIZE()
END_MESSAGE_MAP()
ON_WM_SIZE() はコンパイルされると、そのクラスの OnSize 関数を呼び出すようにマクロ定義されているもので、その関数の型と引数は、
afx_msg void OnSize(UINT nType,int cx,int cy);
です。ですので MyWnd クラスにこれと同じ定義を追加した訳です。クラスにこの定義を追加しないと、コンパイルしたときにはメッセージマップの定義が無視されてしまいます(基本クラスのものが使用されてしまう)ので注意して下さい。
最後に MyWnd::OnSize の実体は、
afx_msg void MyWnd::OnSize(UINT nType,int cx,int cy)
{
RECT rect;
if(edit_box==NULL)return;
GetClientRect(&rect);
edit_box->MoveWindow(&rect);
}
とします。この関数では親ウインドウのサイズを取得してエディットボックスのウインドウを移動させる操作をしているだけなのですが、問題は、
if(edit_box==NULL)return;
です。実は、OnSize は WM_SIZE メッセージが発生したときに呼び出される関数なのですが、親ウインドウが生成されたときにも発生しているのです。ということは、MyWndのコンストラクタ部分では、エディットボックスが生成される前に呼び出すことになりますので、矛盾が生じてしまいます。そこで、MyWnd のコンストラクタでは MyWnd を生成する前に edit_box=NULL としてエディットボックスのポインタを NULL で初期化しておき、エディットボックスが生成される前に呼び出された OnSize では何もしないことにしているのです。この操作をしておかないとプログラムは不正なアクセスを起こしたことになりストップしてしまいます。
それでは、ここまでの全文。
#include <afxwin.h>
#include "resource.h"
class MyApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
class MyWnd : public CFrameWnd
{
private:
CEdit *edit_box;
public:
MyWnd();
afx_msg void OnOpen();
afx_msg void OnSize(UINT nType,int cx,int cy);
DECLARE_MESSAGE_MAP()
};
class MyApp MyApp;
BOOL MyApp::InitInstance()
{
m_pMainWnd = new class MyWnd;
m_pMainWnd->SetIcon(LoadIcon(ID_MYAPP_ICON),TRUE);
return TRUE;
}
MyWnd::MyWnd()
{
RECT rect;
edit_box=NULL;
Create(NULL,"",WS_OVERLAPPEDWINDOW | WS_VISIBLE ,rectDefault,NULL,
"MYAPP_MENU");
GetClientRect(&rect);
edit_box=new CEdit();
edit_box->Create(
WS_CHILD | WS_VISIBLE | WS_DLGFRAME | WS_HSCROLL | WS_VSCROLL
| ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL,
rect,this,ID_EDIT_BOX);
::SetFocus(edit_box->m_hWnd);
}
BEGIN_MESSAGE_MAP(MyWnd,CFrameWnd)
ON_COMMAND(IDM_OPEN,OnOpen)
ON_WM_SIZE()
END_MESSAGE_MAP()
afx_msg void MyWnd::OnOpen()
{
MessageBox("OnOpen was called !","Open file",MB_OK);
}
afx_msg void MyWnd::OnSize(UINT nType,int cx,int cy)
{
RECT rect;
if(edit_box==NULL)return;
GetClientRect(&rect);
edit_box->MoveWindow(&rect);
}
このプログラムでウインドウのサイズの変更に対応するようになりましたが、まだファイルへの書き込みも、ファイルからのロードもできません。これらについては次回以降に説明します。