Why a form with 500 components is slow? - delphi

I am creating a form where there are icons- like on desktop and they can be moved freely.
I want to show sometimes even 500 or more icons so they need to work fast.
My icon is:
TMyIcon = class(TGraphicControl)
so it does not have a Windows handle.
The drawing is:
1 x Canvas.Rectangle (which is about 64x32)
1 x Canvas.TextOut (a bit smaller than the rectangle)
1 x Canvas.Draw (image is 32x32)
The code to move stuff is like this:
MyIconMouseMove:
Ico.Left := Ico.Left + X-ClickedPos.X;
Ico.Top := Ico.Top + Y-ClickedPos.Y;
On the form there is usually like 50 or so icons- the rest is outside the visible area.
When I have 100 icons- I can move them freely and it works fast. But when I create 500 icons then it gets laggy- but the number of visible icons is still the same.
How can I tell Windows to completely ignore the invisible icons so everything works smoothly?
Or maybe there is a component which can show desktop-like icons with ability to move them around? Something like TShellListView with AutoArrange = False?

TGraphicControl is a control that doesn't have a handle of its own. It uses its parent to display its content. That means, that changing the appearance of your control will force the parent to be redrawn as well. That may also trigger repainting all other controls.
In theory, only the part of the parent where control X is positioned needs to be invalidated, so only controls that overlap that part should need to be repainted. But still, this might cause a chain reaction, causing lots of paint methods be called everytime you change a single pixel in one of those controls.
Apparently, also icons outside the visible area are repainted. I think you can optimize this by setting the Visible property of the icons to False if they are outside the visible area.
If this doesn't work, you may need a completely different approach: there's the option to paint all icons on a single control, allowing you to buffer images. If you are dragging an icon, you can paint all other icons on a bitmap once. On every mouse move, you only need to paint that buffered bitmap and the single icon that is dragged, instead of 100 (or 500) separate icons. That should speeds things up quite a bit, although it is gonna take a little more effort to develop.
You could implement it like this:
type
// A class to hold icon information. That is: Position and picture
TMyIcon = class
Pos: TPoint;
Picture: TPicture;
constructor Create(Src: TBitmap);
destructor Destroy; override;
end;
// A list of such icons
//TIconList = TList<TMyIcon>;
TIconList = TList;
// A single graphic controls that can display many icons and
// allows dragging them
TIconControl = class(TGraphicControl)
Icons: TIconList;
Buffer: TBitmap;
DragIcon: TMyIcon;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Initialize;
// Painting
procedure ValidateBuffer;
procedure Paint; override;
// Dragging
function IconAtPos(X, Y: Integer): TMyIcon;
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;
end;
{ TMyIcon }
// Some random initialization
constructor TMyIcon.Create(Src: TBitmap);
begin
Picture := TPicture.Create;
Picture.Assign(Src);
Pos := Point(Random(500), Random(400));
end;
destructor TMyIcon.Destroy;
begin
Picture.Free;
inherited;
end;
Then, the graphiccontrol itself:
{ TIconControl }
constructor TIconControl.Create(AOwner: TComponent);
begin
inherited;
Icons := TIconList.Create;
end;
destructor TIconControl.Destroy;
begin
// Todo: Free the individual icons in the list.
Icons.Free;
inherited;
end;
function TIconControl.IconAtPos(X, Y: Integer): TMyIcon;
var
r: TRect;
i: Integer;
begin
// Just return the first icon that contains the clicked pixel.
for i := 0 to Icons.Count - 1 do
begin
Result := TMyIcon(Icons[i]);
r := Rect(0, 0, Result.Picture.Graphic.Width, Result.Picture.Graphic.Height);
OffsetRect(r, Result.Pos.X, Result.Pos.Y);
if PtInRect(r, Point(X, Y)) then
Exit;
end;
Result := nil;
end;
procedure TIconControl.Initialize;
var
Src: TBitmap;
i: Integer;
begin
Src := TBitmap.Create;
try
// Load a random file.
Src.LoadFromFile('C:\ff\ff.bmp');
// Test it with 10000 icons.
for i := 1 to 10000 do
Icons.Add(TMyIcon.Create(Src));
finally
Src.Free;
end;
end;
procedure TIconControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
if Button = mbLeft then
begin
// Left button is clicked. Try to find the icon at the clicked position
DragIcon := IconAtPos(X, Y);
if Assigned(DragIcon) then
begin
// An icon is found. Clear the buffer (which contains all icons) so it
// will be regenerated with the 9999 not-dragged icons on next repaint.
FreeAndNil(Buffer);
Invalidate;
end;
end;
end;
procedure TIconControl.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
if Assigned(DragIcon) then
begin
// An icon is being dragged. Update its position and redraw the control.
DragIcon.Pos := Point(X, Y);
Invalidate;
end;
end;
procedure TIconControl.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
if (Button = mbLeft) and Assigned(DragIcon) then
begin
// The button is released. Free the buffer, which contains the 9999
// other icons, so it will be regenerated with all 10000 icons on
// next repaint.
FreeAndNil(Buffer);
// Set DragIcon to nil. No icon is dragged at the moment.
DragIcon := nil;
Invalidate;
end;
end;
procedure TIconControl.Paint;
begin
// Check if the buffer is up to date.
ValidateBuffer;
// Draw the buffer (either 9999 or 10000 icons in one go)
Canvas.Draw(0, 0, Buffer);
// If one ican was dragged, draw it separately.
if Assigned(DragIcon) then
Canvas.Draw(DragIcon.Pos.X, DragIcon.Pos.Y, DragIcon.Picture.Graphic);
end;
procedure TIconControl.ValidateBuffer;
var
i: Integer;
Icon: TMyIcon;
begin
// If the buffer is assigned, there's nothing to do. It is nilled if
// it needs to be regenerated.
if not Assigned(Buffer) then
begin
Buffer := TBitmap.Create;
Buffer.Width := Width;
Buffer.Height := Height;
for i := 0 to Icons.Count - 1 do
begin
Icon := TMyIcon(Icons[i]);
if Icon <> DragIcon then
Buffer.Canvas.Draw(Icon.Pos.X, Icon.Pos.Y, Icon.Picture.Graphic);
end;
end;
end;
Create one of those controls, make it fill the form and initialize it with 10000 icons.
procedure TForm1.FormCreate(Sender: TObject);
begin
DoubleBuffered := True;
with TIconControl.Create(Self) do
begin
Parent := Self;
Align := alClient;
Initialize;
end;
end;
It's a bit quick&dirty, but it shows this solution may work very well. If you start dragging (mouse down), you will notice a small delay as the 10000 icons are drawn on the bitmap that passes for a buffer. After that, theres no noticable delay while dragging, because only two images are drawn on each repaint (instead of 500 in your case).

