Ir al contenido



Foto

Ordenar un ListView de forma Ascendente/Descendente por columna


  • Por favor identifícate para responder
14 respuestas en este tema

#1 enecumene

enecumene

    Webmaster

  • Administrador
  • 7.404 mensajes
  • LocationRepública Dominicana

Escrito 15 agosto 2009 - 09:29

Pues eso, vamos a ordenar un ListView de forma Ascendente/Descendente de acuerdo al contenido de una columna, igual como hacemos con el explorador de windows en su vista details.

Lo primero que haremos es declarar dos variables globales:



delphi
  1. var
  2.   Form1: TForm1;
  3.   UltimoSorteo: Integer; //guardará el dato de la última ordenación;
  4.   Ascendente: Boolean; //verificaremos si la ordenación es Ascendente o no;



Luego debemos inicializar las variables que hemos creado:



delphi
  1. procedure TForm1.FormCreate(Sender: TObject);
  2. begin
  3.   UltimoSorteo := -1; //no hay ninguno (también devuelve 1 y 0);
  4.   Ascendente := True; //lo iniciamos de manera ascendente;
  5. end;



Ahora, en el evento OnColumnClick del ListView colocaremos el siguiente code:



delphi
  1. procedure TForm1.ListView1ColumnClick(Sender: TObject;
  2.   Column: TListColumn);
  3. begin
  4.   if Column.Index = UltimoSorteo then
  5.     Ascendente := Not Ascendente
  6.   else
  7.     UltimoSorteo := Column.Index;
  8.  
  9. TListView(Sender).CustomSort(@SortByColumn, Column.Index);
  10. end;



Por último debemos crear una función que hará la ordenación de los datos de acuerdo a la columna:



delphi
  1. function SortByColumn(Item1, Item2: TListItem; Data: integer):
  2.     integer; stdcall;
  3.   begin
  4.     if Data = 0 then
  5.       Result := AnsiCompareText(Item1.Caption, Item2.Caption)
  6.     else
  7.       Result := AnsiCompareText(Item1.SubItems[Data-1],
  8.                                 Item2.SubItems[Data-1]);
  9.     if not Ascendente then Result := -Result;
  10. end;



Pues ya con eso pueden ordenar los datos de una columna de un listview sin importar el tipo de contenido que tenga, Nota: Probado con Delphi 7.

Que lo disfruten, Saludos.
  • 0

#2 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.839 mensajes
  • LocationMadrid - España

Escrito 15 agosto 2009 - 10:51

Interesante y educativo este ejemplo enecumene (y)

Saludos.
  • 0

#3 Caral

Caral

    Advanced Member

  • Administrador
  • 4.241 mensajes
  • LocationCosta Rica

Escrito 15 agosto 2009 - 11:12

Hola
Siempre con codigo interesante y practico, gracias amigo. (y)
Saludos
  • 0

#4 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 13.953 mensajes
  • LocationMéxico

Escrito 15 agosto 2009 - 09:47

Que bien amigo, parece que lo voy a usar muy pronto.

Salud OS
  • 0

#5 enecumene

enecumene

    Webmaster

  • Administrador
  • 7.404 mensajes
  • LocationRepública Dominicana

Escrito 16 agosto 2009 - 04:15

Vale qué bueno les haya gustado :D

Saludos.
  • 0

#6 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 13.953 mensajes
  • LocationMéxico

Escrito 07 diciembre 2009 - 08:43

Que bien amigo, parece que lo voy a usar muy pronto.

Salud OS


4 meses después ......

Funciona muy bien amigo :) Probado con Turbo Delphi (y)

Salud OS
  • 0

#7 monchito_elroro

monchito_elroro

    Advanced Member

  • Miembros
  • PipPipPip
  • 254 mensajes

Escrito 02 octubre 2015 - 11:40

Muchas gracias amigo Enecumene, mas quisiera hacerle una consulta, cuando lo pruebo en lazarus me sale el siguiente mensaje:


php
  1. unit1.pas(72,19) Error: identifier idents no member "CustomSort"

y se detiene aquí:


php
  1. TListView(Sender).CustomSort(@SortByColumn, Column.Index);

