格控件CMFCPropertyGridCtrl實戰(zhàn)教程:從初始化到數(shù)據(jù)回調(diào))
簡介CMFCPropertyGridCtrl 使用示例是一份面向 MFC 開發(fā)者的屬性網(wǎng)格控件演示資源。資源包為 RAR 壓縮格式共 40 個文件、約 152KB以 11 個頭文件、7 個 C 源文件為主配合 vcxproj 工程文件、rc 資源腳本、aps 輔助資源和調(diào)試日志構(gòu)成可直接打開學(xué)習(xí)的完整工程。內(nèi)容預(yù)覽中可見 test 與 dll1 兩套工程分別演示了控件創(chuàng)建、屬性添加、子屬性組織及界面樣式設(shè)置等關(guān)鍵操作能夠幫助讀者快速掌握從初始化到屬性變更處理的全流程。目前已有 735 人學(xué)習(xí)使用適合需要集成屬性網(wǎng)格功能或深入理解 MFC 控件擴(kuò)展機(jī)制的開發(fā)者。通過對照示例代碼讀者可以避免常見的編譯與界面刷新問題并在此基礎(chǔ)上擴(kuò)展自定義屬性編輯器示例代碼結(jié)構(gòu)緊湊適合直接導(dǎo)入 Visual Studio 查看調(diào)試對于希望快速搭建屬性編輯界面的團(tuán)隊具有直接參考價值。 用了這么多年MFC我印象里第一次打開Visual Studio的屬性面板時心里想的是這種控件到底怎么實現(xiàn)的。后來才知道微軟從BCGControlBar Pro里移植了一套界面組件到MFC其中就包括CMFCPropertyGridCtrl這個屬性網(wǎng)格控件。它的出現(xiàn)基本讓自繪屬性面板這件事成了歷史。我最早是在一個內(nèi)部配置工具里用它替換手寫的幾十個編輯框和下拉框改完之后整個界面代碼量直接砍掉一大半。這篇就從一個實際使用者的角度把CMFCPropertyGridCtrl的初始化、屬性項構(gòu)造、事件回調(diào)、數(shù)據(jù)回流以及我踩過的坑完整過一遍。如果你正在做MFC桌面工具、節(jié)點屬性編輯、配置面板這類功能這篇文章應(yīng)該能幫你少走不少彎路。1. 為什么屬性網(wǎng)格能免掉你一半的界面代碼先說說這個控件到底解決了什么問題。做工具類軟件的時候總免不了要給一堆對象編輯屬性。比如一個繪圖程序的畫布屬性寬、高、背景色、網(wǎng)格開關(guān)、對齊方式、導(dǎo)出路徑……如果全用普通控件實現(xiàn)一個屬性一個編輯框加一個標(biāo)簽再手動處理布局屬性一多界面就亂成一團(tuán)動態(tài)增刪屬性更是噩夢。CMFCPropertyGridCtrl的工作方式更像一張屬性表每一行左側(cè)顯示屬性名右側(cè)顯示屬性值。屬性可以是文本、數(shù)字、下拉選擇、顏色、文件路徑、字體、布爾開關(guān)、甚至自定義的熱點按鈕。它內(nèi)置了編輯框、下拉列表、顏色選擇器、文件選擇對話框還帶分組和折疊功能。你只需要往里面塞屬性對象剩下的繪制和交互它自己處理。這個控件能省代碼的核心在于它把一行屬性封裝成了一個對象而不是一堆分散的控件。每個CMFCPropertyGridProperty對象自帶值、描述、類型、是否可編輯等狀態(tài)。一個幾百行代碼的編輯頁面用這個控件濃縮成幾十行AddProperty調(diào)用。另外它還支持運行時增刪屬性配置文件切換、動態(tài)模板這類需求處理起來特別順手。從源流上看CMFCPropertyGridCtrl是VS2008 SP1開始隨MFC一起發(fā)布的底層結(jié)構(gòu)和BCGControlBar Pro的屬性網(wǎng)格一脈相承。所以你在網(wǎng)上搜到BCG相關(guān)的用法放到MFC版本里大多也對得上。它在MFC九年義務(wù)教育里屬于老師沒教但工作中總碰到的控件文檔有但比較零散好用但也藏了不少需要注意的地方。2. 啟用控件前的三個隱藏前置條件2.1 視覺樣式必須提前打開這是新人最容易碰到的第一個坑控件拖上去了代碼也寫了運行出來卻是一片空白或者背景顏色不對。原因多半是沒啟用視覺樣式管理器。CMFCPropertyGridCtrl的繪制內(nèi)部依賴CMFCVisualManager必須在程序初始化階段調(diào)用BOOL CMyApp::InitInstance() { // 必須在創(chuàng)建屬性網(wǎng)格之前執(zhí)行 CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerVS2008)); // 如果你希望控件外觀跟隨系統(tǒng)主題可以換用 // CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows)); ... }這里不調(diào)用的后果就是控件能創(chuàng)建但自繪邏輯走不通表現(xiàn)通常是空白或者底色一片異常。還有一個坑是開發(fā)機(jī)如果關(guān)閉了系統(tǒng)的視覺樣式比如在某些精簡版Windows上CMFCVisualManagerVS2008的繪制會退化這時候換用CMFCVisualManagerWindows往往能救回來。2.2 消息映射用ON_REGISTERED_MESSAGE而不是ON_MESSAGE屬性網(wǎng)格的值變化通知是一條注冊消息不是普通的WM_COMMAND或WM_NOTIFY。在對話框或視圖類的消息映射里要這樣寫B(tài)EGIN_MESSAGE_MAP(CMyDialog, CDialogEx) ON_REGISTERED_MESSAGE(AFX_WM_PROPERTY_CHANGED, CMyDialog::OnPropertyChanged) // AFX_WM_PROPERTY_CHANGING 也可以注冊它在編輯器中數(shù)據(jù)修改但尚未提交時觸發(fā) END_MESSAGE_MAP() afx_msg LRESULT OnPropertyChanged(WPARAM wParam, LPARAM lParam);我把這條單獨拎出來是因為老代碼里很多人習(xí)慣性寫ON_MESSAGE結(jié)果編譯報錯或者收不到消息折騰半天。AFX_WM_PROPERTY_CHANGED的wParam是控件句柄lParam是發(fā)生變化的CMFCPropertyGridProperty指針。注意這個宏必須寫在BEGIN_MESSAGE_MAP/END_MESSAGE_MAP之間不需要額外的消息ID定義系統(tǒng)內(nèi)部已經(jīng)注冊好了。2.3 典型初始化代碼骨架控件可以資源編輯器里拖也可以動態(tài)創(chuàng)建。我習(xí)慣用動態(tài)創(chuàng)建因為屬性網(wǎng)格多半需要調(diào)整位置和大小而且動態(tài)創(chuàng)建可以隨時RemoveAll重建靈活很多。// 在對話框OnInitDialog里 CRect rectClient; GetClientRect(rectClient); m_wndPropGrid.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP, CRect(0, 0, 100, 100), this, IDC_PROP_GRID); m_wndPropGrid.EnableHeaderCtrl(TRUE); // 顯示屬性 / 值列頭 m_wndPropGrid.SetAlterColor(RGB(247, 247, 247)); // 交替行底色提升可讀性 m_wndPropGrid.SetLeftColumnWidth(160); // 左側(cè)屬性名列寬幾個細(xì)節(jié)說明。EnableHeaderCtrl(TRUE)會顯示屬性/值的分欄標(biāo)題如果你對界面空間敏感也可以關(guān)掉。SetAlterColor是交替行背景色不開也不影響功能但是屬性多的時候連續(xù)看錯行是常有的事開了之后視覺上舒服很多。SetLeftColumnWidth雖然名字看著是設(shè)置像素寬度但它有個使用時機(jī)問題后面第五部分會專門說我建議在AddProperty全部做完之后調(diào)用一次相對穩(wěn)妥。3. 屬性項的構(gòu)造與常用類型從普通文本到下拉列表3.1 基礎(chǔ)文本屬性的創(chuàng)建CMFCPropertyGridProperty有幾個重載構(gòu)造函數(shù)實際開發(fā)中最常用的就兩個一個是無描述信息的短版本另一個是帶描述和業(yè)務(wù)ID的完整版本。我強烈建議用完整版本CMFCPropertyGridProperty* pProp new CMFCPropertyGridProperty( _T(項目名稱), // 屬性顯示名稱 COleVariant(_T(默認(rèn)項目)), // 屬性值注意要包成COleVariant _T(顯示在狀態(tài)欄的說明文字), // 描述 ID_PROP_PROJECT_NAME); // 業(yè)務(wù)ID后面用GetData()取回 m_wndPropGrid.AddProperty(pProp);最后一個參數(shù)是DWORD_PTR類型的dwData這一步很多人會省覺得沒用。但實際項目里屬性一多回調(diào)里靠屬性名字符串比較會很痛苦。SetData/GetData是屬性和業(yè)務(wù)邏輯之間最直接的關(guān)聯(lián)手段傳一個枚舉或資源ID進(jìn)去后面值變化的回調(diào)處理會清爽很多。3.2 下拉選擇、顏色、文件路徑、字體屬性下拉列表用AddOption逐個添加可選項。注意默認(rèn)值是通過COleVariant((long)索引)來指定的不是傳字符串CMFCPropertyGridProperty* pMode new CMFCPropertyGridProperty( _T(工作模式), COleVariant((long)0), _T(選擇程序運行模式)); pMode-AddOption(_T(自動)); pMode-AddOption(_T(手動)); pMode-AddOption(_T(半自動)); pMode-AllowEdit(FALSE); // 禁止手動輸入只能從下拉列表選 m_wndPropGrid.AddProperty(pMode);COleVariant((long)0)對應(yīng)第一個選項。這個寫法容易踩坑的地方是有人會傳字符串自動作為默認(rèn)值但下拉屬性內(nèi)部保存的是索引而不是字符串初始化后界面可能顯示異?;蛘呤撬饕?。AllowEdit(FALSE)這個調(diào)用經(jīng)常被忽略如果允許編輯用戶就可以在框里輸入一個列表里不存在的值后面取值時你會拿到一個非法索引處理起來很麻煩。顏色屬性、文件屬性和字體屬性有專門的派生類// 顏色屬性 CMFCPropertyGridColorProperty* pColor new CMFCPropertyGridColorProperty( _T(背景色), RGB(255, 255, 255), _T(界面背景顏色)); // 文件屬性第二個參數(shù)TRUE表示保存文件對話框FALSE表示打開文件對話框 CMFCPropertyGridFileProperty* pFile new CMFCPropertyGridFileProperty( _T(導(dǎo)出路徑), TRUE, _T(C:\\out.txt), _T(txt), _T(文本文件(*.txt)|*.txt||)); // 字體屬性 CMFCPropertyGridFontProperty* pFont new CMFCPropertyGridFontProperty( _T(正文字體), _T(微軟雅黑), _T(界面正文字體)); m_wndPropGrid.AddProperty(pColor); m_wndPropGrid.AddProperty(pFile); m_wndPropGrid.AddProperty(pFont);這三類是直接從CMFCPropertyGridProperty繼承下來的AddProperty接受基類指針?biāo)圆挥米鰪娭妻D(zhuǎn)型。文件屬性的過濾器字符串格式和CFileDialog一致最后必須以||結(jié)尾這個格式寫錯的話文件對話框里看不到任何文件。3.3 用分組屬性做折疊分區(qū)屬性一多平鋪展示就不夠看了。CMFCPropertyGridCtrl的分組功能本質(zhì)上是構(gòu)造一個沒有值的父屬性再把子屬性掛到它下面CMFCPropertyGridProperty* pGroup new CMFCPropertyGridProperty(_T(基本設(shè)置)); pGroup-AddSubItem(new CMFCPropertyGridProperty( _T(名稱), COleVariant(_T(obj1)), _T(對象名稱))); pGroup-AddSubItem(new CMFCPropertyGridProperty( _T(可見), COleVariant((BOOL)TRUE), _T(是否可見))); pGroup-AddSubItem(new CMFCPropertyGridProperty( _T(透明度), COleVariant((long)50), _T(百分制))); m_wndPropGrid.AddProperty(pGroup);注意父屬性本身不要調(diào)用SetValue設(shè)置值否則會出現(xiàn)一個分組本身竟然也能編輯值的詭異效果。分組的意義在于視覺層次和折疊它自己不是一條可編輯屬性。另外分組屬性仍然可以用Enable(FALSE)整組置灰這個技巧在權(quán)限控制里很實用一個分組代表一個功能模塊禁用的時候就整組disable掉。還有一個批量操作的小經(jīng)驗動態(tài)填充大量屬性時先把SetRedraw(FALSE)掛起全部AddProperty完畢后再SetRedraw(TRUE)并Invalidate能明顯減少閃爍屬性多的時候效果尤其突出。4. 值變化回調(diào)屬性變了業(yè)務(wù)數(shù)據(jù)怎么跟上4.1 事件回調(diào)里拿到變化的是哪個屬性在消息映射里注冊了AFX_WM_PROPERTY_CHANGED之后處理函數(shù)第一件事是解析lParam確定到底是哪個屬性變了。用GetData()拿業(yè)務(wù)ID是效率最高的判斷方式LRESULT CMyDialog::OnPropertyChanged(WPARAM wParam, LPARAM lParam) { CMFCPropertyGridCtrl* pGrid (CMFCPropertyGridCtrl*)wParam; CMFCPropertyGridProperty* pProp (CMFCPropertyGridProperty*)lParam; if (pGrid nullptr || pProp nullptr) return 0; switch (pProp-GetData()) { case ID_PROP_PROJECT_NAME: // 處理項目名稱變化 break; case ID_PROP_WORK_MODE: // 處理工作模式變化 break; default: break; } return 0; }如果你當(dāng)時沒SetData那這里就只剩比較屬性名的路可走。短屬性名倒還好長英文屬性名容易寫錯而且代碼里散布一堆魔法字符串重構(gòu)屬性名的時候還不容易查全。所以回到第三部分說的從一開始就養(yǎng)成SetData的習(xí)慣。4.2 COleVariant轉(zhuǎn)C數(shù)據(jù)類型的完整套路回調(diào)里拿到CMFCPropertyGridProperty指針后用GetValue()取到的其實是一個COleVariant。不同屬性類型對應(yīng)的字段不一樣我整理了一個常用對照屬性類型取值方式說明文本屬性pProp-GetValue().bstrVal轉(zhuǎn)成CString即可Int/Long屬性pProp-GetValue().lVal注意不是intValDouble屬性pProp-GetValue().dblVal浮點數(shù)值布爾屬性pProp-GetValue().boolVal ! VARIANT_FALSEboolVal是VARIANT_BOOL下拉屬性pProp-GetValue().lVal拿到的是選項索引顏色屬性pProp-GetColor()直接返回COLORREF文件屬性pProp-GetValue().bstrVal返回路徑字符串CString接收最常見的轉(zhuǎn)換套路是這樣// 文本屬性 CString strName pProp-GetValue().bstrVal; // 布爾屬性 BOOL bVisible (pProp-GetValue().boolVal VARIANT_TRUE); // 整型屬性 int nValue (int)pProp-GetValue().lVal; // 下拉屬性需要選項字符串時 int nIndex (int)pProp-GetValue().lVal; CString strOption pProp-GetOption(); // 獲取當(dāng)前選中項文本顏色屬性和文件屬性并沒有走通用值那條路它們有自己的專用接口GetColor()返回COLORREFGetValue().bstrVal返回路徑字符串。如果你試圖從顏色屬性里取lVal結(jié)果大概率是0或者垃圾值。這個區(qū)分在最初接本文還有配套的精品資源點擊獲取