Tutorial
Erste Schritte in der WinAPI mit Freebasic und FBEdit
von stephanbrunker | Seite 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:
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: 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.
Zusätzliche Informationen und Funktionen | |||||||
---|---|---|---|---|---|---|---|
|
|