Tutorial 33: RichEdit Control: Basics

以前より、リッチエディットコントロールのチュートリアルについて沢山の要望があったので、リッチエディットについてのチュートリアルを書けるようになるまで、リッチエディットを使い倒していました。 ようやく、チュートリアルを書けるぐらい知識を深めたので、チュートリアルを書くことにします。チュートリアルには、私が知っている全てを書くつもりです。 その情報量は少々多すぎるので、いくつかの章に分けることにしました。この章は最初のパートで、リッチエディットコントロールが何かということと、それの作り方と、データのロード/セーブ方法を紹介します。
 メインソース   ヘッダファイル   リソース   実行結果 

Theory:

リッチエディットコントロールはエディットコントロールの高機能版で、エディットコントロールに不足していた機能を備えている。 例えば、複数のフォントを使用できたり、任意回のundo/redoが可能となっていたり、テキスト検索ができたり、OLEの埋め込みができたり、ドラッグアンドドロップができるようになったり、と色々ある。
リッチエディットはこのように非常に多機能なので、他のコントロールとは分けられたDLLに格納されている。 これはつまり、他のコントロールと同じようにInitCommonControls関数をCALLするのではなく、リッチエディットコントロール用のDLLをロードするために、LoadLibrary関数をCALLしなければならない。

そこで問題になるのが、リッチエディットのバージョンで、今のところ3つのバージョンが存在する。以下の表に、それぞれのバージョン毎のDLL名を示している。

DLL Name RichEdit version Richedit Class Name
Riched32.dll 1.0 RICHEDIT
RichEd20.dll 2.0 RICHEDIT20A
RichEd20.dll 3.0 RICHEDIT20A

バージョン2と3が同じDLL名だとうことがわかると思う。しかも同一のクラス名となっている。 これにより、バージョン3の機能を使用したい場合に問題が起こり得る。今まで、バージョン2と3を識別する方法がわからなかったのだが、回避策があるので、それでOKだ。 その回避策は後ほど紹介しよう。

.data RichEditDLL db "RichEd20.dll",0 ..... .data? hRichEditDLL dd ? .code invoke LoadLibrary,addr RichEditDLL mov hRichEditDLL,eax ...... invoke FreeLibrary,hRichEditDLL

リッチエディットDLLがロードされると、リッチエディットウィンドウクラスが登録される。 なので、コントロールを作成する前にDLLをロードしておくことを忘れないようにしよう。 リッチエディットコントロールクラスの名前も異なっている。たぶん、こんな疑問を抱くだろう。

「自分が使用すべきリッチエディットコントロールはどのバージョンかをどうやって知ればいいのだろう?」

これに対する答えは以下の表を参考にして自分で考えなければならない。 新しいバージョンになるにつれ、より高機能になっていくわけだが、それらの機能が必ずしも必要と言うわけではないので、適当なものを選択しよう。

Feature Version 1.0 Version 2.0 Version 3.0
selection bar
x
x
x
unicode editing
x
x
character/paragraph formatting
x
x
x
search for text
forward
forward/backward
forward/backward
OLE embedding
x
x
x
Drag and drop editing
x
x
x
Undo/Redo
single-level
multi-level
multi-level
automatic URL recognition
x
x
Accelerator key support
x
x
Windowless operation
x
x
Line break
CRLF
CR only
CR only (can emulate version 1.0)
Zoom
x
Paragraph numbering
x
simple table
x
normal and heading styles
x
underline coloring
x
hidden text
x
font binding
x

上記テーブルはわかりやすいものではない。単に重要な機能をリストしただけだから。

●リッチエディットコントロールの作成

リッチエディットDLLをロードしたあとは、CreateWindowEx関数をCALLしてコントロールを作成できるようになる。 ES_LOWERCASE, ES_UPPERCASEES_OEMCONVERT を除く、エディットコントロールスタイルとコモンウィンドウスタイルをCreateWindowEx関数で使用することができる。

.const RichEditID equ 300 .data RichEditDLL db "RichEd20.dll",0 RichEditClass db "RichEdit20A",0 ... .data? hRichEditDLL dd ? hwndRichEdit dd ? .code ..... invoke LoadLibrary,addr RichEditDLL mov hRichEditDLL,eax invoke CreateWindowEx,0,addr RichEditClass,\ WS_VISIBLE or ES_MULTILINE or WS_CHILD or WS_VSCROLL or WS_HSCROLL, \ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,RichEditID,hInstance,0 mov hwndRichEdit,eax

