MFC CComboBoxEx をダークモードにするとDropdownのリストが白い原因とCComboBoxでの回避策(OnCtlColor/SetWindowTheme)

MFCアプリをダークモード対応すると、CComboBoxExだけがDropdown時にリスト背景が白くなり、UIの統一感が崩れることがあります。本記事では原因の整理と、見た目を安定させるためにCComboBoxへ切り替えつつEdit入力制御を維持する実装例をまとめます。

目次

起きている現象を整理する

「CComboBoxExをダークモードにしたいのに、Dropdown(入力できるドロップダウン)にするとリスト部分だけ白い」「新規プロジェクトだとOnCtlColorが想定通り拾われないように見える」といった症状は、MFCのダイアログ/ビューでダーク配色を進めるほど目立ちます。特に、OnCtlColorでブラシを返したり、SetWindowThemeでDarkMode_ExplorerやDarkMode_CFDのようなテーマ名を当てて暗色化しているケースで遭遇しやすいです。

まずは、同じ“コンボボックスに見えるもの”でも、内部構造と描画経路が違う点を押さえるのが近道です。見た目の崩れが「Dropdownのリストだけ」なのか、「Edit部分の背景・文字色」なのか、「枠線やハイライト」なのかで、打つべき手が変わります。

観点通常のCComboBoxCComboBoxEx(ComboBoxEx)
役割標準コンボ拡張コンボ(画像/インデント/拡張スタイルなど)
内部構造スタイルに応じてEdit+ListBoxを内部生成複数の子ウィンドウを束ねた共通コントロール(内部に“本体Combo”を持つことが多い)
Dropdown時のリストComboLBox(リスト用の別HWND)が表示される同様に別HWNDだが、テーマ/色の引き回しが一致しないことがある
OnCtlColorの当たりやすさ比較的素直(環境差はある)プロジェクト設定や生成タイミングで「拾っているはずが効かない」に見えやすい
ダークモード統一の手間低〜中中〜高(特にDropdownのリスト部分)

なぜCComboBoxExのDropdownだけ崩れやすいのか

CComboBoxExは「見た目がコンボ」でも、実体はWindowsの共通コントロール(COMBOBOXEXクラス)です。内部にEditや本体Combo、さらにドロップダウンのListBox(多くの場合クラス名はComboLBox)など、複数のウィンドウが関与します。ここで重要なのは、Dropdownのリストは表示の瞬間に“別ウィンドウ”として生成され、親子関係やテーマ適用の流れがDialog上の他コントロールと揃わないことがあるという点です。

OnCtlColorは、基本的に「親(DialogやView)が子コントロールを描画するときにWM_CTLCOLORxxxが飛び、そこでブラシや色を返す」仕組みです。しかし、Dropdownのリストは“自分の子”として扱われにくかったり、生成タイミングが遅かったり、テーマ(Visual Styles)の描画が優先されてブラシが反映されなかったりして、結果として白背景が残るケースがあります。

発生ポイント起こりがちなこと見た目の症状
SetWindowThemeをComboBoxEx本体にだけ適用内部のEdit/ListBoxに同じテーマが当たらない枠は暗いのにリストだけ白い
Dropdownを開いた瞬間にListBoxが生成OnInitDialog時点ではHWNDが存在せず、後追い適用が必要初回だけ白い/開くたびに色が戻る
WM_CTLCOLORLISTBOXが親に届かない親子関係・所有関係が想定と違うOnCtlColorを書いてもリストに効かない
テーマ描画が強いWM_CTLCOLORで返したブラシよりテーマが優先される背景だけでなく選択色/境界線がチグハグ

ここまでを踏まえると、「CComboBoxExのDropdownを完璧にダーク化する」こと自体は不可能ではないものの、安定して同じ見た目を出すには、リスト用HWNDの捕捉・サブクラス化・再テーマ適用などの手当てが増えやすいのが実情です。UI全体の統一を優先するプロダクトでは、ここがコストの分岐点になります。

ダークモードでつまずきやすいWM_CTLCOLOR系の整理

MFCで「OnCtlColorを書いたのに効かない」と感じるときは、そもそも想定しているnCtlColorで呼ばれていない、もしくは呼ばれていてもテーマ描画で上書きされていることが多いです。まずは、どのコントロールにどのnCtlColorが割り当たるかを押さえておくと、調査が一気に早くなります。

