SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

軽量で高速なハイパーリンクコントロールを実現するC++クラス

Win32 APIとMFCの両方で使えるハイパーリンクコントロールの自作

私が作成したクラスの機能

 まずは、静的コントロールをハイパーリンクにするために必要な変更のうち、すでにNealが解決していたものを紹介しましょう。

  1. ハイパーリンクテキストがクリックされたら、そのテキストで指定された場所を参照するブラウザウィンドウを開く。
  2. カーソルがハイパーリンクの上にきたら、標準の矢印カーソルを指差しカーソルに変更する。
  3. カーソルがハイパーリンクの上にきたら、そのテキストをアンダーライン表示にする。
  4. ハイパーリンクコントロールのテキストを黒以外の色で表示する。

 以下は、私が追加した機能です。

  1. リンク先が訪問済みであるハイパーリンクコントロールの表示色を変える。
  2. キーボードからハイパーリンクコントロールにアクセスできるようにする。
  3. フォーカスまたはカーソルがハイパーリンクコントロール上にあるときにプログラムによって何らかの動作を行えるように、ある種のフックを導入する。

 これらの新機能をどのように実装したかを説明する前に、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);
};

 この変更を行った理由は次のとおりです。

  1. ハイパーリンクを選択または選択解除したときの動作をカスタマイズできるようにするため。このクラスから新しいクラスを派生させれば、動作を自由にカスタマイズできます。
  2. ウィンドウプロシージャにおける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;
}

 キーボード操作をサポートするには、次のメッセージを処理しなければなりません。

  1. WM_KEYUP
  2. WM_SETFOCUS
  3. WM_KILLFOCUS

 このハイパーリンクコントロールは、スペースキーの押下に反応します。WM_SETFOCUSWM_KILLFOCUSによって、フォーカスを示す矩形が描かれます。この矩形は親ウィンドウに対して描かれます。この理由は、第一に、そうしないと矩形の各辺がハイパーリンクテキストに近くなりすぎて文字が読みにくくなるからです。第二の理由は、このサンプルではWM_CTLCOLOR_STATICハンドラから透明ブラシを返すことでハイパーリンクコントロールを透明にしているからです。親ウィンドウがハイパーリンクコントロールの背景を消すという処理にすると、フォーカス矩形の扱いが面倒になります。親ウィンドウに対してフォーカス矩形を描くことにより、こうしたささいな問題を解決することができます。

 ここでもう1つ説明しておきたいのは、公開されているハイパーリンクコントロールの多くはWM_LBUTTONDOWNを使っているのに、なぜWM_KEYUPWM_LBUTTONUPを使うことにしたのか、という点です。この理由は簡単です。Internet Explorer(IE)のハイパーリンクや従来のWindowsコントロールと動作を一貫させるためです。実は私もそうだったのですが、きっとほとんどの皆さんは、このささいな点には注意を払わなかったと思います。それではIEで試してみましょう。ハイパーリンクをクリックし、ボタンを押し下げたままにしてください。マウスボタンを放すまではリンク先にジャンプしません。ダイアログ内のプッシュボタンも同様です。また、プッシュボタンにフォーカスを合わせてスペースキーを押しても、キーを押し下げているかぎりは何の動作も起こりません。ところで、私はキーボード入力のサポート方法の調査中に、MSDNでPaul DiLasciaのすばらしい記事を読みました。彼は、WM_GETDLGCODEメッセージハンドラとWM_CHARメッセージハンドラを組み合わせて使用し、WM_GETDLGCODEからDLGC_WANTCHARSを返すことで、コントロールがWM_CHARメッセージの受け取りを求めていることをダイアログボックスマネージャに知らせていました。しかし、私はこのやり方には賛成しません。理由は次のとおりです。

  1. 簡潔性:私の方法では1つのハンドラ(WM_KEYUP)で済むのに対し、2つのハンドラ(WM_GETDLGCODEWM_CHAR)を必要とします。
  2. 正確性:従来のコントロールのような、スペースキーを放したときにハイパーリンクがアクティブになるという動作を実現できません。また、WM_CHARを使用した場合、キーが押されたままだとコントロールが何度もメッセージを受け取るという問題があります。
  3. 最後に、これらの主張を裏付けるように、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)

次のページ
デモプログラム

この記事は参考になりましたか?

japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

lano1106(lano1106)

電気通信業界にて組み込みプロトコルスタックなどに携わる。米連邦航空局(FAA)の次世代の洋上航空交通管理(ATM)システム開発では(冗談のようだが)ATMプロトコルを担当。UNIXという歴史の深いテクノロジーを用いる環境に倦怠感を覚え、現在はフリーランスの開発者としてWindowsを対象としたC/C++開発に従事。お問い合わせは本人のWebサイトまで。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/341 2006/04/11 19:14

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー