Sort ListIconGadget [Windows only]

Share your advanced PureBasic knowledge/code with the community.
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Sort ListIconGadget [Windows only]

Post by Zapman »

Here is the Zapman library for sorting ListIconGadget:

There are already many codes like this posted on the forum. I just tried to make something practical and robust that can work in as many contexts as possible (but only for Windows).

Image

Highlights of this code:
- The callback implementation does not prevent other callbacks from being implemented for the same main window.
- No global variables are used, except for the CSLI_RemoveAccentsWhenSorting, CSLI_NoCaseSorting, CSLI_DateFormat$ and CSLI_DecimalSeparator$ parameters.
- Once this library is included in your project, only one line of code is needed to make a ListIconGadget sortable: MakeListIconSortable(NoGadget).
- Setgadgetitemdata can be used without problem in the rest of the code because it is not used by this library.
- It is possible to choose between sort icons on the left, sort icons above the titles and no sort icon.
- The code works on all versions of Windows from Windows XP to Windows 11 and with x86 and x64 systems.
- Date and time sorting works for all formats used in all countries (thanks to the help of forum members).
- Alphabetical sorting can be done with or without accents (for all accentuated languages) and with or without casing.
- Decimal and hexadecimal values are also sorted.
- The code is compact and fully commented.
- You can easily adapt the CSLI_ListIconSortCallback() procedure to sort 'exotic' data if needed.

To be sure to get the last version and discover other Zapman libraries, you can visit https://www.editions-humanis.com/downlo ... ads_EN.htm

Code: Select all

;*****************************************************************************
;
;                      ColumnSortedListIconGadget.pbi
;                   Zapman - March 2025-3 - Windows Only
;
;   This file must be saved under the name 'ColumnSortedListIconGadget.pbi'
;
;      This library provides functionality to add sorting capabilities
;  to a ListIconGadget, including sorting based on ascending or descending
;         order, and displaying arrows for sorting in the header.
; The library can also be used to remove accents from text for proper sorting.
;
;
; The main function of this library is MakeListIconSortable() which can accept
;  four possible values as second parameter:
;   #CSLI_NotSortable ; Reset the gadget to normal (not sortable).
;   #CSLI_NoArrow     ; Make the gadget sortable but don't show arrows.
;   #CSLI_LeftArrows  ; Show arrows on left side.
;   #CSLI_TopArrows   ; Show arrows above the column title.
;
;   Many thanks for the forum members (ChrisR, Mesa, dcr3, Mindphazer, Lord,
; firace, ebs, Kwai chang caine, Quin) for their testing, help and suggestions.
;
;*****************************************************************************
;
CompilerIf #PB_Compiler_IsMainFile
  EnableExplicit