nCtlColor対象になりやすいコントロールダークモード対応での注意点
CTLCOLOR_DLGダイアログ背景背景色を変えるだけでなく、静的テキスト(Static)が透過になっているかで見え方が変わる
CTLCOLOR_STATICStatic(ラベルなど)pDC->SetBkMode(TRANSPARENT)と組み合わせないと「ラベルの四角い地」が出ることがある
CTLCOLOR_EDITEdit、Dropdownの入力欄(Edit部分)コンボのEditは“子ウィンドウ”として存在するため、コンボ本体の色変更だけでは揃わない
CTLCOLOR_LISTBOXListBox、コンボのドロップダウンリストコンボのリストはComboLBoxとして別HWNDで生成され、親にWM_CTLCOLORLISTBOXが届かないケースがある

特にComboBoxExのDropdownで問題になりやすいのがCTLCOLOR_LISTBOX周りです。「ここでブラシを返せば暗くなるはず」と思っても、実際には別のウィンドウ階層で描画されていて届かない、あるいは届いてもテーマに上書きされる、という状況が起こりえます。

現実的な落とし所:見た目が安定するCComboBoxに切り替える

実務で選ばれやすい解決策はシンプルです。見た目(特にDropdown時のリスト部分)の安定性を優先し、CComboBoxExをやめて通常のCComboBoxへ切り替える。そして、CComboBoxでも「入力欄(Edit)にアクセスして数値入力制御を入れたい」という要件は、子ウィンドウからEditを取得すれば満たせます。

「CComboBoxExを使いたい理由が“Editに触りやすいから”」であれば、切り替えの痛みは小さく、得られる安定性は大きいです。逆に「項目にアイコンを付けたい」「ComboBoxEx特有のスタイルが必須」という場合は、後半の“どうしてもCComboBoxExを使う場合”を検討してください。

判断軸CComboBoxへ切り替えCComboBoxExを維持
UIの一貫性(ダーク統一)高い調整次第だが手間が増えがち
実装コスト低い(Edit取得+入力制御)中〜高(ListBoxの追従、テーマ、サブクラス化)
将来のWindows更新耐性比較的高い内部実装差分の影響を受けやすい
アイコン付き項目など苦手(工夫が必要)得意(本来の用途)

CComboBoxでEditコントロールを取得する方法

CComboBoxをDropdown(入力できるタイプ)で使っている場合、内部にEditが作られます。ここに対して数値入力制限や入力補助を入れれば、CComboBoxExを使わなくても同等の「入力制御」を実現できます。

最短手:GetWindow(GW_CHILD)でEditを取る

いちばん手軽なのは、子ウィンドウの先頭をEditとして扱う方法です。環境によって子の順序が変わる可能性はありますが、まずはここから始めるのが実務的です。

// ダイアログ側などで(m_combo は CComboBox)
// ※CBS_DROPDOWN / CBS_SIMPLE のときに Edit が存在します(CBS_DROPDOWNLISTには無い)
// ※RTTIが有効なら dynamic_cast でもOK。無効なら static_cast や DYNAMIC_DOWNCAST を検討。
CEdit* pEdit = (CEdit*)m_combo.GetWindow(GW_CHILD);
if (pEdit != nullptr)
{
    // 例:最大文字数を制限
    pEdit->SetLimitText(6);

    // 例:簡易的に数字のみ(ES_NUMBER)
    pEdit->ModifyStyle(0, ES_NUMBER);
}

実際には、次のように「コンボから子Editを取り、CEditとして扱う」だけで目的を満たせるケースが多いです。

// 例:RTTIが有効なプロジェクトなら dynamic_cast でも書けます
CEdit* pEdit2 = dynamic_cast<CEdit*>(m_combo.GetWindow(GW_CHILD));

ただし、MFCプロジェクトではC++ RTTI(/GR)を無効にしていることがあります。その場合はdynamic_castが使えないので、static_castやMFCのDYNAMIC_DOWNCAST(対象がCObject派生のとき)を選びます。

ポイントは、CComboBoxがCBS_DROPDOWN(またはCBS_SIMPLE)であることです。DropList(CBS_DROPDOWNLIST)だとEdit自体が存在しないため、この方法では取得できません。

