Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why a form with 500 components is slow?

Tags:

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?

like image 347
Tom Avatar asked Oct 29 '12 19:10

Tom


2 Answers

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).

like image 69
GolezTrol Avatar answered Oct 09 '22 11:10

GolezTrol


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.

like image 28
Warren P Avatar answered Oct 09 '22 11:10

Warren P