Ein ListView sortieren
Jeder hat schon einmal ein ListView in der Report-Ansicht gesehen, z. b. im Windows-Explorer. Durch Klicken auf den Header kann man die Ansicht sortieren:
Sortierung in einem ListView |
Hier wird aufsteigend nach "Name" sortiert.
Leider ist es in Delphi (mindestens bis Version 7) nicht ohne Weiteres möglich, die Darstellung so zu beeinflussen, dass
- der Pfeil erscheint, der die Richtung anzeigt und
- die sortierte Spalte farblich markiert wird.
Mit geringem Aufwand ist es einfach, diese Funktionalität in Delphi nachzurüsten.
Achtung: Wenn die Richtungspfeile angezeigt werden, können im Spaltenheader keine Bilder mehr angezeigt werden.
Teilen...
Wenden wir uns zuerst dem Sortieren des ListView zu. Der Einfachheit halber habe ich eine Unit geschrieben, die die Sortierung für uns übernimmt. Die Deklaration ist recht einfach gehalten:type TSortDirection = (sdUp, sdDown); procedure Sort(const LV: TCustomListView; const Column: Integer; Direction: TSortDirection);Es wird also das ListView übergeben, der Spaltenindex sowie die Richtung. Die Funktion erledigt den Rest.
Als Besonderheit implementiert meine Funktion die "Natürliche Sortierung", d. h. Zahlen werden nach ihrem Wert sortiert, und nicht nach der ersten Ziffer. Der Unterschied wird schnell deutlich:
Klassische Sortierung |
Natürliche Sortierung |
Diese Funktion müssen wir jetzt in unser Programm einbauen. Dazu müssen wir uns erstmal die bisherige Sortierung merken. Dafür bietet sich die "Tag"-Eigenschaft der Spaltenköpfe an. Steht diese auf -1, so ist die Spalte unsortiert, bei 0 aufsteigend und bei 1 absteigend sortiert. Deshalb etzen wir diese anfangs überall auf -1.
Ein ListView hat ein Ereignis "OnColumnClick". Das ist genau das, was wir brauchen. Nach einem Doppelklick im Objektinspektor auf das Ereignis befinden wir uns im Rumpf der Methode. Hier muss folgendes eingegeben werden:
procedure TForm1.ListView1ColumnClick(Sender: TObject; Column: TListColumn); var C: Integer; begin // Wir setzen alle anderen Spalten auf "nicht sortieren" for C := 0 to TListView(Sender).Columns.Count - 1 do if TListView(Sender).Columns[C] <> Column then TListView(Sender).Columns[C].Tag := -1; // war die Spalte schon sortiert? if Column.Tag = -1 then Column.Tag := 0 // nein -> aufwärts else // sonst Sortierreihenfolge umdrehen Column.Tag := 1 - Column.Tag; if Column.Tag = 0 then// wir sortieren aufwärts Sort(TListView(Sender), Column.Index, sdUp) else // wir sortieren abwärts Sort(TListView(Sender), Column.Index, sdDown); end;Damit ist das Sortieren eigentlich schon fertig. Bei jedem Klick auf einen Spaltenknopf ändert sich die Sortierung.
...und herrschen
Was ist jetzt aber mit dem Pfeil und der Markierung? Die Markierung gibt es erst ab Version 6.0 der Common Controls, und ohne diese müssen wir die Pfeile auch noch selbst zeichnen. Aus diesem Grund ist auch eine Ressourcendatei dabei, die Bilder der Pfeile enthält.Um die Markierung zu aktivieren, benutzt man am Besten folgendes Makro:
ListView_SetSelectedColumn(ListView1.Handle, Index);wobei der Index der Spalte übergeben wird. Dieses Makro ist in meiner Unit integriert.
Um den Pfeil darzustellen, ist mehr Aufwand nötig. Dafür sind zwei Prozeduren zuständig. Die erste Prozedur entfernt den Pfeil, die zweite setzt ihn:
procedure RemoveImage(const LV: TCustomListView; Index: Integer); var Header: THandle; Version: Integer; HDItem: THDItem; begin Header := ListView_GetHeader(LV.Handle); // Handle des Headers holen Version := SendMessage(Header, CCM_GETVERSION, 0, 0); // Version ermitteln FillChar(HDItem, SizeOf(HDItem), 0); HDItem.Mask := HDI_FORMAT; if Version < 6 then HDItem.Mask := HDItem.Mask or HDI_BITMAP or HDI_IMAGE; Header_GetItem(Header, Index, HDItem); // jetzigen Status sichern HDItem.fmt := HDItem.fmt and not HDF_SORTUP and not HDF_SORTDOWN;// Pfeil löschen if Version < 6 then begin HDItem.fmt := HDItem.fmt and not HDF_BITMAP and not HDF_IMAGE; HDItem.iImage := I_IMAGENONE; end; Header_SetItem(Header, Index, HDItem); // und abschicken end; procedure SetImage(const LV: TCustomListView; Index: Integer; Direction: TSortDirection); var Header: THandle; Version: Integer; HDItem: THDItem; begin // Handle des Headers holen Header := ListView_GetHeader(LV.Handle); Version := SendMessage(Header, CCM_GETVERSION, 0, 0); // Version ermitteln FillChar(HDItem, SizeOf(HDItem), 0); HDItem.Mask := HDI_FORMAT; if Version < 6 then HDItem.Mask := HDItem.Mask or HDI_BITMAP or HDI_IMAGE; Header_GetItem(Header, Index, HDItem); // jetzigen Status sichern HDItem.fmt := HDItem.fmt and not HDF_SORTUP and not HDF_SORTDOWN;// Pfeil soll rechts erscheinen if Version < 6 then begin HDItem.fmt := HDItem.fmt and not HDF_IMAGE or HDF_BITMAP_ON_RIGHT; Header_SetImageList(Header, hImageList); // Bilder aus Imagelist verwenden HDItem.fmt := HDItem.fmt or HDF_IMAGE; if Direction = sdUp then HDItem.iImage := 0 // aufsteigend else HDItem.iImage := 1; // absteigend end else begin // Systembilder verwenden if Direction = sdUp then HDItem.fmt := HDItem.fmt or HDF_SORTUP // aufsteigend else HDItem.fmt := HDItem.fmt or HDF_SORTDOWN; // oder absteigend end; Header_SetItem(Header, Index, HDItem); // abschicken end;Wir müssen also alle Header durchgehen und bei jedem den Pfeil löschen, nur beim gewünschten Header setzen wir den Pfeil; dabei können wir gleich die Spalte markieren:
procedure SetSorted(const LV: TListView; Index: Integer; Direction: TSortDirection); var C: Integer; begin for C := 0 to LV.Columns.Count - 1 do if C = Index then SetImage(LV, C, Direction) else RemoveImage(LV, C); ListView_SetSelectedColumn(LV.Handle, Index); end;Jetzt haben wir alles zusammen, um die Sortierung vorzunehmen und dem Benutzer eine optische Rückmeldung zu geben, nach welcher Spalte sortiert wird und in welche Richtung.
Um die Sache etwas komfortabler zu machen erledigt meine Funktion all das gleich mit. Damit bleibt nur der Aufruf der Sort-Funktion, um in den Genuss der Optik zu kommen.
Aufräumen
So, jetzt sind wir doch fertig. oder? Leider nein. Borland hat mit sowas natürlich nicht gerechnet. Sobald die Größe der Spalte geändert wird verschwindet der Pfeil einfach wieder. Wer die Quellen der VCL hat, kann Delphi da etwas unter die Arme greifen. Dazu müssen wir die Datei "ComCtrls.pas" anpassen, genauer die Methode "TCustomListView.UpdateColumn".Im Original sieht diese Methode so aus:
procedure TCustomListView.UpdateColumn(AnIndex: Integer); const IAlignment: array[Boolean, TAlignment] of LongInt = ((LVCFMT_LEFT, LVCFMT_RIGHT, LVCFMT_CENTER), (LVCFMT_RIGHT, LVCFMT_LEFT, LVCFMT_CENTER)); var Column: TLVColumn; AAlignment: TAlignment; begin if HandleAllocated then with Column, Columns.Items[AnIndex] do begin mask := LVCF_TEXT or LVCF_FMT or LVCF_IMAGE; iImage := FImageIndex; pszText := PChar(Caption); AAlignment := Alignment; if Index <> 0 then fmt := IAlignment[UseRightToLeftAlignment, AAlignment] else fmt := LVCFMT_LEFT; if FImageIndex <> -1 then fmt := fmt or LVCFMT_IMAGE or LVCFMT_COL_HAS_IMAGES else mask := mask and not LVCF_IMAGE; if WidthType > ColumnTextWidth then begin mask := mask or LVCF_WIDTH; cx := FWidth; ListView_SetColumn(Handle, Columns[AnIndex].FOrderTag, Column); end else begin ListView_SetColumn(Handle, Columns[AnIndex].FOrderTag, Column); if ViewStyle = vsList then ListView_SetColumnWidth(Handle, -1, WidthType) else if (ViewStyle = vsReport) and not OwnerData then ListView_SetColumnWidth(Handle, Columns[AnIndex].FOrderTag, WidthType); end; end; end;Diese Methode müssen wir nun so anpassen, dass unsere Änderungen nicht überschrieben werden:
procedure TCustomListView.UpdateColumn(AnIndex: Integer); const IAlignment: array[Boolean, TAlignment] of LongInt = ((LVCFMT_LEFT, LVCFMT_RIGHT, LVCFMT_CENTER), (LVCFMT_RIGHT, LVCFMT_LEFT, LVCFMT_CENTER)); // Ein paar Konstanten... HDF_SORTUP = $0400; HDF_SORTDOWN = $0200; CCM_GETVERSION = CCM_FIRST + $08; var Column: TLVColumn; AAlignment: TAlignment; // ...und ein paar Variablen Item1, Item2: THDItem; Version: Integer; begin if HandleAllocated then with Column, Columns.Items[AnIndex] do begin //////////////////////////////////////////////////// // den aktuellen Zustand sichern FillChar(Item1, SizeOf(Item1), 0); Item1.Mask := HDI_BITMAP or HDI_IMAGE or HDI_FORMAT; Header_GetItem(FHeaderHandle, AnIndex, Item1); //////////////////////////////////////////////////// mask := LVCF_TEXT or LVCF_FMT or LVCF_IMAGE; iImage := FImageIndex; pszText := PChar(Caption); AAlignment := Alignment; if Index <> 0 then fmt := IAlignment[UseRightToLeftAlignment, AAlignment] else fmt := LVCFMT_LEFT; if FImageIndex <> -1 then fmt := fmt or LVCFMT_IMAGE or LVCFMT_COL_HAS_IMAGES else mask := mask and not LVCF_IMAGE; if WidthType > ColumnTextWidth then begin mask := mask or LVCF_WIDTH; cx := FWidth; ListView_SetColumn(Handle, Columns[AnIndex].FOrderTag, Column); end else begin ListView_SetColumn(Handle, Columns[AnIndex].FOrderTag, Column); if ViewStyle = vsList then ListView_SetColumnWidth(Handle, -1, WidthType) else if (ViewStyle = vsReport) and not OwnerData then ListView_SetColumnWidth(Handle, Columns[AnIndex].FOrderTag, WidthType); end; //////////////////////////////////////////////////// // den Zustand nach den Änderungen holen FillChar(Item2, SizeOf(Item2), 0); Item2.Mask := HDI_FORMAT; Header_GetItem(FHeaderHandle, AnIndex, Item2); // Text einschalten Item2.Mask := Item2.Mask or HDI_TEXT; Item2.pszText := PChar(Caption); Item2.cchTextMax := Length(Caption); // die Version ermitteln Version := SendMessage(FHeaderHandle, CCM_GETVERSION, 0, 0); Item2.fmt := Item2.fmt or (Item1.fmt and HDF_BITMAP_ON_RIGHT); if Version >= 6 then // je nach Version // den Pfeil wieder "einschalten" Item2.fmt := Item2.fmt or (Item1.fmt and HDF_SORTUP) or (Item1.fmt and HDF_SORTDOWN) else begin // oder das Bild wieder zuweisen Item2.fmt := Item2.fmt or (Item1.fmt and HDF_IMAGE); Item2.iImage := Item1.iImage; end; Header_SetItem(FHeaderHandle, AnIndex, Item2); //////////////////////////////////////////////////// end; end;Nach diesen Änderungen "vergisst" unser Programm die Einstellungen nicht mehr.
Einpassen
Jetzt müssen wir nur noch dem Programm mitteilen, dass es nicht die vorkompilierten DCUs der VCL verwenden soll, sondern unsere angepassten.Dazu muss in den Projektoptionen der Suchpfad um die VCL-Quellen erweitert werden.
In Delphi 2006 z. B. lautet der Standardpfad "C:\Programme\Borland\BDS\4.0\source\Win32\vcl".
Damit werden die VCL-Quellen neu compiliert und unsere Änderungen ins Programm übernommen.
Im Anhang befindet sich die Unit "SortListView", in der die hier vorgestellten Routinen implementiert sind.
Anhang:
SortListView (9 KB)
Kommentare
Kommentar erstellen