戰(zhàn):顯示查詢結(jié)果與列格式化)
簡(jiǎn)介面向 MFC 開(kāi)發(fā)者的 DataGrid 控件用法詳解文檔重點(diǎn)解決在 VC 對(duì)話框程序中網(wǎng)格化展示與操作數(shù)據(jù)的問(wèn)題適合需要快速上手 ADO 數(shù)據(jù)綁定、列控制與格式化的讀者。文檔以可運(yùn)行工程 TestGrid 為基礎(chǔ)系統(tǒng)講解將查詢結(jié)果通過(guò) SetRefDataSource 綁定到控件、利用 CColumns/CColumn 調(diào)整列寬、使用 SetNumberFormat 將小數(shù)顯示為百分?jǐn)?shù)、設(shè)置行高實(shí)現(xiàn)多行顯示以及通過(guò) HeadClick 消息進(jìn)行排序等關(guān)鍵用法并配有 VC6.0 下的調(diào)試說(shuō)明使讀者能夠邊讀邊練、直接對(duì)照實(shí)踐。資源為單份 doc 文檔壓縮包大小 226KB內(nèi)容集中、輕量精煉適合作為 MFC 界面開(kāi)發(fā)中網(wǎng)格控件的案頭參考目前已有 849 人學(xué)習(xí)下載是入門(mén)與進(jìn)階 DataGrid 控件時(shí)不可多得的實(shí)用資料。1. MFC DataGrid控件老項(xiàng)目里最省事的網(wǎng)格方案先搞懂再動(dòng)手接觸過(guò)VC6.0時(shí)代MFC項(xiàng)目的工程師大概率都跟DataGrid控件打過(guò)照面。這玩意是微軟自家提供的ActiveX網(wǎng)格控件用來(lái)顯示數(shù)據(jù)庫(kù)查詢結(jié)果非常順手當(dāng)年很多桌面管理系統(tǒng)里的表格界面都是它撐起來(lái)的。它的優(yōu)勢(shì)在于和Windows系統(tǒng)兼容性極好你不需要自己實(shí)現(xiàn)排序、列寬調(diào)整、單元格格式化這些功能只要把數(shù)據(jù)源接上界面就出來(lái)了。網(wǎng)上雖然有不少第三方網(wǎng)格控件功能看著更花哨但用微軟自家的東西有個(gè)實(shí)際好處——它隨系統(tǒng)升級(jí)你的代碼幾乎不用大改就能繼續(xù)跑。這篇筆記我把實(shí)際拆過(guò)的工程VC6.0 Windows XP工程名TestGrid里的用法整理出來(lái)覆蓋三塊核心操作顯示查詢結(jié)果、控制列寬、格式化單元格內(nèi)容。適合正在維護(hù)老MFC項(xiàng)目、或者被迫用VC6.0干活的朋友新手按步驟走也能跑通。2. 準(zhǔn)備工作插入控件與類封裝別在這步偷懶2.1 找到并插入Microsoft DataGrid控件先建立一個(gè)基于對(duì)話框的MFC工程然后在對(duì)話框編輯器里右鍵選擇插入ActiveX控件。在彈出的列表里找到Microsoft DataGrid Control, version 6.0 (OLEDB)確定插入。注意這里要選OLEDB版本不是ODBC那個(gè)老版本因?yàn)镺LEDB的連接方式更靈活配合ADO用起來(lái)最順手。插入后對(duì)話框上會(huì)出現(xiàn)一個(gè)空的網(wǎng)格默認(rèn)帶一個(gè)示例列。這時(shí)候先別急著寫(xiě)代碼右鍵這個(gè)控件選擇建立類。VC6.0會(huì)彈出一個(gè)確認(rèn)類名的對(duì)話框這里需要手動(dòng)選擇要封裝的類。一般情況下至少選三個(gè)CDataGrid控件主類、CColumns列集合類、CColumn單列類。提示這幾個(gè)類是對(duì)ActiveX控件COM接口的封裝本質(zhì)上是調(diào)用了控件的COM查詢接口。所以你看不到控件內(nèi)部源碼只能通過(guò)方法名推斷功能。這也是為什么很多新手一開(kāi)始覺(jué)得它像個(gè)黑匣子——其實(shí)方法名起得很直白見(jiàn)名知意。2.2 認(rèn)識(shí)三個(gè)核心類的關(guān)系CDataGrid是控件本身負(fù)責(zé)整體顯示和數(shù)據(jù)源掛接。CColumns是列集合通過(guò)CDataGrid.GetColumns()拿到。CColumn是單個(gè)列對(duì)象通過(guò)CColumns.GetItem()拿到。這個(gè)關(guān)系一定要理清楚。打個(gè)比方CDataGrid是一張表CColumns是所有列的目錄CColumn是目錄里某一列的詳細(xì)檔案。你要改列寬、改格式、改標(biāo)題最終操作的都是CColumn對(duì)象。還有個(gè)容易忽略的點(diǎn)GetItem()的參數(shù)是VARIANT類型傳索引時(shí)要小心類型轉(zhuǎn)換。常見(jiàn)做法是構(gòu)造一個(gè)VARIANT變量賦值vt.iVal 列索引vt.vt VT_I4。直接傳整數(shù)在某些編譯器版本會(huì)出類型不匹配的警告。// 正確獲取列對(duì)象的方式 CColumns cols m_ctrlDG.GetColumns(); VARIANT vt; vt.vt VT_I4; vt.iVal 0; // 第0列 CColumn col cols.GetItem(vt);這段代碼先拿到列集合再通過(guò)VARIANT類型的索引取出具體列。參數(shù)說(shuō)明vt是VARIANT結(jié)構(gòu)體必須顯式設(shè)置vt類型為VT_I44字節(jié)整數(shù)不然COM調(diào)用可能失敗。3. 顯示查詢結(jié)果ADO連接數(shù)據(jù)庫(kù)把Recordset綁定給控件3.1 建立數(shù)據(jù)庫(kù)連接和執(zhí)行查詢?cè)挠玫氖茿CCESS數(shù)據(jù)庫(kù)文件note.mdb連接字符串用Jet OLEDB 4.0 Provider。這個(gè)Provider在32位系統(tǒng)上很成熟但64位系統(tǒng)上經(jīng)常翻車(chē)——后面避坑章節(jié)會(huì)詳細(xì)說(shuō)。CADODatabase m_DBCn; CADORecordset m_Rs; CDataGrid m_ctrlDG; CString strConnection; strConnection.Format(_T(ProviderMicrosoft.Jet.OLEDB.4.0;) _T(Data Sourcenote.mdb)); m_DBCn.Open((LPCTSTR)strConnection); m_Rs.SetDatabase(m_DBCn); m_Rs.Open(_T(select * from test;)); m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset());注意這段代碼里的ADO封裝類CADODatabase、CADORecordset不是MFC自帶的需要你自己引入一個(gè)ADO封裝類或者直接用原生ADO方式。邏輯說(shuō)明前三行聲明對(duì)象然后連接字符串指定數(shù)據(jù)庫(kù)文件路徑打開(kāi)連接把記錄集關(guān)聯(lián)到數(shù)據(jù)庫(kù)執(zhí)行查詢最后把記錄集的IUnknown接口傳給DataGrid控件做數(shù)據(jù)源。這里有個(gè)關(guān)鍵點(diǎn)SetRefDataSource的參數(shù)是LPUNKNOWN類型也就是COM接口指針。如果你用的是ADO的RecordsetPtr記得用GetInterfacePtr()獲取原始接口指針而不是直接傳智能指針。3.2 數(shù)據(jù)刷新時(shí)機(jī)與釋放問(wèn)題我第一次做的時(shí)候踩過(guò)一個(gè)坑查詢執(zhí)行完畢后立即SetRefDataSource網(wǎng)格確實(shí)顯示了數(shù)據(jù)但關(guān)閉記錄集后網(wǎng)格又變空了。原因在于DataGrid控件內(nèi)部持有的是記錄集的COM引用記錄集一旦釋放數(shù)據(jù)源就失效了。正確的做法是數(shù)據(jù)源連接和記錄集的生命周期要覆蓋DataGrid的使用周期。一般情況下m_Rs作為成員變量在對(duì)話框銷毀時(shí)才關(guān)閉。如果需要在同一控件上切換不同查詢結(jié)果可以用m_ctrlDG.Refresh()來(lái)刷新顯示。// 刷新數(shù)據(jù)源的通用寫(xiě)法 m_Rs.Close(); m_Rs.Open(_T(select * from table2;)); m_ctrlDG.Refresh();數(shù)據(jù)源切換時(shí)先關(guān)閉舊的記錄集再開(kāi)新的然后調(diào)Refresh讓控件重新取數(shù)據(jù)。參數(shù)說(shuō)明Close后記錄集處于關(guān)閉狀態(tài)必須重新Open才能再次作為數(shù)據(jù)源。4. 控制顯示與格式化列寬、標(biāo)題、百分?jǐn)?shù)格式實(shí)操4.1 設(shè)置列寬和列標(biāo)題之前已經(jīng)講過(guò)怎么用CColumns和CColumn獲取列對(duì)象拿到列之后的控制手段才是重點(diǎn)。CColumns cols m_ctrlDG.GetColumns(); VARIANT vt; vt.vt VT_I4; vt.iVal 0; CColumn col cols.GetItem(vt); col.SetWidth(100); // 設(shè)置列寬為100像素 col.SetCaption(_T(學(xué)號(hào))); // 設(shè)置列標(biāo)題 col.SetVisible(FALSE); // 隱藏該列列的常用屬性就這幾個(gè)寬度、標(biāo)題、可見(jiàn)性。SetWidth的單位是像素twips不這里是像素老版本VC里可能會(huì)有單位換算的問(wèn)題——有次我設(shè)了個(gè)50結(jié)果列窄得看不見(jiàn)后來(lái)發(fā)現(xiàn)是單位不對(duì)。實(shí)際上MSDN的說(shuō)明里寬度單位是OLE_XFORM_PIXELS但有些舊文檔說(shuō)是緹twip實(shí)踐下來(lái)用像素最靠譜。4.2 數(shù)字格式化成百分?jǐn)?shù)這個(gè)是DataGrid比較實(shí)用的功能之一。如果你的數(shù)據(jù)源里存的是0.85這樣的小數(shù)又想顯示成85.0%不用自己在SQL里做字符串拼接直接在列上設(shè)置格式就行。col.SetNumberFormat(_T(0.0%));這個(gè)格式串的含義和Excel里是一樣的0是數(shù)字占位.0表示保留一位小數(shù)%表示乘以100并加百分號(hào)。同理如果你要顯示成兩位小數(shù)的普通數(shù)字用_(0.00)要加千分位分隔符用_(#,##0.00)。提示SetNumberFormat對(duì)整列生效如果你想在同一列里混用不同格式做不到。數(shù)據(jù)格式顯示的統(tǒng)一性在設(shè)計(jì)表格時(shí)就要考慮好。這里有個(gè)細(xì)節(jié)值得注意格式串里的百分號(hào)在CString里沒(méi)有任何特殊含義直接傳字符串就行。但如果是通過(guò)資源文件加載字符串別忘了百分號(hào)在格式化輸出時(shí)的轉(zhuǎn)義問(wèn)題。4.3 行高與多行顯示原文提到了多行顯示對(duì)應(yīng)到代碼就是設(shè)置行高。m_ctrlDG.SetRowHeight(30);行高的單位為像素默認(rèn)值大約是20左右。設(shè)成30后每行高度增加適合顯示內(nèi)容比較長(zhǎng)的單元格也支持自動(dòng)換行。不過(guò)要注意行高只影響數(shù)據(jù)顯示區(qū)域不改變表頭高度。4.4 列排序功能用HeadClick消息實(shí)現(xiàn)DataGrid控件有個(gè)HeadClick事件點(diǎn)擊列標(biāo)題時(shí)觸發(fā)。利用這個(gè)事件可以做點(diǎn)擊列名排序。// 在頭文件中聲明消息處理函數(shù) afx_msg void OnHeadClick(short ColIndex); // 消息映射 ON_EVENT(CDataGrid, 0, 27, OnHeadClick, VTS_I2)消息映射里的事件ID27是HeadClick在DataGrid控件的dispatch ID。不同版本控件可能不一樣如果消息沒(méi)響應(yīng)可以用OLE/COM Object Viewer查一下控件的類型庫(kù)。排序邏輯可以用ADO記錄集的Sort屬性實(shí)現(xiàn)void CMyDlg::OnHeadClick(short ColIndex) { CString strSort; // 根據(jù)列索引構(gòu)造排序字段名 CColumns cols m_ctrlDG.GetColumns(); VARIANT vt; vt.vt VT_I4; vt.iVal ColIndex; CColumn col cols.GetItem(vt); CString strField col.GetCaption(); m_Rs.Sort _T(學(xué)號(hào) ASC); m_ctrlDG.Refresh(); }注意ADO的Sort屬性只在客戶端游標(biāo)模式下有效。如果記錄集用的是服務(wù)器端游標(biāo)Sort屬性會(huì)失效具體表現(xiàn)就是點(diǎn)標(biāo)題沒(méi)反應(yīng)。5. 避坑指南DataGrid常見(jiàn)問(wèn)題與排查記錄5.1 現(xiàn)象64位系統(tǒng)上無(wú)法注冊(cè)控件或者運(yùn)行時(shí)崩潰原因Microsoft DataGrid 6.0是32位ActiveX控件在64位系統(tǒng)上需要注冊(cè)到SysWOW64目錄而且運(yùn)行時(shí)需要通過(guò)32位進(jìn)程加載。如果你的程序編譯成64位直接使用這個(gè)控件會(huì)失敗。解決一種辦法是強(qiáng)制編譯成32位程序VC6.0本身只能產(chǎn)出32位程序所以這個(gè)問(wèn)題主要在把舊代碼遷移到VS2010以上版本時(shí)出現(xiàn)。另一種辦法是用regsvr32注冊(cè)32位版本的控件CD C:\Windows\SysWOW64然后regsvr32 msdatgrd.ocx。5.2 現(xiàn)象SetRefDataSource后網(wǎng)格顯示空白數(shù)據(jù)沒(méi)出來(lái)原因記錄集沒(méi)有處于打開(kāi)狀態(tài)或者記錄集的游標(biāo)類型不支持綁定。DataGrid要求數(shù)據(jù)源是客戶端游標(biāo)adUseClient如果使用adUseServer控件拿不到數(shù)據(jù)。解決打開(kāi)記錄集時(shí)顯式指定客戶端游標(biāo)——在ADO封裝類里調(diào)用Open之前設(shè)置CursorLocation參數(shù)或者在Open語(yǔ)句里加上adCmdText和adUseClient。5.3 現(xiàn)象列寬設(shè)置不起作用或者寬度值不對(duì)原因SetWidth接收的實(shí)際是緹twip值不是像素值。1像素等于15緹按96DPI計(jì)算所以如果直接傳像素值列寬會(huì)縮小15倍。解決把像素值乘以15再傳給SetWidth。想省事的話用屏幕DC的GetDeviceCaps(LOGPIXELSX)把DPI取出來(lái)然后換算。舊代碼里如果看到SetWidth(1500)這種值基本就是這個(gè)原因。5.4 現(xiàn)象點(diǎn)擊列標(biāo)題排序沒(méi)反應(yīng)原因事件映射ID不對(duì)或者記錄集游標(biāo)類型不對(duì)。HeadClick的dispatch ID在不同版本控件里可能有差異用錯(cuò)ID事件不會(huì)觸發(fā)。解決先用OLE/COM Object Viewer打開(kāi)msdatgrd.ocx的類型庫(kù)找到HeadClick的dispatch ID確認(rèn)是你要用的版本。然后檢查記錄集的CursorLocation確保是adUseClient。5.5 現(xiàn)象程序退出時(shí)崩潰報(bào)錯(cuò)在DataGrid釋放階段原因DataGrid控件和記錄集的釋放順序不對(duì)。如果先釋放記錄集再銷毀控件控件內(nèi)部還持有已失效的COM引用釋放時(shí)就會(huì)崩潰。解決在對(duì)話框的OnDestroy里先銷毀控件或者調(diào)用m_ctrlDG.SetRefDataSource(NULL)斷開(kāi)數(shù)據(jù)源然后再關(guān)閉記錄集。釋放順序是先斷開(kāi)數(shù)據(jù)源關(guān)閉記錄集再銷毀控件。6. 進(jìn)階玩法DataGrid控件的邊界與替代方案把原工程跑通之后有幾個(gè)點(diǎn)值得繼續(xù)深入。DataGrid 6.0最明顯的限制是它依賴OLEDB無(wú)法直接綁定自定義數(shù)據(jù)結(jié)構(gòu)比如數(shù)組或C容器。如果不想走數(shù)據(jù)庫(kù)可以考慮的替代方案是MSFlexGrid但MSFlexGrid的格式化能力又差一些。對(duì)于還在維護(hù)老項(xiàng)目的場(chǎng)景我的習(xí)慣做法是把DataGrid封裝成一個(gè)自繪控件類內(nèi)部統(tǒng)一處理數(shù)據(jù)源綁定、列格式設(shè)置、排序邏輯。這樣即使以后換控件界面層的改動(dòng)也能控制在一個(gè)類里面。另一個(gè)比較實(shí)用的技巧是動(dòng)態(tài)生成列。默認(rèn)情況下DataGrid會(huì)根據(jù)數(shù)據(jù)源的字段自動(dòng)生成列。如果只想顯示部分字段可以在綁定之前先清除默認(rèn)列然后手動(dòng)添加需要的列m_ctrlDG.GetColumns().Clear(); // 重新添加列 CColumn col1 m_ctrlDG.GetColumns().Add(0); col1.SetCaption(_T(ID)); col1.SetDataField(_T(id));列集合的Add方法需要傳入列索引返回新的CColumn對(duì)象。然后用SetDataField指定這個(gè)列綁定到記錄集的字段名。這個(gè)方法適合做動(dòng)態(tài)報(bào)表用戶選哪些字段就顯示哪些列。關(guān)于單元格合并、背景色設(shè)置、字體控制這些高級(jí)功能DataGrid 6.0支持得不好。真有這些需求建議跳槽到更現(xiàn)代的控件——但如果是老系統(tǒng)維護(hù)能用DataGrid把數(shù)據(jù)展示好、格式調(diào)對(duì)、排序做出來(lái)功能上已經(jīng)夠業(yè)務(wù)用了。從那以后我每次接DataGrid相關(guān)的活兒都會(huì)強(qiáng)制先跑一遍注冊(cè)控件、連數(shù)據(jù)庫(kù)、綁數(shù)據(jù)源、調(diào)列寬格式、點(diǎn)排序把基礎(chǔ)五步走完再談別的。這套流程走下來(lái)能過(guò)濾掉大部分低級(jí)問(wèn)題。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取