EternalWindows
WebBrowser コントロール / OpenSearchの実装

今回は、OpenSearchの実装について説明します。 OpenSearchとは、特定の検索プロバイダ(検索エンジン)を使用して検索を行う仕組みであり、 これを理解することでgoogleの検索やamazonの検索を同じように行えます。 OpenSearchのためのウインドウは、ブラウザの次の部分に相当します。

エディットコントロール上で何か文字を入力すると、検索候補の文字列がウインドウに表示されます。 ここでEnterを押したり、検索候補の文字列を押したりすれば、 現在選択されている検索プロバイダで検索を行えます。 左側のボタンの矢印をクリックすれば、ポップアップメニューから検索プロバイダを変更できます。

OpenSearchのためのウインドウはCOpenSearchによって識別され、CRebarMgrによって使用されます。

BOOL COpenSearch::Create(HWND hwndParent)
{
	TBBUTTON tbButton[] = {
		{STD_FIND, ID_BUTTON_SEARCH, TBSTATE_ENABLED, BTNS_BUTTON | BTNS_DROPDOWN, {0}, 0, 0},
	};
	int           nCount = sizeof(tbButton) / sizeof(tbButton[0]);
	DWORD         dwStyle;
	REBARBANDINFO bandInfo;

	m_hwndToolbarSearch = CreateToolbarEx(hwndParent, WS_CHILD | WS_VISIBLE | CCS_NORESIZE | CCS_NODIVIDER, ID_SEARCH_TOOLBAR, 0, HINST_COMMCTRL, IDB_STD_SMALL_COLOR, tbButton, nCount, 0, 0, 0, 0, sizeof(TBBUTTON));
	m_hwndEditSearch = CreateWindowEx(0, WC_EDIT, TEXT(""), WS_CHILD | WS_VISIBLE | WS_BORDER, 40, 0, 250, 25, m_hwndToolbarSearch, (HMENU)ID_SEARCH_EDIT, NULL, NULL);
	
	dwStyle = (DWORD)SendMessage(m_hwndToolbarSearch, TB_GETSTYLE, 0, 0) | TBSTYLE_FLAT;
	SendMessage(m_hwndToolbarSearch, TB_SETSTYLE, 0, (LPARAM)dwStyle);
	SendMessage(m_hwndToolbarSearch, TB_SETEXTENDEDSTYLE, 0, TBSTYLE_EX_DRAWDDARROWS);
	
	bandInfo.cbSize     = sizeof(REBARBANDINFO);
	bandInfo.fMask      = RBBIM_STYLE | RBBIM_CHILD | RBBIM_CHILDSIZE;
	bandInfo.fStyle     = RBBS_CHILDEDGE;
	bandInfo.hwndChild  = m_hwndToolbarSearch;
	bandInfo.cxMinChild = 230;
	bandInfo.cyMinChild = 25;
	SendMessage(hwndParent, RB_INSERTBAND, (WPARAM)-1, (LPARAM)&bandInfo);

	CreateSearchProviderData();

	SetWindowSubclass(m_hwndEditSearch, ::SubclassProc, 1, (DWORD_PTR)this);

	m_pSuggestWindow = new CSuggestWindow;
	m_pSuggestWindow->Create(0, m_hwndEditSearch, this);

	return TRUE;
}

Createの第1引数はレバーコントロールのウインドウであり、 これの子ウインドウとしてツールバーを作成しています。 このツールバーは検索ボタンを持っている他、子ウインドウとしてエディットコントロールを作成しています。 ウインドウを用意できたらバンドとして追加するために、RB_INSERTBANDをレバーコントロールに送信します。 CreateSearchProviderDataという自作関数は、レジストリから検索プロバイダのデータを取得します。 この関数については後述します。 SetWindowSubclassでエディットコントロールをサブクラス化しているのは、 エディットコントロール上で押下されたキーを検出するためです。 この入力されたキーを検出することで、検索候補のウインドウ上で文字列を列挙できます。 検索候補のウインドウの処理はCSuggestWindowという別のクラスに実装されており、 エディットコントロール上でキーが入力された際に表示されます。

検索プロバイダは、xmlファイルを実体としてweb上に公開されています。 これをインストールしようとした場合、次のようなダイアログが表示されます。

ここで追加を選択した場合、xmlファイルの情報が下記のレジストリキー以下に書き込まれます。

HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\SearchScopes

上記レジストリキーの中身を示します。

SearchScopesキーのサブキーはGUIDになっていますが、これは検索プロバイダを一意に識別する値です。 DisplayNameから分かるように、現在選択されているのはgoogleの検索プロバイダです。 URLは実際に検索の際にアクセスするURLであり、これとDisplayNameは必ずレジストリに設定されているはずです。 SuggestionsURLは検索候補のデータを取得する際にアクセスするURLであり、 xmlファイル上でそのURLが記述されていない場合は、このエントリは存在しません。 ただし、IE8以前ではxmlファイル上で検索候補のURLが記述されていても、 そのURLがレジストリに記述されないことになっています。 よって、検索候補の機能を使用するためにはIE8以上がインストールされている必要があります。 もし、この制限を回避したいのであれば、各種検索プロバイダのxmlファイルをアプリケーションのフォルダに配置しておき、 そのファイルから検索候補のURLを取得することになるでしょう。 ちなみに、検索プロバイダによってはSuggestionsURLではなく、SuggestionsURL_JSONを設定している場合もあります。 前者の場合は検索候補のデータがXML形式で返されますが、後者の場合はデータがJSON形式で返されます。

今回作成することになるブラウザでは、使用する検索プロバイダを任意のタイミングで切り替えられるように、ポップアップメニューを作成します。 また、検索プロバイダを管理しやすくするためにデータ構造体も作成します。