You might want to check out this control which is exactly what you asked for.
rkView from RMKlever
It is basically an icon or photo thumbnail viewer with scrolling etc.

Related

How to fill cell of a string grid using custom color?

I am trying to write custom date picker(calendar). The dates will be displayed on the stringgrid. I am trying to fill the clicked cell with a custom color and make that selected celltext bold.
Here is my code:
type
TStringGrid = Class(Vcl.Grids.TStringGrid)
private
FHideFocusRect: Boolean;
protected
Procedure Paint;override;
public
Property HideFocusRect:Boolean Read FHideFocusRect Write FHideFocusRect;
End;
TfrmNepaliCalendar = class(TForm)
...
...
...
end;
procedure TfrmNepaliCalendar.StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
begin
if gdSelected in State then begin
StringGrid.Canvas.Brush.Color := $00940A4B;
StringGrid.Canvas.FillRect(Rect);
StringGrid.Canvas.Font.Style := [fsBold];
StringGrid.Canvas.Font.Color := clHighlightText;
StringGrid.Canvas.TextOut(Rect.Left + 3, Rect.Top + 5, StringGrid.Cells[ACol,ARow]);
StringGrid.HideFocusRect := True;
end;
end;
{ TStringGrid }
procedure TStringGrid.Paint;
var
LRect: TRect;
begin
inherited;
if HideFocusRect then begin
LRect := CellRect(Col,Row);
if DrawingStyle = gdsThemed then InflateRect(LRect,-1,-1);
DrawFocusrect(Canvas.Handle,LRect)
end;
end;
The output, I am getting:
Problem #1: I need to hide that unwanted rectangle appearing as border for the selected cell
Problem #2: Avoid the cell background clipping
In the OnDrawCell procedure add just before FillRect
Rect.Left := Rect.Left-4;
Seems to work.
An alternative
The above doesn't fully solve the focus issue even with your paint procedure addon. Sometimes a white line is visible just inside the cell borders.
But the following is an alternative, that solves both your issues. It requires a little more coding, but not so much. On the other hand, subclassing TStringGrid is not needed, neither the Rect adjustment
The basis is to disable default drawing, so set the grids property DefaultDrawing := false;
and then add to the OnDrawCell event:
procedure TForm1.StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
begin
if gdFixed in State then
begin
StringGrid.Canvas.Brush.Color := clGradientInactiveCaption;
StringGrid.Canvas.Font.Style := [];
StringGrid.Canvas.Font.Color := clBlack;
end
else
if gdSelected in State then
begin
StringGrid.Canvas.Brush.Color := $00940A4B;
StringGrid.Canvas.Font.Style := [fsBold];
StringGrid.Canvas.Font.Color := clHighlightText;
end
else
begin
StringGrid.Canvas.Brush.Color := $00FFFFFF;
StringGrid.Canvas.Font.Style := [];
StringGrid.Canvas.Font.Color := clWindowText;
end;
StringGrid.Canvas.FillRect(Rect);
StringGrid.Canvas.TextOut(Rect.Left + 3, Rect.Top + 5, StringGrid.Cells[ACol,ARow]);
end;
With default drawing disabled, the grid draws the grid frame and the grid lines, but leaves all other drawing to the programmer. The caveat is that you have to add fancy themed drawing yourself if you need it.
With above coding I get this result:
I assume you (want to) use the default DefaultDrawing = True setting, otherwise your question does not exist.
To get rid of the focus rect, you need to draw it again (because it is a XOR-operation, the focus rect will disappear), or prevent it from being drawn.
Drawing again is done by utilizing the OnDrawCell event:
procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
begin
if gdFocused in State then
DrawFocusRect(StringGrid1.Canvas.Handle, Rect);
end;
Preventing it from drawing at all e.g. is done by disabling the possibility to set focus to the StringGrid. I assume you do not use its editor, so that should give no further usability concerns.
type
TStringGrid = class(Vcl.Grids.TStringGrid)
public
function CanFocus: Boolean; override;
end;
function TStringGrid.CanFocus: Boolean;
begin
Result := False;
end;
This actually is a bit strange working solution, because you are still able to tab into the control and it keeps responding to keyboard events.
I cannot reproduce your cliping problem with this code (XE2 here):
procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
begin
if gdSelected in State then
begin
StringGrid1.Canvas.Brush.Color := $00940A4B;
StringGrid1.Canvas.FillRect(Rect);
StringGrid1.Canvas.Font.Style := [fsBold];
StringGrid1.Canvas.Font.Color := clHighlightText;
StringGrid1.Canvas.TextOut(Rect.Left + 3, Rect.Top + 5,
StringGrid1.Cells[ACol, ARow]);
end;
end;
The Rect will be and ís the correct CellRect. The cliping effect is due to something else elsewhere.
But if there really is a spurious +4 in the source code of XE8 like Tom Brunberg mentions, which is easily overcome with -4, then that obviously is a bug and should be reported.

