Visual Basic 的屬性視窗以「分類檢視」著稱:左側分類圖示可展開/摺疊,右側屬性名不可編輯,值欄位依型別呈現編輯框、下拉框、按鈕等。本文介紹一個從 CListBox 衍生的 CPropertyList 控制項,單一列表列模擬三欄佈局,完美複刻 VB 風格。
CListBox (Owner-draw variable height 模式)CPropertyItem 封裝分類/屬性節點,含型別枚舉 PIT_EDIT、PIT_COMBO、PIT_BUTTON 等。PropertyList.h/.cpp、Expand.bmp/UnExpand.bmp 至專案目錄。IDB_PROPERTY_EXPAND、IDB_PROPERTY_UNEXPAND。m_cList,手動修改標頭檔:CPropertyList m_cList;#include "PropertyList.h"
CPropertyList* m_pList; // 控制項指標
CPropertyItem* m_pCategory; // 分類節點
CPropertyItem* m_pName; // 屬性節點
BOOL CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
m_pList = &m_cList;
// 建立分類節點:名稱、初始值、型別、選項字串、可見、可展開、圖示索引
m_pCategory = new CPropertyItem(_T("公用屬性"), _T(""), PIT_CATEGORY, _T(""), TRUE, TRUE, 0);
m_pName = new CPropertyItem(_T("名稱"), _T(""), PIT_EDIT, _T(""), FALSE, FALSE, 0);
// 將分類加入控制項
m_pList->AddItem(m_pCategory);
// 將屬性加入分類 (父節點指標)
m_pList->AddItem(m_pName, m_pCategory);
return TRUE;
}
void CMyDialog::OnDestroy()
{
delete m_pCategory;
delete m_pName;
CDialog::OnDestroy();
}
// 寫入
m_pName->m_curValue = pGraph->GetNodeName();
m_pList->Invalidate(); // 刷新顯示
// 讀取
pGraph->SetNodeName(m_pName->m_curValue);
void CPropertyList::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
CRect rc = lpDrawItemStruct->rcItem;
CPropertyItem* pItem = (CPropertyItem*)lpDrawItemStruct->itemData;
// 背景
pDC->FillSolidRect(rc, GetSysColor(COLOR_WINDOW));
// 第 0 欄:展開/摺疊圖示 (僅分類節點)
if (pItem->m_nType == PIT_CATEGORY)
{
CBitmap* pBmp = pItem->m_bExpanded ? &m_bmpUnExpand : &m_bmpExpand;
CDC memDC; memDC.CreateCompatibleDC(pDC);
CBitmap* pOld = memDC.SelectObject(pBmp);
pDC->BitBlt(rc.left + 2, rc.top + 2, 16, 16, &memDC, 0, 0, SRCCOPY);
memDC.SelectObject(pOld);
}
// 第 1 欄:屬性名稱
CRect rcName(rc.left + 22, rc.top, rc.left + 150, rc.bottom);
pDC->DrawText(pItem->m_strName, rcName, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
// 第 2 欄:由內嵌控制項自行繪製 (CreateInplaceControl)
}
void CPropertyList::OnLButtonDown(UINT nFlags, CPoint point)
{
int nIndex = GetTopIndex();
for (int i = 0; i < GetCount(); i++, nIndex++)
{
CRect rc; GetItemRect(nIndex, &rc);
if (rc.PtInRect(point))
{
CPropertyItem* pItem = (CPropertyItem*)GetItemData(nIndex);
if (pItem->m_nType == PIT_CATEGORY && point.x < 22)
{
pItem->m_bExpanded = !pItem->m_bExpanded;
RefreshVisibleItems(); // 重建可見列表陣列
Invalidate();
break;
}
// 其他列點擊 → 啟動內嵌編輯控制項
EditItem(nIndex);
break;
}
}
CListBox::OnLButtonDown(nFlags, point);
}
void CPropertyList::EditItem(int nIndex)
{
CPropertyItem* pItem = (CPropertyItem*)GetItemData(nIndex);
CRect rc; GetItemRect(nIndex, &rc);
rc.left += 152; rc.right -= 2; // 第 2 欄區域
switch (pItem->m_nType)
{
case PIT_EDIT:
m_pEditCtrl = new CEdit;
m_pEditCtrl->Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, rc, this, IDC_INPLACE_EDIT);
m_pEditCtrl->SetWindowText(pItem->m_curValue);
m_pEditCtrl->SetFocus();
break;
case PIT_COMBO:
m_pComboCtrl = new CComboBox;
m_pComboCtrl->Create(WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWNLIST, rc, this, IDC_INPLACE_COMBO);
// 填入選項 pItem->m_strOptions (分號分隔)
ParseOptions(pItem->m_strOptions, m_pComboCtrl);
m_pComboCtrl->SetCurSel(pItem->m_nCurSel);
m_pComboCtrl->SetFocus();
break;
case PIT_BUTTON:
m_pBtnCtrl = new CButton;
m_pBtnCtrl->Create(pItem->m_curValue, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, rc, this, IDC_INPLACE_BUTTON);
m_pBtnCtrl->SetFocus();
break;
}
m_nEditingIndex = nIndex;
}
CPropertyItem 加入 CPropertyItem* m_pParent,RefreshVisibleItems 遞迴展開。EN_KILLFOCUS/CBN_SELENDOK 通知中回寫 m_curValue 並觸發驗證回呼。DrawItem 實作主題感知 (Dark Mode) 配色。WM_KEYDOWN (Up/Down/Enter/Escape) 實現無滑鼠操作。CPropertyList 以 CListBox Owner-draw 機制為基礎,巧妙模擬三欄分類屬性表,代碼量精簡且易於擴充。掌握「資料模型分離、動態內嵌控制項、可見列表重建」三大核心技巧,即可快速整合至任何 MFC 專案,為使用者提供熟悉的 VB 風格屬性編輯體驗。