ListVIEW

Menu

  1. ListView informacje podstawowe.
  2. Tworzenie listy raportu w trakcie działania programu.
  3. Zapisywanie i odczytywanie zawartości obiektu ListView do/z pliku.
  4. Edytowanie właściwości Item bezpośrednio w obiekcie ListView.
  5. Sortowanie zawartości obiektu ListView.


ListView informacje podstawowe.

    Obiekt ListView jest jednym spośród najmniej opisanych i najrzadziej używanych obiektów, wynika to pewnie stąd, że nie bardzo wiadomo jak go używać, a pomoc środowiska BCB na temat tego obiektu jest bardzo uboga i mało przejrzysta. W tej poradzie przedstawię możliwości tego obiektu tylko ogólnie, kolejne porady będą prezentowały w sposób szczegółowy zastosowanie tego obiektu.
   
ListView może być stosowany podobnie jak ListBox, jednak może dodatkowo zawierać oprócz elementów Items, również elementy SubItems, dodatkowo komponent posiada możliwość tworzenia kolumn. Przed utworzeniem listy w obiekcie należy najpierw utworzyć minimum jedną kolumnę poprzez wybranie w Inspektorze Obiektów właściwości Columns. Utworzenie jednej kolumny umożliwia utworzenie tylko listy przypominającej ListBox, jednak w odróżnieniu od tego obiektu możliwe jest włączenie nagłówka poprzez ustawienie właściwości ShowColumnHeader na true. ListView posiada dwa zasadnicze elementy umieszczające wartości na liście, pierwszy element to właściwość Items, kliknięcie na tej właściwości w Inspektorze Obiektów wywoła okno ListView Items Editor, w oknie tym można klikając na przycisku New Item utworzyć nowy element listy (Items), jest ona zawsze umieszczany w pierwszej kolumnie, przy czym
kolumny numerowane są od 0, więc element ten jest umieszczany w kolumnie 0. Nazwę temu elementowi nadaje się poprzez wypełnienie jego właściwości Caption. Do każdej właściwości Items można przypisać właściwość SubItems klikając na przycisku New SubItem. Właściwość SubItems jest zawsze przypisywana do zaznaczonej właściwości Items i można tworzyć wiele tych właściwości przypisanych do różnych właściwości Items. Właściwości SubItems są umieszczane w kolejnych kolumnach obiektu ListView, więc jeżeli np. utworzymy 4 kolumny to możemy utworzyć tylko dla każdej właściwości Items tylko 3 właściwości SubItems, jeśli utworzymy ich więcej, to wszystkie ponad 3 nie będą widoczne. Można tworzyć dowolną liczbę Items i jak napisałem wyżej ta właściwość zawsze jest umieszczana w kolumnie 1 (kolumna 0), natomiast właściwości SubItems są umieszczane w kolejnych kolumnach, w kolejności w jakiej są dodawane do listy.


Właściwości
Items w połączeniu z SubItems są dostępne dla ListView tylko wtedy gdy jego właściwość ViewStyle jest ustawiona na vsReport, w pozostałych przypadkach widoczna jest tylko właściwość Items, a SubItems jest ignorowana. Istnieje możliwość przypisywania właściwościom Items i SubItems ikon poprzez dodanie do projektu obiektu ImageList i umieszczenia w nim ikon i bitmap. Żeby móc dodawać do właściwości Items ikony, należy we właściwości StateImages włączyć obiekt ImageList. Ikony do właściwości Items przypisuje się poprzez zmianę wartości parametru State Index, wartośc -1 oznacza brak ikony, wszystkie wartości powyżej przypisują kolejne ikony do tej właściwości z obiektu ImageList. Przypisanie obiektu ImageList do właściwości SmallImages i LargeImages umożliwi przypisywanie ikon do właściwości SubItems poprzez właściwość Image Index. Jak widać właściwość Items posiada dwie właściwości umożliwiające przypisanie ikon, pierwsza to Image Index przypisująca ikony do tej właściwości przy ustawionym ViewStyle na vsIcon, pod uwagę brane są ikony umieszczone w obiekcie ImageList przypisanym do LargeImage w trybie vsIcon, natomiast we wszystkich pozostałych trybach ViewStyle przy ustawionym parametrze Image Index brane są pod uwagę ikony umieszczone w ImageList przypisanym do właściwości SmallImages. Druga właściwość State Index bierze pod uwagę ikony umieszczone w ImageList przypisanym do właściwości StateImages obiektu ListView. Parametr State Index jest brany pod uwagę we wszystkich trybach właściwości ViewStyle. Do właściwości Items można przypisać dwie ikony, pierwsza pochodzi z ImageList przypisanego do StateImages, a druga z ImageList przypisanego do SmallImages, a w trybie ViewStyle = vsIcon z ImageList przypisanego do LargeImages. Właściwość SubItems pobiera ikony tylko z ImageList przypisanego do SmallImages i tylko w trybie ViewStyle = vsReport. Kolumnom również można przypisywać ikony, ale tylko w trybie ViewStyle = vsReport i tylko z ImageList przypisanego do właściwości SmallIcon.
W trybie
ViewStyle ustawionym na vsIcon wyświetlanę są duże ikony na ListView (o ile zostały przypisane), a pod ikonami przypisywany jest tekst. o tym czy tekst ma być zawijany decyduje właściwość IconOptions -> WrapText. Przy ViewStyle ustawionym na vsSmallIcon wyświetlane są ikony z tekstem po prawej stronie, o tym czy w tych trybach ikony są rozmieszczane w poziomie czy w pionie, decyduje właściwość IconOptions -> Arangement.

