當對話方塊控制項過多,分類管理成為必要。MFC 提供 `CPropertySheet`(屬性工作表)與 `CPropertyPage`(屬性頁)標準架構。本文比較三種整合方案:方案一:單一對話方塊 + Tab Control 手動顯示/隱藏、方案二:標準 PropertySheet 模態/非模態彈出、方案三:繼承 CPropertySheet 嵌入主對話方塊並保留自有控制項。重點解析方案三的嵌入技巧與座標計算。
// 核心邏輯:TCN_SELCHANGE 通知中切換可見性
void CMyDialog::OnSelchangeTab(NMHDR* pNMHDR, LRESULT* pResult)
{
int nTab = m_tab.GetCurSel();
m_group1.ShowWindow(nTab == 0 ? SW_SHOW : SW_HIDE);
m_group2.ShowWindow(nTab == 1 ? SW_SHOW : SW_HIDE);
*pResult = 0;
}
void CMainDialog::OnShowPropSheet()
{
CPropertySheet sheet(_T("設定選項"));
CPage1 page1; CPage2 page2;
sheet.AddPage(&page1);
sheet.AddPage(&page2);
sheet.DoModal(); // 或 sheet.Create(this) 非模態
}
缺點:無法在主對話方塊保留其他控制項(如工具列、狀態列、常用按鈕)。
// MySheet.h
class CMySheet : public CPropertySheet
{
DECLARE_DYNAMIC(CMySheet)
public:
CMySheet(UINT nIDCaption, CWnd* pParent = NULL, UINT iSelectPage = 0);
CMySheet(LPCTSTR pszCaption, CWnd* pParent = NULL, UINT iSelectPage = 0);
virtual ~CMySheet();
CButton m_btnExtra; // 主對話區額外按鈕
protected:
DECLARE_MESSAGE_MAP()
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
virtual BOOL OnInitDialog();
};
BOOL CMySheet::OnInitDialog()
{
BOOL bResult = CPropertySheet::OnInitDialog();
// 1. 取得目前視窗大小
CRect rcSheet; GetWindowRect(&rcSheet);
// 2. 擴寬視窗以容納右側額外按鈕區(寬度 +100)
SetWindowPos(NULL, 0, 0, rcSheet.Width() + 100, rcSheet.Height(),
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
// 3. 建立額外按鈕(座標相對擴寬後的客戶區)
CRect rcBtn(rcSheet.Width() + 10, 20, rcSheet.Width() + 90, 50);
m_btnExtra.Create(_T("執行"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, rcBtn, this, IDC_BTN_EXTRA);
// 4. 移除預設的 OK/Cancel/Apply 按鈕(可選)
GetDlgItem(IDOK)->ShowWindow(SW_HIDE);
GetDlgItem(IDCANCEL)->ShowWindow(SW_HIDE);
GetDlgItem(ID_APPLY_NOW)->ShowWindow(SW_HIDE);
// 5. 將分頁頁籤移至底部(可選)
// ModifyStyle(0, PSH_PROPTITLE); // 不同版本行為不同
return bResult;
}
void CMainDialog::OnShowEmbeddedSheet()
{
// 建立非模態 Sheet,父視窗為 this
if (!m_pSheet)
{
m_pSheet = new CMySheet(_T("嵌入式屬性頁"), this);
m_pSheet->AddPage(&m_page1);
m_pSheet->AddPage(&m_page2);
m_pSheet->Create(this, WS_CHILD | WS_VISIBLE | WS_TABSTOP, 0);
}
// 定位 Sheet 於主對話方塊客戶區特定矩形
CRect rc; GetClientRect(&rc);
rc.DeflateRect(10, 10, 10, 40); // 留邊距
m_pSheet->SetWindowPos(NULL, rc.left, rc.top, rc.Width(), rc.Height(),
SWP_NOZORDER | SWP_NOACTIVATE);
m_pSheet->ShowWindow(SW_SHOW);
}
CMainDialog::~CMainDialog()
{
if (m_pSheet)
{
m_pSheet->DestroyWindow();
delete m_pSheet;
m_pSheet = NULL;
}
}

主對話方塊保留標題列、工具列,中間區域嵌入帶分頁的屬性頁,右側保留自訂按鈕,分頁切換流暢,資料交換獨立於各 `CPropertyPage` 衍生類。
| 技巧 | 說明 |
|---|---|
| Style=Child, Border=Thin | 屬性頁資源必須設為 Child 模式,否則無法嵌入 |
| Create() 而非 DoModal() | 非模態建立,傳入父視窗指標 |
| SetWindowPos 定位 | 嵌入後需手動調整大小位置適應主對話區 |
| 隱藏預設按鈕 | IDOK/IDCANCEL/ID_APPLY_NOW 在嵌入模式下通常不需要 |
| 訊息路由 | Sheet 為子視窗,命令訊息自動路由至對應 Page 類別 |
方案三結合了 `CPropertySheet` 的分頁管理優勢與主對話方塊的佈局彈性,是大型對話方塊應用的最佳架構。關鍵在於:頁面資源設為 Child → Sheet 繼承類別建立額外控制項 → 主對話方塊非模態 Create 並定位。此模式廣泛應用於 IDE 選項對話框、資料庫設定精靈、多步驟安裝嚮導等場景。