Jump to content

[SOLVED] Sort column of checkboxes in ListView


ogeiz
 Share

Recommended Posts

Hi!

Having a listview where the 1st column contains only checkboxes.

When clicking on the header "Column1", I'm trying to sort the rows according their checkbox state, i.e. group the checked and group the unchecked ones.

The code below contains the callback function "LVSort" that is registered via GUICtrlRegisterListViewSort() and shall compare two items for their ordering.

The function LVSort gets the arguments $nItem1, $nItem2 that (somehow) refer the listview items.

The UDFs _GUICtrlListView_GetItemText() and _GUICtrlListView_GetItemChecked() cannot be used here to get the content of the items to sort, as $nItem1 and $nItem2 are not the required item index. Hmpf.

Fortunately as example the UDF help pages contain the definition of func GetSubItemText() to read the (sub)item strings in that case.

Kudos to its author!

With small improvements I use it for sorting of column 2 and 3.

But I cannot think a way how to adapt this function for reading the checkbox state.

Nevertheless I see 3 options to proceed:

  • use a function to read the state (analogous to GetSubItemText)
  • find a way to map argument $nItem1 to listview item index, then use _GUICtrlListView_GetItemXXX
  • mark the listview items before inserting them in the listview, then use the marker for to reference the items. _GUICtrlListView_MapIDToIndex seems to be a good candidate, but how can I then access the state of the checkboxes? Using an external array to mirror the check state seems to be a waste of resources to me.

I ask you for suggestions or solutions.

Background info to the LVSort's arguments is welcome, too.

Thanks,

ogeiz

(BTW: I'm running Environment = 3.3.0.0 under  WIN_XP/Service Pack 2 X86 )

EDIT: solved by workaround, see #5

This implementation has problems in the lines marked with: ### Wrong

; *******************************************************
; Example 1 - sorting different columns
; *******************************************************
#include <WindowsConstants.au3>
#include <ListViewConstants.au3>
#include <GuiListView.au3>
#include <GUIConstants.au3>
#include <GuiListView.au3>
Opt("GUIOnEventMode", 1)


Dim $nSortDir = 1
Dim $bSet = 0
Dim $nCol = -1

$hGUI = GUICreate("Test", 300, 200)
GUISetOnEvent($GUI_EVENT_CLOSE, "_Terminate")

$lv = GUICtrlCreateListView("Column1|Col2|Col3", 10, 10, 280, 180, -1, $LVS_EX_CHECKBOXES)
GUICtrlSetOnEvent($lv, "_Setsort")
GUICtrlRegisterListViewSort($lv, "LVSort"); Register the function "SortLV" for the sorting callback

$lvi1 = GUICtrlCreateListViewItem("|AAA|10.05.2003", $lv)
_GUICtrlListView_SetItemChecked($lv, 0, True)
$lvi2 = GUICtrlCreateListViewItem("|BBB|11.05.2001", $lv)
_GUICtrlListView_SetItemChecked($lv, 1, False)
$lvi3 = GUICtrlCreateListViewItem("|CCC|12.05.2002", $lv)
_GUICtrlListView_SetItemChecked($lv, 2, True)
$lvi4 = GUICtrlCreateListViewItem("|DDD|13.05.2000", $lv)
_GUICtrlListView_SetItemChecked($lv, 3, False)

GUISetState()

While 1
    Sleep(10)
WEnd

Func _Terminate()
    Exit
EndFunc   ;==>_Terminate

Func _Setsort()
    $bSet = 0
    GUICtrlSendMsg($lv, $LVM_SETSELECTEDCOLUMN, GUICtrlGetState($lv), 0)
    DllCall("user32.dll", "int", "InvalidateRect", "hwnd", GUICtrlGetHandle($lv), "int", 0, "int", 1)
    Local $state ; print current checkbox state before sorting
    For $i = 0 To 3
        $state &= '  #' & $i & ': Col2="' & _GUICtrlListView_GetItemText($lv, $i, 1) & _
                '" checked=' & 0 + _GUICtrlListView_GetItemChecked($lv, $i)
    Next
    ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') : State before sorting' & $state & @CRLF)
EndFunc   ;==>_Setsort


