C# a WinForms

Page 1

E N C Y K L O P E D I E

Z O N E R

P R E S S

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

© Foto: Jiří Heller

C h r i s www.zonerpress.cz

S e l l s

Microsoft .NET Development Series


C# a WinForms programová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

Chris Sells


Microsoft .NET Development Series

Windows 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 Professional; 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ářů Windows Autor: Chris Sels Copyright © ZONER software s.r.o. Vydání první v roce 2005. Všechna práva vyhrazena. Katalogové číslo: ZR502 Zoner Press ZONER 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: knihy@zoner.cz http://www.zonerpress.cz

ISBN 80-86815-25-0


Mé ženě Melisse, a bratrstvu Sells, neboli mý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.


Přehled 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 A B C D

6

Ahoj, formuláře Windows! Formuláře Dialogy Zásady kreslení Kreslení textu Kreslení pro pokročilé Tisk Ovládací prvky Integrace po dobu designu Prostředky Aplikace a nastavení Sady dat a podpora Designéra Vázání dat a mřížky dat Uživatelská rozhraní s více vlákny Rozmisťování přes web Přechod z MFC Delegáti a události Základy serializace Standardní komponenty a ovládací prvky Windows Bibliografie Rejstřík

37 67 113 139 187 205 227 249 297 361 389 431 461 509 535 571 585 595 605 631 634


Obsah Obrázky Tabulky Předmluva Úvod 1 Ahoj, formuláře Windows!

Aplikace WinForms úplně od začátku Formuláře Windows ve Visual Studiu .NET Uspořádávání ovládacích prvků Ovládací prvky Nastavení pro aplikaci Prostředky Dialogy Kreslení a tisk Vázání dat Uživatelská rozhraní s více vlákny Rozmisťování Přechod z MFC Kam jsme se dostali? Poznámky v textu

2 Formuláře

Zobrazování formulářů Vlastnické a vlastněné formuláře

Doba života formuláře Velikost a pozice formuláře Omezování velikosti formuláře Pořadí podle osy z

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

15 25 27 31 37

37 42 47 50 52 55 57 59 61 63 63 64 65 65

67

67 68

70 72 77 78

78 80

7


8

Obsah Formuláře, které nemají tvar obdélníka

Nabídky formuláře Kontextové nabídky

Dceřiné ovládací prvky Pořadí ovládacích prvků podle osy z Tabulátorové pořadí prvků Motivy ovládacích prvků Hostitelství ovládacích prvků COM

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

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

Vizuální dědění Kam jsme se dostali? Poznámky

3 Dialogy

Standardní dialogy Styly Dynamické nastavování modálního a nemodálního chování

Výměna dat Zpracování tlačítek OK a Storno Data nemodálních formulářů

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

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

Kam jsme se dostali? Poznámky

4 Zásady kreslení

Kreslení na obrazovku Zpracování události Paint Spouštění události Paint

Barvy Známé barvy

81

84 87

88 89 89 90 91

93 93 95 97 99 100 101 102

104 106

109 111 112

113

113 115 116

117 118 121

123 124 125 126

128 128 128 130 131 134 136 137

137 138

139

139 141 142

144 145


Obsah Překlad barev

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

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

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

Cesty Režimy vyplňování

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

Kam jsme se dostali? Poznámky

5 Kreslení textu Písma

Vytváření písem Rodiny písem Charakteristiky písma Velikost písma

Řetězce

9 147

147 149 150 151 152 153

156 157 159 160 160 161

162 163 165 165

166 169

170 170 171 173 174 175 176 179 181 182

185 185

187

187 189 190 192 194

195

Formátování Řetězce a cesty

196 203

Kam jsme se dostali?

204

Poznámky

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

Převod pixelů na jednotky stránky

Transformace Změna měřítka Změna měřítka písem Otáčení

204

205

205 208

209 210 211 212


10

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

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

213 215 215 216 217

219 219 220 221

Optimalizované kreslení

222

Dvojité bufferování Další kreslicí volby

223 224

Kam jsme se dostali?

225

Poznámky

7 Tisk

Tiskové dokumenty Kontroléry tisku

225

227

227 229

Náhled před tiskem

230

Základní tiskové události Okraje Nastavení stránky Nastavení tiskárny

233 234 239 241

Rozsah tisku Příprava měr pro tisk

243 245

Kam jsme se dostali? Poznámky

8 Ovládací prvky

Standardní ovládací prvky Akční ovládací prvky Hodnotové ovládací prvky Ovládací prvky pro seznam Kontejnerové ovládací prvky Ovládací prvky pro seznamy obrázků Ovládací prvky kreslené vlastníkem

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

Uživatelské ovládací prvky Přetahování myší Cíl přetahování Zdroj, zahájení přetahovací operace

247 247

249

249 250 251 253 257 259 260

265 265 266 268 269 272 274 280 282 284

285 287 288 292


Obsah Kam jsme se dostali? Poznámky

9 Integrace po dobu designu Komponenty

Standardní komponenty Vlastní komponenty

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

Poskytovatelé rozšiřujících vlastností Konvertory typů Konvertor objektu, který lze rozbalovat

Editory typů v uživatelském rozhraní Rozvírací editory typu v uživatelském rozhraní Modální editory typu v uživatelském rozhraní

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

Kam jsme se dostali? Poznámky

10 Prostředky

