La mia prima classe - IniManag - passo per passo (da principiante)
a cura di Diego Cattaruzza (requisiti: conoscenza generica di VB)Premessa: In questo articolo intendo illustrare i passi concettuali che mi hanno portato alla creazione della mia prima classe - IniManag - per la gestione dei file di inizializzazione. Il modulo di classe è già stato messo a disposizione nell'Area DownLoad. Potete quindi evitare di "copiare" i passi di codice e scaricare direttamente il sorgente IniManag
Fin dall'inizio della mia storia di programmatore in Visual Basic (non molto lunga, in verità: ho cominciato con la versione 4), ho fatto uso di file di appoggio in cui scrivere e leggere informazioni. Intendo riferirmi ai file di inizializzazione, quelli con estensione .ini, nei quali trovavo estremamente comodo depositare e prelevare informazioni necessarie allo svolgimento delle mie applicazioni, cioè informazioni di impostazione o di stato, non dati - che preferivo mettere in tabelle.
All'inizio facevo degli "assaggi", delle "prove" e non mi curavo della riusabilità del codice. Sviluppavo delle applicazioni ricopiando ed adattando il codice già scritto per applicazioni precedenti. Mi sono trovato così ad avere circa una decina di applicazioni con la stessa funzione "LeggiIni" o "LeggInfo" o "ScriviIni" o "ScrivInfo" e le stesse dichiarazioni API dichiarate in diversi moduli .bas, creati solo a questo scopo. In queste funzioni facevo sempre le stesse operazioni, cambiavano solo alcuni parametri.
Quando ho cominciato a sviluppare applicazioni VB per la mia azienda ho cambiato approccio e ho riunito tutte le funzioni che riguardavano la gestione dei file di inizializzazione in un unico modulo .bas, che incorporavo in ogni mio nuovo progetto. In pratica, tornavo a fare una programmazione strutturata, come già avevo fatto con Clipper: diversi file sorgente in cui stavano diverse funzioni e diverse variabili pubbliche.
Ad un certo punto, cercando di imparare qualcosa di più su VB, mi sono detto: adesso provo a fare una classe. OK. Ma da quale comincio? Ho scelto IniManag perché era il modulo più diffuso tra i miei programmi.
A questo punto della trattazione è opportuno fare un inciso sulle funzioni API.
All'epoca non conoscevo il mondo della programmazione con le API, mi stavo appena appena avventurando nella programmazione ad oggetti, e delle API avevo timore.
Delle API per leggere/scrivere su file INI e sul loro uso ebbi notizia da una mia casuale escursione sull'area di discussione di Planet.Source.Code, sulla quale ero capitato nella mia ricerca di interlocutori su VB che ne sapessero qualcosa più di me. Non ricordo di chi era l'esempio che mi aiutò, ma ricordo che citava come propria fonte il sorgente del Setup1 di VB.
Le dichiarazioni API necessarie per scrivere/leggere in file di inizializzazione sono le seguenti (vengono citate solo quelle per l'ambiente a 32 bit):Declare Function WritePrivateProfileString Lib "kernel32" Alias "WritePrivateProfileStringA" _
(ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, _
ByVal lpString As Any, _
ByVal lpFileName As String) As Long
Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _
(ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, _
ByVal lpDefault As String, _
ByVal lpReturnedString As String, _
ByVal nSize As Long, _
ByVal lpFileName As String) As LongI parametri da fornire sono:
lpFileName corrisponde al nome del file.ini
lpApplicationName corrisponde a ciò che nel seguito verrà chiamato Sezione, prende questo nome perché nei file di inizializzazione Win.ini e System.ini c'erano, per molte applicazioni, delle sezioni apposite.
lpKeyName corrisponde a ciò che nel seguito verrà chiamato Chiave, designa quello che nel codice può essere definito una variabile, prende questo nome perché viene ricercata all'interno del file (chiave di ricerca, appunto).
lpReturnedString e lpString corrispondono a ciò che nel seguito verrà chiamato Valore, e designa appunto il valore che assume la chiave. A seconda della funzione in cui viene usata, viene definita returned o no, ad indicare che il valore viene scritto o "restituito".
- Nelle funzioni API, infatti, succede spesso che i risultati debbano essere ricercati nei parametri stessi che vengono loro passati, nel senso che tutti i parametri da passare ad una funzione API devono di solito venir dichiarati (DIMensionati) prima di passarli alla funzione. In effetti, alla funzione vengono passati il puntatore all'indirizzo di memoria dove è registrata la variabile-parametro (ecco cosa significa il prefisso "lp": long pointer).
lpDefault designa il valore di default che deve assumere la chiave ricercata, nel caso non venga trovata
nSize corrisponde a ciò che nel seguito verrà chiamato Buffer, ed indica il numero di caratteri a disposizione della lpReturnedString (la sua dimensione). (In realtà il termine buffer è improprio, ma in questo contesto è sufficiente per intenderci)Entrambe le funzioni restituiscono il numero di caratteri scritti o letti (0 in caso di errore).
Riprendendo il discorso momentaneamente interrotto, ecco un tipico esempio d'uso delle sopradescritte funzioni API, in una nelle mie prime applicazioni (stavolta non è nemmeno in funzioni a parte, ma addirittura in una sub che si occupava di gestire il cambio di causale di una spesa):
sezione = "corrente" chiave = "tasca" default = String(20, "0") stringa = default buffer = Len(stringa) + 1 i = GetPrivateProfileString(sezione, chiave, default, stringa, buffer, inifilename) stringa = Left(stringa, i) sezione = "ultima" i = WritePrivateProfileString(sezione, chiave, stringa, inifilename) chiave = "data" stringa = CStr(Date) buffer = Len(stringa) + 1 i = WritePrivateProfileString(sezione, chiave, stringa, inifilename)inifilename è una variabile pubblica che contiene il nome ed il percorso del file.ini dell'applicazione
il valore assegnato alla variabile default non ha senso: sarebbe stato sufficiente "0"
il valore assegnato alla variabile stringa avrebbe dovuto essere una sequenza di caratteri nulli
il valore assegnato alla variabile buffer è giusto, ma sarebbe stato migliore il codice seguente:sezione = "corrente" chiave = "tasca" buffer = 20 stringa = String(buffer,chr(0)) default = "0"Le dichiarazioni delle API erano in un modulo.bas, assieme ad altre funzioni d'uso generale, la funzione che ne faceva uso era invece nel modulo di una form, nella sub dell'evento Change di una TextBox. (Abbastanza sciocco, lo so, ma chiedo comprensione ai principianti: ero come loro ai primi passi e stavo facendo esperimenti, senza nessuna intenzione seria).
Come potete vedere anche ad un'occhiata superficiale, le variabili che intervengono sono sempre le stesse o quasi e le cose che si possono fare sono solo due: leggere o scrivere un valore.
E' opportuno a questo punto che vi mostri il file.ini cui fa riferimento il codice precedente:
[ultima] data=02/11/98 tasca=478500 [corrente] data=02/11/98 tasca=478500Il fatto che alcune righe sono eguali è la normale conclusione di una sessione di registrazione, nel corso della quale i valori sono via via diversi fino a coincidere, momento in cui la registrazione termina. A prescindere da questo fatto, mi preme indicare che ultima e corrente sono le sezioni, data e tasca sono le chiavi, 02/11/98 e 478500 sono i valori.
Come dicevo, quando mi accorsi che con i file ini facevo sempre le stesse cose, ed usavo sempre le stesse funzioni API, e le variabili che definivo erano sempre le stesse (sia pure con valori diversi di volta in volta), mi decisi a raggruppare tutto questo insieme di elementi in un modulo, che chiamai IniManag.bas.
Esso contiene le dichiarazioni API, una variabile pubblica per il nome del file.ini, e le funzioni LeggiIni e ScriviIni, le quali richiedono i parametri necessari alle API (meno il nome del file ini, già pubblico) e restituiscono la stringa del valore letto o il numero di caratteri scritti, rispettivamente.
Option Explicit Declare Function WritePrivateProfileString Lib "kernel32" Alias "WritePrivateProfileStringA" _ (ByVal lpApplicationName As String, _ ByVal lpKeyName As Any, _ ByVal lpString As Any, _ ByVal lpFileName As String) As Long Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String, _ ByVal lpKeyName As Any, _ ByVal lpDefault As String, _ ByVal lpReturnedString As String, _ ByVal nSize As Long, _ ByVal lpFileName As String) As Long Public IniFileName As String Public Function LeggiIni (sezione, chiave, default) As String Dim stringa As String, buffer As Long, ret As Long buffer = 255 stringa = String(buffer,Chr(0)) ret = GetPrivateProfileString(sezione, chiave, default, _ stringa, buffer, IniFileName) LeggiIni = Left$(stringa, ret) End Function Public Function ScriviIni (sezione, chiave, valore) As Long ScriviIni = WritePrivateProfileString(sezione, chiave, _ valore, IniFileName) End FunctionIn questo codice mancano le tipizzazioni dei parametri, cosa piuttosto importante quando si vuole scrivere del codice "universale". Qualche tempo dopo, corressi le dichiarazioni delle funzioni:
Public Function LeggiIni (sezione As String, chiave As String, default As String) As String Public Function ScriviIni (sezione As String, chiave As String, valore As String) As LongIn questo modo, a questo punto, avevo un modulo riutilizzabile nei miei vari programmi. Dovevo solo preoccuparmi di valorizzare la variabile IniFileName. Il resto mi permetteva di usare un codice più "umano" delle dichiarazioni API.
Il codice di quella sub che si occupava di gestire il cambio di causale di una spesa, diventava il seguente:stringa = LeggiIni("corrente", "tasca", "0") i = ScriviIni("ultima", "tasca", stringa) i = ScriviIni("ultima", "data", CStr(Date))Molto più leggibile e comprensibile, vero?
Ad un certo punto, mi sono accorto del fatto che la variabile pubblica IniFileName "viveva" per tutta la durata dell'applicazione. Per quanto poca fosse la memoria che occupava, era pur sempre uno spreco. Inoltre, in qualche applicazione avevo bisogno di consultare DUE file di inizializzazione diversi, uno sul computer locale dell'utente, e l'altro sulla directory condivisa del server. Quindi, come ho già scritto sopra, mi sono detto: adesso provo a fare una classe.
Mi sono chiesto se il discorso poteva "filare": una classe definisce un oggetto, il quale ha alcune caratteristiche (in OOP - programmazione orientata agli oggetti - si chiamano proprietà), e con il quale si possono fare alcune azioni (in OOP si chiamano metodi). Io avevo un oggetto, un "gestore di file.ini", con il quale dovevo "leggere" o "scrivere" su file.ini (e questi erano i metodi), di "qualsiasi" file.ini mi servivano sempre le stesse cose: nome del file, sezione, chiave (e queste erano le proprietà). Questo oggetto doveva fare da tramite tra l'applicazione che lo usava ed il file.ini dell'applicazione stessa, passando valori. Con questo bel ragionamento mi sono sentito giustificato a provare a fare una classe.
Quindi ho creato un modulo di classe e gli ho dato (per comodità) lo stesso nome del vecchio modulo.bas: IniManag.cls
Ho deciso che, oltre ai due metodi per leggere/scrivere nel file.ini, rispettivamente Leggi e Scrivi, il mio oggetto doveva avere solo due proprietà: una per il nome del file.ini e l'altra per la sezione.
Avrei potuto considerare anche una proprietà per la chiave, ma da un punto di vista di leggibilità preferivo scrivere codice simile all'assegnazione chiave = valore, con entrambi gli elementi sulla stessa riga.A questo punto non avevo altro da fare che copiare il codice dal mio IniManag.bas ed apportare poche variazioni: La dichiarazione delle API, a questo punto, poteva (anzi doveva, nell'ottica della OOP) essere Private.
Private Declare Function WritePrivateProfileString Lib "kernel32" Alias "WritePrivateProfileStringA" _ (ByVal lpApplicationName As String, _ ByVal lpKeyName As Any, _ ByVal lpString As Any, _ ByVal lpFileName As String) As Long Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String, _ ByVal lpKeyName As Any, _ ByVal lpDefault As String, _ ByVal lpReturnedString As String, _ ByVal nSize As Long, _ ByVal lpFileName As String) As LongHo aggiunto due costanti, giusto per avere un codice più letterale possibile (è solo uno sfizio): una per il massimo numero di caratteri nel buffer, una per lo zero.
Private Const maxl As Integer = 255 Private Const z As Integer = 0Per la stessa ragione di incapsulamento del codice, non avevo più bisogno della variabile pubblica IniFileName, bensì di due variabili visibili a tutto il modulo di classe ma non a tutto il progetto, quindi Private, per contenere i valori "interni all'oggetto" delle proprietà Nome e Sezione. Quindi ho dichiarato
Private pNomeFileIni As String Private pSezione As StringPer ciascuna delle proprietà, ho creato le funzioni Property Let/Get.
La funzione Property Let riceve un valore e lo assegna alla "variabile interna all'oggetto", la funzione Property Get restituisce il valore della stessa "variabile interna all'oggetto".
Semplice. Una volta capito è addirittura banale. (Quello che frega è il concetto, che si afferra solo se si cambia punto di vista).
Per l'assegnazione del nome del file.ini, ho aggiunto una comodità: gli posso passare il solo nome del file, senza l'estensione e senza il percorso, se manca uno di questi elementi, viene usato quello di default.Public Property Let nome(ByVal valore As String) pNomeFileIni = valore If InStr(pNomeFileIni, ".") = z Then pNomeFileIni = pNomeFileIni & ".ini" If InStr(pNomeFileIni, "\") = z Then pNomeFileIni = App.Path & "\" & pNomeFileIni End Property Public Property Get nome() As String nome = pNomeFileIni End Property Public Property Get sezione() As String sezione = pSezione End Property Public Property Let sezione(ByVal valore As String) pSezione = valore End PropertyPoi ho scritto le funzioni per leggere e scrivere, che a questo punto ho a buon diritto chiamato metodi Scrivi e Leggi. Sono praticamente uguali alle funzioni LeggiIni e ScriviIni, se si eccettua il parametro per la sezione (che adesso è una proprietà dell'oggetto) e l'uso delle costanti per definire il buffer.
Public Function scrivi(chiave As String, valore As String) As Long scrivi = WritePrivateProfileString(pSezione, chiave, valore, pNomeFileIni) End Function Public Function leggi(chiave As String, default As String) As String Dim valore As String, lret As Long valore = String$(maxl, z) lret = GetPrivateProfileString(pSezione, chiave, default, valore, maxl, pNomeFileIni) leggi = Left(valore, lret) End FunctionIl risultato finale è il modulo di classe IniManag.cls, che ho anche (suprema immodestia) scaricato nell'Area Download del nostro sito.
Il codice di quella sub che si occupava di gestire il cambio di causale di una spesa, diventa il seguente:Dim Ini As New IniManag With Ini .Nome = "MioFile" .Sezione = "corrente" stringa = .Leggi("tasca", "0") .Sezione = "ultima" i = .Scrivi("tasca", stringa) i = .Scrivi("data", CStr(Date)) End With Set Ini = NothingLa prima riga dichiara la variabile Ini come istanza di un nuovo oggetto definito dalla classe IniManag.
Di esso quindi posso impostare le proprietà ed eseguire i metodi, con la sintassi propria degli oggetti (nomeoggetto, punto, proprietà o metodo) e facendo il tipico uso del costrutto With...End With.Ho ottenuto così i vantaggi di un codice più leggibile, e di un più efficiente uso della memoria. Oltre alla soddisfazione di aver creato la mia prima classe.
Nota finale: Adesso è possibile (e caldamente consigliato da MicroSoft) immettere le informazioni di inizializzazione nel registro, con le funzioni VB Get/Save/Delete...Setting, ma io preferisco ancora i file.ini; soltanto, faccio ben attenzione a tenerli nella stessa cartella dell'applicazione, senza "intasare" la Windir.