void COpenSearch::CreateSearchProviderData()
{
	DWORD            i;
	HKEY             hKey, hSubKey;
	LONG             lResult;
	TCHAR            szGuid[256], szSubKey[256], szDefaultGuid[256], szName[256];
	DWORD            dwType, dwSize;
	BOOL             bSuggest;
	LPSEARCHPROVIDER lpSP;
	WCHAR            szUrl[MAX_URL_LENGTH], szSuggestUrl[MAX_URL_LENGTH];
	
	lResult = RegOpenKeyEx(HKEY_CURRENT_USER, TEXT("Software\\Microsoft\\Internet Explorer\\SearchScopes"), 0, KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE, &hKey);
	if (lResult != ERROR_SUCCESS)
		return;
	
	dwSize = sizeof(szDefaultGuid);
	RegQueryValueEx(hKey, TEXT("DefaultScope"), NULL, &dwType, (LPBYTE)szDefaultGuid, &dwSize);

	m_hmenuSearch = CreatePopupMenu();

	m_nDefaultProviderIndex = 0;
	m_hdpaSP = DPA_Create(1);

	for (i = 0;; i++) {
		dwSize = sizeof(szGuid);
		lResult = RegEnumKeyEx(hKey, i, szGuid, &dwSize, NULL, NULL, NULL, NULL);
		if (lResult != ERROR_SUCCESS)
			break;

		wsprintf(szSubKey, TEXT("Software\\Microsoft\\Internet Explorer\\SearchScopes\\%s"), szGuid);
		lResult = RegOpenKeyEx(HKEY_CURRENT_USER, szSubKey, 0, KEY_QUERY_VALUE, &hSubKey);
		if (lResult != ERROR_SUCCESS)
			continue;
		
		
		dwSize = sizeof(szName);
		szName[0] = '\0';
		RegQueryValueEx(hSubKey, NULL, NULL, &dwType, (LPBYTE)szName, &dwSize);
		if (szName[0] == '\0') {
			dwSize = sizeof(szName);
			RegQueryValueEx(hSubKey, TEXT("DisplayName"), NULL, &dwType, (LPBYTE)szName, &dwSize);
		}

		dwSize = sizeof(szUrl);
		RegQueryValueEx(hSubKey, TEXT("URL"), NULL, &dwType, (LPBYTE)szUrl, &dwSize);
		
		dwSize = sizeof(szSuggestUrl);
		lResult = RegQueryValueEx(hSubKey, TEXT("SuggestionsURL"), NULL, &dwType, (LPBYTE)szSuggestUrl, &dwSize);
		bSuggest = lResult == ERROR_SUCCESS;
		
		RegCloseKey(hSubKey);

		if (lstrcmp(szGuid, szDefaultGuid) == 0) {
			m_nDefaultProviderIndex = i;
			SendMessage(m_hwndEditSearch, EM_SETCUEBANNER, 0, (LPARAM)szName);
		}

		InitializeMenuItem(m_hmenuSearch, szName, ID_SEARCH_FIRST + i, NULL);

		lpSP = (LPSEARCHPROVIDER)HeapAlloc(GetProcessHeap(), 0, sizeof(SEARCHPROVIDER));
		lstrcpy(lpSP->szName, szName);
		lstrcpyW(lpSP->szUrl, szUrl);
		lstrcpyW(lpSP->szSuggestUrl, szSuggestUrl);
		lpSP->bSuggest = bSuggest;
		
		DPA_InsertPtr(m_hdpaSP, i, (void *)lpSP);
	}
	
	RegCloseKey(hKey);
}

まず、検索プロバイダのレジストリキーをオープンし、DefaultScopeからデフォルトの検索プロバイダのGUIDを取得します。 また、ポップアップメニューの作成とSEARCHPROVIDER構造体を管理するDPAも作成しておきます。 ループ内では、RegEnumKeyExでSearchScopesキーのサブキー名を取得し、これを基にそのサブキーをオープンします。 取得したいデータはそれぞれ、DisplayName、URL、SuggestionsURLですが、 名前については既定のエントリに設定されていることもあるため、 それが空の文字であった場合にDisplayNameを取得するようにしています。 JSON形式のデータをサポートする場合は、SuggestionsURL_JSONも取得するようにします。 今回の列挙の対象となった検索プロバイダのGUIDがデフォルトの検索プロバイダと一致した場合は、 その検索プロバイダのインデックスを保存するようにします。 また、エディットコントロール上で薄く名前を表示できるように、EM_SETCUEBANNERを送信します。 InitializeMenuItemという自作関数によって、検索プロバイダの名前がポップアップメニューに追加されるようになります。 最後に、検索プロバイダの情報を格納するためのメモリを確保し、 各種情報を設定してからDPAに追加します。 これで後は、インデックスベースで検索プロバイダの情報を取得できるようになります。

CreateSearchProviderDataで作成したポップアップメニューは、ツールバーのボタンの矢印が押下された場合に表示されます。 矢印が押下された場合は、CRebarMgrによってCOpenSearch::OnNotifyが呼ばれます。

void COpenSearch::OnNotify(WPARAM wParam, LPARAM lParam)
{
	int              i, nId, nCount, nIndex;
	POINT            pt;
	LPSEARCHPROVIDER lpSP;
	
	if (((LPNMHDR)lParam)->idFrom != ID_SEARCH_TOOLBAR || ((LPNMHDR)lParam)->code != TBN_DROPDOWN)
		return;

	nCount = GetMenuItemCount(m_hmenuSearch);
	for (i = 0; i < nCount; i++) {
		nId = ID_SEARCH_FIRST + i;
		SetMenuItem(m_hmenuSearch, nId, TRUE, i == m_nDefaultProviderIndex);
	}
			
	GetCursorPos(&pt);
	nId = TrackPopupMenu(m_hmenuSearch, TPM_RETURNCMD, pt.x, pt.y, 0, m_hwndToolbarSearch, NULL);
	if (nId == 0)
		return;
	
	nIndex = nId - ID_SEARCH_FIRST;
	if (m_nDefaultProviderIndex != nIndex)
		m_pSuggestWindow->CloseConnect();
	m_nDefaultProviderIndex = nIndex;

	lpSP = (LPSEARCHPROVIDER)DPA_GetPtr(m_hdpaSP, m_nDefaultProviderIndex);
	SendMessage(m_hwndEditSearch, EM_SETCUEBANNER, 0, (LPARAM)lpSP->szName);
}

