Scroll TTreeView while dragging over/near the edges - delphi

I have a TTreeView that can have lots of nodes, when a lot of nodes are expanded the tree uses a lot of screen space.
Now suppose I want to drag a node that is near the bottom of the TreeView to the top, I can't physically see the top part of the TreeView because the node I am selecting is at the bottom. When dragging the node to the top of the TreeView I would like the TreeView to automatically scroll with me when dragging, by default this does not seem to happen.
A perfect example of this behaviour is seen in Windows Explorer. If you try to drag a file or folder, when you hover the dragged item (node) it automatically scrolls up or down depending on cursor position.
Hope that makes sense.
PS, I already know how to drag nodes, I want the TreeView to scroll with me when dragging if hovering near the top or bottom of the TreeView.
Thanks.

This is the code I use. It will work for any TWinControl descendent: list box, tree view, list view etc.
type
TAutoScrollTimer = class(TTimer)
private
FControl: TWinControl;
FScrollCount: Integer;
procedure InitialiseTimer;
procedure Timer(Sender: TObject);
public
constructor Create(Control: TWinControl);
end;
{ TAutoScrollTimer }
constructor TAutoScrollTimer.Create(Control: TWinControl);
begin
inherited Create(Control);
FControl := Control;
InitialiseTimer;
end;
procedure TAutoScrollTimer.InitialiseTimer;
begin
FScrollCount := 0;
Interval := 250;
Enabled := True;
OnTimer := Timer;
end;
procedure TAutoScrollTimer.Timer(Sender: TObject);
procedure DoScroll;
var
WindowEdgeTolerance: Integer;
Pos: TPoint;
begin
WindowEdgeTolerance := Min(25, FControl.Height div 4);
GetCursorPos(Pos);
Pos := FControl.ScreenToClient(Pos);
if not InRange(Pos.X, 0, FControl.Width) then begin
exit;
end;
if Pos.Y<WindowEdgeTolerance then begin
SendMessage(FControl.Handle, WM_VSCROLL, SB_LINEUP, 0);
end else if Pos.Y>FControl.Height-WindowEdgeTolerance then begin
SendMessage(FControl.Handle, WM_VSCROLL, SB_LINEDOWN, 0);
end else begin
InitialiseTimer;
exit;
end;
if FScrollCount<50 then begin
inc(FScrollCount);
if FScrollCount mod 5=0 then begin
//speed up the scrolling by reducing the timer interval
Interval := MulDiv(Interval, 3, 4);
end;
end;
if Win32MajorVersion<6 then begin
//in XP we need to clear up transient "fluff"; results in flickering so only do it in XP where it is needed
FControl.Invalidate;
end;
end;
begin
if Mouse.IsDragging then begin
DoScroll;
end else begin
Free;
end;
end;
Then to use it you add an OnStartDrag event handler for the control and implement it like this:
procedure TMyForm.SomeControlStartDrag(Sender: TObject; var DragObject: TDragObject);
begin
TAutoScrollTimer.Create(Sender as TWinControl);
end;

Here's an alternative based on the fact that the selected node always automatically scrolls in view.
type
TForm1 = class(TForm)
TreeView1: TTreeView;
TreeView2: TTreeView;
procedure TreeViewDragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
procedure TreeViewEndDrag(Sender, Target: TObject; X, Y: Integer);
procedure TreeViewMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
private
FDragNode: TTreeNode;
FNodeHeight: Integer;
end;
...
procedure TForm1.TreeViewMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
with TTreeView(Sender) do
begin
FDragNode := GetNodeAt(X, Y);
if FDragNode <> nil then
begin
Selected := FDragNode;
with FDragNode.DisplayRect(False) do
FNodeHeight := Bottom - Top;
BeginDrag(False, Mouse.DragThreshold);
end;
end;
end;
procedure TForm1.TreeViewDragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
var
Pt: TPoint;
DropNode: TTreeNode;
begin
Accept := Source is TTreeView;
if Accept then
with TTreeView(Source) do
begin
if Sender <> Source then
Pt := ScreenToClient(Mouse.CursorPos)
else
Pt := Point(X, Y);
if Pt.Y < FNodeHeight then
DropNode := Selected.GetPrevVisible
else if Pt.Y > (ClientHeight - FNodeHeight) then
DropNode := Selected.GetNextVisible
else
DropNode := GetNodeAt(Pt.X, Pt.Y);
if DropNode <> nil then
Selected := DropNode;
end;
end;
procedure TForm1.TreeViewEndDrag(Sender, Target: TObject; X, Y: Integer);
var
DropNode: TTreeNode;
begin
with TTreeView(Sender) do
if Target <> nil then
begin
DropNode := Selected;
DropNode := Items.Insert(DropNode, '');
DropNode.Assign(FDragNode);
Selected := DropNode;
Items.Delete(FDragNode);
end
else
Selected := FDragNode;
end;
You may want to link the OnDragOver event handler to the parent of the TreeView too, which results in scrolling ánd dropping when the mouse is outside the TreeView. If you dó want the scrolling, but not the dropping when the mouse is outside the TreeView, then check if Target = Sender in the OnEndDrag event handler.

