Tutorial
Erste Schritte in der WinAPI mit Freebasic und FBEdit
von stephanbrunker | Seite 9 von 13 |
Datei öffnen / Speichern Dialoge
Wenn wir vom User eine Datei auswählen lassen wollen, dann stellt Windows dafür fertige Dialoge zur Verfügung. Die Funktion heißt einfach GetOpenFileName, aber damit ist es natürlich nicht getan. Die dafür nötigen Parameter speichert Windows in einer OPENFILENAME Struktur, also einem UDT, dessen Member die Parameter sind, und die auch für die Ausgabe verwendet werden. Am einfachsten ist es dabei, sich einmal eine universell verwendbare Function zu bauen, die mit ein paar Anpassungen universell einbaubar ist. In der Beispieldatei öffnen wir einmal eine einzelne und dann mehrere Dateien und zeigen diese in einer Listbox an, die wir damit gleich auch abhandeln. Wenn wir die Anwendung mit der Tastatur bedienen wollen - dann noch ein Hinweis: Buttons und Checkboxen kann man mit der Leertaste auslösen bzw. umschalten - während die Return-Taste ja für den Default-Button reserviert ist.
Zuerst müssen wir unsere Includes um die "win\shlobj.bi" ergänzen. Wenn der Compiler meckert dass er die Funktion nicht findet und man nicht weiß, in welcher der vielen *.bi Dateien sie enthalten ist, dann empfehle ich die Windows-Suche im Verzeichnis FB\inc\win mit der Option "Dateiinhalte durchsuchen".
Um eine universell einsetzbare Helperfunction für die OPENFILENAME Struktur zu bekommen, geben wir der Funktion eine Variable modus mit, die in den unteren 4 Bits den gewünschten Filter angibt und in den oberen 4 Bits weitere Flags, im Moment ist das nur die Option, mehrere Dateien auf einmal zu laden (ALLOWMULTISELECT). Außerdem können wir das Startverzeichnis vorgeben, indem wir in die Variable Filepath einen Wert schreiben.
Die Function sieht dann so aus:
Function file_getopenname( ByVal hWnd As HWND, ByVal modus As UByte, ByRef filepath As String, filetitle () As String) As Integer
'Open File Dialog, copies the path and an array of filenames, with flags in 'modus'
'to define an inital Directory and Filename, send filepath and filetitle(0) not empty
'Init Variables
Dim As ZString * 2048 filelist = filetitle(0) 'Size of the buffer for the filelists and initial filename
Dim As ZString * MAX_PATH filename
Dim As String tempstring
Dim As Integer i
Dim As OPENFILENAME ofn
Dim As UShort nfiles, pathlen,extenlen
'Members OPENFILENAME Structure
With ofn
.lStructSize = SizeOf( OPENFILENAME )
.hwndOwner = hWnd
.hInstance = hInstance
.lpstrCustomFilter = NULL 'no User customisation
.nMaxCustFilter = 0
.nFilterIndex = 1 'we define our Filters seperately, preselect the first
.lpstrFile = @filelist 'proposed filename if first character not 0
.nMaxFile = SizeOf( filelist )
.lpstrFileTitle = @filename
.nMaxFileTitle = SizeOf( filename )
.lpstrInitialDir = StrPtr(filepath) 'start Directory - use filepath for in/out
.nFileOffset = pathlen
.nFileExtension = extenlen
.lpstrDefExt = NULL 'no Default extension
.lCustData = 0
.lpfnHook = NULL
.lpTemplateName = NULL
End With
'Customisation File-Type-List and Multiselect
'--------------------------------------------
'Title = Title of the Open ... Window
'Filter in 0-separated pairs, containing a description text and an ";" separated list of extensions
'terminate with extra 0
Select Case modus And 15 'Lower 4 Bits
Case 0
ofn.lpstrTitle = @"Datei laden"
ofn.lpstrFilter = StrPtr( !"Alle Dateien, (*.*)\0*.*\0\0" )
Case 1
ofn.lpstrTitle = @"ausführbare Datei laden"
ofn.lpstrFilter = StrPtr( !"Anwendung, (*.exe)\0*.exe\0\0" )
Case 2
ofn.lpstrTitle = @"Freebasic-Datei laden"
ofn.lpstrFilter = StrPtr( !"Basic-Code, (*.bas *.bi)\0*.bas;*.bi\0\0" )
Case 3
ofn.lpstrTitle = @"Datei laden"
ofn.lpstrFilter = StrPtr( !"Basic-Code, (*.bas *.bi)\0*.bas;*.bi\0Alle Dateien, (*.*)\0*.*\0\0" )
End Select
'Our Flag for the multiselect option
Select Case modus Shr 4 'Higher 4 Bits
Case 0
ofn.Flags = OFN_EXPLORER Or OFN_FILEMUSTEXIST Or OFN_PATHMUSTEXIST
Case 1
ofn.Flags = OFN_EXPLORER Or OFN_FILEMUSTEXIST Or OFN_PATHMUSTEXIST Or OFN_ALLOWMULTISELECT
End Select
'Init Array of filenames
'-----------------------
ReDim filetitle (0 To 0)
filetitle(0) = ""
If( GetOpenFileName( @ofn ) = FALSE ) Then 'no File selected or buffer to small
filepath = ""
Return FALSE
Else
pathlen = ofn.nFileOffset 'get output of Openfile Function
extenlen = ofn.nFileExtension
If extenlen <> 0 Then 'Single-selection
filepath = Left(filelist,pathlen) 'devide path and filename
filetitle(0) = filename
Else 'Multiselect
filepath = filelist & "\" 'Copy only the path, because of the 0-separation
For i = pathlen To SizeOf(filelist)-1
If filelist[i] <> 0 then 'filename continues, copy bytes
tempstring &= Chr(filelist[i])
Else '0-separator
If tempstring = "" Then Exit For '00 means end of filelist
ReDim Preserve filetitle(0 To nFiles) As String 'extend the Array
filetitle(nFiles) = tempstring 'copy to Array
tempstring = ""
nFiles += 1 'next Array index
End If
Next i
End If
Return TRUE
End If
End Function
Eine ganze Menge Code, aber ich habe das hoffentlich gut kommentiert - man kann der Funktion jede Menge Optionen mitgeben, aber man braucht nur die wichtigsten, die meisten Member der OPENFILENAME-Struktur kann man fixieren. Die Variable modus wählt aus verschiedenen Kombinationen von Fenstertitel und Filter und wählt mit einer OR-Verknüpfung die Mehrfachauswahl-Option. ALLOWMULTISELECT muss in dem Fall auf 16 definiert werden. Es macht den Code lesbarer, wenn man seine eigenen Flags verwendet und die dann einen sinnvollen Namen haben. Die Filter sind zur Auswahl bestimmter Dateitypen oder in Gruppen, jeweils mit einem 0-Zeichen separiert - was man wenn man die Strings mit einem "!" beginnen lässt mit "\0" schreiben kann. Beim Dateiöffnen sind existierende Pfade eine sinnvolle Voreinstellung und wenn man die Strings, die man der Funktion übergibt mit einem Pfad und einem Namen füllt, dann zeigt der Öffnen-Dialog direkt aufs richtige Verzeichnis und schlägt einen Dateinamen vor. Der zweite Teil der Funktion klaubt dann das Ergebnis auseinander und kopiert es in ein Array aus Strings. Tricky ist dabei, dass GetOpenFileName unterschiedlich arbeitet wenn nur eine oder mehrere Dateien ausgegeben werden. Im ersteren Fall wird ein zusammenhängender String für Pfad und Dateiname ausgeworfen, während die Längen für Pfad und Erweiterung angegeben werden. Im zweiten Fall kommt zuerst der Pfad, dann eine Null und dann die Null-separierten Dateinamen.
Die Combobox für die Auswahl der Filter hatten wir ja schon in einem vorherigen Beispiel. Der Code für den Klick auf den Button ist ziemlich simpel:
Case WM_COMMAND 'Message sent by Usercommand
Select Case HiWord(wParam)
Case BN_CLICKED 'Left Mousebutton
Select Case LoWord(wParam)
Case IDC_BTN1 'Click on the Button
'Check Combobox
modus = ComboBox_GetCurSel(hCBO1)
'Check Checkbox and OR Multiselect Flag
If Button_GetCheck(hCHK1) = BST_CHECKED Then modus Or= ALLOWMULTISELECT
'OPENFILENAME helper function
filepath="C:\Windows\" 'start Directory
If file_getopenname(hWin,modus,filepath,filetitle()) = TRUE Then
'copy the result in the Listbox
For i=0 To UBound(filetitle)
tempstring = filepath & filetitle(i)
ListBox_AddString(hLST1,StrPtr(tempstring))
Next i
End If
'Set focus to listbox for Keyboard Input
SetFocus(hLST1)
End Select
End Select
Dabei werden nur der Status der Checkbox und der Combobox abgefragt (CB_GETCURSEL und BM_GETCHECK, dann die Helperfunktion aufgerufen und mit LB_ADDSTRING die Elemente des Dateinamen-Arrays in die List geschrieben. SetFocus setzt dann den Fokus auf die Liste für den Keyboardinput. Denn wir wollen die Elemente ja auch wieder löschen können mit ENTF. Wie im vorherigen Kapitel beschrieben, müssen wir dafür die Listbox in eine Subklasse verlagern:
'initializing Handles, Subclasses
hLST1 = GetDlgItem(hMain,IDC_LST1)
hCBO1 = GetDlgItem(hMain,IDC_CBO1)
hCHK1 = GetDlgItem(hMain,IDC_CHK1)
SetWindowSubclass(hLST1, @SubProc, 1, 1 )
Die erste "1" ist die ID der Subklasse die zusammen mit der Callbackfunction die Klasse eindeutig definiert. Die zweite "1" ist eine ID, die wir zusätzlich mitgeben können. Damit können wir die Aufrufe von verschiedenen Elementen auseinanderklamüsern, wenn wir für alle die gleiche Subklasse verwenden. Der Code in der Callbackfunction für den Keyboardinput sieht dann so aus:
'Subclass Callback Function
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
Dim As Integer i,nStrings,sel
Select Case uMsg
Case WM_KEYDOWN 'Keyboard input
Select Case wParam
Case VK_DELETE 'Delete Key
'get number of selected Items to delete
nStrings = ListBox_GetSelCount(hLST1)
If nStrings = 0 Then
Return 0
ElseIf nStrings = LB_ERR Then
'Single select version
'get selected Item
sel = ListBox_GetCurSel(hLST1)
If sel = LB_ERR Then
Return 0
Else
'delete it
ListBox_DeleteString(hLST1,sel)
EndIf
Else
'Multiselect version
'allocate memory for item numbers
Dim psel As Integer Ptr = New Integer [nStrings]
'get Array of selected items and delete them
ListBox_GetSelItems(hLST1,nStrings,psel)
For i = nStrings - 1 To 0 Step -1
ListBox_DeleteString(hLST1,psel[i])
Next i
'free the array
Delete[] psel
EndIf
End Select
Case Else
'Proceed with Default Callback Function
Return DefSubclassProc(hWin, uMsg, wParam, lParam)
End Select
Return 0
End Function
Die Message heißt WM_KEYDOWN und die Werte der einzelnen Tasten stehen unter Virtual Key Codes. Weil es mir besser gefiel, habe ich die fehlenden Makros für die Controls verwendet und dann eben selbst in der *.bi definiert:
'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)
Damit der Code universell verwendbar bleibt, wird zuerst die Anzahl der selektierten Elemente abgefragt. Das ist ein Property der Listbox die wir in der Resocurce einstellen können, Multiselect TRUE oder FALSE. Bei nur einem Element zu löschen ist es ganz einfach, in dem anderen wird ein Pointerarray übergeben. Wir reservieren zuerst einmal genügend Speicher mit NEW[], dann holen wir uns den Pointer, lesen das Array stückweise aus und löschen die Elemente von oben nach unten, denn die rutschen automatisch zusammen, am Ende nicht vergessen, den Speicher wieder freizugeben!
Das fertige Projekt: Tutorial6.zip
Zusätzliche Informationen und Funktionen | |||||||
---|---|---|---|---|---|---|---|
|
|