通知の内容が検索ボタンであると分かったら、これから表示するメニューのチェック状態を調整します。 デフォルトの検索プロバイダの項目にはチェックを付けるようにしたいため、このような処理を行っています。 ポップアップメニューの表示はTrackPopupMenuで可能ですが、 第2引数にTPM_RETURNCMDを指定しているため、 選択された項目のIDは戻り値として返ることになります。 項目のIDからインデックスを求めたら、それをデフォルトの検索プロバイダと異なるかを調べます。 もし、異なる場合は新しい検索プロバイダの検索が必要になるため、 現在のサーバーへの接続をCloseConnectで切断するようにします。 CSuggestWindowの詳細については後述します。 新しい検索プロバイダのインデックスが決まったら、それを基にSEARCHPROVIDER構造体を取得し、 検索プロバイダの名前をエディットコントロールに設定します。

検索ボタンが押下された場合は、CRebarMgrによってCOpenSearch::OnCommandが呼ばれます。

void COpenSearch::OnCommand(int nId)
{
	if (nId == ID_BUTTON_SEARCH) {
		WCHAR szKeyword[256];
		GetWindowTextW(m_hwndEditSearch, szKeyword, 256);
		SearchStart(szKeyword);
	}
}

エディットコントロールに入力されている文字列は、GetWindowTextによって取得できます。 後は、この文字列で検索を行えばよいわけですが、 この文字列はURLに対してどのように追加すればよいのでしょうか。 これを理解するためには、先に示した図のURLエントリの中身を確認する必要があります。 googleのURLを例にして見ていきます。

http://www.google.com/search?q={searchTerms}&rlz=1I7DAJP_ja&ie={inputEncoding}&oe={outputEncoding}&sourceid=ie7

{searchTerms}の部分を検索したい文字列で置き換えることになります。 {inputEncoding}には、{searchTerms}を置き換えた文字列がどのような形式でエンコードされているかを指定します。 たとえば、検索したい文字列をUTF-8型式で指定したならば、{inputEncoding}の部分はUTF-8になります。 {outputEncoding}には、返されるデータをどのような型式で受け取るかを指定します。 この他、{language}という文字列が含まれることがありますが、これは日本語を示すjaを指定すればよいでしょう。 {inputEncoding}が含まれていない場合のエンコード形式については、UTF-8が妥当であると思われます。 一部の検索プロバイダでは、UNICODE形式の検索文字列を受け付けないようになっています。

検索を開始するSearchStartの実装は、次のようになっています。

void COpenSearch::SearchStart(LPWSTR lpszKeyword)
{
	WCHAR            szNewUrl[MAX_URL_LENGTH];
	LPSEARCHPROVIDER lpSP = (LPSEARCHPROVIDER)DPA_GetPtr(m_hdpaSP, m_nDefaultProviderIndex);

	ReplaceString(lpSP->szUrl, lpszKeyword, L"ja-JP", L"UTF-8", L"UTF-8", szNewUrl);
	
	g_pWebBrowserContainer->Navigate(szNewUrl, FALSE);
}

ReplaceStringという自作メソッドは、先に述べた{searchTerms}のような文字列を置き換えます。 最後の引数には置き換えられた新しいURLが返されるようになっており、 これをNavigateに指定することで実際にページへアクセスします。 ReplaceStringの内部は次のようになっています。

void COpenSearch::ReplaceString(LPWSTR lpszUrl, LPWSTR lpszSearchTerms, LPWSTR lpszLanguage, LPWSTR lpszInputEncoding, LPWSTR lpszOutputEncoding, LPWSTR lpszNewUrl)
{
	LPWSTR lpszReplace[] = {
		L"{searchTerms}", L"{language}", L"{inputEncoding}",  L"{outputEncoding}"
	};
	int nReplaceLen[] = {
		lstrlenW(lpszReplace[0]), lstrlenW(lpszReplace[1]), lstrlenW(lpszReplace[2]), lstrlenW(lpszReplace[3])
	};
	int nLen;
	int nTotalLen = 0;

	while (*lpszUrl != '\0') {
		*lpszNewUrl = *lpszUrl;
		if (StrCmpNW(lpszUrl, lpszReplace[0], nReplaceLen[0]) == 0) {
			WCHAR  sz[256];
			LPWSTR lpsz;

			if (lpszInputEncoding == NULL || lstrcmpW(lpszInputEncoding, L"UTF-8") == 0) {
				UnicodeToUTF8(lpszSearchTerms, sz, sizeof(sz));	
				lpsz = sz;
			}
			else
				lpsz = lpszSearchTerms;

			nLen = lstrlenW(lpsz);
			if (nTotalLen + nLen >= MAX_URL_LENGTH)
				break;
			lstrcpynW(lpszNewUrl, lpsz, nLen + 1);
			lpszUrl += nReplaceLen[0];
			lpszNewUrl += nLen;
			nTotalLen += nLen;
		}
		else if (StrCmpNW(lpszUrl, lpszReplace[1], nReplaceLen[1]) == 0) {
			nLen = lstrlenW(lpszLanguage);
			if (nTotalLen + nLen >= MAX_URL_LENGTH)
				break;
			lstrcpynW(lpszNewUrl, lpszLanguage, nLen + 1);		
			lpszUrl += nReplaceLen[1];
			lpszNewUrl += nLen;
			nTotalLen += nLen;
		}
		else if (StrCmpNW(lpszUrl, lpszReplace[2], nReplaceLen[2]) == 0) {
			nLen = lstrlenW(lpszInputEncoding);
			if (nTotalLen + nLen >= MAX_URL_LENGTH)
				break;
			lstrcpynW(lpszNewUrl, lpszInputEncoding, nLen + 1);		
			lpszUrl += nReplaceLen[2];
			lpszNewUrl += nLen;
			nTotalLen += nLen;
		}
		else if (StrCmpNW(lpszUrl, lpszReplace[3], nReplaceLen[3]) == 0) {
			nLen = lstrlenW(lpszOutputEncoding);
			if (nTotalLen + nLen >= MAX_URL_LENGTH)
				break;
			lstrcpynW(lpszNewUrl, lpszOutputEncoding, nLen + 1);		
			lpszUrl += nReplaceLen[3];
			lpszNewUrl += nLen;
			nTotalLen += nLen;
		}
		else {
			lpszUrl++;
			lpszNewUrl++;
			nTotalLen++;
		}
	}

	*lpszNewUrl = '\0';
}