; sorting callback funtion
Func LVSort($hWnd, $nItem1, $nItem2, $nColumn)
    Local $nSort

    ; Switch the sorting direction
    If Not $bSet Then
        If $nColumn = $nCol Then
            $nSortDir = $nSortDir * - 1
        Else
            $nSortDir = 1
        EndIf
        $bSet = 1
    EndIf
    $nCol = $nColumn

    ; sort depends on content of column (column starts with 0)
    Switch $nColumn
        Case 0 ; checkboxes
            $val1 = 0 + _GUICtrlListView_GetItemChecked($lv, $nItem1) ; ### Wrong
            $val2 = 0 + _GUICtrlListView_GetItemChecked($lv, $nItem2) ; ### Wrong
            ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') :' & _
                    ' $nItem1=' & $nItem1 & ' Col2="' & GetSubItemText($lv, $nItem1, 1) & '" $val1=' & $val1 & '  ' & _
                    ' $nItem2=' & $nItem2 & ' Col2="' & GetSubItemText($lv, $nItem2, 1) & '" $val2=' & $val2 & @CRLF)
        Case 1 ; text
            $val1 = GetSubItemText($lv, $nItem1, $nColumn)
            $val2 = GetSubItemText($lv, $nItem2, $nColumn)
        Case 2 ; dates
            $val1 = GetSubItemText($lv, $nItem1, $nColumn)
            $val2 = GetSubItemText($lv, $nItem2, $nColumn)
            $val1 = StringRight($val1, 4) & StringMid($val1, 4, 2) & StringLeft($val1, 2)
            $val2 = StringRight($val2, 4) & StringMid($val2, 4, 2) & StringLeft($val2, 2)
    EndSwitch

    $nResult = 0 ; No change of item1 and item2 positions

    If $val1 < $val2 Then
        $nResult = -1 ; Put item2 before item1
    ElseIf $val1 > $val2 Then
        $nResult = 1 ; Put item2 behind item1
    EndIf

    $nResult = $nResult * $nSortDir

    Return $nResult
EndFunc   ;==>LVSort


; Retrieve the text of a listview item in a specified column
Func GetSubItemText($nCtrlID, $nItemID, $nColumn)
    Local $stLvfi = DllStructCreate("uint;ptr;int;int[2];int")
    Local $nIndex, $stBuffer, $stLvi, $sItemText

    DllStructSetData($stLvfi, 1, $LVFI_PARAM)
    DllStructSetData($stLvfi, 3, $nItemID)

    $stBuffer = DllStructCreate("char[260]")

    $nIndex = GUICtrlSendMsg($nCtrlID, $LVM_FINDITEM, -1, DllStructGetPtr($stLvfi));

    $stLvi = DllStructCreate("uint;int;int;uint;uint;ptr;int;int;int;int")

    DllStructSetData($stLvi, 1, $LVIF_TEXT)
    DllStructSetData($stLvi, 2, $nIndex)
    DllStructSetData($stLvi, 3, $nColumn)
    DllStructSetData($stLvi, 6, DllStructGetPtr($stBuffer))
    DllStructSetData($stLvi, 7, 260)

    GUICtrlSendMsg($nCtrlID, $LVM_GETITEMA, 0, DllStructGetPtr($stLvi));

    $sItemText = DllStructGetData($stBuffer, 1)

    $stLvi = 0
    $stLvfi = 0
    $stBuffer = 0

    Return $sItemText
EndFunc   ;==>GetSubItemText
Edited by ogeiz
Link to comment
Share on other sites

  • 2 weeks later...

I've solved it sometimes so (but i don't find my script).

Thats the way:

- add a new column with width=0 (following named as col_state)

- add an function that checks the state of checkboxes if an mouseclick occured

- write in col_state an '1' if is checked and an '0' or empty string otherwise

- now you can sort by col_state

Don't check the state in an loop before you want to sort. It needs a lot of time to read state and write '0' or '1'.

Thats why use the mouse event.

Best Regards BugFix  

Link to comment
Share on other sites

Hi BugFix

Thanks for the hint. Storing the info twice, one  version sortable but invisible, and the checkboxes as user interface is a clever workaround.

