Jump to content

ListView Column Order


Recommended Posts

With a listview control, when I drag a column to a new location, I can no longer loop through the columns in ascending sequence. I would like to set the new column order as an ascending numeric sequence, so that I can loop through the columns normally (after dragging). Is there a way to force the ListView control to forget/reset the order of columns. The alternative is to implement complicated loop sequences and unwieldly code, which is painful and hell to debug.

Edit: What I've tried: _GUICtrlListView_SetColumnOrderArray() moves the columns back to their original locations (that's not it :'(). I can't set the column number as an attribute - the method appears to be unsupported. It looks like I'm just going to have to track the column positions (all the time) and factor that in for every action taken which will probably add about 5% bloat to my project. Pity!

Edited by czardas
Link to comment
Share on other sites

You could use _GUICtrlListView_GetColumnOrderArray and loop through the array instead.

If I posted any code, assume that code was written using the latest release version unless stated otherwise. Also, if it doesn't work on XP I can't help with that because I don't have access to XP, and I'm not going to.
Give a programmer the correct code and he can do his work for a day. Teach a programmer to debug and he can do his work for a lifetime - by Chirag Gude
How to ask questions the smart way!

I hereby grant any person the right to use any code I post, that I am the original author of, on the autoitscript.com forums, unless I've specifically stated otherwise in the code or the thread post. If you do use my code all I ask, as a courtesy, is to make note of where you got it from.

Back up and restore Windows user files _Array.au3 - Modified array functions that include support for 2D arrays.  -  ColorChooser - An add-on for SciTE that pops up a color dialog so you can select and paste a color code into a script.  -  Customizable Splashscreen GUI w/Progress Bar - Create a custom "splash screen" GUI with a progress bar and custom label.  -  _FileGetProperty - Retrieve the properties of a file  -  SciTE Toolbar - A toolbar demo for use with the SciTE editor  -  GUIRegisterMsg demo - Demo script to show how to use the Windows messages to interact with controls and your GUI.  -   Latin Square password generator

Link to comment
Share on other sites

Yes and that would have to be done for every change made to the listview, if dragging columns is allowed. That's where the 5% unwieldy code comes from. This is for an undo buffer I'm intending to implement, so the code is quite complicated already. It seems I have no alternative other than to than to do it your way, but I was hoping I might be able to simplify the code. I implemented a similar tracking process recently with TagSort, so I can do it using the same idea: I just hate bloat for the sake of a simple reset feature not existing (apparently). Sadly, loop code is no longer straight forward (having to always refer to a key).

On the other hand there are also some advantages to be gained from this implementation. I would just like to choose when I need to track something and when tracking is not needed. It's not a big deal.

Edited by czardas
Link to comment
Share on other sites

What do you mean exactly by looping through the columns in ascending sequence? Do you mean to loop through the columns from the leftmost column to the rightmost column? But this is not the usual opinion (at least not the MicroSoft opinion) of running through the columns in ascending order.

The normal way to loop through the columns in ascending sequence and the way which is supported by MicroSoft code in the header and the listview controls is to use $iSubItem from $iSubItem = 0 to $iSubItem = $iColumns - 1. Looping through the columns in this way works for a large majority of all listview commands whether the columns have been dragged to other positions or not.

$iSubItem always refers to the same column whether the columns have been rearranged or not.

If two or more columns have been dragged to other positions the sequence from $iSubItem = 0 to $iSubItem = $iColumns - 1 does of course not match the left to right order of the columns.

If you insists of looping through the columns in a left to right order (which changes every time the columns are rearranged) you need to use _GUICtrlListView_GetColumnOrderArray. But don't do that. You should use $iSubItem to loop through the columns.

_GUICtrlListView_GetSubItemRect is an example of a command in which there is a problem when columns have been rearranged. It only works for $iSubItem > 0. For $iSubItem = 0 you must use _GUICtrlListView_GetItemRect. In this example I have only used _GUICtrlListView_GetSubItemRect. That's the reason why it doesn't work for the column with $iSubItem = 0, if that column has been dragged to a new position. I'll fix it.

Conclusion: If you use $iSubItem to keep track of columns, you can usually add the $LVS_EX_HEADERDRAGDROP extended style to support rearranging of columns without changing any code at all.

Edited by LarsJ
Conclusion
Link to comment
Share on other sites

46 minutes ago, LarsJ said:

$iSubItem always refers to the same column whether the columns have been rearranged or not.

Yes and that has merit, but it also imposes some restrictions. When we are in the business of manipulating data within a listview, writing an undo/redo buffer makes sense and presents certain challenges. The user is interacting with the control (not Microsoft) and they wouldn't be dragging columns to different positions simply for fun. To write consistent undo history code, one has to decide on the main architecture and try to stick with the same coherent method throughout. Starting with the premise that a table can always be read from top left to bottom right in an ascending sequence is a very good design concept.

Thanks for the tips, I will look at the functions you mention. I wondered about your implementation.

46 minutes ago, LarsJ said:

That's the reason why it doesn't work for the column with $iSubItem = 0, if that column has been dragged to a new position. I'll fix it.

I have already written a patch which forces the first row to always hide itself before the user gets time to interact with it. If by some fluke the user somehow manages to drag the column at lightning speed, it instantly snaps back before the user gets chance to click anywhere else. It might be possible to get around these prevention measures using a script, but that's not particularly important.

Edited by czardas
Link to comment
Share on other sites

  • 3 years later...
On 5/25/2016 at 4:27 PM, BrewManNH said:

You could use _GUICtrlListView_GetColumnOrderArray and loop through the array instead

 

On 5/26/2016 at 7:49 AM, LarsJ said:

Conclusion: If you use $iSubItem to keep track of columns, you can usually add the $LVS_EX_HEADERDRAGDROP extended style to support rearranging of columns without changing any code at all.

This topic is important. I'm resurrecting it after 3.5 years to provide a runnable script that shows how to keep looping through listview columns from left to right, even after you made a mess by scrambling all listview headers, dragging & dropping them anywhere. Thanks to LarsJ above and another post of BrewManNH, here.

1523267969_901f3-draggableheaders.png.367043350c59891ad6ce80e4c028f66c.png

; updated 23 Dec. 2019 - listview draggable headers => keep scrolling left to right

#include <GUIConstantsEx.au3>
#include <GuiListView.au3>
#include <WindowsConstants.au3>
#include <WinAPIvkeysConstants.au3>

Global $g_aCol[0], $g_bEndDrag = False, $g_hHeader, $g_hListView
Global $g_idDummy_Dbl_Click, $g_idListView, $g_iSubItem = -1, $g_iItem = -1

Example()

;============================================
Func Example()

    Local $hGUI = GUICreate("Wandering through ListView (901f #3)", 460, 500, -1, -1, _
        BitOR($GUI_SS_DEFAULT_GUI, $WS_MAXIMIZEBOX, $WS_SIZEBOX, $WS_THICKFRAME))

    $g_idListView = GUICtrlCreateListView _
        ("      Col 0       |    Col 1|    Col 2|    Col 3|    Col 4|" & _
        "    Col 5|    Col 6|    Col 7|    Col 8|    Col 9", 15, 15, 430, 470)

    $g_hListView = GuiCtrlGetHandle($g_idListView)
    $g_hHeader = _GUICtrlListView_GetHeader($g_idListView)
    $g_aCol = _GUICtrlListView_GetColumnOrderArray($g_idListView)

    _GUICtrlListView_SetExtendedListViewStyle($g_idListView, _
        BitOR($LVS_EX_HEADERDRAGDROP, $LVS_EX_FULLROWSELECT, $WS_EX_CLIENTEDGE, $LVS_EX_GRIDLINES))

    _GUICtrlListView_SetCallBackMask($g_idListView, 4) ; "disable state information about focused item" (LarsJ) . Can be useful (keep that line)

    Local $sRow
    For $iRow = 0 To 99
        $sRow = StringFormat("%2s", $iRow)
        GUICtrlCreateListViewItem( _
            "Row " & $sRow & " / Col 0 |" & "Row " & $sRow & " / Col 1 |" & _
            "Row " & $sRow & " / Col  2|" & "Row " & $sRow & " / Col 3 |" & _
            "Row " & $sRow & " / Col  4|" & "Row " & $sRow & " / Col 5 |" & _
            "Row " & $sRow & " / Col  6|" & "Row " & $sRow & " / Col 7 |" & _
            "Row " & $sRow & " / Col  8|" & "Row " & $sRow & " / Col 9", $g_idListView)
    Next

    $g_idDummy_Dbl_Click = GUICtrlCreateDummy()
    Local $idDummy_Enter = GUICtrlCreateDummy()

    Local $aAccelKeys[1][2] = [["{ENTER}", $idDummy_Enter]]
    GUISetAccelerators($aAccelKeys)

    GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY")
    GUISetState(@SW_SHOW)

    While 1
        If $g_bEndDrag Then
            $g_aCol = _GUICtrlListView_GetColumnOrderArray($g_hListView)
            $g_bEndDrag = False
        EndIf

        Switch GUIGetMsg()
            Case $GUI_EVENT_CLOSE
                GUIDelete($hGUI)
                Exit

            Case $g_idDummy_Dbl_Click
                Horizontal_Scroll()
                MsgBox($MB_TOPMOST, "Double-click activated cell", _
                    "Row " & $g_iItem & " / Col " & $g_iSubItem)

            Case $idDummy_Enter
                Horizontal_Scroll()
                If _WinAPI_GetFocus() = $g_hListView And $g_iItem > -1 Then
                    MsgBox($MB_TOPMOST, "Enter activated cell", _
                        "Row " & $g_iItem & " / Col " & $g_iSubItem)
                EndIf
        EndSwitch
    WEnd