●文字列色と背景色の設定

エディットコントロールでは、文字色と背景色を設定するのが一つの問題だったが、リッチエディットコントロールでは改善されている。 背景色を設定するには、EM_SETBKGNDCOLORメッセージを送信すればよく、詳細は以下のようになっている。

wParam == オプションで、もし0ならlParamを背景色として使用することをWindowsに明示する。 非0なら、Windowsはシステムの背景色を使用する。背景色を変更するためにこのメッセージを送信するので、ここは0にする。

lParam == COLORREF構造体がここに入ってくることになっており、wParamが0なら使用される

例えば、青を設定したければ、以下のようにすればよい。

invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,0FF0000h

文字色を設定したい時は、EM_SETCHARFORMATメッセージを送信する。 このメッセージは選択中の文字列に対してとコントロール中の全ての文字に対してと2通りの動作を行う。 詳細は以下のようになっている。

wParam == オプション

SCF_ALL コントロール内の全ての文字列に影響する
SCF_SELECTION 選択した文字列だけに影響する
SCF_WORD or SCF_SELECTION 選択した単語に影響する。もし何も選択されていなければ、つまり単語中にキャレットしかなければ、その単語にしか影響しない。SCF_WORDフラグはSCF_SELECTIONと一緒に使用しなければならない。

lParam == CHARFORMAT構造体、もしくはCHARFORMAT2構造体へのポインタ。これらの構造体には文字列のフォーマットに関する情報が格納されている。 CHARFORMAT2はバージョン2かそれ以上でないと使用できない。もちろんバージョン2以降でCHARFORMATを使用することも可能なので、プログラムの要求によって使い分ければよい。

CHARFORMATA STRUCT cbSize DWORD ? dwMask DWORD ? dwEffects DWORD ? yHeight DWORD ? yOffset DWORD ? crTextColor COLORREF ? bCharSet BYTE ? bPitchAndFamily BYTE ? szFaceName BYTE LF_FACESIZE dup(?) _wPad2 WORD ? CHARFORMATA ENDS

Field Name Description
cbSize 構造体のサイズ。リッチエディットコントロールはこの値によりこの構造体がCHARFORMATなのかCHARFORMAT2なのかを判断するために使用する
dwMask

ビットフラグで、以下に紹介するメンバが有効かどうかを決定する。

CFM_BOLD dwEffectsCFE_BOLDが有効となる
CFM_CHARSET bCharSetメンバが有効となる
CFM_COLOR dwEffectsCFE_AUTOCOLORcrTextColorが有効となる
CFM_FACE szFaceNameが有効となる
CFM_ITALIC dwEffectsCFE_ITALICが有効となる
CFM_OFFSET yOffsetが有効となる
CFM_PROTECTED dwEffectsCFE_PROTECTEDが有効となる
CFM_SIZE The yHeightが有効となる
CFM_STRIKEOUT dwEffectsCFE_STRIKEOUTが有効となる
CFM_UNDERLINE dwEffectsCFE_UNDERLINEが有効となる
dwEffects

文字書式を表し、以下の値を複数指定できる

CFE_AUTOCOLOR システムカラーを使用する
CFE_BOLD ボールド
CFE_ITALIC イタリック
CFE_STRIKEOUT 横棒
CFE_UNDERLINE アンダーライン
CFE_PROTECTED 保護属性:変更しようとするとEN_PROTECTEDメッセージが発生する
yHeight 文字の高さで単位はtwips(1/1440インチ、もしくはプリンタの1/20ポイント)
yOffset 基準線からのオフセット値で、単位は同じくtwips。正の値なら上付き文字で、負なら下付き文字となる。
crTextColor 文字色。このメンバは、文字書式が有効でなければ無視される。
bCharSet キャラクタセット
bPitchAndFamily フォントファミリとピッチ
szFaceName フォント名
_wPad2 パディング

上の表で文字列の色や字体を色々と変更できるところがあるのがわかるだろう。 それらの中で注目すべきはCFE_RPOTECTEDフラグで、ユーザが編集しようとするとEN_PROTECTED通知メッセージが親ウィンドウに送信される。 なので、その後変更の可否を操作できる。

