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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With