本章介紹 WTL 提供的進階 UI 功能類別:自繪/自訂繪製嵌入類別、WTL 獨有新控件、對話框控件 UI 更新、資料驗證 (DDV)、對話框動態調整大小。
自繪控件需回應 WM_MEASUREITEM, WM_DRAWITEM, WM_COMPAREITEM, WM_DELETEITEM。COwnerDraw 封裝訊息分派,僅需覆寫虛擬函式。
// 訊息映射內部結構
BEGIN_MSG_MAP(COwnerDraw<T>)
MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem) // 父視窗處理
MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)
MESSAGE_HANDLER(WM_COMPAREITEM, OnCompareItem)
MESSAGE_HANDLER(WM_DELETEITEM, OnDeleteItem)
ALT_MSG_MAP(1) // 反射訊息處理
MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
MESSAGE_HANDLER(OCM_MEASUREITEM, OnMeasureItem)
MESSAGE_HANDLER(OCM_COMPAREITEM, OnCompareItem)
MESSAGE_HANDLER(OCM_DELETEITEM, OnDeleteItem)
END_MSG_MAP()
class CSomeDlg : public COwnerDraw<CSomeDlg>, ...
{
BEGIN_MSG_MAP(CSomeDlg)
CHAIN_MSG_MAP(COwnerDraw<CSomeDlg>)
END_MSG_MAP()
void DrawItem(LPDRAWITEMSTRUCT lpdis); // 覆寫此函式
};
class CSomeButtonImpl : public CWindowImpl<CSomeButtonImpl, CButton>,
public COwnerDraw<CSomeButtonImpl>
{
BEGIN_MSG_MAP(CSomeButtonImpl)
CHAIN_MSG_MAP_ALT(COwnerDraw<CSomeButtonImpl>, 1)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
void DrawItem(LPDRAWITEMSTRUCT lpdis);
};
void DrawItem(LPDRAWITEMSTRUCT);
void MeasureItem(LPMEASUREITEMSTRUCT);
int CompareItem(LPCOMPAREITEMSTRUCT);
void DeleteItem(LPDELETEITEMSTRUCT);
可呼叫 SetMsgHandled(FALSE) 讓訊息繼續傳遞。
void CODButtonImpl::DrawItem(LPDRAWITEMSTRUCT lpdis)
{
CDCHandle dc = lpdis->hDC; CDC dcMem; dcMem.CreateCompatibleDC(dc);
dc.SaveDC(); dcMem.SaveDC();
// 背景:擁有焦點紅色,否則藍色
if (lpdis->itemState & ODS_FOCUS) dc.FillSolidRect(&lpdis->rcItem, RGB(255,0,0));
else dc.FillSolidRect(&lpdis->rcItem, RGB(0,0,255));
// 位圖繪製:按下時偏移 1 像素
dcMem.SelectBitmap(m_bmp);
if (lpdis->itemState & ODS_SELECTED) dc.BitBlt(1,1,80,80,dcMem,0,0,SRCCOPY);
else dc.BitBlt(0,0,80,80,dcMem,0,0,SRCCOPY);
dcMem.RestoreDC(-1); dc.RestoreDC(-1);
}

// 可覆寫的階段處理函式 (預設傳回 CDRF_DODEFAULT)
DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnItemPostErase(int idCtrl, LPNMCUSTOMDRAW);
DWORD OnSubItemPrePaint(int idCtrl, LPNMCUSTOMDRAW);
DWORD CBuffyTreeCtrl::OnPrePaint(int, LPNMCUSTOMDRAW) { return CDRF_NOTIFYITEMDRAW; }
DWORD CBuffyTreeCtrl::OnItemPrePaint(int, LPNMCUSTOMDRAW lpNMCD)
{
if (1 == lpNMCD->lItemlParam) // 节點 item data = 1
lpNMCD->clrText = RGB(0,128,0); // 綠色
return CDRF_DODEFAULT;
}
CImageList 而非單一位圖,減少 GDI 資源佔用 (對 Win9x 關鍵)。CImageList iml;
iml.CreateFromImage(IDB_ALYSON_IMGLIST, 81, 1, CLR_NONE, IMAGE_BITMAP, LR_CREATEDIBSECTION);
m_wndBmpBtn.SubclassWindow(GetDlgItem(IDC_ALYSON_BMPBTN));
m_wndBmpBtn.SetToolTipText(_T("Alyson"));
m_wndBmpBtn.SetImageList(iml);
m_wndBmpBtn.SetImages(0, 1, 2, 3); // 正常、按下、焦點/游標、停用 索引

