以下の文章は、私がプログラミングを覚えていく上での単なる覚え書きです。 読み手のことは、1ミリも考えていません。 面白いことは何ひとつありませんので、ご注意ください。 さて、無事 the Spoke のインストール完了! 早速使ってみよう。 起動してみた。 スタートページ、プロジェクト、何じゃこりゃ。 とりあえず何も考えずに、新しいプロジェクト作成をクリック。 MFC、Win32 などなど……試しに、MFC アプリケーションをクリック。 OK クリック。 有効な名前を入力してください? あー、プロジェクト名か。 プロジェクト名にてすとあぷりけーしょんと入れて OK。 アプリケーションの種類、シングルドキュメント。 完了。 てすとあぷりけーしょんのビルド。 デバッグなしで開始。 おー、でけた。 さてさて、練習がてらテキストエディタでも作るか。 Win32 API で作ることにする。 いったん終了。 てすとあぷりけーしょんフォルダ削除。 再起動。 新しいプロジェクト。 Win32。 Win32 プロジェクト。 プロジェクト名にテキストエディタ。 OK。 アプリケーションの設定 → 空のプロジェクト。 完了。 WinMain.cpp 追加。 // WinMain.cpp // WinMain 関数 //------------------------------------------------------------------------------ #include //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ // WinMain 関数 //------------------------------------------------------------------------------ int WINAPI WinMain(HINSTANCE, HINSTANCE, PSTR, int) { MessageBox(NULL, TEXT("テストをしています!"), TEXT("警告"), MB_ICONINFORMATION); return true; } //------------------------------------------------------------------------------ テキストエディタのビルド。 デバッグなしで開始。 でけた。 さて、次。 ウィンドウの表示。 でけた。 エディットコントロールの作成。 でけた。が、32K までしか編集でけんらしい。 どうしようか。まあ、後で考えるか。 メニューの作成。 でけた。 ヘルプ→バージョン情報。 でけた。 ……う〜ん、めんどい。こんなん、やってられへん。 やっぱ MFC 使うか。 ちうわけで、 テキストエディタ実装技術 ttp://vivi.dyndns.org/tech/tech.phtml あたりを参考に、作成開始。 起動。 新しいプロジェクト。 MFC → MFC アプリケーション。 プロジェクト名は「TextEditorForKAG」(仮)。 アプリケーションの種類、シングルドキュメント(とりあえず) ファイルの拡張子、ks。 フィルタ名、KAG Scenario Files(*.ks)。 ツールバー、なし(とりあえず)。 印刷、なし。 ビューの基本クラス、CScrollView。 まずは、テキストの読み込みと表示から。 でけた。 改行・タブを記号で表示。 でけた。 レジストリ方式から ini 方式に変えたい。 CTextEditorForKAGApp::InitInstance() の SetRegistryKey(_T("ほにゃらら")); LoadStdProfileSettings(4); // 標準の INI ファイルのオプションをロードします (MRU を含む) とかいう部分を、 // ini ファイルを実行ファイルと同じディレクトリに保存 char dir[MAX_PATH]; ::GetModuleFileName(NULL, dir, MAX_PATH); char* pdest = strrchr(dir, '\\'); pdest[1] = '\0'; ::SetCurrentDirectory(dir); strcat(dir, m_pszProfileName); delete ((void*)m_pszProfileName); m_pszProfileName = _tcsdup(dir); LoadStdProfileSettings(4); // 標準の INI ファイルのオプションをロードします (MRU を含む) に変更。また、関連付けの部分、 RegisterShellFileTypes(TRUE); を、コメント化。 んー、多分でけた。 ここらで、ビルドした実行ファイルを WindowsME の方でも実行してみる。 ん? エラーが出た。 「MFCほにゃららがほにゃららません……」 ちょっと調べてみた。 スタティックライブラリにしてないかららしい。 スタティックライブラリにしてみた。 すると、ME でも実行できるようになったものの、なぜか英語になった。 英語バージョンを作っているわけではないので、いろいろ調べてみた。 日本語ロケールうんぬんの問題らしい。 プロジェクト→プロパティで、 リソース→追加のインクルード→C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\atlmfc\include\l.jpn としたら、直った。 でも、ビルドにかかる時間が長くなった。 再び、ME で実行してみる。 きちんと日本語で実行でけた。 ちなみに、現在の実行ファイルのサイズは 328 KB。 これを ZIP で圧縮すると、168 KB。 さて、これからの予定。 もうちょい色分け部分の実装をしっかりして、 ユーザー側でカスタマイズできるようにして、 それから、テキストを編集・保存できるようにして、 検索・置換もできるようにして、 さらに、入力補間を……う〜ん、テキストエディタちうのは練習がてら作るもんじゃないですな。 大変むずい。けどまあ、力はつくか。 とりあえずは、シナリオファイル用テキストビューアとして作りこむことにする。 その前に、「TODO:」が沢山あって邪魔くさいので、ソースコードの整理をしておく。 でけた。 あと、行番号を表示してほしいので、ツール→オプションから設定。 ついでに色分けとかもある程度カスタマイズ(Visual Studio 側のはなし)。 あと、改行とタブの色分け部分を関数化。 じゃあまずは、タグの色分けから。 @ マークが行頭にくるタイプのタグ。 でけた。 次、[]形式のタグ。 う〜ん、「ー」を表示する部分で文字化けしてるな。 色々と調べてみた。……3時間ぐらい。 1バイト、2バイト文字が混在する場合、CharNext 関数を使うらしい。 ちうわけで、[]形式のタグも無事でけた。 にしても時間かかりすぎ。解決したからいいけど。 次、コメント。これは簡単そうだ。 と思ったら、画面の右端まで塗りつぶす部分でちょっと詰まった。 けどでけた。 次、ラベル。これこそは簡単そうだ。 でけた。 次、見出し。 でけた。 次、値の中の引用句の処理。 でけた。 次、エスケープ文字の処理。 でけた。 とりあえず色分けはこんなもんかな。 あとビューアとして必要なのは、色分けのカスタマイズ、行番号、ルーラとか。 その前に、[EOF]の表示。 でけた。 スクロールバーのスクロール量の調節。 SetScrollSizes(MM_TEXT, sizeTotal, sizePage, sizeLine); でけた。 じゃあ、行番号から。 まずは、左端に行番号表示のためのスペースを作る。 でけた。 その部分の塗りつぶし。一応、システムカラーを使う。 CBrush brush(GetSysColor(COLOR_3DFACE)); でけた。 行番号の表示。 でけた。 何か行番号と文字がくっついて読みにくいな。 左オフセットでも作るか。 でけた。 仮に、1 にしておく。 あー、断然読みやすくなった。 次、右端で折り返し表示。 ……うーん、むずい。 けど、何とかでけたっぽい。 ルーラと色分けのカスタマイズは簡単そうだし後まわし。 じゃあそろそろ編集機能つけるか。 まずは、キャレットの表示。 キャレット、キャレット……。 うーん、よーわからん。 とりあえず、入力位置に縦長の棒を描くってぐらいでいいか。 でけた。点滅はなし。 クリックしたらその位置にキャレットを移動。 うーん、めんどいなー。 気晴らしに色々と調査。 と、MFC には CEdit なるものがあることを発見。 これは、テキストエディタ用のやつか!? と思ったら、エディットコントロールとたいして変わらんらしい。がっかり。 日本語入力の場合の入力ウィンドウの位置ってどうやって指定するのかな。 とりあえずキャレット無視して、調査。 ImmGetContext (入力コンテキストのハンドルを取得) ImmSetCompositionWindow (入力ウインドウに場所などを指定) ImmReleaseContext (取得した入力コンテキストのハンドルを解放) というのを使うらしい。それで入力された文字を受け取るには……と、その前にやっぱりキャレットやろう。まずは、左クリックを受けつけ。 void CTextEditorForKAGView::OnLButtonDown(UINT nFlags, CPoint point) { // クリック位置を保持 CPoint scpos = GetScrollPosition(); caretLeft = point.x; caretTop = scpos.y + point.y; // 再描画 RedrawWindow(); } で、クリック位置を保持&再描画。 そんでもって、テキスト描画の時点で、クリック位置がどこに挟まるかを割り出しつつ描画したい。 でけた。 ついでにあたらシックファイルを開いたときは、キャレット位置のリセット。 void CTextEditorForKAGView::OnInitialUpdate() { CScrollView::OnInitialUpdate(); CTextEditorForKAGDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); caretLeft = lineShowWidth + offsetLeft; caretTop = 0; } あ、カーソルをIビームにしてなかった。 BOOL CTextEditorForKAGView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { if(nHitTest == HTCLIENT) { SetCursor(LoadCursor(NULL, IDC_IBEAM)); return true; } return CScrollView::OnSetCursor(pWnd, nHitTest, message); } でけた。ついでに、 BOOL CTextEditorForKAGView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { if(nHitTest == HTCLIENT) { DWORD dwPos = GetMessagePos(); WORD x = LOWORD(dwPos); WORD y = HIWORD(dwPos); CPoint cursorPos(x, y); ScreenToClient(&cursorPos); if(cursorPos.x < lineShowWidth) SetCursor(LoadCursor(NULL, IDC_ARROW)); else SetCursor(LoadCursor(NULL, IDC_IBEAM)); return true; } return CScrollView::OnSetCursor(pWnd, nHitTest, message); } で、行番号の位置にカーソルがあるときは、普通のカーソルになるようにした。 でも、左右が反対のカーソルにしたい。 リソースビューから「リソースの追加」→「Cursor」→「IDC_POINTER」 リソースエディタで編集。「左右反転」→「範囲選択」→「ドラッグ&ドロップ」→「ホットスポットの設定ツール」。で、さっきの、 SetCursor(LoadCursor(NULL, IDC_ARROW)); のところを、 SetCursor(AfxGetApp()->LoadCursor(IDC_POINTER)); に変更。でけた。 HTMLエディタ「UlyNetSphere」の開発日記 ttp://www.ulyssesrecords.com/product/apps/ulynetsphere/ ってのを発見。フォントの変更とか右クリックメニューとか、いずれ参考にさせてもらうかも。 クラスビューからメンバ変数、メンバ関数を追加できることを今さら知る。今まで全部手動でやってたんですけど。 テキスト保存機能をつける。 BOOL CTextEditorForKAGDoc::OnSaveDocument(LPCTSTR lpszPathName) { CStdioFile file; if( !file.Open(lpszPathName, CFile::modeCreate | CFile::modeWrite | CFile::typeText)) return false; CString text; int i = 0; while(m_ee.getText(i++, text)) { file.WriteString(text); file.WriteString("\n"); } file.Close(); return true; } で、でけたっぽい。 今んとこ Unicode とかは良く分からんので、後回し。 .pch ファイルとか結構サイズがでかいので、今までのバージョンの「Debug」フォルダと「Release」フォルダを全消ししておいた。 日本語入力時の変換ウィンドウをキャレットの位置に表示させる。 View.cpp ファイルに、 #include "imm.h" を追加&「プロジェクトのプロパティ」の「追加の依存ファイル」に「IMM32.LIB」を追加。 で、キャレット描画の部分で、 // IME の入力ウィンドウ位置の指定 CWnd* pWnd = GetActiveWindow(); HWND hWnd; if(pWnd != NULL) hWnd = pWnd->GetSafeHwnd(); else hWnd = m_hWnd; HIMC hIMC = ImmGetContext(hWnd); if(hIMC) { COMPOSITIONFORM cpf; cpf.dwStyle = CFS_POINT; cpf.ptCurrentPos.x = caretRect.left; cpf.ptCurrentPos.y = caretRect.top; ImmSetCompositionWindow(hIMC, &cpf); ImmReleaseContext(hWnd, hIMC); } とする。でけた。 入力ウィンドウからの文字列の取得は、 ttp://www.eva.hi-ho.ne.jp/minoru-f/diary/diary200005.html とか参考になりそう。 あと、テキストエディタ全体としては、 ttp://www.kmonos.net/alang/texted/ とか参考になりそう。 GetTextExtent というのを使って、正確な位置に文字が表示されるようにした。 ソースコードの整理。 うーん汚いな。ほとんど1から書き直し。 けっこう時間かかった。 けど、今までのと比べると大分すっきりしたはず。 あれれ? 明らかに表示が遅くなったな。 速くなるようにコーディングしたつもりだったのに。 うわー、かなしいな。 でも遅くなるんじゃしょうがないから、整理前のに戻した。 まあいいや、コードの整理は後まわし。 一応、高速化についてのメモ。 現状では全ての行に対してループを回しているため、 その分時間がかかっているのが大きい(と思われる)。 これは、折り返しの行数をきちんと保持するためにわざとそうしてある。 初めてファイルが読み込まれたとき、サイズが変更されたときに、 あらかじめ折り返した状態での CStringArray とそれに対応する行番号を保持するようにすれば、ある程度は高速化できるはず。 また、初めて読み込まれた時点で、いったん全ての文字を描画し、 それからは、ファイルの読み込み、入力、サイズ変更の時点で、 必要な部分だけを描画しなおすようにするのもいいかも。 ん? ……まてよ。 折り返しのときに毎回右端の位置を得て、それから折り返し処理を考えてるのが、そもそもの間違いのような気がする。 まず、指定文字数で折り返す機能をつけておいて、右端で折り返す場合は、 ウィンドウサイズが変更されたときに、このサイズなら何文字描画できるかというのを調べて、 それを指定文字数にするのがいいような気がする。 うーん、とりあえず右端で折り返す機能を無くして、 指定文字数で折り返す機能をつけてみよう。 まずは、80字限定。 ファイルが読み込まれた時点で、ループを回しながら、その行数を保持。 で、80文字を超えないようなら、次の行へ。超えるようなら、超えたあとの文字列を次の行に回す。 折り返されたあとの行数は、NULL のままにしておく。 これは……とりあえず view クラスに含めちまうか。 でかくなってきたら、クラス分けするとして。 まずは、CStringArray bufArray; と、 int 配列は……あー std::vector がいいか。使ったことないけど。 いずれ勉強してみよ。 うーん、また気分転換に色々調べてみた。 ttp://hp.vector.co.jp/authors/VA007799/vivi.htm の「ViViの内部に関するドキュメント」が参考になりそう。あと、 ttp://cgi.members.interq.or.jp/gold/marken/ ttp://mfc.acty-net.ne.jp/ml/mfc/ とかで、「テキストエディタ」で検索してみたら、参考になる記事がいくつか見つかった。 CScrollView は行数の多いファイルを開いたときに動作が狂ったり、 色々お節介なことをしてくれるらしいので、 自分でスクロールバー作って、OnVScroll を処理したほうが良いらしい。 あと、CStringArray よりも CStringList の方がいいのかな。 それと、やっぱり必要な部分だけを描画するってのが大事っぽい。 あー、テキストエディタ開発についての解説書って無いのかな。 あっても良さそうなのに。 でも、方向性としては間違ってなかったみたいだから、ちょっと安心。 んー、やっぱり今のうちにソースコードの整理をやっとこう。 とりあえず、CStringList 型のバッファを Doc に含める。 でけた。 CTextEditorForKAGView とかクラス名が長すぎるな。 プロジェクト名「Editor」として作り直し。 ツールバーもステータスバーも無し。 CScrollView も無し。 レジストリと関連付けを解除。 きちんと日本語表示にする。 TODO: の整理とかはめんどくさいので後回し。 んー、これを機に1から作り直すか。 まずは、CEditorDoc クラスに、CStringList buf; を追加。 CEditorDoc::OnNewDocument に、buf.RemoveAll(); を追加。 プロパティのオーバーライドから、OnOpenDocument を追加。 BOOL CEditorDoc::OnOpenDocument(LPCTSTR lpszPathName) { buf.RemoveAll(); CStdioFile file; if( !file.Open(lpszPathName, CFile::modeRead | CFile::typeText)) return false; CString text; while(file.ReadString(text)) buf.AddTail(text); file.Close(); return true; } CEditorView クラスに、CDC bufDC; を追加。デストラクタに、bufDC.DeleteDC(); を追加。 プロパティのオーバーライドで、CEditorView::OnInitialUpdate を追加。 OnInitialUpdate で bufDC にテキストを全て描画。 void CEditorView::OnInitialUpdate() { CEditorDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CDC* pDC = GetDC(); bufDC.CreateCompatibleDC(pDC); CBitmap* bmp; bmp = new CBitmap(); CRect rect; GetClientRect(rect); int bottom = pDoc->buf.GetSize() * fontHeight; bmp->CreateCompatibleBitmap(pDC, rect.Width(), bottom); bufDC.SelectObject(bmp); CBrush brush; brush.CreateSolidBrush(RGB(0xff, 0xff, 0xff)); bufDC.SelectObject(&brush); bufDC.PatBlt(0, 0, rect.Width(), bottom, PATCOPY); for(int line = 0, py = 0; py < bottom; line++, py += fontHeight) { CString text = pDoc->buf.GetAt(pDoc->buf.FindIndex(line)); bufDC.TextOut(0, py, text); } bmp->DeleteObject(); ReleaseDC(pDC); CView::OnInitialUpdate(); } OnDraw でそこから必要な部分をコピー。 void CEditorView::OnDraw(CDC* pDC) { CRect rect; GetClientRect(rect); pDC->BitBlt(rect.left, rect.top, rect.Width(), rect.Height(), &bufDC, rect.left, rect.top, SRCCOPY); } 縦スクロールバーをつける。 CMainFrame::PreCreateWindow に、 cs.style |= WS_VSCROLL; CEditorView::OnInitialUpdate の最後の方で、 // スクロールバーの設定 SCROLLINFO si; si.cbSize = sizeof(SCROLLINFO); si.fMask = SIF_RANGE | SIF_PAGE; si.nMin = 0; si.nMax = pDoc->buf.GetSize(); si.nPage = rect.bottom / fontHeight; CWnd* m_pMainWnd = AfxGetMainWnd(); m_pMainWnd->SetScrollInfo(SB_VERT, &si, true); とする。で、プロパティのオーバーライドで、 void CEditorView::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { SCROLLINFO si; CWnd* m_pMainWnd = AfxGetMainWnd(); m_pMainWnd->GetScrollInfo(SB_VERT, &si, SIF_ALL); switch(nSBCode) { case SB_TOP: si.nPos = si.nMin; break; case SB_BOTTOM: si.nPos = si.nMax; break; case SB_LINEUP: si.nPos--; break; case SB_LINEDOWN: si.nPos++; break; case SB_PAGEUP: si.nPos -= si.nPage; break; case SB_PAGEDOWN: si.nPos += si.nPage; break; case SB_THUMBPOSITION: si.nPos = nPos; break; case SB_THUMBTRACK: si.nPos = nPos; break; } m_pMainWnd->SetScrollInfo(SB_VERT, &si, true); RedrawWindow(); CView::OnVScroll(nSBCode, nPos, pScrollBar); } を追加。で、 void CEditorView::OnDraw(CDC* pDC) { CWnd* m_pMainWnd = AfxGetMainWnd(); int sbPos = m_pMainWnd->GetScrollPos(SB_VERT) * fontHeight; CRect rect; GetClientRect(rect); pDC->BitBlt(rect.left, rect.top, rect.Width(), rect.Height(), &bufDC, rect.left, sbPos, SRCCOPY); } とする。あ、そういえば、int fontHeight; 変数を追加した。 すると……、おお! きちんとスクロールした! この段階での動作はかなり軽め。色分けやら記号やら何やらでどこまで重くなるか。 んーこのメモ、プログラムをコピーするばっかになってきたな。まあいいか。 あれ? 今さらだけど、改行ってどうなってるんだろう。 CStdioFile::ReadString(CString&) じゃ得られないんだったけか。 書きなおさにゃ。 BOOL CEditorDoc::OnOpenDocument(LPCTSTR lpszPathName) { buf.RemoveAll(); FILE *fp = fopen(lpszPathName, "r"); if( !fp ) return false; CFileStatus fStatus; CFile::GetStatus(lpszPathName, fStatus); char* text = new char[fStatus.m_size+1]; while(fgets(text, fStatus.m_size+1, fp)) buf.AddTail(text); delete[] text; fclose(fp); return true; } としてみた。間違ってないかなー。本当は、 ttp://hp.vector.co.jp/authors/VA007799/tips/tips4.htm の内容を理解したかったけど、よーわからんかった。 タブ文字と改行の描画。