EndFunc   ;==>Example

;============================================
Func WM_NOTIFY($hWnd, $iMsg, $wParam, $lParam)
    #forceref $hWnd, $iMsg, $wParam

    Local $tNMHDR, $hWndFrom, $iIDFrom, $iCode
    $tNMHDR = DllStructCreate($tagNMHDR, $lParam)
    $hWndFrom = DllStructGetData($tNMHDR, "hWndFrom")
    $iCode = DllStructGetData($tNMHDR, "Code")

    Static $bMouseDown = False

    Switch $hWndFrom
        Case $g_hListView
            Switch $iCode
                Case $NM_CUSTOMDRAW
                    Local $tCustDraw = DllStructCreate($tagNMLVCUSTOMDRAW, $lParam)
                    Local $iDrawStage = DllStructGetData($tCustDraw, "dwDrawStage")
                    If $iDrawStage = $CDDS_PREPAINT Then Return $CDRF_NOTIFYITEMDRAW
                    If $iDrawStage = $CDDS_ITEMPREPAINT Then Return $CDRF_NOTIFYSUBITEMDRAW
                    Local $iItem = DllStructGetData($tCustDraw, "dwItemSpec")
                    Local $iSubItem = DllStructGetData($tCustDraw, "iSubItem")
                    Local $iColor = 0xFF000000 ; this is $CLR_DEFAULT in ColorConstants.au3
                    If $iItem = $g_iItem And $iSubItem = $g_iSubItem Then
                        $iColor = 0xFFFFC0 ; light blue for 1 subitem (BGR)
                    EndIf
                    DllStructSetData($tCustDraw, "clrTextBk", $iColor)
                    Return $CDRF_NEWFONT

                Case $LVN_KEYDOWN
                    If $bMouseDown Or $g_iItem = -1 Then Return 1 ; don't process
                    ; =====
                    ; https://www.autoitscript.com/forum/topic/195016-lvn_keydowntagnmlvkeydown-does-not-work-in-64-bit
                    Local Static $tagNMLVKEYDOWN = $tagNMHDR & ";align 2;word VKey;uint Flags" ; mandatory Local as Global Const in StructureConstants.au3
                    ; =====
                    Local $tInfo = DllStructCreate($tagNMLVKEYDOWN, $lParam)
                    Local $iVK = DllStructGetData($tInfo, "VKey")
                    Switch $iVK
                        Case $VK_RIGHT
                            Local $iCol = _ArraySearch($g_aCol, $g_iSubItem, 1)
                            If $iCol < $g_aCol[0] Then
                                $g_iSubItem = $g_aCol[$iCol + 1]
                                Horizontal_Scroll()
                                _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem)
                            EndIf

                        Case $VK_LEFT
                            Local $iCol = _ArraySearch($g_aCol, $g_iSubItem, 1)
                            If $iCol > 1 Then
                                $g_iSubItem = $g_aCol[$iCol - 1]
                                Horizontal_Scroll()
                                _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem)
                            EndIf

                        Case $VK_SPACE; don't process spacebar (it would select the whole row)
                            Return 1
                    EndSwitch

                Case $NM_RELEASEDCAPTURE
                    $bMouseDown = True
                    Local $iItemSave = $g_iItem
                    Local $aHit = _GUICtrlListView_SubItemHitTest($g_hListView)
                    $g_iItem = $aHit[0]
                    $g_iSubItem = $aHit[1]
                    If $g_iItem = -1 And $iItemSave > -1 Then
                        _GUICtrlListView_RedrawItems($g_hListView, $iItemSave, $iItemSave)
                    EndIf

                Case $LVN_ITEMCHANGED
                    Local $tInfo = DllStructCreate($tagNMLISTVIEW, $lParam)
                    Local $iNewState = DllStructGetData($tInfo, "NewState")
                    Switch $iNewState
                        Case BitOr($LVIS_FOCUSED, $LVIS_SELECTED)
                            $g_iItem = DllStructGetData($tInfo, "Item")
                            _GUICtrlListView_SetItemSelected($g_hListView, $g_iItem, False)
                    EndSwitch

                Case $NM_CLICK, $NM_RCLICK
                    $bMouseDown = False

                Case $NM_DBLCLK
                    $bMouseDown = False
                    If $g_iItem > -1 Then GUICtrlSendToDummy($g_idDummy_Dbl_Click)
            EndSwitch

        Case $g_hHeader
            Switch $iCode
                Case $HDN_ENDDRAG ; sent by a header control when a drag operation has ended on one of its items
                    $g_bEndDrag = True
                    Return False ; allow the control to automatically place and reorder the item
            EndSwitch

    EndSwitch
    Return $GUI_RUNDEFMSG