| 旗標 | 效果 |
|---|---|
| BMPBTN_HOVER | Hot-tracking (游標移入顯示焦點狀態) |
| BMPBTN_AUTO3D_SINGLE/DOUBLE | 自動產生 3D 邊框 (DOUBLE 較粗) |
| BMPBTN_AUTOSIZE | 依圖示大小自動調整按鈕尺寸 (預設開啟) |
| BMPBTN_SHAREIMAGELISTS | 不銷毀共用 ImageList (預設銷毀) |
| BMPBTN_AUTOFIRE | 長按產生連續 WM_COMMAND |
CWindowImpl,預設擴展樣式 LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT。CCheckListViewCtrlImplTraits 指定視窗/擴展樣式。SetCheckState(int index, bool), GetCheckState(int index), CheckSelectedItems(int index)。
CTreeItem 封裝 HTREEITEM + 控件指標,操作不需重複傳遞控件控制代碼。CTreeViewCtrlEx 方法回傳 CTreeItem 而非 HTREEITEM。// 使用 HTREEITEM
HTREEITEM hti = m_wndTree.InsertItem("foo", TVI_ROOT, TVI_LAST);
HTREEITEM hti2 = m_wndTree.InsertItem("bar", hti, TVI_LAST);
m_wndTree.SetItemData(hti2, 100);
// 使用 CTreeItem
CTreeItem ti = m_wndTreeEx.InsertItem("foo", TVI_ROOT, TVI_LAST);
CTreeItem ti2 = ti.AddTail("bar", 0);
ti2.SetData(100);
SetLabel/GetLabel (顯示文字)、SetHyperLink/GetHyperLink (目標 URL)、Navigate() (啟動導航)。m_wndLink.SetHyperLink(_T("https://www.codeproject.com/"));

MFC 需回應 WM_KICKIDLE;WTL 簡單得多,但精靈有 Bug 需手動修正。
CUpdateUI 需在程式訊息迴圈控制下運作 (空閒時間處理)。模態對話框由系統迴圈驅動,無空閒處理 → 無 UI 更新。
class CMainDlg : public CDialogImpl<CMainDlg>,
public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler
{
virtual BOOL PreTranslateMessage(MSG* pMsg);
virtual BOOL OnIdle();
BEGIN_MSG_MAP_EX(CMainDlg)
MSG_WM_INITDIALOG(OnInitDialog)
COMMAND_ID_HANDLER_EX(IDOK, OnOK)
COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)
END_MSG_MAP()
BEGIN_UPDATE_UI_MAP(CMainDlg)
UPDATE_ELEMENT(IDC_ALYSON_BMPBTN, UPDUI_CHILDWINDOW)
END_UPDATE_UI_MAP()
};
CMessageLoop* pLoop = _Module.GetMessageLoop();
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
UIAddChildWindowContainer(m_hWnd); // 告知包含子視窗容器
BOOL CMainDlg::OnIdle()
{
UIUpdateChildWindows(); // 精靈漏掉這行!
return FALSE;
}
void CMainDlg::OnAlysonODBtn(UINT, int, HWND)
{
static bool s_bBtnEnabled = true;
s_bBtnEnabled = !s_bBtnEnabled;
UIEnable(IDC_ALYSON_BMPBTN, s_bBtnEnabled);
}
MFC 分離 DDX/DDV 巨集;WTL 整合在單一巨集中:
DDX_TEXT_LEN:文字長度驗證DDX_INT_RANGE / DDX_UINT_RANGE:整數範圍驗證DDX_FLOAT_RANGE:浮點數範圍驗證BEGIN_DDX_MAP(CMainDlg)
DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)
END_DDX_MAP()

void CMainDlg::OnDataValidateError(UINT nCtrlID, BOOL bSave, _XData& data)
{
CString sMsg;
sMsg.Format(_T("請輸入 %d 到 %d 之間的數字"), data.intData.nMin, data.intData.nMax);
MessageBox(sMsg, _T("ControlMania2"), MB_ICONEXCLAMATION);
::SetFocus(GetDlgItem(nCtrlID));
}
_XData 結構包含 nDataType (指示聯合體有效成員) 與 intData/floatData/textData。
CDialogResizeOnInitDialog 呼叫 DlgResize_Init()CHAIN_MSG_MAP(CDialogResize)詳細用法請參考 Michael Dunn 專文 Using WTL's Built-in Dialog Resizing Class。
介紹對話框中使用 ActiveX 控件與事件處理。