Just to be complete, workarounds like in the other answers are not required anymore. Later versions have an option for this:
TreeOptions.AutoOptions.toAutoScroll := True

Related

DBCtrlGrid Drag and Drop

Tried with no success to drag and drop a row to switch positions (using a ClientDataSet in memory)
The specific case is: a ClientDataSet with image file names who will result in an ordered list that will be used to create export to a PDF document where each image is a page (this is why the order is important).
The DbCtrlGrid is used to visualize a thumbnail of the image, and I was trying to use drag-and-drop to exchange their positions, but I couldn't get information about the row where I dropped in the end.
It would help a method to get info about the row where the mouse is over when the OnDragDrop event triggers or any other idea
please
I imagine your q is prompted by the fact that although the TDBCtrlGrid has a
PanelIndex property which tells you which one of the grid's virtual panels
is active (i.e. is the one for the current row in the dataset), this doesn't
change while you've moving the mouse around e.g. during a drag operation. However,
it is not difficult to calculate this yourself, as follows.
The Height and Width of a TDBCtrlGrid are exact multiples of its RowCount and
ColCount. In the simple case of ColCount =1, it is trivially simple
to calculate which Row contains a given Y coordinate within the grid:
function TForm1.PanelIndexFromYPos(Y : Integer) : Integer;
var
PanelHeight : Integer;
begin
PanelHeight := DBCtrlGrid1.ClientHeight div DBCtrlGrid1.RowCount;
Result := Y div PanelHeight;
end;
(obviously this is for the simple case of a single column goVertical orientated grid but would be easy to generalise)
Now, the TBDCtrlGrid's EndDrag (and MouseOver) tells you the Y coordinate of the TPoint where the
drag operation ends, so you can use this PanelIndexFromYPos function to tell you
which row index the user has dropped the dragged row onto. As #KenWhite explained,
you then need to re-order your CDS to reflect the new position the dragged row should be in.
This is easy to do if your CDS has a DisplayIndex field representing what row position
a given record in the CDS and the CDS has an active index on this field. Re-ordering
the CDS's records is a bit of a rigmarole, as will be apparent from the following sample project.
TForm1 = class(TForm)
CDS1: TClientDataSet;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBCtrlGrid1: TDBCtrlGrid; // Note: DragMode set to dmManual;
DBText1: TDBText; // In the DBCtrlGrid
DBText2: TDBText;
DBText3: TDBText;
edSourceIndex: TEdit;
edDestIndex: TEdit;
btnTest: TButton;
Memo1: TMemo;
Label1: TLabel;
procedure FormCreate(Sender: TObject);
procedure DBCtrlGrid1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure DBCtrlGrid1DragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
procedure DBCtrlGrid1DragDrop(Sender, Source: TObject; X, Y: Integer);
procedure btnTestClick(Sender: TObject);
private
procedure MoveRow(SourceIndex, DestIndex : Integer);
procedure LogMove(OldValue, NewValue: Integer);
procedure ShowPanelInfo(Y: Integer);
protected
function PanelIndexFromYPos(Y : Integer) : Integer;
public
SourceIndex : Integer; // the DbCtrlGrid PanelIndex of the row being dragged
DestIndex : Integer; // the PanelIndex where the row is dropped
end;
[...]
function TForm1.PanelIndexFromYPos(Y : Integer) : Integer;
var
PanelHeight : Integer;
begin
PanelHeight := DBCtrlGrid1.ClientHeight div DBCtrlGrid1.RowCount;
Result := Y div PanelHeight;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
begin
// Create the fields for the CDS
AField := TIntegerField.Create(Self);
AField.FieldName := 'ID';
AField.DataSet := CDS1;
// This DisplayIndex field will be used to determine which row number in
// the DBCtrlGrid will occupy, by indexing the CDS on this field
AField := TIntegerField.Create(Self);
AField.FieldName := 'DisplayIndex';
AField.DataSet := CDS1;
AField := TStringField.Create(Self);
AField.FieldName := 'Name';
AField.Size := 20;
AField.DataSet := CDS1;
CDS1.CreateDataSet;
// Add some data which will appear in the grid in reverse-alphabetical order
CDS1.InsertRecord([1, 3, 'A']);
CDS1.InsertRecord([2, 2, 'B']);
CDS1.InsertRecord([3, 1, 'C']);
CDS1.InsertRecord([4, 0, 'D']);
CDS1.IndexFieldNames := 'DisplayIndex';
end;
procedure TForm1.DBCtrlGrid1MouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then begin
SourceIndex := PanelIndexFromYPos(Y);
DBCtrlGrid1.BeginDrag(False);
end;
end;
procedure TForm1.DBCtrlGrid1DragOver(Sender, Source: TObject; X,
Y: Integer; State: TDragState; var Accept: Boolean);
begin
Accept := True;
end;
procedure TForm1.DBCtrlGrid1DragDrop(Sender, Source: TObject; X,
Y: Integer);
begin
ShowPanelInfo(Y);
DestIndex := PanelIndexFromYPos(Y);
MoveRow(SourceIndex, DestIndex);
end;
procedure TForm1.MoveRow(SourceIndex, DestIndex : Integer);
var
BM : TBookMark;
Index : Integer;
procedure SetCDSIndex(Value : Integer);
var
OldValue : Integer;
begin
OldValue := CDS1.FieldByName('DisplayIndex').AsInteger;
CDS1.Edit;
CDS1.FieldByName('DisplayIndex').AsInteger := Value;
CDS1.Post;
LogMove(OldValue, Value);
end;
begin
if SourceIndex = DestIndex then exit;
CDS1.DisableControls;
try
if CDS1.FindKey([SourceIndex]) then begin
BM := CDS1.GetBookmark; // This is to keep track of the dragged row without needing to
// keep track of its (changing) DisplayIndex
if SourceIndex > DestIndex then begin
// i.e. we're moving the dragged row up in the grid
// so starting with the row above it we move the rows upwards
// eventually leaving a gap to drop the dragged row into
Index := SourceIndex - 1;
while Index >= DestIndex do begin
if CDS1.FindKey([Index]) then begin
SetCDSIndex(Index + 1);
end;
Dec(Index);
end;
end
else begin
// i.e. we're moving the dragged row down in the grid
// so starting with the row below it we move the rows upwards
// eventually leaving a gap to drop the dragged row into
Index := SourceIndex + 1;
while Index <= DestIndex do begin
if CDS1.FindKey([Index]) then begin
SetCDSIndex(Index - 1);
end;
Inc(Index);
end;
end;
end;
CDS1.GotoBookMark(BM);
if CDS1.FieldByName('DisplayIndex').AsInteger = SourceIndex then begin
SetCDSIndex(DestIndex);
end;
CDS1.FreeBookmark(BM); // should really have it's own try...finally but hey!
finally
CDS1.EnableControls;
end;
end;
procedure TForm1.LogMove(OldValue, NewValue : Integer);
begin
Memo1.Lines.Add(Format('Name: %s Old: %d New: %d ', [CDS1.FieldByName('Name').AsString, OldValue, NewValue]));
end;
procedure TForm1.ShowPanelInfo(Y : Integer);
begin
Label1.Caption := Format('y: %d panelindex: %d', [Y, PanelIndexFromYPos(Y)]);
end;
procedure TForm1.btnTestClick(Sender: TObject);
begin
// For debugging, to test mving rows without needing to drag/drop
MoveRow(StrToInt(edSourceIndex.Text), StrToInt(edDestIndex.Text));
end;
end.

