Buchempfehlung
Windows System Programming
Windows System Programming
Das Kompendium liefert viele interessante Informationen zur Windows-Programmierung auf Englisch. [Mehr Infos...]
FreeBASIC-Chat
Es sind Benutzer im FreeBASIC-Chat online.
(Stand:  )
FreeBASIC bei Twitter
Twitter FreeBASIC-Nachrichten jetzt auch über Twitter erhalten. Follow us!

Tutorial

Erste Schritte in der WinAPI mit Freebasic und FBEdit

von MitgliedstephanbrunkerSeite 13 von 13

Listview

Das bekannteste Anwendungsbeispiel für das Listview-Element dürfte der Windows-Explorer sein, wo die Dateien in verschiedenen Ansichten (Report, Icon, Small Icon und Liste) dargestellt werden, mit beliebig vielen Spalten und so weiter. Eine Einführung in die Möglichkeiten dieses Element schließt mein Tutorial erstmal ab.

Das Listview-Element befindet sich in der Werkzeugleiste des Recourcen-Editors, damit bauen wir uns diesen Dialog:

WinTut22.jpg

Im Editor hat das Listview-Element erstmal keine Spalten; wichtig ist erstmal nur die Property Type, die wir auf "Report" setzen. Dann fehlen in den Windows-header-Dateien von Freebasic mal wieder eine ganze Reihe Einträge, die wir in unsere *.bi schreiben:

    'ListBox Macros
    #Define ListBox_DeleteString(hLST,index) SendMessage(hLST,LB_DELETESTRING,index,NULL)
    #Define ListBox_AddString(hLST,htext) SendMessage(hLST,LB_ADDSTRING,NULL,Cast(LPARAM,htext))
    #Define ListBox_GetCurSel(hLST) SendMessage(hLST,LB_GETCURSEL,NULL,NULL)
    #Define ListBox_GetSelCount(hLST) SendMessage(hLST,LB_GETSELCOUNT,NULL,NULL)
    #Define ListBox_GetSelItems(hLST,nStrings,psel) SendMessage(hLST,LB_GETSELITEMS, nStrings, Cast(LPARAM,psel))

    'Button Macros
    #Define Button_GetCheck(hBTN) SendMessage(hBTN,BM_GETCHECK,NULL,NULL)
    #Define Button_SetCheck(hBTN) SendMessage(hBTN,BM_SETCHECK,NULL,NULL)

    'Combo Box Macros
    #Define ComboBox_AddString(hCBO,htext) SendMessage(hCBO,CB_ADDSTRING,NULL,Cast(LPARAM,htext))
    #Define ComboBox_GetCurSel(hCBO) SendMessage(hCBO,CB_GETCURSEL,NULL,NULL)
    #Define ComboBox_SetCurSel(hCBO,index) SendMessage(hCBO,CB_SETCURSEL,index,NULL)

Dabei sind auch die fehlenden Makros für Button, ComboBox und Listbox aus den vorherigen Lektionen. Meiner Meinung nach gibt es den schöneren Code, wenn man die Makros statt der SendMessage-Funktion verwendet, aber das ist natürlich Geschmackssache und dem fertigen Programm herzlich egal.

Wir haben also keine Spalten in unserem ListView, und das ändern wir in der Intialisierung:

'Init the Columns of the Listview with a user defined type
Function InitListViewColumns(ByVal hLSV As HWND, lvci () As LISTVIEWCOLUMS) As Integer
    Dim As LVCOLUMN lvc
    Dim As Integer index

    lvc.mask = LVCF_FMT Or LVCF_WIDTH Or LVCF_TEXT
    lvc.fmt = LVCFMT_LEFT

    For index = 0 To UBound(lvci)
        lvc.pszText = StrPtr(lvci(index).title)
        lvc.cx      = lvci(index).colwidth
        ListView_InsertColumn(hLSV, index, @lvc)
    Next index

    Return TRUE

