Entwicklung
  SVGImage
  MWKEdit
  PaletteForm
  MWKComboBoxEx
  MWKToolPalette
  GlowLabel
  ShellDropper
  Tutorials
    ListView sortieren
    Interfaces
    Undo/Redo realisieren
    Splashscreens
  Bibliotheken
  Silbentrennung
Linux
Kontakt
impressum
Druckansicht

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
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 Es ist zwar möglich, einem ListView über eine ImageList Bilder mit Pfeilen zuzuordnen, welche dann den Spaltenköpfen zugewiesen werden. Aber dabei bleibt der Pfeil links der Überschrift und die ImageList ist ja eigentlich für den Inhalt des ListViews da.
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
Klassische Sortierung
Natürliche 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