How to correctly free a DragObject

I got a memory leak error after drag & drop.
TMyDragObject = class(TDragObject)
public
MyInfo : string;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
System.ReportMemoryLeaksOnShutdown := True;
Button1.DragMode := dmAutomatic;
end;
procedure TForm1.Button1StartDrag(Sender: TObject; var DragObject: TDragObject);
begin
DragObject := TMyDragObject.Create;
TMyDragObject(DragObject).MyInfo := 'hi!';
end;
Where should DragObject be freed?
Thanks to all.
Free the object in the OnDragDrop handler for the control accepting the drop. Here's the event for a TMemo:
procedure TForm4.Memo1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
if IsDragObject(Source) then
begin
Memo1.Lines.Add(TMyDragObject(Source).MyInfo);
Source.Free;
end;
end;
Here's a more complete (and very trivial) example. Drop a TButton and TMemo on the form, set Button1.DragMode to dmAutomatic, add the variable Ctr: Integer to the form's private section, and wire up the event handlers; it allows you to drag the button into the memo, adding the text Item + the current value of Ctr to the memo's lines.
type
TMyDragObject=class(TDragObject)
Info: string;
end;
procedure TForm4.Button1StartDrag(Sender: TObject; var DragObject: TDragObject);
begin
Inc(Ctr);
DragObject := TMyDragObject.Create;
TMyDragObject(DragObject).Info := 'Item ' + IntToStr(Ctr);
end;
procedure TForm4.Memo1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
if IsDragObject(Source) then
begin
Memo1.Lines.Add(TMyDragObject(Source).Info);
Source.Free;
end;
end;
procedure TForm4.Memo1DragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
begin
Accept := IsDragObject(Source);
end;
As an alternative, inherit from TDragObjectEx, which is automatically freed at the end of the drop operation, according to the documentation.
Note: TDragObject is not automatically freed at the end of a drag
operation. To work with a drag object that is freed at the end of the
drag operation, use TDragObjectEx instead