Estoy usando Lazarus en windows 10, me podrían dar una manito.


  • 0

#8 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.257 mensajes
  • LocationArgentina

Escrito 02 octubre 2015 - 05:47

Muchas gracias amigo Enecumene, mas quisiera hacerle una consulta, cuando lo pruebo en lazarus me sale el siguiente mensaje:


php
  1. unit1.pas(72,19) Error: identifier idents no member "CustomSort"

y se detiene aquí:


php
  1. TListView(Sender).CustomSort(@SortByColumn, Column.Index);

Estoy usando Lazarus en windows 10, me podrían dar una manito.

 

Lamentablemente Lazarus no cuenta con este método aún. La idea está, pero parece que tienen unas dificultades en su implementación por lo que está en "stand by".

 

De lo que estoy viendo si existen propiedades relacionadas con el ordenamiento: SortColumn que recibe el nro de columna por la cual ordenar, SortDirection que indica si es ascendente o descendente, y SortType que establece el tipo de dato por el cual ordenar.

Pero no le veo un método Sort o algo parecido en forma pública. Explorando el código resulta ser que se realiza el ordenamiento de forma automática cuando establece el valor de SortDirection o de cualquiera de las otras.

En su código aprecio que invoca al método @CompareItems que en su interior invoca al método para el evento OnCompare (si hay uno asignado) o a uno por defecto (que compara la data de item1 e item2). El método de respuesta a OnCompare vendría a ser el análogo del método SortByColumn que ha puesto el compañero enecumene en su ejemplo.

 

Ahora bien, la documentación oficial (la disponible al menos desde el ide con F1) sobre OnCompare no aclara mucho sobre su diseño. El tipo de puntero a método de OnCompare está declarado así:


delphi
  1. TLVCompareEvent = procedure(Sender: TObject; Item1, Item2: TListItem;
  2. Data: Integer; var Compare: Integer) of object;

Como he dicho, el método Sort interno recibe como parámetro @CompareItems. Cuya implementación es:


delphi
  1. function CompareItems(Item1, Item2: Pointer): Integer;
  2. var
  3. Str1: String;
  4. Str2: String;
  5. ListView: TCustomListView;
  6. begin
  7. Result := 0;
  8. ListView := TListItem(Item1).Owner.Owner;
  9. if Assigned(ListView.FOnCompare) then
  10. ListView.FOnCompare(ListView, TListItem(Item1), TListItem(Item2),0 ,Result)
  11. else
  12. begin
  13. if ListView.SortType in [stData] then
  14. Result := CompareValue(PtrUInt(TListItem(Item1).Data), PtrUInt(TListItem(Item2).Data))
  15. else
  16. begin
  17. if ListView.FSortColumn = 0 then
  18. begin
  19. Str1 := TListItem(Item1).Caption;
  20. Str2 := TListItem(Item2).Caption;
  21. end else
  22. begin
  23. if ListView.FSortColumn <= TListItem(Item1).SubItems.Count then
  24. Str1 := TListItem(Item1).SubItems.Strings[ListView.FSortColumn-1]
  25. else
  26. Str1 := '';
  27. if ListView.FSortColumn <= TListItem(Item2).SubItems.Count then
  28. Str2 := TListItem(Item2).SubItems.Strings[ListView.FSortColumn-1]
  29. else
  30. Str2 := '';
  31. end;
  32. Result := AnsiCompareText(Str1, Str2);
  33. end;
  34. if ListView.SortDirection = sdDescending then
  35. Result := -Result;
  36. end;
  37. end;

Creí que al ver en como invoca a OnCompare() podría aclararme mejor el punto pero no es el caso. No se a que se refiere o que se supone que debe significar o que uso darle a los parámetros Data y Compare. También no me queda claro que valor debe regresar este método (nota que Compare es una variable por valor).

 