CompilerEndIf
;
;
; You can decide how you want the alphabetical sorts to be done:
;
Global CSLI_RemoveAccentsWhenSorting = #True
Global CSLI_NoCaseSorting            = #True
;
;
; ****************************************************************************************
;
;-   1--- FIRST PART: MISCELLANEOUS FUNCTIONS (possibly reusable for other needs) ---
;
CompilerIf Not(Defined(RemoveAccents, #PB_Procedure))
  Procedure.s RemoveAccents(Text$)
    ; Function to remove accents from a string. By Zapman.
    If Text$
      Protected length = Len(Text$) * 2
      Protected OPos, NPos, DoubleLength
      Protected NormalizedText$ = Space(Length)
      ;
      ; FoldString_() will replace each accentuated character by a pair of characteres
      ; as this: (NonAccentuatedCharactere) + (diacritic)
      Length = FoldString_(#MAP_COMPOSITE, @Text$, - 1, @NormalizedText$, Length) - 1
      ;
      ; Examine the result:
      If Length > 0 And Length <> Len(Text$)
        DoubleLength = (Length - 1) * 2
        For NPos = 0 To DoubleLength Step 2
          If PeekC(@Text$ + OPos) <> PeekC(@NormalizedText$ + NPos)
            ; If the character has been replaced, replace it into the original text:
            PokeC(@Text$ + OPos, PeekC(@NormalizedText$ + NPos))
            ; The following character contains the diacritic. Jump over it:
            NPos + 2
          EndIf
          OPos + 2
        Next
      EndIf
      ;
      ProcedureReturn Text$
    EndIf
  EndProcedure
CompilerEndIf
;
CompilerIf Not(Defined(GetLocalDateFormat, #PB_Procedure))
  Procedure.s GetLocalDateFormat()
    ;
    ; Date format can change depending on the country when the program is running.
    ; The following attempts to prepare 'DateFormat$' reflecting the format locally used.
    ;
    Protected DateFormat$ = Space(255)   ; Create a buffer to get the local date format:
      GetLocaleInfo_(#LOCALE_USER_DEFAULT, #LOCALE_SSHORTDATE, @DateFormat$, Len(DateFormat$))
      ; Depending on the country where your computer is runing, this API function will
      ; return something like "dd/MM/yyyy" (France and England) or "yyyy-MM-dd" (Canada) or "M/d/yyyy" (USA), etc.
    ;
    Protected DateSeparator$ = Space(4) ; Create a buffer to get the local separator:
      GetLocaleInfo_(#LOCALE_USER_DEFAULT, #LOCALE_SDATE, @DateSeparator$, Len(DateSeparator$))
      ; Depending on the country where your computer is runing, this API function will
      ; return something like "/" or "." or perhaps "-",...
    ;
    ; The date format now needs to be prepared to be used by PureBasic to correctly
    ; format the date:
    ;
    ; 1- Add the "%" caractere before days, month and years, conforming to the PureBasic needs:
         DateFormat$ = "%" + LCase(ReplaceString(DateFormat$, DateSeparator$, DateSeparator$ + "%"))
    ; 2- If the format returned by the API function only uses one caractere for day and month,
    ;    -> double it:
         DateFormat$ = ReplaceString(DateFormat$, "%d" + DateSeparator$, "%dd" + DateSeparator$)
         DateFormat$ = ReplaceString(DateFormat$, "%m" + DateSeparator$, "%mm" + DateSeparator$)
    ;
    ProcedureReturn DateFormat$
    ;
  EndProcedure
CompilerEndIf
;
CompilerIf Not(Defined(GetLocalDecimalSeparator, #PB_Procedure))
  Procedure.s GetLocalDecimalSeparator()
    ; The decimal separator can change from one country to another.
    ; Get the local one:
    Protected DecimalSeparator$ = Space(2)   ; Create a buffer to get the local decimal separator
      GetLocaleInfo_(#LOCALE_USER_DEFAULT, #LOCALE_SDECIMAL, @DecimalSeparator$, Len(DecimalSeparator$))
    ;
    ProcedureReturn DecimalSeparator$
    ;
  EndProcedure
CompilerEndIf
;
CompilerIf Not(Defined(ConvertHourFormats, #PB_Procedure))
  Procedure.q ConvertHourFormats(Hour$)
    ;
    ; From an hour string formated as:
    ;   9:35
    ;   09:35
    ;   9:35:00
    ;   9h35
    ;   9 h 35 mn
    ;   9 Hr 35 Mn 12 sec.
    ;   9:35 AM
    ;   9h35PM
    ; -> returns the number of seconds from midnight.
    ;
    ; From an hour string formated as Sporty time (i.e. 3'40''80'''),
    ; -> value will be returned in hundredths of a second:
    ;
    Protected APM, DH.q
    ;
    If Right(UCase(Hour$),2) = "AM" Or Right(UCase(Hour$),2) = "PM"
      If Right(UCase(Hour$),2) = "PM"
        APM = 1
      EndIf
      Hour$ = Trim(Left(Hour$, Len(Hour$) - 2))
    EndIf
    ;
    Hour$ = ReplaceString(Hour$, " ", "")
    Hour$ = ReplaceString(Hour$, ".", "")
    ;
    Hour$ = ReplaceString(LCase(Hour$), "hr", ":")
    Hour$ = ReplaceString(Hour$, "h", ":")
    Hour$ = ReplaceString(Hour$, "mn", ":")
    Hour$ = ReplaceString(Hour$, "m", ":")
    Hour$ = ReplaceString(Hour$, "sec", "")
    Hour$ = ReplaceString(Hour$, "s", "")
    
    If Right(Hour$, 1) = ":"
      Hour$ + "00"
    EndIf
    If CountString(Hour$, ":") = 1
      Hour$ + ":00"
    EndIf
    DH = ParseDate("%hh:%ii:%ss", Hour$)
    If DH <> -1
      If APM
        DH + ParseDate("%hh", "12")
      EndIf
    ElseIf FindString(Hour$, "'") And Val(ReplaceString(Hour$, "'", ""))
      ; Sporty time notation. Value will be returned in hundredths of a second
      DH = 0
      Protected posMin  = FindString(Hour$, "'")
      Protected posSec  = FindString(Hour$, "''")
      Protected posCent = FindString(Hour$, "'''")
      Protected posDep  = 0
      If posMin = posSec  : posMin = 0 : EndIf
      If posSec = posCent : posSec = 0 : EndIf
      ;
      If posCent = 0 : posCent = Len(Hour$) + 1 : EndIf
      If posMin : posDep  = posMin : EndIf
      If posSec : posDep  = posSec : EndIf
      DH = Val(ReplaceString(Mid(Hour$, posDep), "'", ""))
      Hour$ = Left(Hour$, posDep - 1)
      ;
      If posSec = 0 : posSec = Len(Hour$) + 1 : EndIf
      posDep = 0
      If posMin : posDep  = posMin : EndIf
      DH + Val(ReplaceString(Mid(Hour$, posDep), "'", "")) * 100
      Hour$ = Left(Hour$, posDep - 1)
      ;
      DH + Val(ReplaceString(Hour$, "'", "")) * 6000
      If DH = 0 : DH = -1 : EndIf
    EndIf
    ProcedureReturn DH
  EndProcedure
CompilerEndIf
;
;
; ****************************************************************************************
;
;-              2--- SECOND PART: SPECIALIZED FUNCTIONS FOR THIS LIBRARY ---
;
; To sort items by dates, it is necessory to know how date's data is formatted.
; Date format can change depending on the country when the program is running.
; The following attempts to prepare a 'CSLI_DateFormat$' global variable reflecting
; the format locally used.
; But, eventually, you can run the program in France with data coming from US.
; So, you'll need to use a US date format even if you program is running in France.
; In that case, you'll have to use another manner to define CSLI_DateFormat$.
; i.e., you can do CSLI_DateFormat$ = "%mm/%dd/%yyyy" to force using US date format.
;
Global CSLI_DateFormat$ = GetLocalDateFormat()
Define Test = 5
Select Test
  Case 1
  CSLI_DateFormat$="%dd.%mm.%yyyy"
  Case 2
  CSLI_DateFormat$="%dd-%mm-%yyyy"
  Case 3
  CSLI_DateFormat$="%dd/%mm/%yyyy"
  Case 4
  CSLI_DateFormat$="%yyyy.%mm.%dd"
  Case 5
  CSLI_DateFormat$="%yyyy/%mm/%dd"
EndSelect
Global CSLI_DecimalSeparator$ = GetLocalDecimalSeparator()
;
;
; This library offers two different manners of showing the
; current sorting order for a column: showing a left up-down
; arrow or showing a top up-down arrow (or showing no-arrow,
; even if the data is sorted).
;
;
; Enumeration to define the type of arrows used to show the sorting order.
; You can use one of this value as a parameter when calling
; the 'MakeListIconSortable()' function:
Enumeration CSLI_GadgetSetting
  #CSLI_NotSortable ; Reset the gadget to normal.
  #CSLI_NoArrow     ; Make the gadget sortable but don't show arrows.
  #CSLI_LeftArrows  ; Show arrows on left side.
  #CSLI_TopArrows   ; Show arrows above the column title.
EndEnumeration
;
; Enumeration to define the sort of a particular column.
; You can use one of this value as a parameter when calling
; the 'SortListIcon()' function:
Enumeration CSLI_SortType
  #CSLI_Descent   ; Ascending sort.
  #CSLI_Ascent    ; Descending sort.
  #CSLI_Unsorted  ; No sorting (default).
EndEnumeration
;
Procedure CSLI_CreateListIconArrows(ArrowType)
  ;
  ; Function to create sorting arrow images (Up, Down, and Up-Down arrows).
  ; This will be used for #CSLI_LeftArrows sorting.
  ;
  Protected TSize = DesktopScaledX(16)
  Protected ASize = TSize / 4
  Protected SX = TSize / 2 - ASize
  Protected SY = TSize / 2
  Protected NewImage = CreateImage(#PB_Any, TSize, TSize, 32, #PB_Image_Transparent)
  Protected VASize, Color
  ;
  If NewImage
    If ArrowType = #CSLI_Ascent       ; UpArrow.
      VASize = -ASize
    ElseIf ArrowType = #CSLI_Descent  ; DownArrow.
      VASize = ASize
      SY - ASize + 1
    ElseIf ArrowType = #CSLI_Unsorted ; UpDownArrow.
      VASize = ASize
      SY     = TSize / 2 + 1
    EndIf
    If StartDrawing(ImageOutput(NewImage))
      ;
      DrawingMode(#PB_2DDrawing_AlphaBlend)
      Box(0, 0, TSize, TSize, 0) ; Transparent background.
                                 ;
      If ArrowType = #CSLI_Unsorted
        Color = RGBA(192, 192, 192, 255) ; Gray for unsorted.
      Else
        Color = RGBA(1, 1, 1, 255) ; Quasi-Black for sorted arrows.
                                   ; A 'Full-Black' (0, 0, 0, 255) is not used because the FillArea()
                                   ; function doesn't work with full-black.
      EndIf
      ;
      Line(SX, SY, ASize * 2 + 1, 1, Color) ; Draw the arrow shape.
      Line(SX, SY, ASize, VASize, Color)
      Line(SX + ASize, SY + VASize, ASize, - VASize, Color)
      FillArea(SX + ASize, SY + VASize / 2, Color, Color)
      If ArrowType = #CSLI_Unsorted
        ; A double arrow (UpDown) is drawn for #CSLI_Unsorted.
        SY - ASize + 1
        VASize = -ASize
        Line(SX, SY, ASize * 2 + 1, 1, Color)
        Line(SX, SY, ASize, VASize, Color)
        Line(SX + ASize, SY + VASize, ASize, - VASize, Color)
        FillArea(SX + ASize, SY + VASize / 2, Color, Color)
      EndIf
      StopDrawing()
    EndIf
    ProcedureReturn NewImage
  EndIf
EndProcedure
;
Procedure CSLI_CreateListIconImgList(LIHandle)
  ;
  ; Function to create an image list for the ListIcon header.
  ; This will be used for #CSLI_LeftArrows sorting.
  ;
  Protected hHeader, hImgL, ct, Img
  ;
  If IsGadget(LIHandle)
    LIHandle = GadgetID(LIHandle) ; Get the control handle from the gadget's num.
  EndIf
  ;
  hHeader = SendMessage_(LIHandle, #LVM_GETHEADER, 0, 0)
  If SendMessage_(hHeader, #HDM_GETIMAGELIST, 0, 0) = 0
    hImgL = ImageList_Create_(16, 16, #ILC_COLOR32 | #ILC_MASK, 0, 0)
    For ct = #CSLI_Descent To #CSLI_Unsorted
      Img = CSLI_CreateListIconArrows(ct)
      ImageList_Add_(hImgL, ImageID(Img), 0)
      ; Microsoft doc says to free the original image after adding it to the image list.
      FreeImage(Img)
    Next
    SendMessage_(hHeader, #HDM_SETIMAGELIST, 0, hImgL)
  EndIf
EndProcedure
;
Procedure CSLI_FreeImageListFromGadget(LIHandle)
  ;
  ; Destroy both image lists of a ListIconGadget.
  ;
  ; Retrieve and destroy the ImageList for list items (row icons)
  Protected hImageList, hHeader, hHeaderImageList
  ;
  hImageList = SendMessage_(LIHandle, #LVM_GETIMAGELIST, #LVSIL_NORMAL, 0)
  If hImageList
    ImageList_Destroy_(hImageList)
    SendMessage_(LIHandle, #LVM_SETIMAGELIST, #LVSIL_NORMAL, 0)
  EndIf
  ;
  ; Retrieve the handle of the header control
  hHeader = SendMessage_(LIHandle, #LVM_GETHEADER, 0, 0)
  If hHeader
    ; Retrieve and destroy the ImageList for the header (column icons)
    hHeaderImageList = SendMessage_(hHeader, #HDM_GETIMAGELIST, 0, 0)
    If hHeaderImageList
      ImageList_Destroy_(hHeaderImageList)
      SendMessage_(hHeader, #HDM_SETIMAGELIST, 0, 0)
    EndIf
  EndIf
EndProcedure

; Define structure for sorting information.
Structure SortInfoStruct
  GadgetHandle.i
  Column.l
  AscentDescent.l
EndStructure
;
Procedure CSLI_ListIconSortCallback(item1, item2, *SortInfo.SortInfoStruct)
  ;
  ; Sorting callback function used by the ListIcon to compare items.
  ; (You can eventually complete or adapt this procedure to sort exotic data values
  ; using a particular manner).
  ;
  Protected A$ = Space(200), B$ = Space(200), A2$, B2$, lvi.LV_ITEM
  Protected A.q = 0, B.q = 0, DA.q = 0, DB.q = 0
  Protected result = 0, AscentDescent
  ;
  lvi\iSubItem   = *SortInfo\Column
  lvi\pszText    = @A$
  lvi\cchTextMax = 200
  SendMessage_(*SortInfo\GadgetHandle, #LVM_GETITEMTEXT, item1, @lvi)
  lvi\pszText = @B$
  SendMessage_(*SortInfo\GadgetHandle, #LVM_GETITEMTEXT, item2, @lvi)
  ;
  If A$ = B$
    ProcedureReturn 0 ; Items are equal.
  EndIf
  ;
  ; Determine if sorting is ascending or descending.
  If *SortInfo\AscentDescent = #CSLI_Descent : AscentDescent = -1 : Else : AscentDescent = 1 : EndIf
  ;
  ; Analysis of the string:
  ; For non-english countries, the decimal separator is not necessary
  ; a point. If it is not, replace the used one by a point:
  A$ = ReplaceString(A$, CSLI_DecimalSeparator$, ".")
  B$ = ReplaceString(B$, CSLI_DecimalSeparator$, ".")
  ;
  ; Decide if the string represents a numeric value,
  ; even if it is equal to "0"
  If FindString(A$, "0")
    A2$ = ReplaceString(A$, "0", "")
    If A2$ = "" Or A2$ = "." Or A2$ = "$" ; <- $ is used for hexadecimal values.
      A$ = "0"
    EndIf
  EndIf
  ;
  If FindString(B$, "0")
    B2$ = ReplaceString(B$, "0", "")
    If B2$ = "" Or B2$ = "." Or B2$ = "$" ; <- $ is used for hexadecimal values.
      B$ = "0"
    EndIf
  EndIf
  ;
  If A$ = "0" Or B$ = "0" Or Abs(Val(A$)) > 0 Or Abs(Val(B$)) > 0
    A = Val(A$)
    B = Val(B$)
  EndIf
  ;
  If Abs(ValF(A$)) > Abs(A) Or Abs(ValF(B$)) > Abs(B)
    A = ValF(A$)
    B = ValF(B$)
  EndIf
  ;
  If Abs(ValD(A$)) > Abs(A) Or Abs(ValD(B$)) > Abs(B)
    A = ValD(A$)
    B = ValD(B$)
  EndIf
  ;
  DA = ParseDate(CSLI_DateFormat$, A$)
  DB = ParseDate(CSLI_DateFormat$, B$)
  If DA <> -1 Or DB <> -1
    ; Date sorting:
    If DA > DB
      Result = AscentDescent
    Else
      Result = -AscentDescent
    EndIf
    ProcedureReturn result
  EndIf
  ;
  DA = ConvertHourFormats(A$)
  DB = ConvertHourFormats(B$)
  ;
  If DA <> -1 Or DB <> -1
    ; Hour sorting:
    If DA > DB
      Result = AscentDescent
    Else
      Result = -AscentDescent
    EndIf
    ProcedureReturn result
  EndIf
  ;
  If (Abs(A) > 0 Or A$ = "0") And (Abs(B) > 0 Or B$ = "0")
    ; Numerical sorting:
    If A > B Or ValF(A$) > ValF(B$)
      Result = AscentDescent
    Else
      Result = -AscentDescent
    EndIf
    ;
  Else
    ; Alphabetical sorting:
    If CSLI_RemoveAccentsWhenSorting
      A$ = RemoveAccents(A$)
      B$ = RemoveAccents(B$)
    EndIf
    If CSLI_NoCaseSorting
      A$ = LCase(A$)
      B$ = LCase(B$)
    EndIf
    If A$ > B$
      Result = AscentDescent
    Else
      Result = -AscentDescent
    EndIf
  EndIf
  ;
  ProcedureReturn result
EndProcedure
;
Procedure CLSI_GetSortOrderFromTopIcon(LIHandle, Column.i)
  ; https://www.purebasic.fr/english/viewtopic.php?t=55085
  ;
  ; This library offers two different manners of showing the
  ; current sorting order for a column: showing a left up-down
  ; arrow or showing a top up-down arrow (or showing no-arrow,
  ; even if the data is sorted).
  ;
  ; When a top up-down arrow is used, this procedure will
  ; retreive if it is actually the top arrow or the down arrow
  ; which is used.
  ;
  Protected ColumnHeader.i
  Protected hditem.HD_ITEM
  Protected RetVal
  ;
  ColumnHeader = SendMessage_(LIHandle, #LVM_GETHEADER, 0, 0)
  hditem\mask = #HDI_FORMAT
  If SendMessage_(ColumnHeader, #HDM_GETITEM, Column, @hditem)
    If (hditem\fmt & #HDF_SORTUP) = #HDF_SORTUP
      RetVal = #CSLI_Ascent
    ElseIf (hditem\fmt & #HDF_SORTDOWN) = #HDF_SORTDOWN
      RetVal = #CSLI_Descent
    Else
      RetVal = #CSLI_Unsorted
    EndIf    
  EndIf
  ;
  ProcedureReturn RetVal
  ;
EndProcedure
;
Procedure CLSI_SetTopSortingIcon(LIHandle, Column.i, SortOrder.i)
  ; http://stackoverflow.com/questions/254129/how-To-i-display-a-sort-arrow-in-the-header-of-a-List-view-column-using-c
  ;
  ; This library offers two different manners of showing the
  ; current sorting order for a column: showing a left up-down
  ; arrow or showing a top up-down arrow (or showing no-arrow,
  ; even if the data is sorted).
  ;
  ; When a top up-down arrow is used, this procedure will
  ; set the up arrow or the down arrow depending of the value
  ; of the 'SortOrder' parameter.
  ;
  Protected ColumnHeader.i
  Protected ColumnCount.i
  Protected hditem.HD_ITEM
  Protected Cnt.i
  ;
  ColumnHeader = SendMessage_(LIHandle, #LVM_GETHEADER, 0, 0)
  ColumnCount = SendMessage_(ColumnHeader, #HDM_GETITEMCOUNT, 0, 0)
  ;
  For Cnt = 0 To ColumnCount - 1
    hditem\mask = #HDI_FORMAT
    SendMessage_(ColumnHeader, #HDM_GETITEM, Cnt, @hditem)
    ;
    If (Cnt = Column And SortOrder <> #CSLI_UnSorted)
      Select SortOrder
        Case #CSLI_Ascent
          hditem\fmt & ~#HDF_SORTDOWN
          hditem\fmt | #HDF_SORTUP
        Case #CSLI_Descent
          hditem\fmt & ~#HDF_SORTUP
          hditem\fmt | #HDF_SORTDOWN
      EndSelect
    Else
      hditem\fmt & ~#HDF_SORTUP
      hditem\fmt & ~#HDF_SORTDOWN
    EndIf
    ;
    SendMessage_(ColumnHeader, #HDM_SETITEM, Cnt, @hditem)
    ;
  Next
EndProcedure
;
Procedure CSLI_ListIconHideHeaderImages(LIHandle, NColumn = -1)
  ;
  ; Hide images in the ListIcon header (useful when arrows are hidden).
  ; It will keep all other values stored in the column's header by #LVM_SETCOLUMN
  ; (inclusing index of the current image for each column), but no image will be shown.
  ;
  ; If NColumn <> -1, only the specified column is set. Else, all columns
  ; of the gadget are set.
  ;
  Protected LVC.LVCOLUMN, Column, Res
  ;
  If IsGadget(LIHandle)
    LIHandle = GadgetID(LIHandle) ; Get the control handle from the gadget's num.
  EndIf
  ;
  If NColumn = -1
    Column = 0
  Else
    Column = NColumn
  EndIf
  ;
  Repeat
    ; Hide images
    LVC\mask   = #LVCF_FMT
    Res = SendMessage_(LIHandle, #LVM_GETCOLUMN, Column, @LVC)
    If Res
      LVC\fmt & ~#LVCFMT_IMAGE | #LVCFMT_COL_HAS_IMAGES
      LVC\iImage = 0
      SendMessage_(LIHandle, #LVM_SETCOLUMN, Column, @LVC)
    EndIf
    ;
    CLSI_SetTopSortingIcon(LIHandle, Column, #CSLI_UnSorted)
    ;
    Column + 1
  Until Res = 0 Or NColumn <> -1
  ;
EndProcedure
;
; Declare SortListIcon procedure:
Declare SortListIcon(LIHandle, column, AscentDescent)
;
Procedure CSLI_WinLIProc(hWnd, uMsg, wParam, lParam)
  ;
  ; Window callback procedure to handle ListIcon sorting interaction.
  ;
  Protected NColumn, LVC.LVCOLUMN, LIHandle, AscentDescent
  Protected *NMHDR.NMHDR, *NMLV.NMLISTVIEW
  ;
  Select uMsg
    Case #WM_NOTIFY
      *NMHDR = lParam
      If GetProp_(*NMHDR\hWndFrom, "CSLI_Sort")
        ; Handle sort operations on column click:
        If *NMHDR\code = #LVN_COLUMNCLICK
          *NMLV    = lParam
          LIHandle = *NMHDR\hWndFrom
          ;
          If GetProp_(LIHandle, "CSLI_ShowArrow") = #CSLI_TopArrows
            ; Retrieve the actual sorting order from the top up-down arrow
            ; actually shown:
            AscentDescent = CLSI_GetSortOrderFromTopIcon(LIHandle, *NMLV\iSubItem)
          Else
            ; Retrieve the actual sorting order from the actual image-index
            ; used to show the left arrow:
            LVC\mask = #LVCF_IMAGE
            SendMessage_(LIHandle, #LVM_GETCOLUMN, *NMLV\iSubItem, @LVC)
            AscentDescent = LVC\iImage
            ;
            ; Set image-index to UpDown for all columns:
            While SendMessage_(LIHandle, #LVM_GETCOLUMN, NColumn, @LVC)
              LVC\iImage = #CSLI_Unsorted
              SendMessage_(LIHandle, #LVM_SETCOLUMN, NColumn, @LVC)
              NColumn + 1
            Wend
            ;
            If GetProp_(LIHandle, "CSLI_ShowArrow") <> #CSLI_LeftArrows
              ; If CSLI_ShowArrow is not set, hide the images:
              CSLI_ListIconHideHeaderImages(LIHandle)
            EndIf
          EndIf
          ;
          ; Switch the index:
          If AscentDescent = #CSLI_Ascent
            AscentDescent = #CSLI_Descent
          Else
            AscentDescent = #CSLI_Ascent
          EndIf
          ;
          ; Sort and update the icon for the clicked column.
          SortListIcon(LIHandle, *NMLV\iSubItem, AscentDescent)
          ;
        ElseIf *NMHDR\code = #LVN_DELETEALLITEMS
          ; The ListIcon gadget is being destroyed.
          ; Clean the memory from images:
          CSLI_FreeImageListFromGadget(*NMHDR\hWndFrom)
        EndIf
      EndIf
  EndSelect
  ;
  Protected CSLI_OldCallBack = GetProp_(hWnd, "CSLI_OldCallBack")
  ProcedureReturn CallWindowProc_(CSLI_OldCallBack, hWnd, uMsg, wParam, lParam)
EndProcedure
;
Procedure CSLI_InitializeSortableListIcon(LIHandle, Sortable = #True)
  ;
  ; Initialyze all the needed values of 'LIHandle' to allow it
  ; to be sortable.
  ;
  Protected LVC.LVCOLUMN, NColumn
  ;
  If Sortable                           ; Check if sorting should be enabled.
    SetProp_(LIHandle, "CSLI_Sort", 1)  ; Store the sorting state in the ListIcon handle property.
                                        ; This will allow to know that this gadget had been made sortable.
    If GetProp_(LIHandle, "CSLI_ShowArrow") < #CSLI_TopArrows
      ; The left sorting arrows will be used.
      CSLI_CreateListIconImgList(LIHandle); Create the image list for left sorting arrows icons.
      If GetProp_(LIHandle, "CSLI_ShowArrow") = #CSLI_NoArrow        ; If CSLI_ShowArrow is not set, hide the header images.
        CSLI_ListIconHideHeaderImages(LIHandle)
      ElseIf GetProp_(LIHandle, "CSLI_ShowArrow") = #CSLI_LeftArrows ; If the left sorting ShowArrows flag is enabled..
        NColumn  = 0
        LVC\mask = #LVCF_IMAGE
        While SendMessage_(LIHandle, #LVM_GETCOLUMN, NColumn, @LVC)
          LVC\mask   = #LVCF_IMAGE  ; Set the column to use image (for sorting arrows).
          LVC\iImage = #CSLI_Unsorted  ; Set the initial sorting state as unsorted.
          SendMessage_(LIHandle, #LVM_SETCOLUMN, NColumn, @LVC)  ; Set the column information.
          NColumn + 1
        Wend
      EndIf
    EndIf
    ;
    Protected hWnd = GetAncestor_(LIHandle, #GA_ROOT)  ; Get the root window handle for the ListIcon.
    Protected CSLI_OldCallBack = GetProp_(hWnd, "CSLI_OldCallBack")  ; Retrieve the CSLI_OldCallBack procedure if any.
    If CSLI_OldCallBack = 0                                          ; If there's no existing CSLI_OldCallBack, set it:
      CSLI_OldCallBack = SetWindowLongPtr_(hWnd, #GWL_WNDPROC, @CSLI_WinLIProc())  ; Set a custom window procedure.
      SetProp_(hWnd, "CSLI_OldCallBack", CSLI_OldCallBack)                         ; Store the Oldcallback procedure address.
    EndIf
  Else  ; If sorting is disabled
    CSLI_ListIconHideHeaderImages(LIHandle)  ; Hide any header images.
    CSLI_FreeImageListFromGadget(LIHandle)   ; Free the image list from the gadget.
    hWnd = GetAncestor_(LIHandle, #GA_ROOT)  ; Get the root window handle for the ListIcon.
    CSLI_OldCallBack = GetProp_(hWnd, "CSLI_OldCallBack")  ; Retrieve the old callback procedure.
    If CSLI_OldCallBack And GetWindowLongPtr_(hWnd, #GWL_WNDPROC) = @CSLI_WinLIProc()
      ; If there was a custom callback set earlier,
      ; reset to standard management:
      SetWindowLongPtr_(hWnd, #GWL_WNDPROC, CSLI_OldCallBack) ; Restore the original window procedure.
      RemoveProp_(hWnd, "CSLI_OldCallBack")                   ; Remove the stored callback address.
    EndIf
    RemoveProp_(LIHandle, "CSLI_Sort") ; Signal that the gadget is no longer sortable.
  EndIf
EndProcedure
;
;
; ****************************************************************************************
;
;-                 3--- THIRD PART: MAIN FUNCTIONS OF THIS LIBRARY ---
;
; ****************************************************************************************
;
Procedure MakeListIconSortable(GadgetNum, Setting = #CSLI_LeftArrows)
  ;
  ; Hide or show the sorting arrows in the header of 'LIHandle' gadget.
  ;
  ; The 'Setting' parameter can be:
  ;   #CSLI_NotSortable ; Reset the gadget to normal (not sortable).
  ;   #CSLI_NoArrow     ; Make the gadget sortable but don't show arrows.
  ;   #CSLI_LeftArrows  ; Show arrows on left side.
  ;   #CSLI_TopArrows   ; Show arrows above the column title.
  ;
  If IsGadget(GadgetNum)
    Protected LIHandle = GadgetID(GadgetNum) ; Get the control handle from the gadget's num.
  Else
    LIHandle  = GadgetNum
    GadgetNum = GetProp_(LIHandle, "PB_ID") ; Get the gadget's num from its handle.
  EndIf
  ;
  If IsGadget(GadgetNum) And GadgetType(GadgetNum) = #PB_GadgetType_ListIcon  ; Ensure it's a valid ListIcon gadget.
    ;
    CSLI_InitializeSortableListIcon(LIHandle, #False) ; Reset the ListIcon with no arrows setting.
    ;
    If Setting <> #CSLI_NotSortable
      SetProp_(LIHandle, "CSLI_ShowArrow", Setting)  ; Store whether the sorting arrows should be shown at left, at top, or hidden.
      CSLI_InitializeSortableListIcon(LIHandle, #True); Initialize the ListIcon with the updated sorting arrows setting.
    EndIf
  EndIf
  ;
EndProcedure
;
Procedure GetLastSortingColumn(GadgetNum)
  ProcedureReturn GetProp_(GadgetID(GadgetNum), "CSLI_LastSortingColumn")
EndProcedure
;
Procedure GetLastSortingAscentDescent(GadgetNum)
  ProcedureReturn GetProp_(GadgetID(GadgetNum), "CSLI_LastSortingAscentDescent")
EndProcedure
;
Procedure SortListIcon(GadgetNum, column, AscentDescent)
  ;
  ; Sort the whole content of a ListIconGadget from one column values.
  ;
  ; column : from 0 to the last column number.
  ; AscentDescent : #CSLI_Descent for descending, #CSLI_Ascent for ascending
  ;,,
  Protected SortInfo.SortInfoStruct, LVC.LVCOLUMN
  ;
  If IsGadget(GadgetNum)
    Protected LIHandle = GadgetID(GadgetNum) ; Get the control handle from the gadget's num.
  Else
    LIHandle  = GadgetNum
    GadgetNum = GetProp_(LIHandle, "PB_ID") ; Get the gadget's num from its handle.
  EndIf
  ;
  If IsGadget(GadgetNum) And GadgetType(GadgetNum) = #PB_GadgetType_ListIcon  ; Ensure it's a ListIcon gadget
    ;
    ; Memorize the used parameter:
    SetProp_(LIHandle, "CSLI_LastSortingColumn", column)
    SetProp_(LIHandle, "CSLI_LastSortingAscentDescent", AscentDescent)
    ;
    ; Initialize the ListIcon gadget with sorting enabled:
    CSLI_InitializeSortableListIcon(GadgetNum)  
    ;
    SortInfo\GadgetHandle  = LIHandle       ; Store the gadget handle in the sorting structure.
    SortInfo\Column        = column         ; Store the column to be sorted in the sorting structure.
    SortInfo\AscentDescent = AscentDescent  ; Store the sorting order (ascending or descending).
    SendMessage_(LIHandle, #LVM_SORTITEMSEX, @SortInfo, @CSLI_ListIconSortCallback())  ; Perform the sorting using the callback function.
    ;
    If GetProp_(LIHandle, "CSLI_ShowArrow") = #CSLI_TopArrows ; The top arrows will be shown:
      CLSI_SetTopSortingIcon(LIHandle, column, AscentDescent)
    Else ; The image-index will be uddated:
      ; Update the image index for the column to reflect the sorting order:
      LVC\mask   = #LVCF_IMAGE                             ; Set the column to use an image (for the sorting left arrow).
      LVC\iImage = AscentDescent                           ; Set the image index based on the sorting order.
      SendMessage_(LIHandle, #LVM_SETCOLUMN, column, @LVC) ; Set the updated column information.
    EndIf
    ;
    If GetProp_(LIHandle, "CSLI_ShowArrow") = #CSLI_NoArrow ; If the image must be hidden...
      ; The following will keep the value stored in the column by the preceeding #LVM_SETCOLUMN message
      ; (index of the current image for the column), but no image will be shown.
      CSLI_ListIconHideHeaderImages(LIHandle, column)
      ;
    EndIf
    ;
    ProcedureReturn #True  ; Return True to indicate success.
  EndIf
  ;
EndProcedure
;
; ****************************************************************************************
;
;-                         4--- FORTH PART: DEMO PROCEDURE ---
;
; ****************************************************************************************
;
CompilerIf #PB_Compiler_IsMainFile
  ; The following won't run when this file is used as 'Included'.
  Define HWindow = OpenWindow(#PB_Any, 0, 0, 550, 280, "Sortable ListIconGadget demo", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ;
  Define ListIconGagdet = ListIconGadget(#PB_Any, 0, 0, WindowWidth(HWindow), 225, "", 0, #PB_ListIcon_FullRowSelect | #PB_ListIcon_AlwaysShowSelection)
  ;
  AddGadgetColumn(ListIconGagdet, 0, "Alpha", 70)
  SetGadgetItemAttribute(ListIconGagdet, 0, #PB_ListIcon_ColumnAlignment, #PB_ListIcon_Center, 0)
  AddGadgetColumn(ListIconGagdet, 1, "Integer", 70)
  AddGadgetColumn(ListIconGagdet, 2, "Decimal", 80)
  AddGadgetColumn(ListIconGagdet, 3, "Hexa", 80)
  AddGadgetColumn(ListIconGagdet, 4, "Dates", 80)
  AddGadgetColumn(ListIconGagdet, 5, "Hours", 80)
  AddGadgetColumn(ListIconGagdet, 6, "Sporty", 80)
  SetGadgetItemAttribute(ListIconGagdet, 0, #PB_ListIcon_ColumnAlignment, #PB_ListIcon_Center, 6)
  ;
  Define ct
  For ct = 1 To 5
    SetGadgetItemAttribute(ListIconGagdet, 0, #PB_ListIcon_ColumnAlignment, #PB_ListIcon_Right, ct)
  Next
  ;
  Define mDate.q = Date()
  Define Line$
  Define AlphaList$ = "élève,devant,front,Absent,Column,Final,tirant,global,$,"
  Define HourList$ = "12:30:20,12:30:00,12:30,18:10,6:00 PM,11:00 AM,9:45:15,8:45,16h30mn,"
  Define SportyTimeList$ = "30'',5'20'',1'14''80''',1'14,5'80''',20'',10'50'',4'40''30''',90'''"
  For ct = 1 To 9
    Line$ = StringField(AlphaList$, ct, ",") + Chr(10)
    Line$ + Str(Random($FFFF)) + Chr(10)
    Line$ + StrF(Random($FFFFFF)/10000, 3) + Chr(10)
    Line$ + "$" + RSet(Hex(Random($7FFFFFFF)), 8, "0") + Chr(10)
    Line$ + FormatDate(CSLI_DateFormat$, mDate) + Chr(10)
    mDate + 100000
    Line$ + StringField(HourList$, ct, ",") + Chr(10)
    Line$ + StringField(SportyTimeList$, ct, ",") + Chr(10)
    AddGadgetItem(ListIconGagdet, - 1, Line$)
  Next
  ;
  Define NotSortableOption     = OptionGadget(#PB_Any, 15, WindowHeight(HWindow) - 50, 140, 22, "Reset to not sortable")
  Define DontShowArrowsOption  = OptionGadget(#PB_Any, 190, WindowHeight(HWindow) - 50, 150, 22, "Sortable with no arrows")
  Define ShowLeftArrowsOption  = OptionGadget(#PB_Any, 375, WindowHeight(HWindow) - 50, 150, 22, "Sortable with left arrows")
  Define ShowTopArrowsOption   = OptionGadget(#PB_Any, 15, WindowHeight(HWindow) - 30, 400, 22, "Sortable with top arrows (when clicked) - (XP compatible)")
  SetGadgetState(ShowLeftArrowsOption, 1)
  :
  ;
  MakeListIconSortable(ListIconGagdet, #CSLI_LeftArrows) ; <-- This is the only line you need to make the gadget sortable.
  ;
  ; But you can also decide to sort the gadget by default from one column content:
  SortListIcon(ListIconGagdet, 0, #CSLI_Ascent)
  ;
  Repeat
    Define Event = WaitWindowEvent()
    If Event = #PB_Event_Gadget
      Select EventGadget()
        Case NotSortableOption
          MakeListIconSortable(ListIconGagdet, #CSLI_NotSortable)
        Case DontShowArrowsOption
          MakeListIconSortable(ListIconGagdet, #CSLI_NoArrow)
        Case ShowLeftArrowsOption
          MakeListIconSortable(ListIconGagdet, #CSLI_LeftArrows)
        Case ShowTopArrowsOption
          MakeListIconSortable(ListIconGagdet, #CSLI_TopArrows)
      EndSelect
    EndIf
  Until Event = #PB_Event_CloseWindow
CompilerEndIf
Last edited by Zapman on Thu Mar 06, 2025 10:40 am, edited 22 times in total.
User avatar
Kwai chang caine
Always Here
Always Here
Posts: 5494
Joined: Sun Nov 05, 2006 11:42 pm
Location: Lyon - France

Re: Sort ListIconGadget [Windows only]

Post by Kwai chang caine »

Works nice, thanks for sharing 8)
It's already the begining of EXCEL :wink:
ImageThe happiness is a road...
Not a destination
Quin
Addict
Addict
Posts: 1122
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: Sort ListIconGadget [Windows only]

Post by Quin »

Works well here, thanks! :)
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: Sort ListIconGadget [Windows only]

Post by Zapman »

Thanks for testing, mates.
User avatar
Lord
Addict
Addict
Posts: 900
Joined: Tue May 26, 2009 2:11 pm

Re: Sort ListIconGadget [Windows only]

Post by Lord »

Hi!

Nice worg, but is there any reason why this doesn't work with PB 6.20 x64 (C- and ASM back end) on WIndows 11?
On PB 6.04LTS (C + ASM) and 6.11LTS (C) it works.
Image
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: Sort ListIconGadget [Windows only]

Post by Zapman »

Lord wrote: Sun Feb 23, 2025 12:43 pmNice worg, but is there any reason why this doesn't work with PB 6.20 x64 (C- and ASM back end) on WIndows 11?
Hi, Lord.
It works here with PB 6.20 Beta 4 - C Backend (Windows - x64) with Windows 11.
What happens exactly for you?
User avatar
Lord
Addict
Addict
Posts: 900
Joined: Tue May 26, 2009 2:11 pm

Re: Sort ListIconGadget [Windows only]

Post by Lord »

No arrows, no sorting happens.
System:
Intel Core i5-13400F
Microsoft Windows 11 Professional (x64) Build 22631.3880 (23H2)
NVIDIA GeForce RTX 4060 Ti 8GB
Image
User avatar
Mindphazer
Enthusiast
Enthusiast
Posts: 456
Joined: Mon Sep 10, 2012 10:41 am
Location: Savoie

Re: Sort ListIconGadget [Windows only]

Post by Mindphazer »

Same here : nothing happens, and no arrows
PB 6.20, Windows Server 2019 and Windows Server 2022
MacBook Pro 16" M4 Pro - 24 Gb - MacOS 15.4.1 - Iphone 15 Pro Max - iPad at home
...and unfortunately... Windows at work...
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: Sort ListIconGadget [Windows only]

Post by Zapman »

Lord wrote: Sun Feb 23, 2025 5:06 pmNo arrows, no sorting happens.
Mindphazer wrote: Sun Feb 23, 2025 6:00 pmSame here : nothing happens, and no arrows
I've updated the code figuring in the first post of this subject, hoping this will fix the problem.
User avatar
Mindphazer
Enthusiast
Enthusiast
Posts: 456
Joined: Mon Sep 10, 2012 10:41 am
Location: Savoie

Re: Sort ListIconGadget [Windows only]

Post by Mindphazer »

I can confirm it's now working for me

Thanks Zapman
MacBook Pro 16" M4 Pro - 24 Gb - MacOS 15.4.1 - Iphone 15 Pro Max - iPad at home
...and unfortunately... Windows at work...
dcr3
Enthusiast
Enthusiast
Posts: 181
Joined: Fri Aug 04, 2017 11:03 pm

Re: Sort ListIconGadget [Windows only]

Post by dcr3 »

I think your date routine needs to be fixed. :?:
User avatar
Mindphazer
Enthusiast
Enthusiast
Posts: 456
Joined: Mon Sep 10, 2012 10:41 am
Location: Savoie

Re: Sort ListIconGadget [Windows only]

Post by Mindphazer »

dcr3 wrote: Sun Feb 23, 2025 9:05 pm I think your date routine needs to be fixed. :?:
Indeed
MacBook Pro 16" M4 Pro - 24 Gb - MacOS 15.4.1 - Iphone 15 Pro Max - iPad at home
...and unfortunately... Windows at work...
User avatar
Lord
Addict
Addict
Posts: 900
Joined: Tue May 26, 2009 2:11 pm

Re: Sort ListIconGadget [Windows only]

Post by Lord »

Hi Zapman!

You can ignore my PM because I read first your PM and then your answer in this thread.
I can confirm, that the code is here also now working as it should. :D
Image
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: Sort ListIconGadget [Windows only]

Post by Zapman »

Mindphazer wrote: Sun Feb 23, 2025 8:17 pm I can confirm it's now working for me
Lord wrote: Mon Feb 24, 2025 8:32 amI can confirm, that the code is here also now working as it should. :D
Thanks a lot, both of you, for your time and your returns. It helped me a lot.
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: Sort ListIconGadget [Windows only]

Post by Zapman »

Mindphazer wrote: Sun Feb 23, 2025 9:39 pm
dcr3 wrote: Sun Feb 23, 2025 9:05 pm I think your date routine needs to be fixed. :?:
Indeed
Sorry, I don't understand. It's working fine with a french setting. What happens for you? What do you see?
Post Reply