TScrollbox MouseDown override

I created a Custom scrollbox derives from TScrollbox that works the same except that it will scrolls when dragging in the client area aside from its scrollbars.
My problem now is i cannot Drag To Scroll when mouse is on a button or panel inside my CustomScrollbox.
the MouseDown, MouseUp, MouseMove override will not trigger because it hovers into different controls.
How can I keep tracking the MouseDown, MouseUp, MouseMove and prevent Button/Panels events from firing(inside my CustomScrollbox) when i start dragging?
here's the video of my smooth CustomScrollbox
So you want to adjust the mouse down behaviour of all childs, in such way that when a dragging operation is being initiated, the mouse events of the clicked child should be ignored. But when no drag is performed, then it would be required to fire the child's mouse events as usual.
Not a bad question actually. Since most of the default control interaction is tight to the release of the mouse button (e.g. OnClick is handled in WM_LBUTTONUP), this still should be possible in an intuitive manner.
I tried the code below, and it feels quite nice indeed. It involves:
handling WM_PARENTNOTIFY to catch when a child control is clicked on,
bypassing Child.OnMouseMove and Child.OnMouseUp,
transfer control to the scrollbox when the move exceeds Mouse.DragThreshold,
resetting focus to the previous focussed control before the drag,
canceling all changes made to the child's mouse events after the drag.
unit Unit2;
interface
uses
Windows, Messages, Classes, Controls, Forms, StdCtrls, ExtCtrls;
type
TScrollBox = class(Forms.TScrollBox)
private
FChild: TControl;
FDragging: Boolean;
FPrevActiveControl: TWinControl;
FPrevScrollPos: TPoint;
FPrevTick: Cardinal;
FOldChildOnMouseMove: TMouseMoveEvent;
FOldChildOnMouseUp: TMouseEvent;
FSpeedX: Single;
FSpeedY: Single;
FStartPos: TPoint;
FTracker: TTimer;
function ActiveControl: TWinControl;
procedure ChildMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure ChildMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
function GetScrollPos: TPoint;
procedure SetScrollPos(const Value: TPoint);
procedure Track(Sender: TObject);
procedure WMParentNotify(var Message: TWMParentNotify);
message WM_PARENTNOTIFY;
protected
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer); override;
public
constructor Create(AOwner: TComponent); override;
property ScrollPos: TPoint read GetScrollPos write SetScrollPos;
end;
TForm2 = class(TForm)
ScrollBox1: TScrollBox;
...
end;
implementation
{$R *.dfm}
{ TScrollBox }
type
TControlAccess = class(TControl);
function TScrollBox.ActiveControl: TWinControl;
var
Control: TWinControl;
begin
Result := Screen.ActiveControl;
Control := Result;
while (Control <> nil) do
begin
if Control = Self then
Exit;
Control := Control.Parent;
end;
Result := nil;
end;
procedure TScrollBox.ChildMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
if (Abs(FChild.Left + X - FStartPos.X) > Mouse.DragThreshold) or
(Abs(FChild.Top + Y - FStartPos.Y) > Mouse.DragThreshold) then
begin
MouseCapture := True;
TControlAccess(FChild).OnMouseMove := FOldChildOnMouseMove;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
MouseDown(mbLeft, Shift, FChild.Left + X, FChild.Top + Y);
FChild := nil;
if FPrevActiveControl <> nil then
FPrevActiveControl.SetFocus;
end
else
if Assigned(FOldChildOnMouseMove) then
FOldChildOnMouseMove(Sender, Shift, X, Y);
end;
procedure TScrollBox.ChildMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if FChild <> nil then
begin
if Assigned(FOldChildOnMouseUp) then
FOldChildOnMouseUp(Sender, Button, Shift, X, Y);
TControlAccess(FChild).OnMouseMove := FOldChildOnMouseMove;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
FChild := nil;
end;
end;
constructor TScrollBox.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FTracker := TTimer.Create(Self);
FTracker.Enabled := False;
FTracker.Interval := 15;
FTracker.OnTimer := Track;
end;
function TScrollBox.GetScrollPos: TPoint;
begin
Result := Point(HorzScrollBar.Position, VertScrollBar.Position);
end;
procedure TScrollBox.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
FDragging := True;
FPrevTick := GetTickCount;
FPrevScrollPos := ScrollPos;
FTracker.Enabled := True;
FStartPos := Point(ScrollPos.X + X, ScrollPos.Y + Y);
Screen.Cursor := crHandPoint;
inherited MouseDown(Button, Shift, X, Y);
end;
procedure TScrollBox.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
if FDragging then
ScrollPos := Point(FStartPos.X - X, FStartPos.Y - Y);
inherited MouseMove(Shift, X, Y);
end;
procedure TScrollBox.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
FDragging := False;
Screen.Cursor := crDefault;
inherited MouseUp(Button, Shift, X, Y);
end;
procedure TScrollBox.SetScrollPos(const Value: TPoint);
begin
HorzScrollBar.Position := Value.X;
VertScrollBar.Position := Value.Y;
end;
procedure TScrollBox.Track(Sender: TObject);
var
Delay: Cardinal;
begin
Delay := GetTickCount - FPrevTick;
if FDragging then
begin
if Delay = 0 then
Delay := 1;
FSpeedX := (ScrollPos.X - FPrevScrollPos.X) / Delay;
FSpeedY := (ScrollPos.Y - FPrevScrollPos.Y) / Delay;
end
else
begin
if (Abs(FSpeedX) < 0.005) and (Abs(FSpeedY) < 0.005) then
FTracker.Enabled := False
else
begin
ScrollPos := Point(FPrevScrollPos.X + Round(Delay * FSpeedX),
FPrevScrollPos.Y + Round(Delay * FSpeedY));
FSpeedX := 0.83 * FSpeedX;
FSpeedY := 0.83 * FSpeedY;
end;
end;
FPrevScrollPos := ScrollPos;
FPrevTick := GetTickCount;
end;
procedure TScrollBox.WMParentNotify(var Message: TWMParentNotify);
begin
inherited;
if Message.Event = WM_LBUTTONDOWN then
begin
FChild := ControlAtPos(Point(Message.XPos, Message.YPos), False, True);
if FChild <> nil then
begin
FPrevActiveControl := ActiveControl;
FOldChildOnMouseMove := TControlAccess(FChild).OnMouseMove;
TControlAccess(FChild).OnMouseMove := ChildMouseMove;
FOldChildOnMouseUp := TControlAccess(FChild).OnMouseUp;
TControlAccess(FChild).OnMouseUp := ChildMouseUp;
end;
end;
end;
end.
Note: When no drag is initiated (mouse movement < Mouse.DragThreshold), all mouse and click events of the clicked child remain intact. Otherwise only Child.OnMouseDown will fire!
For testing purposes, this answer is incorporated in the code above.
With thanks to #TLama for suggesting to use WM_PARENTNOTIFY.

