Wyświetlenie dużej ilości elementów w komponencie

Operując na bardzo długich listach, dowolnego typu, wymagane jest czasem wyświetlenie ich zawartości w jakiejś kontrolce.
Przy niewielkiej ilości elementów, powiedzmy tak do stu tysięcy (100 000), w dobie dzisiejszych komputerów niemal nie odczujemy problemu z odświeżeniem kontrolki z taką zawartością.
Co gdy jednak do naszej listy będziemy dodawać kolejne pozycje i za każdym razem odświeżać zawartość kontrolki. Tak przy pięciuset tysiącach pozycji operacja odświeżenia zacznie nas denerwować a przy milionie elementów będzie nie do zniesienia.

Przykładowe testy

Załóżmy nowy projekt (VCL Forms Application), na formatce umieśćmy trzy kontrolki: listBox, listView i treeView - jedna obok drugiej na kształt trzech kolumn. Pod każdą z powyższych trzech kontrolek umieśćmy przycisk, uruchamiający odpowiednie procedury odświeżające dane w każdym z powyższych komponentów. Załóżmy też, że nasza lista elementów jest typu TStringList i jest dostępna wewnątrz klasy formularza.
Deklaracja listy:

type
  TMojForm = class(TForm)
  //tu deklaracja kontrolek i procedur generowana automatycznie
  private
    { Private declarations }
    lista : TStringList; //deklaracja naszej listy
  public
    { Public declarations }
  end;

Nasza lista elementów inicjaliowana jest w konstruktorze formularza, zetem FormCreate wygląda jak poniżej

procedure TMojForm.FormCreate(Sender: TObject);
var ile_elementow : Integer;
begin
//inicjalizacja listy
Self.lista := TStringList.Create();

//określenie ile elementów ma znajdować się na liście
ile_elementow := 1000000;

//wypełnienie listy milionem elementów
//wypełnienie odbywa się w procedurze
Self.DodajDoListyXelementow(ile_elementow);
end;

Jednocześnie trzeba pamiętać o tym, żeby po wyjściu z programu, po jego zamknięciu zwolnić pamięć zajmowaną przez listę. Stąd zdarzenie FormClose będzie wyglądać jak poniżej:

procedure TMojForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
//na zakończenie programu zwalniana jest pamięć zajmowana przez listę
if (Self.lista <> nil) then
   FreeAndNil(Self.lista);
end;

W konstruktorze formularza wywoływana jest procedura DodajDoListyXelementow, kod procedury jest następujący:

procedure TMojForm.DodajDoListyXelementow(const ile_elementow : integer);
var i : Integer;
begin
//wypełnienie listy przekazaną ilością elementów
for i := 0 to ile_elementow-1 do
  Self.lista.Append(IntToStr(i+1));
end;

Powyższa procedura jest prywatna, zatem jej deklaracja znajduje się w sekcji Private declarations.
Podepnijmy teraz i wypełnijmy kodem zdarzenia na kliknięcie w poszczególne przyciski.
Ten pod listBoxem (o nazwie btn_DemoListBox) będzie wykonywał poniższe operacje:

/// 
/// Procedura wypełniająca ListBoxa zawartością listy
/// 
procedure TMojForm.btn_DemoListBoxClick(Sender: TObject);
begin
//wyczyszczenie zawartości kontrolki
listBox_Demo.Items.Clear();
listBox_Demo.Items.BeginUpdate();

//wypełnienie zawartością listy
listBox_Demo.Items.Assign(Self.lista);

listBox_Demo.Items.EndUpdate();
end;

Przycisk pod listViewem (o nazwie btn_DemoListView) będzie wykonywał poniższe operacje:

/// 
/// Procedura wypełniająca ListViewa zawartością listy
/// 
procedure TMojForm.btn_DemoListViewClick(Sender: TObject);
var i : Integer;
    listItem : TListItem;
begin
//wyczyszczenie zawartości kontrolki
listViewDemo.Items.Clear();
listViewDemo.Items.BeginUpdate();

//wypełnienie zawartością listy
for i := 0 to Self.lista.Count - 1 do
 begin
      listItem := listViewDemo.Items.Add;
      listItem.Caption := Self.lista[i];
 end;

listViewDemo.Items.EndUpdate();
end;

Przycisk pod treeViewem (o nazwie btn_DemoTreeView) będzie wykonywał poniższe operacje:

/// 
/// Procedura wypełniająca TreeViewa zawartością listy
/// 
procedure TMojForm.btn_DemoTreeViewClick(Sender: TObject);
var i : Integer;
    node : TTreeNode;