CHARFORMAT2はさらに多くのスタイルを備えているが、これらの追加機能を使用する必要が無ければCHARFORMATを使用すればよい。

文字フォーマットを設定する文字列の範囲を考えなければならない。 リッチエディットは文字列の範囲を指定でき、0から始まる番号をそれぞれの文字に割り振っている。 つまり、最初の文字は0となり、2文字目は1、というようになる。文字列の範囲を指定するには、2つの数字を指定しなければならない。 1つ目の番号から2つ目の番号までがその範囲となる。EM_SETCHARFORMATを指定することにより、3つのオプションを選択することができる。

1.コントロール内の全ての文字に対して適用する(SCF_ALL)
2.現在選択している文字列に対して適用する(SCF_SELECTION)
3.現在選択している文字列全てに対して適用する(SCF_WORD or SCF_SELECTION)

1番と2番はすぐわかるだろう。問題は3番目だ。もし単語全体ではなく、1つか複数の文字を選択していた場合、SCF_WORD + SCF_SELECTIONを指定することにより、単語全体に文字書式を反映させることができる。 何も選択していなくても、だ。つまり、単語中のキャレット位置だけで、単語全体の書式を変更することができる。

EM_SETCHARFORMATを使用するには、CHARFORMAT(もしくはCHARFORMAT2)構造体のいくつかのメンバをセットしなければならない。 例えば、文字色をセットしたければ、CHARFORMAT構造体を以下のようにする。

.data? cf CHARFORMAT <> .... .code mov cf.cbSize,sizeof cf mov cf.dwMask,CFM_COLOR mov cf.crTextColor,0FF0000h invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cf

上記プログラムはリッチエディットコントロールの文字色を青色にセットする。 もしリッチエディットコントロール内に文字が無ければ、以降入力される文字列はEM_SETCHARFORMATメッセージで指定されたフォーマットが使用される。

●文字の入力と保存

エディットコントロールを使っていた人にとっては、WM_GETTEXT/WM_SETTEXTといった文字列を取得/セットするメッセージに慣れているだろう。 この方法はリッチエディットでも健在だが、ファイルが大きなときは効果的ではない。 エディットコントロールは64KBの制限があったが、リッチエディットはその制限が緩和されている。 そのため、WM_GETTEXTで文字列を取得するために10MBなどといった非常に巨大なメモリを確保すると言う作業は非常にわずらわしい。 リッチエディットは「ストリーム」という画期的な方法を提供している。

単純に言うと、リッチエディットにコールバック関数のアドレスを指定するのである。 そして、リッチエディットはそのコールバック関数にバッファのアドレスを渡してCALLする。 コールバック関数は、リッチエディットコントロールに送りたいデータをバッファにセットしたり、バッファからデータを読み込んだりする。 この考え方は、ストリームへの入力と出力両方に適用できる。この方法は非常に効率的なことがわかるだろう。
バッファは、リッチエディットコントロール自身により用意されるので、つまりデータはぶつ切りにされるのである。 この操作はEM_STREAMINEM_STREAMOUTの2つのメッセージによって実現する。

EM_STREAMINEM_STREAMOUTは同じ構文となっている。

wParam == フォーマットオプション

SF_RTF リッチテキストフォーマット(RTF)
SF_TEXT テキストフォーマット
SFF_PLAINRTF すべての言語で共通のキーワードのみがストリームに送られる
SFF_SELECTION ストリームに関する実行ターゲットが現在選択されている文字列になる。ストリームに文字列を送ると、現在選択されているものと交換される。もしストリームから文字列を取り出すと、現在選択中の文字列を取得することになる。このフラグが指定されていなければ、対象はリッチエディット全体の文字列となる。
SF_UNICODE (バージョン2.0かそれ以降で)UNICODE文字となる

wParam == EDITSTREAM構造体へのポインタ。定義は以下の通り。

EDITSTREAM STRUCT dwCookie DWORD ? dwError DWORD ? pfnCallback DWORD ? EDITSTREAM ENDS