...powrót do menu. 

Tworzenie listy raportu w trakcie działania programu.

    Lista raportu, zawiera elementy typu Items z przypisanymi do nich elementami SubItems znajdujące się w jednym wierszu, sposób tworzenia takiej listy na etapie programowania opisałem w poradzie ListView informacje podstawowe, dlatego w tej poradzie pokażę jak tworzyć taką listę w trakcie działania programu. Dodawanie nowych elementów Items do listy odbywa się za pomocą funkcji Add(), funkcja nie pobiera żadnych elementów, a tworzy jedynie pusty element typu Items nie zawierający nazwy, nazwę temu elementowi przypisuje się za pomocą właściwości Caption. Właściwość ViewStyle obiektu ListView1 należy ustawić na vsReport:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 ListView1->Items->Add();
 ListView1->Items->Item[0]->Caption = "Item 0";
}
//--------------------------------

Na żółto zaznaczyłem wartość liczbową określającą, któremu elementowi listy przypisujemy właściwość Caption. W tak skonstruowanym kodzie, klikanie w przycisk będzie powodowało tworzenie nowych elementów listy (Items) , ale wartość Caption zawsze będzie zmieniana tylko dla pierwszego elementu listy, numerowanie odbywa się od 0. Jeżeli chcemy, żeby przy każdym kliknięciu przycisku przypisywać Caption nowo utworzonemu elementowi, należy dodać zmienną static int:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 static int i = 0;
 ListView1->Items->Add();
 ListView1->Items->Item[i]->Caption = "Item " + IntToStr(i);
 i++;
}
//--------------------------------

Zmienna statyczna ma to do siebie, że raz zainicjowana jakąś wartością, nie jest inicjowana ponownie przy każdym wywołaniu, lecz przechowuje zawsze ostatnią zapamiętaną wartość, dlatego jeżeli chcemy jej zmienić tą wartość, przywracając np początkową to należ ją jej przypisać. Ten sam efekt można osiągnąć posługując się zamiast zmiennej static klasą TListItem, klasa ta jest wykorzystywana przez klasę TListView:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 TListItem *Li;
 Li = ListView1->Items->Add();
 Li->Caption = "Items";
 Li = ListView1->Items->Add();
 delete Li;
}
//--------------------------------

Należy zwrócić uwagę na dwukrotne użycie funkcji Add() przed dodaniem elementu listy i po jego dodaniu, w poprzednim przykładzie byłoby to niedopuszczalne, a przy tej metodzie jest niezbędne.
Przedstawione przykłady dodawały jak dotychczas do listy tylko elementy Items, ale można oczywiście dodawać również elementy SubItems, przy czym należy można je dodawać razem z elementami Items, lub później podają numer elementu Item do którego ma być przypisany SubItem, oto przykład dodawania elementów SubItems razem z Items - pierwszy z wykorzystaniem operatora static, drugi z wykorzystaniem klasy TListItem:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 static int i = 0;
 ListView1->Items->Add();
 ListView1->Items->Item[i]->Caption = "Item " + IntToStr(i);
 ListView1->Items->Item[i]->SubItems->Add("SubItem " + IntToStr(i) + "A");
 ListView1->Items->Item[i]->SubItems->Add("SubItem " + IntToStr(i) + "B");
 i++;
}
//--------------------------------


// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 TListItem *Li;
 Li = ListView1->Items->Add();
 Li->Caption = "Items";
 Li->SubItems->Add("SubItem 1");
 Li->SubItems->Add("SubItem 2");
 Li = ListView1->Items->Add();
 delete Li;
}
//--------------------------------