begin
//wyczyszczenie zawartości kontrolki
treeViewDemo.Items.Clear();
treeViewDemo.Items.BeginUpdate();

//wypełnienie zawartością listy
for i := 0 to Self.lista.Count - 1 do
  begin
        node := treeViewDemo.Items.Add(nil, 'Node' + Self.lista[i]);
  end;

treeViewDemo.Items.EndUpdate();
end;

Wynik testów

Wypełnienie listBoxa milionem (1 000 000) pozycji zajmuje 48s.
Wypełnienie listViewa tą samą ilością pozycji zajmuje ponad 30min, a treeViewa około 30min.

Czas odświeżania elementów w każdej z tych trzech kontrolek jest nie do przyjecia, jednak jak widać listBox jest zdecydowanie najszybszy.

Jak zatem osiągnąć satysfakcjonujące rozwiązanie.

Proponowane rozwiązanie

Kontrolki wizualne działają znacznie wolniej przy dużej ilości elementów. Aby przyspieszyć ich działanie można wyświetlać tylko część z kolekcji danych, z zawartości listy. W tym celu dane trzymamy w osobnym obiekcie (tak jak nasza przykładowa lista) a do listBoxa wstawiamy tylko kilka pozycji, te które mają być w danej chwili widoczne.
Jak osiągnąć zamierzony cel?
Dodajmy na formę kolejny listBox i pionowy scrollBar. Zmodyfikujmy też zawartość procedury DodajDoListyXelementow

procedure TMojForm.DodajDoListyXelementow(const ile_elementow : integer);
var i : Integer;
begin
//wypełnienie listy przekazaną ilością elementów
for i := 0 to ile_elementow-1 do
  Self.lista.Append(IntToStr(i+1));

//obliczenie MAX dla ScrollBar-a
//ważne jest aby robić to po każdej zmianie
//ilości elementów na liście
scrollBar.Max := Self.ObliczMAXScrollBara();
end;

Dodatkowo dodajmy funkcję liczącą ilość elementów mogących jednorazowo znajdować się w naszym listBoxie:

function TMojForm.ObliczIlePozycjiMaBycWidocznych() : Integer;
begin
Result := Floor(listBox.ClientHeight / listBox.ItemHeight) - 1;
end;

Dodajemy też funkcję liczącą Max dla scrollBara ObliczMAXScrollBara:

function TMojForm.ObliczMAXScrollBara() : Integer;
begin
Result := Self.lista.Count-1 - Self.ObliczIlePozycjiMaBycWidocznych();
end;

I procedurę WyswietlXelementowWKontrolce, która ma za zadanie odświeżania zawartości kontrolki

procedure TMojForm.WyswietlXelementowWKontrolce(const ile_elementow : integer; const startIndex : integer);
var i : Integer;
begin
//czyszczenie zawartości listBoxa
listBox.Items.Clear();

//wstawienie określonej ilości elementów do kontrolki
for i := startIndex to startIndex + ile_elementow do
  listBox.Items.Add(Self.lista[i]);
end;

Wszystkie powyższe funkcje są prywatne, ich deklaracje znajdują się w sekcji Private declarations. Ostatnią rzeczą jaką musimy zrobić to obsłużyć zdarzenie OnScroll naszego pionowego paska przewijania, który będzie odświeżał zawartość kontrolki.

procedure TMojForm.scrollBarScroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer);
var ile : Integer;
begin
//ilość widocznych pozycji w listBoxie można wyliczyć
//jeden raz ale tylko w przypadku gdy jego rozmiar jest stały
//w przeciwnym wypadku (jak poniżej) rozmiar liczony jest
//za każdym razem
ile := Self.ObliczIlePozycjiMaBycWidocznych();

//wypełnienie listBoxa określoną ilością elementów
Self.WyswietlXelementowWKontrolce(ile, ScrollPos);
end;

I to by było na tyle. Kod całego programu wygląda zetem tak:

unit UnitMojForm;

interface

uses
  Windows, SysUtils, Forms, Math, Controls, Classes, StdCtrls, ComCtrls;

type
  TMojForm = class(TForm)
    listBox: TListBox;
    scrollBar: TScrollBar;
    procedure FormCreate(Sender: TObject);
    procedure scrollBarScroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormResize(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private declarations }
    lista : TStringList;
    function ObliczIlePozycjiMaBycWidocznych() : Integer;
    function ObliczMAXScrollBara() : Integer;

    procedure WyswietlXelementowWKontrolce(const ile_elementow : integer; const startIndex : integer);
    procedure DodajDoListyXelementow(const ile_elementow : integer);
  public
    { Public declarations }
  end;

