在 Windows 桌面開發中,採用 VC++ 搭配 SQL Server 建立主從表(Master-Detail)資料呈現是經典需求。本文以一個輕量級 Demo 為例,完整記錄了從視窗分割、多視圖通訊、列表與樹狀控件操作,到動態主從表綁定的核心技術細節,供開發者參考。
專案採用 SDI 架構,主框架視窗使用 CSplitterWnd 實現三欄式佈局:左側樹狀導航、右上主表列表、右下從表明細。關鍵在於巢狀分割器:先建立 1 行 2 列的外層分割器,再在第 2 列建立 2 行 1 列的內層分割器。
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
CRect rect;
GetClientRect(&rect);
// 外層:1 行 2 列(左樹、右區)
m_wndSplitter1.CreateStatic(this, 1, 2);
m_wndSplitter1.CreateView(0, 0, RUNTIME_CLASS(CClassTreeView), CSize(150, 150), pContext);
// 內層:掛載在外層第 (0,1) 格,2 行 1 列(上主表、下從表)
m_wndSplitter2.CreateStatic(&m_wndSplitter1, 2, 1,
WS_CHILD | WS_VISIBLE, m_wndSplitter1.IdFromRowCol(0, 1));
m_wndSplitter2.CreateView(0, 0, RUNTIME_CLASS(CDagView), CSize(0, 0), pContext); // 主表
m_wndSplitter2.CreateView(1, 0, RUNTIME_CLASS(CDagDetialView), CSize(0, 0), pContext); // 從表
m_wndSplitter2.SetRowInfo(0, rect.Height() / 2, 0); // 初始均分高度
return TRUE; // 切記不可呼叫基類 OnCreateClient
}
要點:靜態分割器建立後窗格數量固定,但可拖曳分隔條調整比例;每個窗格獨立擁有捲軸,適合多視圖協作場景。
專案僅有一個 Document,三個 View 類別獨立運作。跨視圖呼叫的關鍵在於取得目標 View 的指標。採用「主框架中轉」策略:
// 在任意 View 或其他模組中取得 CDagDetialView 指標
CMainFrame* pFrame = static_cast<CMainFrame*>(AfxGetMainWnd());
CDagDetialView* pDetailView = static_cast<CDagDetialView*>(
pFrame->m_wndSplitter2.GetPane(1, 0) ); // 內層分割器第 2 行第 1 列
取得指標後即可直接呼叫該 View 的公開方法(如 RefreshData()、SetFilter() 等),實現資料同步與命令傳遞。建議統一使用 static_cast 替代 C 風格強轉,提升型別安全。
採用 LVS_REPORT 風格呈現類 DataGrid 效果。關鍵設定與事件對照表:
SetWindowLong(m_hWnd, GWL_STYLE, LVS_REPORT | LVS_SINGLESEL | WS_BORDER | WS_TABSTOP)LVN_ITEMCHANGED,判斷 NM_LISTVIEW::uNewState & LVIS_SELECTEDNM_CLICK、NM_DBLCLKvoid CDagView::OnItemchanged(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
CListCtrl& list = GetListCtrl();
if (pNMListView->uNewState & LVIS_SELECTED)
{
int nRow = pNMListView->iItem;
// 取得主鍵值 → 查詢從表 → 通知從表視圖刷新
UpdateDetailView(nRow);
}
*pResult = 0;
}
資料表結構:menucode (PK)、menuname、parentcode (FK)。利用「父代碼長度 + 2 = 子代碼長度」特徵,遞歸插入節點:
void CClassTreeView::InitTree(CTreeCtrl& tree, HTREEITEM hParent, const CString& strParentCode)
{
_RecordsetPtr pRs = ...; // 查詢 WHERE parentcode = strParentCode
while (!pRs->adoEOF)
{
_variant_t var = pRs->GetCollect(_T("menucode"));
CString strCode = (var.vt != VT_NULL) ? (LPCSTR)_bstr_t(var) : _T("");
CString strName = ...; // 取得 menuname
// 判斷是否為直系子節點(長度差 2 且前綴相符)
if (strCode.Find(strParentCode) == 0 && strCode.GetLength() == strParentCode.GetLength() + 2)
{
HTREEITEM hChild = tree.InsertItem(strName, hParent, TVI_LAST);
InitTree(tree, hChild, strCode); // 遞歸處理下一層
}
pRs->MoveNext();
}
}
選取節點事件(TVN_SELCHANGED)中取得節點文字,呼叫主表視圖依條件重查資料。
假設資料庫結構不固定,需動態發現主鍵、動態構建查詢。核心流程:
sp_pkeys 或 INFORMATION_SCHEMA.KEY_COLUMN_USAGE)SELECT * FROM Detail WHERE MasterID = ?)CListCtrl(先清空欄位再 InsertColumn、InsertItem/SetItemText)// 取得欄位索引輔助函式
int CDagView::GetColumnIndex(CListCtrl& list, LPCTSTR pszColName)
{
CHeaderCtrl* pHeader = list.GetHeaderCtrl();
int nCount = pHeader->GetItemCount();
HDITEM hdi; hdi.mask = HDI_TEXT; TCHAR buf[256]; hdi.pszText = buf; hdi.cchTextMax = 256;
for (int i = 0; i < nCount; ++i)
{
pHeader->GetItem(i, &hdi);
if (_tcscmp(hdi.pszText, pszColName) == 0) return i;
}
return -1;
}
// 使用範例
int nCol = GetColumnIndex(list, _T("MasterID"));
CString strKey = list.GetItemText(nCurRow, nCol); // 取得主鍵值
雖較 .NET DataGrid 繁瑣,但掌握後可構建高度靈活的通用資料瀏覽元件。
本 Demo 在 Windows 2000 / VC6.0 環境開發測試通過。涵蓋分割視窗、多視圖協作、列表/樹狀控件深度操作、ADO 動態主從表綁定等實戰技巧。完整原始碼已打包,歡迎下載研究、改進與分享。