Jak napisałem wyżej elementy SubItems można przypisywać do elementów Items, później:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 ListView1->Items->Item[2]->SubItems->Add("SubItem 2A");
 ListView1->Items->Item[2]->SubItems->Add("SubItem 2B");
}
//--------------------------------

Spowoduje to dodanie nowych elementów SubItems do trzeciego w kolejności elementu Item. Możliwe jest również zmienianie nazwy elementów Item i SubItem. W przypadku Item sprawa jest oczywista ponieważ wystarczy posłużyć się właściwością Caption, natomiast w przypadku SubItems, trzeba skorzystać z właściwości Strings przypisując jej numer elementu SubItems:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 ListView1->Items->Item[2]->Caption = "Zmiana Items";
 ListView1->Items->Item[2]->SubItems->Strings[0] = "Zmiana 1 SubItem");
 ListView1->Items->Item[2]->SubItems->Strings[1] = "Zmiana 2 SubItem");
}
//--------------------------------

Elementy Items i SubItems można również tworzyć wewnątrz pętli:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 for(int i = 0; i < 10; i++)
 {
  ListView1->Items->Add();
  ListView1->Items->Item[i]->Caption = "Item " + IntToStr(i);
  for(int j = 0; j < 5; j++)
  {
   ListView1->Items->Item[i]->SubItems->Add("SubItem " + IntToStr(i) + "-" + IntToStr(j));
  }
 }
}
//--------------------------------

Można również przypisywać ikony do elementów Items i SubItems:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 static int i = 0;
 ListView1->Items->Add();
 ListView1->Items->Item[i]->Caption = "Item " + IntToStr(i);
 ListView1->Items->Item[i]->ImageIndex = i;
 ListView1->Index->Item[i]->StateIndex = i;

 ListView1->Items->Item[i]->SubItems->Add("SubItem " + IntToStr(i) + "A");
 ListView1->Items->Item[i]->SubItemImages[0] = i;
 ListView1->Items->Item[i]->SubItems->Add("SubItem " + IntToStr(i) + "B");
 ListView1->Items->Item[i]->SubItemImages[1] = i + 1;
 i++;
}
//--------------------------------

...powrót do menu. 

Zapisywanie i odczytywanie zawartości obiektu ListView do/z pliku.

ListView posiada bardzo ubogą funkcję zapisywania i odczytywania własnej zawartości:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 ListView1->Items->Item[0]->SubItems->SaveToFile("Nazwa pliku.roz");
}
//--------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
 ListView1->Items->Item[0]->SubItems->LoadFromFile("Nazwa pliku.roz");
}
//--------------------------------

I to jest w zasadzie wszystko, jak widać można w ten sposób zapisać tylko zawartość wybranego elementu Item i to zapiszą się tylko elementy SubItem, sam Item nie zostanie zapisany, dlatego jeżeli chcemy zapisać wszystko co znajduje się w obiekcie ListView lepiej jest posłużyć się klasą TFileStream i funkcjami WriteComponent i ReadComponent:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 TFileStream *File = new TFileStream("nazwa pliku.roz", fmCreate);
 File->WriteComponent(ListView1);
 delete File;
}
//--------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
 TFileStream *File = new TFileStream("nazwa pliku.roz", fmOpenRead | fmShareExclusive);
 File->ReadComponent(ListView1);
 delete File;
}
//--------------------------------

Taki sposób zapisania komponentu z zawartością jest szybki i bardzo wygodny, zapisana zostanie nie tylko zawartość obiektu ListView, ale również zawartość obiektów ImageList przypisanych do ListView. Występuje jednak pewna wada, jeżeli ustawimy szerokość kolumn na auto (Columns->AutoSize = true), to ten parametr nie zostanie zapisany do pliku, dlatego przed zapisaniem lepiej jest ustawić AutoSize na false.

...powrót do menu. 

Edytowanie właściwości Item bezpośrednio w obiekcie ListView.

Edytowanie właściwości Item, czyli zmiana wartości może być dokonana za pomocą kodu poprzez zmianę właściwości Caption, o czym było w poradzie tworzenie listy raportu w trakcie działania programu. Ta porada dotyczy jednak innego sposobu, takiego w którym klikając np. dwukrotnie na wybranej właściwości Item poddajemy ją do edycji tak jak to ma np. miejsce w Edit1:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::ListView1DblClick(TObject *Sender)
{
 ListView1->Items->Item[ListView1->ItemIndex]->EditCaption();
}
//--------------------------------