How to drag report in ppviewer?

Anyone know how to drag the report in TppViewer? (Delphi 7) i try to use the dagdrop event and dragover event of ppviewer but failed, anyone can help?
procedure Tfrm1.ppviewer1DragOver(Sender, Source: TObject; X,
Y: Integer; State: TDragState; var Accept: Boolean);
begin
inherited;
Accept := Source IS TppViewer;
end;
procedure Tfrm1.ppviewer1DragDrop(Sender, Source: TObject; X,
Y: Integer);
begin
inherited;
if Source is TppViewer then begin
TppViewer(Source).Left := X;
TppViewer(Source).Top := Y;
end;
end;
This answer assumes that you are trying to scroll in the report, by dragging.
TReportPreviewer is the Form
ReportViewer is the ppViewer
Dragging is a Boolean
SaveX, SaveY are Integer
procedure TReportPreviewer.ReportViewerMouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
Dragging := true;
SaveX := X;
SaveY := Y;
end;
procedure TReportPreviewer.ReportViewerMouseMove(Sender: TObject;
Shift: TShiftState; X, Y: Integer);
begin
if Dragging then
begin
if ReportViewer.ScrollBox.Visible then
ReportViewer.ScrollBox.HorzScrollBar.Position := ReportViewer.ScrollBox.HorzScrollBar.Position - (X - SaveX);
if ReportViewer.ScrollBox.Visible then
ReportViewer.ScrollBox.VertScrollBar.Position := ReportViewer.ScrollBox.VertScrollBar.Position - (Y - SaveY);
SaveX := X;
SaveY := Y;
end;
end;
procedure TReportPreviewer.ReportViewerMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
Dragging := false;
end;
I tried using ScrollBy instead of moving the scrollbar position, but it seemed to reset for some reason.
Are you trying to drag a report file into the Viewer? if so biased on the following advice:
How to Drop Images from Windows Explorer to a TImage control
Delphi - Drag & Drop with ListView
WM_DROPFILES Message
You can achieve this by using the following code:
procedure TMainForm.FormCreate(Sender: TObject);
begin
//Tell Windows that the Report Viewer accepts files
ShellAPI.DragAcceptFiles(ppViewer1.Handle,True);
Application.OnMessage := ApplicationMessage;
end;
procedure TMainForm.ApplicationMessage(var Msg: TMsg; var Handled: Boolean);
begin
if (Msg.hwnd = ppViewer1.Handle) and (Msg.message = WM_DROPFILES) then
begin
Handled := ReportFileDrop(Msg);
end;
end;
function TMainForm.ReportFileDrop(var Msg: TMsg):Boolean ;
var
numFiles : longInt;
buffer : array[0..MAX_PATH] of char;
l_file:String;
l_filemsg:TWMDROPFILES;
begin
Result := False;
//Convert the TMsg into a TWMDROPFILES record
l_filemsg.Msg := Msg.message;
l_filemsg.Drop := Msg.wParam;
l_filemsg.Unused := Msg.lParam;
l_filemsg.Result := 0;
numFiles := DragQueryFile(l_filemsg.Drop, $FFFFFFFF, nil, 0) ;
if numFiles > 1 then
begin
ShowMessage('You can drop only one file at a time!') ;
end
else
begin
try
DragQueryFile(l_filemsg.Drop, 0, #buffer, sizeof(buffer)) ;
l_file := buffer;
//Only try and load the report if the file has the correct extension
if (Length(l_file) > 0) and (ExtractFileExt(LowerCase(l_file)) = '.rtm') then
begin
//Load the Report
Result := True;
end;
except
//Handle errors
end;
end;
end;

Can Delphi dragging be "promoted" to docking?

I have a TPageControl whose pages are all various forms that are attached using ManualDock(). The user should be able to rearrange the tabs by dragging them, which works already. It should however also be possible to undock the docked forms.
For now I have the following code:
procedure TMainForm.PageControlMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if (Button = mbLeft) and (Shift * [ssShift, ssCtrl] = [])
and PageControl.DockSite
then begin
PageControl.BeginDrag(False, 32);
end;
end;
If either the Shift or the Ctrl key are held down, then a docking operation will be started, otherwise the tabs can be rearranged by dragging them.
Using the keys as modifiers is awkward though. Is there any way to cancel the active drag operation when the mouse cursor is outside of the tab area of the page control, and start docking the child form? This is with Delphi 2009.
I have a solution now which works for me, so I'll answer myself - maybe somebody has a use for this too.
Let's start with a small sample application that creates a TPageControl with 8 docked forms, with code to allow for runtime reordering of the tabs. Tabs will be moved live, and when the dragging is canceled the active tab index will revert to its original value:
unit uDragDockTest;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
ComCtrls;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
fPageControl: TPageControl;
fPageControlOriginalPageIndex: integer;
function GetPageControlTabIndex(APosition: TPoint): integer;
public
procedure PageControlDragDrop(Sender, Source: TObject; X, Y: Integer);
procedure PageControlDragOver(Sender, Source: TObject; X, Y: Integer;
AState: TDragState; var AAccept: Boolean);
procedure PageControlEndDrag(Sender, Target: TObject; X, Y: Integer);
procedure PageControlMouseDown(Sender: TObject; AButton: TMouseButton;
AShift: TShiftState; X, Y: Integer);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
const
FormColors: array[1..8] of TColor = (
clRed, clGreen, clBlue, clYellow, clLime, clMaroon, clTeal, clAqua);
var
i: integer;
F: TForm;
begin
fPageControlOriginalPageIndex := -1;
fPageControl := TPageControl.Create(Self);
fPageControl.Align := alClient;
// set to False to enable tab reordering but disable form docking
fPageControl.DockSite := True;
fPageControl.Parent := Self;
fPageControl.OnDragDrop := PageControlDragDrop;
fPageControl.OnDragOver := PageControlDragOver;
fPageControl.OnEndDrag := PageControlEndDrag;
fPageControl.OnMouseDown := PageControlMouseDown;
for i := Low(FormColors) to High(FormColors) do begin
F := TForm.Create(Self);
F.Caption := Format('Form %d', [i]);
F.Color := FormColors[i];
F.DragKind := dkDock;
F.BorderStyle := bsSizeToolWin;
F.FormStyle := fsStayOnTop;
F.ManualDock(fPageControl);
F.Show;
end;
end;
const
TCM_GETITEMRECT = $130A;
function TForm1.GetPageControlTabIndex(APosition: TPoint): integer;
var
i: Integer;
TabRect: TRect;
begin
for i := 0 to fPageControl.PageCount - 1 do begin
fPageControl.Perform(TCM_GETITEMRECT, i, LPARAM(#TabRect));
if PtInRect(TabRect, APosition) then
Exit(i);
end;
Result := -1;
end;
procedure TForm1.PageControlDragDrop(Sender, Source: TObject; X, Y: Integer);
var
Index: integer;
begin
if Sender = fPageControl then begin
Index := GetPageControlTabIndex(Point(X, Y));
if (Index <> -1) and (Index <> fPageControl.ActivePage.PageIndex) then
fPageControl.ActivePage.PageIndex := Index;
end;
end;
procedure TForm1.PageControlDragOver(Sender, Source: TObject; X, Y: Integer;
AState: TDragState; var AAccept: Boolean);
var
Index: integer;
begin
AAccept := Sender = fPageControl;
if AAccept then begin
Index := GetPageControlTabIndex(Point(X, Y));
if (Index <> -1) and (Index <> fPageControl.ActivePage.PageIndex) then
fPageControl.ActivePage.PageIndex := Index;
end;
end;
procedure TForm1.PageControlEndDrag(Sender, Target: TObject; X, Y: Integer);
begin
// restore original index of active page if dragging was canceled
if (Target <> fPageControl) and (fPageControlOriginalPageIndex > -1)
and (fPageControlOriginalPageIndex < fPageControl.PageCount)
then
fPageControl.ActivePage.PageIndex := fPageControlOriginalPageIndex;
fPageControlOriginalPageIndex := -1;
end;
procedure TForm1.PageControlMouseDown(Sender: TObject; AButton: TMouseButton;
AShift: TShiftState; X, Y: Integer);
begin
if (AButton = mbLeft)
// undock single docked form or reorder multiple tabs
and (fPageControl.DockSite or (fPageControl.PageCount > 1))
then begin
// save current active page index for restoring when dragging is canceled
fPageControlOriginalPageIndex := fPageControl.ActivePageIndex;
fPageControl.BeginDrag(False);
end;
end;
end.
Paste this into the editor and run it, all necessary components and their properties will be created and set up at runtime.
Note that undocking the forms is possible only by double-clicking the tabs. It's also somewhat ugly that the drag cursor will be shown until the left mouse button is released, regardless of the distance from the tabs. It would be much better if the dragging was automatically canceled and the form be undocked instead, when the mouse is outside of the page control tab area with a few pixels margin.
This can be achieved by creating a custom DragObject in the OnStartDrag handler of the page control. In this object the mouse is captured, so all mouse messages while dragging can be handled in it. When the mouse cursor is outside of the tab influence rectangle the dragging is canceled, and a docking operation for the form in the active page control sheet is started instead:
type
TConvertDragToDockHelper = class(TDragControlObjectEx)
strict private
fPageControl: TPageControl;
fPageControlTabArea: TRect;
protected
procedure WndProc(var AMsg: TMessage); override;
public
constructor Create(AControl: TControl); override;
end;
constructor TConvertDragToDockHelper.Create(AControl: TControl);
const
MarginX = 32;
MarginY = 12;
var
Item0Rect, ItemLastRect: TRect;
begin
inherited;
fPageControl := AControl as TPageControl;
if fPageControl.PageCount > 0 then begin
// get rects of first and last tab
fPageControl.Perform(TCM_GETITEMRECT, 0, LPARAM(#Item0Rect));
fPageControl.Perform(TCM_GETITEMRECT, fPageControl.PageCount - 1,
LPARAM(#ItemLastRect));
// calculate rect valid for dragging (includes some margin around tabs)
// when this area is left dragging will be canceled and docking will start
fPageControlTabArea := Rect(
Min(Item0Rect.Left, ItemLastRect.Left) - MarginX,
Min(Item0Rect.Top, ItemLastRect.Top) - MarginY,
Max(Item0Rect.Right, ItemLastRect.Right) + MarginX,
Max(Item0Rect.Bottom, ItemLastRect.Bottom) + MarginY);
end;
end;
procedure TConvertDragToDockHelper.WndProc(var AMsg: TMessage);
var
MousePos: TPoint;
CanUndock: boolean;
begin
inherited;
if AMsg.Msg = WM_MOUSEMOVE then begin
MousePos := fPageControl.ScreenToClient(Mouse.CursorPos);
// cancel dragging if outside of tab area with margins
// optionally start undocking the docked form (can be canceled with [ESC])
if not PtInRect(fPageControlTabArea, MousePos) then begin
fPageControl.EndDrag(False);
CanUndock := fPageControl.DockSite and (fPageControl.ActivePage <> nil)
and (fPageControl.ActivePage.ControlCount > 0)
and (fPageControl.ActivePage.Controls[0] is TForm)
and (TForm(fPageControl.ActivePage.Controls[0]).DragKind = dkDock);
if CanUndock then
fPageControl.ActivePage.Controls[0].BeginDrag(False);
end;
end;
end;
The class descends from TDragControlObjectEx instead of from TDragControlObject so that it will be automatically freed. Now if a handler for the TPageControl in the sample application is created (and set for the page control object):
procedure TForm1.PageControlStartDrag(Sender: TObject;
var ADragObject: TDragObject);
begin
// do not cancel dragging unless page control has docking enabled
if (ADragObject = nil) and fPageControl.DockSite then
ADragObject := TConvertDragToDockHelper.Create(fPageControl);
end;
then the tab dragging will be canceled when the mouse moves far enough away from the tabs, and if the active page is a dockable form then a docking operation for it will be started, which can still be canceled with the ESC key.

Resources