第74回 プログラミングについて『テキストエディタを作ろう』

今回も前回に続いて簡易テキストエディタを作っていきましょう。
まず前回のソースコード。
#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);
}
今回はまず MyWnd::OnOpen() を実際にファイルをロードできるように変更しましょう。ファイル名を入力するには、CFileDialog というクラスを使用します。このクラスは通常Windowsのアプリケーションが使用しているダイアログボックスです。また、このクラスのヘッダーファイルは afxdlgs.h に記述されていますので、このヘッダーファイルをインクルードするようにして下さい。
afx_msg void MyWnd::OnOpen()
{
CFileDialog FileDlg(
TRUE,"txt",NULL,OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
"Text files (*.txt)|*.txt|All files (*.*)|*.*||",this);
if(FileDlg.DoModal()==IDOK){
MessageBox(
FileDlg.GetPathName().GetBuffer(_MAX_PATH),
"Open file",MB_OK);
}
}
最初の行は、CFileDialog クラスのオブジェクトを生成しています。第1引数は TRUE を指定すると『ファイルを開く』、FALSE を指定すると『名前を付けて保存』になります。第2引数はディフォルトのファイル拡張子です。第3引数はこのオブジェクトの動作モードで、第1引数が TRUE のときにはこの値はあまり関係ないので、ここでは説明を省
略します。第4引数はこのダイアログボックス内の『ファイルの種類』に登録されるファイルのフィルタです。詳細はヘルプを見て下さい。第5引数は親ウインドウのポインタですのでここでは this とします。
CFileDialog FileDlg(
TRUE,"txt",NULL,OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
"Text files (*.txt)|*.txt|All files (*.*)|*.*||",this);
このままではダイアログボックスが表示されませんので次の行が必要になります。
if(FileDlg.DoModal()==IDOK){
MessageBox(
FileDlg.GetPathName().GetBuffer(_MAX_PATH),
"Open file",MB_OK);
}
FileDlg.DoModal() 関数で初めてダイアログボックスが表示されてファイルの指定をすることができます。ファイルを選択すると IDOK が戻り値になりますので、指定されたファイルを MessageBox で試しに表示させています。
FileDlg.GetPathName().GetBuffer(_MAX_PATH)
はちょっとだけ複雑です。ファイルが指定されると、
FileDlg.GetPathName()
とすればファイルのフルパス名の入った CString クラスのポインタが帰ってきます。CString クラスは GetBufferメンバー関数でその中の文字列へのポインタを得ることができますので、このような表現になってしまう訳です。
ここまでの変更を加えたらコンパイル、リンクして実験してみて下さい。指定したファイル名が表示されましたか。
次は MessageBox の代わりにファイルをロードするように改修を加えましょう。上のMessageBox の部分を次の様に変更します。
if(FileDlg.DoModal()==IDOK){
LoadFile(FileDlg.GetPathName().GetBuffer(_MAX_PATH));
}
更に MyWnd クラスの定義に、
void LoadFile(char *file_name);
のメンバー関数を、現在のファイル名を保持しておくために次の変数を加えておきます。
char current_file_name[_MAX_PATH];
current_file_name を初期化するために、MyWnd::MyWnd の
edit_box=NULL;
の後にでも、
current_file_name[0]='\0';
と記述しておきます。
LoadFile の本体は、
void MyWnd::LoadFile(char *file_name)
{
FILE *fp;
char data[513];
int ldata;
fp=fopen(file_name,"rb");
if(fp==NULL){
sprintf(data,"--- Can not open ---\n%s",file_name);
MessageBox(data,"Open error",MB_OK);
return;
}
edit_box->SetSel(0,-1);
edit_box->Clear();
strcpy(current_file_name,file_name);
while(1){
if(fgets(data,sizeof(data),fp)==NULL)break;
if(data[0]==0x1a)break;
ldata=strlen(data);
if(data[ldata-1]==0x1a)data[--ldata]='\0';
edit_box->ReplaceSel(data);
}
edit_box->SetSel(0,0);
edit_box->SetModify(FALSE);
fclose(fp);
}
としました。ファイルをオープンするときにバイナリーモードにしているのは、通常DOSやWindowsのテキストファイルの改行が <CR><LF> であり、エディットボックスの改行も同じなのでこのままの方が都合がいいからです。読み込んだデータが0x1a のときに読み込みを終了しているのは、DOSのプログラムなどでは、ファイルの終了にこの値を使っているからです。
edit_box->SetSel(0,-1);
はエディットボックスのすべてのテキストを選択します。更に、
edit_box->Clear();
で、すべてのテキストを削除します。
edit_box->ReplaceSel(data);
は現在のカーソルの位置に文字列を挿入しています。ファイルをすべてエディットボックスに挿入した後は、カーソルの位置がファイルの末端になっているので、
edit_box->SetSel(0,0);
でファイルの最初の位置に移動させます。最後にもう1つやっておかなければならないのは、エディットボックスの内容を変更していますので、このオブジェクトの内部には変更されているというフラグが設定されていますので、
edit_box->SetModify(FALSE);
でそのフラグをオフにしておきます。
だいぶ変更したので、一度プログラムの全文をのせておきます。
#include <afxwin.h>
#include <afxdlgs.h>
#include "resource.h"
class MyApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
class MyWnd : public CFrameWnd
{
private:
CEdit *edit_box;
char current_file_name[_MAX_PATH];
public:
MyWnd();
afx_msg void OnOpen();
afx_msg void OnSize(UINT nType,int cx,int cy);
void LoadFile(char *file_name);
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()
{
CFileDialog FileDlg(
TRUE,"txt",NULL,OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
"Text files (*.txt)|*.txt|All files (*.*)|*.*||",this);
if(FileDlg.DoModal()==IDOK){
LoadFile(FileDlg.GetPathName().GetBuffer(_MAX_PATH));
}
}
afx_msg void MyWnd::OnSize(UINT nType,int cx,int cy)
{
RECT rect;
if(edit_box==NULL)return;
GetClientRect(&rect);
edit_box->MoveWindow(&rect);
}
void MyWnd::LoadFile(char *file_name)
{
FILE *fp;
char data[513];
int ldata;
fp=fopen(file_name,"rb");
if(fp==NULL){
sprintf(data,"--- Can not open ---\n%s",file_name);
MessageBox(data,"Open error",MB_OK);
return;
}
edit_box->SetSel(0,-1);
edit_box->Clear();
strcpy(current_file_name,file_name);
while(1){
if(fgets(data,sizeof(data),fp)==NULL)break;
if(data[0]==0x1a)break;
ldata=strlen(data);
if(data[ldata-1]==0x1a)data[--ldata]='\0';
edit_box->ReplaceSel(data);
}
edit_box->SetSel(0,0);
edit_box->SetModify(FALSE);
fclose(fp);
}
このプログラムを実際に動かしてみて下さい。実際にファイルの内容が表示されると思います。しかしながら、読み込みがニュルニュルと遅くていらいらするはずです。これは1行をファイルから読み込んだ直後にエディットボックスに挿入しているためで、その経過がそのまま表示されてしまうからです。
そこで、MyWnd::LoadFile を次のように変更します。
void MyWnd::LoadFile(char *file_name)
{
#define MAX_BUF 60000
FILE *fp;
char buf[MAX_BUF];
int lbuf;
char data[513];
int ldata;
fp=fopen(file_name,"rb");
if(fp==NULL){
sprintf(data,"--- Can not open ---\n%s",file_name);
MessageBox(data,"Open error",MB_OK);
return;
}
edit_box->SetSel(0,-1);
edit_box->Clear();
strcpy(current_file_name,file_name);
buf[0]='\0';
lbuf=0;
while(1){
if(fgets(data,sizeof(data),fp)==NULL)break;
if(data[0]==0x1a)break;
ldata=strlen(data);
if(data[ldata-1]==0x1a)data[--ldata]='\0';
if(ldata+lbuf>MAX_BUF-1)break;
strcpy(buf+lbuf,data);
lbuf+=ldata;
}
if(lbuf>2){
if(buf[lbuf-2]=='\r' && buf[lbuf-1]=='\n')buf[lbuf-2]='\0';
}
edit_box->ReplaceSel(buf);
edit_box->SetSel(0,0);
edit_box->SetModify(FALSE);
fclose(fp);
#undef MAX_BUF
}
まとめてファイル全部を読み込んだ後に一度だけエディットボックスに挿入するよう
にしたものです。ファイル全体のテキストを保持する buf に読み込んだテキストをコピ
ーするときに、
strcat(buf,data);
としてもいいのですが文字列を連結するときに、buf の先頭から順に最後のヌルを検索していくため、ファイルが大きくなったら連結に時間がかかるため、常に buf の最後の位置(ヌルの位置)に、コピーするようにしています。また最後の行の改行も追加してあります。
この変更でずいぶんロードが速くなったと思います。エディットボックスのテキスト全部を変えるならば、ローカルメモリーを使ってエディットボックスのテキスト用のバッファを付け替える方法もありますが、ここではその説明は省略します。
もう1つ機能を付けましょう。エディットボックスの内容が変更されているときにファイルをオープンしようとしたときにセーブするか否かを問い合わせるようにしましょう。ただし、実際にセーブするのは次回にします。
MyWnd::OnOpen メンバー関数を次のように変更します。
afx_msg void MyWnd::OnOpen()
{
if(edit_box->GetModify()){
if(MessageBox(
"Text was modified !\n"
"Save ?",
"Open error",MB_YESNO)==IDYES){
; //ここでセーブする関数を呼び出す。
}
}
CFileDialog FileDlg(
TRUE,"txt",NULL,OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
"Text files (*.txt)|*.txt|All files (*.*)|*.*||",this);
if(FileDlg.DoModal()==IDOK){
LoadFile(FileDlg.GetPathName().GetBuffer(_MAX_PATH));
}
}
長くなってしまったのでまた次回にしましょう。