dwCookie 以下に出てくるpfnCallbackメンバに指定するコールバック関数の引数に渡す値。通常、ストリーム処理で使用するファイルハンドルのような重要なものを渡すのに使用する。
dwError 読み込み、書き込み操作の結果を表す。0なら正常、非0ならEditStreamCallback関数の戻り値か、もしくはエラーが発生したことになる。
pfnCallback EditStreamCallback関数へのポインタで、データを転送するためリッチエディットコントロールがこの関数をCALLする。この関数は繰り返し呼び出され、毎回データの一部が転送されることになっている。

コールバック関数は以下のようなプロトタイプになっている。

EditStreamCallback proto dwCookie:DWORD, pBuffer:DWORD, NumBytes:DWORD, pBytesTransferred:DWORD

アプリケーションではこのプロトタイプで関数を定義し、EDITSTREA構造体にその関数のアドレスを指定する。

コールバック関数は成功すると0を返す。リッチエディットコントロールはデータが残っている限りコールバック関数をCALLし続ける。 コールバック関数の処理中にエラーが発生した場合に中止したければ、非0を返し、リッチエディットコントロールはpBufferで指されたデータを破棄する。 エラー/成功の値は、EDITSTREAM構造体のdwErrorにセットされるので、SendMessage関数の戻り値により、 ストリームオペレーションのエラー/成功をチェックできる。

Example:

以下のサンプルコードはasmソースコードファイルをオープン、編集、保存するだけの単純なエディターだ。リッチエディットのバージョン2.0以上を使用している。