But is it really impossible for the user to widen the 0-width column?

ogeiz

Edit:

I've tried and run into two problems:

- with function

 _GUICtrlListView_SetColumnWidth($hWnd, $iCol, $iWidth)

I can't change the second's column width, the help says: "For list-view mode, this (=$iCol) parameter must be set to zero". With iCol=0, I will make the column of the checkboxes invisible.

- the zeroed column an be opened by the user, when he badly puts the mouse pointer on the header column sepatators.

Any idea?

How to show checkboxes in the second column? 

Edited by ogeiz
Link to comment
Share on other sites

Implemented the workaround suggested by BugFix, see source below.

Use a hidden column with numeric values for sorting the column of checkboxes.

The function LVSort contains a few improvements in relation to the GUICtrlRegisterListViewSort() function reference example:

  • fewer compares with each call of the function, fewer statements
  • each sort will reverse the sequence of equal values, too. So the second click on the column header will now really revert the whole list, and not only the different items.
  • some bells and whisles: show sort direction by arrow in list header, mark sorted column by grey background

May the source be with you...

ogeiz

Here is the corrected, working script for all people interested in it:

#cs ----------------------------------------------------------------------------

 AutoIt Version: 3.3.0.0
 Author:         ogeiz

 Script Function:
    Improved example for GUICtrlRegisterListViewSort - sort column with checkboxes.

#ce ----------------------------------------------------------------------------

#include <WindowsConstants.au3>
#include <ListViewConstants.au3>
#include <GuiListView.au3>
#include <GUIConstants.au3>
#include <GuiListView.au3>
Opt("GUIOnEventMode", 1)


Dim $nSortDir = 1 ; toggle for sort up/down
Dim $bSet = 0 ; detect the 1st call of LVSort() within a sorting sequence
Dim $nCol = -1 ; preserve last column to detect 2nd click (for reverse direction)

$hGUI = GUICreate("Test", 300, 200)
GUISetOnEvent($GUI_EVENT_CLOSE, "_Terminate")

; create an zero-width column between 'Column1' and 'Col2' just for sorting the checkboxes
$lv = GUICtrlCreateListView("Column1||Col2|Col3", 10, 10, 280, 180, -1, BitOR($LVS_EX_CHECKBOXES, $LVS_EX_HEADERDRAGDROP))
GUICtrlSetOnEvent($lv, "_Setsort") ; clicking the column header will start the sort
GUICtrlRegisterListViewSort($lv, "LVSort") ; Register the function "SortLV" as the sorting callback

; Note: the hidden column #1 contains [x]<->'0', [ ]<->'1' to ease sorting active checkboxes at top with 1st click
$lvi1 = GUICtrlCreateListViewItem("|0|AAA|10.05.2003", $lv)
_GUICtrlListView_SetItemChecked($lv, 0, True)
$lvi2 = GUICtrlCreateListViewItem("|1|BBB|11.05.2001", $lv)
_GUICtrlListView_SetItemChecked($lv, 1, False)
$lvi3 = GUICtrlCreateListViewItem("|0|CCC|12.05.2002", $lv)
_GUICtrlListView_SetItemChecked($lv, 2, True)
$lvi4 = GUICtrlCreateListViewItem("|1|DDD|13.05.2000", $lv)
_GUICtrlListView_SetItemChecked($lv, 3, False)

; shrink the (hidden) sorting column #1 to 0 pt width
_GUICtrlListView_SetColumnWidth($lv, 1, 0)

; use WM_NOTIFY to detect change of checkbox state and update hidden column
GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY")

GUISetState()

While 1
  Sleep(10)
WEnd

;-------------------------------------------------------------------------------------------------

Func _Terminate()
  Exit
EndFunc   ;==>_Terminate