置き換えるべき文字列を配列として宣言し、文字列を走査している際にそれらと一致するかを調べます。 一致する場合は対応する変数、たとえば{language}ならlpszLanguageで置き換えることになりますが、 {searchTerms}の場合は少し特殊です。 この場合は、lpszInputEncodingがNULLかUTF-8の際にエンコードを行うことになるため、 UnicodeToUTF8という自作メソッドで返された文字列を使用します。 UnicodeToUTF8は、次のように定義されています。

void COpenSearch::UnicodeToUTF8(LPWSTR lpszKeyWord, LPWSTR lpszUrlEncode, DWORD dwBufferSize)
{
	char            szUtf8[256], szUrlEncode[256], *p;
	unsigned char   c;
	int             i;
	DWORD           dwMode;
	UINT            uSrcSize, uDestSize;
	IMultiLanguage2 *pMultiLanguage2;

	CoCreateInstance(CLSID_CMultiLanguage, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pMultiLanguage2));

	uSrcSize = (lstrlenW(lpszKeyWord) + 1) * sizeof(WCHAR);
	uDestSize = sizeof(szUtf8);
	dwMode = 0;
	
	pMultiLanguage2->ConvertStringFromUnicode(&dwMode, 65001, lpszKeyWord, &uSrcSize, szUtf8, &uDestSize);

	p = szUrlEncode;
	for (i = 0; i < lstrlenA(szUtf8); i++) {
		c = szUtf8[i];
		
		if((c >= '0' && c <= '9')
		|| (c >= 'A' && c <= 'Z')
		|| (c >= 'a' && c <= 'z')
		|| (c == '\'')
		|| (c == '*')
		|| (c == ')')
		|| (c == '(')
		|| (c == '-')
		|| (c == '.')
		|| (c == '_')) {
			*p = c;
			++p;
		}
		else if (c == ' ') { 
			*p = '+';
			++p;
		}
		else {
			wsprintfA(p, "%%%02X", c);
			p += 3;
		}
	}
	*p = '\0';

	uSrcSize = lstrlenA(szUrlEncode) + 1;
	uDestSize = dwBufferSize;
	dwMode = 0;
	pMultiLanguage2->ConvertStringToUnicode(&dwMode, 65001, szUrlEncode, &uSrcSize, lpszUrlEncode, &uDestSize);

	pMultiLanguage2->Release();
}

UNICODE文字列からUTF-8への変換には、IMultiLanguage2::ConvertStringFromUnicodeを使用できます。 WideCharToMultiByteにCP_UTF8を指定しても同じことができますが、 今回のようなブラウザの開発ではIMultiLanguage2のほうがよいと判断しました。 第2引数は変換先のコードページであり、65001はUTF-8を識別しています。 第3引数は変換元の文字列を指定し、第5引数は変換後の文字列を受け取るバッファを指定します。 UTF-8への変換が終了したら、それをURLで使用できるようにするためにURLエンコードを行います。 この方法についてはループ文の通りです。 URLエンコードが終わったらこの文字列を返せばよいのですが、 呼び出し側は文字列をUNICODEで要求しているため、ConvertStringToUnicodeで変換するようにしています。 結局のところ、どのような変換結果になるのかというと、たとえばlpszKeyWordがabcラジオならば、 lpszUrlEncodeはabc%E3%83%A9%E3%82%B8%E3%82%AAになります。

ここからは、CSuggestWindowの実装について説明します。 このクラスの目的は、エディットコントロールに何か文字が入力された際にウインドウを表示することであり、 そこには検索候補となる文字列が列挙されます。 この文字列を取得するためにはレジストリのSuggestionsURLで識別されるURLにアクセスし、 返されたXML形式のデータを解析する必要がありますが、 前者についてはWinHTTP、後者についてはXmlLiteを使用します。 こうしたAPIを使用することで、通信や解析に必要な処理を大幅に削減できます。 次に示すCSuggestWindow::Createは、COpenSearch::Createで呼ばれています。

BOOL CSuggestWindow::Create(HWND hwndParent, HWND hwndEdit, COpenSearch *pOpenSearch)
{
	m_hSession = WinHttpOpen(g_szUserAgentW, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);
	if (m_hSession == NULL)
		return FALSE;

	InitializeCriticalSection(&m_cs);

	m_hwndSuggest = CreateWindowEx(0, g_szSuggestClassName, TEXT(""), WS_CHILD, 0, 0, 0, 0, g_pWebBrowserContainer->GetWindow(), (HMENU)ID_SUGGEST, NULL, NULL);
	SetWindowLongPtr(m_hwndSuggest, GWLP_USERDATA, (LONG_PTR)this);
	
	m_hwndEdit = hwndEdit;

	m_pOpenSearch = pOpenSearch;

	return TRUE;
}

WinHTTPを使用するためにはセッションハンドルが必要になるため、WinHttpOpenで取得しておきます。 第1引数はユーザーエージェント文字列を指定し、プロキシサーバーを使用しない場合は、第2引数から第4引数は上記の通りで問題ありません。 第5引数は、非同期通信のためにWINHTTP_FLAG_ASYNCを指定します。 InitializeCriticalSectionでクリティカルセクションを初期化している理由は後述します。 検索候補のウインドウは独自に作成することになるため、独自のウインドウクラスを登録しておきます。 CreateWindowExでウインドウが作成されますが、この時点ではまだウインドウを表示する必要はないため、 WS_VISIBLEは含まれていません。 SetWindowLongPtrを呼び出しているのは、静的なウインドウプロシージャからクラスのウインドウプロシージャにアクセスできるようにするためです。 エディットコントロールのハンドルとCOpenSearchへのポインタは後で必要になるため、メンバとして保存するようにします。

検索候補のウインドウを表示するためには、エディットコントロールに文字が入力されたタイミングを検出しなければなりません。 COpenSearchはエディットコントロールをサブクラス化しており、 SubclassProc内で必要となるメッセージを独自に処理することになります。 まず、次のメッセージの処理を確認します。

case WM_KEYDOWN:
	if (wParam == VK_RETURN) { 
		WCHAR szKeyword[256];
		GetWindowTextW(m_hwndEditSearch, szKeyword, 256);
		SearchStart(szKeyword);
		SetFocus(g_pWebBrowserContainer->GetWindow());
	}
	else if (wParam == VK_UP || wParam == VK_DOWN)
		m_pSuggestWindow->TrackUpDownKey(wParam, lParam);
	else
		;
	break;