MediaPlayer returning wrong mouse position

I've run into a strange problem with the MediaPlayer component
that appears to be a bug with either the API or Delphi.
I display the video directly on the Form2 canvas and allow
the user to zoom and drag the video within the window.
I use the Form2.MouseDown and MouseUp events to drag.
The drag was behaving erratically and I traced the problem
to inconsistent use of the window coordinates in the events.
An MPG or WMV video (compressed) will report the MouseDown
coordinates relative to the video, but the MouseUp relative
to the form. An AVI video (uncompressed) will report both
relative to the form.
I'm using Delphi XE3 with Windows 7.
Has anyone else encountered this anomaly, and how can I
get consistent X,Y coordinates?
Added 7/20:
I don't know what MCVE means, but I added some code in case someone
wants to try and duplicate the problem.
Label1 & Label2 report the mouse coordinates, and if the video is moved
out of the (0,0) position then the coordinates will jump as the mouse
crosses into or out of the video. It should not do that, it should
always report coordinates relative to the window, not the video.
FormMouseUp will always report relative to the window.
FormMouseDown and FormMouseMove jump back and forth.
procedure TForm1.MFLoadFileClick(Sender: TObject);
begin
MediaPlayer1.Open;
MediaPlayer1.Display := Form2;
end;
procedure TForm2.FormMouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
Label1.Caption := IntToStr(X); {diag}
Label2.Caption := IntToStr(Y); {diag}
VidLoc := Form1.MediaPlayer1.DisplayRect;
mX := X; mY := Y;
MouseDown := True;
end;
procedure TForm2.FormMouseMove(Sender: TObject;
Shift: TShiftState; X, Y: Integer);
begin
Label1.Caption := IntToStr(X); {diag}
Label2.Caption := IntToStr(Y); {diag}
end;
procedure TForm2.FormMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var VL : tRect;
begin
Label1.Caption := IntToStr(X); {diag}
Label2.Caption := IntToStr(Y); {diag}
if (MouseDown and Form1.Loaded) then begin
VL := VidLoc;
VL.Left := VidLoc.Left - mX + X;
VL.Top := VidLoc.Top - mY + Y;
Form1.MediaPlayer1.DisplayRect := VL;
Form1.MediaPlayer1.Step;
end;
MouseDown := False;
end;
Oddly enough, moving the video in a panel doesn't work, but moving the panel did.
I didn't want to do that because it requires keeping track of too many objects, but it'll have to do.
Moving the panel uses the same code as above, but with other assorted properties.

