MFCのステータスバー上CComboBoxがダークモードにならない原因と対処法|WM_CTLCOLORLISTBOXとOnCtlColorで解決

MFCでCComboBoxをステータスバーに重ねて配置したとき、コンボ本体は暗くなるのにドロップダウンだけ白いままになることがあります。ポイントはWM_CTLCOLOR*が「親ウィンドウ」に送られること。仕組みと確実な直し方をまとめます。

目次

現象:ステータスバー上のCComboBoxだけドロップダウンがダークモードにならない

状況をもう一度、開発目線で整理します。

  • CComboBox(ドロップダウンリスト)を2つ作り、ステータスバーのペイン上に重ねて表示している
  • コンボボックスはCreateで動的に作成し、親ウィンドウはステータスバーにしている(ステータスバー自体はオーナードロー)
  • ダイアログ上にリソース配置した通常のコンボボックスは、OnCtlColorが呼ばれダークモードの色が正しく反映される
  • しかしステータスバーに載せた方は、コンボ本体はそれっぽく暗くできても「ドロップダウン(リスト部分)だけ」期待通りに暗くならない
  • GetComboBoxInfohwndListを取り、SetWindowTheme(hwndList, L"DarkMode_Explorer", nullptr)等を試しても見た目が揃わない

この手の「本体は良いのにドロップダウンだけ白い」問題は、ダークモードAPIそのものよりもメッセージの到達先が変わっていることが原因になりやすいです。

結論:WM_CTLCOLOR*は“コントロールの親ウィンドウ”に送られる

Windowsの古典的な色付けは、テーマ名を設定するより先にWM_CTLCOLOR*WM_CTLCOLORLISTBOXなど)の流れで決まる場面が多くあります。そして重要なのは次の仕様です。

WM_CTLCOLOR*系メッセージは、そのコントロールの「親ウィンドウ(Parent)」に送られる

つまり、あなたの実装ではこうなっています。

  • ダイアログ上のコンボ:親=ダイアログ → ダイアログのOnCtlColorが呼ばれる → ダークモード用のブラシ・色設定が効く
  • ステータスバー上のコンボ:親=ステータスバー → ダイアログのOnCtlColorは呼ばれない → 既存のダークモード処理が適用されない

「ドロップダウンだけ」目立って破綻するのは、コンボボックスが内部的に複数の要素からできており、特にリスト部分が別ウィンドウ(LISTBOX)として描画され、別のWM_CTLCOLOR経路を通るためです。

CComboBoxは“複合コントロール”:どこが何で塗られるのか

コンボボックスの見た目は、ざっくり以下の部品で成立します。

部品実体(イメージ)関係しやすいWM_CTLCOLORダークモードで破綻しやすい点
本体(枠、ボタン含む)COMBOBOX本体(テーマ・既定描画の影響が大きい)テーマ適用で“それっぽく”見えるが、背景色が揃わないことがある
表示部(編集領域/選択表示)EDITまたはSTATIC相当WM_CTLCOLOREDIT / WM_CTLCOLORSTATIC親側で処理していないと文字色・背景色が既定に戻る
ドロップダウン(リスト)LISTBOXウィンドウWM_CTLCOLORLISTBOXここだけ白い、文字が黒いなどの症状が出やすい

ドロップダウン部分は、内部的にはリストボックスとして扱われるため、親が変わると色付けの責任者(WM_CTLCOLORを受け取る側)も変わります。ダイアログに置いたときに動いていた処理が、そのままでは届きません。

なぜSetWindowThemeだけでは揃わないのか

SetWindowTheme(hwnd, L"DarkMode_Explorer", nullptr)のような呼び出しは確かに有効な場面があります。ただし、ここでハマりやすいポイントがあります。

  • “テーマ(ビジュアルスタイル)”と“実際の塗りつぶし色”は別物
  • クラシック寄りのコントロールは、テーマを変えても背景ブラシはWM_CTLCOLORの戻り値(HBRUSH)に従うことがある
  • 結果として、テーマ名をDark系にしても、親側が既定ブラシ(白)を返していると白い背景で描かれる

さらに、GetComboBoxInfoで取れるhwndList(ドロップダウンのLISTBOX)は、タイミングによっては生成・再生成されることがあり、テーマ設定だけを一度行っても描画の最終決定がWM_CTLCOLOR側に寄ると“期待通りの黒”にはなりません。

チェック:今どのウィンドウが親になっているかを確認する

原因を確信に変えるために、次の2点を確認すると早いです。

親ウィンドウの確認

コンボボックスを作ったときの親が本当にステータスバーになっているか、そしてドロップダウンのLISTBOXがどこにぶら下がっているかを確認します。

COMBOBOXINFO cbi = { sizeof(COMBOBOXINFO) };
if (GetComboBoxInfo(m_combo.GetSafeHwnd(), &cbi))
{
    HWND hCombo = m_combo.GetSafeHwnd();
    HWND hList  = cbi.hwndList;

    HWND hParentCombo = GetParent(hCombo);
    HWND hParentList  = GetParent(hList);

    // ここでデバッガ表示やログ出力
}

期待する結論はシンプルで、ダイアログではなくステータスバー側が親(もしくはメッセージの受け先)になっていることです。

WM_CTLCOLORLISTBOXが誰に届いているか

Spy++等でメッセージを追うと、WM_CTLCOLORLISTBOXがダイアログではなくステータスバー(あるいはダイアログ以外)へ飛んでいることが確認できます。ここが確認できれば、解決方針は一択になります。

解決策:ステータスバー側でWM_CTLCOLOR*(OnCtlColor)を処理する

結論に沿って、実装も一直線です。“親ウィンドウ”で色を決める必要があるので、コンボボックスの親であるステータスバーがWM_CTLCOLOR*に反応し、ダークモード用のブラシとテキスト色を返すようにします。

対応方針は大きく2つあります。

方針対象メリット注意点
ステータスバーをサブクラス化してWM_CTLCOLOR*を処理Win32 / MFCどちらでも可能既存の親構造を変えずに解決ブラシの寿命管理、テーマ切替時の再描画が必要
そもそもコンボの親をダイアログ/フレームにして配置だけステータスバーに合わせるMFCで構造を変えられる場合既存のOnCtlColor資産をそのまま使えるクリップ、Zオーダー、レイアウト追従の設計が必要

この記事の本筋は前者です。質問の状況(親がステータスバー)を維持したまま直すのが最短です。

MFCでの実装例:CStatusBar派生クラスでOnCtlColorを拾う

MFCの場合、質問者の最終解と同じ方向性が最もきれいです。ステータスバーを継承したクラス(例:CDarkModeStatusBar)を作り、OnCtlColorをオーバーライドします。

ポイントは次の2つです。

  • まず基底クラスのOnCtlColorを呼んで「通常のMFC処理」を尊重する
  • その戻り値(HBRUSH)をダークモード共通処理に渡し、必要なら差し替える
HBRUSH CDarkModeStatusBar::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    // まず通常の MFC 処理
    HBRUSH hbr = __super::OnCtlColor(pDC, pWnd, nCtlColor);

    // ダークモード用の色・ブラシに差し替え
    return CDarkModeBase::OnCtlColor(hbr, pDC, pWnd, nCtlColor);
}

