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)

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值