Základní pojmy týkající se prostředků Manifestní prostředky Typové prostředky Manažer prostředků Prostředky Designéra

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

Kam jsme se dostali? Poznámky

11 Aplikace a nastavení Aplikace

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

11 295 295

297

297 298 300

307 307 309 311 312 314 317 321

324 330 336

341 344 346

350 353 356

359 359

361 361 362 366 370 372

375 376 377 379 382 384 386

387 387

389

389 390 391 393 394 396 397


12

Obsah Aplikace SDI s více okny

Prostředí Nastavení pro dobu kompilace Nastavení prostředí

Nastavení Typy nastavení Soubory .config Dynamické vlastnosti Registr Speciální složky Nastavení a proudy Izolované úložiště Verze datových cest Volba mechanizmu pro nastavení

Kam jsme se dostali? Poznámky

12 Sady dat a podpora Designéra Sady dat

Získávání dat Vytváření dat Aktualizace dat Odstraňování dat Sledování změn Potvrzování změn Sady dat s více tabulkami Omezení Relace Navigace Výrazy

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

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

Kam jsme se dostali? Poznámky

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

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

401

406 406 407

409 409 410 412 414 419 422 423 427 429

429 430

431

431 432 434 435 435 437 438 440 442 442 443 445

446 446 447 448

449 449 451 453 454 455

459 459

461

461 462 465 468 469 470 476


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

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

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

Kam jsme se dostali? Poznámky

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

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

Asynchronní webové služby Kam jsme se dostali? Poznámky

13 476 479 481 484 486

489 490 493 493

494 494 496 497 502

506 506

509

509 510 512 514 519 520 523 524 525

529 533 533

15 Rozmisťování přes web

535

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

536 537

Zabezpečení přístupu ke kódu

539

Internet Explorer jako hostitel ovládacích prvků

Kontrola povolení Udělování povolení

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

535

541 542

543 543 545 546

Úvahy o částečně důvěryhodných kompletech

550

Povolení částečně důvěryhodných volajících Nastavení Vstup od uživatele Komunikace přes webové služby Čtení a zápis souborů Argumenty příkazového řádku Ladění NTD

550 552 553 554 555 556 558

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

560


14

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

Authenticode Kam jsme se dostali? Poznámky

A Přechod z MFC

Několik slov o MFC MFC versus WinForms Odlišnosti Strategie

Genghis Poznámky

B Delegáti a události Delegáti

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

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

563 565

568 569 570

571

571 573 574 580

581 583

585

585 586 587 588

589 590 590 591 592

Pohoda v celém vesmíru

592

C Základy serializace

595

Proudy Formátovače

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

ISerializable Verze dat Poznámky

D Standardní komponenty a ovládací prvky Windows Definice komponenty a ovládacího prvku Standardní komponenty Standardní dialogy Ikony na hlavním panelu Časovač Seznam obrázků Hlavní nabídky a kontextové nabídky

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

595 598 600 600

601 603 604

605

606 607 607 611 612 613 614

616 616 628 629

Bibliografie

631

Rejstřík

634


2 Formuláře 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, nabídky, dceřiné ovládací prvky, ale také vyspělejší témata, jako jsou průhledné formuláře, formulář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ě }

Takto se zobrazí modálně: void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular()

67


68

Formuláře

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í formulář 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ě metodou Close nebo nastavením vlastnosti DialogResult (více o tom v kapitole 3: Dialogy).

Vlastnické a vlastněné formuláře Když metoda ShowDialog zobrazí nový formulář, použije jako logického vlastníka pro nový formulář 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í vlastní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ý formulář vlastníka. Vlastník nemodálního formuláře se nastaví vlastností Owner nového formuláře: void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular() 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 vytvoří, bude mít modální formulář vlastnost Owner nastavenou na null, pokud se nenastaví explicitně.


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á parametr IWin32Window 3: void button1_Click(object sender, System.EventArgs e) { JinyFormular formular = new JinyFormular() 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) { 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čovské 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


70

Formuláře

Doba života formuláře Třebaže uživatel formulář neuvidí do té doby, dokud se nezavolá Show nebo ShowDialog, existuje formulář od okamžiku, kdy se vytvoří jeho objekt. Nový objekt formuláře se probouzí v konstruktoru objektu, kterého runtime volá, když se objekt vytváří poprvé. Během práce konstruktoru 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á nastavení, 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 vygenerované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 // 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); ... } 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:


Formuláře

71

void Form1_Load(object sender, EventArgs e) { // nezobrazit formulář this.Visible = 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); ... } 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 formulářů 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); ... } 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 viditelnost. 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


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 formulář už odešel do věčných lovišť.

Velikost a pozice formuláře Je pravděpodobné, že během svého života bude formulář zabírat na nějakém místě nějaký prostor. Počáteční pozici formuláře vládne vlastnost StartPosition, která může nabývat jedné z hodnot 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í-


Formuláře

73

mu rohu, aby se nová okna nezakrývala, ani se nedostala mimo obrazovku. Velikost formulář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, nahoř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čí jediný 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řadnice 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 nahoře, jak to vidíte na obrázku 2.3, využijte vlastnost DesktopLocation. Ukázka:


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 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ímavé čá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)


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í statický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 pt3 = Point.Round(pt1); // vede na Point(1,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řovaná vlastností ClientSize, která je znázorněná na obrázku 2.3. Je docela vhodné ukládat si vlastnost 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 formuláře, ne jeho kompletní velikosti: private void Form1_Load(object sender, System.EventArgs e) { this.ClientSize = new Size(100,100); // Zavolá SetClientSizeCore


76

Formuláře

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 relativně vzhledem k samotnému formuláři a popisuje klientkou oblast formuláře. Nejvíce se z těchto 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.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 // na souřadnice vzhledem ke klientovi 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.


Formuláře

77

Omezování velikosti formuláře Když si pečlivě připravíte rozvržení ovládacích prvků, často je třeba pro ně zajistit určitý minimá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); // limit na maximální šířku není, maximální výška bude 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 maximalizovanou velikostí: void Form1_Closing(object sender, CancelEventArgs e) { // Zachytí vlastnosti dřív, než formulář zmizí FormWindowState stav = this.WindowState; this.WindowState = FormWindowState.Normal; Point pozice = this.Location; Size velikost = this.ClientSize; // ... uloží stav, pozice a velikost mezi sezeními