.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\comdlg32.inc include \masm32\include\gdi32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\gdi32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib WinMain proto :DWORD,:DWORD,:DWORD,:DWORD .const IDR_MAINMENU equ 101 IDM_OPEN equ 40001 IDM_SAVE equ 40002 IDM_CLOSE equ 40003 IDM_SAVEAS equ 40004 IDM_EXIT equ 40005 IDM_COPY equ 40006 IDM_CUT equ 40007 IDM_PASTE equ 40008 IDM_DELETE equ 40009 IDM_SELECTALL equ 40010 IDM_OPTION equ 40011 IDM_UNDO equ 40012 IDM_REDO equ 40013 IDD_OPTIONDLG equ 101 IDC_BACKCOLORBOX equ 1000 IDC_TEXTCOLORBOX equ 1001 RichEditID equ 300 .data ClassName db "IczEditClass",0 AppName db "IczEdit version 1.0",0 RichEditDLL db "riched20.dll",0 RichEditClass db "RichEdit20A",0 NoRichEdit db "Cannot find riched20.dll",0 ASMFilterString db "ASM Source code (*.asm)",0,"*.asm",0 db "All Files (*.*)",0,"*.*",0,0 OpenFileFail db "Cannot open the file",0 WannaSave db "The data in the control is modified. Want to save it?",0 FileOpened dd FALSE BackgroundColor dd 0FFFFFFh ; default to white TextColor dd 0 ; default to black .data? hInstance dd ? hRichEdit dd ? hwndRichEdit dd ? FileName db 256 dup(?) AlternateFileName db 256 dup(?) CustomColors dd 16 dup(?) .code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke LoadLibrary,addr RichEditDLL .if eax!=0 mov hRichEdit,eax invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT invoke FreeLibrary,hRichEdit .else invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR .endif invoke ExitProcess,eax WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:DWORD mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,IDR_MAINMENU mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .while TRUE invoke GetMessage, ADDR msg,0,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wParam ret WinMain endp StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0 xor eax,1 ret StreamInProc endp StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0 xor eax,1 ret StreamOutProc endp CheckModifyState proc hWnd:DWORD invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 .if eax!=0 invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL .if eax==IDYES invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0 .elseif eax==IDCANCEL mov eax,FALSE ret .endif .endif mov eax,TRUE ret CheckModifyState endp SetColor proc LOCAL cfm:CHARFORMAT invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor invoke RtlZeroMemory,addr cfm,sizeof cfm mov cfm.cbSize,sizeof cfm mov cfm.dwMask,CFM_COLOR push TextColor pop cfm.crTextColor invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm ret SetColor endp OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL clr:CHOOSECOLOR .if uMsg==WM_INITDIALOG .elseif uMsg==WM_COMMAND mov eax,wParam shr eax,16 .if ax==BN_CLICKED mov eax,wParam .if ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .elseif ax==IDC_BACKCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push BackgroundColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop BackgroundColor invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif .elseif ax==IDC_TEXTCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push TextColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop TextColor invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif .elseif ax==IDOK ;================================================================================== ; Save the modify state of the richedit control because changing the text color changes the ; modify state of the richedit control. ;================================================================================== invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 push eax invoke SetColor pop eax invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0 invoke EndDialog,hWnd,0 .endif .endif .elseif uMsg==WM_CTLCOLORSTATIC invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX .if eax==lParam invoke CreateSolidBrush,BackgroundColor ret .else invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX .if eax==lParam invoke CreateSolidBrush,TextColor ret .endif .endif mov eax,FALSE ret .elseif uMsg==WM_CLOSE invoke EndDialog,hWnd,0 .else mov eax,FALSE ret .endif mov eax,TRUE ret OptionProc endp WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL chrg:CHARRANGE LOCAL ofn:OPENFILENAME LOCAL buffer[256]:BYTE LOCAL editstream:EDITSTREAM LOCAL hFile:DWORD .if uMsg==WM_CREATE invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,\ WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\ CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0 mov hwndRichEdit,eax ;============================================================= ; Set the text limit. The default is 64K ;============================================================= invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0 ;============================================================= ; Set the default text/background color ;============================================================= invoke SetColor invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0 .elseif uMsg==WM_INITMENUPOPUP mov eax,lParam .if ax==0 ; file menu .if FileOpened==TRUE ; a file is already opened invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED .else invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED .endif .elseif ax==1 ; edit menu ;============================================================================= ; Check whether there is some text in the clipboard. If so, we enable the paste menuitem ;============================================================================= invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0 .if eax==0 ; no text in the clipboard invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED .endif ;========================================================== ; check whether the undo queue is empty ;========================================================== invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED .endif ;========================================================= ; check whether the redo queue is empty ;========================================================= invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED .endif ;========================================================= ; check whether there is a current selection in the richedit control. ; If there is, we enable the cut/copy/delete menuitem ;========================================================= invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg mov eax,chrg.cpMin .if eax==chrg.cpMax ; no current selection invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED .endif .endif .elseif uMsg==WM_COMMAND .if lParam==0 ; menu commands mov eax,wParam .if ax==IDM_OPEN invoke RtlZeroMemory,addr ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,offset ASMFilterString mov ofn.lpstrFile,offset FileName mov byte ptr [FileName],0 mov ofn.nMaxFile,sizeof FileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST invoke GetOpenFileName,addr ofn .if eax!=0 invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE mov hFile,eax ;================================================================ ; stream the text into the richedit control ;================================================================ mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamInProc invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream ;========================================================== ; Initialize the modify state to false ;========================================================== invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile mov FileOpened,TRUE .else invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR .endif .endif .elseif ax==IDM_CLOSE invoke CheckModifyState,hWnd .if eax==TRUE invoke SetWindowText,hwndRichEdit,0 mov FileOpened,FALSE .endif .elseif ax==IDM_SAVE invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE @@: mov hFile,eax ;================================================================ ; stream the text to the file ;================================================================ mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamOutProc invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream ;========================================================== ; Initialize the modify state to false ;========================================================== invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile .else invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR .endif .elseif ax==IDM_COPY invoke SendMessage,hwndRichEdit,WM_COPY,0,0 .elseif ax==IDM_CUT invoke SendMessage,hwndRichEdit,WM_CUT,0,0 .elseif ax==IDM_PASTE invoke SendMessage,hwndRichEdit,WM_PASTE,0,0 .elseif ax==IDM_DELETE invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0 .elseif ax==IDM_SELECTALL mov chrg.cpMin,0 mov chrg.cpMax,-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg .elseif ax==IDM_UNDO invoke SendMessage,hwndRichEdit,EM_UNDO,0,0 .elseif ax==IDM_REDO invoke SendMessage,hwndRichEdit,EM_REDO,0,0 .elseif ax==IDM_OPTION invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0 .elseif ax==IDM_SAVEAS invoke RtlZeroMemory,addr ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,offset ASMFilterString mov ofn.lpstrFile,offset AlternateFileName mov byte ptr [AlternateFileName],0 mov ofn.nMaxFile,sizeof AlternateFileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST invoke GetSaveFileName,addr ofn .if eax!=0 invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE jmp @B .endif .endif .elseif ax==IDM_EXIT invoke SendMessage,hWnd,WM_CLOSE,0,0 .endif .endif .elseif uMsg==WM_CLOSE invoke CheckModifyState,hWnd .if eax==TRUE invoke DestroyWindow,hWnd .endif .elseif uMsg==WM_SIZE mov eax,lParam mov edx,eax and eax,0FFFFh shr edx,16 invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE .elseif uMsg==WM_DESTROY invoke PostQuitMessage,NULL .else invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .endif xor eax,eax ret WndProc endp end start ;=================================================================== ; The resource file ;=================================================================== #include "resource.h" #define IDR_MAINMENU 101 #define IDD_OPTIONDLG 101 #define IDC_BACKCOLORBOX 1000 #define IDC_TEXTCOLORBOX 1001 #define IDM_OPEN 40001 #define IDM_SAVE 40002 #define IDM_CLOSE 40003 #define IDM_SAVEAS 40004 #define IDM_EXIT 40005 #define IDM_COPY 40006 #define IDM_CUT 40007 #define IDM_PASTE 40008 #define IDM_DELETE 40009 #define IDM_SELECTALL 40010 #define IDM_OPTION 40011 #define IDM_UNDO 40012 #define IDM_REDO 40013 IDR_MAINMENU MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open", IDM_OPEN MENUITEM "&Close", IDM_CLOSE MENUITEM "&Save", IDM_SAVE MENUITEM "Save &As", IDM_SAVEAS MENUITEM SEPARATOR MENUITEM "E&xit", IDM_EXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo", IDM_UNDO MENUITEM "&Redo", IDM_REDO MENUITEM "&Copy", IDM_COPY MENUITEM "C&ut", IDM_CUT MENUITEM "&Paste", IDM_PASTE MENUITEM SEPARATOR MENUITEM "&Delete", IDM_DELETE MENUITEM SEPARATOR MENUITEM "Select &All", IDM_SELECTALL END MENUITEM "Options", IDM_OPTION END IDD_OPTIONDLG DIALOG DISCARDABLE 0, 0, 183, 54 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | DS_CENTER CAPTION "Options" FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,137,7,39,14 PUSHBUTTON "Cancel",IDCANCEL,137,25,39,14 GROUPBOX "",IDC_STATIC,5,0,124,49 LTEXT "Background Color:",IDC_STATIC,20,14,60,8 LTEXT "",IDC_BACKCOLORBOX,85,11,28,14,SS_NOTIFY | WS_BORDER LTEXT "Text Color:",IDC_STATIC,20,33,35,8 LTEXT "",IDC_TEXTCOLORBOX,85,29,28,14,SS_NOTIFY | WS_BORDER END