case WM_SHOWSUGGEST:
	m_pSuggestWindow->Show();
	return 0;

case WM_KILLFOCUS:
	m_pSuggestWindow->CloseConnect();
	break;

WM_KEYDOWNでEnterの押下を検出した場合は、検索を開始するためにSearchStartを呼び出します。 SetFocusで他のウインドウにフォーカスを渡しているのは、自身のウインドウに対してWM_KILLFOCUSが送られるようにするためです。 これにより、検索後に依然として検索候補のウインドウが表示され続けることを防げます。 WM_SHOWSUGGESTは独自に定義したメッセージであり、これが送られた場合は検索候補のウインドウを実際に表示します。 これまで、ウインドウが表示されるタイミングを文字が入力された際と述べましたが、 正確にはこれはサーバーからXML形式のデータが返された際です。 このデータの取得は、WinHTTPによって作成された別スレッドで行っており、 データの取得が完了したらWM_SHOWSUGGESTでメインスレッドに通知します。 表示されたウインドウが非表示になるべきタイミングは、 エディットコントロールのフォーカスが失われた場合が妥当であると考えたため、 WM_KILLFOCUSを処理するようにしています。 CloseConnectはサーバーへの接続を切断し、確保したデータを開放してウインドウを非表示にします。

検索候補ウインドウの表示開始するために、次のメッセージに注目します。

case WM_KEYUP:
	if (!m_bImeSart) {
		WCHAR szKeyword[256];
		GetWindowTextW(m_hwndEditSearch, szKeyword, 256);
		m_pSuggestWindow->StartSuggest(szKeyword);
	}
	break;

case WM_IME_STARTCOMPOSITION:
	m_bImeSart = TRUE;
	break;

case WM_IME_ENDCOMPOSITION:
	m_bImeSart = FALSE;
	break;

case WM_IME_COMPOSITION:
	if (lParam & GCS_COMPSTR) {
		HIMC  himc;
		WCHAR szKeyword[256];
		LONG  lResult, lSize;
	
		himc = ImmGetContext(hWnd);

		lSize = ImmGetCompositionStringW(himc, GCS_COMPSTR, NULL, 0);
		if (lSize > 0 || lSize < 256) {
			lResult = ImmGetCompositionStringW(himc, GCS_COMPSTR, szKeyword, lSize);
			lResult /= sizeof(WCHAR);
			szKeyword[lResult] = '\0';
			m_pSuggestWindow->StartSuggest(szKeyword);
		}
	
		ImmReleaseContext(hWnd, himc);
	}
	break;

WM_KEYUPが送られた場合はキーが入力されたことを意味するため、 その文字列をGetWindowTextで取得し、StartSuggestで検索候補のウインドウを表示しようとします。 ただし、IMEにオンがなっている状態で入力された文字列はGetWindowTextに含まれないため、 IMEがオンの場合は処理をしないようにしています。 IMEがオンがなっている状態で入力された文字列とは、次のような変換文字列(未確定の文字列)です。

簡単に言えば、日本語を入力しようとしたときにはIMEがオンがなります。 そして、オンになった場合はWM_IME_STARTCOMPOSITIONが送られ、 オフになった場合はWM_IME_ENDCOMPOSITIONが送られます。 これらのメッセージでは、WM_KEYUPのためにm_bImeSartを適切に調整します。 WM_IME_COMPOSITIONは、IMEがオンの状態で何らかの文字列が入力された場合に送られ、 ImmGetCompositionStringにGCS_COMPSTRを指定すれば、その文字列を取得できます。 ただし、一回目の呼び出しでは文字列のサイズを確認するため、NULLと0を指定しています。 サイズがバッファ内であれば、実際にszKeywordを指定してImmGetCompositionStringを呼び出し、 入力された文字列を取得します。 GCS_COMPSTRを指定した場合のImmGetCompositionStringの戻り値は文字列のサイズであり、 この値から文字列の終端のインデックスを割り出して、\0'文字を格納するようにします。 このような処理が必要なのは、既定で'\0'文字が格納されないからです。 StartSuggestの実装は次のようになっています。

void CSuggestWindow::StartSuggest(LPWSTR lpszKeyword)
{
	WCHAR            szUrl[MAX_URL_LENGTH];
	LPSEARCHPROVIDER lpSP = m_pOpenSearch->GetDefaultProvider();

	if (!lpSP->bSuggest)
		return;

	if (lstrcmpW(m_szSearchPrev, lpszKeyword) == 0)
		return;
	lstrcpyW(m_szSearchPrev, lpszKeyword);
	
	m_pOpenSearch->ReplaceString(lpSP->szSuggestUrl, lpszKeyword, L"ja-JP", L"UTF-8", L"UTF-8", szUrl);

	if (m_hConnect == NULL)
		ConnectServer(szUrl);

	if (m_hConnect != NULL)
		SendRequest(szUrl);
}

現在の検索プロバイダが検索候補をサポートしていない場合は、処理を続行する必要はありません。 渡された文字列が以前の検索文字列と異なる場合に限り、その文字列を基に検索候補を取得します。 検索候補の際に使用するURLはszSuggestUrlから取得でき、 ReplaceStringを呼び出すことで{searchTerms}などの部分を置き換えます。 m_hConnectがNULLであるということはszUrlのサーバーにまだ接続していないということであり、 この場合はConnectServerでサーバーに接続するようにします。 そして、接続が完了したらSendRequestでデータをサーバーに要求します。 ConnectServerの処理は次のようになっています。

BOOL CSuggestWindow::ConnectServer(LPWSTR lpszUrl)
{
	URL_COMPONENTSW urlComponents;
	WCHAR           szHostName[256];
	
	ZeroMemory(&urlComponents, sizeof(URL_COMPONENTSW));
	urlComponents.dwStructSize     = sizeof(URL_COMPONENTSW);
	urlComponents.lpszHostName     = szHostName;
	urlComponents.dwHostNameLength = sizeof(szHostName) / sizeof(WCHAR);

	if (!WinHttpCrackUrl(lpszUrl, lstrlenW(lpszUrl), 0, &urlComponents))
		return FALSE;

	m_hConnect = WinHttpConnect(m_hSession, szHostName, INTERNET_DEFAULT_PORT, 0);
	if (m_hConnect == NULL)
		return FALSE;

	return TRUE;
}

