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