In questo articolo scriveremo una classe che possa aiutarci ad estrarre le risorse (icone, immagini, testo) dagli assembly che compongono la nostra applicazione. Questa classe è autonoma e pertanto può essere inserita in un progetto già esistente oppure se ne può creare uno apposta (magari con una nnostra libreria di classi del genere).
Per lo scopo didattico dell’articolo la classe sarà limitata all’estrazione di risorse di tipo icona (System.Drawing.Icon), ma può facilmente essere estesa per estrarre altri tipi di risorse (bitmap, stringhe, file mp3…).
Nel progetto che pereferiamo creiamo una nuova classe che chiameremo ResourceManager; come al solito prima della definizione della classe inseriamo le Imports necessarie:
Imports System.Drawing Imports System.IO Imports System.Reflection Imports System.Text Public Class ResourceManager End Class
- La namespace System.Drawing contiene la classe Icon, che è l’oggetto della nostra estrazione.
- La namespace System.IO contiene le classi Stream e MemoryStream per mezzo delle quali leggiamo dall’assembly la risorsa e la “salviamo in memoria”, per così dire.
- La namespace System.Reflection contiene la classe Assembly di cui sfrutteremo alcuni metodi statici per accedere alle risorse dell’assembly.
Ho accennato al fatto che la classe può essere utilizzata per estrarre qualsiasi risorsa che sia stata inclusa in un assembly; a questo scopo definiamo un enumerativo che rappresenti i possibili tipi di risorsa estraibili:
Public Enum ResourceType Icon = 0 ' est. = ICO ' Aggiungere qui i tipi interessati: ad ogni tipo corrisponde un estensione del file End Enum
Normalmente preferisco definire gli enumerativi al di fuori della classe cui si riferiscono, ma si tratta di una mia scelta opinabile. Entrando invece decisamente nel codice della classe, possiamo a questo punto definirne i costruttori:
Public Sub New() m_Source = [Assembly].GetExecutingAssembly() End Sub Public Sub New(ByVal executablePath As String) m_Source = [Assembly].LoadFrom(executablePath) End Sub Public Sub New(ByVal sourceAssembly As [Assembly]) m_Source = sourceAssembly End Sub
Analizziamo un momento i tre costruttori:
- Usando il primo costruttore, senza parametri, le risorse verranno cercate all’interno dello stesso assembly in cui si trova la classe ResourceManager.
- Per usare il secondo costruttore è invece necessario passare come parametro il percorso completo all’assembly che contiene le risorse.
- Il terzo costruttore, infine, è forse quello più utile, dal momento che accetta come parametro l’assembly che contiene la risorse: in questo modo possiamo utilizzare la classe per estrarre risorse che si trovino in un qualsiasi assembly referenziato dall’applicazione anche se non ne conosciamo la locazione fisica nel file-system.
Abbiamo visto che i costruttori valorizzano una variabile m_Source: si tratta di una variabile privata di tipo System.Reflection.Assembly, resa accessibile all’esterno per mezzo di una proprietà:
Private m_Source As System.Reflection.Assembly Public Property Source() As System.Reflection.Assembly Get Return m_Source End Get Set(ByVal Value As System.Reflection.Assembly) m_Source = Value End Set End Property
Infine definisco una proprietà (in sola lettura) Errors che riporti gli eventuali errori durante l’estrazione delle risorse:
Private m_Errors As New Collections.Specialized.StringCollection Public ReadOnly Property Errors() As Collections.Specialized.StringCollection Get Return m_Errors End Get End Property
Bene, adesso viene il codice di estrazione vero e proprio: ho cercato di suddividerlo il più possibile per rendere più comprensibile il compito di ciascun pezzo di codice:
Public Function GetIcon(ByVal name As String) As Icon Dim SourceStream As Stream Dim ResourceStream As MemoryStream Dim bmp As Bitmap ' Variabile coerente con il tipo restituito dal metodo Dim result As Icon Errors.Clear() Try SourceStream = Source.GetManifestResourceStream(FormattaNomeRisorsa(name, type)) If Not IsNothing(SourceStream) Then ' Il tipo passato alla FormattaNomeRisorsa deve essere coerente con il tipo restituito dal metodo ResourceStream = ConvertIntoMemStream(SourceStream) End If If Not IsNothing(ResourceStream) Then ' La gestione della risorsa deve essere coerente con il tipo restituito dal metodo bmp = New Bitmap(ResourceStream) result = Icon.FromHandle(bmp.GetHicon) End If Catch ex As Exception Errors.Add(String.Format("Errore: {0} in {1}", ex.Message, ex.TargetSite.Name)) End Try Return result End Function
Private Function ConvertIntoMemStream(ByVal InputStream As Stream) As MemoryStream Dim reader As BinaryReader Dim writer As BinaryWriter Dim result As MemoryStream Dim index As Long result = Nothing Try result = New MemoryStream result.SetLength(InputStream.Length) result.Position = 0 reader = New BinaryReader(InputStream) writer = New BinaryWriter(result) For index = 1 To InputStream.Length writer.Write(reader.ReadByte()) writer.Flush() Next Catch ex As Exception Errors.Add(String.Format("Errore: {0} in {1}", ex.Message, ex.TargetSite.Name)) End Try Return result End Function
Private Function FormattaNomeRisorsa(ByVal NomeRisorsa As String, ByVal type As ResourceType) As String Dim result As New StringBuilder result.AppendFormat("{0}.{1}", Source.GetName.Name, NomeRisorsa) Select Case type Case ResourceType.Icon result.Append(".ICO") End Select Return result.ToString() End Function
Tanto per fare gli originali, cominciamo dalla fine: la routine FormattaNomeRisorsa restituisce il nome di una risorsa anteponendogli il nome dell’assembly e posponendogli l’estensione abbinata a quella risorsa (ICO per le icone, ad esempio).
La routine GetIcon estrae dall’assembly indicato come origine (proprietà Source) lo stream associato alla risorsa il cui nome le è stato passato come parametro; questo stream viene datio in pasto alla routine ConvertIntoMemoryStream che converte lo stream generico in uno stream di memoria.
Quest’ultimo infine verrà utilizzato per instanziare un oggetto di classe Bitmap; di questo oggetto viene invocato il metodo GetHIcon che restituisce il puntatore all’icona; questo puntatore viene passato come argomento al metodo (statico) FromHandle della classe Icon per ottenere l’icona vera e propria. Questo è quanto.
Volendo estendere la classe per aggiungere nuovi tipi di risorse, occorre intervenire in tre punti diversi della classe:
- Aggiungere un nuovo valore all’enumerativo ResourceType per ogni estensione gestita
- Aggiungere la gestione del nuovo valore nella routine FormattaNomeRisorsa
- Aggiungere un metodo pubblico che restituisca uno oggetto del tipo appena aggiunto nell’enumerativo; questo metodo sarà molto simile al metodo GetIcon appena descritto tranne che nei punti che ho messo in evidenza con commenti
Chiudo questo articolo ricordando velocemente i passi per includere una risorsa in un assembly (dando per scontato di averne aperto la solution in Visual Studio):
-
Nel Solution Explorer, selezionare il progetto, fare apparire il menu contestuale (tasto destro del mouse) e scegliere la voce Add | Add existing item
-
Nella dialog che apparirà selezionare il file da includere (es. TRAYMENU.ICO) e premere OK
-
Nel Solution Explorer, selezionare il file appena giunto e premere il tasto F4 per visualizzarne le proprietà.
-
Nel campo Build action scegliere la voce Embedded Resource al posto del valore Content che rappresenta il default
A questo punto possiamo ricompilare l’assembly per includere il file; si noti che se si fa uso di un code-obfuscator del codice IL è necessario informare l’eseguibile del fatto che questa risorsa non deve essere obfuscata.
Pingback: Un programma nella tray-bar – Il Gatto Informatico