サーバーへの接続は、WinHttpConnectで可能です。 第1引数には接続するサーバーのホスト名を指定する必要があるため、 URLからホスト名を取得するためにWinHttpCrackUrlを呼び出しています。 続いて、SendRequestの処理を確認します。

BOOL CSuggestWindow::SendRequest(LPWSTR lpszUrl)
{
	URL_COMPONENTSW urlComponents;
	WCHAR           szUrlPath[MAX_URL_LENGTH];
	HINTERNET       hRequest;
	LPREQUEST       lpRequest;

	ZeroMemory(&urlComponents, sizeof(URL_COMPONENTSW));
	urlComponents.dwStructSize    = sizeof(URL_COMPONENTSW);
	urlComponents.lpszUrlPath     = szUrlPath;
	urlComponents.dwUrlPathLength = sizeof(szUrlPath) / sizeof(WCHAR);

	if (!WinHttpCrackUrl(lpszUrl, lstrlenW(lpszUrl), 0, &urlComponents))
		return FALSE;

	hRequest = WinHttpOpenRequest(m_hConnect, L"GET", szUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
	if (hRequest == NULL)
		return FALSE;

	WinHttpSetStatusCallback(hRequest, ::WinHttpStatusCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0);

	lpRequest = (LPREQUEST)HeapAlloc(GetProcessHeap(), 0, sizeof(REQUEST));
	lpRequest->dwReadSize = 0;
	lpRequest->pSuggestWindow = this;

	WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, (DWORD)lpRequest);

	return TRUE;
}

サーバーに対してデータを要求するためには、その要求を識別するハンドルが必要になります。 これは、WinHttpOpenRequestで取得できます。 第3引数はURL内のホスト名以下の部分を指定しますが、これはWinHttpCrackUrlで取得できます。 ハンドルを取得したら、WinHttpSendRequestで実際にデータを要求できますが、 その前にWinHttpSetStatusCallbackでコールバック関数の設定を行います。 これにより、データを受信したタイミングを検出できます。 WinHttpSendRequestの最後の引数には、独自に定義したREQUEST構造体を関連付けています。 これは、その要求によって取得したデータを管理するために存在します。 thisポインタを設定しているのは、静的なコールバック関数からクラスのコールバック関数にアクセスするためです。

コールバック関数であるWinHttpStatusCallbackの実装は次のようになっています。

void CSuggestWindow::WinHttpStatusCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
	HINTERNET hRequest = hInternet;
	LPREQUEST lpRequest = (LPREQUEST)dwContext;

	if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE)
		WinHttpReceiveResponse(hRequest, NULL);
	else if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE) {
		DWORD dwStatusCode = 0;
		DWORD dwSize = sizeof(DWORD);
		
		WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &dwStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX);

		if (dwStatusCode == HTTP_STATUS_OK)
			WinHttpReadData(hRequest, lpRequest->buffer, sizeof(lpRequest->buffer), NULL);
	}
	else if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_READ_COMPLETE) {
		HGLOBAL hglobal;
		IStream *pStream;

		hglobal = GlobalAlloc(GPTR, dwStatusInformationLength);
		CopyMemory(hglobal, lpRequest->buffer, dwStatusInformationLength);

		CreateStreamOnHGlobal(hglobal, FALSE, &pStream);
		GetXmlData(pStream);
		
		pStream->Release();
		GlobalFree(hglobal);
	
		PostMessage(m_hwndEdit, WM_SHOWSUGGEST, 0, 0);

		WinHttpCloseHandle(hRequest);
		HeapFree(GetProcessHeap(), 0, lpRequest);
	}
}

コールバック関数の第1引数は、WinHttpOpenRequestで取得したハンドルです。 また、第2引数はWinHttpSendRequestの最後の引数が渡されます。 WinHttpSendRequestが成功した場合は、WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETEという通知が送られます。 ここでWinHttpReceiveResponseを呼び出せば、今度はWINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLEが送られ、 ここではWinHttpQueryHeadersを呼び出してHTTPのヘッダ部分を取得できます。 この結果がHTTP_STATUS_OKであるということは実際に要求したデータが存在するということであり、 WinHttpReadDataでそのデータを取得できます。 引数からも分かるように、データはREQUEST構造体のbufferメンバに格納されます。 WinHttpReadDataの第3引数に指定されたサイズだけデータの読み込みが完了したら、 WINHTTP_CALLBACK_STATUS_READ_COMPLETEが送られることになります。 実際に読み込まれたサイズはdwStatusInformationLengthに格納されており、 このサイズ分だけメモリを確保します。 そして、バッファの中身をメモリにコピーし、メモリをIStreamとして識別するようにします。 このような手順が必要なのは、XMLデータを解析するがXmlLiteがデータをIStreamとして要求するからです。 GetXmlDataの内部は、次のようになっています。

void CSuggestWindow::GetXmlData(IStream *pStream)
{
	IXmlReader  *pReader;
	XmlNodeType nodeType;
	LPCWSTR     lpszValue;
	LPWSTR      lpsz;
	int         i = 0;

	EnterCriticalSection(&m_cs);

	CreateXmlReader(IID_PPV_ARGS(&pReader), NULL);
	
	pReader->SetInput(pStream);
	
	FreeSuggestData();
		
	m_hdpaSuggest = DPA_Create(1);

	while (pReader->Read(&nodeType) == S_OK) {
		if (nodeType == XmlNodeType_Text) {
			pReader->GetValue(&lpszValue, NULL);
			lpsz = (LPWSTR)HeapAlloc(GetProcessHeap(), 0, (lstrlenW(lpszValue) + 1) * sizeof(WCHAR));
			lstrcpyW(lpsz, lpszValue);
			DPA_InsertPtr(m_hdpaSuggest, i, (void *)lpsz);

			if (++i == 10)
				break;
		}
	}
	
	pReader->Release();
	
	LeaveCriticalSection(&m_cs);
}