...


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 z Další 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 jedné 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 oknů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áře Kromě velikosti a pozice mají formuláře řadu dalších vlastností, které spravují různé další aspekty 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 navrž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 formulář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ěnitelnou velikostí – používají se při budování volně plovoucích oken ve stylu panelů nástrojů (toolbars).


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 kliknutí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 MinimizeBox, 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 MinimizeBox 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 poskytnout 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ý indikuje, ž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 FormBorderStyle 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 formulář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.


80

Formuláře

Průhlednost formuláře Kromě vlastností, které specifikují, jak má systém Windows realizovat neklientskou část formulář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.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; this.Text = "Opacity = " + this.Opacity.ToString(); } void Form1_Deactivate(object sender, EventArgs e) { timer1.Enabled = false; 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ý.


4 Zásady kreslení Formuláře jsou sice zručné, zvláště jsou-li naládované příhodnými ovládacími prvky, někdy však zabudované ovládací prvky1 nestačí na to, aby realizovaly nějaký stav vaší aplikace takový, jaký ho chcete mít. Pak si takový stav budete muset nakreslit sami. Kreslit se může na obrazovku, do souboru, na tiskárnu, ale ať už budete kreslit kamkoliv, budete stále zacházet se stejnými základními prvky – barvami, štětci, pery a písmy – a se stejnými druhy věcí, které máte nakreslit: tvary, obrázky a řetězce. Kapitolu začneme prozkoumáním základů kreslení na obrazovku a hlavních stavebních kamenů kreslení. Připomínám, že všechny kreslicí techniky, které se probírají zde a v příštích dvou kapitolách, se stejnou měrou týkají ovládacích prvků i formulářů. Informace o budování vlastních ovládacích prvků najdete v kapitole 8: Ovládací prvky. Než začneme, je žádoucí zmínit se o tom, že jmenný prostor System.Drawing je implementovaný nad GDI+ (Graphics Device Interface+), což je následník GDI. Původní GDI bylo hlavní oporou ve Windows už od dob, kdy vůbec byly nějaké Windows, a poskytovalo abstrakci nad obrazovkami a tiskárnami, aby bylo snadnější psát aplikace s grafickým uživatelským rozhraním, neboli ve stylu GUI (Graphics User Interface).2 GDI+ je DLL Win32 (gdiplus.dll), která se dodává s Windows XP a je k dispozici i pro straší verze Windows. GDI+ je také neřízená (unmanaged) knihovna tříd C++, která obaluje GDI+. Protože třídy ze System.Drawing sdílejí mnohdy stejné názvy s třídami C++ GDI+, klidně se může stát, že při prohlížení tříd .NET v online dokumentaci zakopnete o nějaké neřízené třídy. Jedná se o stejné pojmy, ale kódovací detaily jsou velmi odlišné, porovnáme-li neřízený C++ s čímkoli řízeným, takže mějte oči na stopkách.

Kreslení na obrazovku Bez ohledu na to, na jaký druh kreslení se chystáte, budete zacházet se stejnou podkladovou abstrakcí, s třídou Graphics ze jmenného prostoru System.Drawing. Třída Graphics poskytuje abstraktní povrch, na který kreslíte, ať už se výsledky vašich kreslicích operací zobrazí na obra-

139


140

Základy kreslení

zovce, uloží do souboru, nebo odešlou na tiskárnu. Třída Graphics je příliš obsáhlá na to, abych ji zde mohl předvést celou, ale budu se k ní v průběhu kapitoly mnohokrát vracet. Jedním ze způsobů, jak lze získat objekt grafiky, je zavolat CreateGraphics, čímž se vytvoří objekt grafiky sdružený s formulářem: bool drawEllipse = false; void drawEllipseButton_Click(object sender, EventArgs e) { // Indikátor, zda se bude kreslit elipsa nebo ne drawEllipse = !drawEllipse; Graphics g = this.CreateGraphics(); try { if( drawEllipse ) { // Nakreslí elipsu g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle); } else { // Smaže dříve nakreslenou elipsu g.FillEllipse(SystemBrushes.Control, this.ClientRectangle); } } finally { g.Dispose(); } }

Poté, co získáme objekt grafiky, můžeme s jeho pomocí kreslit na formulář. Protože tlačítkem přepínáme, zda se bude kreslit elipsa nebo ne, buď nakreslíme elipsu tmavě modrou, nebo použijeme stejnou barvu pozadí, jakou má formulář. To je asi všechno srozumitelné, ale možná se divíte, k čemu tam je ten blok try-finally. Protože objekt grafiky drží podkladový prostředek, který spravuje systém Windows, je na nás, abychom prostředek uvolnili, když skončíme, dokonce i tehdy, když dojde k nějaké výjimce, a to je důvod, proč jsme do kódu zařadili blok try-finally. Třída Graphics, podobně jako mnohé třídy v .NET, implementuje rozhraní IDisposable. Když nějaký objekt implementuje rozhraní IDisposable, je to pro klienta takového objektu signál, aby zavolal metodu Dispose rozhraní IDisposable, až s objektem skončí. Dá se tím objektu na vědomí, že nastal čas na uvolnění všech prostředků, které držel, například nějaký soubor nebo databázové připojení. V našem případě implementace metody Dispose rozhraní IDisposable třídy Graphics uvolní podkladový objekt grafiky, který udržovala. Daná záležitost se dá v C# zjednodušit tím, že se blok try-finally nahradí blokem using: void drawEllipseButton_Click(object sender, EventArgs e) { using( Graphics g = this.CreateGraphics() ) {


Základy kreslení

141

g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle); } // g.Dispose se zde zavolá automaticky }