End Function

        'init ListView
        hLSV1=GetDlgItem(hMain,IDC_LSV1)
        Dim lvci(0 To 1) As LISTVIEWCOLUMS
        lvci(0).title="Name": lvci(0).colwidth=200
        lvci(1).title="Path": lvci(1).colwidth=150
        InitListViewColumns(hLSV1,lvci())
        hLarge1 = ImageList_Create(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON),ILC_MASK, 1, 1)
        hSmall1 = ImageList_Create(GetSystemMetrics(SM_CXSMICON),GetSystemMetrics(SM_CYSMICON),ILC_MASK, 1, 1)
        ListView_SetImageList(hLSV1, hLarge1, LVSIL_NORMAL)
        ListView_SetImageList(hLSV1, hSmall1, LVSIL_SMALL)
        SetWindowSubclass(hLSV1, @SubProc, 1, 1 )

dafür verwenden wir einen eigenen Type:

    Type LISTVIEWCOLUMS
        As String title
        As Integer colwidth
    End Type

Man könnte natürlich die Werte der Helperfunktion auch einfach so übergeben, aber mich hat für dieses Tutorial der Ehrgeiz gepackt, einen besonders schönen Code zu fabrizieren. Dazu gehört auch der geschickte Einsatz der internen SCOPE-Blöcke. Jede Select Case oder IF ... ENDIF-Schleife erzeugt einen internen Scope-Block, so dass wir die Variablen, die wir nur für diesen einen Vorgang brauchen, nur dort zu definieren brauchen. So oder so - wir erstellen Spalten, die einen Namen und eine Breite haben, erstellen die ImageLists (eine für großes und eine für kleines Icon) und ordnen sie dem ListView zu. Zuletzt erstellen wir wie bei der ListBox eine Subclass für den Tastaturinput.

Die Combobox füllen wir mit den Ansichtsoptionen des ListView:

        'init Combobox
        hCBO1=GetDlgItem(hMain,IDC_CBO1)
        ComboBox_AddString(hCBO1,@"Icons")
        ComboBox_AddString(hCBO1,@"Liste")
        ComboBox_AddString(hCBO1,@"Report")
        ComboBox_AddString(hCBO1,@"SmallIcon")
        ComboBox_SetCurSel(hCBO1,2)

Und der Code für die Combobox sieht so aus:

    Case CBN_SELCHANGE      'Combo Box changing
        Select Case LoWord(wParam)
            Case IDC_CBO1       'List view options
                Select Case ComboBox_GetCurSel(hCBO1)
                    Case 0
                        ListView_SetView(hLSV1,LV_VIEW_ICON)
                    Case 1
                        ListView_SetView(hLSV1,LV_VIEW_LIST)
                    Case 2
                        ListView_SetView(hLSV1,LV_VIEW_DETAILS)
                    Case 3
                        ListView_SetView(hLSV1,LV_VIEW_SMALLICON)
                End Select
        End Select

Für die Datei-öffnen und Datei-Speichern-Dialoge können wir unsere fertigen Funktionsbausteine von vorher nehmen - Baukastensystem sei Dank.

Wenn der Benutzer jetzt auf unseren Datei-öffnen-Button klickt, dann sieht das im Code so aus:

    Case IDC_BTN1       'Click on BTN1 - FileOpen Dialog and insert items in listview
        Dim As String filepath
        ReDim As String filetitle(0 To 0)
        Dim As Integer modus=0 Or ALLOWMULTISELECT
        If File_GetOpenName(hWin,modus,filepath,filetitle()) = TRUE Then
            'copy the result in the Listbox
            Dim i As Integer
            For i=0 To UBound(filetitle)
                AddListViewRow(hLSV1,filepath,filetitle(i),hLarge1,hSmall1)
            Next i
        End If

Nicht viel Code, wir übergeben der Helperfunktion den Pfad und das Array an Dateinamen, die wir aus dem Öffnen-Dialog bekommen haben:

Function AddListViewRow(ByVal hLSV As HWND, ByRef filepath As String, filetitle As String, ByRef hLarge As HIMAGELIST, ByRef hSmall As HIMAGELIST ) As Integer
    Dim As LVITEM lvi
    Dim As Integer lvsize,index
    Dim As String tempstring
    Dim As ZString * MAX_PATH gettextfile, gettextpath
    Dim As HICON hIconItem
    Dim As WORD nicon = 1

    lvi.mask = LVIF_TEXT Or LVIF_IMAGE
    lvsize = ListView_GetItemCount(hLSV)

    'check if row is already existing
    For index = 0 To lvsize - 1
        ListView_GetItemText(hLSV,index,0,@gettextfile,SizeOf(gettextfile))
        ListView_GetItemText(hLSV,index,1,@gettextpath,SizeOf(gettextpath))
        If filetitle = gettextfile And filepath = gettextpath   Then Return FALSE
    Next index

    'fill the LVITEM structure
    lvi.iitem   = lvsize
    lvi.pszText = StrPtr(filetitle)
    lvi.iimage  = lvsize
    'insert item and subitem #1 = path
    ListView_InsertItem(hLSV,@lvi)
    ListView_SetItemText(hLSV,lvsize,1,StrPtr(filepath))
    'get the associated item from the registry
    tempstring = filepath & filetitle
    hIconItem = ExtractAssociatedIcon(hInstance,StrPtr(tempstring),@nicon)
    'insert icon in imagelist
    ImageList_ReplaceIcon(hLarge,-1,hIconItem)
    ImageList_ReplaceIcon(hSmall,-1,hIconItem)
    DestroyIcon(hIconItem)

    Return TRUE

End Function

Im wesentlichen ist das eine LVITEM-Struktur, die wir ausfüllen und an das Element übergeben, nachdem wir getestet haben, ob das Element schon existiert. Wie bei einer Reihe von anderen Windows-Strukturen auch, hat das mask-Member die Aufgabe anzugeben, was wir übergeben wollen. Die Struktur ist nämlich noch größer, aber wir brauchen nur bestimmte Einträge, in diesem Fall eben Text und Image. Dann sieht das Listview-Element so aus, dass es ein "Item" gibt, in diesem Fall unseren Dateinamen. Die weiteren Spalten heißen "Subitem" und werden von 1 an durchnummeriert - hier gibt es nur die 1 für unseren Pfad. Dieses Subitem können wir einfach mit ListView_SetItemText hinzufügen. Damit die Dateien noch die aus dem Explorer bekannten Icons bekommen, holen wir diese mit der richtigen Funktion und schreiben sie in die ImageLists. Der Eintrag "Image" bei jedem Item verweist nämlich auf die dazugehörige Bildnummer in der Imagelist. Das Problem beinnt nur dann, wenn wir die Liste sortieren oder Einträge löschen.

Zum Löschen verwenden wir wieder unsere Subclass mit Keyboardinput und der dazugehörigen Callback-Funktion:

'Subclass Callback Listview
Function SubProc(ByVal hWin As HWND,ByVal uMsg As UINT,ByVal wParam As WPARAM,ByVal lParam As LPARAM,ByVal uIdSubclass As UINT_PTR, dwRefData As DWORD_PTR) As Integer

    Select Case uMsg
        Case WM_KEYDOWN         'Keyboard input
            Select Case wParam
                Case VK_DELETE      'Delete Key
                    DeleteListViewSelRows(hLSV1)
            End Select

        Case Else
            'Proceed with Default Callback Function
            Return DefSubclassProc(hWin, uMsg, wParam, lParam)
    End Select

End Function

Die Helperfunktion zum Löschen besteht aus zwei Teilen:

'Updates the Image entry of the ListView items
Function UpdateListViewImageMap(ByVal hLSV As HWND) As Integer
    Dim As LVITEM lvi
    Dim As Integer lvsize,i
    lvi.mask = LVIF_IMAGE
    lvsize = ListView_GetItemCount(hLSV)
    For i=0 To lvsize-1
        lvi.iimage = i
        lvi.iitem  = i
        ListView_SetItem(hLSV,@lvi)
    Next i
    Return TRUE
End Function

