創建客戶區窗口
列表框之間項的拖拽操作
在發送繪畫(paint)消息時,系統是如何識別某個窗口的客戶區或非客戶區?當我用 ::CreateWindow 創建窗口時,如何指定客戶區矩形?
在創建窗口時不必指定客戶區,當收到 WM_NCCALCSIZE 消息時才指定客戶區。不管什麼時候,只要 Windows 想知道窗口客戶區的大小,它便會發送這個消息。在 MFC 中實現 OnNcCalcSize 處理例程。該處理函數有兩個參數,從 WPARAM 和 LPARAM 轉換而來:
void OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp);
該函數告訴應用程序是否“計算有效矩形”(稍後還要講到);NCCALCSIZE_PARAMS 結構保存三個矩形數組,第一個保存窗口的客戶區。以下是實現 OnNcCalcSize 的基本模式:
// got WM_NCCALCSIZE
void CMainFrame::OnNcCalcSize(...)
{
// do default thing (important!)
CFrameWnd::OnNcCalcSize(...);
CRect& rc = (CRect&)lpncsp->rgrc[0];
// adjust rc; eg, rc.DeflateRect(...);
}
我寫了一個小程序,NCCalc,它將標准客戶區四周收縮7個像素並將該區域繪制成 3D 外觀顏色(典型的淺灰色)。Figure 1 列出了源代碼的精華部分。重要的函數是 OnNcCalcSize,它調整客戶區矩形大小,OnNcPaint 繪制邊界。繪制代碼簡單直白,我就不再贅言。具體細節請下載源代碼。
如果你改寫主窗口的 WM_NCCALCSIZE/OnNcCalcSize,一定要確保調用基類的默認窗口處理例程,以便實現缺省處理。這樣程序一運行便會有得到默認的客戶區矩形,然後你可以調整其大小。同樣,還應該在OnNcPaint/WM_NCPAINT 中調用基類默認的處理過程。否則 Windows 不會繪制邊界,滾動欄或其它標准非客戶區元素。如果你實現自己的窗口類,像定制工具欄或調色板,其中要計算客戶區矩形並進行繪制處理,你可以不必調用基類默認的窗口過程。隨便哪種方法,當窗口收到 WM_NCPAINT 消息時,你都得負責繪制整個非客戶區。
有些人可能想知道 bCalcValidRects 以及 NCCALCSIZE_PARAMS 中的其它矩形是做什麼用的。如果讀一下文檔,你會發現 WM_NCCALCSIZE 的語義相當復雜。文檔中說如果 bCalcValidRects 為 TRUE:“應用程序應該指示客戶區的哪一部分包含有效的信息。系統將有效信息拷貝到新客戶區中指定的區域”這種情況下,“第二個[矩形]在被移走或重新調整大小之前包含該窗口客戶區的坐標”盡管所有這些描述好像夠清晰,我還是不能完全把握。我也從來沒有見過那個應用程序使用這些額外的矩形。我見過的應用程序都忽略 bCalcValidRects 並簡單地修改第一個矩形,在 NCCALCSIZE_PARAMS 中設置客戶區。
我之所以提到這個,是因為按照文檔所言,如果 WPARAM/bCalcValidRects 是 FALSE,那麼 LPARAM 不會指向 NCCALCSIZE_PARAMS 結構,而是單個的 RECT,客戶區,然而 MFC 在所有情況中將 LPARAM 強制轉換為 NCCALCSIZE_PARAMS。這似乎是個bug,雖然只要你僅修改 NCCALCSIZE_PARAMS 中的第一個矩形,你的程序是絕不會垮掉的。我運行了一些測試程序,確定當第一次創建窗口時,Windows 僅有一次用 bCalcValidRects=FALSE 來發送 WM_NCCALCSIZE。隨後,不論窗口如何調整大小,Windows 都用 bCalcValidRects=TRUE 來發送 WM_NCCALCSIZE。你必須對兩種情況都設置客戶區,以便正常顯示你的窗口。
唉,恐怕到現在我都沒有闡明 bCalcValidRects 到底是做什麼用的。微軟的大佬們也沒有給出足夠文檔來說明這個神秘的參數,只是說它是從 Windows 3.1 遠古時期延續下來的。如今只要你始終對兩種情況都作處理,並且只修改第一個矩形,一切都會OK。
我正在做一個商業棒球游戲程序。在我的用戶界面中,我想讓用戶能在兩個列表框之間實現拖拽操作。 MFC 有沒有簡單的方法來做到這一點?
針對這種情況,MFC 沒有內建的處理方法,但用兩種方法可以實現你的要求。COM 有其自己的接口來處理應用程序之間常規的拖拽操作。它需要實現幾個接口:IDropTarget,IDropSource 和 IDataObject,此外還有一個函數,DoDragDrop 實現具體的拖拽操作。使用 COM 來實現拖拽操作需要編寫大量的代碼,這樣有可能超出了能力所及。再說使用 COM 來實現這個功能也有些誇張,因為 COM 本身是專門設計用來以更一般的方式實現應用程序之間的交互。如果你僅僅是想在應用程序中將一個項目從一個控件拖拽到另一個控件,那麼自己編寫代碼來的更簡單和容易。
我寫了一個小類庫,其中包含一個類,CDragDropMgr,用這個類可以在自己的應用程序窗口間添加拖拽行為。我還寫了一個測試程序,DDTest,示范了如何使用 CDragDropMgr 類(參見 Figure 2)。Figure 3 是程序運行的畫面。DDTest 有兩個列表框和一個編輯框。你可以將第一個列表框中的項目拖拽到第二個列表框,或者編輯框。此外,你還能在第二個列表框裡通過拖拽重排項目。DDTest 就是使用 CDragDropMgr 來實現上述這些功能的。下面我首先示范如何使用 CDragDropMgr,然後在探討它的工作原理。
Figure 3 運行中的 DDTest
為了使用拖拽管理器,首先要在主窗口或對話框中實例化 CDragDropMgr,然後用一個表對之進行初始化,就像下面的代碼這樣:
static DRAGDROPWND MyDragDropWindows[] = {
{ IDC_LIST1, DDW_SOURCE },
{ IDC_LIST2, DDW_SOURCE|DDW_TARGET },
{ IDC_EDIT1, DDW_TARGET },
{ 0, 0 },
};
m_ddm.Install(this, MyDragDropWindows);
我的專欄的愛好者們知道我編程的五大秘訣之一便是 一張表勝過一千行代碼。表的形式比長長的一串過程代碼更加簡練、優雅、可靠和可維護。在本文的例子中,表告訴拖拽管理器哪個子窗口是拖拽操作的源和/或目標。每一個表入口都有一個子窗口ID以及一個 DDW_SOURCE 和 DDW_TARGET 的組合標志。在 DDTest 中,第一個列表框是源,第二個列表框既可以是源,也可以是目標,編輯框只能是目標。但是不管怎麼樣,不要忘了在表的末尾加上 NULL!http://bianceng.cn(編程入門)
一旦你用窗口表對拖拽管理器進行了初始化,下一步便是改寫主窗口的 PreTranslateMessage 函數以便將消息傳遞給拖拽管理器:
BOOL CMyDlg::PreTranslateMessage(MSG* pMsg)
{
return m_ddm.PreTranslateMessage(pMsg) ? TRUE :
CDialog::PreTranslateMessage(pMsg);
}
一切准備就緒,當用戶試圖從一個窗口到另一個窗口實施拖拽操作時,拖拽管理器便會察覺到並通知應用程序要做相應的處理。CDragDropMgr 可以發送四個消息/通知:WM_DD_DRAGENTER、WM_DD_DRAGOVER、WM_DD_ DRAGDROP 和 WM_DD_DRAGABORT。收到這些消息/通知後,做什麼樣的處理由你來決定。WM_DD_DRAGENTER 和 WM_DD_DRAGDROP 是拖拽操作必須要做的處理。其它兩個可選。WM_DD_DRAGABORT 用來處理用戶取消操作時的清除工作。WM_DD_ DRAGOVER 使你能夠在用戶實施拖拽操作而移動鼠標時進行連續不斷的處理。DDTest 這樣簡單的程序不需要處理這些消息。
當拖拽管理器發送 WM_DD_DRAGENTER 消息時,它在 LPARAM 中傳遞一個 DRAGDROPINFO 結構。你的任務是將 DRAGDROPINFO::data 指向一個包含你想要拖拽數據的 CDragDropData 實例,然後返回 TRUE。如果拖拽是不允許的(也許用戶單擊了列表框中的某個死區(dead area))則應該返回 FALSE,並不要設置 DRAGDROPINFO::data,CDragDropData 類似 COM 的 IDataObject,但要簡單得多:它保存擬拽動的數據。CDragDropData 有三個虛擬函數:OnGetDragSize 獲得一個拖拽圖像的綁定矩形,OnDrawData 繪制拖拽圖像,OnGetData 獲取數據本身。我在我的庫中提供了一個叫 CDragDropText 的類,它實現了這些函數,用來處理文本拖拽操作。它將文本保存在一個 CString 中。OnGetData 返回這個串,OnGetDragSize 計算該文本矩形,OnDrawData 則繪制該文本:
void CDragDropText::OnDrawData(CDC& dc, CRect& rc)
{
dc.DrawText(m_text, &rc, DT_LEFT|DT_END_ELLIPSIS);
}
如果你想得到這個文本,你唯一需要調用的函數是 OnGetData,拖拽管理器需要時在其內部調用 OnGetDragSize 和 OnDrawData。
那麼所有這些工作是如何實現的呢?當 DDTest 收到 WM_DD_DRAGENTER 消息,它調用一個內部函數 GetLBItemUnderPt 來確定光標下是哪個列表框(如果有的話)。然後 DDTest 以這一項的文本作為數據創建一個 CDragDropText 對象並將 DRAGDROPINFO 中的 data 指針指向該對象:
// in CMyDlg::OnDragEnter
DRAGDROPINFO& ddi = *(DRAGDROPINFO*)lp;
int item = GetLBItemUnderPt(...);
if (item>=0) {
CString text = // get item text
ddi.data = new CDragDropText(text);
return TRUE; // allow drag-drop
}
return FALSE; // nothing to drag
由 CDragDropMgr 來做剩余的工作。當用戶拖拽它時在周邊繪制文本並根據光標是否出於拖拽目的地上方而相應地改變鼠標光標。
當用戶松開鼠標,CDragDropMgr 便給應用程序發送一個 WM_DD_DRAGDROP 消息。暗示數據已經拖拽完成。對於 DDTest 而言,這意味著如果鼠標出於編輯框上方,則要設置編輯框中的文本,或者如果鼠標是在列表框上方,則要將文本添加到列表框中。在真正實現中,DDTest 稍顯復雜,因為它可以讓用戶重新安排第二個列表框中的項目。DDTest 有代碼可以察覺是否需要添加文本或修改列表框中文本的位置。具體細節就留給你來做了,OnDragDrop 實現的基本要點都是一樣的:
// OnDragDrop handler
DRAGDROPINFO& ddi = *(DRAGDROPINFO*)lp;
void* data = ddi.data->OnGetData();
// do something with data
return 0;
以上都是關於文本的操作,如果要拖拽其它類型的數據怎麼辦呢?為此,你必須通過 CDragDropData 派生並改寫三個基本函數來擴展我的庫。例如,為了拖拽圖像,你得派生一個 CDragDropImage 類,在這個類中,OnGetData 返回 BITMAP 或 CBitmap,OnGetDragSize 返回位圖的尺寸,OnDrawData 調用 BltBit 或其它什麼函數來繪制該位圖。
我已經示范了 CDragDropMgr 的使用方法,但它是如何工作的呢?基本思路很簡單。CDragDropMgr::PreTranslateMessage 查找發送到拖拽源窗口之一的鼠標消息並發送相應的通知到你的應用程序主窗口。CDragDropMgr 實現了一個典型的具有三種狀態的有限狀態機:NONE、CAPTURED 和 DRAGGING。當用戶按下鼠標鍵,CDragDropMgr 進入 CAPTURED 狀態。當用戶移動鼠標,則進入 DRAGGING 狀態。具體細節簡單直白。
CDragDropMgr 使用 PreTranslateMessage 而不是子類化主窗口,因為它需要解釋發送到可能的拖拽源窗口之一鼠標消息,該拖拽源窗口由前述的拖拽窗口表確定。MFC 的優點之一是它在主窗口中僅通過虛擬 PreTranslateMessage 方法便可以過濾所有子窗口消息。這使得 CDragDropMgr 可以僅在單一的函數中便可截獲發送到任何潛在拖拽源窗口的鼠標消息,從而避免了必須子類化每一個窗口。當 CDragDropMgr::PreTranslateMessage 看到 WM_LBUTTONDOWN,它便查找該窗口句柄(HWND)以便檢查它是否被列入源窗口表。如果它是一個源窗口,則進行拖拽初始化,否則忽略該消息。
拖拽數據的機制是很簡單直白的,甚至有些單調無趣,所以細節我就不再贅言。唯一一個亮點是 CDragDropData 使用 CImageList 來繪畫。如果你實現自己的拖拽管理器,我鼓勵你也這麼做,CImageList 包含如下幾個函數:BeginDrag、DragEnter、DragMove 和 EndDrag,用它們可以很快解決比特繪畫問題,它們使用特有的光柵操作使圖像呈半透明,從其以前位置擦除等等。
沒有 CImageList,這些繪制細節冗長乏味。有了它,CDragDropMgr 只要將拖拽圖像繪制到圖像列表位圖一次即可。當用戶初始化拖拽操作時,CDragDropMgr 通知主應用程序,該主應用程序將 DRAGDROPINFO::data 設置為一個 CDragDropData,正如我前面描述的那樣。然後拖拽管理器調用 CDragDropData:: CreateDragImage (參見 Figure 4),它創建一個包含要繪制的圖像列表。CreateDragImage 調用虛擬函數 CDragDropData::OnGetDragSize 來獲取拖拽圖像的尺寸,CDragDropData::OnDrawData 將數據繪制到圖像列表的位圖中。一旦完成了些工作,CDragDropMgr 調用圖像列表函數繪制拖拽期間的圖像。例如,每次用戶移動鼠標,CDragDropMgr 都調用 CImageList::DragMove。還有比這更容易的嗎?其優美之處在於這個代碼完全是通用的。為了處理新的數據類型,你只要實現 OnGetDragSize 和 OnDrawData 即可。