Delphi VCL中DragDrop功能的底层实现

前段时间在论坛里看了一篇关于剖析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)


 

原文地址:https://www.cnblogs.com/zhangzhifeng/p/2110250.html