; some initializations when starting the sort
Func _Setsort()
  $bSet = 0 ; initialize for new sorting

  ; bells and whistles (I):
  ; mark the sorted column with a grey rectangle
  GUICtrlSendMsg($lv, $LVM_SETSELECTEDCOLUMN, GUICtrlGetState($lv), 0)
  DllCall("user32.dll", "int", "InvalidateRect", "hwnd", GUICtrlGetHandle($lv), "int", 0, "int", 1)

  ; bells and whistles (II):
  ; create an arrow in the listview header
  Local $iFormat
  Local Const $hHeader = _GUICtrlListView_GetHeader($lv)
  ; clear existing arrows
  For $x = 0 To  _GUICtrlHeader_GetItemCount($hHeader) - 1
    $iFormat = _GUICtrlHeader_GetItemFormat($hHeader, $x)
    If BitAND($iFormat, $HDF_SORTDOWN) Then
      _GUICtrlHeader_SetItemFormat($hHeader, $x, BitXOR($iFormat, $HDF_SORTDOWN))
    ElseIf BitAND($iFormat, $HDF_SORTUP) Then
      _GUICtrlHeader_SetItemFormat($hHeader, $x, BitXOR($iFormat, $HDF_SORTUP))
    EndIf
  Next
  ; set arrow in current column
  Local $nColumn = GUICtrlGetState($lv)
  $iFormat = _GUICtrlHeader_GetItemFormat($hHeader, $nColumn)
  If $nSortDir == 1 And $nCol == $nColumn Then ; ascending
    _GUICtrlHeader_SetItemFormat($hHeader, $nColumn, BitOR($iFormat, $HDF_SORTUP))
  Else ; descending
    _GUICtrlHeader_SetItemFormat($hHeader, $nColumn, BitOR($iFormat, $HDF_SORTDOWN))
  EndIf
EndFunc   ;==>_Setsort


;===============================================================================
;
; Function Name..: LVSort
; Description....: Sort listview columns - checkboxes, text, dates.
; Parameters.....: $hWnd    - The controlID of the listview control for which the callback function is used.
;                  $nItem1  - The lParam value of the first item (by default the item controlID).
;                  $nItem2  - The lParam value of the second item (by default the item controlID).
;                  $nColumn - The column that was clicked for sorting (the first column number is 0).
; Requirements...: use as callback function for GUICtrlRegisterListViewSort()
; Return Values..: -1       - 1st item should precede the 2nd.
;                   0       - No Change.
;                   1       - 1st item should follow the 2nd.
; Author.........: see example of function reference GUICtrlRegisterListViewSort()
; Modified.......: ogeiz: sort checkboxes, optimized direction checks, stable sort (revert sequence of equal values
;                  with direction change to permit sort sequences: least important column to most important column)
;
;===============================================================================
Func LVSort($hWnd, $nItem1, $nItem2, $nColumn)
  Local $val1, $val2
  ; Switch the sorting direction
  If Not $bSet Then
    If $nColumn = $nCol Then
      $nSortDir = -$nSortDir
    Else
      $nSortDir = 1
    EndIf
    $bSet = 1
  EndIf
  $nCol = $nColumn

  ; sort depends on content of column (column starts with 0)
  Switch $nColumn
    Case 0, 1 ; checkboxes (column 0) => use always column 1 to sort
      $val1 = GetSubItemText($lv, $nItem1, 1) ; use '[x]'==0 to sort at top and '[ ]'==1 at bottom
      $val2 = GetSubItemText($lv, $nItem2, 1)
    Case 2 ; text
      $val1 = GetSubItemText($lv, $nItem1, $nColumn)
      $val2 = GetSubItemText($lv, $nItem2, $nColumn)
    Case 3 ; dates - reorder from dd.mm.yyyy -> yyyymmdd, then sort by value
      $val1 = GetSubItemText($lv, $nItem1, $nColumn)
      $val2 = GetSubItemText($lv, $nItem2, $nColumn)
      $val1 = StringRight($val1, 4) & StringMid($val1, 4, 2) & StringLeft($val1, 2)
      $val2 = StringRight($val2, 4) & StringMid($val2, 4, 2) & StringLeft($val2, 2)
  EndSwitch

  If $val1 < $val2 Or ($val1 == $val2 And $nItem1 < $nItem2) Then
    Return -$nSortDir ; Put item2 before item1
  Else
    Return $nSortDir ; Put item2 behind item1
  EndIf
EndFunc   ;==>LVSort


