So, jetzt kommt der harte Teil: das Sortieren der Einträge, wenn man auf den Spaltenkopf klickt. Was wollen wir erreichen? Wenn wir eine Spalte auswählen, dann sollen die Einträge in dieser Spalte aufsteigend (A - Z) oder absteigend (Z - A) sortiert werden.
Daher benötigen wir zuerst einmal eine Variable, mit der wir die Sortierrichtung beeinflussen können. Da wir nur zwei Richtungen haben (aufsteigend, absteigend), genügt eine byte- oder bool-Variable. Das bleibt eigentlich Ihnen überlassen. Ich habe mich für unser Beispiel für eine byte-Variable entschieden, die mit dem Wert Null initialisiert wird:
var
SortOrder : byte = 0;
Was passiert nun, wenn wir auf einen Spaltenkopf klicken? Die List-View sendet die Benachrichtigung "LVN_COLUMNCLICK" (in Form einer "WM_NOTIFY"-Nachricht) an unser Hauptfenster. Mitgeliefert werden das Handle der List-View und (in einem TNMListView-Record) die angeklickte Spalte. Dieser Wert ist über den lParam zugänglich, der auf das besagte Record zeigt.
Ausgehend davon, sieht die Kontrolle in unserem Programm erst einmal so aus:
WM_NOTIFY:
with PNMHdr(lp)^ do
if(hwndFrom = hLV) then
case code of
LVN_COLUMNCLICK:
begin
{ ... }
end;
end;
Zum Sortieren der Einträge gibt es die Nachricht "LVM_SORTITEMS" bzw. das Makro "ListView_SortItems". Beiden ist gemeinsam, dass Sie einen Anwendungs-bezogenen Parameter und die Adresse unserer eigenen Sortierfunktion erwarten. Fangen wir mit letzterer an. Der Name der Funktion ist Ihnen überlassen, lediglich an die Deklaration sollten Sie sich halten:
type
PFNLVCOMPARE = function(lParam1, lParam2, lParamSort: Integer): Integer stdcall;
Die zu vergleichenden Werte
Diesen Parameter geben wir beim Aufruf der o.g. Nachricht oder Funktion an. In vielen Fällen wird hier ein Wert gewählt, der Aussagen über die Sortierrichtung o.ä. trifft. Ich persönlich bin jedoch der Meinung, die Angabe des geklickten Spaltenkopfes ist die bessere Lösung.
Damit habe ich es schon verraten: wenn wir die Sortierung auslösen, dann geben wir den Index der angeklickten Spalte als Anwendungs-bezogenen Parameter an. Ich sage auch gleich den Grund, aber erst schauen wir uns den Aufruf an. Einmal mit Hilfe der Nachricht:
SendMessage(hwndFrom,LVM_SORTITEMS,WPARAM(PNMListView(lp)^.iSubItem),LPARAM(@CompareFunc));
und einmal mit Hilfe des Makros:
ListView_SortItems(hwndFrom,@CompareFunc,PNMListView(lp)^.iSubItem);
Nun passiert folgendes: die Index-Werte des ersten und zweiten Eintrags der List-View werden mit dem Index-Wert der angeklickten Spalte an unsere Sortierfunktion übergeben. Mit Hilfe des Spaltenindex entscheiden wir, wie wir sortieren wollen.
Dazu erinnern wir uns an das Beispielprogramm. Es besitzt drei Spalten. Die erste zeigt den Dateinamen, die mittlere zeigt die Dateigröße, und die letzte zeigt den Dateityp an. Das heißt, wir haben zwei String-Werte und einen Integer-Wert. Dementsprechend unterscheidet sich die Sortierung, da wir einen integer nicht mit Hilfe eines String-Vergleichs sortieren können.
Doch zuerst holen wir uns erst einmal den Text der beiden Items, die miteinander verglichen werden sollen. Im Beispiel habe ich zwei string-Variablen benutzt, die zuerst natürlich initialisiert werden. Wenn das geschehen ist, benutzen wir den Wert von "lp1" zum Auslesen des Textes. Der Spaltenindex darf dabei natürlich nicht vergessen werden:
ListView_GetItemText(hLV,lp1,SubItem,@buf1[1],MAX_PATH);
Angenommen, wir haben die erste Spalte angeklickt, dann wäre "SubItem" Null, und da "lp1" in dem Fall den Index des allerersten Eintrags enthält, hätten wir mit obiger Anweisung den Dateinamen des ersten Eintrags in der List-View ermittelt.
Das gleiche machen wir dann mit dem zweiten Eintrag, dessen Index in "lp2" übergeben wird:
ListView_GetItemText(hLV,lp2,SubItem,@buf2[1],MAX_PATH);
Wenn wir jetzt davon ausgehen, dass es sich um zwei Strings handelt (also um den Inhalt der Spalten #1 und #3), dann können wir mit der Funktion "lstrcmpi" einen recht einfachen Vergleich durchführen. Diese Funktion liefert Null zurück, wenn beide Strings identisch sind. Ist der erste String kleiner als der zweite, dann ist das Ergebnis negativ. Ist der erste String größer als der zweite, liefert die Funktion ein positives Ergebnis zurück.
Kleiner und größer bezieht sich dabei nicht ausschließlich auf die Länge des jeweiligen Strings. Damit sind auch die Zeichen im direkten Vergleich gemeint. Folgende Strings sind identisch und hätten das Ergebnis Null zur Folge:
Abcd = abcd
Da wir mit "lstrcmpi" unabhängig von der Schreibweise sind, können Sie meine Aussage so akzeptieren. :o)
Folgender Vergleich hätte ein positives Ergebnis zur Folge, denn obwohl der erste String kürzer ist, hat er das "höhere" Zeichen an einer früheren Stelle als der zweite String:
abcz > abcdefg
Würden Sie die beiden Strings vertauschen, bekämen Sie ein negatives Ergebnis, da es diesmal der zweite String wäre, der das höherwertige Zeichen zuerst besitzt:
abcdefg < abcz
Nach dem selben Prinzip vergleichen wir nun unsere beiden Strings. Allerdings möchte ich Sie an unsere Bedingung vom Anfang erinnern: wir wollen auf- oder absteigend sortieren. Das bedeutet, wir müssen unsere oben deklarierte Variable zu Rate ziehen. Wie gehen wir vor? Ganz einfach: wollen wir aufsteigend sortieren (A - Z), dann vergleichen wir das erste Item mit dem zweiten. Wollen wir absteigend sortieren (Z - A), liefern wir die beiden Strings vertauscht an die Funktion und vergleichen sozusagen das zweite mit dem ersten Item:
if(SortOrder = 1) then Result := lstrcmpi(@buf2[1],@buf1[1]) else Result := lstrcmpi(@buf1[1],@buf2[1]);
Das eigentliche Sortieren übernimmt nun wieder die List-View für uns. Von unserer Seite waren nur die Werte (negativ < Null < positiv) wichtig, da sie entscheiden, wie die List-View arbeitet. Geben wir einen negativen Wert zurück, dann bedeutet das: das erste Item soll vor dem zweiten eingefügt werden. Geben wir einen positiven Wert zurück, dann bedeutet das: das erste Item soll hinter dem zweiten eingefügt werden. Und der Wert Null bedeutet, dass beide Items identisch sind und nicht sortiert werden.
Nachdem die List-View dies durchgeführt hat, wird die Funktion erneut aufgerufen. Diesmal werden ihr aber die Indexwerte des zweiten und dritten Parameters übergeben ... Auf diese Weise werden alle Einträge der List-View berücksichtigt, und die Sortierfunktion läuft bis es nichts mehr zum Sortieren gibt.
Richtig! Die Integerwerte der mittleren Spalte (die Dateigrößen). Hier wird nun auch klar, warum wir mit "lstrcmpi" nicht weit kommen. Ein Vergleich zwischen den Werten 13 und 2 z.B. hätte das Ergebnis, dass die Zwei (im Vergleich mit der Eins als erstem Zeichen der Dreizehn) als größer eingestuft wird.
Aus diesem Grund prüfen wir in unserer Sortierfunktion, welche Spalte angeklickt wurde. Wenn der übermittelte Index den Wert Eins hat, dann entspricht dies der mittleren Spalte mit den Dateigrößen, und wir verzweigen in eine separate Prüfung.
In dieser wandeln wir die beiden Strings in Integer-Werte um. Der Einfachheit halber habe ich dafür die Funktion "StrToIntDef" nachgebildet, der gleich noch ein Defaultwert übergeben werden kann. Zu beachten ist allerdings, dass eine Größenangabe in der List-View z.B. so aussehen kann: 26 B. Wir müssen also alles nach einem evtl. vorhandenen Leerzeichen entfernen, damit wir bei der Konvertierung des Strings in einen numerischen Wert tatsächlich nur die Ziffern berücksichtigen.
Wenn das erledigt ist, denken wir wieder an unsere Sortierrichtung und weisen die Integer-Werte dementsprechend unterschiedlich zu:
if(SortOrder = 1) then begin b := StrToIntDef(buf1,0); a := StrToIntDef(buf2,0); end else begin a := StrToIntDef(buf1,0); b := StrToIntDef(buf2,0); end;
Der Vergleich der beiden Integer sollte nun das geringste Problem sein. Ausgehend von der Maßgabe (negativ < Null < positiv) vergleichen wir wie folgt:
if(a > b) then Result := 1 else if(a < b) then Result := -1 else Result := 0;
Wenn Sie die Sortierung so ausprobieren, werden Sie feststellen, das nichts passiert. Warum? Die Antwort findet sich in der Erklärung des TLVItem-Records im PSDK, wo es (frei übersetzt) heißt:
Da unsere Items aber beim Füllen keinen Wert für die lParam-Membervariable des TLVItem-Records zugewiesen bekamen, passiert auch nichts. Wir müssen dies also noch tun, wobei wir ganz einfach nur den jeweiligen Item-Index als lParam-Wert festlegen:
lvi.mask := LVIF_PARAM; lvi.iSubItem := 0;
for i := 0 to ListView_GetItemCount(hLV) - 1 do begin lvi.iItem := i; lvi.lParam := i; SendMessage(hLV,LVM_SETITEM,0,LPARAM(@lvi)); end;
Im Beispielprogramm finden Sie diese Anweisungen in der Prozedur "UpdateLParam", die jeweils vor der Sortierfunktion aufgerufen wird.
Bleibt nur eins zu tun: wenn wir aufsteigend sortiert haben, müssen wir dafür sorgen, dass beim nächsten Klick in umgekehrter Reihenfolge sortiert wird ... und wieder umgekehrt, nach einem nochmaligen Klick, usw. Dazu gehen wir zurück zu unserer "LVN_COLUMNCLICK"-Auswertung und negieren den aktuellen Wert der Variable "SortOrder" einfach:
SortOrder := 1 - SortOrder;
Jetzt weist unser Programm aber einen Schönheitsfehler auf: egal welchen Spaltenkopf wir anklicken, es wird immer der aktuelle Wert von "SortOrder" zur Sortierrichtung herangezogen. Schauen Sie sich das aber mal im Windows Explorer an: wenn Sie dort auf einen Spaltenkopf klicken, wird immer erst aufsteigend (A - Z) sortiert. Erst der nochmalige Klick auf den gleichen Spaltenkopf kehrt die Richtung um.
Es ist daher zweckmäßig, den jeweils aktuellen Spaltenindex zu sichern und im Fall einer Änderung "SortOrder" wieder auf Null zu setzen (für die aufsteigende Sortierung), was verkürzt so aussieht:
if(LastCol <> PNMListView(lp)^.iSubItem) then SortOrder := 0;
{ ... } // sortieren
LastCol := PNMListView(lp)^.iSubItem;
Während des Sortierprozesses ist der Inhalt der List-View (laut PSDK) "instabil". Nach Möglichkeit sollte also die Arbeit mit der List-View während der Sortierung vermieden werden.