コンボのスタイル入力欄(Edit)ユーザーが文字入力できるかEdit取得の前提
CBS_DROPDOWNありできるGetWindow(GW_CHILD) / GetComboBoxInfo でOK
CBS_SIMPLEあり(常にリスト表示)できる同上
CBS_DROPDOWNLISTなしできない(選択のみ)Edit取得は不可(別UIで入力)

より確実:子ウィンドウを列挙して“Edit”を探す

子ウィンドウの並び順が変わることを避けたい場合は、子を順に辿ってクラス名が「Edit」のものを見つける方法が安全です。CComboBoxをサブクラス化して関数化しておくと、同種の入力コンボを増やすときも流用できます。

// CComboBox を継承したクラス内に置く想定
CEdit* GetEditCtrl()
{
    CWnd* p = GetWindow(GW_CHILD);
    while (p != nullptr)
    {
        char cls[64] = {};
        ::GetClassNameA(p->GetSafeHwnd(), cls, (int)sizeof(cls));
        if (_stricmp(cls, "Edit") == 0)
        {
            return (CEdit*)p;
        }
        p = p->GetWindow(GW_HWNDNEXT);
    }
    return nullptr;
}

この方式は、「将来Windowsの内部実装やテーマの都合で子の並びが変わる」リスクに対して強めです。逆に、クラス名判定に依存するため、特殊なカスタムEditを使っている場合は条件を調整してください。

さらに堅牢:GetComboBoxInfoでEditとListBoxのHWNDを取得する

Windows APIのGetComboBoxInfoは、コンボの内部ハンドル(Edit用とListBox用)をまとめて取得できます。ダークモード対応で「リスト側にも何かしたい」場合にも役立つため、覚えておくと便利です。

// m_combo は CComboBox
COMBOBOXINFO cbi = {};
cbi.cbSize = sizeof(cbi);

if (::GetComboBoxInfo(m_combo.GetSafeHwnd(), &cbi))
{
    HWND hwndEdit = cbi.hwndItem;  // Edit のHWND(CBS_DROPDOWN/SIMPLEのとき)
    HWND hwndList = cbi.hwndList;  // ドロップダウンリストのHWND(開いたときに有効になることが多い)

    if (hwndEdit != nullptr)
    {
        // 例:EditにES_NUMBERを付与(※既存スタイルを壊さないよう注意)
        LONG_PTR style = ::GetWindowLongPtr(hwndEdit, GWL_STYLE);
        ::SetWindowLongPtr(hwndEdit, GWL_STYLE, style | ES_NUMBER);
    }
}

GetWindow(GW_CHILD)より少し手間ですが、「Editを確実に取りたい」「ListBoxのHWNDも欲しい」場合に役立ちます。

数値入力の制御例:ES_NUMBERだけで足りないときの実装パターン

ES_NUMBERは手軽ですが、要件次第では不足します。例えば「空欄は許すが、確定時には範囲チェック」「0〜9999だけ」「先頭ゼロは許可しない」など、プロダクト要件は意外と細かいです。ここでは、よく使う実装パターンをまとめます。

やりたいことおすすめ手段メリット注意点
数字だけ打てれば良いES_NUMBER最速で実装できる貼り付け/IME/記号など、厳密な制御は別途必要な場合あり
範囲(min/max)を保証したいフォーカスアウト/OK時に解析して補正UXが良い(入力中は自由)確定タイミングの設計が必要
入力中から完全に弾きたいEditをサブクラス化してWM_CHAR/WM_PASTEを制御堅牢コード量が増える
選択肢+自由入力を両立コンボの候補は保持しつつ、Editで入力検索性と自由度の両立候補同期(入力→選択)をどうするか

「CComboBoxExだからEditに触れる」わけではなく、DropdownのCComboBoxでも同様にEditを制御できます。暗色化の安定性を優先するなら、ここが大きなメリットになります。

ダークモード対応の実装ポイント(CComboBox編)

CComboBoxに切り替えたあとも、MFC側のダークモード対応は基本的に同じです。OnCtlColorで背景ブラシと文字色を返し、必要ならSetWindowThemeでテーマ名を当てます。ただし、コンボは内部にEditとListBoxを持つため、コンボ本体だけでなく、Edit側の色を揃えることが大切です。

OnCtlColorの最小構成例

