前段时间在论坛里看了一篇关于剖析VCL结构的文件,其中不少高手的开怀畅谈让小辈们心里感觉非常的痛快!看完余又觉得不能光看,也该将自己的心得拿出来与大家分享,于是就边夜翻看VCL源码,终于将VCL如何实现DragDrop功能的过程弄个“基本明白”,其中可能会有不当之处,再加上小弟的文学水平也只是初中毕业,有些地方也许会表达不当,但其意思也基本上八九不离十了,故也请大家开怀畅言、批评指正,都是为了进步嘛!哈哈……
虽然DragDock操作与DragDrop操作是密切相关,并且很大一部分操作是相同的,但本文暂且不讨论与DragDock有关的部分,留待下回分解或也给大家表现表现………………
一、与DragDrop操作相关的属性、事件、函数
VCL的DragDrop功能是在TControl类中现的,因此所有从TControl类派生出来的控件类者继承了这些属性、事件和函数,包括:
属性:DragCursor: Drag时的鼠标类型:(TCursor); DragKind: Drag的类型:(dkDrag, dkDock); DragMode: Drag的方式:手动(dmManual)或自动(dmAutomatic);
事件:OnStartDrag:Drag开始事件; OnDragOver: Drag经过某个控件; OnDragDrop: Drag到某个控件并放开; OnEndDrag: Drag动作结束;
函数:BeginDrag: 开始控件的Drag动作; Dragging: 返回控件是否正被Dragging; CancelDrag: 取消正在执行的Drag操作; EndDrag: 结束正在执行的Drag操作,与CancelDrag不同,EndDrag允许操作指定是否产生Drop操作(由Drop参数决定)。
此外还有一些与DragDrop相关的函数,在随后的介绍中将逐一说明。
二、DragDrop操作产生与执行的过程
1、自动产生过程。
我们知道在控件上单击鼠标左键时便会产生WM_LBUTTONDOWN消息,TControl类的WinProc消息处理方法捕捉到该消息时,便判断控件的DragMode是否为dmAutomatic,即是否自动执行DragDrop操作,如果是则调用类保护函数BeginAutoDrag,立即进入DragDrop状态,详见下面代码:
procedure TControl.WndProc(var Message: TMessage); begin ... case Message.Msg of WM_LBUTTONDOWN, WM_LBUTTONDBLCLK: begin if FDragMode = dmAutomatic then begin BeginAutoDrag; // 进行DragDrop操作 Exit; end; Include(FControlState, csLButtonDown); end; ... else ... end; ... end;
procedure TControl.BeginAutoDrag; begin BeginDrag(Mouse.DragImmediate, Mouse.DragThreshold); end;
从上面代码可知它只是简单的调用了BeginDrag函数,具体开始DragDrop是由BeginDrag函数执行的。
2、手动产生过程。
当DragMode为dmManual时,将由程序在代码中显式调用BeginDrag方法产生。如:
procedure TForm1.Panel1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Panel1.BeginDrag(True, -1); end;
3、BeginDrag函数
分析前请先留意在 Controls 单元中声明的几个全局变量: var DragControl: TControl; // 被Drag的控件 DragObject: TDragObject; // 管理整个DragDrop过程的TDragObject对象 DragInternalObject: Boolean; // TDragObject对象是否由内部创建 DragCapture: HWND; // 管理DragDrop过程的Wnd实例句柄 DragStartPos: TPoint; // Drag开始时的鼠标位置 DragSaveCursor: HCURSOR; // Drag开始的的鼠标类型 DragThreshold: Integer; // Drag操作延迟位置 ActiveDrag: TDragOperation; // 正在执行的Drag操作:(dopNone, dopDrag, dopDock); DragImageList: TDragImageList; // Drag过程中代替鼠标显示的图像列表
BeginDrag的函数原型声明为: procedure BeginDrag(Immediate: Boolean; Threshold: Integer = -1);
参数: Immediate:是否直接进入DragDrop状态; Threshold:若Immediate参数为False,当鼠标移动量超过Threshold给出的值时进入DragDrop状态;
且先看其实现代码: procedure TControl.BeginDrag(Immediate: Boolean; Threshold: Integer); var P: TPoint; begin // DragDrop操作的对象不允许是窗体
if (Self is TCustomForm) and (FDragKind <> dkDock) then raise EInvalidOperation.CreateRes(@SCannotDragForm);
// 前面提过暂且不讨论DragDock相关部分,所以对CalcDockSizes的函数调用不作分析。 CalcDockSizes;
// DragControl 不为 nil 或 Pointer($FFFFFFFF) 说明已经进入了DragDrop状态 // 这里的判断避免了递归调用
if (DragControl = nil) or (DragControl = Pointer($FFFFFFFF)) then begin DragControl := nil;
// 如果被Drag控件处于鼠标按下状态(如前面的手动产生方式)时应先清除其状态 // if csLButtonDown in ControlState then begin GetCursorPos(P); P := ScreenToClient(P); Perform(WM_LBUTTONUP, 0, Longint(PointToSmallPoint(P))); end;
{ 如果传递的Threshold变量小于0,则使用系统默认的值 } if Threshold < 0 then Threshold := Mouse.DragThreshold; // 以Pointer($FFFFFFFF)为标志防止在BeginDrag中调用EndDrag if DragControl <> Pointer($FFFFFFFF) then DragInitControl(Self, Immediate, Threshold); // !!!!!! end;
end;
在BeginDrag的最后一行代码,由TControl类转入全局函数DragInitControl中。函数DragInitControl、DragInit、DragTo、DragDone共同组成了DragDrop核心与VCL类的交互接口。
4、DragInitControl、DragInit函数
DragInitControl函数接收了BeginDrag函数的Immediate和Threshold参数,还多了一个Control参数,该参数但是被Drag的控件。下面来看DragInitControl函数的实现代码:
procedure DragInitControl(Control: TControl; Immediate: Boolean; Threshold: Integer); var DragObject: TDragObject; StartPos: TPoint; begin DragControl := Control; try DragObject := nil; DragInternalObject := False; if Control.FDragKind = dkDrag then begin Control.DoStartDrag(DragObject); // 产生StartDrag事件 if DragControl = nil then Exit; if DragObject = nil then begin DragObject := TDragControlObjectEx.Create(Control); DragInternalObject := True; end end else begin ... // DragDock控件部分 end; DragInit(DragObject, Immediate, Threshold); except DragControl := nil; raise; end; end;
DragInitControl函数只是简单地进行一些判断然后调用TControl的DoStartDrag函数(该函数产生的OnStartDrag事件)并创建TDragControlObjectEx对象,就直接进入了DragInit函数,也就是真正由VCL控件类进入DragDrop管理核心的部分。 TDragControlObjectEx的内部保存了被Drag的控件及执行DragDrop的所需的其他参数,该类的实现及内部功能我们稍候再介绍。
DragInit函数接收的实现代码:
procedure DragInit(ADragObject: TDragObject; Immediate: Boolean; Threshold: Integer); begin // 在全局变量中保存参数 DragObject := ADragObject; DragObject.DragTarget := nil; GetCursorPos(DragStartPos); DragObject.DragPos := DragStartPos; DragSaveCursor := Windows.GetCursor;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
DragCapture := DragObject.Capture; // 启动DragDrop管理核心
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
DragThreshold := Threshold;
if ADragObject is TDragDockObject then begin ... // DragDock控制部分 end else begin if Immediate then ActiveDrag := dopDrag // 直接进入DragDrop操作 else ActiveDrag := dopNone; end;
// -> 以下部分可以忽略 DragImageList := DragObject.GetDragImages; if DragImageList <> nil then with DragStartPos do DragImageList.BeginDrag(GetDeskTopWindow, X, Y); QualifyingSites := TSiteList.Create; // <-
if ActiveDrag <> dopNone then DragTo(DragStartPos); end;
到此,便完全由TDragControlObjectEx(由全局变量DragObject保存)控制整个DragDrop操作;当DragObject检测到鼠标移动消息(WM_MOUSEMOVE)时,便会调用DragTo函数;DragTo函数查找鼠标所在位置的VCL控件,并产生DragOver事件。 5、DragTo函数
procedure DragTo(const Pos: TPoint);
function GetDropCtl: TControl; begin ... end;
var DragCursor: TCursor; // Target: TControl; // 鼠标所在位置(Pos)的VCL控件 TargetHandle: HWND; // 控件的句柄 DoErase: Boolean; // 是否执行擦除背景操作 begin // 只有当Drag操作为dopDrag或dopDock,或鼠标移动量大于Threshold(传递给BeginDrag的值)时, // 才执行后面的操作 if (ActiveDrag <> dopNone) or (Abs(DragStartPos.X - Pos.X) > = DragThreshold) or (Abs(DragStartPos.Y - Pos.Y) > = DragThreshold) then begin
// 查找鼠标当前位置的VCL控件 Target := DragFindTarget(Pos, TargetHandle, DragControl.DragKind, DragControl);
// -> // 如果尚未开始Drag,则初始化图像列表为Dragging状态 if (ActiveDrag = dopNone) and (DragImageList <> nil) then with DragStartPos do DragImageList.BeginDrag(GetDeskTopWindow, X, Y); // <-
if DragControl.DragKind = dkDrag then begin ActiveDrag := dopDrag; DoErase := False; // Drag操作只改变鼠标形状,不需要迫擦除移动框的背景 end else begin ... end;
// 如果鼠标位置移动前后所在的VCL控件不同
if Target <> DragObject.DragTarget then begin DoDragOver(dmDragLeave); // 原来的控件产生DragOver(dmDragLeave[离开])事件 if DragObject = nil then Exit; DragObject.DragTarget := Target; DragObject.DragHandle := TargetHandle; DragObject.DragPos := Pos; DoDragOver(dmDragEnter); // 新位置的控件产生DragOver(dmDragEnter[进入])事件 if DragObject = nil then Exit; end;
// 计算Drag的当前位置 DragObject.DragPos := Pos; if DragObject.DragTarget <> nil then DragObject.DragTargetPos := TControl(DragObject.DragTarget).ScreenToClient(Pos);
// 获取Drag操作的鼠标形状 // 注意GetDragCursor的参数,它的参数正在DragOver(dmDragMove[移动])事件的返回值 DragCursor := TDragObject(DragObject).GetDragCursor(DoDragOver(dmDragMove), Pos.X, Pos.Y);
//-〉 可以暂时忽略 if DragImageList <> nil then begin if (Target = nil) or (csDisplayDragImage in Target.ControlStyle) then begin DragImageList.DragCursor := DragCursor; if not DragImageList.Dragging then DragImageList.BeginDrag(GetDeskTopWindow, Pos.X, Pos.Y) else DragImageList.DragMove(Pos.X, Pos.Y); end else begin DragImageList.EndDrag; Windows.SetCursor(Screen.Cursors[DragCursor]); end; end; // 〈-
Windows.SetCursor(Screen.Cursors[DragCursor]);
if ActiveDrag = dopDock then begin ... // DragDock相关部分 end; end; end;
从代码中,我们可以看出DragTo函数的工作分为两个部分:一是判断是否已经进入了Drag状态中,否则检查是否满足进入Drag状态的条件;二是查找鼠标当前位置的VCL控件,判断鼠标前后位置所在的VCL控件,并产生相应的事件。
当DragObject检测到鼠标放开消息(WM_LBUTTONUP, WM_RBUTTONUP)或ESC键按下消息(CN_KEYDOWN + K_ESCAPE)时,调用DragDone函数结束Drag操作。 6、DragDone函数
DragDone函数接收一个Drop参数,该参数指明是否使目标控件产生DragDrop事件
procedure DragDone(Drop: Boolean);
// -> DragDock相关部分 function CheckUndock: Boolean; begin Result := DragObject.DragTarget <> nil; with DragControl do if Drop and (ActiveDrag = dopDock) then if Floating or (FHostDockSite = nil) then Result := True else if FHostDockSite <> nil then Result := FHostDockSite.DoUnDock(DragObject.DragTarget, DragControl); end; // <-
var DockObject: TDragDockObject; Accepted: Boolean; // 目标控件是否接受DragDrop操作 DragMsg: TDragMessage; TargetPos: TPoint; // ParentForm: TCustomForm; begin DockObject := nil; Accepted := False;
// 防止递归调用 // 检查DragObject的Canceling属性,如为真则直接退出 if (DragObject = nil) or DragObject.Cancelling then Exit;
try DragSave := DragObject; // 保存当前DragDrop控制对象 try DragObject.Cancelling := True; // 设置Cancelling标志,表示正在执行DragDone操作 DragObject.FDropped := Drop; // 在目标控件上释放标志
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! DragObject.ReleaseCapture(DragCapture); // 停止DragDrop管理核心 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if ActiveDrag = dopDock then begin ... // DragDock相关部分 end;
// 取得Drag的位置 if (DragObject.DragTarget <> nil) and (TObject(DragObject.DragTarget) is TControl) then TargetPos := DragObject.DragTargetPos else TargetPos := DragObject.DragPos;
// 目标控件是否接受Drop操作 // 当Drag操作为dopDrag时,目标控件产生DoDragOver(dmDragLeave[离开])事件 // 若传递给DragDone的Drop参数为False时,Accepted恒为False Accepted := CheckUndock and (((ActiveDrag = dopDock) and DockObject.Floating) or ((ActiveDrag <> dopNone) and DoDragOver(dmDragLeave))) and Drop;
if ActiveDrag = dopDock then begin ... // DragDock相关操作 end else begin // -> if DragImageList <> nil then DragImageList.EndDrag else Windows.SetCursor(DragSaveCursor); // <- end;
DragControl := nil; DragObject := nil;
if Assigned(DragSave) and (DragSave.DragTarget <> nil) then begin DragMsg := dmDragDrop; // 产生DragDrop事件 if not Accepted then // 如果Accepted为False,则不产生DragDrop事件 begin // 实际上在VCL中没有处理dmDragCancel的相关代码 DragMsg := dmDragCancel; // 即dmDragCancel只是一个保留操作 DragSave.FDragPos.X := 0; DragSave.FDragPos.Y := 0; TargetPos.X := 0; TargetPos.Y := 0; end; DragMessage(DragSave.DragHandle, DragMsg, DragSave, DragSave.DragTarget, DragSave.DragPos); end; finally // -> QualifyingSites.Free; QualifyingSites := nil; // <-
if Assigned(DragSave) then begin DragSave.Cancelling := False; DragSave.Finished(DragSave.DragTarget, TargetPos.X, TargetPos.Y, Accepted); // 产生EndDrag事件 end;
DragObject := nil; end; finally DragControl := nil; if Assigned(DragSave) and ((DragSave is TDragControlObjectEx) or (DragSave is TDragObjectEx) or (DragSave is TDragDockObjectEx)) then DragSave.Free; ActiveDrag := dopNone; end; end;
至此,与DragDrop核心的接口函数已介绍完毕;我们留意到在这些几个函数中还调用了DragFindTarget、DoDragOver、DragMessage几个函数,这些函数的源码在Control.pas中,功能分别如下:
DragFindTarget:(const Pos: TPoint; var Handle: HWND; DragKind: TDragKind; Client: TControl): Pointer; 根据DragKind的类型查找Pos位置的VCL控件(由函数返回值返回),Handle返回控件的句柄。
DoDragOver:(DragMsg: TDragMessage): Boolean; 产生目标控件的DragOver事件。
DragMessage:(Handle: HWND; Msg: TDragMessage; Source: TDragObject; Target: Pointer; const Pos: TPoint): Longint; 发送Drag相关的消息到Drag控件。
7、DragDrop管理核心
下面的部分将是DragDrop管理的核心部分介绍。先来看一直管理核心类的定义及继承关系: TDragObject = class(TObject); TDragObjectEx = class(TDragObject); TBaseDragControlObject = class(TDragObject); TDragControlObject = class(TBaseDragControlObject); TDragControlObjectEx = class(TDragControlObject);
这里只对TDragObject类的DragDrop控制实现过程作详细介绍,其他部分及其他类的实现就不多作介绍。
在DragInit函数中有这么一句调用: DragCapture := DragObject.Capture;
TDragObject.Capture调用AllocateHWND函数创建了一个内部不可见窗口(Delphi习惯上称为TPUtilWindow),并设置该窗口句柄为Capture窗口,以接收应用程序的所有鼠标和键盘输入消息,实现Drag控制。下面是其实现代码: function TDragObject.Capture: HWND; begin Result := Classes.AllocateHWND(MainWndProc); SetCapture(Result); end;
与TDragObject.Capture对应,有一个TDragObject.ReleaseCapture函数,在DragDone有相应调用: DragObject.ReleaseCapture(DragCapture);
TDragObject.Capture结束DragDrop控制,函数中首先释放系统的Capture句柄,并调用DeallocateHWND释放由AllocateHWND创建的窗口。
当调用WinAPI函数SetCapture将一个窗口(句柄)设置为Capture模式后,系统的所有鼠标、键盘输入消息都将发送到该窗口中,VCL的DragDrop操作便是基于这样的原理来实现的。当调用了TControl.BeginDrag函数后,随后的几个函数设置DragDrop操作所需的参数,并创建了一个这样的Capture窗口,直到这时,鼠标的按键一直是按下的,当Capture窗口接收到鼠标按键释放或ESC键按下的消息时,便结束了DragDrop操作。
我们再来看一下TDragObject的消息处理函数TDragObject.WndProc:
procedure TDragObject.WndProc(var Msg: TMessage); var P: TPoint; begin try case Msg.Msg of
// 鼠标移动时调用DragTo函数,检查鼠标位置的VCL控件并产生相应的事件ss WM_MOUSEMOVE: begin P := SmallPointToPoint(TWMMouse(Msg).Pos); ClientToScreen(DragCapture, P); DragTo(P); end;
// 系统的Capture窗口改变或鼠标按键释放时结束DragDrop操作 WM_CAPTURECHANGED: DragDone(False); // 取消Drag WM_LBUTTONUP, WM_RBUTTONUP: DragDone(True); // 结束Drag并产生DragDrop事件
// 当一个TPUtilWindow获得鼠标Capture时,Forms.IsKeyMsg向其发送所有的键盘消息, // 但是这些键盘消息都加上了CN_BASE,变成了CN_KEYxxx // 如果Ctrl键按下或释放, CN_KEYUP: if Msg.WParam = VK_CONTROL then DragTo(DragObject.DragPos); CN_KEYDOWN: begin case Msg.WParam of VK_CONTROL: DragTo(DragObject.DragPos); VK_ESCAPE: begin { Consume keystroke and cancel drag operation } Msg.Result := 1; DragDone(False); // ESC键按下,取消Drag操作 end; end; end; end; except if DragControl <> nil then DragDone(False); Application.HandleException(Self); end; end; 8、小结
通过全文的介绍,可以总结出下图:
TControl.BeginDrag | DragInitControl --> { TDragObject.Create; } | DragInit --> { TDragObject.Capture; } | |----------> | | TDragObject.WinProc ---> WM_MOUSEMOVE ===> DragTo | | | |---------- <| |-> WM_CAPTURECHANGED ===> DragDone(False) | | DragDone |-> WM_LBUTTONUP, WM_RBUTTONUP ==> DragDone(True) | |-> CN_KEYUP(VK_CONTROL) ===> DragTo | |-> CN_KEYDOWN(VK_CONTROL) ===> DragTo | |-> CN_KEYDOWN(VK_ESCAPE) ===> DragDone(False)
|
请发表评论