'Deletes selected rows in the Listview and call for an update of the images
Function DeleteListViewSelRows(ByVal hLSV As HWND) As Integer
    Dim As Integer lvselected, index, i
    lvselected = ListView_GetSelectedCount(hLSV)    'get the number of selected items
    If lvselected = 0 Then Return FALSE
    'get selected, delete item and image
    For i=1 To lvselected
        index=ListView_GetNextItem(hLSV,-1,LVNI_SELECTED)
        ListView_DeleteItem(hLSV,index)
        ImageList_Remove(hSmall1,index)
        ImageList_Remove(hLarge1,index)
    Next i
    'sync the items and the image numbers
    UpdateListViewImageMap(hLSV)
End Function

Wie bei der Listbox erfragen wir die Anzahl der selektierten Reihen, fragen die der Reihe nach mit ListView_GetNextItem ab und löschen sie zusammen mit dem passenden Eintrag in der Imagelist. Was jetzt passiert ist, dass die index-Nummern der Liste sich automatisch verschieben, da keine Lücken gelassen werden. Da aber der iImage-Eintrag sich nicht ändert, zeigt dieser jetzt auf die falschen Bildnummern. Das korrigieren wir mit der zweiten Funktion, die die Bildnummern wieder gleich der neuen Indexnummern setzt.

Als letztes habe ich noch die allgemein bekannte Sortierung implementiert, wenn man auf die Spaltennamen klickt. Für die Sortierung gibt es zwei grundsätzliche Möglichkeiten: Entweder man definiert bei jedem Element einen lparam-Parameter, zum Beispiel laufende Nummern, nach denen man sortieren will. Das geht dann mit ListView_SortItems. In unserem Fall ist dieser Eintrag leer und wir wollen die Indexnummern, damit wir den Pfad oder den Dateinamen holen können und anhand dessen über die Sortierung bestimmen, die Funktion heißt dann ListView_SortItemsEx. Einfach gesagt ruft diese Funktion eine Callbackfunktion auf, übergibt zwei Einträge und will wissen welcher zuerst kommt anhand der Flags, die auch noch mit übergeben werden. Der Klick auf den Spaltenkopf ist ein WM_NOTIFY-Eintrag:

        Case WM_NOTIFY
            'Pointer zu lParam=NMHDR Struktur for WM_NOTIFY
            Dim lpNMHDR As NMHDR Ptr = Cast(NMHDR Ptr,lParam)

            Select Case lpNMHDR->code
                Case LVN_COLUMNCLICK        'Click at Listview Columns for sorting
                    Dim pnmv As NMLISTVIEW Ptr = Cast(NMLISTVIEW Ptr,lparam)
                    Select Case pnmv->iSubItem   'get the clicked row
                        Case 0
                            ListView_SortItemsEx(hLSV1,@ListViewSort,ASCENDING Or FILECOLUMN)
                        Case 1
                            ListView_SortItemsEx(hLSV1,@ListViewSort,ASCENDING Or PATHCOLUMN)
                    End Select
                    'recreate the image lists in the new order
                    UpdateListViewImageLists(hLSV1,hLarge1,hSmall1)
            End Select

Hier habe ich wieder meine eigenen Flags verwendet, weils schöner ist. Die WM_NOTIFY-Message ist insofern etwas verzwickt, weil hier viel mehr Parameter übergeben werden als bei WM_COMMAND. Hinter dem LPARAM-Member von SendMessage folgen im Speicher weitere Einträge in Form einer NMHDR-Struktur, deren erster Eintrag eben LPARAM ist. Und erst in dieser Struktur ist der eigentliche Code versteckt, was uns die Notify-Meldung sagen will. Die angegebene Callbackfunktion sagt dann bei jeder Paarung, was zuerst kommen soll, in Abhängigkeit von unseren Flags:

Function ListViewSort(ByVal param1 As LPARAM, ByVal param2 As LPARAM, ByVal lparamSort As LPARAM) As Integer
    Dim As Integer i
    Dim As ZString * MAX_PATH sortstring1, sortstring2

    Select Case lparamSort
        'sort ascending filepaths (subitem 1)
        Case (ASCENDING Or PATHCOLUMN)
            'get the filepath from the item
            ListView_GetItemText(hLSV1,param1,1,@sortstring1,SizeOf(sortstring1))
            ListView_GetItemText(hLSV1,param2,1,@sortstring2,SizeOf(sortstring2))
            'deciding sort in case of ASCII numbers
            For i=0 To MAX_PATH-1
                If sortstring1[i] > sortstring2[i] Then
                    Return 1
                ElseIf sortstring1[i] < sortstring2[i] Then
                    Return -1
                EndIf
            Next i
            Return 0

        'sort ascending filenames (item, subitem 0)
        Case (ASCENDING Or FILECOLUMN)
            'get filename from the item
            ListView_GetItemText(hLSV1,param1,0,@sortstring1,SizeOf(sortstring1))
            ListView_GetItemText(hLSV1,param2,0,@sortstring2,SizeOf(sortstring2))
            For i=0 To MAX_PATH-1
                If sortstring1[i] > sortstring2[i] Then
                    Return 1
                ElseIf sortstring1[i] < sortstring2[i] Then
                    Return -1
                EndIf
            Next i
            Return 0
    End Select

End Function

Im Prinzip passiert nichts weiter als dass wir die Indizes erhalten, diese mit ListView_GetItemText auslesen in in Abhängigkeit von der gewünschten Spalte und zum Sortieren habe ich auf die Schnelle mal den ASCII-Code hergenommen. Das Ergebnis -1, 0, 1 besagt dann ob vorher, nachher oder gleich einsortiert werden soll.

Da dann aber unsere iitem-Einträge nicht mehr gleich der Indexnummer der Einträge ist und alles ziemlich durcheinander gerät, wenn wir jetzt einen Eintrag löschen, müssen wir die Imagelisten einmal neu erstellen, dass ist die Funktion UpdateListViewImageLists:

Function UpdateListViewImageLists(ByVal hLSV As HWND, ByRef hLarge As HIMAGELIST, ByRef hSmall As HIMAGELIST) As Integer
    'redo the complete imagelist, function not neccessary at the moment
    Dim As UInteger index,lvsize,imagesize
    Dim hIconItem As HICON
    Dim nicon As WORD = 1
    Dim filename As ZString * MAX_PATH
    Dim pathname As ZString * MAX_PATH

    lvsize = ListView_GetItemCount(hLSV)
        If lvsize = 0 Then Return FALSE

    ImageList_Remove(hLarge,-1)
    ImageList_Remove(hSmall,-1)

    For index = 0 To lvsize-1
        ListView_GetItemText(hLSV,index,1,@pathname,SizeOf(pathname))
        ListView_GetItemText(hLSV,index,0,@filename,SizeOf(filename))

        filename = pathname & filename
        hIconItem = ExtractAssociatedIcon(hInstance,@filename,@nicon)
        ImageList_ReplaceIcon(hLarge,-1,hIconItem)
        ImageList_ReplaceIcon(hSmall,-1,hIconItem)
        DestroyIcon(hIconItem)
    Next index
    UpdateListViewImageMap(hLSV)
    Return TRUE
End Function

Der aufmerksame User bemerkt vielleicht, das im Windows-Explorer ein Text steht, wenn die Liste leer ist. Wie das geht, hatten wir schon mal ... nämlich die Geschichte mit der transparenten STATIC, die wir nur anzeigen, wenn die Liste leer ist.

Das fertige Projekt: Externer Link!Tutorial9.zip

Das war der Kurzeinblick in das Listview-Element und unser Tutorial über die wichtigsten Windows-Elemente. Jetzt ist natürlich naheliegend, dass wir die Elemente in unsere Liste mit Drag und Drop einfügen und herauskopieren wollen und das kommt in einem zweiten Tutorial, denn das ist für sich schon ein ganz schöner Programmieraufwand.

 

Gehe zu Seite Gehe zu Seite  1  2  3  4  5  6  7  8  9  10  11  12  13  
Zusätzliche Informationen und Funktionen
  Bearbeiten Bearbeiten  

  Versionen Versionen