Analysis:

まず始めにリッチエディットDLL(この場合はバージョン2.0なので、riched20.dll)をロードする。ロードできなければ終了する。

invoke LoadLibrary,addr RichEditDLL .if eax!=0 mov hRichEdit,eax invoke WinMain,hInstance,0,0, SW_SHOWDEFAULT invoke FreeLibrary,hRichEdit .else invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR .endif invoke ExitProcess,eax

ロードに成功すれば、リッチエディットコントロールの親となる通常のウィンドウを作成する。WM_CREATEブロック内でリッチエディットコントロールを作成している。

invoke CreateWindowEx, WS_EX_CLIENTEDGE,addr RichEditClass,0,\ WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\ CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0 mov hwndRichEdit,eax

ES_MULTILINEスタイルを指定していることに注目しよう。これを指定しなければシングルラインになってしまう。

invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0

リッチエディットコントロール作成後、文字数の上限値をセットしなければならない。デフォルトだとエディットコントロールと同じ64Kなので、それ以上のファイルを読み込めるように拡張しなければならない。 上記では、0FFFFFFFFh サイズを表す -1 を指定している。非常に大きな値だ。

invoke SetColor

次に、文字列/背景色をセットする。この操作はプログラムの他の場所でも行われるので、SetColorという関数を作成した。

SetColor proc LOCAL cfm:CHARFORMAT invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor

リッチエディットコントロールの背景色を設定するのは非常に簡単で、EM_SETBKGNDCOLORメッセージを送信すればよいだけである (マルチラインエディットコントロールの場合はWM_CTLCOLOREDITとなる)。デフォルトは白だ。

invoke RtlZeroMemory,addr cfm,sizeof cfm mov cfm.cbSize,sizeof cfm mov cfm.dwMask,CFM_COLOR push TextColor pop cfm.crTextColor

背景色の設定後、文字列の色を設定するためCHARFORMAT構造体のメンバに値をセットする。 cbSizeに構造体のサイズを適切にセットして、CHARFORMAT2ではなくCHARFORMATであることがリッチエディットコントロールにわかるようにしている。 dwMaskCFM_COLORだけをセットしている。というのは、単に文字列の色をセットしたいだけだからだ。crTextColorは変更したい色の値がセットされる。

invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm ret SetColor endp

次に、アンドゥバッファをクリアしなければならない。なぜなら、この色の設定自体がアンドゥ可能な処理だからで、EM_EMPTYUNDOBUFFERメッセージを送信する。

invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0

CHARFORMAT構造体に値をセット後、コントロール内の全文字列を対象とするためwParamSCF_ALLを指定して、リッチエディットコントロールにEM_SETCHARFORMATメッセージを送信する。

リッチエディットコントロール作成直後、サイズや位置を指定しなかった。それは、親ウィンドウのクライアントエリア全域に表示したかったからだ。親ウィンドウのサイズが変わればいつでも変更できる。

.elseif uMsg==WM_SIZE mov eax,lParam mov edx,eax and eax,0FFFFh shr edx,16 invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE

上記コードでは、MoveWindow関数でリッチエディットコントロールをリサイズするためlParamに新しいサイズを指定している。

ユーザが「File」もしくは「Edit」メニューをクリックしたら、WM_INITPOPUPMENUメッセージを処理しユーザに見せる前にサブメニューのメニューアイテムの状態を設定する。 例えば、リッチエディットコントロールにファイルが既に開かれていたら、「open」はディセーブルにして、残りのメニューアイテムはイネーブルにしておきたい。

「File」メニューバーの場合は、FileOpened変数をフラグとして使用し、既に開かれているかどうかを判断する。TRUEなら既に開かれているということにする。

.elseif uMsg==WM_INITMENUPOPUP mov eax,lParam .if ax==0 ; file menu .if FileOpened==TRUE ; a file is already opened invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED .else invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED .endif

見ての通り、ファイルがOPENされていれば、「open」メニューアイテムはグレーアウトし、残りにメニューアイテムがイネーブルになる。 FileOpendがFALSEなら逆となる。

「Edit」メニューバーの場合はリッチエディットコントロールコントロールとクリップボードの状態をチェックしなければならない。

invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0 .if eax==0 ; no text in the clipboard invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED .endif

まず始めにEM_CANPASTEメッセージを送信してクリップボードに文字があるかどうかチェックする。 もしあれば、SendMessage関数はTRUEを返すので、「paste」メニューアイテムをイネーブルにする。なければ、グレーアウトする。

invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED .endif

次に、EM_CANUNDOメッセージを送信してアンドゥバッファが空かどうかチェックする。空でなければ、SendMessage関数がTRUEを返すので、メニューアイテムをイネーブルにする。

invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED .endif

今度は、EM_CANREDOメッセージを送信して、リドゥバッファが空かどうかをチェックする。空でなければ、SendMessage関数がTRUEを返すので、リドゥメニューアイテムをイネーブルにする。

invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg mov eax,chrg.cpMin .if eax==chrg.cpMax ; no current selection invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED .endif

最後に、EM_EXGETSELメッセージを送信して、現在選択状態にあるかどうかをチェックする。このメッセージはCHARRANGE構造体を使用する。

CHARRANGE STRUCT cpMin DWORD ? cpMax DWORD ? CHARRANGE ENDS

cpMinは指定する範囲の最初の文字の直前をさすインデックス値
cpMinは指定する範囲の最後の文字の直後をさすインデックス値

EM_EXGETSELから戻ると、CHARRANGE構造体に選択範囲の番号がセットされている。 現在選択されていなければ、cpMincpMaxは同一となり、その場合は「cut」「copy」「delete」メニューアイテムはグレーアウトする。