以下は「ダイアログ背景」「Edit系(コンボのEdit含む)」を暗色にする典型例です。色はプロダクトのデザインに合わせて調整してください。

// 例:CDialogEx 派生クラスのメンバ
CBrush m_brDialogBack;
CBrush m_brEditBack;
COLORREF m_clrText = RGB(230, 230, 230);

// OnInitDialog などでブラシを作る
BOOL CMyDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_brDialogBack.CreateSolidBrush(RGB(32, 32, 32));
    m_brEditBack.CreateSolidBrush(RGB(45, 45, 45));

    // コンボ本体にテーマを当てる(必要に応じて)
    ::SetWindowTheme(m_combo.GetSafeHwnd(), L"DarkMode_Explorer", nullptr);

    // Edit取得して入力制御
    if (CEdit* pEdit = (CEdit*)m_combo.GetWindow(GW_CHILD))
    {
        pEdit->ModifyStyle(0, ES_NUMBER);
        pEdit->SetLimitText(6);
    }
    return TRUE;
}

HBRUSH CMyDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);

    switch (nCtlColor)
    {
    case CTLCOLOR_DLG:
        pDC->SetTextColor(m_clrText);
        pDC->SetBkColor(RGB(32, 32, 32));
        return (HBRUSH)m_brDialogBack.GetSafeHandle();

    case CTLCOLOR_EDIT:
        pDC->SetTextColor(m_clrText);
        pDC->SetBkColor(RGB(45, 45, 45));
        return (HBRUSH)m_brEditBack.GetSafeHandle();

    default:
        break;
    }
    return hbr;
}

この時点で、通常のCComboBoxはDropList/Dropdownのどちらでも比較的揃いやすいはずです。対してCComboBoxExは、Dropdownのリスト側が別経路で描画されるため、同じ書き方では統一が難しくなります。

それでもCComboBoxExが必要な場合の現実的アプローチ

アイコン付き項目やComboBoxEx固有の機能が必須で、どうしてもCComboBoxExを使いたい場合は、「Dropdownを開いたタイミングでListBoxのHWNDを捕まえて後追いでテーマ/描画を当てる」方針になります。ここから先は、コストと保守性を理解したうえで採用してください。

ComboBoxExから内部Combo/Editを取得する

ComboBoxExには内部コントロールを返すメッセージが用意されています。例えば、CBEM_GETCOMBOCONTROLで“内側のComboBox”のHWNDが取得でき、そこに対してGetComboBoxInfoを使うとEdit/ListのHWNDへ辿れます。

// m_comboEx は CComboBoxEx
HWND hwndInnerCombo = (HWND)m_comboEx.SendMessage(CBEM_GETCOMBOCONTROL, 0, 0);
HWND hwndInnerEdit  = (HWND)m_comboEx.SendMessage(CBEM_GETEDITCONTROL, 0, 0);

// さらにリストHWNDを取りたい場合
COMBOBOXINFO cbi = {};
cbi.cbSize = sizeof(cbi);
if (hwndInnerCombo != nullptr && ::GetComboBoxInfo(hwndInnerCombo, &cbi))
{
    HWND hwndList = cbi.hwndList;
    // Dropdown表示中に有効なことが多い
}

ここでの重要点は、ListBoxのHWNDはDropdownを開くまで無効(または別物に差し替わる)ことがある点です。つまり、OnInitDialogで一度取って終わりではなく、Dropdownのたびに再確認する必要が出ます。

CBN_DROPDOWNで“開いた瞬間”にListBoxへテーマを当てる

Dropdownが開くタイミング(CBN_DROPDOWN通知)でListBoxのHWNDを取得し、SetWindowThemeを当て直すパターンです。これで「初回だけ白い」「開くたびに白へ戻る」現象を抑えられることがあります。

// 例:ダイアログ側のメッセージマップで CBN_DROPDOWN を受ける
void CMyDlg::OnCbnDropdownComboEx()
{
    HWND hwndInnerCombo = (HWND)m_comboEx.SendMessage(CBEM_GETCOMBOCONTROL, 0, 0);

    COMBOBOXINFO cbi = {};
    cbi.cbSize = sizeof(cbi);
    if (hwndInnerCombo != nullptr && ::GetComboBoxInfo(hwndInnerCombo, &cbi))
    {
        if (cbi.hwndList != nullptr)
        {
            ::SetWindowTheme(cbi.hwndList, L"DarkMode_Explorer", nullptr);
            // 必要ならここでサブクラス化して独自描画へ
        }
    }
}

