MFCアプリをダークモード対応すると、CComboBoxExだけがDropdown時にリスト背景が白くなり、UIの統一感が崩れることがあります。本記事では原因の整理と、見た目を安定させるためにCComboBoxへ切り替えつつEdit入力制御を維持する実装例をまとめます。
起きている現象を整理する
「CComboBoxExをダークモードにしたいのに、Dropdown(入力できるドロップダウン)にするとリスト部分だけ白い」「新規プロジェクトだとOnCtlColorが想定通り拾われないように見える」といった症状は、MFCのダイアログ/ビューでダーク配色を進めるほど目立ちます。特に、OnCtlColorでブラシを返したり、SetWindowThemeでDarkMode_ExplorerやDarkMode_CFDのようなテーマ名を当てて暗色化しているケースで遭遇しやすいです。
まずは、同じ“コンボボックスに見えるもの”でも、内部構造と描画経路が違う点を押さえるのが近道です。見た目の崩れが「Dropdownのリストだけ」なのか、「Edit部分の背景・文字色」なのか、「枠線やハイライト」なのかで、打つべき手が変わります。
| 観点 | 通常のCComboBox | CComboBoxEx(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_STATIC | Static(ラベルなど) | pDC->SetBkMode(TRANSPARENT)と組み合わせないと「ラベルの四角い地」が出ることがある |
| CTLCOLOR_EDIT | Edit、Dropdownの入力欄(Edit部分) | コンボのEditは“子ウィンドウ”として存在するため、コンボ本体の色変更だけでは揃わない |
| CTLCOLOR_LISTBOX | ListBox、コンボのドロップダウンリスト | コンボのリストは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が別HWND | Spy++で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を子ウィンドウから取得する設計が、最も事故が少ない現実解になります。

コメント