var
  MojForm: TMojForm;

implementation

{$R *.dfm}


procedure TMojForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
//na zakończenie programu zwalniana jest pamięć zajmowana przez listę
if (Self.lista <> nil) then
   FreeAndNil(Self.lista);
end;

procedure TMojForm.FormCreate(Sender: TObject);
var ile_elementow : Integer;
begin
//inicjalizacja listy
Self.lista := TStringList.Create();

//określenie ile elementów ma znajdować się na liście
ile_elementow := 1000000;

//wypełnienie listy milionem elementów
//wypełnienie odbywa się w procedurze
Self.DodajDoListyXelementow(ile_elementow);
end;

procedure TMojForm.FormResize(Sender: TObject);
begin
scrollBar.Max := Self.ObliczMAXScrollBara();
scrollBar.Position := 0;

Self.WyswietlXelementowWKontrolce(Self.ObliczIlePozycjiMaBycWidocznych(), 0);
end;

procedure TMojForm.FormShow(Sender: TObject);
begin
Self.WyswietlXelementowWKontrolce(Self.ObliczIlePozycjiMaBycWidocznych(), 0);
end;

procedure TMojForm.scrollBarScroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer);
var ile : Integer;
begin
//ilość widocznych pozycji w listBoxie można wyliczyć
//jeden raz ale tylko w przypadku gdy jego rozmiar jest stały
//w przeciwnym wypadku (jak poniżej) rozmiar liczony jest
//za każdym razem
ile := Self.ObliczIlePozycjiMaBycWidocznych();

//wypełnienie listBoxa określoną ilością elementów
Self.WyswietlXelementowWKontrolce(ile, ScrollPos);
end;

procedure TMojForm.DodajDoListyXelementow(const ile_elementow : integer);
var i : Integer;
begin
//wypełnienie listy przekazaną ilością elementów
for i := 0 to ile_elementow-1 do
  Self.lista.Append(IntToStr(i+1));

//obliczenie MAX dla ScrollBar-a
//ważne jest aby robić to po każdej zmianie
//ilości elementów na liście
scrollBar.Max := Self.ObliczMAXScrollBara();
end;

function TMojForm.ObliczIlePozycjiMaBycWidocznych() : Integer;
begin
Result := Floor(listBox.ClientHeight / listBox.ItemHeight) - 1;
end;

function TMojForm.ObliczMAXScrollBara() : Integer;
begin
Result := Self.lista.Count-1 - Self.ObliczIlePozycjiMaBycWidocznych();
end;

procedure TMojForm.WyswietlXelementowWKontrolce(const ile_elementow : integer; const startIndex : integer);
var i : Integer;
begin
//czyszczenie zawartości listBoxa
listBox.Items.Clear();

//wstawienie określonej ilości elementów do kontrolki
for i := startIndex to startIndex + ile_elementow do
  listBox.Items.Add(Self.lista[i]);
end;

end.

Kod źródłowy programu: duzo_elementow.zip (291)

Komentarze

W kodzie posługujecie się Państwo zakresem dla stringlisty typu int64.
Komponent TStringList ma Count typu integer, więc deklaracja jest nieprawidłowa.

Witam,
Rzeczywiście TStringList.Count jest typu Integer więc Int64 jest tu zbędne ale na pewno NIE jest nieprawidłowe :)
ile_elementow w procedurze wyświetlającej pozycje w ListBoxie mogło by być w zasadzie typu Word ponieważ nie ma chyba jeszcze tak dużych wyświetlaczy aby w kontrolce wyświetlić więcej niż 65 535 elementów.
Poza tym tak na marginesie TStringList nie jest komponentem, jest klasą :)

Dodaj nowy komentarz

Filtrowany HTML

  • Adresy internetowe są automatycznie zamieniane w odnośniki, które można kliknąć.
  • Dozwolone znaczniki HTML: <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Znaki końca linii i akapitu dodawane są automatycznie.
  • Tekstowe buźki będą zamieniane na ich graficzne odpowiedniki.

Czysty tekst

  • Znaczniki HTML niedozwolone.
  • Adresy internetowe są automatycznie zamieniane w odnośniki, które można kliknąć.
  • Znaki końca linii i akapitu dodawane są automatycznie.
  • Tekstowe buźki będą zamieniane na ich graficzne odpowiedniki.
CAPTCHA
W celu potwierdzenia, że jesteś człowiekiem, prosimy o wykonanie poniższego zadania
Target Image