unit VirtualTrees.DragImage; interface uses WinApi.Windows, WinApi.ActiveX, System.Types, Vcl.Controls, Vcl.Graphics; {$MINENUMSIZE 1, make enumerations as small as possible} type // Drag image support for the tree. TVTTransparency = 0 .. 255; TVTBias = - 128 .. 127; // Simple move limitation for the drag image. TVTDragMoveRestriction = ( dmrNone, dmrHorizontalOnly, dmrVerticalOnly ); TVTDragImageStates = set of ( disHidden, // Internal drag image is currently hidden (always hidden if drag image helper interfaces are used). disInDrag, // Drag image class is currently being used. disPrepared, // Drag image class is prepared. disSystemSupport // Running on Windows 2000 or higher. System supports drag images natively. ); // Class to manage header and tree drag image during a drag'n drop operation. TVTDragImage = class private FOwner : TCustomControl; FBackImage, // backup of overwritten screen area FAlphaImage, // target for alpha blending FDragImage : TBitmap; // the actual drag image to blend to screen FImagePosition, // position of image (upper left corner) in screen coordinates FLastPosition : TPoint; // last mouse position in screen coordinates FTransparency : TVTTransparency; // alpha value of the drag image (0 - fully transparent, 255 - fully opaque) FPreBlendBias, // value to darken or lighten the drag image before it is blended FPostBlendBias : TVTBias; // value to darken or lighten the alpha blend result FFade : Boolean; // determines whether to fade the drag image from center to borders or not FRestriction : TVTDragMoveRestriction; // determines in which directions the drag image can be moved FColorKey : TColor; // color to make fully transparent regardless of any other setting FStates : TVTDragImageStates; // Determines the states of the drag image class. function GetVisible : Boolean; // True if the drag image is currently hidden (used only when dragging) procedure InternalShowDragImage(ScreenDC : HDC); procedure MakeAlphaChannel(Source, Target : TBitmap); public constructor Create(AOwner : TCustomControl); destructor Destroy; override; function DragTo(P : TPoint; ForceRepaint : Boolean) : Boolean; procedure EndDrag; function GetDragImageRect : TRect; procedure HideDragImage; procedure PrepareDrag(DragImage : TBitmap; ImagePosition, HotSpot : TPoint; const DataObject : IDataObject); procedure RecaptureBackground(Tree : TCustomControl; R : TRect; VisibleRegion : HRGN; CaptureNCArea, ReshowDragImage : Boolean); procedure ShowDragImage; function WillMove(P : TPoint) : Boolean; property ColorKey : TColor read FColorKey write FColorKey default clWindow; property Fade : Boolean read FFade write FFade default False; property ImagePosition : TPoint read FImagePosition; property LastPosition : TPoint read FLastPosition; property MoveRestriction : TVTDragMoveRestriction read FRestriction write FRestriction default dmrNone; property PreBlendBias : TVTBias read FPreBlendBias write FPreBlendBias default 0; property Transparency : TVTTransparency read FTransparency write FTransparency default 128; property Visible : Boolean read GetVisible; end; implementation uses WinApi.ShlObj, WinApi.Messages, System.SysUtils, System.Math, VirtualTrees.DragnDrop, VirtualTrees.Types, VirtualTrees.Utils, VirtualTrees.BaseTree; //----------------- TVTDragImage --------------------------------------------------------------------------------------- constructor TVTDragImage.Create(AOwner : TCustomControl); begin FOwner := AOwner; FTransparency := 128; FPreBlendBias := 0; FPostBlendBias := 0; FFade := False; FRestriction := dmrNone; FColorKey := clNone; end; //---------------------------------------------------------------------------------------------------------------------- destructor TVTDragImage.Destroy; begin EndDrag; inherited; end; //---------------------------------------------------------------------------------------------------------------------- function TVTDragImage.GetVisible : Boolean; // Returns True if the internal drag image is used (i.e. the system does not natively support drag images) and // the internal image is currently visible on screen. begin Result := FStates * [disHidden, disInDrag, disPrepared, disSystemSupport] = [disInDrag, disPrepared]; end; //---------------------------------------------------------------------------------------------------------------------- procedure TVTDragImage.InternalShowDragImage(ScreenDC : HDC); // Frequently called helper routine to actually do the blend and put it onto the screen. // Only used if the system does not support drag images. var BlendMode : TBlendMode; begin with FAlphaImage do BitBlt(Canvas.Handle, 0, 0, Width, Height, FBackImage.Canvas.Handle, 0, 0, SRCCOPY); if not FFade and (FColorKey = clNone) then BlendMode := bmConstantAlpha else BlendMode := bmMasterAlpha; with FDragImage do AlphaBlend(Canvas.Handle, FAlphaImage.Canvas.Handle, Rect(0, 0, Width, Height), Point(0, 0), BlendMode, FTransparency, FPostBlendBias); with FAlphaImage do BitBlt(ScreenDC, FImagePosition.X, FImagePosition.Y, Width, Height, Canvas.Handle, 0, 0, SRCCOPY); end; //---------------------------------------------------------------------------------------------------------------------- procedure TVTDragImage.MakeAlphaChannel(Source, Target : TBitmap); // Helper method to create a proper alpha channel in Target (which must be in 32 bit pixel format), depending // on the settings for the drag image and the color values in Source. // Only used if the system does not support drag images. type PBGRA = ^TBGRA; TBGRA = packed record case Boolean of False : (Color : Cardinal); True : (BGR : array [0 .. 2] of Byte; Alpha : Byte); end; var Color, ColorKeyRef : COLORREF; UseColorKey : Boolean; SourceRun, TargetRun : PBGRA; X, Y, MaxDimension, HalfWidth, HalfHeight : Integer; T : Extended; begin UseColorKey := ColorKey <> clNone; ColorKeyRef := ColorToRGB(ColorKey) and $FFFFFF; // Color values are in the form BGR (red on LSB) while bitmap colors are in the form ARGB (blue on LSB) // hence we have to swap red and blue in the color key. with TBGRA(ColorKeyRef) do begin X := BGR[0]; BGR[0] := BGR[2]; BGR[2] := X; end; with Target do begin MaxDimension := Max(Width, Height); HalfWidth := Width div 2; HalfHeight := Height div 2; for Y := 0 to Height - 1 do begin TargetRun := Scanline[Y]; SourceRun := Source.Scanline[Y]; for X := 0 to Width - 1 do begin Color := SourceRun.Color and $FFFFFF; if UseColorKey and (Color = ColorKeyRef) then TargetRun.Alpha := 0 else begin // If the color is not the given color key (or none is used) then do full calculation of a bell curve. T := Exp( - 8 * Sqrt(Sqr((X - HalfWidth) / MaxDimension) + Sqr((Y - HalfHeight) / MaxDimension))); TargetRun.Alpha := Round(255 * T); end; Inc(SourceRun); Inc(TargetRun); end; end; end; end; //---------------------------------------------------------------------------------------------------------------------- function TVTDragImage.DragTo(P : TPoint; ForceRepaint : Boolean) : Boolean; // Moves the drag image to a new position, which is determined from the passed point P and the previous // mouse position. // ForceRepaint is True if something on the screen changed and the back image must be refreshed. var ScreenDC : HDC; DeltaX, DeltaY : Integer; // optimized drag image move support RSamp1, RSamp2, // newly added parts from screen which will be overwritten RDraw1, RDraw2, // parts to be restored to screen RScroll, RClip : TRect; // ScrollDC of the existent background begin // Determine distances to move the drag image. Take care for restrictions. case FRestriction of dmrHorizontalOnly : begin DeltaX := FLastPosition.X - P.X; DeltaY := 0; end; dmrVerticalOnly : begin DeltaX := 0; DeltaY := FLastPosition.Y - P.Y; end; else // dmrNone DeltaX := FLastPosition.X - P.X; DeltaY := FLastPosition.Y - P.Y; end; Result := (DeltaX <> 0) or (DeltaY <> 0) or ForceRepaint; if Result then begin if Visible then begin // All this stuff is only called if we have to handle the drag image ourselves. If the system supports // drag image then this is all never executed. ScreenDC := GetDC(0); try if (Abs(DeltaX) >= FDragImage.Width) or (Abs(DeltaY) >= FDragImage.Height) or ForceRepaint then begin // If moved more than image size then just restore old screen and blit image to new position. BitBlt(ScreenDC, FImagePosition.X, FImagePosition.Y, FBackImage.Width, FBackImage.Height, FBackImage.Canvas.Handle, 0, 0, SRCCOPY); GetPixel(ScreenDC, FImagePosition.X, FImagePosition.Y); if ForceRepaint then UpdateWindow(FOwner.Handle); Inc(FImagePosition.X, - DeltaX); Inc(FImagePosition.Y, - DeltaY); BitBlt(FBackImage.Canvas.Handle, 0, 0, FBackImage.Width, FBackImage.Height, ScreenDC, FImagePosition.X, FImagePosition.Y, SRCCOPY); end else begin // overlapping copy FillDragRectangles(FDragImage.Width, FDragImage.Height, DeltaX, DeltaY, RClip, RScroll, RSamp1, RSamp2, RDraw1, RDraw2); with FBackImage.Canvas do begin // restore uncovered areas of the screen if DeltaX = 0 then begin BitBlt(ScreenDC, FImagePosition.X + RDraw2.Left, FImagePosition.Y + RDraw2.Top, RDraw2.Right, RDraw2.Bottom, Handle, RDraw2.Left, RDraw2.Top, SRCCOPY); end else begin if DeltaY = 0 then begin BitBlt(ScreenDC, FImagePosition.X + RDraw1.Left, FImagePosition.Y + RDraw1.Top, RDraw1.Right, RDraw1.Bottom, Handle, RDraw1.Left, RDraw1.Top, SRCCOPY); end else begin BitBlt(ScreenDC, FImagePosition.X + RDraw1.Left, FImagePosition.Y + RDraw1.Top, RDraw1.Right, RDraw1.Bottom, Handle, RDraw1.Left, RDraw1.Top, SRCCOPY); BitBlt(ScreenDC, FImagePosition.X + RDraw1.Left, FImagePosition.Y + RDraw1.Top, RDraw1.Right, RDraw1.Bottom, Handle, RDraw1.Left, RDraw1.Top, SRCCOPY); end; end; // move existent background ScrollDC(Handle, DeltaX, DeltaY, RScroll, RClip, 0, nil); Inc(FImagePosition.X, - DeltaX); Inc(FImagePosition.Y, - DeltaY); // Get first and second additional rectangle from screen. if DeltaX = 0 then begin BitBlt(Handle, RSamp2.Left, RSamp2.Top, RSamp2.Right, RSamp2.Bottom, ScreenDC, FImagePosition.X + RSamp2.Left, FImagePosition.Y + RSamp2.Top, SRCCOPY); end else if DeltaY = 0 then begin BitBlt(Handle, RSamp1.Left, RSamp1.Top, RSamp1.Right, RSamp1.Bottom, ScreenDC, FImagePosition.X + RSamp1.Left, FImagePosition.Y + RSamp1.Top, SRCCOPY); end else begin BitBlt(Handle, RSamp1.Left, RSamp1.Top, RSamp1.Right, RSamp1.Bottom, ScreenDC, FImagePosition.X + RSamp1.Left, FImagePosition.Y + RSamp1.Top, SRCCOPY); BitBlt(Handle, RSamp2.Left, RSamp2.Top, RSamp2.Right, RSamp2.Bottom, ScreenDC, FImagePosition.X + RSamp2.Left, FImagePosition.Y + RSamp2.Top, SRCCOPY); end; end; end; InternalShowDragImage(ScreenDC); finally ReleaseDC(0, ScreenDC); end; end; FLastPosition.X := P.X; FLastPosition.Y := P.Y; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TVTDragImage.EndDrag; begin HideDragImage; FStates := FStates - [disInDrag, disPrepared]; FBackImage.Free; FBackImage := nil; FDragImage.Free; FDragImage := nil; FAlphaImage.Free; FAlphaImage := nil; end; //---------------------------------------------------------------------------------------------------------------------- function TVTDragImage.GetDragImageRect : TRect; // Returns the current size and position of the drag image (screen coordinates). begin if Visible then begin with FBackImage do Result := Rect(FImagePosition.X, FImagePosition.Y, FImagePosition.X + Width, FImagePosition.Y + Height); end else Result := Rect(0, 0, 0, 0); end; //---------------------------------------------------------------------------------------------------------------------- procedure TVTDragImage.HideDragImage; var ScreenDC : HDC; begin if Visible then begin Include(FStates, disHidden); ScreenDC := GetDC(0); try // restore screen with FBackImage do BitBlt(ScreenDC, FImagePosition.X, FImagePosition.Y, Width, Height, Canvas.Handle, 0, 0, SRCCOPY); finally ReleaseDC(0, ScreenDC); end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TVTDragImage.PrepareDrag(DragImage : TBitmap; ImagePosition, HotSpot : TPoint; const DataObject : IDataObject); // Creates all necessary structures to do alpha blended dragging using the given image. // ImagePostion and HotSpot are given in screen coordinates. The first determines where to place the drag image while // the second is the initial mouse position. // This method also determines whether the system supports drag images natively. If so then only minimal structures // are created. var Width, Height : Integer; DragSourceHelper : IDragSourceHelper; DragInfo : TSHDragImage; lDragSourceHelper2 : IDragSourceHelper2; // Needed to get Windows Vista+ style drag hints. lNullPoint : TPoint; begin Width := DragImage.Width; Height := DragImage.Height; // Determine whether the system supports the drag helper interfaces. if Assigned(DataObject) and Succeeded(CoCreateInstance(CLSID_DragDropHelper, nil, CLSCTX_INPROC_SERVER, IDragSourceHelper, DragSourceHelper)) then begin Include(FStates, disSystemSupport); lNullPoint := Point(0, 0); if Supports(DragSourceHelper, IDragSourceHelper2, lDragSourceHelper2) then lDragSourceHelper2.SetFlags(DSH_ALLOWDROPDESCRIPTIONTEXT); // Show description texts // First let the system try to initialze the DragSourceHelper, this works fine for file system objects (CF_HDROP) StandardOLEFormat.cfFormat := CF_HDROP; if not Succeeded(DataObject.QueryGetData(StandardOLEFormat)) or not Succeeded(DragSourceHelper.InitializeFromWindow(0, lNullPoint, DataObject)) then begin // Supply the drag source helper with our drag image. DragInfo.sizeDragImage.cx := Width; DragInfo.sizeDragImage.cy := Height; DragInfo.ptOffset.X := Width div 2; DragInfo.ptOffset.Y := Height div 2; DragInfo.hbmpDragImage := CopyImage(DragImage.Handle, IMAGE_BITMAP, Width, Height, LR_COPYRETURNORG); DragInfo.crColorKey := ColorToRGB(FColorKey); if not Succeeded(DragSourceHelper.InitializeFromBitmap(@DragInfo, DataObject)) then begin DeleteObject(DragInfo.hbmpDragImage); Exclude(FStates, disSystemSupport); end; end; end else Exclude(FStates, disSystemSupport); if not (disSystemSupport in FStates) then begin FLastPosition := HotSpot; FDragImage := TBitmap.Create; FDragImage.PixelFormat := pf32Bit; FDragImage.SetSize(Width, Height); FAlphaImage := TBitmap.Create; FAlphaImage.PixelFormat := pf32Bit; FAlphaImage.SetSize(Width, Height); FBackImage := TBitmap.Create; FBackImage.PixelFormat := pf32Bit; FBackImage.SetSize(Width, Height); // Copy the given drag image and apply pre blend bias if required. if FPreBlendBias = 0 then with FDragImage do BitBlt(Canvas.Handle, 0, 0, Width, Height, DragImage.Canvas.Handle, 0, 0, SRCCOPY) else AlphaBlend(DragImage.Canvas.Handle, FDragImage.Canvas.Handle, Rect(0, 0, Width, Height), Point(0, 0), bmConstantAlpha, 255, FPreBlendBias); // Create a proper alpha channel also if no fading is required (transparent parts). MakeAlphaChannel(DragImage, FDragImage); FImagePosition := ImagePosition; // Initially the drag image is hidden and will be shown during the immediately following DragEnter event. FStates := FStates + [disInDrag, disHidden, disPrepared]; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TVTDragImage.RecaptureBackground(Tree : TCustomControl; R : TRect; VisibleRegion : HRGN; CaptureNCArea, ReshowDragImage : Boolean); // Notification by the drop target tree to update the background image because something in the tree has changed. // Note: The passed rectangle is given in client coordinates of the current drop target tree (given in Tree). // The caller does not check if the given rectangle is actually within the drag image. Hence this method must do // all the checks. // This method does nothing if the system manages the drag image. var DragRect, ClipRect : TRect; PaintTarget : TPoint; PaintOptions : TVTInternalPaintOptions; ScreenDC : HDC; begin // Recapturing means we want the tree to paint the new part into our back bitmap instead to the screen. if Visible then begin // Create the minimum rectangle to be recaptured. MapWindowPoints(Tree.Handle, 0, R, 2); DragRect := GetDragImageRect; IntersectRect(R, R, DragRect); OffsetRgn(VisibleRegion, - DragRect.Left, - DragRect.Top); // The target position for painting in the drag image is relative and can be determined from screen coordinates too. PaintTarget.X := R.Left - DragRect.Left; PaintTarget.Y := R.Top - DragRect.Top; // The source rectangle is determined by the offsets in the tree. MapWindowPoints(0, Tree.Handle, R, 2); OffsetRect(R, - TBaseVirtualTree(Tree).OffsetX, - TBaseVirtualTree(Tree).OffsetY); // Finally let the tree paint the relevant part and upate the drag image on screen. PaintOptions := [poBackground, poColumnColor, poDrawFocusRect, poDrawDropMark, poDrawSelection, poGridLines]; with FBackImage do begin ClipRect.TopLeft := PaintTarget; ClipRect.Right := ClipRect.Left + R.Right - R.Left; ClipRect.Bottom := ClipRect.Top + R.Bottom - R.Top; // TODO: somehow with clipping, the background image is not drawn on the // backup image. Need to be diagnosed and fixed. For now, we have coded // a work around in DragTo where this is used by using the condition // IsInHeader. (found when solving issue 248) ClipCanvas(Canvas, ClipRect, VisibleRegion); TBaseVirtualTree(Tree).PaintTree(Canvas, R, PaintTarget, PaintOptions); if CaptureNCArea then begin // Header is painted in this part only so when you use this routine and want // to capture the header in backup image, this flag should be ON. // For the non-client area we only need the visible region of the window as limit for painting. SelectClipRgn(Canvas.Handle, VisibleRegion); // Since WM_PRINT cannot be given a position where to draw we simply move the window origin and // get the same effect. GetWindowRect(Tree.Handle, ClipRect); SetCanvasOrigin(Canvas, DragRect.Left - ClipRect.Left, DragRect.Top - ClipRect.Top); Tree.Perform(WM_PRINT, WPARAM(Canvas.Handle), PRF_NONCLIENT); SetCanvasOrigin(Canvas, 0, 0); end; SelectClipRgn(Canvas.Handle, 0); if ReshowDragImage then begin GDIFlush; ScreenDC := GetDC(0); try InternalShowDragImage(ScreenDC); finally ReleaseDC(0, ScreenDC); end; end; end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TVTDragImage.ShowDragImage; // Shows the drag image after it has been hidden by HideDragImage. // Note: there might be a new background now. // Also this method does nothing if the system manages the drag image. var ScreenDC : HDC; begin if FStates * [disInDrag, disHidden, disPrepared, disSystemSupport] = [disInDrag, disHidden, disPrepared] then begin Exclude(FStates, disHidden); GDIFlush; ScreenDC := GetDC(0); try BitBlt(FBackImage.Canvas.Handle, 0, 0, FBackImage.Width, FBackImage.Height, ScreenDC, FImagePosition.X, FImagePosition.Y, SRCCOPY); InternalShowDragImage(ScreenDC); finally ReleaseDC(0, ScreenDC); end; end; end; //---------------------------------------------------------------------------------------------------------------------- function TVTDragImage.WillMove(P : TPoint) : Boolean; // This method determines whether the drag image would "physically" move when DragTo would be called with the same // target point. // Always returns False if the system drag image support is available. begin Result := Visible; if Result then begin // Determine distances to move the drag image. Take care for restrictions. case FRestriction of dmrHorizontalOnly : Result := FLastPosition.X <> P.X; dmrVerticalOnly : Result := FLastPosition.Y <> P.Y; else // dmrNone Result := (FLastPosition.X <> P.X) or (FLastPosition.Y <> P.Y); end; end; end; end.