Blok using C# obaluje kód, který obsahuje, blokem try a vždy na konci bloku zavolá metodu Dispose rozhraní IDisposable na objekt, který byl vytvořen v rámci klauzule using. Je to pohodlný zkrácený zápis pro programátory C#. Je to dobrá technika, kterou byste si měli osvojit. Budete se s ní ostatně dost často setkávat v průběhu knihy.

Zpracování události Paint Poté, co jsme se dozvěděli, jak správně spravovat prostředky Graphics, máme tu další problém: když se změní velikost formuláře, nebo když ho něčím pokryjeme, a pak zase odkryjeme, elipsa se automaticky nepřekreslí. Zařizuje se to tak, že Windows požádá formulář (a všechny dceřiné ovládací prvky), aby překreslil nově odkrytý obsah přes událost Paint, která poskytuje argument PaintEventArgs: class PaintEventArgs { public Rectangle ClipRectangle { get; } public Graphics Graphics { get; } } bool drawEllipse = false; void drawEllipseButton_Click(object sender, EventArgs e) { drawEllipse = !drawEllipse; } void DrawingForm_Paint(object sender, PaintEventArgs e) { if( !drawEllipse ) return; Graphics g = e.Graphics; g.FillEllipse(brush, this.ClientRectangle); }

V okamžiku, kdy se odpálí událost Paint, už je pozadí formuláře nakreslené 3, takže jakákoli elipsa, která byla nakreslena při předchozí události Paint, bude pryč; to znamená, že kreslit elipsu musíme jen tehdy, když je indikátor drawEllipse nastavený na true. Ovšem, i když indikátor nastavíme tak, že se má elipsa nakreslit, Windows se nedozví, že se stav indikátoru změnil, takže se událost Paint nespustí, a formulář nedostane šanci elipsu nakreslit. Abychom nemuseli mít kreslení elipsy v událostní proceduře Click, a zároveň také v události Paint, musíme požádat o událost Paint, a dát vědět systému Windows, že se formulář má překreslit.


142

Základy kreslení