Ten sposób pozwala na poddanie edycji tylko właściwości Item[x]->Caption, nie istnieje natomiast żadna funkcja umożliwiająca poddanie takiej edycji właściwości SubItem, można jednak coś takiego stworzyć używając obiektu Edit1. Nie można jednak określić w której kolumnie wybrano właściwość Items, inaczej niż poprzez kliknięcie na nagłówku kolumny, więc trzeba kod dostosować do tego, że kliknięciem w nagłówek wybieramy z której kolumny ma być edytowana właściwość SubItems, lub podajemy numer kolumny jawnie wewnątrz kodu. Obiekt Edit należy przypisać do ListView, a następnie ustawiać jego pozycję w zależności od wybranej kolumny:

// Plik nagłówkowy np. Unit1.h
//--------------------------------

private:
        int col;


// Plik źródłowy np. Unit1.cpp
//--------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
 Edit1->Parent = ListView1;
}
//--------------------------------
void __fastcall TForm1::ListView1ColumnClick(TObject *Sender,
        TListColumn *Column)
{
 col = Column->Index;
}
//--------------------------------
void __fastcall TForm1::ListView1DblClick(TObject *Sender)
{
 if(col > 0)
 {
  int left = 0;
  for(int i = 0; i < col; i++)
  {
   left = left + ListView1->Column[i]->Width;
  }
  Edit1->Visible = true;
  Edit1->Top = ListView1->Items->Item[ListView1->ItemIndex]->Top;
  Edit1->Left = ListView1->Items->Item[0]->Left + left;
  Edit1->Width = ListView1->Column[col]->Width;
 }
 else
  Edit1->Visible = false;
}
//--------------------------------
void __fastcall TForm1::ListView1Click(TObject *Sender)
{
 Edit1->Visible = false;
}
//--------------------------------

...powrót do menu. 

Sortowanie zawartości obiektu ListView

Zawartość kolumn obiektu ListView można posortować klikając na te kolumny z wykorzystaniem funkcji AlphaSort() wywoływanej w zdarzeniu OnColumnClick tegoż obiektu. Właściwość SortType obiektu ListView należy ustawić na stNone.


// Plik źródłowy np. Unit1.cpp
//--------------------------------

void __fastcall TForm1::ListView1ColumnClick(TObject *Sender,
      TListColumn *Column)
{
 ((TListView *)Sender)->AlphaSort();
}
//--------------------------------

To proste wywołanie funkcji pozwala posortować tylko pierwszą kolumnę i tylko rosnąco i tylko jako tekst. W celu posortowanie dowolnej kolumny i rosnąco lub malejąco jako tekst należy skorzystać z funkcji CustomSort przekazując jej jako pierwszy argument wartość NULL (0), a jako drugi argument zmienną sterującą przyjmującą raz wartość 0, drugi raz 1. Dzięki takiemu sterowaniu zmienną jedno kliknięcie na kolumnę będzie sortowało kolumnę rosnąco, a drugie kliknięcie zmieni kierunek sortowania i posortuje ją malejąco. Wartość tej zmiennej jest przekazywana do zdarzenia OnCompare poprzez argument Data tegoż zdarzenia, dlatego trzeba jeszcze stworzyć dodatkowe zdarzenie OnCompare dla obiektu ListView. W tym zdarzeniu będzie poddawana sortowaniu wybrana kolumna. O tym która kolumna ma być sortowana zadecyduje zmienna Tag wchodząc w skład obiektu ListView i to właśnie przez tą zmienną będzie przekazywany numer wybranej kolumny do zdarzenia OnCompare:

