LivingIndicator: .NET Compact Framework Control
Začínající programátoři kteří se pustili do křížku s potvůrkou zvanou .NET Compact Framework jistě ocení můj příspěvek do diskuze. Od první verze prošel Compact Framework dosti značným vývojem, nicméně i ve verzi 2.0 existuje mnoho věcí které mohou programátorům zvyklým na ten 'tlustý' framework chybět. Jednou z nich je jistě absence dostatečného množství ovládacích prvků.
Ve své praxi jsem se setkal s mnohými požadavky na funkčnost a ovládání programu pro Pocket PC a nyní i Windows Mobile platformu. Jedním z nich byl požadavek na vytvoření ovládacího prvku, který by indikoval běžící program aniž by zablokoval vstup z dotykové obrazovky. Compact Framework má samozřejmě možnosti jak toto udělat. Objekt Cursors.WaitingCursor je v mnoha případech použitelný, nikoli však v tomto, protože to je právě ten případ který zablokuje uživatelský vstup. Po pravdě řečeno, nebyl by problém pověsit na objekt Timer standardní ovládací prvek typu ProgressBar a nechat jej běhat dokola - pumpovat. Ovšem pro hračičky a ty z vás, kteří mají raději elegantnější způsoby tu mám toto řešení.
Řešení ukazuje, jak vytvořit ovládací prvek, který se podobá indikátoru ve Windows XP, nebo snad Windows 2000 při jejich bootování. Jedná se o dvoubarevný pruh, resp. pruh kde dvě barvy tvoří přechod a přecházejí jedna ve druhou a tento přechod rotuje. Nebudu se dále snažit popisovat vzhled tohoto ovládacího prvku, nebo se do toho zamotám. Modří jistě již vědí a ostatní se určitě dovtípí.
A jak toto vytvořit v podmínkách .NET Compact Frameworku 2.0?
Namespaces které budeme potřebovat jsou následující:
using System;
using System.Drawing;
using System.Windows.Forms;
V první řadě je třeba vytvořit vlastní třídu, která je poděděná ze třídy System.Windows.Forms.Control. Tuto třídu jsem nazval LivingIndicator. Do následujícího bloku zahrnuji její definici až po konstruktor, abych příspěvek příliš nekouskoval.
public partial class LivingIndicator : Control
{
// privátní bitmapy do kterých budeme kreslit
private Bitmap offScreenBitmap = null;
private Bitmap gradientBitmap = null;
// oblast ovládacího prvku
private Rectangle clientRectangle;
// Timer zajišťující animaci
private Timer indicatorTimer = null;
// aktuální pozice posunu bitmapy
private int currentPosition = 0;
// krok ve kterém se bitmapa bude posouvat
private const int SHIFT_VALUE = 20;
// konstruktor
public LivingIndicator()
{
InitializeComponent();
}
....
....
....
}
Konstruktor tohoto ovládacího prvku bude obsahovat jedinou funkci a tou je funkce InitializeComponent() kterou vám vygeneroval designer při založení projektu typu Control.
Další funkcí kterou jsem napsal je funkce, která vytvoří GDI objekt. Co je myšleno GDI objektem? Je to oblast kam umístíte ovládací prvek. Pro představu: ovládací prvek se skládá z obdélníkové oblasti dané šířky a výšky a bitmapy která vyplňuje tuto oblast. Šírku a výšku můžete měnit jak za běhu programu, tak hlavně v režimu návrhu, kdy ovládací prvek umisťujete na formulář. Ve chvíli kdy tyto hodnoty změníte, musí se zmenit i velikost bitmapy kterou vykreslujete a tak funkce provádí svůj vnitřní kód jen v případě že se změnily proporce ovládacího prvku.
Poznámka: Nedejte se zmýlit tím, že se bitmapa mění i v průběhu její rotace. Fakticky mění jen svou pozici a počátek vykreslení, nikoli však rozměry a pořadí poskládaných barev.
private void CreateGDIObjects()
{
// pokud se změnily proporce ovládacího prvku
if (clientRectangle == null || clientRectangle.Width != this.ClientRectangle.Width - 1 || clientRectangle.Height != this.ClientRectangle.Height - 1)
{
// vytvoř novou klientskou oblast na základě rozměrů ovládacího prvku
clientRectangle = new Rectangle(0, 0, this.ClientRectangle.Width - 1, this.ClientRectangle.Height - 1);
// vytvoř novou bitmapu o rozměrech ovládacího prvku
gradientBitmap = new Bitmap(this.Width, this.Height);
// vytvoř objekt Graphics
Graphics gGradient = Graphics.FromImage(gradientBitmap);
// počet vertikálních linek (polovina proto, že přechod je tvořen vlastně dvěmi stejnými bitmapami otočenými proti sobě o 180 stupňů
int numOfLines = (this.ClientRectangle.Width) / 2;
// počáteční a koncová barva přechodu - vlastnosti si vypůjčím z bázové třídy (nač tvořit vlastní?)
Color foreColor = ForeColor;
Color backColor = BackColor;
// interval mezi barvami pro jednotlivé složky
float redInterval = (foreColor.R - backColor.R);
float greenInterval = (foreColor.G - backColor.G);
float blueInterval = (foreColor.B - backColor.B);
// koeficient kterým budeme násobit hodnoty jednotlivých složek v každém kroku vykreslení
float redCoef = redInterval / numOfLines;
float greenCoef = greenInterval / numOfLines;
float blueCoef = blueInterval / numOfLines;
// vykreslení základní bitmapy
for (int i = 0; i <= numOfLines; i++)
{
int cRed = (int)(BackColor.R + ((float)i * redCoef));
int cGreen = (int)(BackColor.G + ((float)i * greenCoef));
int cBlue = (int)(BackColor.B + ((float)i * blueCoef));
Color currentColor = Color.FromArgb(cRed, cGreen, cBlue);
gGradient.DrawLine(new Pen(currentColor), i, this.Height, i, 0);
gGradient.DrawLine(new Pen(currentColor), this.Width - i, this.Height, this.Width - i, 0);
}
}
}
Tak to je funkce pro vykreslení ovládacího prvku v klidovém stavu po té, co mu byly nastaveny hodnoty výšky a šířky. Další funkce kterou je nutné napsat, je funkce OnPaintBackground() našeho objektu. Modří jistě vědí že tato metoda je metodou bázové třídy a je volána vždy při překreslení ovládacího prvku. Vzhledem k tomu, že náš ovládací prvek si malujeme sami, není další překreslování žádoucí, takže naše funkce bude vypadat následovně:
protected override void OnPaintBackground(PaintEventArgs e)
{
}
Ano, funkce nebude mít ve svém těle vůbec nic. Tím zajistíme, že tato metoda v bázové třídě bude provádět to co potřebujeme, tedy nic.
Poznámka: Pokud si budete chtít vyzkoušet, jak by to vypadalo bez této metody, tak můžete místo toho abyste ji mazali, do jejího těla vložit řádek base.OnPaintBackground(e);
Další metodou bázové třídy kterou musíme přepsat je metoda OnPaint. Ta zajišťuje vykreslení ovládacího prvku které budeme iniciovat metodou Refresh.
protected override void OnPaint(PaintEventArgs pe)
{
// zavoláme si funkci na vytvoření základní bitmapy
CreateGDIObjects();
// vytvoření bitmapy do paměti - a zase jen v případě že neexistuje a nebo se změnily proporce´
if (offScreenBitmap == null || offScreenBitmap.Width != this.Width || offScreenBitmap.Height != this.Height)
offScreenBitmap = new System.Drawing.Bitmap(this.Width, this.Height);
// vytvoření objektu Graphics
Graphics offScreenGraphics = Graphics.FromImage(offScreenBitmap);
// definice posunutých oblastí (vysvětlení pod funkcí)
Rectangle leftShiftedRectangle = new Rectangle(0, 0, currentPosition, this.Height);
Rectangle rightShiftedRectangle = new Rectangle(currentPosition, 0, this.Width - currentPosition, this.Height);
// vykreslení obou bitmap podle posunu do paměti
offScreenGraphics.DrawImage(gradientBitmap, leftShiftedRectangle, this.Width - currentPosition, 0, currentPosition, this.Height, GraphicsUnit.Pixel, new System.Drawing.Imaging.ImageAttributes());
offScreenGraphics.DrawImage(gradientBitmap, rightShiftedRectangle, 0, 0, this.Width - currentPosition, offScreenBitmap.Height, GraphicsUnit.Pixel, new System.Drawing.Imaging.ImageAttributes());
// posunutí aktuální pozice
currentPosition = currentPosition + SHIFT_VALUE;
// v přílpadě že hodnota posunu přesáhne šířku ovládacího prvku vynuluj hodnotu pusunu
if (currentPosition > this.Width)
currentPosition = 0;
// vykreslení bitmapy z paměti na obrazovku
pe.Graphics.DrawImage(offScreenBitmap, 0, 0);
}
Tady dlužím vysvětlení ohledně posunutých bitmap. Řešil jsem vykreslení tak, že jsem si nakreslil do paměti dvě stejné bitmapy o poloviční šířce, které se liší pouze orientací barevného přechodu. Když bych tyto bitmapy přiložil k sobě, dostanu bitmapu která odpovídá tomu, co potřebuji vykreslit v základním postavení. Každý posun se řeší tak, že zkopíruji oblast této celkové bitmapy a vložím ji do paměťové bitmapy posunutou o požadovanou hodnotu. Debugováním tohoto příkladu sami zjistíte, jak tento ovládací prvek pracuje.
Nyní už jen zbývá vytvořit funkce, které zajistí start a zastavení animace tohoto indikátoru.
// Start animace
public void Start()
{
// inicializace nového objektu Timer
indicatorTimer = new Timer();
// nastavení handleru
indicatorTimer.Tick += new EventHandler(indicatorTimer_Tick);
// nastavení timeoutu, zde je to čtvrt vteřiny
indicatorTimer.Interval = 250;
// spuštění timeru
indicatorTimer.Enabled = true;
}
// zastavení animace
public void Stop()
{
// zastavení timeru a jeho likvidace
indicatorTimer.Enabled = false;
indicatorTimer.Dispose();
}
// Readonly property přes kterou můžeme zjistit zda je animace spuštěná
public bool IsRunning
{
get
{
if (indicatorTimer == null)
return false;
return indicatorTimer.Enabled;
}
}
// timer handler
void indicatorTimer_Tick(object sender, EventArgs e)
{
// tato metoda vyvolá OnPaint() event
this.Refresh();
}
Tak toto je vše. Pro pořádek ještě uvedu sekci kterou vygeneroval designer
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
}
a nyní už nezbývá než obládací prvek zkompilovat do DLL knihovny a pak jej přidat do Toolboxu Visual Studia.
Tohle byl ovládací prvek LivingIndicator krok za krokem. V mé verzi umí LivingIndicator zobrazovat ještě libovolný text přes běžící indikátor, ale to už je na vás, jak si jej vylepšíte. Samotný kód jistě není úplně dokonalý. Některé části jsem rozvedl pro větší srozumitelnost a naopak, zachytáváním případných vyjímek jsem se moc nezabýval. Holt, doba je rychlá a času málo. Pokud byste měli nějaké dotazy či připomínky, rád je uvítám.
Tím končím, tvá Máňa. 