4 Zásady kreslení Formuláře jsou sice zručné, zvláště jsou-li naládované příhodnými ovládacími prvky, někdy však zabudované ovládací prvky1 nestačí na to, aby realizovaly nějaký stav vaší aplikace takový, jaký ho chcete mít. Pak si takový stav budete muset nakreslit sami. Kreslit se může na obrazovku, do souboru, na tiskárnu, ale ať už budete kreslit kamkoliv, budete stále zacházet se stejnými základními prvky – barvami, štětci, pery a písmy – a se stejnými druhy věcí, které máte nakreslit: tvary, obrázky a řetězce. Kapitolu začneme prozkoumáním základů kreslení na obrazovku a hlavních stavebních kamenů kreslení. Připomínám, že všechny kreslicí techniky, které se probírají zde a v příštích dvou kapitolách, se stejnou měrou týkají ovládacích prvků i formulářů. Informace o budování vlastních ovládacích prvků najdete v kapitole 8: Ovládací prvky. Než začneme, je žádoucí zmínit se o tom, že jmenný prostor System.Drawing je implementovaný nad GDI+ (Graphics Device Interface+), což je následník GDI. Původní GDI bylo hlavní oporou ve Windows už od dob, kdy vůbec byly nějaké Windows, a poskytovalo abstrakci nad obrazovkami a tiskárnami, aby bylo snadnější psát aplikace s grafickým uživatelským rozhraním, neboli ve stylu GUI (Graphics User Interface).2 GDI+ je DLL Win32 (gdiplus.dll), která se dodává s Windows XP a je k dispozici i pro straší verze Windows. GDI+ je také neřízená (unmanaged) knihovna tříd C++, která obaluje GDI+. Protože třídy ze System.Drawing sdílejí mnohdy stejné názvy s třídami C++ GDI+, klidně se může stát, že při prohlížení tříd .NET v online dokumentaci zakopnete o nějaké neřízené třídy. Jedná se o stejné pojmy, ale kódovací detaily jsou velmi odlišné, porovnáme-li neřízený C++ s čímkoli řízeným, takže mějte oči na stopkách.
Kreslení na obrazovku Bez ohledu na to, na jaký druh kreslení se chystáte, budete zacházet se stejnou podkladovou abstrakcí, s třídou Graphics ze jmenného prostoru System.Drawing. Třída Graphics poskytuje abstraktní povrch, na který kreslíte, ať už se výsledky vašich kreslicích operací zobrazí na obra-
139
140
Základy kreslení
zovce, uloží do souboru, nebo odešlou na tiskárnu. Třída Graphics je příliš obsáhlá na to, abych ji zde mohl předvést celou, ale budu se k ní v průběhu kapitoly mnohokrát vracet. Jedním ze způsobů, jak lze získat objekt grafiky, je zavolat CreateGraphics, čímž se vytvoří objekt grafiky sdružený s formulářem: bool drawEllipse = false; void drawEllipseButton_Click(object sender, EventArgs e) { // Indikátor, zda se bude kreslit elipsa nebo ne drawEllipse = !drawEllipse; Graphics g = this.CreateGraphics(); try { if( drawEllipse ) { // Nakreslí elipsu g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle); } else { // Smaže dříve nakreslenou elipsu g.FillEllipse(SystemBrushes.Control, this.ClientRectangle); } } finally { g.Dispose(); } }
Poté, co získáme objekt grafiky, můžeme s jeho pomocí kreslit na formulář. Protože tlačítkem přepínáme, zda se bude kreslit elipsa nebo ne, buď nakreslíme elipsu tmavě modrou, nebo použijeme stejnou barvu pozadí, jakou má formulář. To je asi všechno srozumitelné, ale možná se divíte, k čemu tam je ten blok try-finally. Protože objekt grafiky drží podkladový prostředek, který spravuje systém Windows, je na nás, abychom prostředek uvolnili, když skončíme, dokonce i tehdy, když dojde k nějaké výjimce, a to je důvod, proč jsme do kódu zařadili blok try-finally. Třída Graphics, podobně jako mnohé třídy v .NET, implementuje rozhraní IDisposable. Když nějaký objekt implementuje rozhraní IDisposable, je to pro klienta takového objektu signál, aby zavolal metodu Dispose rozhraní IDisposable, až s objektem skončí. Dá se tím objektu na vědomí, že nastal čas na uvolnění všech prostředků, které držel, například nějaký soubor nebo databázové připojení. V našem případě implementace metody Dispose rozhraní IDisposable třídy Graphics uvolní podkladový objekt grafiky, který udržovala. Daná záležitost se dá v C# zjednodušit tím, že se blok try-finally nahradí blokem using: void drawEllipseButton_Click(object sender, EventArgs e) { using( Graphics g = this.CreateGraphics() ) {
Základy kreslení
141
g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle); } // g.Dispose se zde zavolá automaticky }
Blok using C# obaluje kód, který obsahuje, blokem try a vždy na konci bloku zavolá metodu Dispose rozhraní IDisposable na objekt, který byl vytvořen v rámci klauzule using. Je to pohodlný zkrácený zápis pro programátory C#. Je to dobrá technika, kterou byste si měli osvojit. Budete se s ní ostatně dost často setkávat v průběhu knihy.
Zpracování události Paint Poté, co jsme se dozvěděli, jak správně spravovat prostředky Graphics, máme tu další problém: když se změní velikost formuláře, nebo když ho něčím pokryjeme, a pak zase odkryjeme, elipsa se automaticky nepřekreslí. Zařizuje se to tak, že Windows požádá formulář (a všechny dceřiné ovládací prvky), aby překreslil nově odkrytý obsah přes událost Paint, která poskytuje argument PaintEventArgs: class PaintEventArgs { public Rectangle ClipRectangle { get; } public Graphics Graphics { get; } } bool drawEllipse = false; void drawEllipseButton_Click(object sender, EventArgs e) { drawEllipse = !drawEllipse; } void DrawingForm_Paint(object sender, PaintEventArgs e) { if( !drawEllipse ) return; Graphics g = e.Graphics; g.FillEllipse(brush, this.ClientRectangle); }
V okamžiku, kdy se odpálí událost Paint, už je pozadí formuláře nakreslené 3, takže jakákoli elipsa, která byla nakreslena při předchozí události Paint, bude pryč; to znamená, že kreslit elipsu musíme jen tehdy, když je indikátor drawEllipse nastavený na true. Ovšem, i když indikátor nastavíme tak, že se má elipsa nakreslit, Windows se nedozví, že se stav indikátoru změnil, takže se událost Paint nespustí, a formulář nedostane šanci elipsu nakreslit. Abychom nemuseli mít kreslení elipsy v událostní proceduře Click, a zároveň také v události Paint, musíme požádat o událost Paint, a dát vědět systému Windows, že se formulář má překreslit.
142
Základy kreslení
Spouštění události Paint Spuštění události Paint si vyžádáme metodou Invalidate: void drawEllipseButton_Click(object sender, EventArgs e) { drawEllipse = !drawEllipse; this.Invalidate(true); // Požádáme Windows o událost Paint // pro formulář a jeho děti }
Nyní, když uživatel přepne náš indikátor, zavoláme Invalidate, abychom dali Windows na vědomí, že se nějaká část formuláře má překreslit. Protože je však kreslení jednou z nejnákladnějších operací, zpracuje Windows nejdříve jiné události – jako jsou pohyby myší, vstup z klávesnice atd. – teprve pak odpálí událost Paint, pro případ, že by bylo potřeba překreslit současně více oblastí na formuláři. Abychom se této prodlevy vyvarovali, můžeme zavolat metodu Update, kterou systém Windows donutíme, aby zpracoval událost Paint okamžitě. Protože se rušení platnosti a aktualizace celé klientské oblasti formuláře vyskytují běžně, mají formuláře metodu Refresh, která obě dvě metody kombinuje: void drawEllipseButton_Click(object sender, EventArgs e) { drawEllipse = !drawEllipse; // Dá se udělat jedno nebo druhé this.Invalidate(true); // Požádáme Windows o událost Paint // pro formulář a jeho děti this.Update(); // Donutí vykonat událost Paint hned teď // Nebo se dá udělat obojí najednou this.Refresh(); // Invalidate(true) + Update }
Jestliže však můžete počkat, je nejlepší nechat systém Windows, aby událost Paint zpracoval po svém. Její opoždění má svůj důvod: je to nejpomalejší věc, kterou systém dělá. Když se vynucuje, aby se všechno překreslovalo hned, eliminují se tím důležité optimalizace. Jestliže sledujete se mnou práci na naší jednoduché ukázce, možná jste potěšeni, že klikáním na tlačítko rozhodujete, zda bude na formuláři elipsa nebo ne, a že když formulář něčím zakryjete, a pak odkryjete, že se formulář překresluje podle očekávání. Když však budete postupně měnit velikost formuláře, budete patrně zděšeni tím, co uvidíte. Ilustrují to obrázky 4.1 a 4.2.
Základy kreslení
143
Obrázek 4.1: Elipsa na formuláři před jeho zvětšením
Obrázek 4.2: Elipsa na formuláři poté, co se formulář postupně zvětšuje
Na obrázku 4.2 to vypadá, jako kdyby se při zvětšování formuláře elipsa kreslila několikrát, ale ne celá, jen její části. Co se to děje? Když se formulář zvětšuje, kreslí systém Windows jen nově vystavený obdélník, a předpokládá, že existující obdélník není nutné překreslit. Takže i když překreslujeme během každé události Paint celou elipsu, Windows ignoruje vše, co se nachází vně regionu výřezu (clip region) – čímž se rozumí ta část formuláře, která se má překreslit – a to právě vede na to podivné kreslicí chování. Naštěstí můžeme nastavit styl při požadavku, aby Windows překreslil při zvětšování celý formulář: public DrawingForm() { // Required for Windows Form Designer support InitializeComponent(); // Spustí událost Paint, když se mění velikost formuláře this.SetStyle(ControlStyles.ResizeRedraw, true); }
Formuláře (a ovládací prvky) mají několik kreslicích stylů (dozvíte se o nich víc v kapitole 6: Kreslení pro pokročilé). Styl RedrawSize způsobí, že Windows překreslí celou klientskou oblast vždy, když se změní velikost formuláře. Je to samozřejmě méně efektivní, proto je výchozím chováním Windows to původní chování.
144
Základy kreslení
Barvy Doposud jsem kreslil elipsu ve svém formuláři zabudovaným štětcem tmavě modré barvy (DarkBlue). Štětec (brush), jak uvidíte, se používá pro vyplňování vnitřku tvarů, zatímco perem (pen) se kreslí hrany tvarů (obvod). Každopádně předpokládejme, že mě úplně neuspokojuje tmavě modrý štětec. Rád bych místo něj použil některou z více než 16 miliónů barev, které ale nebyly pro mě předem zabudované, takže to znamená, že nejprve musím konkretizovat barvu, o kterou se zajímám. Barvy se v .NET modelují přes strukturu Color: struct Color { // Bez barvy public static readonly Color Empty; // Zabudované barvy public static Color AliceBlue { get; } // ... public static Color YellowGreen { get; } // Vlastnosti public byte A { get; } public byte B { get; } public byte G { get; } public bool IsEmpty { get; } public bool IsKnownColor { get; } public bool IsNamedColor { get; } public bool IsSystemColor { get; } public string Name { get; } public byte R { get; } // Metody public static Color FromArgb(int alpha, Color baseColor); public static Color FromArgb(int alpha, int red, int green, int blue); public static Color FromArgb(int argb); public static Color FromArgb(int red, int green, int blue); public static Color FromKnownColor(KnownColor color); public static Color FromName(string name); public float GetBrightness(); public float GetHue(); public float GetSaturation(); public int ToArgb(); public KnownColor ToKnownColor(); }
Objekt Color v zásadě reprezentuje čtyři hodnoty: množství červené, zelené a modré, a množství neprůhlednosti. Na prvky červená, zelená a modrá se často odkazuje najednou jako na RGB
Základy kreslení
145
(red-green-blue). Každý z nich má rozpětí od 0 do 255, přičemž 0 je nejmenší množství barvy, 255 největší množství barvy. Stupeň neprůhlednosti se specifikuje hodnotou alpha, a někdy se přidává k RGB, takže vznikne ARGB (Alpha-RGB). Hodnota alpha má rozpětí od 0 do 255, přičemž 0 znamená zcela průhledná, 255 kompletně neprůhledná. Objekt Color nevytváříte konstruktorem, ale metodou FromArgb, do které předáte množství červené, zelené a modré barvy: Color Color Color Color Color
red = Color.FromArgb(255, 0, 0); // 255 červená, 0 zelená, 0 modrá green = Color.FromArgb(0, 255, 0); // 0 červená, 255 zelená, 0 modrá blue = Color.FromArgb(0, 0, 255); // 0 červená, 0 zelená, 255 modrá white = Color.FromArgb(255, 255, 255); // bílá black = Color.FromArgb(0, 0, 0); // černá
Chcete-li specifikovat úroveň průhlednosti, přidejte i hodnotu alpha: Color modra25ProcentNepruhledna = Color.FromArgb(255*1/4 255*1/4, 0, 0, 255); 255*3/4, 0, 0, 255); Color modra75ProcentNepruhledna = Color.FromArgb(255*3/4
Tři 8bitové hodnoty barev a jedna 8bitová hodnota alpha tvoří čtyři části jediné hodnoty, která definuje 32 bitovou barvu, jakou umějí zpracovat moderní adaptéry displeje. Předáváte-li raději uvedené čtyři hodnoty jako jedinou hodnotu, dá se to udělat jednou z přetížených variant, ale vypadá to odporně, proto byste se tomu měli vyhýbat: // A = 191, R = 0, G = 0, B = 255 Color modra75ProcentNepruhledna = Color.FromArgb(-1090518785);
Známé barvy Často má barva, o kterou se zajímáte, už přidělený dohodnutý název, což znamená, že je dostupná jako jeden ze statických členů Color, jimiž se definují „známé barvy“, z výčtu KnownColor, nebo názvem: Color blue1 = Color.BlueViolet; Color blue2 = Color.FromKnownColor(KnownColor.BlueViolet); Color blue3 = Color.FromName("BlueViolet");
Kromě 141 barev s názvy jako AliceBlue nebo OldLace, má výčet KnownColor 26 hodnot, které popisují aktuální barvy přiřazené různým částem uživatelského rozhraní Windows, jako jsou barva okraje aktivního okna nebo barva výchozího pozadí ovládacího prvku. Tyto barvy jsou velmi šikovné, když sami něco kreslíte a chcete, aby to bylo v souladu se zbývajícími částmi systému. Systémové barvy výčtu KnownColor jsou vypsané zde: enum KnownColor { // Nesystémové barvy jsem vynechal...
146
Základy kreslení
ActiveBorder, ActiveCaption, ActiveCaptionText, AppWorkspace, Control, ControlDark, ControlDarkDark, ControlLight, ControlLightLight, ControlText, Desktop, GrayText Highlight, HighlightText, HotTrack, InactiveBorder, InactiveCaption, InactiveCaptionText, Info, InfoText, Menu, MenuText, ScrollBar, Window, WindowFrame, WindowText, }
Chcete-li použít některou ze systémových barev, aniž byste museli vytvářet svou vlastní instanci třídy Color, přistupujte k těm, které už byly pro vás vytvořeny a vystaveny jako vlastnosti třídy SystemColors: sealed class SystemColors { // Vlastnosti public static Color ActiveBorder { get; } public static Color ActiveCaption { get; } public static Color ActiveCaptionText { get; } public static Color AppWorkspace { get; } public static Color Control { get; } public static Color ControlDark { get; } public static Color ControlDarkDark { get; } public static Color ControlLight { get; } public static Color ControlLightLight { get; } public static Color ControlText { get; } public static Color Desktop { get; } public static Color GrayText { get; }
Základy kreslení public public public public public public public public public public public public public public
static static static static static static static static static static static static static static
Color Color Color Color Color Color Color Color Color Color Color Color Color Color
147
Highlight { get; } HighlightText { get; } HotTrack { get; } InactiveBorder { get; } InactiveCaption { get; } InactiveCaptionText { get; } Info { get; } InfoText { get; } Menu { get; } MenuText { get; } ScrollBar { get; } Window { get; } WindowFrame { get; } WindowText { get; }
}
Následující dva řádky vedou na objekty Color se stejnými hodnotami barev, a můžete je používat, kde je vám libo. Color color1 = Color.FromKnownColor(KnownColor.GrayText); Color color2 = SystemColors.GrayText;
Překlad barev Máte-li nějakou svou barvu v jednom z tří jiných formátů – HTML, OLE nebo Win32 – nebo chcete barvu přeložit do jednoho z těchto formátů, využijte ColorTranslator, jak to vidíte v ukázce pro HTML: Color htmlModra = ColorTranslator.FromHtml("#0000ff"); string htmlTakyModra = ColorTranslator.ToHtml(htmlBlue);
Když máte nějaký objekt Color, můžete získat jeho hodnoty průhlednosti, červené, zelené a modré barvy, a také název barvy, ať už je to známá barva nebo systémová barva. Můžete také pomocí těchto hodnot vyplnit a obtáhnout tvary, k čemuž ale potřebujete štětce, resp. pera.
Štětce Třída System.Drawing.Brush slouží jako základní třída pro několik druhů štětců, které se používají podle toho, jaké jsou vaše potřeby. Na obrázku 4.3 vidíte pět odvozených tříd štětců, které poskytují jmenné prostory System.Drawing a System.Drawing.Drawing2D.
148
Základy kreslení
Obrázek 4.3: Ukázky štětců
Obrázek 4.3 byl vytvořen tímto kódem: void BrushesForm_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; int x = 0; int y = 0; int width = this.ClientRectangle.Width; int height = this.ClientRectangle.Height/5; Brush whiteBrush = System.Drawing.Brushes.White; Brush blackBrush = System.Drawing.Brushes.Black; using( Brush brush = new SolidBrush(Color.DarkBlue) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, whiteBrush, x, y); y += height; } string file = @"c:\windows\Santa Fe Stucco.bmp"; using( Brush brush = new TextureBrush(new Bitmap(file)) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, whiteBrush, x, y); y += height; } using( Brush brush = new HatchBrush( HatchStyle.Divot, Color.DarkBlue, Color.White) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, blackBrush, x, y); y += height; }
Základy kreslení
149
using( Brush brush = new LinearGradientBrush( new Rectangle(x, y, width, height), Color.DarkBlue, Color.White, 45.0f) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, blackBrush, x, y); y += height; } Point[] points = new Point[] { new Point(x, y), new Point(x + width, y), new Point(x + width, y + height), new Point(x, y + height) }; using( Brush brush = new PathGradientBrush(points) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, blackBrush, x, y); y += height; } }
Barevné štětce Štětec SolidBrush má namíchanou nějakou barvu, kterou se má vyplnit nakreslený tvar. Protože se tyto štětce používají velmi hojně, obsahuje kvůli většímu pohodlí třída Brushes 141 vlastností Brush, jednu pro každou barvu pojmenovanou ve výčtu KnownColors. Tyto vlastnosti jsou šikovné, protože jejich prostředky řídí a udržuje v cache samotný .NET, takže se s nimi pracuje poněkud snadněji než se štětci, které vytváříte sami 4: // Řídí .NET Brush bilyStetec = System.Drawing.Brushes.White; // Řídí váš program using ( Brush mujBilyStetec = new SolidBrush(Color.White) ) { ... }
Obdobně se 21 z 26 systémových barev výčtu KnownColor poskytuje ve třídě SystemBrushes 5. To se hodí, chcete-li vytvořit štětec s některou ze systémových barev, ale chcete, aby podkladový prostředek zpracovávaly WinForms. Štětce, které nejsou dostupné názvem jako vlastnosti SystemBrushes, jsou dostupné přes metodu FromSystemColor. Vrací štětec, který řídí .NET: // Volání Dispose na tento štětec způsobí výjimku Brush stetec = SystemBrushes.FromSystemColor(SystemColors.InfoText);
150
Základy kreslení
Štětce s texturou Štětec TextureBrush je vytvořen z nějakého obrázku. Standardně se obrázek používá opakovaně tak, aby „vydláždil“ prostor uvnitř nakresleného tvaru. Toto chování můžete změnit volbou členu výčtu WrapMode. Různé režimy předvádí obrázek 4.4. enum WrapMode { Clamp, // nakreslí pouze jednou Tile, // výchozí TileFlipX, // překlopí obrázek vodorovně podél osy X TileFlipY, // překlopí obrázek svisle podél osy Y TileFlipXY, // překlopí obrázek podél os X a Y }
Obrázek 4.4: Ukázky různých hodnot WrapMode u štětce s texturou