How to hide TActionMainMenuBar images

I have TActionMainMenuBar placed on the form, which looks like this:
Now, it looks perfectly fine except that blank gap on the left where images should go. Since I don't have need to draw images in the menu, how can I hide that gap completely? Haven't been able to find any properties which I can use to hide this, and Google queries returned no results on the topic.
Below sample tries to demonstrate what it would take to use your own menu style. It just tries to gain the space from the unused images but you can override any aspect of the drawing, see 'xpactnctrls.pas' for possible implementation.
type
TBarStyle = class(TXPStyleActionBars)
public
function GetControlClass(ActionBar: TCustomActionBar;
AnItem: TActionClientItem): TCustomActionControlClass; override;
end;
TMenuStyle = class(TXPStyleMenuItem)
protected
procedure CalcLayout; override;
public
procedure CalcBounds; override;
end;
var
BarStyle: TBarStyle;
function TBarStyle.GetControlClass(ActionBar: TCustomActionBar;
AnItem: TActionClientItem): TCustomActionControlClass;
begin
Result := inherited GetControlClass(ActionBar, AnItem);
if ActionBar is TCustomActionPopupMenu then
Result := TMenuStyle;
end;
procedure TMenuStyle.CalcLayout;
begin
inherited;
GlyphPos := Point(-16, GlyphPos.Y);
end;
procedure TMenuStyle.CalcBounds;
var
R: TRect;
begin
inherited;
R := TextBounds;
OffsetRect(R, -16, 0);
TextBounds := R;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ActionMainMenuBar1.ActionManager.Style := BarStyle;
end;
initialization
BarStyle := TBarStyle.Create;
RegisterActnBarStyle(BarStyle);
finalization
UnregisterActnBarStyle(BarStyle);
BArStyle.Free;

Scroll TTreeView while dragging over/near the edges

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

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