#include #include #include #include #include #include #include #include #include #include #include Opt("GuiCloseOnESC", 0) ;1=Esc closes, 0=Esc won't close (0 will avoid closing the GUI by mistake and lose all modifications) Opt("GUIResizeMode", 1) ;0=no resizing, <1024 special resizing (1 = resize and reposition all controls, according to new window size) ; Global variables $g_ (alphabetically) : please do not modify any value here . Function _BufferCreate() contains delayed Global variables Global $g_aCol, $g_aRect, $g_asDelim[3], $g_bBufferExist = False, $g_bChangesMade = False, $g_bCheck_EOF Global $g_bEndDrag = False, $g_bHeaderMenu = False, $g_bHeaders1stRow = True, $g_bMouseDown = False Global $g_hDll_comctl32 = DllOpen("comctl32.dll"), $g_hEdit, $g_hFont, $g_hGUI, $g_hHeader, $g_hListView Global $g_iBGR = _RGB2BGR(0xC0FFFF), $g_idDummy_Dbl_Click, $g_idListView, $g_idMarker4Del, $g_idStatusBar, $g_iHeight = 0, $g_iRowCount = -1 Global $g_iSortDirection = 0, $g_iSubItem = -1, $g_iSubItemEdit = -1, $g_iItem = -1, $g_iItemEdit = -1, $g_iItemHeight = 0, $g_iWidth = 0 Global $g_pListViewCallback, $g_sDelimiter = ",", $g_sEOL, $g_sFileNameNoPath_Import, $g_sSubItemText_Old = "" Example() ;============================================ Func Example() ; Static variables : please do not modify any value here Local Static $bChainedEdit = False Local Static $iChainedEdit = 0 ; 0 = No chain edit, 1 = Horizontal, 2 = Vertical Local Static $iEditFontSize = 15 Local Static $bMinimize = False ; see $GUI_EVENT_RESTORE, used to keep edited cell opened and selected (depending on the preceding GUI state) ; Gui creation (without listview which will be created dynamically during csv import) ; Gui will be resizable/maximizable just after csv import (as listview needs to be created with its constant coords matching Gui initial constant coords) Local $sGUIDefaultTitle = "CSV File Editor (901x)" $g_hGUI = GUICreate($sGUIDefaultTitle, 800, 450, -1, -1, -1, $WS_EX_ACCEPTFILES) ; allow drag and drop Local $aGUIPos = WinGetPos($g_hGUI) ; keep initial GUI coords (reuse them when a 2nd, 3rd... csv files are imported) Local $idTxt_Import = GUICtrlCreateButton("Text Import (CSV)", 16, 17, 111, 31) Local $idTxt_Close = GUICtrlCreateButton("Close file", 16, 65, 111, 31) GUICtrlSetState(-1, $GUI_DISABLE) GUICtrlCreateGroup(" Import options ", 144, 10, 161, 89) Local $idDelimiter = GUICtrlCreateCombo(" , comma delimited", 154, 32, 141, 25, BitOR($CBS_DROPDOWNLIST,$CBS_AUTOHSCROLL)) Local $sDataSeparator = Opt("GUIDataSeparatorChar", "*") ; changing "|" (pipe is the default) to "*" GUICtrlSetData($idDelimiter, " ; semicolon* tab* | pipe") Opt("GUIDataSeparatorChar", $sDataSeparator) ; reset original Data Separator to "|" (pipe is the default) Local $idHeaders1stRow = GUICtrlCreateCheckbox("1st row = Headers", 154, 66, 133, 21) GUICtrlSetState(-1, $GUI_CHECKED) GUICtrlCreateGroup("", -99, -99, 1, 1) GUICtrlCreateGroup(" Selection / Edit options ", 320, 10, 185, 89) Local $idSelectionColor = GUICtrlCreateButton("Selection color", 328, 32, 81, 25) GUICtrlSetTip(-1, "Click to change the selection background color") Local $idShowColor = GUICtrlCreateLabel("", 416, 37, 50, 15) GUICtrlSetBkColor(-1, _BGR2RGB($g_iBGR)) GUICtrlCreateLabel("Edit font size", 328, 70, 72, 17) Local $idEditFontSize = GUICtrlCreateInput("15", 416, 60, 50, 30, BitOR($GUI_SS_DEFAULT_INPUT,$ES_CENTER,$ES_READONLY,$ES_NUMBER)) GUICtrlSetFont(-1, 10, 100) GUICtrlCreateUpdown($idEditFontSize) GUICtrlSetLimit(-1, 24, 8) GUICtrlCreateGroup("", -99, -99, 1, 1) GUICtrlCreateGroup(" Chained Edit ? ", 520, 10, 135, 89) Local $idChainedEdit_No = GUICtrlCreateRadio("No, thanks", 530, 30, 100, 17) GUICtrlSetState(-1, $GUI_CHECKED) Local $idChainedEdit_H = GUICtrlCreateRadio("Horizontally", 530, 50, 100, 17) GUICtrlSetTip(-1, "If checked, edition will automatically continue with the cell on its right") Local $idChainedEdit_V = GUICtrlCreateRadio("Vertically", 530, 70, 100, 17) GUICtrlSetTip(-1, "If checked, edition will automatically continue with the cell below") GUICtrlCreateGroup("", -99, -99, 1, 1) Local $idTxt_Export = GUICtrlCreateButton("Text Export (CSV)", 672, 40, 111, 31) GUICtrlSetState(-1, $GUI_DISABLE) Local $iLeftLV = 15, $iTopLV = 108, $iWidthLV = 770, $iHeightLV = 326 ; used first for drop control coords, then for listview coords ; Drop control (in case user drags & drops the imported csv file), it will be deleted and replaced by listview control in Import_csv() Local $idDroppable_Zone = GUICtrlCreateGroup("", $iLeftLV, $iTopLV, $iWidthLV, $iHeightLV) GUICtrlSetState(-1, $GUI_DROPACCEPTED) Local $idDroppable_Label = GUICtrlCreateLabel("Import droppable zone", $iLeftLV + 20, $iTopLV + 20, $iWidthLV - 40, $iHeightLV - 40, _ BitOr($SS_CENTERIMAGE, $SS_CENTER)) GUICtrlCreateGroup("", -99, -99, 1, 1) $g_idStatusBar = GUICtrlCreateLabel("", $iLeftLV, $iTopLV + $iHeightLV + 1, $iWidthLV, 14) ; 14 and nothing else (tested) $g_idDummy_Dbl_Click = GUICtrlCreateDummy() Local $idDummy_Enter = GUICtrlCreateDummy() Local $idDummy_Esc = GUICtrlCreateDummy() Local $aAccelKeys[2][2] = [["{ENTER}", $idDummy_Enter], ["{ESC}", $idDummy_Esc]] GUISetAccelerators($aAccelKeys) ; Create ListView callback function, used only to catch scrollbar messages while editing (listview scrollbars are child windows of listview) $g_pListViewCallback = DllCallbackGetPtr(DllCallbackRegister("ListViewCallback", "lresult", "hwnd;uint;wparam;lparam;uint_ptr;dword_ptr")) Local $iGetMsg = 0 GUISetState(@SW_SHOW) ; Main loop While 1 If $g_bHeaderMenu Then ; right click on header control => Header context menu Header_menu() ; $g_bChangesMade will be updated there (depending on option choosen by user) $g_bHeaderMenu = False EndIf If $g_bEndDrag Then ; end drag header control $g_aCol = _GUICtrlListView_GetColumnOrderArray($g_idListView) $g_bChangesMade = True $g_bEndDrag = False EndIf If $bChainedEdit Then Switch $iChainedEdit Case 1 ; horizontal If $g_iSubItem <> $g_aCol[$g_aCol[0]] Then Send("{RIGHT}{ENTER}") ; start automatic editing of the cell on its right EndIf Case 2 ; vertical If $g_iItem < $g_iRowCount Then $g_iItem += 1 Send("{DOWN}{ENTER}") ; start automatic editing of the cell below EndIf EndSwitch $bChainedEdit = False EndIf $iGetMsg = GUIGetMsg() Switch $iGetMsg Case $idDummy_Enter If $g_iItemEdit > -1 Then ; edit in progress... End_Edit(1) ; 1 = update subitem text If $iChainedEdit Then $bChainedEdit = True ; will start automatic editing of the cell on its right (horizontal) or below (vertical) ContinueLoop EndIf If _WinAPI_GetFocus() = $g_hListView And $g_iItem > -1 Then Start_Edit($iEditFontSize) Case $g_idDummy_Dbl_Click ; fired by Case $NM_DBLCLK, when listview got the focus and $g_iItem > -1 If $g_iItemEdit > -1 Then End_Edit(1) ; 1 = update subitem text Start_Edit($iEditFontSize) Case $idDummy_Esc If $g_iItemEdit > -1 Then End_Edit(0) ; 0 = don't update subitem text (revert) Case $idDelimiter $g_sDelimiter = StringMid(GUICtrlRead($idDelimiter), 2, 1) Case $idHeaders1stRow $g_bHeaders1stRow = ((GUICtrlRead($idHeaders1stRow) = $GUI_CHECKED) ? (True) : (False)) Case $idSelectionColor GUICtrlSetState($idSelectionColor, $GUI_DISABLE) Local $iChooseColor = _ChooseColor(1, $g_iBGR, 1, $g_hGUI) ; $g_hGUI prevents clicking inside GUI while Color dialog box is waiting. ; Same behavior applied to ALL message boxes within the script. GUICtrlSetState($idSelectionColor, $GUI_ENABLE) ; ============================================================= If $iChooseColor <> -1 Then $g_iBGR = $iChooseColor GUICtrlSetBkColor($idShowColor, _BGR2RGB($g_iBGR)) If $g_iItem > -1 Then _GUICtrlListView_RedrawItems($g_hListView, $g_iItem, $g_iItem) EndIf Case $idEditFontSize $iEditFontSize = GUICtrlRead($idEditFontSize) Case $idChainedEdit_No $iChainedEdit = 0 ; no chained edit Case $idChainedEdit_H $iChainedEdit = 1 ; horizontal Case $idChainedEdit_V $iChainedEdit = 2 ; vertical Case $g_idStatusBar If $g_iItem > -1 Then _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) Horizontal_Scroll(0) EndIf Case $idTxt_Import, $GUI_EVENT_DROPPED Local $bDropped = (($iGetMsg = $GUI_EVENT_DROPPED) ? True : False) Local $iImport_status = Import_csv($bDropped, $idDroppable_Zone, $idDroppable_Label, $idTxt_Import, _ $idDelimiter, $idHeaders1stRow, $idSelectionColor, $idTxt_Export, $idTxt_Close, _ $iLeftLV, $iTopLV, $iWidthLV, $iHeightLV) Switch $iImport_status Case -9 To -1 ; Return < 0 MsgBox($MB_TOPMOST, "Bad import", _ "Exiting program ", 0, $g_hGUI) ExitLoop Case 0 ; Return 0 (user canceled file import dialog, or user dropped a non-csv file => wait & see) Case Else ; Return > 0 (import successful => modify, then export) GUISetStyle(BitOR($GUI_SS_DEFAULT_GUI, $WS_MAXIMIZEBOX, $WS_SIZEBOX, $WS_THICKFRAME), -1, $g_hGUI) ; only after listview has been created HotKeySet("{INS}", "Ins_Item") ; inserts a row (place it here, only after $g_hListView = a handle, so _WinAPI_GetFocus() = $g_hListView will be tested correctly) HotKeySet("{DEL}", "Del_Item") ; delete a row (place it here, etc...) GUIRegisterMsg($WM_LBUTTONUP, "WM_LBUTTONUP") ; solves a little issue in case a cell is dragged in listview (even if listview had focus) GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY") ; place it here (user is about to modify the listview items/subitems) EndSwitch Case $idTxt_Export If $g_iItemEdit > -1 Then End_Edit(1) ; 1 = update subitem text Export_csv() ; no need to test Return value here Case $idTxt_Close If $g_iItemEdit > -1 Then End_Edit(1) ; 1 = update subitem text If $g_bChangesMade = True Then If _SaveOrNot("Close file", "closing file") = $IDCANCEL Then ContinueLoop ; $IDYES, $IDNO, $IDCANCEL : $IDYES fully treated in _SaveOrNot() EndIf _SplashOn("Closing file") GUIRegisterMsg($WM_NOTIFY, "") GUIRegisterMsg($WM_LBUTTONUP, "") HotKeySet("{INS}") HotKeySet("{DEL}") _SendMessage($g_hListView, $LVM_DELETEALLITEMS) ; possible because code based on _GUICtrlListView_AddArray() was used to populate the listview control. ; it wouldn't have be so easy if GUICtrlCreateListViewItem() had been used. GUICtrlDelete($g_idListView) GUICtrlDelete($g_idMarker4Del) GUICtrlSetData($g_idStatusBar, "") GUISetStyle($GUI_SS_DEFAULT_GUI, -1, $g_hGUI) ; not maxizable/resizable until listview is created & populated . -1 = keep extended style unchanged ($WS_EX_ACCEPTFILES) WinMove($g_hGUI, "", $aGUIPos[0], $aGUIPos[1], $aGUIPos[2], $aGUIPos[3]) ; initial coords (listview coords will be related to them) $idDroppable_Zone = GUICtrlCreateGroup("", $iLeftLV, $iTopLV, $iWidthLV, $iHeightLV) GUICtrlSetState(-1, $GUI_DROPACCEPTED) $idDroppable_Label = GUICtrlCreateLabel("Import droppable zone", $iLeftLV + 20, $iTopLV + 20, $iWidthLV - 40, $iHeightLV - 40, _ BitOr($SS_CENTERIMAGE, $SS_CENTER)) GUICtrlCreateGroup("", -99, -99, 1, 1) WinSetTitle($g_hGUI, "", $sGUIDefaultTitle) GUICtrlSetState($idTxt_Export, $GUI_DISABLE) GUICtrlSetState($idTxt_Close, $GUI_DISABLE) GUICtrlSetState($idTxt_Import, $GUI_ENABLE) GUICtrlSetState($idDelimiter, $GUI_ENABLE) GUICtrlSetState($idHeaders1stRow, $GUI_ENABLE) ; Reset some important global variables (in case another csv file will be opened) Redim $g_aCol[1] $g_aCol[0] = 0 ; $g_aCol[0] always exists, its value may be = 0 if all columns have been deleted $g_bBufferExist = False $g_bChangesMade = False $g_bMouseDown = False $g_hHeader = 0 $g_hListView = 0 $g_iRowCount = -1 $g_iSubItem = -1 $g_iItem = -1 ; if any problem occurs, check if a variable should have been reinitialized here ('here' = in case another csv file will be opened) SplashOff() Case $GUI_EVENT_MAXIMIZE, $GUI_EVENT_RESIZED ; these 2 are mandatory to avoid bad redraw when edit control is opened (tested) If $g_iItemEdit > -1 Then End_Edit(1) ; 1 = update subitem text Case $GUI_EVENT_MINIMIZE $bMinimize = True Case $GUI_EVENT_RESTORE ; have subitem always visible, keep edited cell opened and selected (depending on the preceding GUI state) If $g_iItem > -1 Then _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) Horizontal_Scroll(0) If $g_iItemEdit > -1 Then ; edit in progress... If $bMinimize = False Then End_Edit(1) ; 1 = update subitem text Else _GUICtrlEdit_SetSel($g_hEdit, 0, -1) ; select all text _WinAPI_SetFocus($g_hEdit) EndIf EndIf EndIf $bMinimize = False Case $GUI_EVENT_CLOSE If $g_iItemEdit > -1 Then End_Edit(1) ; 1 = update subitem text If WinGetTitle($g_hGUI) = $sGUIDefaultTitle Then ExitLoop ; no csv imported (the script modifies Gui title when csv is imported) If $g_bChangesMade = True Then If _SaveOrNot("Exit", "exiting") <> $IDCANCEL Then ExitLoop ; $IDYES, $IDNO, $IDCANCEL : $IDYES fully treated in _SaveOrNot() Else ExitLoop EndIf EndSwitch WEnd ; Remove ListView subclass callback _WinAPI_RemoveWindowSubclass($g_hListView, $g_pListViewCallback, 9999) _WinAPI_DeleteObject($g_hFont) ; +++ GUIDelete($g_hGUI) DllClose($g_hDll_comctl32) EndFunc ;==>Example ;============================================ Func ListViewCallback($hWnd, $iMsg, $wParam, $lParam, $iSubclassId, $pData) #forceref $iSubclassId, $pData Switch $iMsg Case $WM_VSCROLL, $WM_HSCROLL, $WM_MOUSEWHEEL If $g_iItemEdit > -1 Then Return ; deactivate both scrollbars & mouse wheel while editing the cell (then redraw will be ok) EndSwitch ; Call next function in subclass chain Return DllCall($g_hDll_comctl32, "lresult", "DefSubclassProc", "hwnd", $hWnd, "uint", $iMsg, "wparam", $wParam, "lparam", $lParam)[0] EndFunc ;==>ListViewCallback ;============================================ 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") 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") If $iItem = $g_iItem And $iSubItem = $g_iSubItem Then DllStructSetData($tCustDraw, "clrTextBk", $g_iBGR) Else DllStructSetData($tCustDraw, "clrTextBk", $CLR_DEFAULT) ; this is 0xFF000000 in ColorConstants.au3 EndIf Return $CDRF_NEWFONT Case $LVN_KEYDOWN If $g_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) $g_iSubItem = (($iCol < $g_aCol[0]) ? $g_aCol[$iCol + 1] : $g_aCol[1]) Horizontal_Scroll(0) _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem) Case $VK_LEFT Local $iCol = _ArraySearch($g_aCol, $g_iSubItem, 1) $g_iSubItem = (($iCol > 1) ? $g_aCol[$iCol - 1] : $g_aCol[$g_aCol[0]]) Horizontal_Scroll(0) _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem) Case $VK_SPACE ; don't process spacebar (it would select the whole row) Return 1 EndSwitch Case $NM_RELEASEDCAPTURE $g_bMouseDown = True If _WinAPI_GetFocus() <> $g_hListView Then _WinAPI_SetFocus($g_hListView) ; solves a non-important little issue in case a cell is dragged in listview (when listview didn't have focus) 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 If $g_iItemEdit > -1 Then ; if edit in progress, test click position (end edit if click outside of the editing rectangle) Local $aPos = MouseGetPos() ScreenToClient($g_hListView, $aPos) ; $aPos ByRef will be changed (+++) . Converts mouse coords to listview coords If Not ($aPos[0] > $g_aRect[0] And $aPos[0] < $g_aRect[0] + $g_iWidth _ And $aPos[1] > $g_aRect[1] And $aPos[1] < $g_aRect[1] + $g_iHeight -1) Then ; tested $g_iHeight -1 (+++) End_Edit(1) ; 1 = update subitem text EndIf 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 $g_bMouseDown = False Case $NM_DBLCLK $g_bMouseDown = False If $g_iItem > -1 Then GUICtrlSendToDummy($g_idDummy_Dbl_Click) EndSwitch Case $g_hHeader Switch $iCode Case $HDN_BEGINTRACK, $HDN_BEGINTRACKW, $HDN_BEGINDRAG If $g_iItemEdit > -1 Then End_Edit(1) ; 1 = update subitem text _GUICtrlListView_RedrawItems($g_hListView, $g_iItem, $g_iItem) ; needed to make selection visible if BEGINTRACK on another column EndIf Return False ; $HDN_BEGINTRACK, $HDN_BEGINTRACKW : allow tracking of the divider. ; $HDN_BEGINDRAG : allow the header control to automatically manage drag-and-drop operations. Case $HDN_ENDDRAG $g_bEndDrag = True Return False ; allow the control to automatically place and reorder the item Case $NM_RCLICK $g_bHeaderMenu = True If $g_iItemEdit > -1 Then End_Edit(1) ; 1 = update subitem text _GUICtrlListView_RedrawItems($g_hListView, $g_iItem, $g_iItem) ; needed to make selection visible EndIf EndSwitch EndSwitch Return $GUI_RUNDEFMSG EndFunc ;==>WM_NOTIFY ;============================================ Func WM_LBUTTONUP($hWnd, $iMsg, $wParam, $lParam) #forceref $hWnd, $iMsg, $wParam, $lParam $g_bMouseDown = False Return $GUI_RUNDEFMSG EndFunc ;==>WM_LBUTTONUP ;============================================ Func Start_Edit(Const ByRef $iEditFontSize) ; Static variables : please do not modify any value here Local Static $iEditFontSize_Old = -1 ; Install ListView subclass callback to handle messages related to both Scrollbars controls. This avoids bad redrawing while editing. _WinAPI_SetWindowSubclass($g_hListView, $g_pListViewCallback, 9999, 0) ; $g_iItem never -1 at this stage _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) ; in case of double-click on an eventual partially hidden row at bottom of LV $g_iWidth = Horizontal_Scroll(1) ; 1 means it's called from here (during edition) and a Return value is required $g_iHeight = $g_iItemHeight + (($g_iItemHeight < 18) ? 2 : 1) ; tests made If $g_iSubItem Then $g_aRect = _GUICtrlListView_GetSubItemRect($g_hListView, $g_iItem, $g_iSubItem) Else ; column 0 needs _GUICtrlListView_GetItemRect() in case it has been dragged elsewhere (LarsJ) $g_aRect = _GUICtrlListView_GetItemRect($g_hListView, $g_iItem, 2) ; 2 = bounding rectangle of the item text $g_aRect[0] -= 4 ; adjust coords (tested) EndIf $g_aRect[1] -= 2 ; better design (tested) $g_iItemEdit = $g_iItem $g_iSubItemEdit = $g_iSubItem $g_sSubItemText_Old = _GUICtrlListView_GetItemText($g_hListView, $g_iItem, $g_iSubItem) $g_hEdit = _GUICtrlEdit_Create($g_hListView, $g_sSubItemText_Old, $g_aRect[0], $g_aRect[1], $g_iWidth, $g_iHeight, _ BitOR($WS_CHILD, $WS_VISIBLE, $ES_AUTOHSCROLL, $ES_LEFT)) ; Edit height relative to Item height If $iEditFontSize <> $iEditFontSize_Old Then _WinAPI_DeleteObject($g_hFont) ; will not exist 1st time, no prob. $g_hFont = _WinAPI_CreateFont($iEditFontSize, 0, 0, 0, 400, False, False, False, $DEFAULT_CHARSET, _ $OUT_DEFAULT_PRECIS, $CLIP_DEFAULT_PRECIS, $DEFAULT_QUALITY, 0, 'Tahoma') $iEditFontSize_Old = $iEditFontSize EndIf _WinAPI_SetFont($g_hEdit, $g_hFont, True) _GUICtrlEdit_SetSel($g_hEdit, 0, -1) ; select all text _WinAPI_SetFocus($g_hEdit) EndFunc ;==>Start_Edit ;============================================ Func End_Edit($iUpdate_Text) Local $sText = (($iUpdate_Text) ? (_GUICtrlEdit_GetText($g_hEdit)) : ($g_sSubItemText_Old)) _GUICtrlListView_SetItemText($g_hListView, $g_iItemEdit, $sText, $g_iSubItemEdit) _WinAPI_DestroyWindow($g_hEdit) If $g_iItemEdit <> $g_iItem Or $g_iSubItemEdit <> $g_iSubItem Then _GUICtrlListView_RedrawItems($g_hListView, $g_iItemEdit, $g_iItemEdit) EndIf If $iUpdate_Text Then $g_bChangesMade = True $g_iItemEdit = -1 ; -1 means no pending edition (+++) ; Remove ListView subclass callback when no cell is being edited (redraw is ok when scrollbars are used while no cell is being edited) _WinAPI_RemoveWindowSubclass($g_hListView, $g_pListViewCallback, 9999) EndFunc ;==>End_Edit ;============================================ Func Horizontal_Scroll($iWhoCalls) ; $iWhoCalls = 0 when called by $VK_RIGHT, $VK_LEFT during WM_NOTIFY() when the selected cell is partially or no more visible in LV client area ; $iWhoCalls = 0 when called from main loop => Case $g_idStatusBar (focus on selected cell when user doesn't know where it is) ; $iWhoCalls = 0 when called from main loop => Case $GUI_EVENT_RESTORE (selected subitem/edited cell will always be visible) ; $iWhoCalls = 0 when called from Header_menu() (insert column / delete column / any kind of sort) ; $iWhoCalls = 1 when called during edition, in case a subitem is edited in a half-hidden column (on left or right side) of listview 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 ; $iWidth < $aClientSize[0] If $aRect[0] < 0 Then _GUICtrlListView_Scroll($g_idListView, $aRect[0], 0) Else ; $aRect[0] >= 0 If $aRect[0] + $iWidth > $aClientSize[0] Then _GUICtrlListView_Scroll($g_idListView, $aRect[0] + $iWidth - $aClientSize[0], 0) EndIf EndIf EndIf ; Nothing special to return when $iWhoCalls = 0 If $iWhoCalls = 1 Then Return $iWidth EndFunc ;==>Horizontal_Scroll ;============================================ Func ScreenToClient($hWindow, ByRef $aCoord) ; convert screen coords to $hWindow client coords (i.e mouse to listview, or listview to GUI, or mouse to header...) ; no problem if $aCoord got more than 2 elements, only the first 2 elements will be updated Local $tPoint = DllStructCreate("int X;int Y") DllStructSetData($tPoint, "X", $aCoord[0]) DllStructSetData($tPoint, "Y", $aCoord[1]) _WinAPI_ScreenToClient($hWindow, $tPoint) $aCoord[0] = DllStructGetData($tPoint, "X") $aCoord[1] = DllStructGetData($tPoint, "Y") ; ByRef modified the Array in the calling function (+++) EndFunc ;==>ScreenToClient ;============================================ Func _BufferCreate() ; Same buffer used, replacing repeated calls to _GUICtrlListView_GetItemText() _GUICtrlListView_SetItemText() or even _GUICtrlListView_AddArray() ; Buffer will stay opened for input/output while a CSV file is opened (speeding up I/O a max) ; Buffer will be reset before another CSV file is opened (as it may be Unicode or not) ; This function will be called by any part of the script who needs it first. ; As $g_idListView isn't defined until a CSV file is imported, global variables will be created/updated within this function. ; Parts of script calling this function for writing : Import (write buffer to populate listview) ; Parts of script calling this function for reading : Sort (numeric or string), Export (read buffer) ; Parts of script calling this function for reading & writing : Natural sort (read buffer, ArrayMultiColSort, write buffer) Global $g_bUnicode = GUICtrlSendMsg($g_idListView, $LVM_GETUNICODEFORMAT, 0, 0) <> 0 Global $g_InsertItem = ($g_bUnicode ? $LVM_INSERTITEMW : $LVM_INSERTITEMA) Global $g_SetItem = ($g_bUnicode ? $LVM_SETITEMW : $LVM_SETITEMA) Global $g_GetItemText = ($g_bUnicode ? $LVM_GETITEMTEXTW : $LVM_GETITEMTEXTA) Global $g_SetItemText = ($g_bUnicode ? $LVM_SETITEMTEXTW : $LVM_SETITEMTEXTA) Global $g_tBuffer = DllStructCreate(($g_bUnicode ? "wchar Text[4096]" : "char Text[4096]")) Global $g_tItem = DllStructCreate($tagLVITEM) Global $g_pItem = DllStructGetPtr($g_tItem) DllStructSetData($g_tItem, "Text", DllStructGetPtr($g_tBuffer)) DllStructSetData($g_tItem, "TextMax", 4096) ; "only used when the structure receives item attributes" (doc 'LVITEM structure') ; "ignored when the structure specifies item attributes" ; "For example, cchTextMax is ignored during LVM_SETITEM and LVM_INSERTITEM" $g_bBufferExist = True EndFunc ;==>_BufferCreate ;============================================ Func _SaveOrNot($sString1, $sString2) HotKeySet("{INS}") ; prevents firing HotKey "Insert" during MsgBox (in case user press it by mistake) HotKeySet("{DEL}") ; same for "Del" key WinSetOnTop($g_hGUI, "", 1) ; 1 = on top, 0 = not on top (on top is good before important MsgBox confirmation) Local $iSaveOrNot = MsgBox(BitOr($MB_TOPMOST, $MB_ICONWARNING, $MB_YESNOCANCEL), _ $sString1, _ "Save your modifications before " & $sString2 & " ?", 0, $g_hGUI) If $iSaveOrNot = $IDYES Then Local $iExport_Return = Export_csv() Switch $iExport_Return Case -1, 0 ; 0 = user canceled file export dialog , -1 = FileOpen error during export => wait & see in both cases $iSaveOrNot = $IDCANCEL Case 1 ; export succeeded Case Else ; impossible MsgBox($MB_TOPMOST, "Export_csv()", _ "Unknown value returned = " & $iExport_Return, 0, $g_hGUI) Exit EndSwitch EndIf WinSetOnTop($g_hGUI, "", 0) ; 1 = on top, 0 = not on top HotKeySet("{DEL}", "Del_Item") HotKeySet("{INS}", "Ins_Item") Return $iSaveOrNot ; $IDYES fully treated in this function. Other possible Return(s) are $IDNO, $IDCANCEL EndFunc ;==>_SaveOrNot ;============================================ Func _IsHeaderNameUnique($sHeader_TextNew) For $i = 1 To $g_aCol[0] ; $g_aCol[0] always exists, its value may be = 0 if all columns have been deleted If $sHeader_TextNew = StringStripWS(_GUICtrlHeader_GetItemText($g_hHeader, $i - 1), 1+2) Then ; -1 as 0-based item index . 1+2 = strip leading & trailing white space Return False ; False => duplicates found EndIf Next Return True ; ok, no duplicates EndFunc ;==>_IsHeaderNameUnique ;============================================ Func _HeaderArrow_Update($iHeader_Index) ; "Natural sort" calls this function, to remove a possible sort arrow from any header... ; ...then insert an up (or down) arrow into the header of the column just being "naturally" sorted Local $iFormat For $i = 0 To _GUICtrlHeader_GetItemCount($g_hHeader) - 1 $iFormat = _GUICtrlHeader_GetItemFormat($g_hHeader, $i) If BitAND($iFormat, $HDF_SORTDOWN) Then _GUICtrlHeader_SetItemFormat($g_hHeader, $i, BitXOR($iFormat, $HDF_SORTDOWN)) ElseIf BitAND($iFormat, $HDF_SORTUP) Then _GUICtrlHeader_SetItemFormat($g_hHeader, $i, BitXOR($iFormat, $HDF_SORTUP)) EndIf Next $iFormat = _GUICtrlHeader_GetItemFormat($g_hHeader, $iHeader_Index) _GUICtrlHeader_SetItemFormat($g_hHeader, $iHeader_Index, _ BitOR($iFormat, (($g_iSortDirection = 0) ? $HDF_SORTUP : $HDF_SORTDOWN))) ; 0 = ascending, 1 = descending EndFunc ;==>_HeaderArrow_Update ;============================================ Func _ExtractFileName($FullPathAndFileName) Local $iPos_AntiSlash = StringInStr($FullPathAndFileName, "\", 0, -1) ; search last "\" starting from the right If $iPos_AntiSlash = 0 Then MsgBox(BitOr($MB_TOPMOST, $MB_ICONERROR), "End of script", _ "No antislash (\) found in " & $FullPathAndFileName & " ", 0, $g_hGUI) ; impossible Exit EndIf ; Local $sPath = StringLeft($FullPathAndFileName, $iPos_AntiSlash) ; ex: "C:\" or "C:\Temp\" etc... always ends with "\" Return StringMid($FullPathAndFileName, $iPos_AntiSlash + 1) ; filename & extension, without path, for example: test.csv EndFunc ;==>_ExtractFileName ;============================================ Func _SplashOn($sFirstLine, $sSecondLine = "please wait...") SplashTextOn("", $sFirstLine & @CRLF & $sSecondLine, _ 250, 50, -1, -1, $DLG_NOTITLE + $DLG_TEXTVCENTER) EndFunc ;==>_SplashOn ;============================================ Func _RGB2BGR($iColor) Return BitAND(BitShift(String(Binary($iColor)), 8), 0xFFFFFF) EndFunc ;==>_RGB2BGR ;============================================ Func _BGR2RGB($iColor) Return BitOR(BitShift(BitAND($iColor, 0x0000FF), -16), BitAND($iColor, 0x00FF00), BitShift(BitAND($iColor, 0xFF0000), 16)) EndFunc ;==>_BGR2RGB ;============================================ Func Ins_Item() HotKeySet("{INS}") If _WinAPI_GetFocus() = $g_hListView Then If $g_aCol[0] = 0 Then ; $g_aCol[0] always exists, its value may be = 0 if all columns have been deleted $g_iItem = -1 $g_iSubItem = -1 $g_iRowCount = -1 HotKeySet("{INS}", "Ins_Item") Return EndIf Switch $g_iRowCount ; at this stage, at least one column exists Case -1 ; LV is empty (no rows) $g_iItem = 0 $g_iSubItem = $g_aCol[1] ; yes, define it here _GUICtrlListView_AddItem($g_idListView, "") _GUICtrlListView_AddSubItem($g_idListView, $g_iItem, "New row", $g_aCol[1]) _GUICtrlListView_SetItemFocused($g_idListView, $g_iItem) Case Else ; non-empty LV (at least one row exists) If $g_iItem = -1 Then ; ex. after click in empty place of LV => do nothing HotKeySet("{INS}", "Ins_Item") Return EndIf Switch $g_iItem Case $g_iRowCount ; last item selected => add after (arbitrary) $g_iItem += 1 _GUICtrlListView_AddItem($g_idListView, "") _GUICtrlListView_AddSubItem($g_idListView, $g_iItem, "New row", $g_aCol[1]) Case Else ; not last item is selected => insert at its place _GUICtrlListView_InsertItem($g_idListView, "", $g_iItem) _GUICtrlListView_AddSubItem($g_idListView, $g_iItem, "New row", $g_aCol[1]) EndSwitch _GUICtrlListView_SetItemFocused($g_idListView, $g_iItem) EndSwitch $g_iRowCount +=1 GUICtrlSetData($g_idStatusBar, ($g_iRowCount + 1) & " rows") $g_bChangesMade = True _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) Else ; other parts of Gui OR other applications Send("{INS}") EndIf HotKeySet("{INS}", "Ins_Item") EndFunc ;==>Ins_Item ;============================================ Func Del_Item() HotKeySet("{DEL}") If _WinAPI_GetFocus() = $g_hListView Then If $g_aCol[0] = 0 Then ; $g_aCol[0] always exists, its value may be = 0 if all columns have been deleted $g_iItem = -1 $g_iSubItem = -1 $g_iRowCount = -1 HotKeySet("{DEL}", "Del_Item") Return EndIf Switch $g_iRowCount ; at this stage, at least one column exists Case -1 ; LV is empty (no rows) => do nothing HotKeySet("{DEL}", "Del_Item") Return Case Else ; non-empty LV (at least one row exists) If $g_iItem = -1 Then ; ex. after click in empty place of LV => do nothing HotKeySet("{DEL}", "Del_Item") Return EndIf _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) _GUICtrlListView_SetItemDropHilited($g_idListView, $g_iItem, True) ; tweak to "highlight" the whole row ! ; special part for $g_idMarker4Del (which is not really necessary since the "highlight" tweak has been applied on the item) Local $aRect = _GUICtrlListView_GetItemRect($g_idListView, $g_iItem, 0) ; 0 = bounding rectangle of the entire item Local $aLVPos = WinGetPos($g_hListView) ; important as the user may have resized the GUI => listview coords are no more constants ScreenToClient($g_hGUI, $aLVPos) ; $aLVPos ByRef will be changed (+++) . Converts listview coords to Gui coords GUICtrlSetPos($g_idMarker4Del, 1, $aLVPos[1] + $aRect[1] + 2) ; hidden red marker (label) is wandering... GUICtrlSetState($g_idMarker4Del, $GUI_SHOW) ; ...red marker appears at the left of the row (totally visible if $iLeftLV >= 15) HotKeySet("{INS}") ; prevents firing HotKey "Insert" during MsgBox (in case user press it by mistake) WinSetOnTop($g_hGUI, "", 1) ; 1 = on top, 0 = not on top (on top is good before MsgBox delete confirmation) Local $iConfirm_Del = MsgBox(BitOr($MB_TOPMOST, $MB_ICONWARNING, $MB_YESNO), _ "Warning", "Delete this row #" & ($g_iItem + 1) & " ?", 0, $g_hGUI) WinSetOnTop($g_hGUI, "", 0) ; 1 = on top, 0 = not on top HotKeySet("{INS}", "Ins_Item") GUICtrlSetState($g_idMarker4Del, $GUI_HIDE) ; hide red marker... until next deletion is requested by the user If $iConfirm_Del = $IDNO Then _GUICtrlListView_SetItemDropHilited($g_idListView, $g_iItem, False) ; "un-highlight" the whole row ; _GUICtrlListView_RedrawItems($g_hListView, $g_iItem, $g_iItem) ; was useful sometimes (b4 DropHilited hack was added) HotKeySet("{DEL}", "Del_Item") Return EndIf Switch $g_iItem Case $g_iRowCount ; last item is selected => delete it _GUICtrlListView_DeleteItem($g_idListView, $g_iItem) $g_iItem -= 1 ; maybe became -1 if LV just emptied If $g_iItem > -1 Then _GUICtrlListView_SetItemSelected($g_hListView, $g_iItem, False) EndIf Case Else ; not last item is selected => delete it _GUICtrlListView_DeleteItem($g_idListView, $g_iItem) EndSwitch _GUICtrlListView_RedrawItems($g_hListView, $g_iItem, $g_iItem) ; useful sometimes : keep that line +++ EndSwitch $g_iRowCount -=1 ; maybe -1 if LV just emptied (no rows) GUICtrlSetData($g_idStatusBar, (($g_iRowCount + 1 > 0) ? ($g_iRowCount + 1 & " rows") : "")) $g_bChangesMade = True Else ; other parts of Gui OR other applications Send("{DEL}") EndIf HotKeySet("{DEL}", "Del_Item") EndFunc ;==>Del_Item ;============================================ Func Import_csv(ByRef $bDropped, ByRef $idDroppable_Zone, ByRef $idDroppable_Label, ByRef $idTxt_Import, _ ByRef $idDelimiter, ByRef $idHeaders1stRow, ByRef $idSelectionColor, ByRef $idTxt_Export, ByRef $idTxt_Close, _ ByRef $iLeftLV, ByRef $iTopLV, ByRef $iWidthLV, ByRef $iHeightLV) If $bDropped Then ; user dragged & dropped a file If StringRight(@GUI_DragFile, 4) <> ".csv" Then MsgBox($MB_TOPMOST, "Bad file dropped : extension is not .csv", @GUI_DragFile, 0, $g_hGUI) Return 0 ; wait & see Else $sFileName_Import = @GUI_DragFile EndIf Else ; Import button Local $sFileName_Import = FileOpenDialog( _ "Import CSV file . Delimiter is >>> " & $g_sDelimiter & " <<<", _ @ScriptDir, _ "CSV files (*.csv)", _ BitOR($FD_FILEMUSTEXIST, $FD_PATHMUSTEXIST), _ "", _ $g_hGUI) If @error Then ;~ MsgBox($MB_TOPMOST, "Import status", _ ;~ "No file selected ", 0, $g_hGUI) ; FileOpenDialog() was exited by Cancel/Red X button, or by Esc key Return 0 ; wait & see EndIf ; Change the working directory (@WorkingDir) back to the location of the script directory as FileOpenDialog sets it to the last accessed folder (on successful return) ; FileChangeDir(@ScriptDir) EndIf GUICtrlDelete($idDroppable_Zone) ; listview will be created at exactly same coords than the deleted drop zone GUICtrlDelete($idDroppable_Label) $g_sFileNameNoPath_Import = _ExtractFileName($sFileName_Import) ; suggest it for Export file name (with suffix based on date/time) WinSetTitle($g_hGUI, "", $g_sFileNameNoPath_Import) ; === 1/3 === _SplashOn("1/3 : CSV Read") Local $sRead = FileRead($sFileName_Import) SplashOff() ;========= Local $aEOL = StringRegExp($sRead, "\R", 1) ; 1 is $STR_REGEXPARRAYMATCH . Search 1st EOL (@CRLF or @CR or @LF) to reuse during export Switch @error ; see help file Case 0 $g_sEOL = $aEOL[0] ; @CRLF or @CR or @LF $g_bCheck_EOF = ((StringRight($sRead, StringLen($g_sEOL))) <> $g_sEOL ? True : False) ; True means no EOL at the end of input file (though input file contains EOL) Case 1 $g_sEOL = "" ; a bit strange : no EOL character found in input file (certainly a single line with no EOL at the end) $g_bCheck_EOF = False Case 2 Msgbox($MB_TOPMOST, "Import error", _ "Bad pattern in RegExp", 0, $g_hGUI) ; impossible as "\R" is a good pattern Return -1 EndSwitch ; === 2/3 === _SplashOn("2/3 : CSV Split") Local $aRead = _CSVSplit($sRead, $g_sDelimiter) ; Czardas UDF . Added ByRef in both Func's _CSVSplit() and __GetSubstitute() ; Changed Local $asDelim[3] to Global $g_asDelim[3] (reasons explained below) ; "Array returned maybe 2D or 1D" : NO, not any more since this version... ; To make it simpler, array returned will ALWAYS be 2D, even if 1 column only, i.e. [][1] Local $iKeep_error = @error $sRead = "" ; free memory SplashOff() ;========= If $iKeep_error Then Msgbox($MB_TOPMOST, "Import error", _ "@error = " & $iKeep_error & " during _CSVSplit() ", 0, $g_hGUI) Return -2 EndIf If Not IsArray($aRead) Then Msgbox($MB_TOPMOST, "Import error", _ "$aRead is not an Array ", 0, $g_hGUI) Return -3 EndIf ; === 3/3 === _SplashOn("3/3 : Populating ListView") GUICtrlSetState($idTxt_Import, $GUI_DISABLE) GUICtrlSetState($idDelimiter, $GUI_DISABLE) GUICtrlSetState($idHeaders1stRow, $GUI_DISABLE) GUICtrlSetState($idSelectionColor, $GUI_DISABLE) Local $iDimensions = UBound($aRead, 0) If $iDimensions <> 2 Then ; impossible, now that Czardas function has been reworked : 1 column will create a 2D array [][1] to simplify things +++ Msgbox($MB_TOPMOST, "Import error", _ "$iDimensions = " & $iDimensions, 0, $g_hGUI) Return -4 EndIf Local $iRows = UBound($aRead, 1) Local $iCols = UBound($aRead, 2) Local $sGUISeparator = $g_asDelim[0] ; or [1] or [2] : re-use any of Czardas 3 unique characters from __GetSubstitute() as $sGUISeparator, ; in case pipe "|" (when not chosen as delimiter) is found in the input file, avoiding listview populating errors. ; Starting with version 901w, eventual pipe(s) "|" in file would have affected headers only because Items won't be imported ; with GUICtrlCreateListViewItem() anymore, but from an array, i.e 3 times faster code based on _GUICtrlListView_AddArray() ; Also, 2nd advantage to import Items from an array is the use of $LVM_DELETEALLITEMS if user wants to import others CSV during the same session. Opt("GUIDataSeparatorChar", $sGUISeparator) If $g_bHeaders1stRow = False Then ; 1st row (0) contains data => create headers named "Col 0" & $sGUISeparator & "Col 1" & $sGUISeparator etc... Local $iFirstItem = 0 Local $sDataHeader = "Col 0" & $sGUISeparator For $j = 1 To $iCols - 1 $sDataHeader &= "Col " & $j & $sGUISeparator Next Else ; 1st row (0) contains headers Local $iFirstItem = 1 Local $sDataHeader = "" For $j = 0 To $iCols - 1 If $aRead[0][$j] = "" And $j = $iCols - 1 Then $aRead[0][$j] = " " ; this will force creation of last header/column $sDataHeader &= $aRead[0][$j] & $sGUISeparator Next EndIf $g_idListView = GUICtrlCreateListView(StringTrimRight($sDataHeader, 1), $iLeftLV, $iTopLV, $iWidthLV, $iHeightLV, _ BitOR($GUI_SS_DEFAULT_LISTVIEW, $LVS_NOSORTHEADER)) ; 4 coords in 4 variables (+++) first used for drag & drop zone, now for listview ; $LVS_NOSORTHEADER => disable left click on header (we'll sort using right click on header) $g_hListView = GuiCtrlGetHandle($g_idListView) _GUICtrlListView_SetExtendedListViewStyle($g_idListView, _ BitOR($LVS_EX_HEADERDRAGDROP, $LVS_EX_FULLROWSELECT, $WS_EX_CLIENTEDGE, $LVS_EX_GRIDLINES)) ; $LVS_EX_GRIDLINES doesn't always show, i.e if windows theme active window background color is same than message box background color _GUICtrlListView_SetCallBackMask($g_idListView, 4) ; "disable state information about focused item" (LarsJ) . Can be useful (keep that line) $g_aCol = _GUICtrlListView_GetColumnOrderArray($g_idListView) ; +++ ; GUICtrlSetFont($g_idListView, 13) ; for test on my computer ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Local $nBegin = TimerInit() If Not $g_bBufferExist Then _BufferCreate() ; Populate an empty listview from an array. ; What follows is based on _GUICtrlListView_AddArray() , a function which is already very fast (because it uses loops in array)... ; ...and is 3 times faster than all GUICtrlCreateListViewItem() created from an existing array to populate a listview control : tested. _GUICtrlListView_BeginUpdate($g_hListView) ; improve speed while populating listview (+++) DllStructSetData($g_tItem, "Mask", $LVIF_TEXT) ; mandatory (or listview will stay empty) . Line is found in _GUICtrlListView_AddArray() ; Local $iLastItem = _GUICtrlListView_GetItemCount($g_idListView) ; no need in this case as $iLastItem = 0 (listview was empty) ; For $i = 0 To $iRows - 1 ; original line can't be used because of $iFirstItem For $i = 0 To $iRows - (($iFirstItem = 0) ? 1 : 2) ; mandatory loop starts at 0 (For $i = 0... even if $iFirstItem = 1), certainly Windows requires it (only 1st columns are populated if For $i = 1... , tested) ; DllStructSetData($g_tItem, "Item", $i + $iLastItem) ; no need in this case as $iLastItem = 0 (listview was empty) DllStructSetData($g_tItem, "Item", $i) DllStructSetData($g_tItem, "SubItem", 0) DllStructSetData($g_tBuffer, "Text", $aRead[$iFirstItem + $i][0]) ; basically, this is why Czardas function has been reworked to ALWAYS return a 2D array ( if only 1 column => [][1] ) GUICtrlSendMsg($g_idListView, $g_InsertItem, 0, $g_pItem) For $j = 1 To $iCols - 1 ; this loop won't perform if 2D array got only 1 column, i.e [][1] , that's ok DllStructSetData($g_tItem, "SubItem", $j) DllStructSetData($g_tBuffer, "Text", $aRead[$iFirstItem + $i][$j]) GUICtrlSendMsg($g_idListView, $g_SetItem, 0, $g_pItem) Next Next DllStructSetData($g_tItem, "Mask", 0) ; empty "Mask" before continuing (it was 0, tested) so $g_tItem will be clean when reused ; Resize Columns (not mandatory but better design) . This loop before _GUICtrlListView_EndUpdate() for a smoother listview display Local $iHeader_Width, $iData_Width For $j = 0 To $iCols - 1 ; Size column to fit header (note that "header length" is not simply based on "header text length", tested) _GUICtrlListView_SetColumnWidth($g_idListView, $j, $LVSCW_AUTOSIZE_USEHEADER) ; not really good on last column ("fills all remaining width of listview control") $iHeader_Width = _GUICtrlListView_GetColumnWidth($g_idListView, $j) ; Now size column to fit data _GUICtrlListView_SetColumnWidth($g_idListView, $j, $LVSCW_AUTOSIZE) $iData_Width = _GUICtrlListView_GetColumnWidth($g_idListView, $j) ; 2 tests on column size If $iHeader_Width > 100 Or $iData_Width > 100 Then ; reset width to 100 pixels (100 = arbitrary) _GUICtrlListView_SetColumnWidth($g_idListView, $j, 100) ElseIf $iHeader_Width > $iData_Width Then ; header is wider, reset width to header width _GUICtrlListView_SetColumnWidth($g_idListView, $j, $iHeader_Width) EndIf Next _GUICtrlListView_EndUpdate($g_hListView) ; Local $nEnd = TimerDiff($nBegin) ; ConsoleWrite("Import phase 3/3 : " & Int($nEnd)/1000 & " s." & @CRLF) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Apart from the "Resize Columns" loop, following code would have worked too (a bit slower) requiring _ArrayDelete in case $iFirstItem = 1 ; If $iFirstItem = 1 Then _ArrayDelete($aRead, 0) ; _GUICtrlListView_AddArray($g_idListview, $aRead) ; 3 times faster than many GUICtrlCreateListViewItem() to populate the listview control. ; Note that _GUICtrlListView_AddArray() works ONLY on 2D arrays... ; ...which explains why Czardas function has been reworked to ALWAYS return a 2D array ( [][1] in case of 1 column only ) $aRead = 0 ; free memory $g_iRowCount = _GUICtrlListView_GetItemCount($g_idListView) -1 ; personal base 0 (=> -1 if no rows, 0 if 1 row etc...) ; Calculate item height (item height depends on each computer, windows theme fonts, scaling...) $g_hHeader = _GUICtrlListView_GetHeader($g_idListView) Local $iHeaderHeight = _WinAPI_GetWindowHeight($g_hHeader) Local $iNbItemsPerPage = _GUICtrlListView_GetCounterPage($g_idListView) $g_iItemHeight = ($iHeightLV - $iHeaderHeight) / $iNbItemsPerPage ;~ ConsoleWrite("$iHeaderHeight = " & $iHeaderHeight & @lf & _ ;~ "$iNbItemsPerPage = " & $iNbItemsPerPage & @lf & _ ;~ "$g_iItemHeight = " & $g_iItemHeight & @lf) $g_idMarker4Del = GUICtrlCreateLabel(">>", 0, 0, 12, $g_iItemHeight - (($g_iItemHeight < 18) ? 1 : 2), _ BitOr($SS_CENTERIMAGE, $SS_CENTER)) ; label used as a red wandering marker in case user wants to delete a row (Del key) GUICtrlSetResizing(-1, $GUI_DOCKSIZE) ; label size will not change (i.e if GUI is resized, LV coords change, but items height stay same) GUICtrlSetColor (-1, 0xFFFFFF) ; white GUICtrlSetBkColor(-1, 0xFF0000) ; red GUICtrlSetState($g_idMarker4Del, $GUI_HIDE) GUICtrlSetState($idSelectionColor, $GUI_ENABLE) GUICtrlSetState($idTxt_Export, $GUI_ENABLE) GUICtrlSetState($idTxt_Close, $GUI_ENABLE) GUICtrlSetData($g_idStatusBar, $iRows - (($g_bHeaders1stRow = False) ? 0 : 1) & " rows") SplashOff() ;========= Return 1 ; import successful EndFunc ; ==>Import_csv ;============================================ Func Export_csv() ; === 1/2 === _SplashOn("1/2 : Export prepare") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Local $nBegin = TimerInit() If Not $g_bBufferExist Then _BufferCreate() Local $iRows = _GUICtrlListView_GetItemCount($g_idListView) Local $iCols = _GUICtrlListView_GetColumnCount($g_idListView) Local $sWrite = "", $sText = "", $sQuote = chr(34), $sDoubleQuote = chr(34) & chr(34), $sPattern = "\R|\" & $g_sDelimiter If $g_bHeaders1stRow = True Then ; 1st row (0) contained headers in the imported CSV file For $j = 0 To $iCols -1 $sText = StringStripWS(_GUICtrlHeader_GetItemText($g_hHeader, $g_aCol[$j +1]), 1+2) ; 1+2 = strip leading & trailing white space If StringRegExp($sText, $sQuote) Then $sText = $sQuote & StringReplace($sText, $sQuote, $sDoubleQuote) & $sQuote ; 1st If +++ If StringRegExp($sText, $sPattern) And StringLeft($sText, 1) <> $sQuote Then $sText = $sQuote & $sText & $sQuote ; 2nd If +++ $sWrite &= $sText & (($j < $iCols - 1) ? $g_sDelimiter : $g_sEOL) Next EndIf For $i = 0 To $iRows -1 For $j = 0 To $iCols -1 DllStructSetData($g_tItem, "SubItem", $g_aCol[$j +1]) GUICtrlSendMsg($g_idListView, $g_GetItemText, $i, $g_pItem) $sText = DllStructGetData($g_tBuffer, "Text") If StringRegExp($sText, $sQuote) Then $sText = $sQuote & StringReplace($sText, $sQuote, $sDoubleQuote) & $sQuote ; 1st If +++ If StringRegExp($sText, $sPattern) And StringLeft($sText, 1) <> $sQuote Then $sText = $sQuote & $sText & $sQuote ; 2nd If +++ $sWrite &= $sText & (($j < $iCols - 1) ? $g_sDelimiter : $g_sEOL) ; $g_sEOL added after each line (even the last one, but look at $g_bCheck_EOF test outside both loops) Next Next ; Test $g_bCheck_EOF : True means input file had EOL within the file, but NOT at the end of file => do same for export file if needed : If $g_bCheck_EOF And StringRight($sWrite, StringLen($g_sEOL)) = $g_sEOL Then $sWrite = StringTrimRight($sWrite, StringLen($g_sEOL)) ; Local $nEnd = TimerDiff($nBegin) ; ConsoleWrite("Export phase 1/2 : " & Int($nEnd)/1000 & " s." & @CRLF) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; SplashOff() ;========= Local $sFileNameProposed_Export = StringTrimRight($g_sFileNameNoPath_Import, 4) & "_" & _ @YEAR & "-" & @MON & "-" & @MDAY & " " & @HOUR & "." & @MIN & "." & @SEC ; first trim ".csv" from right HotKeySet("{INS}") ; "file open and save dialogs get special protection from automated inputs" (read it on the forum) => deactivate hotkey to use it in the dialog HotKeySet("{DEL}") ; same for "Del" key... and that's even more important for "Del" (as "Ins" doesn't seem usable in all windows dialogs anyway) Local $sFileName_Export = FileSaveDialog( _ "Export CSV file", _ @ScriptDir, _ "CSV files (*.csv)", _ BitOR($FD_PROMPTOVERWRITE, $FD_PATHMUSTEXIST), _ $sFileNameProposed_Export, _ $g_hGUI) Local $iKeep_error = @error HotKeySet("{DEL}", "Del_Item") HotKeySet("{INS}", "Ins_Item") If $iKeep_error Then MsgBox($MB_TOPMOST, "Export status", _ "No file was saved ", 0, $g_hGUI) ; FileSaveDialog() was exited by Cancel/Red X button, or by Esc key Return 0 ; wait & see EndIf ; Change the working directory (@WorkingDir) back to the location of the script directory as FileSaveDialog sets it to the last accessed folder (on successful return) ; FileChangeDir(@ScriptDir) Local $hFileOpen = FileOpen($sFileName_Export, $FO_OVERWRITE) If $hFileOpen = -1 Then MsgBox($MB_TOPMOST, "Export error", _ "FileOpen error " & $sFileName_Export & " ", 0, $g_hGUI) Return -1 EndIf WinSetTitle($g_hGUI, "", _ExtractFileName($sFileName_Export)) ; === 2/2 === _SplashOn("2/2 : Export write") FileWrite($hFileOpen, $sWrite) FileClose($hFileOpen) $sWrite = "" ; free memory SplashOff() ;========= MsgBox($MB_TOPMOST, "Done", _ "File saved ", 0, $g_hGUI) $g_bChangesMade = False ; reset to False after a successful export operation Return 1 ; export successful EndFunc ;==>Export_csv ;============================================ Func Header_menu() ; called by right click on header control HotKeySet("{INS}") ; prevents firing HotKey "Insert" during Header_menu() in case user press it by mistake HotKeySet("{DEL}") ; same for "Del" key... especially Del could be needed in InputBox during Header_menu() Local $aPos = MouseGetPos(), $iMenu_Choice = -1, $iCol = -1 ScreenToClient($g_hHeader, $aPos) ; $aPos ByRef will be changed (+++) . Converts mouse coords to header control coords Local $aHeader_Hit = _GUICtrlHeader_HitTest($g_hHeader, $aPos[0], $aPos[1]) If Not ($aHeader_Hit[0] = -1 Or $aHeader_Hit[3] Or $aHeader_Hit[4]) Then ; [0] = 0-based header item index, [3] = position on divider between two items, [4] = position on divider of an item having a zero width Local $sHeader_Text = StringStripWS(_GUICtrlHeader_GetItemText($g_hHeader, $aHeader_Hit[0]), 1+2) ; 1+2 = strip leading & trailing white space. There maybe plenty, ex. " Col 0 " => "Col 0" Local $hMenu = _GUICtrlMenu_CreatePopup() Local $hMenu2 = _GUICtrlMenu_CreatePopup() _GUICtrlMenu_InsertMenuItem($hMenu, 10, ">>> " & StringLeft($sHeader_Text, 20) & " <<<", 10) ; 4th numeric param (here = 10) is the menu item identifier _GUICtrlMenu_InsertMenuItem($hMenu, 20, "Export now ?", 20) _GUICtrlMenu_InsertMenuItem($hMenu, 25, "", 0) _GUICtrlMenu_AppendMenu($hMenu, $MF_POPUP, $hMenu2, "Sort") _GUICtrlMenu_InsertMenuItem($hMenu2, 100, "Numeric (ascending)", 100) _GUICtrlMenu_InsertMenuItem($hMenu2, 110, "Numeric (descending)", 110) _GUICtrlMenu_InsertMenuItem($hMenu2, 150, "", 0) _GUICtrlMenu_InsertMenuItem($hMenu2, 200, "String (ascending)", 200) _GUICtrlMenu_InsertMenuItem($hMenu2, 210, "String (descending)", 210) _GUICtrlMenu_InsertMenuItem($hMenu2, 250, "", 0) _GUICtrlMenu_InsertMenuItem($hMenu2, 300, "Natural (ascending)", 300) _GUICtrlMenu_InsertMenuItem($hMenu2, 310, "Natural (descending)", 310) _GUICtrlMenu_InsertMenuItem($hMenu, 25, "", 0) _GUICtrlMenu_InsertMenuItem($hMenu, 30, "Rename Header", 30) _GUICtrlMenu_InsertMenuItem($hMenu, 35, "", 0) $iCol = _ArraySearch($g_aCol, $aHeader_Hit[0], 1) ; index (in possible scrambled array) of right-clicked column header : 1 to $g_aCol[0] If $iCol > 1 Then _GUICtrlMenu_InsertMenuItem($hMenu, 40, "Insert Column (before)", 40) ; never insert a column before 1st column ! _GUICtrlMenu_InsertMenuItem($hMenu, 50, "Insert Column (after)", 50) _GUICtrlMenu_InsertMenuItem($hMenu, 60, "Delete Column", 60) $iMenu_Choice = _GUICtrlMenu_TrackPopupMenu($hMenu, $g_hHeader, -1, -1, 1, 1, 2) ; 2 = Return the menu item identifier of the user's selection _GUICtrlMenu_DestroyMenu($hMenu2) _GUICtrlMenu_DestroyMenu($hMenu) EndIf Switch $iMenu_Choice Case -1, 0, 10 ; -1 = assigned by design (Local $iMenu_Choice = -1) but right-click in header at a wrong place (including no headers anymore) => no menu appeared. ; 0 = menu appeared, but exited by Esc or click anywhere outside the menu (help file _GUICtrlMenu_TrackPopupMenu) ; 10 = menu appeared, "informative" menu item identifier 10 chosen (it's just an item showing the column header name to the user) Case 20 Export_csv() ; no need to test Return value here Case 30 ; Rename Header $g_bChangesMade = True While 1 Local $sHeader_TextNew = InputBox($sHeader_Text, "Enter NEW header name", $sHeader_Text, " M", _ 220, 140, Default, Default, 0, $g_hGui) If @error Then ExitLoop ; keep in mind that @error 1 <=> user canceled the inputbox (Cancel button or Red X or Esc) $sHeader_TextNew = StringStripWS($sHeader_TextNew, 1+2) ; 1+2 = strip leading & trailing white space If $sHeader_TextNew = "" Then ContinueLoop ElseIf $sHeader_TextNew = $sHeader_Text Then ExitLoop EndIf If _IsHeaderNameUnique($sHeader_TextNew) = True Then ; ok, no duplicates _GUICtrlHeader_SetItemText($g_hHeader, $aHeader_Hit[0], $sHeader_TextNew) ExitLoop Else ; False => refuse this header name as it would duplicate an existing one Msgbox($MB_TOPMOST, $sHeader_TextNew, _ "This header name already exists. Please choose another one ", 0, $g_hGUI) EndIf WEnd Case 40, 50 ; 40 = Insert Column (before), 50 = Insert Column (after) $g_bChangesMade = True Local $iCol_Insert = (($iMenu_Choice = 40) ? ($iCol - 1) : $iCol) ; item menu 40 doesn't even exist when $iCol = 1 (never insert a column before 1st column !) While 1 Local $sHeader_TextNew = InputBox("Insert column #" & $iCol_Insert, "Enter header name", "New col #" & $iCol_Insert, " M", _ 220, 140, Default, Default, 0, $g_hGui) If @error Then ExitLoop ; keep in mind that @error 1 <=> user canceled the inputbox (Cancel button or Red X or Esc) $sHeader_TextNew = StringStripWS($sHeader_TextNew, 1+2) ; 1+2 = strip leading & trailing white space If $sHeader_TextNew = "" Then ContinueLoop If _IsHeaderNameUnique($sHeader_TextNew) = True Then ; ok, no duplicates _GUICtrlListView_InsertColumn($g_idListview, $iCol_Insert, $sHeader_TextNew, 100) ; 100 pixels = arbitrary $g_aCol = _GUICtrlListView_GetColumnOrderArray($g_idListView) ; If $g_iItem > -1 Then ; commenting this line allows to scroll horizontally on inserted column even if no cell is selected (interesting if inserted column after last column +++) $g_iSubItem = $g_aCol[_ArraySearch($g_aCol, $iCol_Insert, 1)] ; $g_iSubItem could now be > -1 when $g_iItem = -1 (see "peace of mind" below) _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) Horizontal_Scroll(0) _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem) If $g_iItem = -1 Then $g_iSubItem = -1 ; "peace of mind", even if $g_iItem value always prevails anywhere in the script. ; EndIf ExitLoop Else ; False => refuse this header name as it would duplicate an existing one Msgbox($MB_TOPMOST, $sHeader_TextNew, _ "This header name already exists. Please choose another one ", 0, $g_hGUI) EndIf WEnd Case 60 ; Delete Column $g_bChangesMade = True WinSetOnTop($g_hGUI, "", 1) ; 1 = on top, 0 = not on top (on top is good before MsgBox delete confirmation) If MsgBox(BitOr($MB_TOPMOST, $MB_ICONWARNING, $MB_YESNO, $MB_DEFBUTTON2), _ "Warning", "Delete Column '" & $sHeader_Text & "' ?", _ 0, $g_hGUI) = $IDYES Then If $g_iItem > -1 Then Local $iCol_SubItem = _ArraySearch($g_aCol, $g_iSubItem, 1) ; index (in possible scrambled array) of column containing selected cell : 1 to $g_aCol[0] EndIf If $g_aCol[$iCol] = 0 Then _SplashOn("Delete in progress...", $sHeader_Text) ; takes a little more time on Col 0 (wherever it has been dragged) EndIf _GUICtrlListView_DeleteColumn($g_idListView, $aHeader_Hit[0]) $g_aCol = _GUICtrlListView_GetColumnOrderArray($g_idListView) SplashOff() If $g_iItem > -1 Then Select Case $iCol_SubItem < $iCol ; column of selected cell < column deleted $g_iSubItem = $g_aCol[_ArraySearch($g_aCol, $g_aCol[$iCol_SubItem], 1)] Case $iCol_SubItem = $iCol ; column of selected cell = column deleted If $iCol <= $g_aCol[0] Then ; it's not the last column that was deleted $g_iSubItem = $g_aCol[$iCol] Else ; last column was deleted $g_iSubItem = $g_aCol[$g_aCol[0]] EndIf Case Else ; column of selected cell > column deleted $g_iSubItem = $g_aCol[_ArraySearch($g_aCol, $g_aCol[$iCol_SubItem - 1], 1)] EndSelect _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) Horizontal_Scroll(0) _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem) EndIf EndIf WinSetOnTop($g_hGUI, "", 0) ; 1 = on top, 0 = not on top Case 100, 110, 200, 210 ; 100 = Numeric Sort (ascending), 110 = Numeric Sort (descending), 200 = String Sort (ascending), 210 = String Sort (descending) $g_bChangesMade = True $g_iSortDirection = (($iMenu_Choice = 100 Or $iMenu_Choice = 200) ? 0 : 1) ; 0 = Ascending, 1 = Descending . Used in _GUICtrlListView_RegisterSortCallBack2() Local $iCompareType = (($iMenu_Choice = 200 Or $iMenu_Choice = 210) ? 0 : 1) ; 0 = String Compare, 1 = Numeric compare (help file) If $g_iItem > - 1 Then ; keep track of the item containing a selected cell Local $iItemID = _GUICtrlListView_MapIndexToID($g_idListview, $g_iItem) ; note that Natural sort (Case 300, 310) could make think that $iItemID isn't accurate, but all will be ok in the end EndIf ; _GUICtrlListView_RegisterSortCallBack($g_hListView, 1, True, "__GUICtrlListView_Sort") ; slow _GUICtrlListView_RegisterSortCallBack2($g_hListView, $iCompareType, True, "__GUICtrlListView_Sort2") ; 4 times faster than preceding line ; 2nd param (see help file) 0 = String comparison 1 = String treated as number 2 = takes looong time (API : takes 10 times longer than 0 or 1 !) and it will be replaced by a personal "Natural Sort" (Case 300, 310) _SplashOn((($iMenu_Choice = 100 Or $iMenu_Choice = 110) ? "Numeric" : "String") & " sort in progress...", $sHeader_Text) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Local $nBegin = TimerInit() If Not $g_bBufferExist Then _BufferCreate() _GUICtrlListView_SortItems($g_hListView, $aHeader_Hit[0]) ; Microsoft: "LVM_SORTITEMSEX message [found in _GUICtrlListView_SortItems()] will use an application-defined comparison ; function to sort the items of the list-view control. The index of each item will change to reflect the new sequence." ; Local $nEnd = TimerDiff($nBegin) ; ConsoleWrite("Sort : " & Int($nEnd)/1000 & " s." & @CRLF) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; SplashOff() _GUICtrlListView_UnRegisterSortCallBack($g_hListView) If $g_iItem > - 1 Then ; one cell was selected before sort, undraw it now, then redraw the same selected cell on its new item Local $Save_iBGR = $g_iBGR $g_iBGR = $CLR_DEFAULT ; used in $NM_CUSTOMDRAW _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem) ; undraw the cell that was selected before sort (another way would be to deactivate $WM_NOTIFY just for this Redraw statement) $g_iBGR = $Save_iBGR $g_iItem = _GUICtrlListView_MapIDToIndex($g_idListview, $iItemID) ; at this stage, it seems impossible that $g_iSubItem = -1 (it should never happen... in theory) _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) Horizontal_Scroll(0) _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem) ; redraw the same selected cell on its new item after sort EndIf Case 300, 310 ; 300 = Natural Sort (ascending), 310 = Natural Sort (descending) : partially scripted (numbers must precede letters) $g_bChangesMade = True $g_iSortDirection = (($iMenu_Choice = 300) ? 0 : 1) ; for Melba23's ArrayMultiColSort UDF (0 = ascending, 1 = descending) _SplashOn("Natural sort in progress...", $sHeader_Text) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Local $nBegin = TimerInit() If Not $g_bBufferExist Then _BufferCreate() Local $iRows = _GUICtrlListView_GetItemCount($g_idListView) Local $iCols = _GUICtrlListView_GetColumnCount($g_idListView) ; Impossible here to use _GUICtrlListView_MapIndexToID() as we did for Numeric & String sorts (which sorted using Windows LVM_SORTITEMSEX message) ; This personal Natural sort uses Melba23's external array, then updates items/subitems text in LV (without Windows participating to the 'sort') so item indexes won't change. Local $aArray[$iRows][$iCols], $aArrayRegExp ; $aArray2 : 2 columns will always contain both parts of the splitted column to sort on. ; 3rd column will contain the item number (before sort). In case $g_iItem > - 1 (1 cell selected before Natural sort), then the corresponding item number will be stored negatively (for ex. 10 => -10 if cell was selected on item 10, any column) ; Creating $aArray2 (apart from $aArray) gives better speed than adding temporary columns at the right of $aArray (tested) Local $aArray2[$iRows][3] ; ===> 1 <=== Get all Items/Subitems text into $aArray => split 1 column & populate $aArray2 For $i = 0 To $iRows -1 For $j = 0 To $iCols -1 DllStructSetData($g_tItem, "SubItem", $j) GUICtrlSendMsg($g_idListView, $g_GetItemText, $i, $g_pItem) $aArray[$i][$j] = DllStructGetData($g_tBuffer, "Text") Next $aArrayRegExp = StringRegExp($aArray[$i][$g_aCol[$iCol]], '^\d+', $STR_REGEXPARRAYGLOBALMATCH) If Not @error Then ; $aArrayRegExp is valid $aArray2[$i][0] = Number($aArrayRegExp[0]) $aArray2[$i][1] = StringMid($aArray[$i][$g_aCol[$iCol]], StringLen($aArrayRegExp[0]) + 1) Else $aArray2[$i][0] = "A" $aArray2[$i][1] = $aArray[$i][$g_aCol[$iCol]] EndIf $aArray2[$i][2] = (($g_iItem <> $i) ? $i : - $i) ; when $g_iItem = -1 (non selected cell before sort) => always $i Next ; ===> 2 <=== Sort on both columns of $aArray2, ascending (0) or descending (1) Local $aSortData[][] = [ _ [0, $g_iSortDirection], _ [1, $g_iSortDirection]] _ArrayMultiColSort($aArray2, $aSortData) ; Melba23's UDF ; ===> 3 <=== Set all $aArray text back in ListView Items/Subitems, depending on sorted $aArray2 GUIRegisterMsg($WM_NOTIFY, "") ; for good speed (before ListView update) AND will not redraw any eventual "wrongly" selected cell after Natural sort. ; The correct selected cell, if any, will be redrawed after reactivating $WM_NOTIFY, below Local $i2, $iSelectedItem = -1 For $i = 0 To $iRows -1 $i2 = $aArray2[$i][2] If $i2 < 0 Then ; could happen only once, when a cell was selected before sort (i.e $g_iItem > - 1) $i2 = - $i2 $iSelectedItem = $i EndIf For $j = 0 To $iCols -1 DllStructSetData($g_tItem, "SubItem", $j) DllStructSetData($g_tBuffer, "Text", $aArray[$i2][$j]) GUICtrlSendMsg($g_idListView, $g_SetItemText, $i, $g_pItem) Next Next $aArray = 0 ; free memory $aArray2 = 0 ; free memory _HeaderArrow_Update($aHeader_Hit[0]) ; $aHeader_Hit[0] = 0-based header item index, same as $g_aCol[$iCol] = original column GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY") If $iSelectedItem > -1 Then ; one cell was selected before Natural sort, redraw it correctly now, after $WM_NOTIFY has been reactivated $g_iItem = $iSelectedItem ; at this stage, it seems impossible that $g_iSubItem = -1 (it should never happen... in theory) _GUICtrlListView_EnsureVisible($g_idListView, $g_iItem) Horizontal_Scroll(0) _GUICtrlListView_RedrawItems($g_hListview, $g_iItem, $g_iItem) _GUICtrlListView_SetItemSelected($g_hListView, $g_iItem, False, True) ; last param. Focused = True (important +++) EndIf ; Local $nEnd = TimerDiff($nBegin) ; ConsoleWrite("Natural Sort : " & Int($nEnd)/1000 & " s." & @CRLF) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; SplashOff() EndSwitch HotKeySet("{DEL}", "Del_Item") HotKeySet("{INS}", "Ins_Item") EndFunc ;==>Header_menu ; #FUNCTION# ==================================================================================================================== ; Name...........: _CSVSplit ; Description ...: Converts a string in CSV format to a two dimensional array (see comments) ; Syntax.........: CSVSplit ( $aArray [, $sDelim ] ) - typo : should be CSVSplit ( $string [, $sDelim ] ) ; Parameters ....: $aArray - The array to convert - typo : should be $string - The string to convert ; $sDelim - Optional - Delimiter set to comma by default (see 2nd comment) ; Return values .: Success - Returns a two dimensional array or a one dimensional array (see 1st comment) ; Failure - Sets @error to: ; |@error = 1 - First parameter is not a valid string ; |@error = 2 - Second parameter is not a valid string ; |@error = 3 - Could not find suitable delimiter replacements ; Author ........: czardas ; Comments ......; Returns a one dimensional array if the input string does not contain the delimiter string ; ; Some CSV formats use semicolon as a delimiter instead of a comma ; ; Set the second parameter to @TAB To convert to TSV ; pixelsearch ; Changed Local $asDelim[3] to Global $g_asDelim[3] at top of script (reasons explained below) ; ; Also, to make it simpler, array returned will ALWAYS be 2D, even if 1 column only, i.e. [][1] ; =============================================================================================================================== ; Func _CSVSplit($string, $sDelim = ",") ; Parses csv string input and returns a one or two dimensional array Func _CSVSplit(ByRef $string, $sDelim = ",") ; Parses csv string input and returns a one or two dimensional array ($string : added ByRef) If Not IsString($string) Or $string = "" Then Return SetError(1, 0, 0) ; Invalid string If Not IsString($sDelim) Or $sDelim = "" Then Return SetError(2, 0, 0) ; Invalid string $string = StringRegExpReplace($string, "[\r\n]+\z", "") ; [Line Added] Remove training breaks ; Local $iOverride = 63743, $asDelim[3] ; $asDelim => replacements for comma, new line and double quote [better say user's delimiter than comma] Local $iOverride = 63743 ; personal : Global $g_asDelim[3] at top of script, so we will be able to re-use any of the 3 "unique" elements [0] or [1] or [2] ; as Gui separator, in case pipe "|" (when not chosen as delimiter) is found in the input file, avoiding listview populating errors. For $i = 0 To 2 $g_asDelim[$i] = __GetSubstitute($string, $iOverride) ; Choose a suitable substitution character If @error Then Return SetError(3, 0, 0) ; String contains too many unsuitable characters Next $iOverride = 0 Local $aArray = StringRegExp($string, '\A[^"]+|("+[^"]+)|"+\z', 3) ; Split string using double quotes delim - largest match $string = "" Local $iBound = UBound($aArray) For $i = 0 To $iBound -1 $iOverride += StringInStr($aArray[$i], '"', 0, -1) ; Increment by the number of adjacent double quotes per element If Mod ($iOverride +2, 2) = 0 Then ; Acts as an on/off switch $aArray[$i] = StringReplace($aArray[$i], $sDelim, $g_asDelim[0]) ; Replace comma delimeters [better say user's delimiter than comma] $aArray[$i] = StringRegExpReplace($aArray[$i], "(\r\n)|[\r\n]", $g_asDelim[1]) ; Replace new line delimeters EndIf $aArray[$i] = StringReplace($aArray[$i], '""', $g_asDelim[2]) ; Replace double quote pairs $aArray[$i] = StringReplace($aArray[$i], '"', '') ; Delete enclosing double quotes - not paired $aArray[$i] = StringReplace($aArray[$i], $g_asDelim[2], '"') ; Reintroduce double quote pairs as single characters $string &= $aArray[$i] ; Rebuild the string, which includes two different delimiters Next $iOverride = 0 $aArray = StringSplit($string, $g_asDelim[1], 2) ; Split to get rows $iBound = UBound($aArray) ; Local $aCSV[$iBound][2], $aTemp Local $aCSV[$iBound][1], $aTemp ; pixelsearch : to make it simpler, array returned will ALWAYS be 2D, even if 1 column only, i.e. [][1] For $i = 0 To $iBound -1 $aTemp = StringSplit($aArray[$i], $g_asDelim[0]) ; Split to get row items If Not @error Then ; pixelsearch : @error 1 means delimiter was not found during StringSplit (see help file +++) If $aTemp[0] > $iOverride Then $iOverride = $aTemp[0] ReDim $aCSV[$iBound][$iOverride] ; Add columns to accomodate more items EndIf EndIf For $j = 1 To $aTemp[0] If StringLen($aTemp[$j]) Then If Not StringRegExp($aTemp[$j], '[^"]') Then ; Field only contains double quotes $aTemp[$j] = StringTrimLeft($aTemp[$j], 1) ; Delete enclosing double quote single char EndIf $aCSV[$i][$j -1] = $aTemp[$j] ; Populate each row EndIf Next Next ;~ If $iOverride > 1 Then Return $aCSV ; Multiple Columns (Czardas) . pixelsearch : no, to make it simpler, array returned will ALWAYS be 2D, even if 1 column only, i.e. [][1] ;~ Else ;~ For $i = 0 To $iBound -1 ;~ If StringLen($aArray[$i]) And (Not StringRegExp($aArray[$i], '[^"]')) Then ; Only contains double quotes ;~ $aArray[$i] = StringTrimLeft($aArray[$i], 1) ; Delete enclosing double quote single char ;~ EndIf ;~ Next ;~ Return $aArray ; Single column ;~ EndIf EndFunc ;==>_CSVSplit ; #INTERNAL_USE_ONLY# =========================================================================================================== ; Name...........: __GetSubstitute ; Description ...: Searches for a character to be used for substitution, ie one not contained within the input string ; Syntax.........: __GetSubstitute($string, ByRef $iCountdown) ; Parameters ....: $string - The string of characters to avoid ; $iCountdown - The first code point to begin checking ; Return values .: Success - Returns a suitable substitution character not found within the first parameter ; Failure - Sets @error to 1 => No substitution character available ; Author ........: czardas ; Comments ......; This function is connected to the function _CSVSplit and was not intended for general use ; $iCountdown is returned ByRef to avoid selecting the same character on subsequent calls to this function ; Initially $iCountown should be passed with a value = 63743 ; =============================================================================================================================== ; Func __GetSubstitute($string, ByRef $iCountdown) Func __GetSubstitute(ByRef $string, ByRef $iCountdown) ; ($string : added ByRef) If $iCountdown < 57344 Then Return SetError(1, 0, "") ; Out of options Local $sTestChar For $i = $iCountdown To 57344 Step -1 $sTestChar = ChrW($i) $iCountdown -= 1 If Not StringInStr($string, $sTestChar) Then Return $sTestChar EndIf Next Return SetError(1, 0, "") ; Out of options EndFunc ;==>__GetSubstitute ; #FUNCTION# ==================================================================================================================== ; Author ........: Gary Frost (original function in GuiListView.au3 is named _GUICtrlListView_RegisterSortCallBack) ; Modified.......: pixelsearch : $nSortDir no more always = 1, it will depend on $g_iSortDirection = 0 (ascending) or 1 (descending) ; "__GUICtrlListView_Sort2" is used instead of "__GUICtrlListView_Sort" (sort will be much faster) ; added Local Static $__LISTVIEWCONSTANT_SORTINFOSIZE = 11 (so script is compatible with 3.3.12.0 & 3.3.14.0) ; =============================================================================================================================== Func _GUICtrlListView_RegisterSortCallBack2($hWnd, $vCompareType = 1, $bArrows = True, $sPrivateCallback = "__GUICtrlListView_Sort2") #Au3Stripper_Ignore_Funcs=$sPrivateCallback If Not IsHWnd($hWnd) Then $hWnd = GUICtrlGetHandle($hWnd) If IsBool($vCompareType) Then $vCompareType = ($vCompareType) ? 1 : 0 Local $hHeader = _GUICtrlListView_GetHeader($hWnd) Local Static $__LISTVIEWCONSTANT_SORTINFOSIZE = 11 ; Global Const in GuiListView.au3 3.3.14.5 (but didn't exist in 3.3.12.0 & 3.3.14.0) ReDim $__g_aListViewSortInfo[UBound($__g_aListViewSortInfo) + 1][$__LISTVIEWCONSTANT_SORTINFOSIZE] $__g_aListViewSortInfo[0][0] = UBound($__g_aListViewSortInfo) - 1 Local $iIndex = $__g_aListViewSortInfo[0][0] $__g_aListViewSortInfo[$iIndex][1] = $hWnd ; Handle/ID of listview $__g_aListViewSortInfo[$iIndex][2] = _ DllCallbackRegister($sPrivateCallback, "int", "int;int;hwnd") ; Handle of callback $__g_aListViewSortInfo[$iIndex][3] = -1 ; $nColumn $__g_aListViewSortInfo[$iIndex][4] = -1 ; nCurCol ; $__g_aListViewSortInfo[$iIndex][5] = 1 ; $nSortDir $__g_aListViewSortInfo[$iIndex][5] = (($g_iSortDirection = 0) ? 1 : -1) ; $nSortDir $__g_aListViewSortInfo[$iIndex][6] = -1 ; $nCol $__g_aListViewSortInfo[$iIndex][7] = 0 ; $bSet $__g_aListViewSortInfo[$iIndex][8] = $vCompareType ; Treat as Strings, Numbers or use Windows API to compare $__g_aListViewSortInfo[$iIndex][9] = $bArrows ; Use arrows in the header of the columns? $__g_aListViewSortInfo[$iIndex][10] = $hHeader ; Handle to the Header Return $__g_aListViewSortInfo[$iIndex][2] <> 0 EndFunc ;==>_GUICtrlListView_RegisterSortCallBack2 ; #INTERNAL_USE_ONLY# =========================================================================================================== ; Name...........: __GUICtrlListView_Sort2 (original function in GuiListView.au3 is named __GUICtrlListView_Sort) ; Description ...: Our sorting callback function ; Syntax.........: __GUICtrlListView_Sort2 ( $nItem1, $nItem2, $hWnd ) ; Parameters ....: $nItem1 - Param of 1st item ; $nItem2 - Param of 2nd item ; $hWnd - Handle of the control ; Return values .: None ; Author ........: Gary Frost (gafrost) ; Modified.......: pixelsearch : 2 lines calling GUICtrlListView_GetItemText() replaced by 3 lines each (sort will be faster) ; Remarks .......: For Internal Use Only ; Related .......: ; Link ..........: ; Example .......: ; =============================================================================================================================== #Au3Stripper_Ignore_Funcs=__GUICtrlListView_Sort2 Func __GUICtrlListView_Sort2($nItem1, $nItem2, $hWnd) Local $iIndex, $sVal1, $sVal2, $nResult For $x = 1 To $__g_aListViewSortInfo[0][0] If $hWnd = $__g_aListViewSortInfo[$x][1] Then $iIndex = $x ExitLoop EndIf Next ; Switch the sorting direction If $__g_aListViewSortInfo[$iIndex][3] = $__g_aListViewSortInfo[$iIndex][4] Then ; $nColumn = nCurCol ? If Not $__g_aListViewSortInfo[$iIndex][7] Then ; $bSet $__g_aListViewSortInfo[$iIndex][5] *= -1 ; $nSortDir $__g_aListViewSortInfo[$iIndex][7] = 1 ; $bSet EndIf Else $__g_aListViewSortInfo[$iIndex][7] = 1 ; $bSet EndIf $__g_aListViewSortInfo[$iIndex][6] = $__g_aListViewSortInfo[$iIndex][3] ; $nCol = $nColumn ; $sVal1 = _GUICtrlListView_GetItemText($hWnd, $nItem1, $__g_aListViewSortInfo[$iIndex][3]) DllStructSetData($g_tItem, "SubItem", $__g_aListViewSortInfo[$iIndex][3]) GUICtrlSendMsg($g_idListView, $g_GetItemText, $nItem1, $g_pItem) ; requires a native-created listview, not a UDF-created listview (which got no creation id)... $sVal1 = DllStructGetData($g_tBuffer, "Text") ; $sVal2 = _GUICtrlListView_GetItemText($hWnd, $nItem2, $__g_aListViewSortInfo[$iIndex][3]) DllStructSetData($g_tItem, "SubItem", $__g_aListViewSortInfo[$iIndex][3]) GUICtrlSendMsg($g_idListView, $g_GetItemText, $nItem2, $g_pItem) ; ...$g_idListView is the global variable corresponding to the native-created listview id $sVal2 = DllStructGetData($g_tBuffer, "Text") If $__g_aListViewSortInfo[$iIndex][8] = 1 Then ; force Treat as Number if possible If StringIsInt($sVal1) Or StringIsFloat($sVal1) Then $sVal1 = Number($sVal1) If StringIsInt($sVal2) Or StringIsFloat($sVal2) Then $sVal2 = Number($sVal2) EndIf If $__g_aListViewSortInfo[$iIndex][8] < 2 Then ; Treat as String or Number $nResult = 0 ; No change of item1 and item2 positions If $sVal1 < $sVal2 Then $nResult = -1 ; Put item2 before item1 ElseIf $sVal1 > $sVal2 Then $nResult = 1 ; Put item2 behind item1 EndIf Else ; Use API handling $nResult = DllCall('shlwapi.dll', 'int', 'StrCmpLogicalW', 'wstr', $sVal1, 'wstr', $sVal2)[0] EndIf $nResult = $nResult * $__g_aListViewSortInfo[$iIndex][5] ; $nSortDir Return $nResult EndFunc ;==>__GUICtrlListView_Sort2 ; #INDEX# ======================================================================================================================= ; Title .........: ArrayMultiColSort ; AutoIt Version : v3.3.8.1 or higher ; Language ......: English ; Description ...: Sorts 2D arrays on several columns ; Note ..........: ; Author(s) .....: Melba23 ; Remarks .......: ; =============================================================================================================================== ; #CURRENT# ===================================================================================================================== ; _ArrayMultiColSort : Sort 2D arrays on several columns ; =============================================================================================================================== ; #INTERNAL_USE_ONLY#================================================================================================= ; __AMCS_SortChunk : Sorts array section ; =============================================================================================================================== ; #FUNCTION# ==================================================================================================================== ; Name...........: _ArrayMultiColSort ; Description ...: Sort 2D arrays on several columns ; Syntax.........: _ArrayMultiColSort(ByRef $aArray, $aSortData[, $iStart = 0[, $iEnd = 0]]) ; Parameters ....: $aArray - The 2D array to be sorted ; $aSortData - 2D array holding details of the sort format ; Format: [Column to be sorted, Sort order] ; Sort order can be either numeric (0/1 = ascending/descending) or a ordered string of items ; Any elements not matched in string are left unsorted after all sorted elements ; $iStart - Element of array at which sort starts (default = 0) ; $iEnd - Element of array at which sort endd (default = 0 - converted to end of array) ; Requirement(s).: v3.3.8.1 or higher ; Return values .: Success: No error ; Failure: @error set as follows ; @error = 1 with @extended set as follows (all refer to $sIn_Date): ; 1 = Array to be sorted not 2D ; 2 = Sort data array not 2D ; 3 = More data rows in $aSortData than columns in $aArray ; 4 = Start beyond end of array ; 5 = Start beyond End ; @error = 2 with @extended set as follows: ; 1 = Invalid string parameter in $aSortData ; 2 = Invalid sort direction parameter in $aSortData ; 3 = Invalid column index in $aSortData ; Author ........: Melba23 ; Remarks .......: Columns can be sorted in any order ; Example .......; Yes ; =============================================================================================================================== Func _ArrayMultiColSort(ByRef $aArray, $aSortData, $iStart = 0, $iEnd = 0) ; Errorchecking ; 2D array to be sorted If UBound($aArray, 2) = 0 Then Return SetError(1, 1, "") EndIf ; 2D sort data If UBound($aSortData, 2) <> 2 Then Return SetError(1, 2, "") EndIf If UBound($aSortData) > UBound($aArray) Then Return SetError(1, 3) EndIf For $i = 0 To UBound($aSortData) - 1 If $aSortData[$i][0] < 0 Or $aSortData[$i][0] > UBound($aArray, 2) -1 Then Return SetError(2, 3, "") EndIf Next ; Start element If $iStart < 0 Then $iStart = 0 EndIf If $iStart >= UBound($aArray) - 1 Then Return SetError(1, 4, "") EndIf ; End element If $iEnd <= 0 Or $iEnd >= UBound($aArray) - 1 Then $iEnd = UBound($aArray) - 1 EndIf ; Sanity check If $iEnd <= $iStart Then Return SetError(1, 5, "") EndIf Local $iCurrCol, $iChunk_Start, $iMatchCol ; Sort first column __AMCS_SortChunk($aArray, $aSortData, 0, $aSortData[0][0], $iStart, $iEnd) If @error Then Return SetError(2, @extended, "") EndIf ; Now sort within other columns For $iSortData_Row = 1 To UBound($aSortData) - 1 ; Determine column to sort $iCurrCol = $aSortData[$iSortData_Row][0] ; Create arrays to hold data from previous columns Local $aBaseValue[$iSortData_Row] ; Set base values For $i = 0 To $iSortData_Row - 1 $aBaseValue[$i] = $aArray[$iStart][$aSortData[$i][0]] Next ; Set start of this chunk $iChunk_Start = $iStart ; Now work down through array For $iRow = $iStart + 1 To $iEnd ; Match each column For $k = 0 To $iSortData_Row - 1 $iMatchCol = $aSortData[$k][0] ; See if value in each has changed If $aArray[$iRow][$iMatchCol] <> $aBaseValue[$k] Then ; If so and row has advanced If $iChunk_Start < $iRow - 1 Then ; Sort this chunk __AMCS_SortChunk($aArray, $aSortData, $iSortData_Row, $iCurrCol, $iChunk_Start, $iRow - 1) If @error Then Return SetError(2, @extended, "") EndIf EndIf ; Set new base value $aBaseValue[$k] = $aArray[$iRow][$iMatchCol] ; Set new chunk start $iChunk_Start = $iRow EndIf Next Next ; Sort final section If $iChunk_Start < $iRow - 1 Then __AMCS_SortChunk($aArray, $aSortData, $iSortData_Row, $iCurrCol, $iChunk_Start, $iRow - 1) If @error Then Return SetError(2, @extended, "") EndIf EndIf Next EndFunc ;==>_ArrayMultiColSort ; #INTERNAL_USE_ONLY# =========================================================================================================== ; Name...........: __AMCS_SortChunk ; Description ...: Sorts array section ; Author ........: Melba23 ; Remarks .......: ; =============================================================================================================================== Func __AMCS_SortChunk(ByRef $aArray, $aSortData, $iRow, $iColumn, $iChunkStart, $iChunkEnd) Local $aSortOrder ; Set default sort direction Local $iSortDirn = 1 ; Need to prefix elements? If IsString($aSortData[$iRow][1]) Then ; Split elements $aSortOrder = StringSplit($aSortData[$iRow][1], ",") If @error Then Return SetError(1, 1, "") EndIf ; Add prefix to each element For $i = $iChunkStart To $iChunkEnd For $j = 1 To $aSortOrder[0] If $aArray[$i][$iColumn] = $aSortOrder[$j] Then $aArray[$i][$iColumn] = StringFormat("%02i-", $j) & $aArray[$i][$iColumn] ExitLoop EndIf Next ; Deal with anything that does not match If $j > $aSortOrder[0] Then $aArray[$i][$iColumn] = StringFormat("%02i-", $j) & $aArray[$i][$iColumn] EndIf Next Else Switch $aSortData[$iRow][1] Case 0, 1 ; Set required sort direction if no list If $aSortData[$iRow][1] Then $iSortDirn = -1 Else $iSortDirn = 1 EndIf Case Else Return SetError(1, 2, "") EndSwitch EndIf ; Sort the chunk Local $iSubMax = UBound($aArray, 2) - 1 __ArrayQuickSort2D($aArray, $iSortDirn, $iChunkStart, $iChunkEnd, $iColumn, $iSubMax) ; Remove any prefixes If IsString($aSortData[$iRow][1]) Then For $i = $iChunkStart To $iChunkEnd $aArray[$i][$iColumn] = StringTrimLeft($aArray[$i][$iColumn], 3) Next EndIf EndFunc ;==>__AMCS_SortChunk