Spouštění události Paint Spuštění události Paint si vyžádáme metodou Invalidate: void drawEllipseButton_Click(object sender, EventArgs e) { drawEllipse = !drawEllipse; this.Invalidate(true); // Požádáme Windows o událost Paint // pro formulář a jeho děti }

Nyní, když uživatel přepne náš indikátor, zavoláme Invalidate, abychom dali Windows na vědomí, že se nějaká část formuláře má překreslit. Protože je však kreslení jednou z nejnákladnějších operací, zpracuje Windows nejdříve jiné události – jako jsou pohyby myší, vstup z klávesnice atd. – teprve pak odpálí událost Paint, pro případ, že by bylo potřeba překreslit současně více oblastí na formuláři. Abychom se této prodlevy vyvarovali, můžeme zavolat metodu Update, kterou systém Windows donutíme, aby zpracoval událost Paint okamžitě. Protože se rušení platnosti a aktualizace celé klientské oblasti formuláře vyskytují běžně, mají formuláře metodu Refresh, která obě dvě metody kombinuje: void drawEllipseButton_Click(object sender, EventArgs e) { drawEllipse = !drawEllipse; // Dá se udělat jedno nebo druhé this.Invalidate(true); // Požádáme Windows o událost Paint // pro formulář a jeho děti this.Update(); // Donutí vykonat událost Paint hned teď // Nebo se dá udělat obojí najednou this.Refresh(); // Invalidate(true) + Update }

Jestliže však můžete počkat, je nejlepší nechat systém Windows, aby událost Paint zpracoval po svém. Její opoždění má svůj důvod: je to nejpomalejší věc, kterou systém dělá. Když se vynucuje, aby se všechno překreslovalo hned, eliminují se tím důležité optimalizace. Jestliže sledujete se mnou práci na naší jednoduché ukázce, možná jste potěšeni, že klikáním na tlačítko rozhodujete, zda bude na formuláři elipsa nebo ne, a že když formulář něčím zakryjete, a pak odkryjete, že se formulář překresluje podle očekávání. Když však budete postupně měnit velikost formuláře, budete patrně zděšeni tím, co uvidíte. Ilustrují to obrázky 4.1 a 4.2.


Základy kreslení

143

Obrázek 4.1: Elipsa na formuláři před jeho zvětšením

Obrázek 4.2: Elipsa na formuláři poté, co se formulář postupně zvětšuje

Na obrázku 4.2 to vypadá, jako kdyby se při zvětšování formuláře elipsa kreslila několikrát, ale ne celá, jen její části. Co se to děje? Když se formulář zvětšuje, kreslí systém Windows jen nově vystavený obdélník, a předpokládá, že existující obdélník není nutné překreslit. Takže i když překreslujeme během každé události Paint celou elipsu, Windows ignoruje vše, co se nachází vně regionu výřezu (clip region) – čímž se rozumí ta část formuláře, která se má překreslit – a to právě vede na to podivné kreslicí chování. Naštěstí můžeme nastavit styl při požadavku, aby Windows překreslil při zvětšování celý formulář: public DrawingForm() { // Required for Windows Form Designer support InitializeComponent(); // Spustí událost Paint, když se mění velikost formuláře this.SetStyle(ControlStyles.ResizeRedraw, true); }

Formuláře (a ovládací prvky) mají několik kreslicích stylů (dozvíte se o nich víc v kapitole 6: Kreslení pro pokročilé). Styl RedrawSize způsobí, že Windows překreslí celou klientskou oblast vždy, když se změní velikost formuláře. Je to samozřejmě méně efektivní, proto je výchozím chováním Windows to původní chování.


144

Základy kreslení

Barvy Doposud jsem kreslil elipsu ve svém formuláři zabudovaným štětcem tmavě modré barvy (DarkBlue). Štětec (brush), jak uvidíte, se používá pro vyplňování vnitřku tvarů, zatímco perem (pen) se kreslí hrany tvarů (obvod). Každopádně předpokládejme, že mě úplně neuspokojuje tmavě modrý štětec. Rád bych místo něj použil některou z více než 16 miliónů barev, které ale nebyly pro mě předem zabudované, takže to znamená, že nejprve musím konkretizovat barvu, o kterou se zajímám. Barvy se v .NET modelují přes strukturu Color: struct Color { // Bez barvy public static readonly Color Empty; // Zabudované barvy public static Color AliceBlue { get; } // ... public static Color YellowGreen { get; } // Vlastnosti public byte A { get; } public byte B { get; } public byte G { get; } public bool IsEmpty { get; } public bool IsKnownColor { get; } public bool IsNamedColor { get; } public bool IsSystemColor { get; } public string Name { get; } public byte R { get; } // Metody public static Color FromArgb(int alpha, Color baseColor); public static Color FromArgb(int alpha, int red, int green, int blue); public static Color FromArgb(int argb); public static Color FromArgb(int red, int green, int blue); public static Color FromKnownColor(KnownColor color); public static Color FromName(string name); public float GetBrightness(); public float GetHue(); public float GetSaturation(); public int ToArgb(); public KnownColor ToKnownColor(); }

Objekt Color v zásadě reprezentuje čtyři hodnoty: množství červené, zelené a modré, a množství neprůhlednosti. Na prvky červená, zelená a modrá se často odkazuje najednou jako na RGB


Základy kreslení

145

(red-green-blue). Každý z nich má rozpětí od 0 do 255, přičemž 0 je nejmenší množství barvy, 255 největší množství barvy. Stupeň neprůhlednosti se specifikuje hodnotou alpha, a někdy se přidává k RGB, takže vznikne ARGB (Alpha-RGB). Hodnota alpha má rozpětí od 0 do 255, přičemž 0 znamená zcela průhledná, 255 kompletně neprůhledná. Objekt Color nevytváříte konstruktorem, ale metodou FromArgb, do které předáte množství červené, zelené a modré barvy: Color Color Color Color Color

red = Color.FromArgb(255, 0, 0); // 255 červená, 0 zelená, 0 modrá green = Color.FromArgb(0, 255, 0); // 0 červená, 255 zelená, 0 modrá blue = Color.FromArgb(0, 0, 255); // 0 červená, 0 zelená, 255 modrá white = Color.FromArgb(255, 255, 255); // bílá black = Color.FromArgb(0, 0, 0); // černá

Chcete-li specifikovat úroveň průhlednosti, přidejte i hodnotu alpha: Color modra25ProcentNepruhledna = Color.FromArgb(255*1/4 255*1/4, 0, 0, 255); 255*3/4, 0, 0, 255); Color modra75ProcentNepruhledna = Color.FromArgb(255*3/4

Tři 8bitové hodnoty barev a jedna 8bitová hodnota alpha tvoří čtyři části jediné hodnoty, která definuje 32 bitovou barvu, jakou umějí zpracovat moderní adaptéry displeje. Předáváte-li raději uvedené čtyři hodnoty jako jedinou hodnotu, dá se to udělat jednou z přetížených variant, ale vypadá to odporně, proto byste se tomu měli vyhýbat: // A = 191, R = 0, G = 0, B = 255 Color modra75ProcentNepruhledna = Color.FromArgb(-1090518785);

Známé barvy Často má barva, o kterou se zajímáte, už přidělený dohodnutý název, což znamená, že je dostupná jako jeden ze statických členů Color, jimiž se definují „známé barvy“, z výčtu KnownColor, nebo názvem: Color blue1 = Color.BlueViolet; Color blue2 = Color.FromKnownColor(KnownColor.BlueViolet); Color blue3 = Color.FromName("BlueViolet");

Kromě 141 barev s názvy jako AliceBlue nebo OldLace, má výčet KnownColor 26 hodnot, které popisují aktuální barvy přiřazené různým částem uživatelského rozhraní Windows, jako jsou barva okraje aktivního okna nebo barva výchozího pozadí ovládacího prvku. Tyto barvy jsou velmi šikovné, když sami něco kreslíte a chcete, aby to bylo v souladu se zbývajícími částmi systému. Systémové barvy výčtu KnownColor jsou vypsané zde: enum KnownColor { // Nesystémové barvy jsem vynechal...


146

Základy kreslení

ActiveBorder, ActiveCaption, ActiveCaptionText, AppWorkspace, Control, ControlDark, ControlDarkDark, ControlLight, ControlLightLight, ControlText, Desktop, GrayText Highlight, HighlightText, HotTrack, InactiveBorder, InactiveCaption, InactiveCaptionText, Info, InfoText, Menu, MenuText, ScrollBar, Window, WindowFrame, WindowText, }

Chcete-li použít některou ze systémových barev, aniž byste museli vytvářet svou vlastní instanci třídy Color, přistupujte k těm, které už byly pro vás vytvořeny a vystaveny jako vlastnosti třídy SystemColors: sealed class SystemColors { // Vlastnosti public static Color ActiveBorder { get; } public static Color ActiveCaption { get; } public static Color ActiveCaptionText { get; } public static Color AppWorkspace { get; } public static Color Control { get; } public static Color ControlDark { get; } public static Color ControlDarkDark { get; } public static Color ControlLight { get; } public static Color ControlLightLight { get; } public static Color ControlText { get; } public static Color Desktop { get; } public static Color GrayText { get; }


Základy kreslení public public public public public public public public public public public public public public

static static static static static static static static static static static static static static

Color Color Color Color Color Color Color Color Color Color Color Color Color Color

147

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

}

Následující dva řádky vedou na objekty Color se stejnými hodnotami barev, a můžete je používat, kde je vám libo. Color color1 = Color.FromKnownColor(KnownColor.GrayText); Color color2 = SystemColors.GrayText;

Překlad barev Máte-li nějakou svou barvu v jednom z tří jiných formátů – HTML, OLE nebo Win32 – nebo chcete barvu přeložit do jednoho z těchto formátů, využijte ColorTranslator, jak to vidíte v ukázce pro HTML: Color htmlModra = ColorTranslator.FromHtml("#0000ff"); string htmlTakyModra = ColorTranslator.ToHtml(htmlBlue);

Když máte nějaký objekt Color, můžete získat jeho hodnoty průhlednosti, červené, zelené a modré barvy, a také název barvy, ať už je to známá barva nebo systémová barva. Můžete také pomocí těchto hodnot vyplnit a obtáhnout tvary, k čemuž ale potřebujete štětce, resp. pera.

Štětce Třída System.Drawing.Brush slouží jako základní třída pro několik druhů štětců, které se používají podle toho, jaké jsou vaše potřeby. Na obrázku 4.3 vidíte pět odvozených tříd štětců, které poskytují jmenné prostory System.Drawing a System.Drawing.Drawing2D.


148

Základy kreslení

Obrázek 4.3: Ukázky štětců

Obrázek 4.3 byl vytvořen tímto kódem: void BrushesForm_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; int x = 0; int y = 0; int width = this.ClientRectangle.Width; int height = this.ClientRectangle.Height/5; Brush whiteBrush = System.Drawing.Brushes.White; Brush blackBrush = System.Drawing.Brushes.Black; using( Brush brush = new SolidBrush(Color.DarkBlue) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, whiteBrush, x, y); y += height; } string file = @"c:\windows\Santa Fe Stucco.bmp"; using( Brush brush = new TextureBrush(new Bitmap(file)) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, whiteBrush, x, y); y += height; } using( Brush brush = new HatchBrush( HatchStyle.Divot, Color.DarkBlue, Color.White) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, blackBrush, x, y); y += height; }


Základy kreslení

149

using( Brush brush = new LinearGradientBrush( new Rectangle(x, y, width, height), Color.DarkBlue, Color.White, 45.0f) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, blackBrush, x, y); y += height; } Point[] points = new Point[] { new Point(x, y), new Point(x + width, y), new Point(x + width, y + height), new Point(x, y + height) }; using( Brush brush = new PathGradientBrush(points) ) { g.FillRectangle(brush, x, y, width, height); g.DrawString(brush.ToString(), this.Font, blackBrush, x, y); y += height; } }