EndFunc   ;==>WM_NOTIFY

;============================================
Func Horizontal_Scroll()

    Local $iWidth = _GUICtrlListView_GetColumnWidth($g_hListView, $g_iSubItem)

    If $g_iSubItem Then
        Local $aRect = _GUICtrlListView_GetSubItemRect($g_hListView, $g_iItem, $g_iSubItem)
    Else ; column 0 needs _GUICtrlListView_GetItemRect() in case it has been dragged elsewhere (LarsJ)
        Local $aRect = _GUICtrlListView_GetItemRect($g_hListView, $g_iItem, 2) ; 2 = bounding rectangle of the item text
        $aRect[0] -= 4 ; adjust coords (tested)
    EndIf

    Local $aClientSize = WinGetClientSize($g_hListView) ; including headers, excluding both scrollbars (if any), great !

    If $iWidth >= $aClientSize[0] Then
        $iWidth = $aClientSize[0]
        If $aRect[0] <> 0 Then
            _GUICtrlListView_Scroll($g_idListView, $aRect[0], 0)
        EndIf
    Else
        If $aRect[0] < 0 Then
            _GUICtrlListView_Scroll($g_idListView, $aRect[0], 0)
        Else
            If $aRect[0] + $iWidth > $aClientSize[0] Then
                _GUICtrlListView_Scroll($g_idListView, $aRect[0] + $iWidth - $aClientSize[0], 0)
            EndIf
        EndIf
    EndIf

EndFunc   ;==>Horizontal_Scroll

Czardas seemed a bit disappointed when LarsJ wrote :
"But this is not the usual opinion (at least not the MicroSoft opinion) of running through the columns in ascending order."

As Czardas, I don't understand why this native left-to-right looping is suddenly broken as soon as you drag a listview column out of its initial place. Don't we do it in Excel since day 1 ?

When you do same in Excel (dragging a column at another place) it doesn't break at all the left-to-right looping, then why should it be different in a listview control, when you're allowed to move columns at your wish ?

I may be mistaken but the answer could be that, natively, you're not supposed to scroll horizontally cell by cell in a listview control (as you do in Excel), that could be the reason why Microsoft didn't care about the scrolling break from left-to-right when you drag a column in listview.

But of course, they carefully change all indexes of the items when using the LVM_SORTITEMSEX message... because you natively scroll vertically by row (it would have been fun if they didn't change the vertical items indexes, happy vertical scrolling !)

If you consider that moving a column means "a new column horizontal sort", then the left-to-right scroll shouldn't have been broken.

Anyway, many thanks guys for your explanations :)  I already have included this important new functionality to my script "CSV file editor", I just have to rewrite a few parts of it (for example, the exported csv file should probably be created according to the new columns order... or not, I'll have to think about it)

Edited by pixelsearch
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...