EnterCriticalSectionを呼び出しているのは、LeaveCriticalSectionまでのコードをスレッドが同時に実行するのを防ぐためです。 WinHTTPで非同期通信する場合はWinHTTPの内部でスレッドが作成されることになっており、 先に示したコールバック関数はこのスレッドによって呼び出されることになります。 サーバーへの要求が連続して行われる場合はスレッドが同時に実行されることも考えられ、 そのようなときにグローバルな情報へのアクセスが同時に発生してしまっては、データに矛盾が生じる可能性があります。 この関数にとってのグローバルな情報というのはm_hdpaSuggestのことであり、 この変数には検索候補の一連のデータを格納します。 XMLデータを解析するためには、CreateXmlReaderでIXmlReaderを取得します。 SetInputを呼び出せば第1引数のストリームを渡すことができるため、 これでXmlLiteは解析すべきデータを理解できたことになります。 Readは現在のノードからデータを取得すると共に、そのノードのタイプを返すようになっており、 これがXmlNodeType_Textである場合は、検索候補のデータを取得したと考えて問題ありません。 よって、GetValueでそのデータを取得し、これをDPAに追加します。

XMLのデータを処理し終えたスレッドは、エディットコントロールに対してWM_SHOWSUGGESTをポストします。 これを取得したCOpenSearch::SubclassProcは、CSuggestWindow::Showを呼び出して検索候補のウインドウを実際に表示します。 このような手順を用いている理由は、UIに関する操作をできるだけメインスレッドで行いたいからです。 Showの内部は次のようになっています。

void CSuggestWindow::Show()
{
	RECT  rc;
	POINT pt;
	int   nHeight;

	GetClientRect(m_hwndEdit, &rc);

	pt.x = 0;
	pt.y = rc.bottom - rc.top;
	ClientToScreen(m_hwndEdit, &pt);
	ScreenToClient(g_pWebBrowserContainer->GetWindow(), &pt);

	SetRect(&m_rcSuggest, 0, 0, rc.right - rc.left, m_nSuggestInterval);
	nHeight = m_nSuggestInterval * DPA_GetPtrCount(m_hdpaSuggest);
	SetWindowPos(m_hwndSuggest, HWND_TOP, pt.x, pt.y, rc.right - rc.left, nHeight, SWP_SHOWWINDOW);

	InvalidateRect(m_hwndSuggest, NULL, TRUE);
	UpdateWindow(m_hwndSuggest);
}

検索候補のウインドウはエディットコントロールの直下に表示したいため、 まずはエディットコントロールの左下隅の座標をスクリーン座標に変換し、 これをメインウインドウのクライアント座標に変換します。 m_rcSuggestは、検索候補に表示される1つの文字列の大きさを格納します。 幅についてはエディットコントロールの幅をベースにし、高さについてはm_nSuggestIntervalという固定の値を指定します。 nHeightはウインドウの幅であり、検索候補の文字列の数によって決定します。 SetWindowPosでは、Zオーダーと位置とサイズの変更に加えて、ウインドウの表示も行います。 InvalidateRectとUpdateWindowによって、クライアント領域が直ちに更新されます。

検索候補のウインドウプロシージャにて、文字列の選択に関する処理は次のようになっています。 は、次のように実装されています。

case WM_MOUSEMOVE: {
	int   i, n;
	POINT pt;
	RECT  rc;
	
	if (m_hdpaSuggest == NULL)
		return 0;

	pt.x = LOWORD(lParam);
	pt.y = HIWORD(lParam);		

	n = DPA_GetPtrCount(m_hdpaSuggest);

	rc = m_rcSuggest;

	for (i = 0; i < n; i++) {
		if (PtInRect(&rc, pt)) {
			if (m_nSelectIndex != i) {
				InvalidateRect(hwnd, NULL, TRUE);
				m_nSelectIndex = i;
				break;
			}
		}
		OffsetRect(&rc, 0, m_nSuggestInterval);
	}

	return 0;
}

case WM_LBUTTONDOWN: {
	int   i, n;
	POINT pt;
	RECT  rc;
	
	if (m_hdpaSuggest == NULL)
		return 0;
	
	pt.x = LOWORD(lParam);
	pt.y = HIWORD(lParam);
	
	n = DPA_GetPtrCount(m_hdpaSuggest);

	rc = m_rcSuggest;

	for (i = 0; i < n; i++) {
		if (PtInRect(&rc, pt)) {
			LPWSTR lpsz = (LPWSTR)DPA_GetPtr(m_hdpaSuggest, i);
			m_pOpenSearch->SearchStart(lpsz);
			lstrcpyW(m_szSearchPrev, lpsz);
			SetWindowTextW(m_hwndEdit, lpsz);
			SetFocus(g_pWebBrowserContainer->GetWindow());
			break;
		}
		OffsetRect(&rc, 0, m_nSuggestInterval);
	}

	return 0;
}

WM_MOUSEMOVEでは、現在のカーソル位置に対応する文字列を選択状態とみなし、 そのインデックスをm_nSelectIndexに格納します。 InvalidateRectによってWM_PAINTが生成され、m_nSelectIndexの文字列がハイライトで描画されることになります。 WM_LBUTTONDOWNでは、押下された文字列をキーワードとして実際に検索を行います。 ClearCompositionStringを呼び出しているのは、IMEがオンの状態で変換文字列が入力された場合を考慮するためです。 この場合にマウスで検索を行うと、文字列が確定ないためにIMEのウインドウが表示されてしまうめ、 ClearCompositionStringで未確定状態を解除しています。 SetFocusでメインウインドウにフォーカスを割り当てているのは、 それによってエディットコントロールがフォーカスを失うからです。 このとき、検索候補のウインドウが非表示になる処理が行われます。

文字列の選択は、エディットコントロールで上下のキーが押された場合にも変化します。

void CSuggestWindow::TrackUpDownKey(WPARAM wParam, LPARAM lParam)
{
	LPWSTR lpsz;

	if (m_hdpaSuggest == NULL)
		return;

	if (wParam == VK_UP) {
		if (--m_nSelectIndex < 0)
			m_nSelectIndex = 0;
	}
	else {
		int n = DPA_GetPtrCount(m_hdpaSuggest) - 1;
		if (++m_nSelectIndex > n)
			m_nSelectIndex = n;
	}

	lpsz = (LPWSTR)DPA_GetPtr(m_hdpaSuggest, m_nSelectIndex);
	lstrcpyW(m_szSearchPrev, lpsz);
	SetWindowTextW(m_hwndEdit, lpsz);

	InvalidateRect(m_hwndSuggest, NULL, TRUE);
	UpdateWindow(m_hwndSuggest);
}

