EternalWindows
エディットコントロール / キャレットの追跡

エディットコントロールには、文字を入力する位置を表すキャレットというものが存在します。 キャレットは、キー入力やクリックで移動し、 多くのエディタでは現在のキャレットの行と列をステータスバーなどに表示しています。 今回は、このようなキャレットの動きを追跡するプログラムを作成します。

#include <windows.h>

WNDPROC g_lpfnDefEditProc = NULL;

LRESULT CALLBACK NewEditProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndEdit = NULL;
	
	switch (uMsg) {
		
	case WM_CREATE:
		hwndEdit = CreateWindowEx(WS_EX_CLIENTEDGE, TEXT("EDIT"), TEXT("abc\r\ndef"), WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_WANTRETURN | WS_VSCROLL, 30, 100, 150, 100, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);		
		g_lpfnDefEditProc = (WNDPROC)GetWindowLongPtr(hwndEdit, GWLP_WNDPROC);
		SetWindowLongPtr(hwndEdit, GWLP_WNDPROC, (LONG_PTR)NewEditProc);
		return 0;

	case WM_DESTROY:
		SetWindowLongPtr(hwndEdit, GWLP_WNDPROC, (LONG_PTR)g_lpfnDefEditProc);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK NewEditProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_MOUSEMOVE:
	case WM_LBUTTONDOWN:
	case WM_KEYDOWN:
	case WM_KEYUP: {
		TCHAR   szBuf[256];
		WORD    wRow, wCol, wRowStart;
		DWORD   dw, dwStart, dwEnd;
		POINT   pt;
		LRESULT lr;
		
		SendMessage(hwnd, EM_GETSEL, (WPARAM)&dwStart, (LPARAM)&dwEnd);
		if (uMsg == WM_MOUSEMOVE && dwStart == dwEnd)
			break;

		lr = CallWindowProc(g_lpfnDefEditProc, hwnd, uMsg, wParam, lParam);
		
		GetCaretPos(&pt);
		dw = (DWORD)SendMessage(hwnd, EM_CHARFROMPOS, 0, (LPARAM)MAKELPARAM(pt.x, pt.y));

		wRow = HIWORD(dw);
		wRowStart = (WORD)SendMessage(hwnd, EM_LINEINDEX, wRow, 0);

		wCol = LOWORD(dw) - wRowStart;

		wsprintf(szBuf, TEXT("%d %d"), wRow, wCol);
		SetWindowText(GetParent(hwnd), szBuf);

		return lr;
	}

	default:
		break;

	}

	return CallWindowProc(g_lpfnDefEditProc, hwnd, uMsg, wParam, lParam);
}

キャレットはキー入力やクリックで移動するため、 WM_KEYDOWNやWM_LBUTTONDOWNでキャレットの位置を取得することになります。 ただし、親ウインドウのウインドウプロシージャでこれらのメッセージを検出しても、 それは親ウインドウへの操作を検出しているたけですから、 エディットコントロールをサブクラス化することにより、 エディットコントロールへの操作を検出することになります。

g_lpfnDefEditProc = (WNDPROC)GetWindowLongPtr(hwndEdit, GWLP_WNDPROC);
SetWindowLongPtr(hwndEdit, GWLP_WNDPROC, (LONG_PTR)NewEditProc);

まず、GetWindowLongPtrにGWLP_WNDPROCを指定して元のウインドウプロシージャを取得します。 この元のウインドウプロシージャは、新しいウインドウプロシージャで元のウインドウプロシージャを呼び出す場合に使用されます。 SetWindowLongPtrにより、第3引数のNewEditProcが新しいウインドウプロシージャに設定され、 この関数内でエディットコントロールのメッセージを検出できるようになります。

NewEditProcでは、WM_KEYDOWNとWM_LBUTTONDOWNの他にWM_KEYUPとWM_MOUSEMOVEも検出しています。 WM_KEYUPは、DelキーやBackキーで文字を削除する場合に意味を持ち、 WM_MOUSEMOVEは文字を複数選択する場合に意味を持ちます。 ただし、WM_MOUSEMOVEはマウスを移動する度に送られるため、 文字が選択されていない場合はこのメッセージを考慮しないようにしています。 各メッセージの処理は共通して記述することができ、 最初にCallWindowProcでデフォルトの処理を実行してから、 キャレットと行と列を取得することになります。

GetCaretPos(&pt);
dw = (DWORD)SendMessage(hwnd, EM_CHARFROMPOS, 0, (LPARAM)MAKELPARAM(pt.x, pt.y));

wRow = HIWORD(dw);
wRowStart = (WORD)SendMessage(hwnd, EM_LINEINDEX, wRow, 0);

wCol = LOWORD(dw) - wRowStart;

wsprintf(szBuf, TEXT("%d %d"), wRow, wCol);
SetWindowText(GetParent(hwnd), szBuf);

GetCaretPosは、現在のキャレットの位置をx座標とy座標で返します。 このような座標だけではキャレットの行や列を特定することはできませんから、 座標から文字の位置情報を取得するEM_CHARFROMPOSを送信することになります。 戻り値の上位ワードには、キャレットの行番号が格納されており、 下位ワードにはキャレットの位置までに存在する文字数が格納されています。 この文字数から、キャレットが存在する行の先頭までの文字数を引けば列番号 を取得することができるため、 EM_LINEINDEXを送信することで行の先頭までの文字数を取得します。 初期化された行番号と列番号は、SetWindowTextによって親ウインドウのタイトルに表示されます。


戻る