Creare OSD Click-Through e animazioni con Windows Forms e C#
Mi sono ritrovato a dover creare un OSD (on-screen-display) ed ecco una miniguida su come realizzarlo utilizzando Windows Forms (io uso SharpDevelop, ma visual studio usa bene o male gli stessi controlli). Bando alle ciance e cominciamo a creare il nostro form.
Il Form Base
Per prima cosa è necessario eliminare il bordo finestra, l’icona sulla barra dei programmi,aggiungiamo un po’ di trasparenza e portiamolo sempre “in primo piano”:
- Click sul Form di base e dalle proprietà scegliamo “Border Style” -> “None”
- Ancora dalle stesse proprietà “Show In Taskbar” -> “False”
- Sempre sulle proprietà del form “Opacity” -> “85%”;
- “TopMost” -> “True”;
Ancora non abbiamo finito di impostare la nostra finestra, bisogna parlare un po’ di Teoria!
Windows non è proprio il massimo come trasparenze, ragion per cui ha più controlli sulle trasparenze: l’opacità, che si occupa di “mixare” i colori dei controlli con lo sfondo e la “transparency key”, cioè quel colore che viene interpretato dal gestore delle finestre come “questa zona non fa parte della finestra”.
La transparency key è un colore PREDEFINITO, non ammette sfumature e, tra l’altro non permette errori (se i valori rgb sono, ad esempio, R:1,G:0,B:0 e la transparency key è R:0,G:0,B:0 …la maschera di trasparenza non funzionerà!!!).
Detto ciò settiamo la “Transparency key” dalle proprietà del forms editor come “Bianco” (White).
Poi creiamo una nostra cornice (vi consiglio Inkscape perché fa veramente le cose per bene) oppure scarichiamo qualcosa da internet (che, possibilmente non contenga bianco).
Una volta ritagliato il tutto, sistemata la key, scegliamo questa immagine come sfondo della finestra e, cosa più importante, controlliamo che non ci siano pixel “orfani” di qui e di lì nell’immagine.
Aggiungiamo poi dei controlli come un’immagine (per un lettore audio la copertina del cd, per un programma di instant messaging l’immagine personale) e un paio di Label con formattazione testo.
Sul testo consiglio un buon Segoe UI come font perché si integra molto bene con Windows, lo sfondo dei nostri controlli dovrà essere precisamente come il Transparency key (WHITE nel nostro caso), altrimenti “risalteranno” rispetto agli altri.
Come potete vedere in questo screenshot ci sono dei contorni fastidiosissimi bianchi che ho sistemato poi con…PAINT.
Per fare ciò ho dipinto tutto lo sfondo di un colore con il riempimento (ad esempio ROSSO) così da contrastare i pixel orfani e poi con un po’ di pazienza ho utilizzato la matita bianca per correggerli (questione di pochi secondi).
Il codice
Adesso bisogna fare uso delle API native di windows.
Per fare ciò ci serviremo di questi due comandi:
[DllImport("user32.dll")] static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport("user32.dll", SetLastError=true)] static extern int GetWindowLong(IntPtr hWnd, int nIndex); const UInt32 WS_EX_TRANSPARENT = 0x00000020; const UInt32 WS_EX_LAYERED = 0x00080000;
Ovviamente da caricare nell’intestazione della classe!
La prima serve per settare le proprietà della finestra e la seconda per acquisirle… .
Ci serviremo, inoltre, di altri due-tre controlli di tipo Timer.
Torniamo alla parte teorica; una volta settata la finestra come “insensitiva” perdiamo il controllo della stessa poiché non è più sensibile agli eventi del mouse.
Ma ciò non può accadere: se, ad esempio, vogliamo cambiare il livello di trasparenza quando il mouse è posizionato sulla finestra.
Quindi, quando ciò accade, cominceremo ad osservare cosa fa il mouse e se esce dalla cornice del nostro OSD dovremo ridare sensibilità al controllo per poter intercettare nuovamente gli eventi del mouse.
Detto ciò, ci serviremo di due timer per le animazioni di Fade-In (comparsa) e Fade-Out (scomparsa) e uno per “tracciare” i movimenti del mouse quando la finestra è insensibile.
private Timer mouseTimer; private Timer fadeInTimer; private Timer fadeoutTimer;
Questi sono i controlli! Aggiungiamo l’evento “FormLoad” ed inizializziamoli:
void Form1Load(object sender, System.EventArgs e) { fadeInTimer = new Timer(); fadeInTimer.Interval = 20; fadeInTimer.Tick += delegate { FadeIn(); }; fadeoutTimer = new Timer(); fadeoutTimer.Interval = 20; fadeoutTimer.Tick += delegate { FadeOut(); }; mouseTimer = new Timer(); mouseTimer.Interval = 150; mouseTimer.Tick += delegate { onTick(); }; this.Location = new System.Drawing.Point(width-410,80); }
Adesso spiego tutti i parametri: il fade-in e fade-out timer andranno a chiamare i metodi FadeIn() e FadeOut() ogni 20 millisecondi, le animazioni devono essere molto veloci.
Il mouse timer, invece, può essere più lento per non sovraccaricare troppo la CPU: vanno bene anche 300ms… questione di gusti!
Adesso passiamo agli Event Handler, uno ad uno:
void Form1MouseEnter(object sender, EventArgs e) { SetWindowLong(this.Handle,-20,GetWindowLong(this.Handle,-20) | 0x00000020); if(!mouseTimer.Enabled) { mouseTimer.Start(); } if(!fadeoutTimer.Enabled) { FadeOut(); } }
Questo è il metodo di gestione dell’ingresso del mouse sul componente: SetWindowLong (lasciatelo così come l’ho scritto) si occupa di desensibilizzare la finestra. MouseTimer comincia a monitorare i movimenti del mouse e parte l’animazione di FadeOut che “sbiadisce” la finestra.
void onTick() { if (!(Cursor.Position.Y < this.Location.Y & Cursor.Position.Y > this.Location.Y + this.Height & Cursor.Position.X < this.Location.X && Cursor.Position.X > this.Location.X + this.Width)) { SetWindowLong(this.Handle,-20,GetWindowLong(this.Handle,-20) | 0x80000); mouseTimer.Stop(); FadeIn(); } }
OnTick è l’handler associato al “MouseTimer”.
La condizione “If” ci permette di capire se il mouse è all’interno o all’esterno del nostro componente (attenzione al NOT all’inizio).
Se il mouse esce dal componente bisogna chiamare SetWindowLong (così come l’ho scritto) per riattivare la sensibilità della finestra, a questo punto fermiamo il mouse timer e ridiamo opacità alla finestra con una bella animazione.
Ormai è tutto fatto mancano solo le animazioni:
public void FadeIn() { fadeInTimer.Start(); fadeoutTimer.Stop(); if(this.Opacity < 0.85) this.Opacity=this.Opacity + 0.05; else this.fadeInTimer.Stop(); } public void FadeOut() { fadeoutTimer.Start(); fadeInTimer.Stop(); if(this.Opacity > 0.15) this.Opacity=this.Opacity - 0.05; else this.fadeoutTimer.Stop(); }
Anche qui niente di ancestrale: il timer richiama l’event handler ogni 20 millisecondi finché non si raggiunge l’opacità desiderata (fermando eventualmente gli altri timer).