c# a winforms

65
E N C Y K L O P E D I E Z O N E R P R E S S www.zonerpress.cz C# a WinForms C# a WinForms programování formulářů Windows programování formulářů Windows Chris Sells Chris Sells © Foto: Jiří Heller Microsoft .NET Development Series

Upload: zoner-software-as

Post on 10-Mar-2016

257 views

Category:

Documents


8 download

DESCRIPTION

C# a WinForms

TRANSCRIPT

Page 1: C# a WinForms

E N C Y K L O P E D I E Z O N E R P R E S S

www.zonerpress.cz

C# a WinForms C# a WinForms programování formulářů Windowsprogramování formulářů Windows

C h r i s S e l l sC h r i s S e l l s

© Foto: Jiří Heller

Microsoft .NET Development Series

Page 2: C# a WinForms

Chris Sells Chris Sells

C# a WinFormsC# a WinFormsprogramování formulářů Windows

M i c r o s o f t . N E T D e v e l o p m e n t S e r i e s

Page 3: C# a WinForms

Microsoft .NET Development SeriesWindows Forms Programming in C#Authorized translation from the English language edition, entitled WINDOWS FORMS PROGRAMMING IN C#, 1st Edition, 0321116208 by SELLS, CHRIS, published by Pearson Edication, Inc. publishing as Addison Wesley Professi-onal; Copyright © 2004 by Chris Sells. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. CZECH language edition published by ZONER SOFTWARE S.R.O. Copyright © 2005 by ZONER software s.r.o.

Autorizovaný překlad anglického vydání nazvaného WINDOWS FORMS PROGRAMMING IN C#, první vydání, 0321116208, autor SELLS, CHRIS, vydal Pearson Education, Inc. ve vydavatelství Addison Wesley Professional; Copyright © 2004 Chris Sells. Všechna práva vyhrazena. Žádná část této publikace nesmí být reprodukována nebo předávána žádnou formou nebo způsobem, elektronicky ani mechanicky, včetně fotokopií, natáčení ani žádnými jinými systémy pro ukládání bez výslovného svolení Pearson Education, Inc. České vydání ZONER SOFTWARE S.R.O. Copyright © 2005 ZONER software s.r.o.

C# a WinForms – programování formulářů WindowsAutor: Chris Sels

Copyright © ZONER software s.r.o. Vydání první v roce 2005. Všechna práva vyhrazena.

Katalogové číslo: ZR502

Zoner PressZONER software s.r.o.Nové sady 18/583, 602 00 Brno

Překlad: RNDr. Jan PokornýŠéfredaktor: Ing. Pavel Kristián

DTP: Pavel (Mr.Penguin) Kristián ml.

© Cover foto: Jiří Heller, HELLER.CZ s.r.o., www.heller.cz© Cover a layout: Ing. Pavel Kristián

Informace, které jsou v této knize zveřejněny, mohou byt chráněny jako patent. Jména produktů byla uvedena bez záruky jejich volného použití. Při tvorbě textů a vyobrazení bylo sice postupováno s maximální péčí, ale přesto nelze zcela vyloučit možnost výskytu chyb. Vydavatelé a autoři nepřebírají právní odpovědnost ani žádnou jinou záruku za použití chybných údajů a z toho vyplývajících důsledků. Všechna práva vyhrazena. Žádná část této publikace nesmí být reprodukována ani distribuována žádným způsobem ani prostředkem, ani reprodukována v databázi či na jiném záznamovém prostředku či v jiném systému bez výslovného svolení vydavatele, s výjimkou zveřejnění krátkých částí textu pro potřeby recenzí.

Veškeré dotazy týkající se distribuce směřujte na:

Zoner Press ZONER software s.r.o. Nové sady 18/583, 602 00 Brno

tel.: 532 190 883, fax: 543 257 245 e-mail: [email protected] http://www.zonerpress.cz

ISBN 80-86815-25-0

Page 4: C# a WinForms

Mé ženě Melisse,a bratrstvu Sells,

nebolimým synům Johnovi a Tomovi.

A mým rodičům, kteří ze mě už v kolébce udělali čtenáře,

a předali mi nějaký tajný spisovatelský gen,nad čímž dost žasnu.

Page 5: C# a WinForms

Přehled

1 Ahoj, formuláře Windows! 37 2 Formuláře 67 3 Dialogy 113 4 Zásady kreslení 139 5 Kreslení textu 187 6 Kreslení pro pokročilé 205 7 Tisk 227 8 Ovládací prvky 249 9 Integrace po dobu designu 297 10 Prostředky 361 11 Aplikace a nastavení 389 12 Sady dat a podpora Designéra 431 13 Vázání dat a mřížky dat 461 14 Uživatelská rozhraní s více vlákny 509 15 Rozmisťování přes web 535 A Přechod z MFC 571 B Delegáti a události 585 C Základy serializace 595 D Standardní komponenty a ovládací prvky Windows 605 Bibliografie 631 Rejstřík 634

6

Page 6: C# a WinForms

Obrázky 15Tabulky 25Předmluva 27Úvod 31

1 Ahoj, formuláře Windows! 37Aplikace WinForms úplně od začátku 37Formuláře Windows ve Visual Studiu .NET 42Uspořádávání ovládacích prvků 47Ovládací prvky 50Nastavení pro aplikaci 52Prostředky 55Dialogy 57Kreslení a tisk 59Vázání dat 61Uživatelská rozhraní s více vlákny 63Rozmisťování 63Přechod z MFC 64Kam jsme se dostali? 65

Poznámky v textu 65

2 Formuláře 67Zobrazování formulářů 67

Vlastnické a vlastněné formuláře 68Doba života formuláře 70Velikost a pozice formuláře 72

Omezování velikosti formuláře 77Pořadí podle osy z 78

Ozdoby formuláře 78Průhlednost formuláře 80

Obsah

7

Page 7: C# a WinForms

8

Formuláře, které nemají tvar obdélníka 81Nabídky formuláře 84

Kontextové nabídky 87Dceřiné ovládací prvky 88

Pořadí ovládacích prvků podle osy z 89Tabulátorové pořadí prvků 89Motivy ovládacích prvků 90Hostitelství ovládacích prvků COM 91

Rozvržení formuláře 93Automatická změna velikosti formuláře 93Kotvení 95Přichycování 97Přichycování a pořadí podle osy z 99Dělicí pruhy 100Seskupování 101Vlastní rozvržení 102

Vícedokumentové rozhraní 104Slučování nabídek 106

Vizuální dědění 109Kam jsme se dostali? 111

Poznámky 112

3 Dialogy 113Standardní dialogy 113Styly 115

Dynamické nastavování modálního a nemodálního chování 116Výměna dat 117

Zpracování tlačítek OK a Storno 118Data nemodálních formulářů 121

Ověřování platnosti dat 123Regulární výrazy a ověřování platnosti 124Oznamování formátu dat 125Pečlivé ověřování platnosti 126

Implementace nápovědy 128Vysvětlivky 128Využívání ErrorProvider pro všeobecné informace 128Zpracování tlačítka s otazníkem a klávesy F1 130Používání nápovědy HTML 131Kompilovaný HTML Help 134Práce s komponentou HelpProvider 136Zobrazení stránek Obsah, Rejstřík a Vyhledávat 137

Kam jsme se dostali? 137Poznámky 138

4 Zásady kreslení 139Kreslení na obrazovku 139

Zpracování události Paint 141Spouštění události Paint 142

Barvy 144Známé barvy 145

Obsah

Page 8: C# a WinForms

9

Překlad barev 147Štětce 147

Barevné štětce 149Štětce s texturou 150Šrafovací štětce 151Štětce s lineárním gradientem 152Štětce s gradientem založeným na cestě 153

Pera 156Tvary začátků a konců čar 157Přerušované čáry 159Zarovnání 160Spoje 160Vytváření per ze štětců 161

Tvary 162Křivky 163Vyhlazovací režimy 165Ukládání a obnovování nastavení grafiky 165

Cesty 166Režimy vyplňování 169

Obrázky 170Načítání a kreslení obrázků 170Změna měřítka, výřezy, záběry a zkosení 171Otáčení a překlápění 173Přebarvování 174Průhlednost 175Animace 176Kreslení do obrázků 179Ikony 181Kurzory 182

Kam jsme se dostali? 185Poznámky 185

5 Kreslení textu 187Písma 187

Vytváření písem 189Rodiny písem 190Charakteristiky písma 192Velikost písma 194

Řetězce 195Formátování 196Řetězce a cesty 203

Kam jsme se dostali? 204Poznámky 204

6 Kreslení pro pokročilé 205Stránkové jednotky 205

Převod pixelů na jednotky stránky 208Transformace 209

Změna měřítka 210Změna měřítka písem 211Otáčení 212

Obsah

Page 9: C# a WinForms

10

Posun počátku souřadnic 213Zkosení 215Kombinování transformací 215Výpomocné metody pro transformace 216Cesty transformací 217

Regiony 219Sestrojení a vyplnění regionu 219Oříznutí na velikost regionu 220Operace umožňující zkombinovat několik regionů 221

Optimalizované kreslení 222Dvojité bufferování 223Další kreslicí volby 224

Kam jsme se dostali? 225Poznámky 225

7 Tisk 227Tiskové dokumenty 227Kontroléry tisku 229

Náhled před tiskem 230Základní tiskové události 233Okraje 234Nastavení stránky 239Nastavení tiskárny 241

Rozsah tisku 243Příprava měr pro tisk 245

Kam jsme se dostali? 247Poznámky 247

8 Ovládací prvky 249Standardní ovládací prvky 249

Akční ovládací prvky 250Hodnotové ovládací prvky 251Ovládací prvky pro seznam 253Kontejnerové ovládací prvky 257Ovládací prvky pro seznamy obrázků 259Ovládací prvky kreslené vlastníkem 260

Vlastní ovládací prvky 265Odvozování přímo ze třídy Control 265Testování vlastních ovládacích prvků 266Realizace ovládacích prvků 268Ambientní vlastnosti 269Vlastní funkcionalita 272Zpracování vstupu v ovládacím prvku 274Zpracování zprávy Windows 280Ovládací prvky vybavené posouváním 282Rozšiřování existujících ovládacích prvků 284

Uživatelské ovládací prvky 285Přetahování myší 287

Cíl přetahování 288Zdroj, zahájení přetahovací operace 292

Obsah

Page 10: C# a WinForms

11

Kam jsme se dostali? 295Poznámky 295

9 Integrace po dobu designu 297Komponenty 297

Standardní komponenty 298Vlastní komponenty 300

Základy integrace v době designu 307Hostitelé, kontejnery a stanoviště 307Ladicí funkcionalita v době designu 309Vlastnost DesignMode 311Atributy 312Integrace prohlížeče vlastností 314Serializace kódu 317Dávková inicializace 321

Poskytovatelé rozšiřujících vlastností 324Konvertory typů 330

Konvertor objektu, který lze rozbalovat 336Editory typů v uživatelském rozhraní 341

Rozvírací editory typu v uživatelském rozhraní 344Modální editory typu v uživatelském rozhraní 346

Vlastní designéři 350Vlastnosti určené jen pro dobu designu 353Slovesa pro kontextovou nabídku komponenty v době designu 356

Kam jsme se dostali? 359Poznámky 359

10 Prostředky 361Základní pojmy týkající se prostředků 361

Manifestní prostředky 362Typové prostředky 366Manažer prostředků 370Prostředky Designéra 372

Lokalizace prostředků 375Informace o kultuře 376Sondování prostředku 377Lokalizace prostředku 379Lokalizace prostředku pro ty, kteří nejsou vývojáři 382Překrývání názvů prostředků 384Vstupní jazyk 386

Kam jsme se dostali? 387Poznámky 387

11 Aplikace a nastavení 389Aplikace 389

Doba života aplikace 390Kontext aplikace 391Události aplikace 393Výjimky vlákna uživatelského rozhraní 394Aplikace s jedinou instancí 396Předávání argumentů příkazového řádku 397

Obsah

Page 11: C# a WinForms

12

Aplikace SDI s více okny 401Prostředí 406

Nastavení pro dobu kompilace 406Nastavení prostředí 407

Nastavení 409Typy nastavení 409Soubory .config 410Dynamické vlastnosti 412Registr 414Speciální složky 419Nastavení a proudy 422Izolované úložiště 423Verze datových cest 427Volba mechanizmu pro nastavení 429

Kam jsme se dostali? 429Poznámky 430

12 Sady dat a podpora Designéra 431Sady dat 431

Získávání dat 432Vytváření dat 434Aktualizace dat 435Odstraňování dat 435Sledování změn 437Potvrzování změn 438Sady dat s více tabulkami 440Omezení 442Relace 442Navigace 443Výrazy 445

Podpora Designéra 446Objekty připojení 446Objekty příkazu 447Objekty datových adaptérů 448

Typové sady dat 449Vytvoření typové sady dat 449Omezení v typové sadě dat 451Relace v typové sadě dat 453Dopočítávané sloupce v typové sadě dat 454Přidání typové sady dat do formuláře 455

Kam jsme se dostali? 459Poznámky 459

13 Vázání dat a mřížky dat 461Vázání dat 461

Vázání a zdroje dat 462Jednoduché vázání dat k prvkům 465Jednoduché vázání na seznamy 468Jednoduché vázání na sady dat 469Manažeři vázání dat 470Aktuální řádek dat 476

Obsah

Page 12: C# a WinForms

13

Změny v sadě dat 476Změny v ovládacích prvcích 479Komplexní vázání dat 481Pohledy na data 484Relace hlavního záznamu s podrobnostmi 486

Mřížky dat 489Formátování mřížek dat 490Výměna dat a mřížky dat 493Dáme to všechno dohromady 493

Vlastní zdroje dat 494Vlastní prvkové zdroje dat 494Deskriptory typů a vázání dat 496Konverze typů 497Seznamové zdroje dat 502

Kam jsme se dostali? 506Poznámky 506

