2004-04-06 [長年日記]

カスタムコントロールでビジュアルスタイル対応のボーダーを引く

Windows XPでVisualStyleが有効な場合、コモンコントロールに含まれているコントロールは、WS_EX_CLIENTEDGEを指定するとVisualStyle対応のボーダーを引いてくれます(デフォルトのテーマだと青い線)。ところがカスタムコントロールの場合には、ボーダーの部分はクライアントエリアではないにも関わらず、OS側で面倒をみてくれません。このため、昔ながらの凹んだエッジが描かれます。

これをVisualStyleっぽくするためには自前で描く必要があるのですが、結構厄介で色々はまったのでメモです。

まず、ボーダー自体はDrawThemeBackgroundで描きます。DrawThemeEdgeというそれっぽい関数は、ボーダーではなくてエッジを描くため、普通の凹んだエッジが描かれてしまいます。

そもそもボーダーとエッジの明確な定義が良くわからないので、少し実験してみました。実験の対象はTreeViewコントロールです。スタイルを組み合わせていくつか作ってみると、

なし
なにもなし(非クライアントエリアの幅は0ドット)
WS_BORDER
黒い枠(非クライアントエリアの幅は1ドット)
WS_EX_CLIENTEDGE
青い枠(非クライアントエリアの幅は2ドット)
WS_BORDER + WS_EX_CLIENTEDGE
青い枠 + 凹み(非クライアントエリアの幅は3ドット)

という結果になりました。これらを元に調べていくと、

  • WS_EX_CLIENTEDGEが指定されているとVisualStyleな枠が描かれる
  • VisualStyleな枠はデフォルトの処理が行われた後で上書きしている
  • VisualStyleな枠は外側からエッジの幅分だけ上書きする

らしいということが分かります。WS_EX_CLIENTEDGEだけを指定された場合、青い枠の幅は1ドットでエッジは2ドットあるため、内側の1ドットは背景色で塗りつぶされます。WS_BORDER + WS_EX_CLIENTEDGEの場合、元々外側1ドットに黒い枠が、その内側2ドット分に凹みが描画されているのですが、外側2ドット分がVisualStyleによって上書きされるため、実際には外側から、青い枠、背景色、凹みの残骸の順番で1ドットずつ描かれているということになっています。

テーマの設定でOSのエッジの幅(デフォルトでは2ドット)よりも幅の広いボーダーが指定されるとどうなるんだろうという疑問がわきますが、おそらくクリップされるんでしょう(試してません)。

というわけで、この動作をまねるためにはこんな感じにすれば良いのではないかと思われます。

case WM_NCPAINT:
  // まずは普通にOSに描かせる
  ::DefWindowProc(hwnd, uMsg, wParam, lParam);

  if (::GetWindowLong(hwnd, GWL_EXSTYLE) & WS_EX_CLIENTEDGE && ::IsThemeActive(hTheme)) {
    // ウィンドウのサイズを取得して左上を0に補正
    RECT rect;
    ::GetWindowRect(hwnd, &rect);
    rect.right -= rect.left;
    rect.left = 0;
    rect.bottom -= rect.top;
    rect.top = 0;

    HDC hdc = ::GetWindowDC(hwnd);

    // OSのエッジの幅だけウィンドウの色で塗りつぶす
    int nEdgeWidth = ::GetSystemMetrics(SM_CXEDGE);
    int nEdgeHeight = ::GetSystemMetrics(SM_CYEDGE);
    ::ExcludeClipRect(hdc, nEdgeWidth, nEdgeHeight,
      rect.right - nEdgeWidth, rect.bottom - nEdgeHeight);
    ::SetBkColor(hdc, ::GetSysColor(COLOR_WINDOW));
    ::ExtTextOut(hdc, rect.left, rect.top,
      ETO_CLIPPED | ETO_OPAQUE, &rect, _T(""), 0, 0);

    // テーマで設定されたボーダーの幅でクリップして背景を描く
    int nBorderWidth = ::GetThemeSysSize(hTheme, SM_CXBORDER);
    int nBorderHeight = ::GetThemeSysSize(hTheme, SM_CYBORDER);
    ::ExcludeClipRect(hdc, nBorderWidth, nBorderHeight,
      rect.right - nBorderWidth, rect.bottom - nBorderHeight);
    ::DrawThemeBackground(hdc, EP_EDITTEXT, 0, &rect, 0);

    ::ReleaseDC(hwnd, hdc);
  }

素のAPIだけで書くように書き換えたので微妙に間違っているかもしれません。エラー処理はご随意に。

ちなみにここでは、EP_EDITTEXTを指定していますが、おおよそ似ているコントロールを使えば良いと思われます。GP_BORDERというばっちりな定義がヘルプに書かれていますが、tmschema.hに含まれていませんので使えません(ベータの時にはあった?)。

それから、テーマ系のAPIのヘルプは重要なところが抜けているのでuxtheme.hのコメントを見たほうが有益です。


トップ «前の日記(2004-04-05) 最新 次の日記(2004-04-07)»