ただし、テーマ適用だけで完全に揃わない場合があり、そのときはListBox側のWM_CTLCOLORLISTBOX/WM_PAINT/WM_ERASEBKGNDなどを処理する必要が出ます。ここまで来ると「安定させるための追加工数」が増え、最初にCComboBoxへ切り替えたほうが総コストが低い、という判断になりがちです。

アプローチ効果実装負荷壊れにくさ
ComboBoxEx本体にSetWindowThemeだけ部分的低中
CBN_DROPDOWNでListBoxへSetWindowTheme改善することがある中中
ListBoxをサブクラス化して描画を制御高い(狙い通りにできる)高低〜中(OS更新の影響を受けやすい)
CComboBoxへ切り替え高い(安定)低高

「OnCtlColorが拾われない」に見えるときのチェックリスト

新規プロジェクトや環境差で「OnCtlColorが効いていない気がする」という場合、原因は“色を返していない”よりも“返しているが別経路で上書きされている”ことが多いです。確認の手順を表にまとめます。

手元で再現条件を絞るときは、まず「今どのウィンドウ(クラス名)が描画対象になっているか」をログに出すのが効果的です。Spy++が使えるなら最速ですが、プロジェクト内だけで完結させたい場合はGetClassNameとOutputDebugStringで十分追えます。

// 例:OnCtlColorの先頭に一時的に入れて、描画対象のクラス名を確認する
TCHAR cls[128] = {};
::GetClassName(pWnd->GetSafeHwnd(), cls, 128);

CString msg;
msg.Format(_T("[CTLCOLOR] n=%u hwnd=0x%p class=%s\\n"), nCtlColor, pWnd->GetSafeHwnd(), cls);
::OutputDebugString(msg);
現象原因候補確認方法対処の方向性
コンボのEditだけ色が変わらないCTLCOLOR_EDITの分岐不足、Editが別HWNDSpy++でEditの存在を確認GetWindow/GetComboBoxInfoでEditを捕捉して個別対応
Dropdownのリストだけ白いListBoxが別ウィンドウでWM_CTLCOLORが届かないDropdown中にSpy++でComboLBoxを確認CBN_DROPDOWNでListBoxへ後追いテーマ/サブクラス化、またはCComboBoxへ切替
初回だけ白い/2回目以降は暗い生成タイミングが遅いDropdownのイベントでHWNDが変わるか見る開くたびに適用、またはサブクラス化のタイミングを調整
Windowsのテーマ設定で挙動が変わるVisual Stylesやテーマ描画の優先テーマON/OFFで比較テーマを切る/当て直す、OwnerDrawへ寄せる

実務でおすすめの設計メモ

最後に、似た問題を繰り返さないための設計メモを残します。ダークモード対応は「個別のコントロールを一つずつ暗くする」作業になりやすく、後からUIが増えるほどメンテコストが跳ねます。以下は、筆者が“手戻りが少ない”と感じる方針です。

  • 見た目が安定している標準コントロールを優先し、特殊な共通コントロールは最小限にする(ComboBoxExは代表例)。
  • 「入力制御が必要だからComboBoxEx」という発想を捨て、標準CComboBoxのEditを掴んで制御する設計へ寄せる。
  • テーマ名(DarkMode_Explorer等)に依存する場合は、適用箇所を関数化し、OS差分の調整点を一か所に集約する。
  • DropdownのListBoxなど“後から生えるHWND”は、イベント駆動(CBN_DROPDOWN等)で捕まえる前提にする。
  • 最終的に合わない場合は、OwnerDraw/カスタム描画に進む前に、UI仕様を見直す(コンボに入力させない、別Editにする)のも有効。

CComboBoxExのDropdownのダークモード崩れは、単なる「色設定漏れ」ではなく、内部構造と描画経路の差が表面化したものです。プロダクトとしての優先順位が「UIの統一感」なら、CComboBoxへ寄せてEditを子ウィンドウから取得する設計が、最も事故が少ない現実解になります。

この記事を書いた人

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

コメント

コメントする

目次