私が作成したクラスの機能
まずは、静的コントロールをハイパーリンクにするために必要な変更のうち、すでにNealが解決していたものを紹介しましょう。
- ハイパーリンクテキストがクリックされたら、そのテキストで指定された場所を参照するブラウザウィンドウを開く。
- カーソルがハイパーリンクの上にきたら、標準の矢印カーソルを指差しカーソルに変更する。
- カーソルがハイパーリンクの上にきたら、そのテキストをアンダーライン表示にする。
- ハイパーリンクコントロールのテキストを黒以外の色で表示する。
以下は、私が追加した機能です。
- リンク先が訪問済みであるハイパーリンクコントロールの表示色を変える。
- キーボードからハイパーリンクコントロールにアクセスできるようにする。
- フォーカスまたはカーソルがハイパーリンクコントロール上にあるときにプログラムによって何らかの動作を行えるように、ある種のフックを導入する。
これらの新機能をどのように実装したかを説明する前に、Nealのコードに加えた大きな構造的な変更について述べておきたいと思います。具体的には、NealのコードをCHyperLinkという1つのクラスにしました。このクラスの定義を次に示します。
class CHyperLink { public: CHyperLink(void); virtual ~CHyperLink(void); BOOL ConvertStaticToHyperlink(HWND hwndCtl, LPCTSTR strURL); BOOL ConvertStaticToHyperlink(HWND hwndParent, UINT uiCtlId, LPCTSTR strURL); BOOL setURL( LPCTSTR strURL); LPCTSTR getURL(void) const { return m_strURL; } protected: /* * Override if you want to perform some action when * the link has the focus or when the cursor is over * the link such as displaying the URL somewhere. */ virtual void OnSelect(void) {} virtual void OnDeselect(void) {} LPTSTR m_strURL; // hyperlink URL private: // Hyperlink colors static COLORREF g_crLinkColor, g_crVisitedColor; static HCURSOR g_hLinkCursor; // Cursor for hyperlink static HFONT g_UnderlineFont; // Font for underline display static int g_counter; // Global resources user // counter BOOL m_bOverControl; // cursor over control? BOOL m_bVisited; // Has it been visited? HFONT m_StdFont; // Standard font WNDPROC m_pfnOrigCtlProc; void createUnderlineFont(void); static void createLinkCursor(void); void createGlobalResources(void) { createUnderlineFont(); createLinkCursor(); } static void destroyGlobalResources(void) { /* * No need to call DestroyCursor() for cursors * acquired through LoadCursor(). */ g_hLinkCursor = NULL; DeleteObject(g_UnderlineFont); g_UnderlineFont = NULL; } void Navigate(void); static void DrawFocusRect(HWND hwnd); static LRESULT CALLBACK _HyperlinkParentProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK _HyperlinkProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); };
この変更を行った理由は次のとおりです。
- ハイパーリンクを選択または選択解除したときの動作をカスタマイズできるようにするため。このクラスから新しいクラスを派生させれば、動作を自由にカスタマイズできます。
- ウィンドウプロシージャにおける
GetProp()の呼び出し回数を減らすため。コードを1つのクラスにまとめたことにより、GetProp()の呼び出しを1回行うだけで、必要な変数をすべて含んだオブジェクトのポインタを取得できます。
理由1に挙げたカスタマイズを行うには、OnSelect()およびOnDeselect()関数をオーバーライドします。後ほど、デモアプリケーションを紹介するときに実例を交えて説明します。
このクラスでは、別の点でも改善が行われています。メンバの一部がstatic宣言されていることに気づいたでしょうか。これによって、複数のハイパーリンクコントロールで同じリソース(ハンドカーソルやアンダーライン付きフォントなど)を共有することができます。ConvertStaticToHyperlink()関数には次のブロックを追加してあります。
if( g_counter++ == 0 )
{
createGlobalResources();
}
また、コントロールウィンドウプロシージャ内のWM_DESTROYメッセージハンドラには次のコードを追加してあります。
if( --CHyperLink::g_counter <= 0 )
{
destroyGlobalResources();
}
最初のConvertStaticToHyperlink()の呼び出し時にグローバルリソースの割り当てが行われ、最後のハイパーリンクの破棄時に共有リソースも一緒に解放されます。このアプローチの利点は、メモリの利用を効率化できることと、ハンドカーソルの読み込みが一度で済むことです。WM_SETCURSORの新しいコードは、次のようになります。
case WM_SETCURSOR: { SetCursor(CHyperLink::g_hLinkCursor); return TRUE; }
それでは、新しい機能の説明に戻りましょう。最も簡単なのは、訪問済みのハイパーリンクの表示色を変更するという機能です。これはWM_CTLCOLORSTATICハンドラをほんの少し変更するだけで実現できます。必要なのは、リンクが訪問済みかどうかを表すブール型変数をチェックすることだけです(この変数はリンク訪問時にtrueに設定されます)。関連部分のコードを以下に示します。
inline void CHyperLink::Navigate(void) { SHELLEXECUTEINFO sei; ::ZeroMemory(&sei,sizeof(SHELLEXECUTEINFO)); sei.cbSize = sizeof( SHELLEXECUTEINFO ); // Set Size sei.lpVerb = TEXT( "open" ); // Set Verb sei.lpFile = m_strURL; // Set Target // To Open sei.nShow = SW_SHOWNORMAL; // Show Normal WINXDISPLAY(ShellExecuteEx(&sei)); m_bVisited = TRUE; } case WM_CTLCOLORSTATIC: { HDC hdc = (HDC) wParam; HWND hwndCtl = (HWND) lParam; CHyperLink *pHyperLink = (CHyperLink *)GetProp(hwndCtl, PROP_OBJECT_PTR); if(pHyperLink) { LRESULT lr = CallWindowProc(pfnOrigProc, hwnd, message, wParam, lParam); if (!pHyperLink->m_bVisited) { // This is the most common case for static // branch prediction optimization SetTextColor(hdc, CHyperLink::g_crLinkColor); } else { SetTextColor(hdc, CHyperLink::g_crVisitedColor); } return lr; } break; }
キーボード操作をサポートするには、次のメッセージを処理しなければなりません。
WM_KEYUPWM_SETFOCUSWM_KILLFOCUS
このハイパーリンクコントロールは、スペースキーの押下に反応します。WM_SETFOCUSとWM_KILLFOCUSによって、フォーカスを示す矩形が描かれます。この矩形は親ウィンドウに対して描かれます。この理由は、第一に、そうしないと矩形の各辺がハイパーリンクテキストに近くなりすぎて文字が読みにくくなるからです。第二の理由は、このサンプルではWM_CTLCOLOR_STATICハンドラから透明ブラシを返すことでハイパーリンクコントロールを透明にしているからです。親ウィンドウがハイパーリンクコントロールの背景を消すという処理にすると、フォーカス矩形の扱いが面倒になります。親ウィンドウに対してフォーカス矩形を描くことにより、こうしたささいな問題を解決することができます。
ここでもう1つ説明しておきたいのは、公開されているハイパーリンクコントロールの多くはWM_LBUTTONDOWNを使っているのに、なぜWM_KEYUPとWM_LBUTTONUPを使うことにしたのか、という点です。この理由は簡単です。Internet Explorer(IE)のハイパーリンクや従来のWindowsコントロールと動作を一貫させるためです。実は私もそうだったのですが、きっとほとんどの皆さんは、このささいな点には注意を払わなかったと思います。それではIEで試してみましょう。ハイパーリンクをクリックし、ボタンを押し下げたままにしてください。マウスボタンを放すまではリンク先にジャンプしません。ダイアログ内のプッシュボタンも同様です。また、プッシュボタンにフォーカスを合わせてスペースキーを押しても、キーを押し下げているかぎりは何の動作も起こりません。ところで、私はキーボード入力のサポート方法の調査中に、MSDNでPaul DiLasciaのすばらしい記事を読みました。彼は、WM_GETDLGCODEメッセージハンドラとWM_CHARメッセージハンドラを組み合わせて使用し、WM_GETDLGCODEからDLGC_WANTCHARSを返すことで、コントロールがWM_CHARメッセージの受け取りを求めていることをダイアログボックスマネージャに知らせていました。しかし、私はこのやり方には賛成しません。理由は次のとおりです。
- 簡潔性:私の方法では1つのハンドラ(
WM_KEYUP)で済むのに対し、2つのハンドラ(WM_GETDLGCODEとWM_CHAR)を必要とします。 - 正確性:従来のコントロールのような、スペースキーを放したときにハイパーリンクがアクティブになるという動作を実現できません。また、
WM_CHARを使用した場合、キーが押されたままだとコントロールが何度もメッセージを受け取るという問題があります。 - 最後に、これらの主張を裏付けるように、Petzoldの著書『Programming Windows』では、コントロールのサブクラス化に
WM_KEYUPが使われています。
いずれにせよ、関連部分のコードは次のようになります。
inline void CHyperLink::DrawFocusRect(HWND hwnd) { HWND hwndParent = GetParent(hwnd); if( hwndParent ) { // calculate where to draw focus rectangle, in screen // coords RECT rc; GetWindowRect(hwnd, &rc); INFLATERECT(&rc,1,1); // add one pixel all around // convert to parent // window client coords ::ScreenToClient(hwndParent, (LPPOINT)&rc); ::ScreenToClient(hwndParent, ((LPPOINT)&rc)+1); HDC dcParent = GetDC(hwndParent); // parent window's DC ::DrawFocusRect(dcParent, &rc); // draw it! ReleaseDC(hwndParent,dcParent); } } case WM_KEYUP: { if( wParam != VK_SPACE ) { break; } } // Fall through case WM_LBUTTONUP: { pHyperLink->Navigate(); return 0; } case WM_SETFOCUS: // Fall through case WM_KILLFOCUS: { if( message == WM_SETFOCUS ) { pHyperLink->OnSelect(); } else // WM_KILLFOCUS { pHyperLink->OnDeselect(); } CHyperLink::DrawFocusRect(hwnd); return 0; }
Navigate()とDrawFocusRect()を両方ともインライン関数にしていることに気がついたでしょうか。これらの関数は、どちらもハイパーリンクのウィンドウプロシージャから呼び出されます。インライン関数になっているのは、可読性を最大限に高めると同時に、不必要な関数呼び出しを避けてウィンドウプロシージャを最適化するためです。
今度は、バグ修正に取り組みましょう。このコントロールでは、ReleaseCapture()を呼び出す以外の方法でもマウスキャプチャを解放できます。たとえば、リンクをクリックして、カーソルをそのリンクの上に置いたままにしておきます。Webブラウザのウィンドウがポップアップすると、マウスキャプチャは失われます。このとき、実際にはマウスキャプチャが失われているのに、コントロールのほうではそれを認識していないという矛盾した状態になってしまうのです。このバグを修正する秘訣は、コントロールが明示的に解放しなくてもマウスキャプチャが解放される場合があるという点を理解すること、そしてWM_CAPTURECHANGEDメッセージを適切に処理することです。コードは次のようになります。
case WM_MOUSEMOVE: { if ( pHyperLink->m_bOverControl ) { // This is the most common case for static branch // prediction optimization RECT rect; GetClientRect(hwnd,&rect); POINT pt = { LOWORD(lParam), HIWORD(lParam) }; if (!PTINRECT(&rect,pt)) { ReleaseCapture(); } } else { pHyperLink->m_bOverControl = TRUE; SendMessage(hwnd, WM_SETFONT, (WPARAM)CHyperLink::g_UnderlineFont, FALSE); InvalidateRect(hwnd, NULL, FALSE); pHyperLink->OnSelect(); SetCapture(hwnd); } return 0; } case WM_CAPTURECHANGED: { pHyperLink->m_bOverControl = FALSE; pHyperLink->OnDeselect(); SendMessage(hwnd, WM_SETFONT, (WPARAM)pHyperLink->m_StdFont, FALSE); InvalidateRect(hwnd, NULL, FALSE); return 0; }
ウィンドウプロシージャの話題を終えるにあたって、もう1つ大事な点に触れておきます。処理されたメッセージは、静的コントロールのプロシージャには戻されません(静的コントロールには必要ないからです)。基本的にはこれでうまく動作しますが、その静的コントロールがすでにサブクラス化されている場合は問題が起こり得るので注意が必要です。たとえば、静的コントロールが、マウスメッセージを処理する必要のあるツールチップコントロールによってサブクラス化されているとします。この場合、ツールチップコントロールは期待どおりには動作しません。ツールチップコントロールをCHyperLinkクラスと連携させて使う方法については、デモプログラムのところで説明します。
最後に、GetProp()の呼び出しを高速化するために、文字列ではなく「アトム」を使います。簡単なグローバルオブジェクトを使ってアトムの格納を行うことで、CHyperLinkが一度も使用されないうちにアトムの初期化が行われ、プログラムの実行期間の全体にわたってアトムが存在することになります。また、システム全体を通して一意な名前を作成するために、意味のある文字列にGUIDを追加しています。
/* * typedefs */ class CGlobalAtom { public: CGlobalAtom(void) { atom = GlobalAddAtom(TEXT("_Hyperlink_Object_Pointer_") TEXT("\\{AFEED740-CC6D-47c5-831D-9848FD916EEF}")); } ~CGlobalAtom(void) { DeleteAtom(atom); } ATOM atom; }; /* * Local variables */ static CGlobalAtom ga; #define PROP_OBJECT_PTR ((LPCTSTR)(DWORD)ga.atom) #define PROP_ORIGINAL_PROC ((LPCTSTR)(DWORD)ga.atom)