上のキーが押された場合はm_nSelectIndexを減算し、下のキーが押された場合はm_nSelectIndexを増加します。 選択状態になった文字列はエディットコントロール上に表示されます。

必須であるとはいえませんが、CSuggestWindowにはテーマに関する処理が含まれています。 こうした処理が存在する理由は、次のウインドウを見比べてみると分かりやすくなります。

テーマが有効になっている環境においては、ウインドウの枠やフォントが変更されることになっています。 こうした環境においては、左のウインドウのようにクラシックな文字列を表示するのではなく、 右のウインドウのように現在のテーマのフォントを使用した文字列のほうが統一感があるため、 そのような処理を行っています。

現在のテーマのフォントを使用するべきなのは、エディットコントロールとCSuggestWindowのウインドウです。 これらがフォントを使用できるようになるための準備は、ChangeThemeInfoで行われています。

void CSuggestWindow::ChangeThemeInfo()
{
	if (m_hTheme != NULL) {
		CloseThemeData(m_hTheme);
		m_hTheme = NULL;
	}

	if (m_hfontEditPrev != NULL)
		m_hfontEditPrev = (HFONT)SendMessage(m_hwndEdit, WM_GETFONT, 0, 0);
	
	if (IsThemeActive()) {
		LOGFONT lf;
		HFONT   hfont;

		m_hTheme = OpenThemeData(NULL, VSCLASS_EXPLORERBAR);
		GetThemeFont(m_hTheme, NULL, EBP_HEADERBACKGROUND, EBHC_NORMAL, TMT_FONT, &lf);
		hfont = CreateFontIndirect(&lf);
		SendMessage(m_hwndEdit, WM_SETFONT, (WPARAM)hfont, TRUE);
	}
	else
		SendMessage(m_hwndEdit, WM_SETFONT, (WPARAM)m_hfontEditPrev, TRUE);
}

IsThemeActiveが成功した場合は、現在テーマが有効になっていることを意味し、 OpenThemeDataでテーマのハンドルを使用できます。 第2引数はコントロールを識別する文字列を指定するのが一般的ですが、 今回のようにフォントを目的としている場合はVSCLASS_EXPLORERBARで構いません。 GetThemeFontを呼び出せばフォントの情報を格納したLOGFONT構造体を取得できるため、 これをCreateFontIndirectに指定すればフォントを作成できます。 そして後は、エディットコントロールにWM_SETFONTを送れば、エディットコントロールのフォントは変更されたことになります。 ChangeThemeInfoはテーマが変更された場合(WM_THEMECHANGEDが送られた場合)にも呼ばれるため、 テーマが有効でなくなったのであれば、デフォルトのフォントに戻す必要があります。 このためのフォントは、m_hfontEditPrevとして保存しています。

WM_PAINTでは検索候補の文字列を描画しますが、 テーマのハンドルがあるかで描画の方法を変更しています。

case WM_PAINT: {
	int         i, n;
	HDC         hdc;
	PAINTSTRUCT ps;
	RECT        rc;
	LPWSTR      lpsz;
	HBRUSH      hbr;
	DWORD       dwFormat = DT_SINGLELINE | DT_LEFT | DT_VCENTER;
	
	if (m_hdpaSuggest == NULL)
		return 0;

	hdc = BeginPaint(hwnd, &ps);
	
	n = DPA_GetPtrCount(m_hdpaSuggest);
	
	rc = m_rcSuggest;

	for (i = 0; i < n; i++) {
		lpsz = (LPWSTR)DPA_GetPtr(m_hdpaSuggest, i);

		if (i == m_nSelectIndex) {
			if (m_hTheme == NULL) {
				COLORREF crPrev; 

				hbr = GetSysColorBrush(COLOR_HIGHLIGHT);
				FillRect(hdc, &rc, hbr);

				crPrev = SetTextColor(hdc, GetSysColor(COLOR_HIGHLIGHTTEXT));
				SetBkMode(hdc, TRANSPARENT);
				DrawTextW(hdc, lpsz, -1, &rc, dwFormat);
				SetTextColor(hdc, crPrev);
			}
			else{
				DTTOPTS dttOpts;
				
				hbr = CreateSolidBrush(GetThemeSysColor(m_hTheme, COLOR_HIGHLIGHT));
				FillRect(hdc, &rc, hbr);
				
				ZeroMemory(&dttOpts, sizeof(DTTOPTS));
				dttOpts.dwSize  = sizeof(DTTOPTS);
				dttOpts.dwFlags = DTT_TEXTCOLOR;
				dttOpts.crText  = GetThemeSysColor(m_hTheme, COLOR_HIGHLIGHTTEXT);
				DrawThemeTextEx(m_hTheme, hdc, EBP_HEADERBACKGROUND, EBHC_NORMAL, lpsz, lstrlenW(lpsz), dwFormat, &rc, &dttOpts);

				DeleteObject(hbr);
			}
		}
		else {
			if (m_hTheme == NULL)
				DrawTextW(hdc, lpsz, -1, &rc, dwFormat);
			else
				DrawThemeTextEx(m_hTheme, hdc, EBP_HEADERBACKGROUND, EBHC_NORMAL, lpsz, lstrlenW(lpsz), dwFormat, &rc, NULL);
		}
		OffsetRect(&rc, 0, m_nSuggestInterval);
	}

	EndPaint(hwnd, &ps);

	return 0;
}

iがm_nSelectIndexと一致する場合はテキストをハイライトで描画し、 そうでない場合は普通に描画します。 テーマのハンドルがない場合はDrawTextで描画しますが、 テーマのハンドルがある場合はDrawThemeTextExで描画します。 また、テキストの色についても前者はGetSysColorで取得しますが、 後者はGetThemeSysColorで取得します。 最後にOffsetRectを呼び出しているのは、次の文字列を描画すべき場所を取得するためです。


戻る