ここでの“効きどころ”は、ステータスバーを親に持つ以下の描画が、ステータスバーのOnCtlColor経由で統一される点です。

  • ステータスバー上のCComboBox本体(EDIT/STATIC相当も含む)
  • ドロップダウンのLISTBOX(WM_CTLCOLORLISTBOX

メッセージマップも忘れずに

派生クラス側でON_WM_CTLCOLOR()を受けられるようにします。

BEGIN_MESSAGE_MAP(CDarkModeStatusBar, CStatusBar)
    ON_WM_CTLCOLOR()
END_MESSAGE_MAP()

共通処理(CDarkModeBase)の考え方

実務で安定する形は、「nCtlColorに応じて色を決め、ブラシはメンバーとして保持する」方式です。毎回CreateSolidBrushするとGDIリークやちらつきの温床になります。

HBRUSH CDarkModeBase::OnCtlColor(HBRUSH hbrDefault, CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    if (!IsDarkModeEnabled())
        return hbrDefault;

    switch (nCtlColor)
    {
    case CTLCOLOR_LISTBOX:
    case CTLCOLOR_EDIT:
    case CTLCOLOR_STATIC:
        pDC->SetTextColor(RGB(230, 230, 230));
        pDC->SetBkColor(RGB(32, 32, 32));
        // m_brDarkBack はメンバー CBrush として生成済みを返す想定
        return (HBRUSH)m_brDarkBack.GetSafeHandle();

    default:
        return hbrDefault;
    }
}

この形にしておくと、ダークモード対象のコントロールが増えても、条件分岐を共通部に集約でき、保守が圧倒的に楽になります。

Win32での実装例:ステータスバーをSetWindowSubclassしてWM_CTLCOLOR*を処理

MFCではなく純Win32の実装でも考え方は同じです。ステータスバー(親)をサブクラス化して、WM_CTLCOLORLISTBOX等を拾ってブラシを返します。

struct DarkBrushSet
{
    HBRUSH hbrBack = nullptr;
    COLORREF crBack = RGB(32, 32, 32);
    COLORREF crText = RGB(230, 230, 230);
};

LRESULT CALLBACK StatusBarSubclassProc(
    HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    auto* p = reinterpret_cast<DarkBrushSet*>(dwRefData);

    switch (uMsg)
    {
    case WM_CTLCOLORLISTBOX:
    case WM_CTLCOLOREDIT:
    case WM_CTLCOLORSTATIC:
    {
        HDC hdc = reinterpret_cast<HDC>(wParam);
        SetTextColor(hdc, p->crText);
        SetBkColor(hdc, p->crBack);

        // 背景ブラシを返す(寿命は保持しておく)
        return reinterpret_cast<LRESULT>(p->hbrBack);
    }

    case WM_NCDESTROY:
        // 後始末
        RemoveWindowSubclass(hWnd, StatusBarSubclassProc, uIdSubclass);
        break;
    }

    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

実装の現場では、DarkBrushSetを確保してhbrBack = CreateSolidBrush(...)してから、次のように関連付けます。

auto* p = new DarkBrushSet;
p->hbrBack = CreateSolidBrush(p->crBack);

SetWindowSubclass(hStatusBar, StatusBarSubclassProc, 1, reinterpret_cast<DWORD_PTR>(p));

そして破棄タイミング(アプリ終了やステータスバー破棄)でDeleteObjectdeleteを忘れないようにします。GDIオブジェクトの寿命管理は、ダークモード周りで最も事故りやすいポイントです。

追加の実務ポイント:ドロップダウンのhwndListはタイミングで変わる

GetComboBoxInfoで取れるhwndListは便利ですが、環境やスタイルによっては「ドロップダウン表示のタイミングで生成される」「再生成される」ことがあります。見た目を完全に揃えたいなら、次の運用が堅いです。

  • コンボ作成直後に一度GetComboBoxInfoを試す
  • イベント(例:CBN_DROPDOWN)で再度GetComboBoxInfoし、SetWindowThemeや追加のサブクラス処理を適用する

MFCなら、コンボの通知を拾って次のようにします。

void CMyDlg::OnCbnDropdownMyCombo()
{
    COMBOBOXINFO cbi = { sizeof(COMBOBOXINFO) };
    if (GetComboBoxInfo(m_combo.GetSafeHwnd(), &cbi) && cbi.hwndList)
    {
        SetWindowTheme(cbi.hwndList, L"DarkMode_Explorer", nullptr);
    }
}

ただし本題はここではありません。テーマ設定をしても、最終的に背景ブラシを返す親側が既定色のままだと、見た目は揃いません。「親がWM_CTLCOLORを処理しているか」が最優先です。

オーナードローとの関係:ステータスバーがオーナードローでも別問題として扱う

今回の状況ではステータスバーがオーナードローとのことですが、ここで混同しがちな点を切り分けておきます。

  • ステータスバーのオーナードローは、基本的にステータスバー自身のペイン描画(文字や背景)を自分で描く話
  • ステータスバー上に載せたコンボボックスは「子ウィンドウ」なので、別の描画パイプラインを持つ
  • 子ウィンドウの背景色の決定で重要になるのがWM_CTLCOLOR*(=親に聞きに来る)

つまり、ステータスバーがオーナードローだからといって、子コントロールが自動でダークに揃うわけではありません。むしろ親として責任が増えると捉えるのが実務的です。

代替案:親をダイアログ/フレームに戻し、位置だけステータスバーに合わせる

設計を変えられるなら、「そもそも親をステータスバーにしない」というのも有効です。たとえば親はメインフレーム(またはダイアログ)にしておき、見た目の位置だけステータスバー上に合わせます。

この方法の良いところは、既にダイアログ(またはフレーム)側にあるOnCtlColor資産をそのまま使える点です。

項目親=ステータスバー親=ダイアログ/フレーム(位置だけ合わせる)
OnCtlColorの到達ステータスバー側に実装が必要既存のOnCtlColorがそのまま効きやすい
レイアウト追従ステータスバー基準で比較的シンプルリサイズ時に位置計算が必要
クリッピング/Z順比較的素直状況によってはチラつきや隠れが出るため調整が必要

ただし質問の前提(親=ステータスバー)を崩さず直すなら、やはりステータスバー側でWM_CTLCOLORを処理するのが王道です。

よくあるハマりどころ集:直ったはずなのに色が戻る、文字だけ黒い

最後に、同じ症状で再発しやすい落とし穴をまとめます。

ブラシを関数内で作って返している

OnCtlColor内で毎回CreateSolidBrushして返すと、GDIリークや不安定な描画につながります。ブラシはメンバーとして保持し、使い回してください。

CTLCOLOR_LISTBOXだけ処理していて、EDIT/STATIC側が抜けている

コンボの種類(CBS_DROPDOWNCBS_DROPDOWNLISTか)によって、表示部がEDIT相当だったりSTATIC相当だったりします。結果として、ドロップダウンは暗いのに表示部だけ色が合わない…という中途半端な状態になります。CTLCOLOR_EDITCTLCOLOR_STATICも併せて見ておくと安定します。

ダークモード切替時の再描画・再生成に追従していない

Windowsの設定変更やテーマ変更で再描画が走ると、ブラシやテーマが初期状態に戻ったように見えることがあります。アプリ側で「ダークモード状態が変わったらブラシを作り直す」「必要なら子コントロールをInvalidateする」などの運用を入れておくと、現場での事故が減ります。

コンボがオーナードローの場合は別途WM_DRAWITEMが必要

もしコンボボックスにCBS_OWNERDRAWFIXED等を付けている場合、項目自体はWM_DRAWITEMで描かれるため、CTLCOLORだけでは文字色が揃わないことがあります。今回の主題は「親が違うのでOnCtlColorが呼ばれない」ですが、オーナードローを使っている場合は併せて描画処理もダーク対応にしてください。

現場向けチェックリスト:同じ症状を最短で潰す

チェック項目確認方法OKの状態
コンボの親は誰かCreate時の親、GetParentで確認想定通り(今回ならステータスバー)
WM_CTLCOLORLISTBOXが誰に届くかSpy++等親(ステータスバー)が受け、ダーク用HBRUSHを返している
CTLCOLOR_EDIT/STATICも必要かコンボのスタイル確認表示部の背景・文字が統一されている
ブラシの寿命管理コードレビュー、GDIリーク監視メンバー保持で使い回し、破棄も実装済み
ドロップダウン再生成への追従CBN_DROPDOWN等で確認必要ならその都度theme設定・再サブクラス化ができている

まとめ:動的に作ったコントロールが暗くならないときは「親で処理しているか」を疑う

ステータスバーやツールバーの上にコントロールを載せる実装は、UIをリッチにできる一方で、色・テーマの責任範囲が変わるため落とし穴が増えます。今回のように「ダイアログでは暗いのに、ステータスバー上だけ暗くならない」場合は、ダークモードAPIの不足を疑う前に、まずWM_CTLCOLOR*がどこに送られているかを確認してください。

親がステータスバーなら、ステータスバー側でOnCtlColor(またはWM_CTLCOLOR*)を処理する。それだけで、コンボ本体とドロップダウンを含む見た目の統一が一気に安定します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次