Word 的字型顏色下拉選單以色塊+文字呈現,極具直觀性。MFC `CComboBox` 支援 `CBS_OWNERDRAWFIXED`/`VARIABLE` 自繪模式,配合 `WM_MEASUREITEM`、`WM_DRAWITEM`、`WM_PAINT` 三大訊息,即可實作專業級顏色選擇器。本文完整解析自繪流程,提供 `CBSColorComboBox` 類別原始碼,含系統顏色列舉、自訂顏色項目、雙緩衝防閃爍、鍵盤/滑鼠操作支援。
| 訊息 | 用途 | 關鍵結構成員 |
|---|---|---|
| WM_MEASUREITEM | 決定每個項目高度 | LPMEASUREITEMSTRUCT::itemHeight |
| WM_DRAWITEM | 繪製單一項目內容 | LPDRAWITEMSTRUCT::hDC, rcItem, itemID, itemState, itemData |
| WM_PAINT | 重繪控制項客戶區(編輯框/按鈕部分) | CPaintDC |
| WM_ERASEBKGND | 抑制預設背景抹除,防閃爍 | 返回 TRUE |
class CBSColorComboBox : public CComboBox
{
DECLARE_DYNAMIC(CBSColorComboBox)
public:
CBSColorComboBox();
virtual ~CBSColorComboBox();
// 初始化:填入系統顏色 + 自訂顏色 + "More Colors..."
void Initialize();
// 取得/設定目前選取顏色
COLORREF GetSelectedColor() const;
void SetSelectedColor(COLORREF clr);
protected:
struct ColorItem { COLORREF clr; CString name; bool isCustom; };
CArray m_items;
// 訊息處理
afx_msg void MeasureItem(LPMEASUREITEMSTRUCT lpMIS);
afx_msg void DrawItem(LPDRAWITEMSTRUCT lpDIS);
afx_msg void OnPaint();
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
afx_msg void OnDestroy();
DECLARE_MESSAGE_MAP()
};
void CBSColorComboBox::MeasureItem(LPMEASUREITEMSTRUCT lpMIS)
{
// 項目高度 = 2 * 系統小圖示高度
lpMIS->itemHeight = 2 * GetSystemMetrics(SM_CYSMICON);
}
void CBSColorComboBox::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
CDC* pDC = CDC::FromHandle(lpDIS->hDC);
CRect rc = lpDIS->rcItem;
int idx = lpDIS->itemID;
UINT state = lpDIS->itemState;
if (idx < 0 || idx >= m_items.GetSize()) return;
const ColorItem& item = m_items[idx];
bool bSelected = (state & ODS_SELECTED);
bool bFocus = (state & ODS_FOCUS);
// 1. 背景
COLORREF clrBkg = bSelected ? ::GetSysColor(COLOR_HIGHLIGHT) : ::GetSysColor(COLOR_WINDOW);
COLORREF clrText = bSelected ? ::GetSysColor(COLOR_HIGHLIGHTTEXT) : ::GetSysColor(COLOR_WINDOWTEXT);
CBrush brush(clrBkg);
pDC->FillRect(&rc, &brush);
// 2. 色塊區域:左側 20x20
CRect rcColor(rc.left + 2, rc.top + 2, rc.left + 22, rc.bottom - 2);
CBrush colorBrush(item.clr);
pDC->FrameRect(&rcColor, CBrush::FromHandle((HBRUSH)GetStockObject(BLACK_BRUSH))); // 黑框
rcColor.DeflateRect(1,1);
pDC->FillRect(&rcColor, &colorBrush);
// 3. 文字區域:右側剩餘
CRect rcText(rcColor.right + 6, rc.top, rc.right - 4, rc.bottom);
pDC->SetBkMode(TRANSPARENT);
pDC->SetTextColor(clrText);
pDC->DrawText(item.name, &rcText, DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
// 4. 焦點虛線框
if (bFocus)
pDC->DrawFocusRect(&rc);
}
void CBSColorComboBox::OnPaint()
{
// 雙緩衝重繪編輯框區域
CPaintDC dc(this);
CMemDC memDC(&dc);
CRect rcClient;
GetClientRect(&rcClient);
// 僅繪製編輯框部分(下拉清單由 DrawItem 處理)
CRect rcEdit = rcClient;
rcEdit.bottom = rcEdit.top + 2 * GetSystemMetrics(SM_CYSMICON); // 近似高度
// 填背景
memDC.FillSolidRect(&rcEdit, ::GetSysColor(COLOR_WINDOW));
// 畫目前選取顏色色塊
if (GetCurSel() >= 0)
{
COLORREF clr = m_items[GetCurSel()].clr;
CRect rcColor(rcEdit.left + 2, rcEdit.top + 2, rcEdit.left + 22, rcEdit.bottom - 2);
memDC.FillSolidRect(&rcColor, clr);
memDC.FrameRect(&rcColor, CBrush::FromHandle((HBRUSH)GetStockObject(BLACK_BRUSH)));
}
// 畫邊框
memDC.DrawEdge(&rcEdit, EDGE_SUNKEN, BF_RECT);
}
BOOL CBSColorComboBox::OnEraseBkgnd(CDC* pDC)
{
return TRUE; // 抑制預設抹除,由 OnPaint/DrawItem 全權處理
}
void CBSColorComboBox::Initialize()
{
ResetContent();
m_items.RemoveAll();
// 1. 系統顏色(前 16 色)
static const struct { int idx; LPCTSTR name; } sysColors[] = {
{COLOR_SCROLLBAR, _T("Scrollbar")}, {COLOR_BACKGROUND, _T("Background")},
{COLOR_ACTIVECAPTION, _T("Active Caption")}, {COLOR_INACTIVECAPTION, _T("Inactive Caption")},
{COLOR_MENU, _T("Menu")}, {COLOR_WINDOW, _T("Window")},
{COLOR_WINDOWFRAME, _T("Window Frame")}, {COLOR_MENUTEXT, _T("Menu Text")},
{COLOR_WINDOWTEXT, _T("Window Text")}, {COLOR_CAPTIONTEXT, _T("Caption Text")},
{COLOR_ACTIVEBORDER, _T("Active Border")}, {COLOR_INACTIVEBORDER, _T("Inactive Border")},
{COLOR_APPWORKSPACE, _T("App Workspace")}, {COLOR_HIGHLIGHT, _T("Highlight")},
{COLOR_HIGHLIGHTTEXT, _T("Highlight Text")}, {COLOR_BTNFACE, _T("Button Face")},
};
for (auto& sc : sysColors)
{
ColorItem item; item.clr = ::GetSysColor(sc.idx); item.name = sc.name; item.isCustom = false;
m_items.Add(item);
AddString(item.name); // 文字僅供 DrawItem 參考,實際不顯示
SetItemData(GetCount()-1, m_items.GetSize()-1);
}
// 2. 常用自訂顏色
static const struct { COLORREF clr; LPCTSTR name; } customColors[] = {
{RGB(255,0,0), _T("Red")}, {RGB(0,255,0), _T("Green")}, {RGB(0,0,255), _T("Blue")},
{RGB(255,255,0), _T("Yellow")}, {RGB(255,0,255), _T("Magenta")}, {RGB(0,255,255), _T("Cyan")},
{RGB(0,0,0), _T("Black")}, {RGB(255,255,255), _T("White")}, {RGB(128,128,128), _T("Gray")},
};
for (auto& cc : customColors)
{
ColorItem item; item.clr = cc.clr; item.name = cc.name; item.isCustom = true;
m_items.Add(item);
AddString(item.name);
SetItemData(GetCount()-1, m_items.GetSize()-1);
}
// 3. "More Colors..." 項目
ColorItem more; more.clr = CLR_NONE; more.name = _T("More Colors..."); more.isCustom = false;
m_items.Add(more);
AddString(more.name);
SetItemData(GetCount()-1, m_items.GetSize()-1);
SetCurSel(0);
}
void CMyDialog::OnCbnSelchangeColorCombo()
{
int sel = m_colorCombo.GetCurSel();
if (sel < 0) return;
DWORD_PTR data = m_colorCombo.GetItemData(sel);
if (data < m_colorCombo.m_items.GetSize())
{
const auto& item = m_colorCombo.m_items[data];
if (item.name == _T("More Colors..."))
{
CColorDialog dlg(m_colorCombo.GetSelectedColor(), CC_FULLOPEN | CC_RGBINIT, this);
if (dlg.DoModal() == IDOK)
{
COLORREF clr = dlg.GetColor();
// 可選:加入自訂顏色列表頂端
m_colorCombo.InsertCustomColor(clr);
m_colorCombo.SetSelectedColor(clr);
}
// 恢復原選擇或設為第一項
m_colorCombo.SetCurSel(0);
}
}
}

下拉清單顯示色塊+名稱,編輯框同步顯示目前顏色,選擇 "More Colors..." 彈出標準色彩對話框,支援鍵盤上下鍵選擇、滑鼠滾輪、高對比度模式。
自繪 `CComboBox` 的關鍵在於:MeasureItem 定高 → DrawItem 分區繪製(色塊/文字/焦點) → OnPaint 雙緩衝繪製編輯框 → EraseBkgnd 抑制閃爍。`CBSColorComboBox` 封裝完整,可直接放入專案作為屬性頁、工具列、屬性格的顏色選擇器。同樣架構可擴充至字型下拉框、圖示選擇器、縮圖瀏覽器等任意視覺化下拉控制項。