Barevné štětce Štětec SolidBrush má namíchanou nějakou barvu, kterou se má vyplnit nakreslený tvar. Protože se tyto štětce používají velmi hojně, obsahuje kvůli většímu pohodlí třída Brushes 141 vlastností Brush, jednu pro každou barvu pojmenovanou ve výčtu KnownColors. Tyto vlastnosti jsou šikovné, protože jejich prostředky řídí a udržuje v cache samotný .NET, takže se s nimi pracuje poněkud snadněji než se štětci, které vytváříte sami 4: // Řídí .NET Brush bilyStetec = System.Drawing.Brushes.White; // Řídí váš program using ( Brush mujBilyStetec = new SolidBrush(Color.White) ) { ... }

Obdobně se 21 z 26 systémových barev výčtu KnownColor poskytuje ve třídě SystemBrushes 5. To se hodí, chcete-li vytvořit štětec s některou ze systémových barev, ale chcete, aby podkladový prostředek zpracovávaly WinForms. Štětce, které nejsou dostupné názvem jako vlastnosti SystemBrushes, jsou dostupné přes metodu FromSystemColor. Vrací štětec, který řídí .NET: // Volání Dispose na tento štětec způsobí výjimku Brush stetec = SystemBrushes.FromSystemColor(SystemColors.InfoText);