14 Uživatelská rozhraní s více vlákny 509Dlouhotrvající operace 509

Indikace průběhu operace 510Asynchronní operace 512Bezpečnost a vícevláknitost 514Zjednodušená vícevláknitost 519Stornování dlouhotrvající operace 520Komunikace se sdílenými daty 523Komunikace přes parametry metod 524Komunikace předáváním zprávy 525

Asynchronní webové služby 529Kam jsme se dostali? 533

Poznámky 533

15 Rozmisťování přes web 535Internet Explorer jako hostitel ovládacích prvků 535

Vytvoření ovládacího prvku 536Interakce s ovládacím prvkem 537

Zabezpečení přístupu ke kódu 539Kontrola povolení 541Udělování povolení 542

Bezdotykové rozmisťování 543Stažení aplikace 543Verze 545Soubory sdružené s aplikací 546

Úvahy o částečně důvěryhodných kompletech 550Povolení částečně důvěryhodných volajících 550Nastavení 552Vstup od uživatele 553Komunikace přes webové služby 554Čtení a zápis souborů 555Argumenty příkazového řádku 556Ladění NTD 558

Udělování většího rozsahu povolení 560

Obsah

Page 13: C# a WinForms

14

Udělování většího rozsahu povolení programátorsky 563Rozmisťování povolení 565

Authenticode 568Kam jsme se dostali? 569

Poznámky 570

A Přechod z MFC 571Několik slov o MFC 571MFC versus WinForms 573

Odlišnosti 574Strategie 580

Genghis 581Poznámky 583

B Delegáti a události 585Delegáti 585

Rozhraní 586Delegáti 587Statičtí odběratelé 588

Události 589Sklizeň všech výsledků (inkasovat od všech) 590Asynchronní oznámení: odpal a zapomeň 590Asynchronní oznámení: periodické zjišťování, zda už něco přišlo 591Asynchronní oznámení: delegáti 592

Pohoda v celém vesmíru 592

C Základy serializace 595Proudy 595Formátovače 598

Přeskočení neserializovaného členu 600IDeserializationCallback 600

ISerializable 601Verze dat 603

Poznámky 604

D Standardní komponenty a ovládací prvky Windows 605Definice komponenty a ovládacího prvku 606Standardní komponenty 607

Standardní dialogy 607Ikony na hlavním panelu 611Časovač 612Seznam obrázků 613Hlavní nabídky a kontextové nabídky 614

Standardní ovládací prvky 616Ovládací prvky, které nejsou kontejnery 616Kontejnerové ovládací prvky 628Poznámky 629

Bibliografie 631

Rejstřík 634

Obsah

Page 14: C# a WinForms

67

V technologii, která se jmenuje „Formuláře Windows – WinForms“ se dá očekávat, že stěžejní roli bude hrát formulář. V této kapitole prozkoumáme základy, tedy jak se formuláře zobrazí, co je to doba života formuláře, velikost a umístění formuláře, neklientské ozdůbky formuláře, na-bídky, dceřiné ovládací prvky, ale také vyspělejší témata, jako jsou průhledné formuláře, formu-láře, které nemají tvar obdélníka, rozvržení ovládacích prvků, formuláře MDI a vizuální dědění. A aby toho nebylo dost, je celá kapitola 3 věnována používání formulářů jako dialogů.

Něco z látky probírané v této kapitole – jmenovitě témata týkající se dceřiných ovládacích prvků, jako jsou kotvení a přichycování – se stejně dobře jako na formuláře dají aplikovat i na uživatelské ovládací prvky. Přestože je část látky společná oběma tématům, tak věci, které se běž-ně sdružují s formuláři, se probírají zde, témata spíše sdružovaná s ovládacími prvky se probírají v kapitole 8: Ovládací prvky.

Zobrazování formulářůJakýkoli formulář – tj. jakákoli třída, která je odvozená ze základní třídy Form – se dá zobrazit jedním ze dvou způsobů.

Nemodálně takto:

void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular() formular.Show(); // Zobrazí formulář nemodálně formular.Show(); // Zobrazí formulář nemodálně}

Takto se zobrazí modálně:

void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular()

2Formuláře

Page 15: C# a WinForms

68 Formuláře

formular.ShowDialog(); // Zobrazí formulář modálně formular.ShowDialog(); // Zobrazí formulář modálně}

Form. Show zobrazí nový formulář nemodálně a vrátí řízení okamžitě, aniž by vytvořila nějaký vztah mezi aktuálně aktivním oknem a novým formulářem. To znamená, že se existující formu-lář může zavřít, a nový formulář zůstane1. Naproti tomu Form. ShowDialog zobrazí formulář modálně a nevrátí řízení do té doby, dokud se vytvořený formulář nezavře, buď explicitně meto-dou Close nebo nastavením vlastnosti DialogResult (více o tom v kapitole 3: Dialogy).

Vlastnické a vlastněné formulářeKdyž metoda ShowDialog zobrazí nový formulář, použije jako logického vlastníka pro nový for-mulář aktuálně aktivní formulář 2. Vlastník ( owner) je okno, které připívá k chování vlastněného (owned) formuláře. Například, má-li nějaký vlastník modálního potomka, pak se aktivací vlast-níka, jako třeba přepnutí pomocí kláves Alt+Tab, aktivuje vlastněný formulář. V nemodálním případě, když je vlastnický formulář minimalizovaný nebo obnovený, bude takový i vlastněný formulář. Dále, vlastněný formulář se vždy zobrazuje před vlastnickým formulářem, i když je aktuálně vlastník aktivní, jako kdyby uživatel klikl na vlastníkovi, viz obrázek 2.1.

Obrázek 2.1: Vztah vlastník – vlastněný

Když se nějaký formulář aktivuje nemodálně, metodou Show, standardně nemá nový for-mulář vlastníka. Vlastník nemodálního formuláře se nastaví vlastností Owner nového formu-láře:

void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular() formular.Owner = this; // Zřídí vztah vlastník /vlastněný formular.Owner = this; // Zřídí vztah vlastník /vlastněný formular.Show();}

V modálním případě, navzdory implicitnímu vztahu vlastník – vlastněný, který WinForms vy-tvoří, bude mít modální formulář vlastnost Owner nastavenou na null, pokud se nenastaví ex-plicitně.

Page 16: C# a WinForms

Formuláře 69

To se dá udělat tak, že nastavíte vlastnost Owner těsně před voláním ShowDialog, nebo když předáte vlastnický formulář jako argument do překryté metody ShowDialog, která přebírá para-metr IWin32Window 3:

void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular() formular.ShowDialog(this); // předá jako argument vlastníka formular.ShowDialog(this); // předá jako argument vlastníka}

Formulář fungující jako vlastník může projít seznam formulářů, které vlastní, pomocí kolekce OwnedForms:

void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular(); formular.Owner = this; formular.Show();

foreach (Form ownedForm in this.OwnedForms) { foreach (Form ownedForm in this.OwnedForms) { MessageBox.Show(ownedForm.Text); } }}

Možná jste si při přidávání nepovinného vlastníka všimli, že formulář také může mít nepovinné-ho rodiče, což je vystaveno vlastností Parent. Normálně mají všechny normální formuláře vždy nastavenou vlastnost Parent na null. Jedinou výjimkou z tohoto pravidla jsou dceřiné formuláře MDI, které budu probírat později. Na rozdíl od vztahu vlastník-vlastněný, diktuje vztah rodič--potomek, že se bude ořezávat – tj. strana potomka bude zarovnaná se stranou rodiče, což vidíte na obrázku 2.2.

Vztah rodič-potomek je rezervovaný pro rodičovské formuláře (nebo kontejnerové rodičov-ské ovládací prvky) a dceřiné ovládací prvky (s výjimkou MDI, což se probírá později).

Obrázek 2.2: Dceřiný seznam (ListBox) zarovnaný podle klientské oblasti svého rodičovského formuláře

Page 17: C# a WinForms

70 Formuláře

Doba života formulářeTřebaže uživatel formulář neuvidí do té doby, dokud se nezavolá Show nebo ShowDialog, existu-je formulář od okamžiku, kdy se vytvoří jeho objekt. Nový objekt formuláře se probouzí v kon-struktoru objektu, kterého runtime volá, když se objekt vytváří poprvé. Během práce konstruk-toru se volá InitializeComponent, a tehdy se vytvářejí a inicializují všechny dceřiné ovládací prvky.

Není dobrý nápad strkat svůj vlastní kód do funkce InitializeComponent, protože ho Designér pravděpodobně vyhodí. Chcete-li ale přidávat další ovládací prvky, nebo změnit nějaká nastave-ní, která připravila InitializeComponent, můžete to udělat v konstruktoru. Jestliže jste si nechali počáteční implementaci formuláře vygenerovat některým z průvodců VS.NET, máte ve vygene-rovaném kódu dokonce komentář s pokynem, kam máte vkládat svůj inicializační kód:

public Form1() { // Required for Windows Form Designer support InitializeComponent();

// TODO: Add any constructor code after InitializeComponent call // TODO: Add any constructor code after InitializeComponent call

// Přidáme nový ovládací prvek Button jineTlacitko = new Button(); this.Controls.Add(jineTlacitko);

// Změníme hodnotu nějaké vlastnosti jineTlacitko.Text = "Něco, co není k mání v době návrhu";}

Když se zavolá Form.Show nebo Form.ShowDialog, je na formuláři, aby zobrazil sám sebe a všechny své dceřiné ovládací prvky. Chcete-li, můžete si nechat oznámit, že se to stalo, když vhodným způsobem zpracujete událost Load:

void InitializeComponent() { ... this.Load += new System.EventHandler(this.Form1_Load); this.Load += new System.EventHandler(this.Form1_Load); ...}

void Form1_Load(object sender, EventArgs e) {void Form1_Load(object sender, EventArgs e) { MessageBox.Show("Vítejte na formuláři Form1");}

Událost Load je prospěšná pro všechny závěrečné inicializace, které se mají udělat těsně předtím, než se formulář zobrazí. Událost Load je také vhodným místem pro změnu vlastností Visible a ShowInTaskbar. Ty se týkají viditelnosti, a hodí se, chcete-li mít na začátku formulář skrytý 4:

Page 18: C# a WinForms

Formuláře 71

void Form1_Load(object sender, EventArgs e) {

// nezobrazit formulář // nezobrazit formulář this.Visible = false; this.Visible = false; this.ShowInTaskbar = false; this.ShowInTaskbar = false;}

Když se formulář zobrazí, stane se aktivním formulářem. Je to aktivní formulář, který obdrží vstup z klávesnice. Neaktivní formulář se zaktivuje tehdy, když na něm uživatel klikne, nebo jinak indikuje systému Windows, že ho chce aktivovat – například se do něho přepne pomocí Alt+Tab. Neaktivní formulář můžete také aktivovat programátorsky metodou Form.Activate.5 Když se formulář učiní aktivním formulářem, včetně situace, kdy se poprvé načítá, obdrží udá-lost Activated:

void InitializeComponent() { ... this.Activated += new System.EventHandler(this.Form1_Activated); this.Activated += new System.EventHandler(this.Form1_Activated); ...}

void Form1_Activated(object sender, System.EventArgs e) {void Form1_Activated(object sender, System.EventArgs e) {

this.game.Resume();}

Má-li aplikace nějaký formulář, který je právě aktivním oknem z pohledu operačního systému, můžete to odhalit statickou vlastností Form.ActiveForm. Je-li null, znamená to, že žádný z for-mulářů aplikace není právě aktivní. Chcete-li sledovat deaktivace formuláře, zpracujte událost Deactivate:

void InitializeComponent() { ... this.Deactivate += new System.EventHandler(this.Form1_Deactivate); this.Deactivate += new System.EventHandler(this.Form1_Deactivate); ...}

void Form1_Deactivate(object sender, System.EventArgs e) {void Form1_Deactivate(object sender, System.EventArgs e) { this.game.Pause();}

Určovat můžete nejen to, zda má být formulář aktivní nebo ne, můžete také určovat jeho vidi-telnost. Buď metodami Hide a Show, které nastavují vlastnost Visible, nebo nastavit vlastnost Visible přímo:void hideButton_Click(object sender, System.EventArgs e) { this.Hide(); // Nastaví vlastnost Visible nepřímo

Page 19: C# a WinForms

72 Formuláře

this.Visible = false; // Nastaví vlastnost Visible přímo}

Jak asi očekáváte, existuje také událost, kterou se dají zpracovat situace, kdy formulář mizí z dohledu, nebo se chystá zjevit. Jmenuje se VisibleChanged. Všechny tři události, Activated, Deactivate a VisibleChanged se hodí pro restartování a pozastavování činností, které vyžadují interakci s uživatelem, nebo mají přitáhnout jeho pozornost, což je typické pro hry. Chcete-li činnosti úplně zastavit, zpracovává se buď událost Closing, nebo Closed. Událost Closing lze stornovat, pokud uživatel změní názor na to, co chtěl udělat:

void Form1_Closing(object sender, CancelEventArgs e) { DialogResult res = MessageBox.Show( "Ukončit hru?", "Hra běží", MessageBoxButtons.YesNo); e.Cancel = (res == DialogResult.No);}

void Form1_Closed(object sender, EventArgs e) { MessageBox.Show("Hru jste ukončili");}

Všimněte si, že během události Closing může zpracovatel nastavit vlastnost CancelEventArgs.Cancel na true, čímž se uzavření formuláře stornuje. Je to také nejlepší místo pro serializaci těch vlastností formuláře, které se týkají jeho vzhledu, například jeho velikosti a umístění, ještě dřív, než Windows formulář zavře. Naproti tomu je událost Closed v podstatě jen oznámení, že for-mulář už odešel do věčných lovišť.

Velikost a pozice formulářeJe pravděpodobné, že během svého života bude formulář zabírat na nějakém místě nějaký pro-stor. Počáteční pozici formuláře vládne vlastnost StartPosition, která může nabývat jedné z hod-not výčtu FormStartPosition:

enum FormStartPosition { CenterParent, CenterScreen, Manual, WindowsDefaultBounds, WindowsDefaultLocation, // výchozí}

Jednotlivé hodnoty znamenají následující chování:

• WindowsDefaultLocation. Startovací pozici formuláře určí systém Windows. Pokusí se najít takové místo počínaje od levého horního rohu obrazovky směrem k pravému dolní-

Page 20: C# a WinForms

Formuláře 73

mu rohu, aby se nová okna nezakrývala, ani se nedostala mimo obrazovku. Velikost formu-láře bude taková, jak byla v Designérovi nastavena vlastnost Size.

• WindowsDefaultBounds. Žádáte systém Windows, aby určil výchozí velikost i výchozí umístění.

• CenterScreen. Formulář se umístí do středu plochy (desktop), což je oblast, do které se nepočítá hlavní panel a podobné věci.

• CenterParent. Když se volala ShowDialog, umístí se formulář se do středu vlastníka (nebo aktuálně aktivního formuláře, není-li žádný vlastník). Jestliže se volala Show, bude chování jako u WindowsDefaultLocation.

• Manual. Umožňuje nastavit počáteční pozici i velikost formuláře ručně, bez jakýchkoli intervencí ze strany Windows.

Velikost a pozice formuláře jsou vystavené přes vlastnosti Size a Location, které jsou typu Size, resp. Point (obě ze jmenného prostoru System.Drawing). Vlastnosti určující velikost formuláře jsou také pro větší pohodlí vystaveny přes vlastnosti formuláře Height a Width (výška a šířka), vlastnosti určující pozici přes vlastnosti formuláře Left, Right, Top a Bottom (vlevo, vpravo, na-hoře a dole). Základní vlastnosti formuláře pro velikost a pozici vidíte na obrázku 2.3.

Obrázek 2.3: Vlastnosti DesktopLocation, Location, ClientSize a Size

Když se změní levý horní roh formuláře, je to posun, který se dá zpracovat ve zpracovateli událostí Move nebo LocationChanged. Když se změní výška nebo šířka formuláře, což je změna velikosti, dá se to zpracovat ve zpracovateli událostí Resize nebo SizeChanged 6. Někdy stačí je-diný pohyb myší, aby nastaly všechny události týkající se posunu a změny velikosti. Například, když změníte velikost formuláře tak, že táhnete jeho levý horní roh, měníte zároveň jeho velikost i pozici.

Pozice formuláře je v absolutních souřadnicích obrazovky. Zajímáte-li se o relativní souřad-nice formuláře vzhledem k ploše (desktop) – aby se, například, titulkový pruh vašeho formuláře nikdy nedostal za hlavní panel, (a byl jím tedy zakrytý) – ani když je hlavní panel Windows na-hoře, jak to vidíte na obrázku 2.3, využijte vlastnost DesktopLocation. Ukázka:

Page 21: C# a WinForms

74 Formuláře

private void Form1_Load(object sender, System.EventArgs e) { // Může skončit za hlavním panelem this.Location = new Point(1, 1);

// Vždy bude na ploše // Vždy bude na ploše this.DesktopLocation = new Point(1, 1); this.DesktopLocation = new Point(1, 1);

// Jednodušší zápis předchozího řádku this.SetDesktopLocation(1, 1);}

Pozice se vyjadřují pomocí struktury Point ze jmenného prostoru System. Drawing, jejíž zajíma-vé části jsou uvedeny zde:

struct Point { // členské proměnné public static readonly Point Empty;

// konstruktory public Point(int x, int y);

// vlastnosti public bool IsEmpty { get; } public int X { get; set; } public int Y { get; set; }

// metody public static Point Ceiling(PointF value); public void Offset(int dx, int dy); public static Point Round(PointF value); public virtual string ToString(); public static Point Truncate(PointF value);}

Struktura PointF je velmi podobná struktuře Point, používá se ale v kreslicích aplikacích, v nichž se požaduje přesnější měření v pohyblivé řádové čárce. Tu a tam budete potřebovat převádět z Point na objekt PointF, abyste mohli volat některé metody nebo nastavit některé vlastnosti. Dá se to udělat celkem bez námahy:

// Dá se přímo převádět z Point do PointF:Point pt1 = new Point(10, 20);PointF pt2 = pt1; // vede na PointF(10.0f, 20.0f)PointF pt2 = pt1; // vede na PointF(10.0f, 20.0f)

Page 22: C# a WinForms

Formuláře 75

Protože však čísla v pohyblivé řádové čárce obsahují přesnost navíc (ta se při konverzi ztratí), musíte při převodu PointF na objekt Point explicitně říct, jak se to má udělat, a to pomocí static-kých metod Truncate, Round, nebo Ceiling třídy Point:

// Musíte být explicitní, převádíte-li z PointF do Point:PointF pt1 = PointF(1.2f, 1.8f);Point pt2 = Point.Truncate(pt1); // vede na Point(1,1);Point pt2 = Point.Truncate(pt1); // vede na Point(1,1);Point pt3 = Point.Round(pt1); // vede na Point(1,2);Point pt3 = Point.Round(pt1); // vede na Point(1,2);Point pt4 = Point.Ceiling(pt1); // vede na Point(2,2);Point pt4 = Point.Ceiling(pt1); // vede na Point(2,2);

Velikost okna se odráží ve vlastnosti Size, která pochází také ze System.Drawing (Size má také protějšek SizeF, a poskytuje stejné schopnosti pro konverze):

struct Size { // členské proměnné public static readonly Size Empty;

// konstruktory public Size(int width, int height);

// vlastnosti public int Height { get; set; } public bool IsEmpty { get; } public int Width { get; set; }

// metody public static Size Ceiling(SizeF value); public virtual bool Equals(object obj); public static Size Round(SizeF value); public virtual string ToString(); public static Size Truncate(SizeF value);}

Přestože vlastnost Size reprezentuje velikost celého okna, není formulář zodpovědný za realizaci veškerého svého obsahu. Formulář může mít ohraničení, titulkový pruh, posuvníky, a to vše kreslí Windows. Část formuláře, za kterou je zodpovědný formulář, je klientská oblast, vyjadřo-vaná vlastností ClientSize, která je znázorněná na obrázku 2.3. Je docela vhodné ukládat si vlast-nost ClientSize mezi sezeními aplikace, protože je nezávislá na aktuálních nastaveních různých ozdůbek, které si zřídil uživatel. Obdobně, když měníte velikost formuláře tak, aby bylo zajištěno dost místa pro realizace celého formuláře, často se to zařizuje na základě klientské oblasti formu-láře, ne jeho kompletní velikosti:

private void Form1_Load(object sender, System.EventArgs e) { this.ClientSize = new Size(100,100); // Zavolá SetClientSizeCore this.ClientSize = new Size(100,100); // Zavolá SetClientSizeCore

Page 23: C# a WinForms

76 Formuláře

this.SetClientSizeCore(100, 100); this.SetClientSizeCore(100, 100);}

Rectangle ( obdélník) kombinuje Point a Size a má také protějšek RectangleF. Obdélník formulá-ře pro okna nejvyšší úrovně (ne pro dceřiná okna) relativně k obrazovce dává vlastnost Bounds, obdélník relativně k ploše vlastnost DesktopBounds. Vlastnost ClientRectangle je obdélník rela-tivně vzhledem k samotnému formuláři a popisuje klientkou oblast formuláře. Nejvíce se z těch-to tří vlastností užívá patrně ClientRectangle, když ne z jiných důvodů, tak proto, že popisuje, která oblast se použije, když se bude kreslit:

void Form1_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; g.FillEllipse(Brushes.Yellow, this.ClientRectangle); g.FillEllipse(Brushes.Yellow, this.ClientRectangle); g.DrawEllipse(Pens.DarkBlue, this.ClientRectangle); g.DrawEllipse(Pens.DarkBlue, this.ClientRectangle);}

Někdy je třeba převést nějaký bod, který je v souřadnicích relativně k obrazovce, na bod, který má souřadnice relativně ke klientské oblasti, nebo provést opačnou konverzi. Například, udá-lost HelpRequested, která se generuje, když uživatel klikne na tlačítko Help, a pak na nějaký ovládací prvek, se odesílá zpracovateli v souřadnicích obrazovky. Abyste však zjistili, na kterém ovládacím prvku uživatel klikl, musíte mít pozici myši v klientských souřadnicích. Mezi oběma systémy souřadnic se převádí pomocí PointToScreen a PointToClient:

void Form1_HelpRequested(object sender, HelpEventArgs e) { // Převede souřadnice vzhledem k obrazovce // Převede souřadnice vzhledem k obrazovce // na souřadnice vzhledem ke klientovi // na souřadnice vzhledem ke klientovi Point pt = this.PointToClient(e.MousePos); Point pt = this.PointToClient(e.MousePos);

// Vyhledá ovládací prvek, na kterém uživatel klikl foreach (Control control in this.Controls) { if (control.Bounds.Contains(pt) ) { Control ovladaciPrvekKteryPotrebujeNapovedu = control; ... break; } }}

Chcete-li konvertovat souřadnice celého obdélníka mezi oběma systémy souřadnic, můžete také použít RectangleToScreen a RectangleToClient.

Page 24: C# a WinForms

Formuláře 77

Omezování velikosti formulářeKdyž si pečlivě připravíte rozvržení ovládacích prvků, často je třeba pro ně zajistit určitý mini-mální prostor; nebo to diktují požadavky při jejich realizaci. Méně často je třeba, aby formuláře nepřekračovaly určitou maximální velikost (s tím může hodně vypomoci kotvení a přichycová-ní, která popíšu později). Každopádně je vždy možné nastavit minimální či maximální velikost formuláře pomocí vlastností MinimumSize, resp. MaximumSize. Následující ukázka nastaví pevnou výšku 200, minimální šířku 300, přičemž horní limit pro šířku není stanoven:

void Form1_Load(object sender, System.EventArgs e) { // minimální šířka bude 300, minimální výška 200 this.MinimumSize = new Size(300, 200); this.MinimumSize = new Size(300, 200);

// limit na maximální šířku není, maximální výška bude 200 this.MaximumSize = new Size(int.MaxValue, 200); this.MaximumSize = new Size(int.MaxValue, 200);}

Všimněte si, že se v kódu používá maximální hodnota celočíselného typu, čímž se vlastně říká, že žádný horní limit není.

Velikost a umístění formuláře řídí ještě jedna vlastnost, která se jmenuje WindowState, a mů-že nabývat jedné z hodnot výčtu FormWindowState:

enum FormWindowState { Maximized, Minimized, Normal, //výchozí hodnota Form.WindowState}

Standardně je WindowState nastavena na Normal, což znamená, že okno není maximalizované na celou plochu, ani není minimalizované, kdy formulář není vidět, a je jen v podobě tlačítka na hlavním panelu. Ve svém programu můžete tuto vlastnost získávat i nastavovat podle chuti, chcete-li ovlivňovat stav svého formuláře. Jestliže však ukládáte velikost a pozici svého formuláře mezi sezeními aplikace, možná se rozhodnete, že obnovíte WindowState na Normal, aby byla uloženou velikostí reprezentovaná velikost v normálním stavu, a ne minimalizovanou či maxi-malizovanou velikostí:

void Form1_Closing(object sender, CancelEventArgs e) { // Zachytí vlastnosti dřív, než formulář zmizí FormWindowState stav = this.WindowState; FormWindowState stav = this.WindowState; this.WindowState = FormWindowState.Normal; this.WindowState = FormWindowState.Normal; Point pozice = this.Location; Point pozice = this.Location; Size velikost = this.ClientSize; Size velikost = this.ClientSize;

// ... uloží stav, pozice a velikost mezi sezeními ...

Page 25: C# a WinForms

78 Formuláře

// ... obnoví vlastnosti v události Load ...}

Popis toho, jak a kde je vhodné udržovat nastavení aplikace mezi sezeními, viz kapitola 11: Aplikace a sezení.

Pořadí podle osy zDalší vlastností související s pozicí, kterou by mohli ovlivňovat uživatelé, nebo kterou byste mohli udržovat mezi sezeními, je vlastnost TopLevel. Doposud jsme se zabývali pozicí jen v jed-né rovině, podle os x a y. Když se však uživatel přepíná mezi okny, žongluje také systém Windows s pořadím podle osy z ( z-order). To diktuje, které okno bude před kterým.

Dále, pořadí podle osy z je rozděleno do dvou vrstev. Normální okna jsou zobrazena od nejmenšího pořadí podle osy z vpředu k největšímu vzadu. Nad všemi normálními okny jsou okna nejvyšší úrovně ( topmost windows), která se také kreslí relativně vzhledem k ostatním ok-nům nejvyšší úrovně, vpředu nejmenší pořadí podle osy z, vzadu největší, ale vždy se kreslí nad všemi normálními okny. Chcete-li se podívat na okno nejvyšší úrovně, stiskněte Ctrl+Shift+Esc. V mnoha verzích Windows se vám objeví před všemi ostatními okny okno Správce úloh ( Task Manager). Standardně je to okno nejvyšší úrovně a kreslí se vždy před všemi normálními okny, ať už je to aktivní okno nebo ne. Toto chování můžete změnit (já to dělám vždycky), když zrušíte zaškrtnutí políčka Vždy navrchu na stránce Obecné okna vlastností hlavního panelu (Options | Always On Top). Kdyby byl Správce úloh implementovaný pomocí WinForms, implementoval by zmíněný rys přepínáním vlastnosti TopMost svého hlavního formuláře.

Ozdoby formulářeKromě velikosti a pozice mají formuláře řadu dalších vlastností, které spravují různé další as-pekty jejich vzhledu a odpovídajícího chování. Následující nastavení ovlivňují neklientské ozdoby ( non-client adornments) formuláře: tedy ty části, které leží vně klientské oblasti, a které kreslí Windows.

• FormBorderStyle určuje, zda bude mít formulář ohraničení, zda se bude moci měnit jeho velikost, a zda má mít titulkový pruh v normální nebo ve zmenšené velikosti. Dobře na-vržené formuláře a dialogy ponechávají výchozí hodnotu Sizable. Dialogy, které jdou na nervy, mají změněnou hodnotu této vlastnosti na některou z voleb, kdy se velikost formu-láře nedá měnit. Programátoři obvykle volí možnosti, u nichž se nedá měnit velikost proto, že se obávají různých potíží s rozvržením, WinForms to však zvládají hladce, což proberu v kapitole později.

Kromě toho existují ještě dva styly pro okna nástrojů – jeden s pevnou, jeden s měnitel-nou velikostí – používají se při budování volně plovoucích oken ve stylu panelů nástrojů ( toolbars).

Page 26: C# a WinForms

Formuláře 79

• ControlBox je Booleovská hodnota určující, zda bude, nebo nebude mít formulář v levém horním rohu ikonu, a zda bude mít zároveň v pravém horním rohu tlačítko pro zavření formuláře. Je-li vlastnost nastavená na false, pak se nezobrazí ovládací nabídka ani kliknu-tím v levém horním rohu formuláře, ani kliknutím pravým tlačítkem na titulkovém pruhu. Obdobně, když má ControlBox hodnotu false, ignorují se vlastnosti MaximizeBox a Mi-nimizeBox, a jejich tlačítka se nezobrazí. Výchozí hodnota vlastnosti je true, ale často se nastavuje na false u modálních dialogů.

• Vlastnosti MaximizeBox a MinimizeBox určují, zda se v titulkovém pruhu formuláře zobrazí tlačítka pro maximalizaci, resp. minimalizaci formuláře. Výchozí hodnota obou vlastností je true, ale často se nastavují na false u modálních dialogů.

• Vlastnost HelpButton zobrazí vlevo od tlačítka pro zavření formuláře tlačítko s otazníkem, ale jen tehdy, je-li ControlBox nastavena na true, a obě vlastnosti MaximizeBox a Minimi-zeBox jsou nastavené na false.Výchozí hodnota této vlastnosti je false, ale často se zapíná na true u modálních dialogů. Když uživatel klikne na tlačítko s otazníkem, a pak někde jinde na formuláři, odpálí se událost HelpRequested formuláře, aby se uživateli mohla poskyt-nout nějaká nápověda. Ale bez ohledu na to, zda je vlastnost HelpButton nastavena na true nebo na false, událost HelpRequested se odpaluje vždy, když uživatel stiskne F1.

• Vlastnost Icon určuje obrázek použitý pro ikonu formuláře.

• Vlastnost SizeGripStyle povoluje hodnoty z výčtu SizeGripStyle: Auto, Hide nebo Show. Úchyt pro změnu velikosti (size grip) je prvek v pravém dolním rohu formuláře, který in-dikuje, že se velikost formuláře dá měnit. Výchozí hodnota je Auto, která vyjadřuje, že je úchyt v pravém dolním rohu „podle potřeby“, v závislosti na hodnotě vlastnosti FormBor-derStyle formuláře. Nastavení Auto rozhodne, že je úchyt zapotřebí tehdy, má-li formulář měnitelnou velikost, a je-li zobrazený modálně. Dále, má-li formulář stavový řádek, pak se vlastnost SizeGripStyle formuláře ignoruje, protože se dává přednost Booleovské vlastnosti SizingGrip samotného ovládacího prvku pro stavový řádek ( StatusBar).

• ShowInTaskbar je Booleovská vlastnost, která určuje, zda se hodnota vlastnosti Text for-muláře objeví jako tlačítko na hlavním panelu Windows. Výchozí hodnota vlastnosti je true, ale často se u modálních formulářů nastavuje na false.

Přestože je většina z výše uvedených vlastností na sobě nezávislých, nefungují souběžně všechny jejich možné kombinace. Například, je-li FormBorderStyle nastavena na jeden ze dvou stylů pro okna nástrojů, nezobrazují se tlačítka pro maximalizaci a minimalizaci, bez ohledu na to, jakou mají hodnotu odpovídající vlastnosti MaximizeBox a MinimizeBox. Co funguje, a co ne, zjistíte nejlépe experimentováním.

Page 27: C# a WinForms

80 Formuláře

Průhlednost formulářeKromě vlastností, které specifikují, jak má systém Windows realizovat neklientskou část for-muláře, poskytuje třída Form sadu vlastností, jimiž lze měnit vzhled formuláře jako celku, včetně toho, že může být průsvitný, nebo úplně průhledný, neviditelný.

Vlastnost, která řídí průhlednost celého formuláře, se jmenuje Opacity, a její výchozí hodnota je 1.0, neboli stoprocentně neprůhledný. Hodnota mezi 0.0 a 1.0 označuje stupeň průhlednosti na základě podpory tzv. alpha-blending 7 v modernějších verzích Windows, přičemž jakákoli hodnota menší než 1.0 znamená, že je formulář částečně průhledný (průsvitný). Průsvitnost je převážně salónní trik, je to ale docela zábavné a může potěšit, když uděláte okna nejvyšší úrovně méně nápadná a méně otravná, než jak by vypadala normálně. Viz ukázka:

void InitializeComponent() { ... this.Opacity = 0.5; this.Opacity = 0.5; this.Text = "Opacity = 0.5"; this.TopMost = true; ... }

void OpacityForm_Activated(object sender, EventArgs e) { timer1.Enabled = true;}

void timer1_Tick(object sender, EventArgs e) { if( this.Opacity < 1.0 ) this.Opacity += 0.1; if( this.Opacity < 1.0 ) this.Opacity += 0.1; this.Text = "Opacity = " + this.Opacity.ToString();}

void Form1_Deactivate(object sender, EventArgs e) { timer1.Enabled = false; this.Opacity = 0.5; this.Opacity = 0.5; this.Text = "Opacity = " + this.Opacity.ToString();}

Ukázka obsahuje kód formuláře nejvyšší úrovně, jehož vlastnost Opacity startuje na 50 %. Když se formulář aktivuje, začne tikat časovač, který při každém svém tiknutí zvýší hodnotu Opacity o 10 %, čímž se vyrobí hezký efekt „roztmívání“, který vidíte na obrázku 2.4. Když se formulář deaktivuje, nastaví se opět na poloprůhledný (50 %), takže bude dostatečně vidět na to, aby se dalo přečíst, co je na něm, a dalo se na něm klikat, ale nebude působit tak rušivě, jako kdyby byl neprůhledný.

Page 28: C# a WinForms

139

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 ta-kový, 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 kapito-lá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 implemento-vaný 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 rozhra-ní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á (unma-naged) 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 do-kumentaci 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 obrazovkuBez 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-

4Zásady kreslení

Page 29: C# a WinForms

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(); Graphics g = this.CreateGraphics(); try { 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 { finally { g.Dispose(); 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 po-už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() ) { using( Graphics g = this.CreateGraphics() ) {

Page 30: C# a WinForms

Základy kreslení 141

g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle); } // g.Dispose se zde zavolá automaticky } // 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 po-hodlný 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 PaintPoté, 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) {void DrawingForm_Paint(object sender, PaintEventArgs e) { if( !drawEllipse ) return; Graphics g = e.Graphics; 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 elip-sa, 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.

Page 31: C# a WinForms

142 Základy kreslení

Spouštění události PaintSpuš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 this.Invalidate(true); // Požádáme Windows o událost Paint // pro formulář a jeho děti // pro formulář a jeho děti }

Nyní, když uživatel přepne náš indikátor, zavoláme Invalidate, abychom dali Windows na vědo-mí, ž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 aktuali-zace 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é // Dá se udělat jedno nebo druhé this.Invalidate(true); // Požádáme Windows o událost Paint this.Invalidate(true); // Požádáme Windows o událost Paint // pro formulář a jeho děti // pro formulář a jeho děti this.Update(); // Donutí vykonat událost Paint hned teď this.Update(); // Donutí vykonat událost Paint hned teď

// Nebo se dá udělat obojí najednou // Nebo se dá udělat obojí najednou this.Refresh(); // Invalidate(true) + Update 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 zakry-jete, 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.

Page 32: C# a WinForms

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 // Spustí událost Paint, když se mění velikost formuláře this.SetStyle(ControlStyles.ResizeRedraw, true); 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í.

Page 33: C# a WinForms

144 Základy kreslení

BarvyDoposud 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

Page 34: C# a WinForms

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 red = Color.FromArgb(255, 0, 0); // 255 červená, 0 zelená, 0 modráColor green = Color.FromArgb(0, 255, 0); // 0 červená, 255 zelená, 0 modráColor blue = Color.FromArgb(0, 0, 255); // 0 červená, 0 zelená, 255 modráColor white = Color.FromArgb(255, 255, 255); // bíláColor black = Color.FromArgb(0, 0, 0); // černá

Chcete-li specifikovat úroveň průhlednosti, přidejte i hodnotu alpha:

Color modra25ProcentNepruhledna = Color.FromArgb(255*1/4255*1/4, 0, 0, 255);Color modra75ProcentNepruhledna = Color.FromArgb(255*3/4255*3/4, 0, 0, 255);

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 = 255Color 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 dostup-ná 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 sys-tému. Systémové barvy výčtu KnownColor jsou vypsané zde:

enum KnownColor { // Nesystémové barvy jsem vynechal...

Page 35: C# a WinForms

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; }

Page 36: C# a WinForms

Základy kreslení 147

public static Color Highlight { get; } public static Color HighlightText { get; } public static Color HotTrack { get; } public static Color InactiveBorder { get; } public static Color InactiveCaption { get; } public static Color InactiveCaptionText { get; } public static Color Info { get; } public static Color InfoText { get; } public static Color Menu { get; } public static Color MenuText { get; } public static Color ScrollBar { get; } public static Color Window { get; } public static Color WindowFrame { get; } public static Color WindowText { get; }}

Následující dva řádky vedou na objekty Color se stejnými hodnotami barev, a můžete je po-uží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áz-ce 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 mod-ré 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ětceTřída System.Drawing.Brush slouží jako základní třída pro několik druhů štětců, které se po-uží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.

Page 37: C# a WinForms

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 =Brush brush = new SolidBrush(Color.DarkBlue)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))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 = Brush brush = new HatchBrush( new HatchBrush( HatchStyle.Divot, Color.DarkBlue, Color.White)HatchStyle.Divot, Color.DarkBlue, Color.White) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, blackBrush, x, y); y += height; }

Page 38: C# a WinForms

Základy kreslení 149

using( Brush brush =Brush brush = new LinearGradientBrush( new LinearGradientBrush( new Rectangle(x, y, width, height), new Rectangle(x, y, width, height), Color.DarkBlue, Color.DarkBlue, Color.White, Color.White, 45.0f) 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)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 vlastnos-tí 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í .NETBrush bilyStetec = System.Drawing.Brushes.White;

// Řídí váš programusing ( 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 podklado-vý 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ýjimkuBrush stetec = SystemBrushes.FromSystemColor(SystemColors.InfoText);SystemBrushes.FromSystemColor(SystemColors.InfoText);

Page 39: C# a WinForms

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

Page 40: C# a WinForms

249

Základní jednotkou uživatelského rozhraní je ve WinForms ovládací prvek. Ovládacím prvkem je cokoliv, co komunikuje přímo s uživatelem v nějakém regionu definovaném nějakým kontej-nerem. Patří sem ovládací prvky, které dělají všechno samy, a také standardní ovládací prvky jako je textové pole (TextBox), uživatelské ovládací prvky (ovládací prvky, které obsahují jiné ovládací prvky), a dokonce i samotná třída formulářů, Form.

V kapitole probereme hlavní kategorie standardních ovládacích prvků, které poskytují WinForms. Vysvětlíme si, jak se budují vlastní ovládací prvky i uživatelské ovládací prvky, a také jak poskytnete podporu přetahování myší (drag and drop), což je nejpopulárnější druh vzájemné komunikace mezi ovládacími prvky. Chcete-li si udělat předběžný přehled o standardních ovlá-dacích prvcích, prolistujte dodatek D: Standardní komponenty a ovládací prvky WinForms.

Standardní ovládací prvky Ovládací prvek (control) je nějaká třída, která je odvozená ze základní třídy System.Windows.Forms.Control (buď přímo, nebo nepřímo) a je zodpovědná za kreslení nějaké části kontejneru, což je buď formulář, nebo jiný ovládací prvek. WinForms nabízejí řadu standardních ovládacích prvků, které jsou standardně dostupné v Toolboxu VS.NET. Dají se rozčlenit do několika ad-hoc kategorií:

• Akční ovládací prvky. Typickými představiteli jsou tlačítko nebo panel nástrojů ( Button, ToolBar). Existují proto, aby na nich uživatel mohl kliknout, což způsobí, že se něco stane.

• Hodnotové ovládací prvky. Některé hodnotové ovládací prvky, jako popisek a obrázek ( Label, PictureBox), zobrazují uživateli nějakou hodnotu, jako třeba text nebo obrázek, ale neumožňují mu hodnotu měnit.

Ovládací prvky8

Page 41: C# a WinForms

250 Ovládací prvky

Jiné hodnotové ovládací prvky, jako textové pole nebo prvek pro výběr data a času ( Tex-tBox, DateTimePicker), umožňují uživateli měnit zobrazenou hodnotu.

• Ovládací prvky pro seznam. Otevřený seznam a pole se seznamem ( ListBox, ComboBox) zobrazí uživateli seznam nějakých údajů. Jiné ovládací prvky z této kategorie, jako je mříž-ka dat ( DataGrid), umožňují uživateli přímo měnit údaje.

• Kontejnerové ovládací prvky. Jsou tři, rámeček skupiny prvků, panel a listovací rámeček ( GroupBox, Panel, TabControl). Existují proto, aby umožnily seskupovat do kontejneru, a v něm uspořádávat, jiné ovládací prvky.

Přestože se v dodatku D: Standardní komponenty a ovládací prvky WinForms vypisují i předvádějí jednotlivé standardní ovládací prvky, bude prospěšné, když se podíváme na spo-lečné schopnosti, které sdílejí ovládací prvky jednotlivých kategorií.

Akční ovládací prvkyAkční ovládací prvky jsou tlačítko, panel nástrojů, hlavní nabídka a kontextová nabídka (Button, ToolBar, MainMenu a ContextMenu).1 Existují proto, aby uživatel mohl tím, že na nich klikne, spustit v aplikaci nějakou akci. Každá z dostupných akcí má nějaký popisek a, v případě panelu nástrojů, může mít i nepovinný obrázek. Hlavní události akčních ovládacích prvků je událost Click:

void button1_Click(object sender, EventArgs e) { MessageBox.Show("Buch!");}

S výjimkou tlačítka (Button) jsou všechny ostatní ovládací prvky ve skutečnosti kontejnery pro více podobjektů, s nimiž teprve uživatel komunikuje. Například, objekt MainMenu obsahuje je-den nebo více objektů MenuItem, jeden pro každý prvek nabídky, který může odpálit událost Click:

void fileExitMenuItem_Click(object sender, EventArgs e) { this.Close();}

Ovládací prvek ToolBar (panel nástrojů) také obsahuje kolekci objektů, mají typ ToolBarButton. Když ovšem uživatel klikne, odešle se událost samotnému objektu ToolBar, takže zpracovatel události musí pomocí vlastnosti Button třídy ToolBarButtonClickEventArgs zjistit, na kterém tlačítku se kliklo:

void toolBar1_ButtonClick( object sender, ToolBarButtonClickEventArgs e) {

Page 42: C# a WinForms

Ovládací prvky 251

if( e.Button == fileExitToolBarButton ) { if( e.Button == fileExitToolBarButton ) { this.Close(); } } else if( e.Button == helpAboutToolBarButton ) { else if( e.Button == helpAboutToolBarButton ) { MessageBox.Show("Standardní ovládací prvky jsou super!"); } }}

Protože prvky nabídek a odpovídající tlačítka panelu nástrojů často spouštějí stejnou akci, jako že třeba zobrazí dialog „O něčem“, bývá dobrým zvykem kód centralizovat a volat ho z obou zpracovatelů událostí:

void FileExit() {...}void HelpAbout() {...}

void fileExitMenuItem_Click(object sender, EventArgs e) { FileExit();}

void helpAboutMenuItem_Click(object sender, EventArgs e) { HelpAbout();}

void toolBar1_ButtonClick(object sender, ToolBarButtonClickEventArgs e) { if( e.Button == fileExitToolBarButton ) { FileExit(); } else if( e.Button == helpAboutToolBarButton ) { HelpAbout(); }}

Když zpracování akce dáte na jediné místo, nebudete se muset starat o to, který ovládací prvek akci spustil; je jedno, kolik jich bude, všechny se budou chovat stejně.

Když už hovoříme o ovládacím prvku ToolBar, jste možná zvědaví, jak se jeho tlačítkům při-řadí obrázky. Chcete-li nějakému tlačítku na panelu nástrojů přiřadit obrázek, musíte ho uložit do komponenty ImageList a opatřit ho tam pořadovým číslem (indexem). Pomocí komponenty ImageList se připravují seznamy obrázků, které pak zobrazují jiné ovládací prvky. Seznamy ob-rázků probereme v kapitole později.

Hodnotové ovládací prvkyHodnotové ovládací prvky tvoří sadu ovládacích prvků, které slouží k zobrazení (a někdy také k editování) jediné hodnoty. Dají se dále rozčlenit podle datového typu hodnoty:

Page 43: C# a WinForms

252 Ovládací prvky

• Řetězcové hodnoty. Popisek, popisek simulující hypertextový odkaz, textové pole, tex-tové pole s formátováním a stavový řádek (Label, LinkLabel, TextBox, RichTextBox, StatusBar).

• Číselné hodnoty. Číselník, vodorovný a svislý posuvník, ukazatel průběhu a „reostat“ ( NumericUpDown, HScrollBar, VScrollBar, ProgressBar, TrackBar).

• Booleovské hodnoty. Zaškrtávací políčko a přepínač ( CheckBox, RadioButton).

• Datum a čas. Výběr data a času, několikaměsíční kalendář ( DateTimePicker, MonthCa-lendar).

• Grafické hodnoty. Obrázek, náhled před tiskem ( PictureBox, PrintPreviewControl).

Ovládací prvky pro řetězec vystavují vlastnost Text, která obsahuje hodnotu ovládacího prv-ku v řetězcovém formátu. Ovládací prvek Label (popisek) pouze zobrazí text. Ovládací prvek LinkLabel zobrazí text tak, jako kdyby to byl odkaz HTML, a odpálí jistou událost, když se na odkazu klikne. Ovládací prvek StatusBar zobrazí text stejně jako Label (až na to, že je stavový řádek standardně přichycený ke spodní straně svého kontejneru), umožňuje ale také rozčlenit text do několika panelů.

TextBox kromě toho, že zobrazí text, umožňuje uživateli také text editovat, a to buď v jedno-řádkovém, nebo ve více řádkovém režimu (závisí to na hodnotě vlastnosti Multiline). Ovládací prvek RichTextBox umožňuje editování podobně jako TextBox, ale podporuje také data RTF ( Rich Text Format), což zahrnuje formátování různými písmy a barvami, a také grafiky. Když se u těchto dvou ovládacích prvků změní hodnota vlastnosti Text, odpálí se událost TextChanged.

Všechny ovládací prvky pro číselné hodnoty vystavují číselnou vlastnost Value, jejíž hodno-ta může být v rozsahu od hodnoty vlastnosti Minimum až po hodnotu vlastnosti Maximum. Rozdíl v nich je jenom v tom, jaké uživatelské rozhraní chcete uživateli zobrazit. Když se změní hodnota vlastnosti Value, odpálí se událost ValueChanged.

Ovládací prvky pro Booleovské hodnoty – CheckBox a RadioButton – vystavují vlast-nost Checked, která odráží skutečnost, zda jsou zaškrtnuté nebo ne. Oba ovládací prvky pro Booleovské hodnoty lze ještě nastavit na třetí „mezilehlý“ ( indeterminate) stav, což je jeden ze tří možných stavů, které vystavuje vlastnost CheckedState. Když se Checked změní, odpálí se události CheckedChanged a CheckStateChanged.

Ovládací prvky pro datum a čas umožňují uživateli pohodlně vybírat jednu nebo více instancí typu DateTime. MonthCalendar umožňuje uživateli, aby si zvolil počáteční a koncové datum přes vlastnost SelectionRange (signalizuje to událost DateChanged). DateTimePicker umožňu-je uživateli zadat jediné datum a čas, což se vystavuje vlastností Value (a signalizuje to událost ValueChanged).

Ovládací prvky pro grafické hodnoty zobrazují obrázky, ale ani jeden z nich nepovoluje ob-rázky měnit. Ovládací prvek PictureBox zobrazí jakýkoli obrázek nastavený ve vlastnosti Image.

Page 44: C# a WinForms

Ovládací prvky 253

PrintPreviewControl zobrazí, stránku po stránce, náhled dat z objektu PrintDocument určených k vytištění (popisuje se v kapitole 7: Tisk).

Ovládací prvky pro seznamJe-li v daném okamžiku jedna hodnota fajn, musí být více hodnot najednou ještě lepší. Ovládací prvky pro seznam – ComboBox, CheckedListBox, ListBox, DomainUpDown, ListView, DataGrid a TreeView – umějí zobrazovat najednou více hodnot, ne pouze jednu.

Většina ovládacích prvků pro seznam – ComboBox, CheckedListBox, ListBox a DomainUp-Down zobrazují seznam objektů vystavený kolekcí Items. Nový prvek přidáte do seznamu právě prostřednictvím této kolekce:

void Form1_Load(object sender, EventArgs e) { listBox1.Items.Add("nějaký prvek"); listBox1.Items.Add("nějaký prvek");}

V ukázce se přidá do seznamu prvků řetězcový objekt, můžete tam ale přidat jakýkoli objekt:

void Form1_Load(object sender, EventArgs e) { DateTime datumNarozeni = DateTime.Parse("1995-08-30 6:02pm"); DateTime datumNarozeni = DateTime.Parse("1995-08-30 6:02pm"); listBox1.Items.Add(datumNarozenidatumNarozeni);}

Ovládací prvky pro seznam, které jako prvky přebírají objekty, volají metodu ToString, aby se nedostaly do konfliktu s tím, že mají zobrazit řetězec. Chcete-li v nějakém ovládacím prvku pro seznam zobrazit své vlastní prvky, implementujte prostě metodu ToString:

class Osoba { string jmeno; int vek;

public Osoba(string jmeno, int vek) { this.jmeno = jmeno; this.vek = vek; }

public string Jmeno { get { return jmeno; } set { jmeno = value; } } public int Vek { get { return vek; } set { vek = value; } }

Page 45: C# a WinForms

254 Ovládací prvky

public override string ToString() { return string.Format("{0} má {1} let", Jmeno, Vek); }}

void Form1_Load(object sender, EventArgs e) { Osoba[] kluci = { new Osoba("Tom", 7), new Osoba("John", 8) }; foreach( Osoba kluk in kluci ) { listBox1.Items.Add(kluk); }}

Instance vlastního typu zobrazené v ovládacím prvku ListBox vidíte na obrázku 8.1:

Obrázek 8.1: Vlastní typ zobrazený v ovládacím prvku ListBox

Protože ovládací prvek ListView umí zobrazovat prvky ve více sloupcích a stavech, plní se jeho kolekce Items instancemi třídy ListViewItem. Každý prvek má vlastnost Text, která repre-zentuje text v prvním sloupci, pak kolekci podprvků, která reprezentuje zbývající sloupce:

void Form1_Load(object sender, EventArgs e) { Osoba[] kluci = { new Osoba("Tom", 7), new Osoba("John", 8) }; foreach( Osoba kluk in kluci ) { // POZNÁMKA: předpokládá se, že kolekce Columns už má 2 sloupce ListViewItem item = new ListViewItem(); ListViewItem item = new ListViewItem(); item.Text = kluk.Jmeno; item.Text = kluk.Jmeno; item.SubItems.Add(kluk.Vek.ToString()); item.SubItems.Add(kluk.Vek.ToString()); listView1.Items.Add(item); listView1.Items.Add(item); }}

Vícesloupcový ListView naplněný tímto kódem vidíte na obrázku 8.2:

Obrázek 8.2: Vícesloupcový ListView

Page 46: C# a WinForms

Ovládací prvky 255

Ovládací prvek TreeView zobrazuje hierarchii prvků, které jsou instancemi typu TreeNode. Každý objekt TreeNode obsahuje text, nepovinné obrázky a kolekci Nodes ( uzly), která obsahu-je poduzly. Podle toho, do kterého uzlu přidáte, určíte, kde se přidaný uzel objeví ve stromové struktuře:

void Form1_Load(object sender, EventArgs e) { TreeNode rodicovskyUzel = new TreeNode(); rodicovskyUzel.Text = "Chris";

// přidá uzel do kořene stromu // přidá uzel do kořene stromu treeView1.Nodes.Add(rodicovskyUzel); treeView1.Nodes.Add(rodicovskyUzel);

TreeNode dcerinyUzel = new TreeNode(); dcerinyUzel.Text = "John";

// Přidá uzel pod existující uzel // Přidá uzel pod existující uzel rodicovskyUzel.Nodes.Add(dcerinyUzel); rodicovskyUzel.Nodes.Add(dcerinyUzel);}

Ovládací prvek TreeView naplněný tímto kódem vidíte na obrázku 8.3:

Obrázek 8.3: Rodičovský uzel a dceřiný uzel v ovládacím prvku TreeView

Ovládací prvek DataGrid přebírá svá data z kolekce nastavené pomocí vlastnosti DataSour-ce:

void Form1_Load(object sender, EventArgs e) { Osoba[] kluci = { new Osoba("Tom", 7), new Osoba("John", 8) }; dataGrid1.DataSource = kluci; dataGrid1.DataSource = kluci;}

DataGrid zobrazí všechny veřejné vlastnosti objektů v kolekci jako sloupce, jak ukazuje obrázek 8.4 na následující straně.

Obrázek 8.4: DataGrid zobrazující kolekci vlastních typů

Page 47: C# a WinForms

256 Ovládací prvky

DataGrid umí také zobrazovat hierarchická data i spoustu všelijakých fascinujících věcí. Mnoho podrobností se o ovládacím prvku DataGrid dozvíte v kapitole 13: Vázání dat a mřížky dat.

Výběr prvku ze seznamuVšechny ovládací prvky pro seznam vystavují nějakou vlastnost, která oznamuje aktuální výběr (nebo seznam vybraných prvků, pokud seznam podporuje vícenásobný výběr), a odpálí něja-kou událost, když se výběr změní. Například, následující kód zpracovává událost SelectedIndex-Changed ovládacího prvku ListBox a pomocí vlastnosti SelectedIndex zobrazí, který objekt je právě vybraný:

void listBox1_SelectedIndexChanged(object sender, EventArgs e) { // Získá vybraný objekt object selection = listBox1.Items[listBox1.SelectedIndex]; MessageBox.Show(selection.ToString());

// Objekt má pořád stejný typ, jako když jsme ho přidali Osoba kluk = (Osoba)selection; MessageBox.Show(kluk.ToString());}

Připomínám, že SelectedIndex je pozice v kolekci Items, ze které se tahá právě vybraný prvek. Prvek se vrátí jako typ „object“, ale prosté přetypování umožňuje s ním zacházet jako s instancí přesně téhož typu, jaký byl, když jsme prvek přidávali. To se právě hodí v případech, když nějaký vlastní typ zobrazuje data pomocí ToString, má ale jinou charakteristiku, jako třeba nějaký jedi-nečný identifikátor, který potřebujeme v programu.

Vskutku, u ovládacích prvků charakteru seznam, které neberou objekty, jako jsou TreeView a ListView, podporuje každý prvek vlastnost Tag, do které si můžeme odložit informaci o jedi-nečném ID:

void Form1_Load(object sender, EventArgs e) { TreeNode rodicovskyUzel = new TreeNode(); rodicovskyUzel.Text = "Chris"; rodicovskyUzel.Tag = "000-00-0000"; // uschováme si nějakou informaci navíc rodicovskyUzel.Tag = "000-00-0000"; // uschováme si nějakou informaci navíc}

void treeView1_AfterSelect(object sender, TreeViewEventArgs e) { TreeNode selection = treeView1.SelectedNode; object tag = selection.Tag; // vytáhneme si uschovanou informaci navíc object tag = selection.Tag; // vytáhneme si uschovanou informaci navíc MessageBox.Show(tag.ToString());}

Page 48: C# a WinForms

Ovládací prvky 257

Ovládací prvky pro seznam podporují buď vlastní typy, nebo vlastnost Tag, ale ne obojí. Je to myšleno tak, že obsahují-li seznamy instance vlastních typů, dají se jakékoli informace navíc snadno udržovat podle momentálních potřeb. Bohužel to, že není vlastnost Tag, poměrně ztěžu-je úlohu sdružit informaci o jednoznačném ID u jednoduchých typů, jako jsou řetězce. Ovšem poměrně prostý obal vám umožní, abyste přidali „značku“ ( tag) k prvku seznamu jakéhokoli druhu:

class TaggedItem { public object Item; public object Tag;

public TaggedItem(object item, object tag) { this.Item = item; this.Tag = tag; }

public override string ToString() { return Item.ToString(); }}

void Form1_Load(object sender, EventArgs e) { // Přidá dva označené řetězce comboBox1.Items.Add(new TaggedItem("Tom", "000-00-0000)); comboBox1.Items.Add(new TaggedItem("John", "000-00-0000"));}

void comboBox1_SelectedIndexChanged(object sender, EventArgs e) { TaggedItem selection = (TaggedItem)comboBox1.Items[comboBox1.SelectedIndex]; object tag = selection.Tag; MessageBox.Show(tag.ToString());}

Obal TaggedItem sleduje prvky a jejich značky. Metoda ToString umožňuje prvku rozhodnout, jak se má zobrazit a vlastnosti Item a Tag vystavují ty části objektu TaggedItem, které se mají použít při zpracování aktuálního výběru.

Kontejnerové ovládací prvkyZatímco ovládací prvky pro seznam obsahují několik objektů, úkolem kontejnerových ovláda-cích prvků ( GroupBox, Panel a TabControl) je udržovat v sobě více ovládacích prvků. Ovládací prvek Splitter (dělicí pruh) sám o sobě není kontejnerem, ale používá se pro změnu velikosti kontejnerů, které jsou přichycené k nějaké straně svého kontejneru.

Page 49: C# a WinForms

258 Ovládací prvky

Všechny principy kotvení, přichycování, dělicích pruhů a seskupování, které jsme probrali v kapitole 2: Formuláře, se také vztahují na kontejnerové ovládací prvky. Na obrázku 8.5 vidíte ukázku kontejnerových prvků v akci.

Obrázek 8.5: Kontejnerové ovládací prvky v akci

Na obrázku 8.5 je vlevo GroupBox, který je přichycený k levé straně svého kontejneru, což je formulář. Vpravo je ovládací prvek TabControl se dvěma stránkami (ovládací prvky TabPage) a uprostřed je ovládací prvek Splitter. Nápis na rámečku vlevo je hodnota vlastnosti Text ovládacího prvku GroupBox. TabControl je vlastně jen kontejner ovládacích prvků TabPage. To ovládací prvky TabPage obsahují jiné ovláda-cí prvky a hodnota jejich vlastnosti Text se zobrazuje jako popisek na záložkách stránek. Jedinou zbývající zajímavostí kontejnerových ovládacích prvků, o které je žádoucí se zmínit, je kolekce Controls, ve které se udržuje seznam ovládacích prvků obsažených v kontejneru. Například, se-znam na obrázku 8.5 se nachází v kolekci Controls rámečku:

void InitializeComponent() { ... // groupBox1 // groupBox1 this.groupBox1.Controls.AddRange( this.groupBox1.Controls.AddRange( new System.Windows.Forms.Control[] { new System.Windows.Forms.Control[] { this.listBox1}); this.listBox1}); ... ... // Form1 // Form1 this.Controls.AddRange( this.Controls.AddRange( new System.Windows.Forms.Control[] { new System.Windows.Forms.Control[] { this.tabControl1, this.tabControl1, this.splitter1, this.splitter1, this.groupBox1}); this.groupBox1}); ...}

V InitializeComponent formuláře si všimněte, že kolekce Controls rámečku obsahuje seznam a kolekce Controls formuláře obsahuje listovací rámeček, splitter a rámeček. Dceřiný kontejner určuje, jak bude daný ovládací prvek uspořádaný.

Například, je-li vlastnost Dock seznamu nastavena na Fill, vztahuje se přichycování k jeho bezprostřednímu kontejneru (rámeček), ne k formuláři, který ve skutečnosti ovládací prvek vy-tvořil. Když se nějaký ovládací prvek přidá do kolekce Controls nějakého kontejneru, stane se

Page 50: C# a WinForms

Ovládací prvky 259

tento kontejner rodičem tohoto dceřiného ovládacího prvku. Dceřiný ovládací prvek může zjis-tit svého rodiče svou vlastností Parent.

Ovládací prvky pro seznamy obrázkůOvládací prvky obvykle zobrazují data v podobě textu, některé z nich – TabPage, ToolBarBut-ton, ListView a TreeView – umějí také zobrazovat volitelné obrázky. Obrázky si berou z nějaké instance komponenty ImageList. Obrázky se dají do ImageList přidávat i v době návrhu pro-střednictvím Designéra, ovládací prvky, které obrázky využívají, je identifikují jejich pořadovým číslem (indexem).

Každý ovládací prvek, který je schopen zobrazovat obrázky, vystavuje jednu nebo více vlast-ností typu ImageList. Vlastnost se jmenuje „ImageList“, jestliže ovládací prvek podporuje jen jedinou sadu obrázků. Jestliže však ovládací prvek podporuje více než jeden seznam obrázků, obsahuje název vlastnosti frázi „ImageList“.

Například, TabControl vystavuje vlastnost ImageList pro potřeby všech ovládacích prvků TabPage, které jsou v něm obsažené, zatímco ovládací prvek ListView vystavuje vlastnosti Lar-geImageList, SmallImageList a StateImageList pro každý druh obrázků, které může zobrazovat (velké, malé, stavové).

Bez ohledu na počet vlastností ImageList, které ovládací prvek podporuje, tak, když nějaký prvek požaduje určitý obrázek ze seznamu obrázků, vystavuje prvek k tomuto účelu vlastnost, jejíž hodnotou je pořadové číslo ( index) obrázku v seznamu obrázků komponenty ImageList. Následující ukázka přidá ke všem prvkům ovládacího prvku TreeView obrázek:

void InitializeComponent() { ... this.treeView1 = new TreeView(); this.treeView1 = new TreeView(); this.imageList1 = new ImageList(this.components); this.imageList1 = new ImageList(this.components); ... ... // ImageList sdružený s TreeView // ImageList sdružený s TreeView this.treeView1.ImageList = this.imageList1; this.treeView1.ImageList = this.imageList1; ... ... // Obrázky načtené z prostředků formuláře // Obrázky načtené z prostředků formuláře this.imageList1.ImageStream = ...; this.imageList1.ImageStream = ...; ...}

void Form1_Load(object sender, EventArgs e) { TreeNode parentNode = new TreeNode(); parentNode.Text = "Chris"; parentNode.ImageIndex = 0; // Obrázek tatínkaparentNode.ImageIndex = 0; // Obrázek tatínka parentNode.SelectedImageIndex = 0; parentNode.SelectedImageIndex = 0; treeView1.Nodes.Add(parentNode); TreeNode childNode = new TreeNode();

Page 51: C# a WinForms

260 Ovládací prvky

childNode.Text = "John";childNode.Text = "John"; childNode.ImageIndex = 1; // Obrázek synka childNode.ImageIndex = 1; // Obrázek synka childNode.SelectedImageIndex = 1; parentNode.Nodes.Add(childNode);}

Když obrázky sdružujete s komponentou ImageList pomocí Designéra, uloží se obrázky v pro-středcích 2 specifických pro formulář. InitializeComponent si je při běhu vytáhne tím, že nasta-ví vlastnost ImageStream seznamu obrázků; InitializeComponent také sdruží seznam obrázků s vlastností ImageList ovládacího prvku TreeView. Každý uzel ve stromu podporuje dva indexy pro obrázky: výchozí obrázek a vybraný obrázek. Oba indexy se vztahují k pořadovým číslům obrázků ve sdruženém seznamu obrázků. Na obrázku 8.6 vidíte výsledek.

Obrázek 8.6: Stromová struktura (TreeView) využívající seznam obrázků (ImageList)

Když shromáždíte obrázky do komponenty ImageList, je pak přiřazení obrázků v tom ovláda-cím prvku, který je má zobrazovat, velmi prostá záležitost. Sdružíte vhodný seznam obrázků (nebo několik seznamů obrázků) s ovládacím prvkem a nastavíte jednotlivým prvkům indexy patřičných obrázků. O nakreslení obrázků se už postará ovládací prvek sám.

Ovládací prvky kreslené vlastníkemSeznamy obrázků umožňují vyzdobit některé ovládací prvky obrázky. Pokud byste měli rádi sami kontrolu nad kreslením nějakého ovládacího prvku, podporují to ovládací prvky kreslené vlastníkem. Ovládací prvek kreslený vlastníkem ( owner-draw control) poskytuje události, které umožňují vlastníkovi ovládacího prvku (nebo ovládacímu prvku samotnému), aby převzal zále-žitosti týkající se kreslení ovládacího prvku od podkladového operačního systému.

Ovládací prvky, které povolují kreslení vlastníkem – jako jsou nabídky, některé ovládací prv-ky pro seznam, listovací rámeček a panel stavového řádku – vystavují vlastnost, kterou se zapne kreslení vlastníkem, a pak se odpalují události, která dají kontejneru na vědomí, že by měl něco nakreslit. Například, ovládací prvek ListBox vystavuje vlastnost DrawMode, která může nabývat jedné z hodnot výčtu DrawMode:

enum DrawMode { Normal, // Ovládací prvek kreslí své vlastní prvky (výchozí) OwnerDrawFixed, // Vlastní kreslení všech prvků o pevné velikosti OwnerDrawVariable, // Vlastní kreslení všech prvků o proměnlivé velikosti }

Na obrázku 8.7 vidíte ovládací prvek ListBox kreslený vlastníkem. Když kreslí právě vybraný prvek, změní styl na kurzívu:

Page 52: C# a WinForms

431

Drtivá většina existujících, dokonce i nových aplikacích Windows je vybudována tak, že přistu-pují k nějaké databázi. Budujete-li takový druh aplikace (a je víc než pravděpodobné, že ano), budete potřebovat vědět, jak .NET podporuje přístup k poskytovatelům relačních dat, i jak je tato podpora integrovaná do VS.NET, aby se vám snadněji vyvíjely databázové aplikace WinForms.

Přístup k databázím je samozřejmě obrovité téma, které nelze probrat v ničem menším, než je celá kniha (možná by bylo zapotřebí několik svazků). V této kapitole budete proto absolvovat jen základy ADO.NET, což je část .NET Framework, která má na starost poskytování přístu-pu k myriádám poskytovatelů dat. Například, přestože budu v kapitole prozkoumávat sady dat, a budu vysvětlovat, jak je používat v aplikacích WinForms, vůbec se nebudu zabývat tzv. čtenáři dat. Čtenář dat může být užitečný, ale nepodporuje vázání dat WinForms, což je v aplikacích WinForms velmi populární technika (a je předmětem kapitoly 13: Vázání dat a mřížky dat).

Tato a příští kapitola vám umožní rychlý start s ADO.NET, ale jeho kompletní příběh, včet-ně všech těch zatracených podrobností, na to opravdu potřebujete nějakou jinou knihu.1

Sady datHlavní jednotkou jakékoli aplikace, jejíž těžištěm zájmu jsou data, je sada dat ( data set), což je kolekce tabulek neutrálních vzhledem k poskytovateli dat a s nepovinnými informacemi o rela-cích a omezeních. Každá sada dat obsahuje tabulky dat ( data tables), a každá tabulka dat obsahu-je nula nebo více řádků dat ( data rows), v nichž jsou skutečná data. Každá tabulka dat obsahuje mimo to jednotlivé sloupce dat ( data columns). Obsahují metadata popisující typ dat ve všech řádcích daného sloupce.

Sadu dat lze naplnit ručně, ale běžně se to dělá přes nějaký datový adaptér, který ví, jak má mluvit s protokolem specifickým pro poskytovatele dat, aby získal a nastavil data. Datový adap-tér pracuje s datovým připojením ( data connection), což je komunikační trubice k samotným

12Sady dat a podpora Designéra

Page 53: C# a WinForms

432 Sady dat a podpora Designéra

datům, ať už jsou v nějakém souboru na systému souborů nebo v nějaké databázi na jiném stroji. Připojení získává požadované řádky, nebo provádí jiné činnosti na poskytovateli dat, pomocí datového příkazu ( data command).

Sady dat, tabulky, řádky a sloupce jsou neutrální vzhledem ke zdroji dat, zato datové adaptéry a připojení jsou specifické vzhledem k zdroji dat. Tato specifika slouží jako most mezi poskyto-vatelem dat a službami .NET, které jsou neutrální vzhledem k poskytovatelům dat, a patří mezi ně vázání dat (probereme je v kapitole 13). Základní prvky architektury dat .NET, známé jako ADO.NET, vidíte na obrázku 12.1.

Tato a následují kapitola 13 obsahují spoustu ukázek kódu, které závisejí na tom, že vám běží instance SQL Serveru a že máte na serveru nainstalovanou a dostupnou databázi Northwind. Jestliže Server SQL nemáte, můžete si nainstalovat Microsoft SQL Server Developer Edition ( MSDE), který se dodává s .NET Framework SDK. Zvolte Start | Programy | Microsoft .NET Framework SDK | Samples and QuickStartTutorials | Install .NET Framework Samples Database 2 a držte se uvedených pokynů.

Získávání dat V rámci dané základní architektury ukazuje následující příklad, jak se naplní objekt DataSet pomocí tříd z jmenného prostoru System.Data a tříd poskytovatele dat SQL Serveru z jmenného prostoru System. DataSqlClient.

using System.Data;using System.Data.SqlClient; // Přístup k SQL Serveru... // Sada dat, se kterou bude pracovat formulář // Sada dat, se kterou bude pracovat formulář DataSet dataset = new DataSet();DataSet dataset = new DataSet();

void Form1_Load(object sender, EventArgs e) {void Form1_Load(object sender, EventArgs e) { // Nakonfiguruje připojení // Nakonfiguruje připojení SqlConnection conn = new SqlConnection(@"Server=localhost;..."); SqlConnection conn = new SqlConnection(@"Server=localhost;...");

// vytvoří z připojení datový adaptér // vytvoří z připojení datový adaptér SqlDataAdapter adapter = new SqlDataAdapter(conn.CreateCommand()); SqlDataAdapter adapter = new SqlDataAdapter(conn.CreateCommand()); adapter.SelectCommand.CommandText = "select * from customers"; adapter.SelectCommand.CommandText = "select * from customers";

// Naplní sadu dat tabulkou Customers // Naplní sadu dat tabulkou Customers adapter.Fill(dataset); adapter.Fill(dataset);

// Naplní seznam PopulateListBox();}

void PopulateListBox() { // Vyprázdní seznam

Page 54: C# a WinForms

Sady dat a podpora Designéra 433

Obrázek 12.1: Architektura dat .NET

listBox1.Items.Clear();

// Projde v cyklu uschovaná data foreach( DataRow row in dataset.Tables[0].Rows ) { foreach( DataRow row in dataset.Tables[0].Rows ) { string item = row["ContactTitle"] + ", " + row["ContactName"]; string item = row["ContactTitle"] + ", " + row["ContactName"]; listBox1.Items.Add(item); }}

Kód vytvoří připojení pomocí připojovacího řetězce ( connection string), který je specifický pro každého poskytovatele dat. Sděluje připojení, kam si má jít pro data. Pak se vytvoří adaptér s vhodným textem příkazu, kterým se data přes připojení získají. Pomocí adaptéru se naplní sada dat, v našem případě se vyprodukuje jediná tabulka. Kód pak prochází v cyklu řádky tabulky, vytahuje z nich sloupce podle jejich názvů, (předpokládá se, že víme, jak se sloupce tabulky jme-nují). Pak se daty naplní seznam na formuláři a výsledek vidíte na obrázku 12.2.

Připomínám, že ačkoliv ukázkový kód vytváří připojení, vůbec nikdy je ani neotvírá, ani ne-zavírá. Připojení otevírá datový adaptér, když je třeba vykonat nějakou operaci – v našem pří-padě získat data a naplnit sadu dat – a až se operace dokončí, připojení zavře. Samotná sada dat nikdy s připojením nepracuje, ani neví, odkud vlastně data přišla. Je na adaptéru, aby přeložil

Page 55: C# a WinForms

434 Sady dat a podpora Designéra

data ve specifickém formátu daného poskytovatele do sady dat, která je vzhledem k poskytova-telům neutrální.

Obrázek 12.2: Zobrazení získaných dat

Protože sada dat nemá ani páru o připojení k poskytovateli, je to jisté úložiště jak pro data, tak pro operace nad nimi. Data se mohou v sadě dat aktualizovat, nebo dokonce z ní odstra-ňovat, ale tyto operace se nepromítají u skutečného poskytovatele do té doby, dokud datovému adaptéru neřeknete, aby to zařídil. Než se ale pustíme do výkladu, jak změny v sadě dat dostat k poskytovateli, podíváme se na zbývající běžné operace nad sadou dat: vytváření, aktualizace a odstraňování dat.

Vytváření datChcete-li do tabulky přidat nový řádek, požádáte tabulku o nový prázdný objekt DataRow a na-plníte hodnoty jednotlivých sloupců:

void addRowMenuItem_Click(object sender, EventArgs e) { // Požádá tabulku o prázdný DataRow // Požádá tabulku o prázdný DataRow DataRow row = dataset.Tables[0].NewRow(); DataRow row = dataset.Tables[0].NewRow();

// Naplní objekt řádku dat hodnotami pro jednotlivé sloupce // Naplní objekt řádku dat hodnotami pro jednotlivé sloupce row["CustomerID"] = "SELLSB"; row["CustomerID"] = "SELLSB"; ... // Přidá řádek dat do tabulky // Přidá řádek dat do tabulky dataset.Tables[0].Rows.Add(row); dataset.Tables[0].Rows.Add(row);

// Aktualizuje seznam PopulateListBox();}

Page 56: C# a WinForms

Sady dat a podpora Designéra 435

Aktualizace datExistují data se dají aktualizovat tak, že sáhnete do sady dat, vytáhnete řádek, o který se zají-máte, a podle svých přání ho aktualizujete:

void updateSelectedRowMenuItem_Click(object sender, EventArgs e) { // Získá index vybraného řádku v seznamu int index = listBox1.SelectedIndex; if( index == -1 ) return;

// Získá odpovídající řádek sady dat // Získá odpovídající řádek sady dat DataRow row = dataset.Tables[0].Rows[index]; DataRow row = dataset.Tables[0].Rows[index];

// Aktualizuje řádek podle svých představ // Aktualizuje řádek podle svých představ row["ContactTitle"] = "CEO"; // pasoval se na ředitele row["ContactTitle"] = "CEO"; // pasoval se na ředitele

// Aktualizuje seznam PopulateListBox();}

Odstraňování dat Než se pustíte do odstraňování řádků z tabulky, je dobré si ujasnit, co činností „odstranit“ bude-me přesně myslet. Chcete-li, aby řádek zmizel navždy, a nezanechal po sobě žádnou stopu, pak zavolejte metodu Remove nad kolekcí DataRowCollection, kterou vystavuje DataTable:

void deleteSelectedRowMenuItem_Click(object sender, EventArgs e) { // Získá index vybraného řádku v seznamu int index = listBox1.SelectedIndex; if( index == -1 ) return;

// Získá odpovídající řádek sady dat DataRow row = dataset.Tables[0].Rows[index];

// Odstraní řádek ze sady dat // Odstraní řádek ze sady dat dataset.Tables[0].Rows.Remove(row); dataset.Tables[0].Rows.Remove(row);

// Aktualizuje seznam PopulateListBox();}

To je však asi drsnější akce„odstranit“, než jakou si opravdu přejete, zvláště pokud plánujete pro-mítnout změny, které jste udělali v sadě dat, zpět do původních dat poskytovatele. Decentnější akce „odstranit“ spočívá v tom, že řádek jen označíte jako odstraněný, takže ze sady dat nadobro nezmizí. Dělá se to metodou Delete samotného objektu DataRow:

Page 57: C# a WinForms

436 Sady dat a podpora Designéra

void deleteSelectedRowMenuItem_Click(object sender, EventArgs e) { // Získá index vybraného řádku v seznamu int index = listBox1.SelectedIndex; if( index == -1 ) return;

// Získá odpovídající řádek sady dat DataRow row = dataset.Tables[0].Rows[index];

// Označí řádek jako odstraněný // Označí řádek jako odstraněný row.Delete(); row.Delete();

// Aktualizuje seznam PopulateListBox();}

Když tabulka dat (DataTable) obsahuje řádky označené jako odstraněné, musíte změnit způsob přístupu k tabulce, protože třída DataTable nepovoluje přímý přístup k odstraněným řádkům. Je to prevence proti tomu, abyste omylem nepovažovali odstraněné řádky za normální řádky. Kontrola odstraněných řádků se dělá testováním vlastnosti RowState řádku, jejíž hodnota je kombinací hodnot výčtu DataRowState:

enum DataRowState { Added, Deleted, Detached, Modified, Unchanged,}

Chcete-li brát v úvahu i řádky označené jako odstraněné, dělá se to takhle:

void PopulateListBox() { // Vyprázdní seznam listBox1.Items.Clear();

// Projde v cyklu uložená data foreach( DataRow row in dataset.Tables[0].Rows ) { if( (row.RowState & DataRowState.Deleted) != if( (row.RowState & DataRowState.Deleted) != DataRowState.Deleted ) continue; DataRowState.Deleted ) continue; string item = row["ContactTitle"] + ", " + row["ContactName"]; listBox1.Items.Add(item); }}

Page 58: C# a WinForms

Sady dat a podpora Designéra 437

Když přistupujete k datům sloupců, tak standardně dostanete „aktuální“ data, která u sloupců odstraněných řádků chybějí (a pokus o přístup k nim skončí výjimkou při běhu). Všechna data jsou označená nějakou hodnotou z výčtu DataRowVersion:

enum DataRowVersion { Current, Default, Original, Proposed,}

Chcete-li získat stará, nebo odstraněná data sloupců, předejte jako druhý argument indexeru řádků patřičnou hodnotu výčtu DataRowVersion:

void PopulateListBox() { // Vyprázdní seznam listBox1.Items.Clear();

// Projde v cyklu uložená data foreach( DataRow row in dataset.Tables[0].Rows ) { if( (row.RowState & DataRowState.Deleted) != DataRowState.Deleted ) { string id = row["CustomerID", DataRowVersion.Original].ToString(); row["CustomerID", DataRowVersion.Original].ToString(); listBox1.Items.Add("***deleted***: " + id); continue; } ... }}

Sledování změn Když objekt řádku dat ( DataRow) skočí svou pouť v sadě dat ( DataSet) jakožto důsledek metody Fill datového adaptéru, nastaví se RowState na Unchanged (nezměněný). Jak už jsem se zmínil výše, způsobí volání metody Delete objektu DataRow, že se RowState nastaví na Deleted (odstra-něný). Obdobně, při přidávání nových řádků, resp. aktualizaci existujících řádků, se RowState nastavuje na Added (přidaný), resp. Modified (modifikovaný). To dělá ze sady dat něco víc, než pouhé úložiště aktuálního stavu dat. Zaznamenávají se v ní také změny, které byly v datech pro-vedeny od chvíle, kdy byla data prvotně získána. Změny můžete získat na úrovni jednotlivých tabulek, když zavoláte metodu GetChanges třídy DataTable:

DataTable tableChanges = dataset.Tables[0].G etChanges(DataRowState.Modified);if( tableChanges != null ) {

Page 59: C# a WinForms

438 Sady dat a podpora Designéra

foreach( DataRow changedRow in tableChanges.Rows ) { MessageBox.Show(changedRow["CustomerID"] + " byl modifikován"); }}

Metoda GetChanges přebírá kombinaci hodnot DataRowState a vrací tabulku, která je kopií vy-mezených řádků. Řádky se zkopírovaly, takže se nemusíte zabývat přístupem k odstraněným řád-kům, což by za normálních okolností vedlo na výjimku při běhu. Pomocí metody GetChanges se dají najít všechny modifikované, přidané a odstraněné řádky, dohromady, nebo selektivně. Hodí se to pro přístup k datům, jejichž změny chcete promítnout zpět u poskytovatele dat.

Potvrzování změnSpoluprací metody GetChanges a výčtu DataRowVersion se dají vybudovat příkazy pro replikaci změn provedených v sadě dat u poskytovatele dat. V případě, že pracujete s datovými adapté-ry nasměrovanými na nějakou databázi, získáváte dat pomocí instance nějakého příkazu (com-mand), který má na starost výběr dat přes vlastnost SelectCommand. Vskutku, když si připome-neme předchozí kód, kterým jsme připravili datový adaptér:

// Nakonfiguruje připojení SqlConnection conn = new SqlConnection(@"...");

// Vytvoří z připojení datový adaptérstring select = "select * from customers";SqlDataAdapter adapter = new SqlDataAdapter(select, conn);new SqlDataAdapter(select, conn);

je to vlastně zkrácený zápis následujícího kódu, který vytvoří příkaz, jímž se výběr provede přímo:

// Nakonfiguruje připojení SqlConnection conn = new SqlConnection(@"...");

// Vytvoří z připojení datový adaptérstring select = "select * from customers";SqlDataAdapter adapter = new SqlDataAdapter();SqlDataAdapter();adapter.SelectCommand = new SqlCommand(select, conn);adapter.SelectCommand = new SqlCommand(select, conn);

Použít připojení a získat jeho prostřednictvím data, to má na starost objekt Command, a úkolem datového adaptéru je stanovit, jaký příkaz má použít k získání dat. Obdobně, když se mají změny v sadě dat promítnout zpět k poskytovateli, přičemž jsou do „změn“ zahrnuté přidané řádky, aktualizované řádky a odstraněné řádky, použije k tomu datový adaptér jiné příkazy. Dělá to pomocí příkazů, které jsou připravené přes vlastnosti InsertCommand, UpdateCommand, resp. DeleteCommand.

Page 60: C# a WinForms

Sady dat a podpora Designéra 439

Tyto příkazy můžete naplnit sami, ale obvykle je jednodušší využít tvůrce příkazu, který to udělá za vás. Tvůrce příkazu ( command builder) je objekt, který použije informace z výběrového dotazu (příkazu select) a řádně podle něho naplní ostatní tři příkazy:

// Vytvoří adaptér z připojení s příkazem selectSqlDataAdapter adapter = new SqlDataAdapter("select * from customers", conn);

// Nechá na tvůrci výrazu, aby vybudoval příkazy pro insert, update a delete// Nechá na tvůrci výrazu, aby vybudoval příkazy pro insert, update a delete// na základě informací z existujícího příkazu select// na základě informací z existujícího příkazu selectnew SqlCommandBuilder(adapter);new SqlCommandBuilder(adapter);

Tvůrce výrazu je natolik soběstačný, že se s ním vůbec nemusíte zabývat. Vytvořte ho a předejte mu adaptér, jehož příkazy potřebujete vybudovat. Dál se o něj nestarejte. Poté, co vám tvůrce příkazu připraví řádně příkazy adaptéru, můžete promítnout změny zpět u poskytovatele dat tím, že zavoláte metodu Update adaptéru:

void commitChangesMenuItem_Click(object sender, EventArgs e) {// Nakonfiguruje připojení SqlConnection conn = new SqlConnection(@"...");

// Vytvoří adaptér z připojení s příkazem select SqlDataAdapter adapter = new SqlDataAdapter("select * from customers", conn);

// Nechá na tvůrci výrazu, aby vybudoval příkazy pro insert, update a delete // Nechá na tvůrci výrazu, aby vybudoval příkazy pro insert, update a delete new SqlCommandBuilder(adapter); new SqlCommandBuilder(adapter);

// Potvrdí změny zpět u poskytovatele dat // Potvrdí změny zpět u poskytovatele dat try { try { adapter.Update(dataset); adapter.Update(dataset); } } catch( SqlException ex ) { catch( SqlException ex ) { MessageBox.Show(ex.Message, "Chyby při potvrzování změn"); MessageBox.Show(ex.Message, "Chyby při potvrzování změn"); } }

// Aktualizuje seznam PopulateListBox();}

V kódu se pomocí tvůrce příkazu vybudují tři příkazy, které jsou zapotřebí při aktualizaci dat u poskytovatele, a pak nechá na adaptéru, aby patřičně sestavil text příkazu. Jestliže kterákoli z aktualizací způsobí chybu, vygeneruje se při běhu výjimka, a to je důvod , proč je volání Update obaleno konstrukcí try-catch. Informace o chybách se udržují pro každý řádek, takže je můžete zobrazit uživateli:

Page 61: C# a WinForms

440 Sady dat a podpora Designéra

void PopulateListBox() { // Vyprázdní seznam listBox1.Items.Clear();

// Projde v cyklu uložená data foreach( DataRow row in dataset.Tables[0].Rows ) { if( (row.RowState & DataRowState.Deleted) != DataRowState.Deleted ) continue; string item = row["ContactTitle"] + ", " + row["ContactName"]; if( row.HasErrors ) item += "(***" + row.RowError + "***)"; if( row.HasErrors ) item += "(***" + row.RowError + "***)"; listBox1.Items.Add(item); listBox1.Items.Add(item); } }}

Booleovská vlastnost HasErrors oznamuje u každého řádku, zda při poslední aktualizaci do-šlo k nějaké chybě, a řetězec RowError oznamuje, co to bylo za chybu. Pokud při aktualizaci došlo k chybám, hodnota RowState daného řádku se nezmění. U řádků, při jejichž aktualizaci k žádným chybám nedošlo, se obnoví stav DataRowState.Unchanged jako příprava pro příští aktualizaci.

Sady dat s více tabulkamiSada dat může v daném okamžiku obsahovat více než jednu tabulku. Když vytváříte sady dat, které mají obsahovat více tabulek, budete mít pro každou načítanou tabulku jeden datový adap-tér. Kromě toho musíte být pečliví, když plníte sadu dat více než jedním adaptérem. Zavoláte-li metodu Fill datového adaptéru na sadu dat několikrát, skončíte s jedinou tabulkou, na jejíž ko-nec se stále přidávají další data. Musíte konkrétně uvést, kterou tabulku se snažíte naplnit:

// Nakonfiguruje připojení// Nakonfiguruje připojeníSqlConnection conn = new SqlConnection(@"...");SqlConnection conn = new SqlConnection(@"...");

// Vytvoří datové adaptéry// Vytvoří datové adaptérySqlDataAdapter customersAdapter = new SqlDataAdapter();SqlDataAdapter customersAdapter = new SqlDataAdapter();SqlDataAdapter ordersAdapter = new SqlDataAdapter();SqlDataAdapter ordersAdapter = new SqlDataAdapter();

// Vytvoří sadu dat // Vytvoří sadu dat DataSet dataset = new DataSet();DataSet dataset = new DataSet();

void MultiTableForm_Load(object sender, EventArgs e) {void MultiTableForm_Load(object sender, EventArgs e) { // Vytvoří z připojení adaptér pro zákazníky // Vytvoří z připojení adaptér pro zákazníky customersAdapter.SelectCommand = conn.CreateCommand(); customersAdapter.SelectCommand = conn.CreateCommand(); customersAdapter.SelectCommand.CommandText = customersAdapter.SelectCommand.CommandText = "select * from customers"; "select * from customers";

// Naplní sadu dat tabulkou zákazníků (Customers) // Naplní sadu dat tabulkou zákazníků (Customers)

Page 62: C# a WinForms

Sady dat a podpora Designéra 441

customersAdapter.Fill(dataset, "Customers"); customersAdapter.Fill(dataset, "Customers");

// Vytvoří z připojení adaptér pro objednávky (Orders) // Vytvoří z připojení adaptér pro objednávky (Orders) ordersAdapter.SelectCommand = conn.CreateCommand(); ordersAdapter.SelectCommand = conn.CreateCommand(); ordersAdapter.SelectCommand.CommandText = ordersAdapter.SelectCommand.CommandText = "select * from orders"; "select * from orders";

// Naplní sadu dat tabulkou objednávek (Orders) // Naplní sadu dat tabulkou objednávek (Orders) ordersAdapter.Fill(dataset, "Orders"); ordersAdapter.Fill(dataset, "Orders");

// Potřebujeme pro každý adaptér jednoho tvůrce příkazu, // Potřebujeme pro každý adaptér jednoho tvůrce příkazu, // předjímáme, že se nakonec budou potvrzovat změny // předjímáme, že se nakonec budou potvrzovat změny new SqlCommandBuilder(customersAdapter); new SqlCommandBuilder(customersAdapter); new SqlCommandBuilder(ordersAdapter); new SqlCommandBuilder(ordersAdapter);

// Naplní seznamy // Naplní seznamy PopulateListBoxes(); PopulateListBoxes();}

Kód naplní sadu dat daty ze dvou různých datových adaptérů, pro každou tabulku je jeden. Když voláte metodu Fill datového adaptéru, musíte specifikovat, která tabulka se má naplnit daty z adaptéru. Když to neuděláte, dostanete jedinou datovou tabulku, která bude obsahovat data z obou metod Fill.

Mohli byste sice při plnění obou tabulek vystačit s jediným datovým adaptérem, ale protože tvůrci příkazu určují pomocí SelectCommand, jak se mají data aktualizovat u poskytovatele, je lepší, když máte mezi tabulkami v sadě dat a datovými adaptéry relace jedna ku jedné. Všimněte si, že poté, co jsme vytvořili datové adaptéry, vytvořili jsme pro každého z nich jednoho tvůrce příkazu, protože předjímáme, že se později budou potvrzovat změny provedené v jednotlivých tabulkách. Protože máme více než jednu tabulku, musíme kód pro potvrzování změn upravit:

void commitChangesMenuItem_Click(object sender, EventArgs e) {void commitChangesMenuItem_Click(object sender, EventArgs e) { // Potvrdí změny v tabulce zákazníků u poskytovatele dat // Potvrdí změny v tabulce zákazníků u poskytovatele dat try { try { customersAdapter.Update(dataset, "Customers"); customersAdapter.Update(dataset, "Customers"); } } catch( SqlException ex ) { catch( SqlException ex ) { MessageBox.Show(ex.Message, MessageBox.Show(ex.Message, "Chyby při potvrzování změn v tabulce zákazníků"); "Chyby při potvrzování změn v tabulce zákazníků"); } } // Potvrdí změny v tabulce objednávek u poskytovatele dat // Potvrdí změny v tabulce objednávek u poskytovatele dat try { try { ordersAdapter.Update(dataset, "Orders"); ordersAdapter.Update(dataset, "Orders"); } } catch( SqlException ex ) { catch( SqlException ex ) { MessageBox.Show(ex.Message, MessageBox.Show(ex.Message,

Page 63: C# a WinForms

442 Sady dat a podpora Designéra

"Chyby při potvrzování změn v tabulce objednávek"); "Chyby při potvrzování změn v tabulce objednávek"); } }

// Aktualizuje seznamy PopulateListBoxes();}

Kód potvrzuje změny u každé tabulky zvlášť tím, že volá metodu Update konkrétního datového adaptéru, a uvede v ní, která tabulka se má aktualizovat. Dávejte pozor, abyste vždy přiřadili správnou tabulku správnému adaptéru, jinak to takhle dobře chodit nebude.

OmezeníJestliže byste rádi zachytili špatná data hned, když je přidává uživatel, a nečekali na chvíli, až se budou zasílat zpět k poskytovateli, můžete zřídit nějaká omezení. Omezení ( constraint) je nějaká restrikce na druh hodnot, které půjde dávat do sloupců. Jmenný prostor System.Data přichá-zí se dvěma omezeními: omezení cizího klíče ( foreign key constraint) a omezení na jedineč-né hodnoty ( unique value constraint). Omezení reprezentují třídy ForeignKeyConstraint, resp. UniqueConstraint.

Například, abyste zajistili, že žádné dva řádky nebudou mít v nějakém sloupci stejnou hodno-tu, můžete přidat do seznamu omezení tabulky omezení na jedinečné hodnoty:

// Přidá omezení na jedinečné hodnotyDataTable customers = dataset.Tables["Customers"];UniqueConstraint constraint = new UniqueConstraint(customers.Columns["CustomerID"]);customers.Constraints.Add(constraint);

Když je výše uvedené omezení v činnosti, a do tabulky se přidá nový řádek, který stanovené omezení porušuje, vygeneruje se výjimka při běhu okamžitě, nebude se podnikat výlet k posky-tovateli a zpět.

Omezení na jedinečné hodnoty se připravují pro jeden nebo několik sloupců jediné tabul-ky, zatímco omezení cizího klíče je založeno na existenci odpovídajících hodnot mezi sloupci různých tabulek. Omezení cizího klíče se nastaví automaticky, jakmile zřídíte nějakou relaci.

RelaceSady dat nejsou jen primitivní kontejnery pro několik tabulek dat, jsou to kontejnery podporují-cí relační data. Podobně jako se síla databáze ukáže teprve tehdy, když v ní jsou relace, i skutečná síla sady dat se vyjeví teprve tehdy, když jsou její tabulky propojené relacemi:

// Získá odkazy na tabulky DataTable customers = dataset.Tables["Customers"];

Page 64: C# a WinForms

Sady dat a podpora Designéra 443

DataTable orders = dataset.Tables["Orders"];

// Vytvoří relaciDataRelation relation = new DataRelation( "CustomersOrders", customers.Columns["CustomerID"], orders.Columns["CustomerID"]);

// Přidá relaci dataset.Relations.Add(relation);

Kód vytvoří relaci mezi tabulkami zákazníků a objednávek založenou na hodnotách sloupce CustomerID, který je v obou tabulkách. Relace je jistý pojmenovaný vztah mezi sloupci dvou tabulek. Sloupce tabulek propojíte pomocí instance třídy DataRelation, do které předáte název relace a sloupce z obou tabulek. Ukázkovou relaci mezi tabulkami Customers a Orders vidíte na obrázku 12.3.

Obrázek 12.3: Ukázková relace mezi tabulkami zákazníků a objednávek (Customers a Orders)

Jakmile se relace vytvoří, přidá se do sady relací, kterou si sada dat udržuje. Touto akcí se také přidá omezení cizího klíče. Relace se kromě toho využívají pro navigaci a ve výrazech.

NavigaceKdyž přidáte relaci, stane se druhý argument předaný do konstruktoru DataRelation rodičov-ským sloupcem ( parent column), a třetí argument dceřiným sloupcem ( child column). Pohybovat se po nich můžete metodami GetParentRows, resp. GetChildRows. To například umožňuje zob-razit k vybranému rodičovskému řádku všechny sdružené dceřiné řádky, jak je to vidět na ob-rázku 12.4.

Page 65: C# a WinForms

444 Sady dat a podpora Designéra

Obrázek 12.4: Zobrazení výsledků GetChildRows pomocí relace

Na obrázku 12.4 jsou v horním seznamu vypsaní zákazníci, kteří tvoří rodiče v relaci CustomerOrders. Když se v seznamu vybere nějaký rodič (zákazník), naplní se spodní seznam odpovídajícími dceřinými řádky (neboli objednávkami vybraného zákazníka):

void PopulateChildListBox() { // Vyprázdní seznam ordersListBox.Items.Clear();

// Získá aktuálně vybraný rodičovský řádek zákazníka // Získá aktuálně vybraný rodičovský řádek zákazníka int index = customersListBox.SelectedIndex; int index = customersListBox.SelectedIndex; if( index == -1 ) return;

// Získá řádek ze sady dat // Získá řádek ze sady dat DataRow parent = dataset.Tables["Customers"].Rows[index]; DataRow parent = dataset.Tables["Customers"].Rows[index];

// Projde v cyklu dceřiné řádky foreach( DataRow row in parent.GetChildRows("CustomersOrders") ) {foreach( DataRow row in parent.GetChildRows("CustomersOrders") ) { ... }}

Obdobně se dá pro jakýkoli dceřiný řádek navigovat zpět po relaci na rodiče pomocí GetParentRows.