ユーザが「Open」メニューアイテムをクリックすると、ファイルをオープンするダイアログボックスを表示し、 ユーザがファイルを選択すると、そのファイルをOPENする。

invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE mov hFile,eax mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamInProc invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream

CreateFile関数でファイルOPENに成功した後は、EM_STREAMINメッセージを受け付ける準備として、EDITSTREAM構造体をセットする。 dwCookieによって、ファイルハンドルを渡すことにし、pfnCallbackにストリームコールバック関数のアドレスを指定する。

コールバック関数の処理自体は非常に単純だ。

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0 xor eax,1 ret StreamInProc endp

コールバック関数の引数は全てReadFile関数とまったく同じであることがわかるだろう。 ReadFile関数の戻り値に対して、1のxorをとり、その結果が1なら(成功)実際の戻り値は0になる。逆もまた同様である。

invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile mov FileOpened,TRUE

EM_STREAMINから戻ったということは、ストリームに関する操作が終わったということだが、実際にはEDITSTREAM構造体のdwErrorメンバの値をチェックしなければならない。

リッチエディットコントロール(とエディットコントロール)は、内容が変更されたかどうかのフラグを持っている。 EM_GETMODIFYメッセージを送信することによりそのフラグの値を取得することができる。変更されていれば、SendMessageはTRUEを返す。 文字列をストリームに入力するということは、変更の一種なので、wParamにFALSEをセットして、EM_SETMODIFYメッセージを送信して変更フラグをFALSEにしなければならない。 すぐにファイルを閉じ、ファイルがオープン中であることを示すため、FileOpenedをTRUEにする。

ユーザが「save」「saveas」メニューアイテムをクリックすると、EM_STREAMOUTメッセージを使用して、リッチエディットコントロールの内容をファイルに書き出す。 ストリーム入力コールバック関数と同じように、ストリーム出力コールバック関数も非常に簡単で、WriteFile関数と全く同じだ。

「cut」「copy」「paste」「redo」「undo」といった文字列操作はリッチエディットコントロールに、「WM_CUT」「WM_COPY」「WM_PASTE」「WM_REDO」「WM_UNDO」といったメッセージを送信することにより簡単に実装することができる。

「delte」「select all」は以下のようにする

.elseif ax==IDM_DELETE invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0 .elseif ax==IDM_SELECTALL mov chrg.cpMin,0 mov chrg.cpMax,-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg

delete処理は現在選択中の文字列に対して行われる。NULL文字を指定して、EM_REPLACESELメッセージを送信すると、現在選択中の文字がNULL文字に変換され、結局これがdelete処理となる。

全てを選択する処理はEM_EXSETSELメッセージを送信することにより、行う。その際、cpMinは0を、cpMaxは-1にする。

ユーザがオプションメニューバーを選択すると、現在の背景色と文字列色を表すダイアログボックスを表示する。

ユーザがカラーボックスをクリックすると、色を選択するダイアログボックスが表示される。 「color box」は、実はSS_NOTIFYWS_BORDERフラグが指定されたスタティックコントロールである。 SS_NOTIFYフラグ付きのスタティックコントロールは、その上でマウスアクションがあれば、BN_CLICKED(STN_CLICKED)といった親ウィンドウに通知メッセージが送信される。 まるで手品だ。

.elseif ax==IDC_BACKCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push BackgroundColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop BackgroundColor invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif

カラーボックスをクリックすると、CHOOSECOLOR構造体に値をセットしてChooseColor関数をCALLして色を選択するダイアログボックスを表示する。 ユーザが色を選択すると、colorrefの値はrgbResultメンバにセットされており、その値をBackgroundColorにセットする。 その後、強制的に再描画するため、InvalidateRect関数をCALLする。 カラーボックスは親ウィンドウにWM_CTLCOLORSTATICメッセージを送信する。

invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX .if eax==lParam invoke CreateSolidBrush,BackgroundColor ret

WM_CTLCOLORSTATICブロックで、lParamにセットされたスタティックコントロールのハンドルを調べ、どちらのカラーボックスが選択されたかを判断する。 もし背景色が指定されていれば、その色でブラシを作成し、すぐにリターンする。 スタティックコントロールはこの新しく作成されたブラシで背景を描画する。



[戻る]