//--------------------------------
void __fastcall TForm1::ListView1ColumnClick(TObject *Sender,
      TListColumn *Column)
{
 static bool test = false;
 ((TListView *)Sender)->Tag = Column->Index;
 ((TListView *)Sender)->CustomSort(NULL, (long)test);
 test = !test;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ListView1Compare(TObject *Sender, TListItem *Item1,
      TListItem *Item2, int Data, int &Compare)
{
 if(((TListView *)Sender)->Tag == 0)
 {
  Compare = CompareText(Item1->Caption, Item2->Caption);
 }
 else
 {
  int c = ((TListView *)Sender)->Tag - 1;
  Compare = CompareText(Item1->SubItems->Strings[c], Item2->SubItems->Strings[c]);
 }
 if(Data == 1) Compare = -Compare;
}
//--------------------------------

To oczywiście działa, ale zawartości kolumn są sortowane jako tekst, w przypadku gdy będą się w którejś kolumnie znajdowały liczby, to również zostaną potraktowane jako tekst i tak posortowane.

W celu posortowania kolumn zgodnie z ich zawartości, czyli tekst jest sortowany jako tekst, a liczby jako liczby, można posłużyć się dwiema metodami, które różnią się od siebie tym, że pierwsza metoda zawiera cały mechanizm sortujący w zdarzeniu OnCompare, druga zaś korzysta z funkcji pomocniczej. W obydwu przypadkach trzeba jednak stworzyć funkcję porównującą liczby, taką jak CompareText porównującą tekst, funkcji tej nadam nazwę foo. Zawartość zdarzenia OnCompare również bardzo się zmienia. Do sprawdzania czy mamy do czynienia z tekstem czy z liczbą posłużyłem się prostą metodą try catch, co podczas testowania aplikacji w środowisku BCB może zaowocować zgłaszaniem ostrzeżenia przez kompilator, ale w trakcie normalnej pracy programu tego komunikatu nie będzie, ale to tak na marginesie, żebyś się nie przestraszył, że coś tutaj nie działa.

// Plik źródłowy np. Unit1.cpp
//--------------------------------

  #pragma warn +csu 
double foo(double u, double i)
{
 double z = u - i;
 return z;
}
//--------------------------------
void __fastcall TForm1::ListView1ColumnClick(TObject *Sender,
      TListColumn *Column)
{
 static bool trend = false;
 ((TListView *)Sender)->Tag = Column->Index;
 ((TListView *)Sender)->CustomSort(NULL, (long)trend);
 trend = !trend;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ListView1Compare(TObject *Sender, TListItem *Item1,
      TListItem *Item2, int Data, int &Compare)
{
 bool test = true;
 String cText1, cText2;

 if(((TListView *)Sender)->Tag == 0)
 {
  cText1 = Item1->Caption;
  cText2 = Item2->Caption;
 }
 else
 {
  int c = ((TListView *)Sender)->Tag - 1;
  cText1 = Item1->SubItems->Strings[c];
  cText2 = Item2->SubItems->Strings[c];
 }

 double rTemp1, rTemp2;

 try{ rTemp1 = cText1.Trim().ToDouble(); } catch(...){ test = false;}
 try{ rTemp2 = cText2.Trim().ToDouble(); } catch(...){ test = false;}

 if(!test)
 {
  Compare = CompareText(cText1.Trim(), cText2.Trim());
 }
 else
 {
  Compare = foo(rTemp1, rTemp2);
 }
 if(Data == 1) Compare = -Compare;
}
//--------------------------------

Druga z prezentowanych metod nie korzysta ze zdarzenia OnCompare, lecz z funkcji CustomAutoSort przekazywanej jako argument funkcji CustomSort, w tym przypadku potrzebna jest również zmienna sterująca kierunkiem sortowania, ale jest to już zmienna globalna sDirection, cała reszta jest bardzo podobna:

// Plik źródłowy np. Unit1.cpp
//--------------------------------

 #pragma warn +csu
double foo(double u, double i)
{
 double z = u - i;
 return z;
}
//--------------------------------
BOOL sDirection = true;
int __stdcall CustomAutoSort(long Item1, long Item2, long lParam)
{
 bool test = true;
 String cTemp1, cTemp2;
 int c = ((TListView *)lParam)->Tag - 1;
 if(c == -1)
 {
  cTemp1 = ((TListItem *)Item1)->Caption;
  cTemp2 = ((TListItem *)Item2)->Caption;
 }
 else
 {
  cTemp1 = ((TListItem *)Item1)->SubItems->Strings[c];
  cTemp2 = ((TListItem *)Item2)->SubItems->Strings[c];
 }

 double rTemp1, rTemp2;

 try{ rTemp1 = cTemp1.Trim().ToDouble(); } catch(...){ test = false;}
 try{ rTemp2 = cTemp2.Trim().ToDouble(); } catch(...){ test = false;}

 if(!test)
 {
  if(!sDirection) return CompareText(cTemp1.Trim(), cTemp2.Trim());
  return -CompareText(cTemp1.Trim(), cTemp2.Trim());
 }

 if(!sDirection) return foo(rTemp1, rTemp2);
 return -foo(rTemp1, rTemp2);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ListView1ColumnClick(TObject *Sender,
      TListColumn *Column)
{
 sDirection = !sDirection;
 ((TListView *)Sender)->Tag = Column->Index;
 ((TListView *)Sender)->CustomSort(CustomAutoSort, (long)((TListView *)Sender));
}
//--------------------------------

...powrót do menu.