; Retrieve the text of a listview item in a specified column
Func GetSubItemText($nCtrlID, $nItemID, $nColumn)
  Local $stLvfi = DllStructCreate("uint;ptr;int;int[2];int")
  Local $nIndex, $stBuffer, $stLvi, $sItemText

  DllStructSetData($stLvfi, 1, $LVFI_PARAM)
  DllStructSetData($stLvfi, 3, $nItemID)

  $stBuffer = DllStructCreate("char[260]")

  $nIndex = GUICtrlSendMsg($nCtrlID, $LVM_FINDITEM, -1, DllStructGetPtr($stLvfi));

  $stLvi = DllStructCreate("uint;int;int;uint;uint;ptr;int;int;int;int")

  DllStructSetData($stLvi, 1, $LVIF_TEXT)
  DllStructSetData($stLvi, 2, $nIndex)
  DllStructSetData($stLvi, 3, $nColumn)
  DllStructSetData($stLvi, 6, DllStructGetPtr($stBuffer))
  DllStructSetData($stLvi, 7, 260)

  GUICtrlSendMsg($nCtrlID, $LVM_GETITEMA, 0, DllStructGetPtr($stLvi));

  $sItemText = DllStructGetData($stBuffer, 1)

  $stLvi = 0
  $stLvfi = 0
  $stBuffer = 0

  Return $sItemText
EndFunc   ;==>GetSubItemText


; use WM_NOTIFY to detect the state change of a checkbox and adapt the value in hidden column accordingly
Func WM_NOTIFY($hWnd, $iMsg, $iwParam, $ilParam)
  #forceref $hWnd, $iMsg, $iwParam
  Local $tNMHDR, $hWndFrom, $iCode
  Local Const $h_lv = GUICtrlGetHandle($lv)

  $tNMHDR = DllStructCreate($tagNMHDR, $ilParam)
  $hWndFrom = HWnd(DllStructGetData($tNMHDR, "hWndFrom"))
  $iCode = DllStructGetData($tNMHDR, "Code")
  Switch $hWndFrom
    Case $h_lv ; need Handle for detecting listview  $h_lv = GUICtrlGetHandle($h_lv)
      Switch $iCode
        Case $LVN_ITEMCHANGED ; An listview item has changed
          Local $tInfo = DllStructCreate($tagNMLISTVIEW, $ilParam)
          Local $iItem = DllStructGetData($tInfo, "Item")
          _GUICtrlListView_SetItem($lv, 1 - _GUICtrlListView_GetItemChecked($h_lv, $iItem), $iItem, 1)
          ; No return value
      EndSwitch
  EndSwitch
EndFunc   ;==>WM_NOTIFY
Link to comment
Share on other sites

Hi,

i've changed a little bit. Now the user can't change the column width of zero-width-column.

Func WM_NOTIFY($hWnd, $iMsg, $iwParam, $ilParam)
  #forceref $hWnd, $iMsg, $iwParam
  Local $tNMHDR, $hWndFrom, $iCode
  Local Const $h_lv = GUICtrlGetHandle($lv)

  $tNMHDR = DllStructCreate($tagNMHDR, $ilParam)
  $hWndFrom = HWnd(DllStructGetData($tNMHDR, "hWndFrom"))
  $iCode = DllStructGetData($tNMHDR, "Code")
  Switch $hWndFrom
    Case $h_lv ; need Handle for detecting listview  $h_lv = GUICtrlGetHandle($h_lv)
      Switch $iCode
; ####### build in this part #################################################################
        Case -12 ; User has changed column width
            If _GUICtrlListView_GetColumnWidth($h_lv, 1) <> 0 Then _
                _GUICtrlListView_SetColumnWidth($h_lv, 1, 0) ; width of column 1 reset to zero
; ############################################################################################
        Case $LVN_ITEMCHANGED ; An listview item has changed
          Local $tInfo = DllStructCreate($tagNMLISTVIEW, $ilParam)
          Local $iItem = DllStructGetData($tInfo, "Item")
          _GUICtrlListView_SetItem($lv, 1 - _GUICtrlListView_GetItemChecked($h_lv, $iItem), $iItem, 1)
          ; No return value
      EndSwitch
  EndSwitch
EndFunc   ;==>WM_NOTIFY
Edited by BugFix

Best Regards BugFix  

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...