我為了移動一個無標題欄的窗體,使用了WM_NCHITTEST消息,這個消息大概如下:
通常,我們拖動對話框窗口的標題欄來移動窗口,但有時候,我們想通過鼠標在客戶區上拖動來移動窗口。
一個容易想到的方案是,處理鼠標消息WM_LBUTTONDOWN和WM_LBUTTONUP。在OnLButtonUp函數中計算鼠標位置的變化,調用MoveWindow實現窗口的移動。
注意,拖動標題欄移動窗口的時候,會出現一個矩形框,它提示了窗口移動的當前位置。當鼠標左鍵放開的時候,窗口就移動到矩形框所在位置。而我們的實現方案中沒有這個功能。
要實現此功能,我們必須自己來畫這些矩形。
事實上,我們沒有必要自己來做這件事情,因為Windows已經給我們做好了。
試想,如果我能夠欺騙Windows,告訴它現在鼠標正在拖動的是標題欄而不是客戶區,那么窗口移動操作就由Windows來代勞了。
要欺騙Windows並不像想像中的困難,甚至非常簡單。
我們利用一個消息:WM_NCHITTEST。
MSDN對它的解釋是:
The WM_NCHITTEST message is sent to a window when the cursor moves, or when a mouse button is pressed or released. If the mouse is not captured, the message is sent to the window beneath the cursor. Otherwise, the message is sent to the window that has captured the mouse.
這個消息是當鼠標移動或者有鼠標鍵按下時候發出的。
Windows用這個消息來做什么? “HITTEST”就是“命中測試”的意思,WM_NCHITTEST消息用來獲取鼠標當前命中的位置。
WM_NCHITTEST的消息響應函數會根據鼠標當前的坐標來判斷鼠標命中了窗口的哪個部位,消息響應函數的返回值指出了部位,例如它可能會返回HTCAPTION,或者HTCLIENT等。(其返回值有很多,請查閱MSDN)。
為了便於理解,我先描述一下Windows對鼠標鍵按下的響應流程:
1. 確定鼠標鍵點擊的是哪個窗口。Windows會用表記錄當前屏幕上各個窗口的區域坐標,當鼠標驅動程序通知Windows鼠標鍵按下了,Windows根據鼠標的坐標確定它點擊的是哪個窗口。
2. 確定鼠標鍵點擊的是窗口的哪個部位。Windows會向鼠標鍵點擊的窗口發送WM_NCHITTEST消息,來詢問鼠標鍵點擊的是窗口的哪個部位。(WM_NCHITTEST的消息響應函數的返回值會通知Windows)。通常來說,WM_NCHITTEST消息是系統來處理的,用戶一般不會主動去處理它(也就是說,WM_NCHITTEST的消息響應函數通常采用的是Windows默認的處理函數)。
3. 根據鼠標鍵點擊的部位給窗口發送相應的消息。例如:如果WM_NCHITTEST的消息響應函數的返回值是HTCLIENT,表示鼠標點擊的是客戶區,則Windows會向窗口發送WM_LBUTTONDOWN消息;如果WM_NCHITTEST的消息響應函數的返回值不是HTCLIENT(可能是HTCAPTION、HTCLOSE、HTMAXBUTTON等),即鼠標點擊的是非客戶區,Windows就會向窗口發送WM_NCLBUTTONDOWN消息。
我們有必要詳細討論一下:如果WM_NCHITTEST的消息響應函數的返回值是HTCAPTION,即指示了鼠標點擊了標題欄,接下去Windows的處理是怎樣的?
上面已經提到,接下來,Windows會向窗口發送WM_NCLBUTTONDOWN消息。
MSDN對WM_NCLBUTTONDOWN消息描述如下:
WM_NCLBUTTONDOWN
nHittest = (INT) wParam; // hit-test value
pts = MAKEPOINTS(lParam); // position of cursor
WM_NCLBUTTONDOWN的wParam指示了鼠標點擊的窗口部位,lParam指示了當前鼠標的坐標。
如果應用程序沒有對該消息響應,則由系統默認處理。
系統默認處理又是怎樣的呢?系統發現wParam指示了鼠標點擊的是標題欄,就會標識當前窗口處於“拖拽狀態”(Windows內部記錄了每個窗口的狀態信息)。由於標識了“拖拽狀態”,則從此刻起到鼠標鍵放開之前,你的鼠標移動狀況完全由Windows跟蹤。它根據鼠標的移動,使得窗口作“同步”移動。
注意,這個過程中,窗口不會收到WM_NCMOUSEMOVE消息,因為窗口和鼠標是“同步”移動的,你的鼠標相對於窗口是靜止的。
但問題同時也出現了, 我想在右鍵這個窗體的時候彈出一個菜單, 當我完成 MSG_WM_RBUTTONDOWN 這個消息的時候,發現窗體收不到這個消息, 將WM_NCHITTEST消息的實現去掉就可以了,看了一原因是:
因為你在WM_NCHITTEST中處理了鼠標消息,把他定位成HTCAPTION,也就是鼠標在標題欄上,而標題欄屬於非客戶區(NC);
非客戶區的事件消息都是以WM_NC開頭的。也就是說,當你的WM_NCHITTEST返回HTCAPTION時,原來可以用WM_LBUTTONUP處理的消息,你只能用WM_NCLBUTTONUP來處理。
解決方法:
同時處理WM_NCHITTEST和WM_NCRBUTTONUP,而不處理WM_RBUTTONUP
http://www.cnblogs.com/GnagWang/archive/2010/09/12/1824394.html
-----------------------------------------------------------------------
· HTBORDER 在不具有可變大小邊框的窗口的邊框上。
· HTBOTTOM 在窗口的水平邊框的底部。
· HTBOTTOMLEFT 在窗口邊框的左下角。
· HTBOTTOMRIGHT 在窗口邊框的右下角。
· HTCAPTION 在標題條中。
· HTCLIENT 在客戶區中。
· HTERROR 在屏幕背景或窗口之間的分隔線上(與HTNOWHERE相同,除了Windows的DefWndProc函數產生一個系統響聲以指明錯誤)。
· HTGROWBOX 在尺寸框中。
· HTHSCROLL 在水平滾動條上。
· HTLEFT 在窗口的左邊框上。
· HTMAXBUTTON 在最大化按鈕上。
· HTMENU 在菜單區域。
· HTMINBUTTON 在最小化按鈕上。
· HTNOWHERE 在屏幕背景或窗口之間的分隔線上。
· HTREDUCE 在最小化按鈕上。
· HTRIGHT 在窗口的右邊框上。
· HTSIZE 在尺寸框中。(與HTGROWBOX相同)
· HTSYSMENU 在控制菜單或子窗口的關閉按鈕上。
· HTTOP 在窗口水平邊框的上方。
· HTTOPLEFT 在窗口邊框的左上角。
· HTTOPRIGHT 在窗口邊框的右上角。
· HTTRANSPARENT 在一個被其它窗口覆蓋的窗口中。
· HTVSCROLL 在垂直滾動條中。
· HTZOOM 在最大化按鈕上。
http://blog.csdn.net/harvic880925/article/details/9785439
http://zhouqingfeidie.blog.163.com/blog/static/301717722011112005828716/
---------------------------------------------------------------------
VCL里的使用情況:
procedure TControl.CMHitTest(var Message: TCMHitTest); begin Message.Result := HTCLIENT; // 凡是轉發到TControl的,都是位於客戶區 end; procedure TWinControl.WMNCHitTest(var Message: TWMNCHitTest); begin with Message do if (csDesigning in ComponentState) and (FParent <> nil) then Result := HTCLIENT // 組件處於設計狀態時,都算作位於客戶區 else inherited; end; procedure TWinControl.WndProc(var Message: TMessage); var Form: TCustomForm; begin case Message.Msg of WM_SETFOCUS: begin Form := GetParentForm(Self); if (Form <> nil) and not Form.SetFocusedControl(Self) then Exit; end; WM_KILLFOCUS: if csFocusing in ControlState then Exit; WM_NCHITTEST: begin inherited WndProc(Message); // 一般情況下委托給父類函數處理 if (Message.Result = HTTRANSPARENT) and (ControlAtPos(ScreenToClient( SmallPointToPoint(TWMNCHitTest(Message).Pos)), False) <> nil) then Message.Result := HTCLIENT; // 但如果父類的處理結果是透明,那么就重新測試,並且算位於客戶區 Exit; end; WM_MOUSEFIRST..WM_MOUSELAST: begin if Message.Msg = WM_LBUTTONUP then begin tag := 50; end; if IsControlMouseMsg(TWMMouse(Message)) then begin { Check HandleAllocated because IsControlMouseMsg might have freed the window if user code executed something like Parent := nil. } if (Message.Result = 0) and HandleAllocated then DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam); Exit; end; end; WM_KEYFIRST..WM_KEYLAST: if Dragging then Exit; WM_CANCELMODE: if (GetCapture = Handle) and (CaptureControl <> nil) and (CaptureControl.Parent = Self) then CaptureControl.Perform(WM_CANCELMODE, 0, 0); end; inherited WndProc(Message); end; procedure TWinControl.WMSetCursor(var Message: TWMSetCursor); var Cursor: TCursor; Control: TControl; P: TPoint; begin with Message do if CursorWnd = FHandle then case Smallint(HitTest) of // 其中HitTest是TWMSetCursor消息結構體自帶的Word類型數據 HTCLIENT: // 如果位於客戶區,則進行一系列處理,包括改變光標和進一步測試 begin Cursor := Screen.Cursor; if Cursor = crDefault then begin GetCursorPos(P); Control := ControlAtPos(ScreenToClient(P), False); if (Control <> nil) then if csDesigning in Control.ComponentState then Cursor := crArrow else Cursor := Control.FCursor; if Cursor = crDefault then if csDesigning in ComponentState then Cursor := crArrow else Cursor := FCursor; end; if Cursor <> crDefault then begin Windows.SetCursor(Screen.Cursors[Cursor]); Result := 1; Exit; end; end; HTERROR: // 如果測試出錯,則把當前程序設置為最前窗口,並且退出測試(以利於進一步測試) if (MouseMsg = WM_LBUTTONDOWN) and (Application.Handle <> 0) and (GetForegroundWindow <> GetLastActivePopup(Application.Handle)) then begin Application.BringToFront; Exit; end; end; inherited; end; procedure TWinControl.CMCursorChanged(var Message: TMessage); var P: TPoint; begin if GetCapture = 0 then begin GetCursorPos(P); if FindDragTarget(P, False) = Self then Perform(WM_SETCURSOR, Handle, HTCLIENT); // 當移動目的控件是當前Win控件的時候,就發送設置焦點的消息,並且傳遞Handle和HTCLIENT做進一步的處理 end; end;
其中還有對CM_HITTEST的處理情況:
function TWinControl.ControlAtPos(const Pos: TPoint; AllowDisabled, AllowWinControls: Boolean): TControl; var I: Integer; P: TPoint; LControl: TControl; function GetControlAtPos(AControl: TControl): Boolean; begin with AControl do begin P := Point(Pos.X - Left, Pos.Y - Top); Result := PtInRect(ClientRect, P) and ((csDesigning in ComponentState) and (Visible or not (csNoDesignVisible in ControlStyle)) or (Visible and (Enabled or AllowDisabled) and (Perform(CM_HITTEST, 0, Longint(PointToSmallPoint(P))) <> 0))); // 在VCL內部測試,當前測試點是否位於當前控件區域(利用了函數返回值) if Result then LControl := AControl; end; end; begin LControl := nil; if AllowWinControls and (FWinControls <> nil) then for I := FWinControls.Count - 1 downto 0 do if GetControlAtPos(FWinControls[I]) then Break; if (FControls <> nil) and (LControl = nil) then for I := FControls.Count - 1 downto 0 do if GetControlAtPos(FControls[I]) then Break; Result := LControl; end;
還需進一步總結。。。