Quizá si le dedicas más tiempo en la investigación que yo puedas encontrar estas respuestas. Por lo pronto yo no encuentro alguna. :(

 

Saludos,


  • 0

#9 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.257 mensajes
  • LocationArgentina

Escrito 02 octubre 2015 - 06:03

Incluyo la implementación de Sort por si les aclara mejor el panorama:


delphi
  1. procedure TCustomListView.Sort;
  2. var
  3. FSavedSelection: TFPList;
  4. FSavedFocused: TListItem;
  5. i: Integer;
  6. AItemIndex: Integer;
  7. begin
  8. if FSortType = stNone then exit;
  9. if (FSortColumn < 0) or (FSortColumn >= ColumnCount) then exit;
  10. if FListItems.Count < 2 then exit;
  11. if lffPreparingSorting in FFlags then exit;
  12.  
  13. if HandleAllocated then
  14. begin
  15. Include(FFlags, lffItemsSorting);
  16. FSavedSelection := TFPList.Create;
  17. try
  18. if (ItemIndex >= 0) then
  19. FSavedFocused := Items[ItemIndex]
  20. else
  21. FSavedFocused := nil;
  22. if Assigned(Selected) then
  23. begin
  24. FSavedSelection.Add(Selected);
  25. if MultiSelect then
  26. for i := 0 to Items.Count-1 do
  27. if Items[i].Selected and (Items[i] <> Selected) then
  28. FSavedSelection.Add(Items[i]);
  29. end;
  30.  
  31. Items.FCacheIndex := -1;
  32. Items.FCacheItem := nil;
  33.  
  34. FListItems.FItems.Sort(@CompareItems);
  35. TWSCustomListViewClass(WidgetSetClass).SetSort(Self, FSortType,
  36. FSortColumn, FSortDirection);
  37.  
  38. if (FSavedSelection.Count > 0) or Assigned(FSavedFocused) then
  39. begin
  40. Selected := nil; // unselect all
  41.  
  42. if FSavedFocused <> nil then
  43. TWSCustomListViewClass(WidgetSetClass).ItemSetState(Self,
  44. FSavedFocused.Index, FSavedFocused, lisFocused, True);
  45.  
  46. for i := FSavedSelection.Count - 1 downto 0 do
  47. begin
  48. AItemIndex := Items.IndexOf(TListItem(FSavedSelection.Items[i]));
  49. if AItemIndex <> -1 then
  50. TWSCustomListViewClass(WidgetSetClass).ItemSetState(Self,
  51. AItemIndex, Items[AItemIndex], lisSelected, True);
  52. end;
  53. end;
  54. finally
  55. FreeThenNil(FSavedSelection);
  56. Exclude(FFlags, lffItemsSorting);
  57. end;
  58. end else
  59. FListItems.FItems.Sort(@CompareItems);
  60. end;

Puede verse allí el Sort interno asociado a FItems, y que se pasa el @CompareItems.

 

El método Sort, que se invoca ante cualquier cambio de las propiedades "Sort" está definido como protegido. Por lo que de última puede heredarse de TListView y ponerlo como público.

 

Aquí pongo de ejemplo de como se lo invoca cuando se cambia la dirección de ordenamiento:


delphi
  1. procedure TCustomListView.SetSortDirection(const AValue: TSortDirection);
  2. begin
  3. if FSortDirection=AValue then exit;
  4. FSortDirection:=AValue;
  5. Sort;
  6. end;

Notará que no es un TListView sino un TCustomView. TListView no es más que una versión "pública" de TCustomView, es decir que cambia la visibilidad de algunas propiedades y métodos.

 

Creo que con todo esto ya se puede hechar un poco más de luz al tema. Mi búsqueda me ha llevado por algunos hilos en los foros de Lazarus, y en ellos no hay respuesta definitiva. Como he dicho antes: esto está en "veremos".

 

Saludos,


  • 0

#10 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.839 mensajes
  • LocationMadrid - España

Escrito 03 octubre 2015 - 09:32

Pongo un fragmento de código de un programa, que inicialmente escribí en C++Builder y mas tarde migré a delphi y Lazarus. Una AlphaSort sin problemas y trabaja como espero que lo haga:
 
 

delphi
  1. procedure TForm1.ListView1ColumnClick(Sender: TObject; Column: TListColumn);
  2. begin
  3. Pulse:= Pulse xor true;
  4. ColumnToSort:= Column.Index;
  5. (Sender as TCustomListView).AlphaSort;
  6. if ListView1.Selected <> nil then
  7. ListView_EnsureVisible(ListView1.Handle, ListView1.Selected.Index, 1);
  8. end;
  9.  
  10. procedure TForm1.ListView1Compare(Sender: TObject; Item1, Item2: TListItem;
  11. Data: Integer; var Compare: Integer);
  12. var ix: integer;
  13. begin
  14. if (ColumnToSort = 0) then
  15. Compare:= StrToInt(Item2.Caption) - StrToInt(Item1.Caption)
  16. else if(ColumnToSort<4) then
  17. begin
  18. ix:= ColumnToSort - 1;
  19. Compare:= CompareText(Item1.SubItems.Strings[ix], Item2.SubItems.Strings[ix]);
  20. end;
  21. if Pulse then Compare:= -Compare;
  22. end;

Saludos.
  • 1

#11 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.257 mensajes
  • LocationArgentina

Escrito 03 octubre 2015 - 11:42

Pongo un fragmento de código de un programa, que inicialmente escribí en C++Builder y mas tarde migré a delphi y Lazarus. Una AlphaSort sin problemas y trabaja como espero que lo haga:
 
 


delphi
  1. procedure TForm1.ListView1ColumnClick(Sender: TObject; Column: TListColumn);
  2. begin
  3. Pulse:= Pulse xor true;
  4. ColumnToSort:= Column.Index;
  5. (Sender as TCustomListView).AlphaSort;
  6. if ListView1.Selected <> nil then
  7. ListView_EnsureVisible(ListView1.Handle, ListView1.Selected.Index, 1);
  8. end;
  9.  
  10. procedure TForm1.ListView1Compare(Sender: TObject; Item1, Item2: TListItem;
  11. Data: Integer; var Compare: Integer);
  12. var ix: integer;
  13. begin
  14. if (ColumnToSort = 0) then
  15. Compare:= StrToInt(Item2.Caption) - StrToInt(Item1.Caption)
  16. else if(ColumnToSort<4) then
  17. begin
  18. ix:= ColumnToSort - 1;
  19. Compare:= CompareText(Item1.SubItems.Strings[ix], Item2.SubItems.Strings[ix]);
  20. end;
  21. if Pulse then Compare:= -Compare;
  22. end;

Saludos.

 

 

Vaya, desconocía AlphaSort. Por lo que estuve viendo, Con AlphaSort se ordena por la columna 0 y en forma ascendente siempre en Lazarus.

 

A lo que todavía no le encuentro sentido es que propósito tiene el parámetro Data del OnCompare. Hasta donde entiendo en Compare debe regresarse -1, 1, o 0 según el criterio a ordenar y la forma que tengamos pensado aplicar la comparación.

 

Saludos,


  • 0

#12 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.257 mensajes
  • LocationArgentina

Escrito 03 octubre 2015 - 11:48

He encontrado un hilo en StackOverflow que da una propuesta interesante para aplicar ordenamiento ascendente y descendente. Quizá pueda ser portado a Lazarus sin demasiadas complicaciones.

 

Saludos,


  • 0

#13 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.257 mensajes
  • LocationArgentina

Escrito 03 octubre 2015 - 12:28

¿Saben de que me he dado cuenta ahora? Es bien sabido que el algoritmo interno de ordenamiento por defecto es QuickSort... pero QuickSort no es bueno cuando se trata de items organizados matricialmente.... como por ejemplo en un TListView configurado de esa forma, una Grid, etc.

En especial cuando la cantidad de sub-items (columnas) se acerque a la cantidad de items, no es bueno que aplique el algoritmo QuickSort (aún cuando fuese su versión in-place).

¿Porqué? Porque al ordenar por una columna debe iterar a su vez por el resto de los subitems para ubicarlos en la misma posición. QuickSort se degrada cuando se debe aumentar la cantidad de elementos a desplazar. Es el mismo caso que se me ha presentado en un ocasión.

 

Lo explico: QuickSort tiene una complejidad media de O(n * log(n)) cuando una lista está formada por n elementos. Ahora la lista en realidad es una matriz (n, m) y no (n, 1). Es decir, cada siendo n la cantidad de items, y m la de subitems. Cada vez que hay un intercambio (swapping en la jerga de los algoritmos de ordenamiento) del item/subitem por el cual se debe realizar el ordenamiento se debe iterar sobre los m subitems restantes a su nuevo lugar. Este "barrido horizontal" completo requiere de O(n*m) operaciones.

Tenemos por tanto que la complejidad es Max( O(n * log(n)), O(n*m)) La máxima complejidad es O(n*m). Para cuando m se aproxima a n, todo se resume a O(n2).

 

Para estos casos es preferible directamente SelectionSort, que reduce ya de por si los intercambios y conserva su complejidad O(n2) al agregarse esta nueva dimensión.

 

Creo que ya el equipo de Lazarus ya está estudiando migrar el algoritmo de ordenamiento por defecto a TimSort, como ya lo han hecho (hace un buen tiempo) otros lenguajes. TimSort puede tener unas ventajas por sobre QuickSort y se me hace ante un esquema multidimensional es absorvente.

 

Saludos,


  • 0

#14 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.839 mensajes
  • LocationMadrid - España

Escrito 03 octubre 2015 - 01:49

Vaya, desconocía AlphaSort. Por lo que estuve viendo, Con AlphaSort se ordena por la columna 0 y en forma ascendente siempre en Lazarus.


Pues ya ves que el código que expongo es capaz de ordenar por la columna escogida y no solo por la cero.
 

A lo que todavía no le encuentro sentido es que propósito tiene el parámetro Data del OnCompare. Hasta donde entiendo en Compare debe regresarse -1, 1, o 0 según el criterio a ordenar y la forma que tengamos pensado aplicar la comparación.

Data está ligado a CustomSort. En realidad no le encuentro utilidad.

 

Compare está pasado por referencia y sirve para que la rutina de ordenación sepa si los elementos son iguales o uno es mayor, en cuyo caso deberá proceder a su ordenación. Cambiando de signo de Compare, cambiamos el sentido del orden ascendente o descendente, de ahí la utilidad de las flechitas del truco que publiqué no hace mucho: Como colocar las flechitas que indican el orden de columnas de un ListView. Aunque en Lazarus sólo tiene sentido en entorno Windows. Fuera de él habría que pintarlas a pelo, como imágenes en las cabeceras de columna.

 

 

Saludos.


  • 0

#15 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.839 mensajes
  • LocationMadrid - España

Escrito 03 octubre 2015 - 06:30

Al releer este tema me doy cuenta de que Delphius había puesto dos entradas más antes de escribir mi mensaje anterior. Leo la consideración sobre QuickSort en un ListView. No estoy tan seguro de que cambiar un Item de lugar implique recorrer todos los sumitem, al menos en Windows y supongo que en Lazarus. Un Item engloba sus subitem y posicionarlo requiere de un simple mensaje sin recorrer más. En realidad depende del trabajo interno de las API.
 
Yo suelo abusar de punteros, y una de as ventajas que tiene trabajar con ellos, es precisamente la ordenación de los elementos apuntados, no tengo que cambiar los elementos complejos, sólo su puntero. Me explico, si tengo elementos en una matriz de una estructura compleja, lo que tengo en realidad es una matriz de punteros que apuntan a cada estructura compleja, me basta con cambiar el orden de los punteros para reordenar todo el tema, solo cambio un número (el puntero) en su matriz, y con esto he ordenado todo a toda velocidad. Bien es cierto que su representación visual va a tardar, pero, basta con anular el repintado de cada cambio para hacer solo uno al final de la ordenación. Windows tiene previsto la inhabilitación del WS_PAINT para usar en este tipo de casos, de esta forma no apreciamos los cambios parciales y el proceso es muy veloz.

Y ahora que recuerdo, ya expuse esto en este truco: Trucos para TListView con la API de Windows.
 
 

cpp
  1. // Evito que se repinte a cada actualización de Item...
  2. SendMessage(ListView->Handle, WM_SETREDRAW, FALSE, 0L);
  3.  
  4. ListView->Items->Clear(); // proceso lento si se permite el repintado
  5.  
  6. // Permito que se repinte y obligo a que se actualize
  7. SendMessage(ListView->Handle, WM_SETREDRAW, TRUE, 0L);
  8. InvalidateRect(ListView->Handle, NULL, TRUE);


Saludos.
  • 1