150

Základy kreslení

Štětce s texturou Štětec TextureBrush je vytvořen z nějakého obrázku. Standardně se obrázek používá opakovaně tak, aby „vydláždil“ prostor uvnitř nakresleného tvaru. Toto chování můžete změnit volbou členu výčtu WrapMode. Různé režimy předvádí obrázek 4.4. enum WrapMode { Clamp, // nakreslí pouze jednou Tile, // výchozí TileFlipX, // překlopí obrázek vodorovně podél osy X TileFlipY, // překlopí obrázek svisle podél osy Y TileFlipXY, // překlopí obrázek podél os X a Y }

Obrázek 4.4: Ukázky různých hodnot WrapMode u štětce s texturou


8 Ovládací prvky 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 kontejnerem. 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.

249


250

Ovládací prvky Jiné hodnotové ovládací prvky, jako textové pole nebo prvek pro výběr data a času (TextBox, 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 společné schopnosti, které sdílejí ovládací prvky jednotlivých kategorií.

Akční ovládací prvky Akč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 jeden 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) {


Ovládací prvky

251

if( e.Button == fileExitToolBarButton ) { this.Close(); } 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 obrázků probereme v kapitole později.

Hodnotové ovládací prvky Hodnotové 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:


252

Ovládací prvky

• Řetězcové hodnoty. Popisek, popisek simulující hypertextový odkaz, textové pole, textové 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, MonthCalendar).

• 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 prvku 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íž hodnota 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í vlastnost 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žňuje 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 obrázky měnit. Ovládací prvek PictureBox zobrazí jakýkoli obrázek nastavený ve vlastnosti Image.


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 seznam Je-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 DomainUpDown 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"); }

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"); datumNarozeni); listBox1.Items.Add(datumNarozeni }

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


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á reprezentuje 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(); item.Text = kluk.Jmeno; item.SubItems.Add(kluk.Vek.ToString()); 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


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á obsahuje 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 treeView1.Nodes.Add(rodicovskyUzel); TreeNode dcerinyUzel = new TreeNode(); dcerinyUzel.Text = "John"; // Přidá uzel pod existující uzel 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 DataSource: void Form1_Load(object sender, EventArgs e) { Osoba[] kluci = { new Osoba("Tom", 7), new Osoba("John", 8) }; 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ů


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 seznamu Vš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ějakou událost, když se výběr změní. Například, následující kód zpracovává událost SelectedIndexChanged 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ý jedineč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 jedineč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 } void treeView1_AfterSelect(object sender, TreeViewEventArgs e) { TreeNode selection = treeView1.SelectedNode; object tag = selection.Tag; // vytáhneme si uschovanou informaci navíc MessageBox.Show(tag.ToString()); }


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ěžuje ú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í prvky Zatímco ovládací prvky pro seznam obsahují několik objektů, úkolem kontejnerových ovládací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.


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ádací 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, seznam na obrázku 8.5 se nachází v kolekci Controls rámečku: void InitializeComponent() { ... // groupBox1 this.groupBox1.Controls.AddRange( new System.Windows.Forms.Control[] { this.listBox1}); ... // Form1 this.Controls.AddRange( new System.Windows.Forms.Control[] { this.tabControl1, this.splitter1, 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 vytvořil. Když se nějaký ovládací prvek přidá do kolekce Controls nějakého kontejneru, stane se


Ovládací prvky

259

tento kontejner rodičem tohoto dceřiného ovládacího prvku. Dceřiný ovládací prvek může zjistit 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, ToolBarButton, 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 prostř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 vlastností 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 LargeImageList, 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.imageList1 = new ImageList(this.components); ... // ImageList sdružený s TreeView this.treeView1.ImageList = this.imageList1; ... // Obrázky načtené z prostředků formuláře this.imageList1.ImageStream = ...; ... } void Form1_Load(object sender, EventArgs e) { TreeNode parentNode = new TreeNode(); parentNode.Text = "Chris"; parentNode.ImageIndex = 0; // Obrázek tatínka parentNode.SelectedImageIndex = 0; treeView1.Nodes.Add(parentNode); TreeNode childNode = new TreeNode();


260

Ovládací prvky

childNode.Text = "John"; 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 prostředcích 2 specifických pro formulář. InitializeComponent si je při běhu vytáhne tím, že nastaví 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ádací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íkem Seznamy 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í prvky 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:


12 Sady dat a podpora Designéra Drtivá většina existujících, dokonce i nových aplikacích Windows je vybudována tak, že přistupují 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řístupu 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četně všech těch zatracených podrobností, na to opravdu potřebujete nějakou jinou knihu.1

Sady dat Hlavní 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 relacích a omezeních. Každá sada dat obsahuje tabulky dat (data tables), a každá tabulka dat obsahuje 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ý adaptér pracuje s datovým připojením (data connection), což je komunikační trubice k samotným

431


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 poskytovatelem 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ář DataSet dataset = new DataSet(); void Form1_Load(object sender, EventArgs e) { // Nakonfiguruje připojení SqlConnection conn = new SqlConnection(@"Server=localhost;..."); // vytvoří z připojení datový adaptér SqlDataAdapter adapter = new SqlDataAdapter(conn.CreateCommand()); adapter.SelectCommand.CommandText = "select * from customers"; // Naplní sadu dat tabulkou Customers adapter.Fill(dataset); // Naplní seznam PopulateListBox(); } void PopulateListBox() { // Vyprázdní seznam


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 ) { 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 jmenují). 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 nezaví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


434

Sady dat a podpora Designéra

data ve specifickém formátu daného poskytovatele do sady dat, která je vzhledem k poskytovatelů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í dat Chcete-li do tabulky přidat nový řádek, požádáte tabulku o nový prázdný objekt DataRow a naplníte hodnoty jednotlivých sloupců: void addRowMenuItem_Click(object sender, EventArgs e) { // Požádá tabulku o prázdný DataRow DataRow row = dataset.Tables[0].NewRow(); // Naplní objekt řádku dat hodnotami pro jednotlivé sloupce row["CustomerID"] = "SELLSB"; ... // Přidá řádek dat do tabulky dataset.Tables[0].Rows.Add(row); // Aktualizuje seznam PopulateListBox(); }


Sady dat a podpora Designéra

435

Aktualizace dat Existují 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 DataRow row = dataset.Tables[0].Rows[index]; // Aktualizuje řádek podle svých představ 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“ budeme 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 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 promí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:


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ý 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) != DataRowState.Deleted ) continue; string item = row["ContactTitle"] + ", " + row["ContactName"]; listBox1.Items.Add(item); } }


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(); 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 (odstraně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 provedeny 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 ) {


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í vymezených řádků. Řádky se zkopírovaly, takže se nemusíte zabývat přístupem k odstraněným řádků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ěn Spoluprací 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 (command), který má na starost výběr dat přes vlastnost SelectCommand. Vskutku, když si připomeneme 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ér string select = "select * from customers"; SqlDataAdapter adapter = 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ér string select = "select * from customers"; SqlDataAdapter adapter = new SqlDataAdapter(); 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.


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 select SqlDataAdapter adapter = new SqlDataAdapter("select * from customers", conn); // 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 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 new SqlCommandBuilder(adapter); // Potvrdí změny zpět u poskytovatele dat try { adapter.Update(dataset); } catch( SqlException ex ) { 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:


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 + "***)"; 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 tabulkami Sada 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ý adapté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íž konec se stále přidávají další data. Musíte konkrétně uvést, kterou tabulku se snažíte naplnit: // Nakonfiguruje připojení SqlConnection conn = new SqlConnection(@"..."); // Vytvoří datové adaptéry SqlDataAdapter customersAdapter = new SqlDataAdapter(); SqlDataAdapter ordersAdapter = new SqlDataAdapter(); // Vytvoří sadu dat DataSet dataset = new DataSet(); void MultiTableForm_Load(object sender, EventArgs e) { // Vytvoří z připojení adaptér pro zákazníky customersAdapter.SelectCommand = conn.CreateCommand(); customersAdapter.SelectCommand.CommandText = "select * from customers"; // Naplní sadu dat tabulkou zákazníků (Customers)


Sady dat a podpora Designéra

441

customersAdapter.Fill(dataset, "Customers"); // Vytvoří z připojení adaptér pro objednávky (Orders) ordersAdapter.SelectCommand = conn.CreateCommand(); ordersAdapter.SelectCommand.CommandText = "select * from orders"; // Naplní sadu dat tabulkou objednávek (Orders) ordersAdapter.Fill(dataset, "Orders"); // Potřebujeme pro každý adaptér jednoho tvůrce příkazu, // předjímáme, že se nakonec budou potvrzovat změny new SqlCommandBuilder(customersAdapter); new SqlCommandBuilder(ordersAdapter); // Naplní seznamy 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) { // Potvrdí změny v tabulce zákazníků u poskytovatele dat try { customersAdapter.Update(dataset, "Customers"); } catch( SqlException ex ) { MessageBox.Show(ex.Message, "Chyby při potvrzování změn v tabulce zákazníků"); } // Potvrdí změny v tabulce objednávek u poskytovatele dat try { ordersAdapter.Update(dataset, "Orders"); } catch( SqlException ex ) { MessageBox.Show(ex.Message,


442

Sady dat a podpora Designéra "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 hodnotu, můžete přidat do seznamu omezení tabulky omezení na jedinečné hodnoty: // Přidá omezení na jedinečné hodnoty DataTable 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 poskytovateli a zpět. Omezení na jedinečné hodnoty se připravují pro jeden nebo několik sloupců jediné tabulky, 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.

Relace Sady 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"];


Sady dat a podpora Designéra

443

DataTable orders = dataset.Tables["Orders"]; // Vytvoří relaci DataRelation 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.

Navigace Když přidáte relaci, stane se druhý argument předaný do konstruktoru DataRelation rodičovský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 zobrazit k vybranému rodičovskému řádku všechny sdružené dceřiné řádky, jak je to vidět na obrázku 12.4.


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 int index = customersListBox.SelectedIndex; if( index == -1 ) return; // Získá řádek ze sady dat DataRow parent = dataset.Tables["Customers"].Rows[index]; // Projde v cyklu dceřiné řádky 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.


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.