154 9 3MB
Polish Pages [397] Year 2006
Wstęp do programowania w języku C#
Autor: Adam Boduch ISBN: 83-246-0523-1 Format: B5, stron: 392 Data wydania: 10/2006 Wstęp do programowania w języku C# Autor: Adam Boduch Copyright © Helion 2006
Wszelkie prawa zastrzeżone. Rozpowszechnianie całości lub fragmentów niniejszej publikacji w jakiejkolwiek postaci, bez zgody autora jest zabronione. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi badź towarowymi ich właścicieli. Autor oraz Wydawnictwo Helion dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz wydawnictwo Helion nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystanie informacji zawartych w książce.
Wstęp w wersji elektronicznej
Prace nad książką Wstęp do programowania w języku C# rozpocząłem pod koniec roku 2005, a zakończyłem pod w marcu 2006 roku. W tym samym roku ukazała się ona nakładem wydawnictwa Helion. W zamyśle miała ona stanowić wprowadzenie do języka C# i mam nadzieje, że dzięki niej - drogi Czytelniku - zapoznasz się z podstawami języka C#. Książka została oparta o najnowszy wówczas framework - 2.0, dlatego też informacje w niej się znajdujące mogą być nieaktualne. Mam nadzieje, że mimo wszystko publikacja ta stanie się przydatna dla osób, które chcą nauczyć się programowania w języku C#. Korzystając z tego chciałbym zaapelować do Czytelników: jeżeli znajdziecie w książce jakiś błąd. Czy to logiczny, czy literówkę. Proszę - poprawcie go! Mechanizm witryny 4programmers.net umożliwia edycję każdego z tekstów. Nawet jeśli to, co napisałem jest ewidentnym kłamstwem wynikającym z mej niewiedzy. Nie boj się edytować artykułów. Wszystkie zmiany są zapisywane na serwerze. Nie przeciągając. Życzę miłej lektury!
Przedmowa Pamiętam, jak wiele lat temu ucząc się programowania, miałem trudności ze zrozumieniem niektórych pojęć i zagadnień znajdujących się w różnego rodzaju literaturze. Nie dlatego, że autorzy byli słabymi programistami — wręcz przeciwnie! Byli na tyle doświadczeni, że zapominali, jak to jest być początkującym, i nie potrafili w umiejętny sposób przekazać swojej wiedzy. Wiem również, że wiele początkujących osób ma problemy ze zrozumieniem nieco trudniejszych aspektów programowania. Zamierzam wobec tego zaprezentować swoją wiedzę w jak najprostszy sposób, tak jakbym to ja uczył się programować. Aby zacząć pisać własne programy, nie musisz posiadać żadnych nadzwyczajnych umiejętności, potężnego komputera czy matematycznego umysłu. Chcąc programować w języku C#, nie potrzebujesz także żadnych drogich narzędzi — każde oprogramowanie potrzebne do tego, aby zacząć programować, możesz ściągnąć za darmo z internetu . Zakładam drogi Czytelniku, że jesteś osobą początkującą, dopiero raczkującą w tej tematyce. Być może jesteś osobą, która miała już wcześniej styczność z programowaniem, lecz chce lub musi nauczyć się języka C#. Tak czy inaczej, w niniejszej książce postaram się przedstawić swoją wiedzę tak, jakbyś nigdy wcześniej nie miał styczności z językiem C#. Sukcesywnie będę prezentował kolejne partie materiału oraz objaśniał coraz trudniejsze pojęcia. Mam nadzieję, że czytanie tej książki będzie dla Ciebie przyjemnością, a programowanie w języku C# stanie się Twoją pasją i na długo zapamiętaną przygodą.
Rozdział 1
Wprowadzenie Wielu ludzi tak naprawdę ma mylne wyobrażenie o programowaniu. Branża ta przesiąknięta jest wieloma mitami, jakby było to zajęcie niezwykle trudne, którym trudnić się może jedynie geniusz matematyczny. Nic bardziej mylnego! Programowanie nie jest wcale trudne. W dobie języków programowania tzw. wysokiego poziomu należy opanować pewne podstawy, nauczyć się „myślenia programistycznego”, a poznawanie bardziej zaawansowanych technik jest kwestią czasu i dobrych chęci. W tym rozdziale zaprezentuję kilka kluczowych pojęć, którymi posługują się programiści, a których Ty również będziesz używał często i po prostu musisz je znać. Czy zastanawiałeś się kiedyś, jak to się dzieje, że aplikacje są uruchamiane, wykonują jakieś czynności? Jak to się dzieje, że działają? Jak powstają programy? Jeżeli nie znasz odpowiedzi na te pytania, ten rozdział jest dla Ciebie obowiązkową lekturą przed przystąpieniem do faktycznej nauki języka.
Czym jest programowanie Czym jest programowanie komputerowe? Na pewno spotkałeś się z tym określeniem nie raz — czy to podczas oglądania telewizji, czy przeglądając prasę, czy też w życiu codziennym. Z drugiej strony wiele osób w ogóle nie wie, co to pojęcie tak naprawdę oznacza. Przeciętny człowiek może nieco mylnie wyobrażać sobie programistę jako człowieka w grubych okularach, który całe dnie spędza przed komputerem obłożony fachową literaturą. Na pewno zdarzają się i tacy zapaleńcy, być może dlatego, że… programowanie jest ciekawe! Tak, jest ciekawe, ale z drugiej strony trzeba mieć predyspozycje do takiej pracy. Jeżeli ktoś zabiera się za programowanie z przymusu, to pewnie nie będzie miał z tego żadnej przyjemności. Jednak jeśli samodzielnie napisany program (choćby nie wiem jak był skromny) przyniesie swojemu twórcy poczucie satysfakcji i dumy — zapewne programowanie wciągnie go jeszcze bardziej. Programowanie, ogólnie mówiąc, polega na rozwiązywaniu pewnych problemów. Jest to dziedzina zajmująca się tworzeniem oprogramowania. Niezłym przykładem jest liczydło. Liczydło służy do wspomagania człowieka w poważniejszych obliczeniach matematycznych. Celem programisty jest napisanie programu, który wspomoże człowieka przy wykonywaniu naprawdę trudnych obliczeń. Programista, jeśli chce zrealizować zadanie, musi zapisać pewien algorytm, korzystając z ciągu instrukcji rozpoznawalnych przez komputer. Wszelkie polecenia w danym języku programowania są oczywiście wpisywane przy użyciu klawiatury. Algorytm to zestaw instrukcji, czynności, które należy wykonać, aby zrealizować pewne zadanie w skończonej liczbie kroków. Oczywiście nie mogą to być dowolne polecenia i instrukcje, lecz takie, które mogą być rozpoznane przez kompilator. Język programowania jest więc sposobem na przekazanie maszynie pewnych poleceń do wykonania. Kompilator natomiast jest narzędziem wspomagającym programistę w jego ciężkiej pracy. Tłumaczy on polecenia języka
programowania na instrukcje procesora (szczegółowo o tym powiem w dalszej części rozdziału). Mówiąc ogólnie — na podstawie tych instrukcji tworzy aplikację wykonywalną. Obecnie istnieje wiele narzędzi, które wspomagają programistę podczas tworzenia aplikacji. Jak to wygląda w środowisku MS Windows? Przede wszystkim potrzebujemy edytora tekstu. Edytory wspomagające cały proces programowania przeważnie są podobne do typowych edytorów tekstu systemu Windows — programów z oknami, menu oraz ikonami. W owym edytorze wpisujemy jakieś polecenia — dajmy na to w języku C#. Oczywiście nie mogą to być byle jakie polecenia, lecz takie, które będą zrozumiałe dla kompilatora. Kod źródłowy, potocznie zwanym źródłem, zazwyczaj jest tekstem zawierającym różne polecenia języka programowania. Kody źródłowe są czytelne dla człowieka, programisty, który może je w dowolny sposób modyfikować. Kompilator może być zintegrowany z edytorem, ale może to też być zewnętrzna aplikacja.
Mnogość języków programowania Istnieją setki różnych języków programowania (kilka z nich pokrótce omówię w dalszej części rozdziału), mniej lub bardziej popularnych. Niektóre z nich prawie w ogóle nie są używane, inne natomiast są tak znane i powszechne, że każdy szanujący się programista powinien znać przynajmniej ich podstawy. Można zadać sobie pytanie, po co istnieje tyle języków programowania. W końcu byłoby znacznie łatwiej, gdyby istniał jeden język, którego używaliby wszyscy programiści. Ewolucja w świecie komputerów wymagała jednak nowych narzędzi — prostszych, szybszych i potężniejszych. Ludzie z natury są leniwi i lubią upraszczać sobie życie, tworząc coraz lepsze rozwiązania, w tym wypadku pozwalające łatwiej i szybciej programować. Generalnie języki programowania można podzielić na kilka kategorii w zależności od stopnia trudności, możliwości czy przeznaczenia. Np. do tworzenia aplikacji na stronach WWW (tzw. skryptów) bardziej nadaje się język PHP, gdyż jest prosty, szybki i zapewnia dużo możliwości. Język Perl, pomimo że również się do tego nadaje, jest trudniejszy, programowanie w nim nie jest tak szybkie i przyjemne, aczkolwiek można go użyć do innych czynności. Język C natomiast daje większą możliwość manipulacji komputerem, jest bardziej zaawansowany. Nie wspominając już o Asemblerze, który daje nam niesamowite możliwości, lecz jest bardzo trudny. Jak widzisz, język jest tylko narzędziem w rękach programisty, który musi go odpowiednio wykorzystać zależnie od sytuacji. Od razu uprzedzę Twoje pytanie. Nie ma najlepszego języka programowania, uniwersalnego, łatwego, szybkiego i potężnego. Zdolny programista wybierze odpowiedni język (narzędzie) w zależności od swoich potrzeb.
Edytory kodu
Edytorów kodu jest mnóstwo. Kod możesz pisać nawet w najprostszej wersji Notatnika z systemu Windows. Na rynku istnieje wiele aplikacji (zarówno darmowych, jak i komercyjnych) wspomagających proces powstawania programów, udostępniających wygodne funkcje przydatne przy edycji kodu. Bardzo zaawansowany, umożliwiający pisanie i kompilowanie aplikacji w C# jest pakiet Borland Developer Studio firmy Borland (www.borland.pl). Jest to produkt komercyjny umożliwiający również pisanie aplikacji w języku Delphi oraz C++. Innym środowiskiem popularnym wśród programistów C# jest narzędzie Visual Studio 2005. Umożliwia programowanie i kompilację aplikacji napisanych w językach C++, C# oraz J#. To narzędzie również jest komercyjne. Firma Microsoft udostępniła jednak wersje uboższe, lecz darmowe, które można ściągnąć za darmo ze strony firmy Microsoft (http://msdn.microsoft.com/vstudio/express/default.aspx). Na rysunku 1.1 zaprezentowano środowisko Visual C# 2005 Express Edition, dzięki któremu z powodzeniem można pisać aplikacje w języku C#.
Rysunek 1.1. Środowisko Visual C# Express Edition
Kompilatory Obowiązkowym narzędziem programisty jest kompilator. To on umożliwia generowanie aplikacji wykonywalnych na podstawie kodu języka. Środowisko Visual C# Express Edition (ale również Borland Developer Studio) ma wbudowaną obsługę kompilatora języka C#. Napisałem „wbudowaną obsługę”, gdyż kompilator C# jest dostarczany za darmo wraz ze środowiskiem .NET
Framework (o platformie .NET opowiem w kolejnym rozdziale). Jest to aplikacja o nazwie csc.exe, wykonywalna z linii poleceń, dlatego też dosyć niewygodna. W związku z tym polecam środowisko Visual C# Express Edition, które umożliwia proste kompilowanie napisanej aplikacji. Jego także będę używał w trakcie prezentowania przykładów w niniejszej książce.
Mity związane z programowaniem W ludzkiej świadomości istnieje wiele mitów i przekłamań związanych z programowaniem. Na samym początku nauki pragnę wyjaśnić pewne wątpliwości, które mogą zrodzić się w Twojej głowie.
Nauka programowania zajmuje wiele lat Każdy z nas ma indywidualną zdolność do nauki. Zależy również, kiedy mogę powiedzieć, że znam dany język. Czy umiejętność pisania prostych programów wystarcza, abym mógł o sobie powiedzieć, że jestem programistą? To również sprawa indywidualna. Chociaż programuję wiele lat, wciąż się uczę, poznaję nowe techniki oraz technologie. Jeżeli mówimy o umiejętności pisania aplikacji na własne potrzeby, to to nieprawda, że nauka programowania zajmuje wiele lat. Obecne języki programowania są tak skonstruowane, aby można było jak najszybciej wykorzystać ich możliwości. Jeżeli mówimy o doświadczeniu, które należy nabyć poprzez ciągłe pisanie aplikacji, to tak — nauka trwa długo. Dobrym programistą zostaje się po kilku latach doświadczenia.
Narzędzia programistyczne są drogie Fakt — są drogie, lecz istnieją darmowe alternatywy. Np. bardzo popularny zestaw kompilatorów GCC jest zupełnie darmowy, również do zastosowań komercyjnych. Niektóre firmy, takie jak Microsoft czy Borland, mają zaawansowane, drogie narzędzia programistyczne, lecz udostępniają również ich uboższe wersje darmowe (nie do zastosowań komercyjnych).
Wszystkie języki są podobne
To nieprawda. Sama idea programowania jest taka sama, w wielu językach stosuje się podobne mechanizmy, ale stwierdzenie, że wszystkie są podobne, jest dużym uogólnieniem. Jeżeli pytasz, czy łatwiej nauczyć się programowania, mając już doświadczenie w innym języku, to tak — jest to prawda. Szczególnie języki Java, JavaScript, C, C++ oraz C# są do siebie na tyle podobne, że nauka drugiego, kiedy znamy już jeden, nie powinna stanowić problemu.
Języki programowania Jak już powiedziałem, kod programu musi być zapisany w określonym języku programowania (np. w tej książce prezentuję programy w utworzone w C#). Istnieją dziesiątki języków programowania — bardziej lub mniej popularne oraz bardziej lub mniej skomplikowane. Każdy z tych języków wyróżnia się inną składnią i innym sposobem działania. W swojej „karierze” możesz się spotkać z następującymi językami:
Ada, Asembler, Algol, Basic, QBasic, QuickBasic, Turbo Basic, Visual Basic, C, C#, C++, Clipper, COBOL, Fortran, Haskel, Java, JavaScript, LISP, Logo, PHP, Pascal, Perl, Python, Prolog, Smalltalk, Tcl.
To jest tylko przykładowa lista dostępnych języków programowania. Pokrótce omówię te z
nich, które miały duży wpływ na rozwój informatyki.
Asembler Asembler jest niskopoziomowym językiem programowania zaprojektowanym w latach 1947 – 1952 przez Grace Hopper. Asembler był w gruncie rzeczy rewolucyjny — uwalniał programistę od konieczności zapisywania programów w formie binarnej. W tym języku jedno polecenie praktycznie odpowiada jednemu rozkazowi procesora. Obecnie Asembler jest uważany za trudny język programowania (w porównaniu np. z Delphi czy C#), ale wciąż bywa używany. Podstawową zaletą Asemblera jest to, że daje niesamowite możliwości w manipulowaniu komputerem. Większość języków wysokiego poziomu pozwala na włączanie wstawek Asemblera do swojego kodu. W praktyce wygląda to tak, że podczas pisania kodu — np. w Pascalu — możemy do tworzonego programu dołączyć fragment kodu Asemblera. W ten sposób większość obecnych programów nie jest pisana w całości w Asemblerze (ze względu na dość skomplikowaną składnię), ale zawiera wstawki tego języka. Przykładowo, system operacyjny Linux był pisany w języku C, mimo to nie obeszło się bez zastosowania fragmentów kodu Asemblera. W powyższym opisie znajdują się dwa pojęcia — język wysokiego oraz niskiego poziomu. W praktyce czynnikiem rozróżniającym język poziomu wysokiego od niskiego jest złożoność. Przykładowo, Pascal jest językiem wysokiego poziomu, bo zawiera gotowe funkcje i procedury (np. do wyświetlania tekstu na ekranie służy komenda writeln) i dzięki temu jest łatwy do nauczenia się. Również projektowanie aplikacji w językach wysokiego poziomu jest o wiele szybsze niż w Asemblerze. W Asemblerze w celu wyświetlenia tekstu na ekranie nie obejdziemy się bez obsługi odpowiedniego przerwania. Pojęcie przerwania związane jest z systemem operacyjnym, a konkretnie z procesorem. Przerwanie powoduje wstrzymanie aktualnie wykonywanego programu i wykonanie przez procesor kodu procedury obsługi. Jest to pojęcie związane z bardziej zaawansowanymi aspektami programowania, nie musisz teraz się tym przejmować.
Fortran Nazwa Fortran jest utworzona od angielskich słów Formula Translator. Fortran jest uważany za pierwszy język wysokiego poziomu. Został zaprojektowany w latach 50. XX wieku przez Johna Backusa i zespół IBM. Fortran jest wykorzystywany przede wszystkim do pisania programów służących do analizy numerycznej i do wykonywania skomplikowanych obliczeń naukowych. Język ten zyskał niesamowitą popularność i jest używany do dzisiaj (jako nowy, ulepszony język programowania — Fortran 90, Fortran 95 oraz Fortran 2000).
C
Rok 1969 był rewolucyjny w informatyce. Wszystko za sprawą dwóch osób — Kena Thompsona oraz Dennisa Ritchiego. Zaczęli oni pracę nad nowym systemem operacyjnym — UNIX. Przy tej okazji Dennis Ritchie na bazie języka B opracował język C, który miał ułatwić pisanie tego systemu operacyjnego. W 1973 w języku C utworzono jądro systemu UNIX, co było wielkim sukcesem. Języka C używano na początku jedynie w firmie Bell Labs (gdzie powstał), jednak wkrótce stał się dominującym językiem programowania, wykorzystywanym na całym świecie — i tak jest do dzisiaj. Język C charakteryzuje się wysoką wydajnością, jest stosunkowo prosty (o wiele prostszy od Asemblera), a jednocześnie niezwykle elastyczny.
C++ W roku 1983 Bjarne Stroustrup zmodyfikował język C, dodając do niego nowe funkcje. Przede wszystkim rewolucyjną zmianą w porównaniu z C jest możliwość programowania obiektowego. To zagadnienie zostanie dokładniej omówione w dalszej części książki. C++ (na początku miał nosić nazwę „C z klasami”) jest również niezwykle popularnym językiem programowania, często stosowanym przez programistów na całym świecie. Języki C oraz C++ są do siebie bardzo podobne, więc jeśli znamy jeden z nich, łatwiej przyjdzie nam nauka drugiego.
Perl Perl (ang. Practical Extraction and Report Language) jest interpretowanym językiem programowania autorstwa Larrego Walla, bardzo często wykorzystywanym jako język skryptowy na stronach WWW. W zamyśle autora język Perl miał pomóc w rozwiązywaniu trudnych zadań oraz przyspieszać pracę nad programem. Perl nie jest kompilowanym językiem programowania. Oznacza to, że nie następuje kompilacja programu, czyli przekształcanie kodu napisanego przez programistę do postaci pliku wykonywalnego. Zamiast tego w komputerze musi być zainstalowany tzw. interpreter, który będzie odczytywał instrukcje języka i odpowiednio na nie reagował. Naturalnym środowiskiem Perla jest system UNIX, aczkolwiek język ten jest także dostępny w wersjach przeznaczonych na inne platformy. Bardzo często jest wykorzystywany podczas tworzenia skryptów CGI przeznaczonych dla stron WWW. Obecnie Perl jest wypierany przez młodszy i prostszy język PHP, choć świetnie się nadaje się do pisania skryptów manipulujących plikami tekstowymi. Obecnie najnowszą, wciąż rozwijaną wersją tego języka jest Perl 6. CGI, czyli Common Gateway Interface, jest obecny praktycznie od początku istnienia internetu
i stron WWW. CGI umożliwia dynamiczne generowanie stron WWW, czyli w praktyce pisanie tak różnych skryptów jak księgi gości, fora dyskusyjne itp.
PHP PHP (angielski akronim rekurencyjny PHP Hypertext Preprocessor) jest językiem programowania, który w ostatnich latach robi oszałamiającą karierę. Autorem tego języka jest Rasmus Lerdorf, który napisał pierwszą jego wersję w roku 1995. PHP — podobnie jak Perl — jest interpretowanym językiem programowania, wykorzystywanym głównie do generowania dynamicznych stron WWW. Jego zaletą jest niezwykła prostota i jednocześnie duże możliwości. Ogólnie rzecz biorąc, PHP jest jednym z najprostszych języków programowania. PHP jest darmowy, oparty na licencji GPL (ang. GNU Public License) i rozwijany w dalszym ciągu przez setki programistów z całego świata.
Turbo Pascal Początki Pascala jako języka programowania sięgają początku lat 70. XX wieku. Opracowano go specjalnie do nauki programowania. Jest to bardzo prosty język, dający jednocześnie duże możliwości. W roku 1983 firma Borland wypuściła na rynek komercyjny produkt o nazwie Turbo Pascal i ta data jest uważana za przełomową. Turbo Pascal natychmiast zyskał popularność, którą w dalszym ciągu utrzymuje. Także dzisiaj Turbo Pascal jest zalecany do nauki programowania dla początkujących: uczy myślenia programistycznego. Jest wykorzystywany również na różnych szczeblach nauczania: na studiach czy w szkołach średnich do rozwiązywania problemów i tworzenia algorytmów.
Java Język Java został opracowany przez firmę Sun Microsystems. Każdy z pewnością nieraz słyszał o tym języku, gdyż ostatnio stał się on bardzo popularny, głównie dzięki niezależności od systemu operacyjnego. Z reguły programy napisane i skompilowane dla platformy Windows nie będą działały w innych systemach, np. w Linuksie. W przypadku Javy jest inaczej, ponieważ kod źródłowy nie jest kompilowany na kod maszynowy, tak jak to ma miejsce w innych językach programowania, ale zostaje poddany procesowi translacji na kod pośredni. Ten z kolei zostaje poddany procesowi interpretacji przez tzw. maszynę wirtualną, która musi być zainstalowana w systemie.
Można powiedzieć, że maszyna wirtualna jest programem, który wykonuje inne programy. W przypadku Javy maszyna wirtualna interpretuje kod pośredni. Maszyna wirtualna Javy została zaimplementowana dla większości systemów operacyjnych i dzięki temu ten język programowania jest tak uniwersalny. O maszynach wirtualnych opowiem w kolejnych rozdziałach tej książki.
Język maszynowy Na ostatnich stronach padło wiele nowych pojęć, takich jak kompilator, kompilacja, translacja czy język interpretowany. Aby nieco usystematyzować tę wiedzę, powiem kilka słów o tym, jak właściwie działają aplikacje i czym jest język maszynowy. Typowy program przeznaczony na platformę Windows ma rozszerzenie .exe i zwykło się o nim mówić plik wykonywalny. Jest to plik w formacie PE (ang. Portable Executable) zawierający m.in. skompilowaną wersję kodu źródłowego, tzw. kod maszynowy. Kod maszynowy jest postacią gotową do bezpośredniego (lub pośredniego) wykonania przez procesor. Oprócz kodu maszynowego plik wykonywalny zawiera również informacje charakterystyczne dla systemu, stąd niemożliwe jest uruchomienie takiej aplikacji na innych systemach operacyjnych. Typowym żądaniem uruchomienia aplikacji jest podwójne kliknięcie ikony reprezentującej dany program. Wówczas system operacyjny ładuje taką aplikację do pamięci, skąd jest wykonywana przez procesor. W każdym bądź razie najważniejsze jest, abyś zapamiętał, iż kod maszynowy to skompilowana wersja kodu źródłowego gotowa do wykonania. Istnieje również pojęcie języka maszynowego. Jest to również język programowania polegający na zapisywaniu instrukcji jako liczb, które są rozkazami pobieranymi przez procesor. Teoretycznie rzecz biorąc, istnieje możliwość operowania na skompilowanym kodzie maszynowym. W taki sposób powstawały pierwsze aplikacje, gdy nie istniały kompilatory (pierwszy kompilator musiał zostać napisany w kodzie maszynowym). Dawniej więc programowanie było profesją, którą zajmowali się najlepsi, gdyż była to czynność niesamowicie trudna. Taka technika nie jest już wykorzystywana (lub szczególnie rzadko), nie będziemy już do niej wracać w dalszej części książki.
Działanie kompilatorów Wiesz już, czym są kompilatory. Ważne dla zrozumienia dalszej części książki jest poznanie zasad ich działania.
Obecnie kompilatory są niezwykle szybkie — pozwalają na skompilowanie programu nawet w kilka milisekund (w zależności od rozmiarów kodu). Jednym z najpopularniejszych na świecie darmowych kompilatorów jest GCC (ang. GNU Compiler Collection), czyli darmowa kolekcja kompilatorów. Popularność GCC jest na pewno związana z kilkoma czynnikami:
ceną (kompilator jest darmowy), niezawodnością (kompilator jest tworzony od ponad 10 lat przez rzeszę programistów), dostępnością dla różnych platform (Windows, Linux, DOS, UNIX),
możliwością kompilacji kodu napisanego w różnych językach (Java, C, C++ i inne).
Pierwsza wersja GCC, napisana przez Richarda Stallmana, ukazała się w 1987 roku. Obecna wersja, oznaczona jako 3.3.2, jest udostępniona do pobrania ze strony http://gcc.gnu.org/. Można by zapytać, w jakim celu piszę o GCC czy o języku C — przecież nie jest to tematem niniejszej książki. To prawda, wspominam o tym tylko dlatego, że działanie kompilatora języka C# jest nieco inne, ale o tym opowiem w dalszej części książki.
Tworzenie kodu źródłowego To jest praca, która należy do programisty. Przed kompilacją programu trzeba napisać jego kod, używając danego języka programowania. Trzeba przy tym używać poleceń, które zostaną rozpoznane przez kompilator. Przed faktycznym momentem skompilowania programu kompilator sprawdza jego składnię — jeżeli napotka na nieznane mu polecenie, wyświetli błąd i zakończy działanie. Składnia to dział gramatyki. Bada zasady łączenia poszczególnych wyrazów w zdania. Programiści pod tym pojęciem rozumieją kod źródłowy aplikacji, poszczególne elementy, instrukcje, które łączą się w jedną całość.
Prekompilacja Ten proces polega na usuwaniu komentarzy z kodu źródłowego. Teraz następuje również odczytanie makrodefinicji języka C, które zaczynają się od słowa #define. Nie jest to jednak tematem tej książki i nie będziemy się tym szczegółowo zajmować.
Czytelnik, który miał wcześniej styczność z językiem C, zapewne wie, czym są komentarze czy makrodefinicje. Tematem komentarzy w językach programowania zajmiemy się w dalszej części książki.
Kompilacja do kodu Asemblera Proces ten polega na wyszukiwaniu słów kluczowych dla danego języka programowania. Potem następuje przekształcenie tego kodu na instrukcje Asemblera. W tym czasie, jeżeli kompilator wykryje jakieś błędy związane z kodem źródłowym, wstrzyma działanie i zasygnalizuje błąd.
Optymalizacja kodu Następnie kompilator analizuje kod Asemblera i poddaje go optymalizacji. Jest to opcjonalna funkcja w kompilatorach GCC, lecz warto ją wykorzystywać, gdyż często jest przydatna. Należy też wspomnieć, że możliwość optymalizacji kodu jest dość zaawansowaną funkcją kompilatora. Optymalizacja jest procesem poprawiania kodu programu, tak aby jego wykonywanie było jak najbardziej efektywne, a sam program zajmował możliwie jak najmniej pamięci. Optymalizacja powinna należeć do programisty jako wyrobiony nawyk pisania efektywnych programów, lecz w obecnych czasach maszyna znacznie ułatwia tę część pracy.
Asemblacja Zoptymalizowany kod Asemblera może zostać poddany asemblacji. Proces asemblacji jest związany z przekształcaniem kodu Asemblera na kod maszynowy.
Konsolidacja Konsolidator, nazywany też często linkerem, tworzy aplikację wykonywalną, którą można uruchomić w sposób niezależny od kompilatora i — przykładowo — przetestować jej działanie. Innymi słowy, linker tworzy plik .exe gotowy do użycia.
Języki interpretowane Istnieją języki, do których nie ma kompilatorów. Takie aplikacje nie są kompilowane do pliku .exe. Program jest cały czas „przechowywany” w postaci kodu źródłowego. Na maszynie, na której taka aplikacja ma zostać uruchomiona, musi być zainstalowany tzw. interpreter. Każdorazowo sprawdza on składnię programu i na bieżąco wykonuje instrukcje zapisane w kodzie źródłowym. Przykładem języków interpretowanych jest PHP i Perl.
Język C# Język C# 1 (czytaj: si szarp) jest nowym językiem programowania zaprojektowanym przez firmę Microsoft i ściśle związanym z platformą .NET. Łączy w sobie zalety języków C++ oraz Java, jest do nich bardzo podobny, co ułatwia jego naukę osobom, które wcześniej miały do czynienia ze wspomnianymi językami. Głównym architektem języka jest Anders Hejlsberg, duński programista. Wcześniej pracował on dla firmy Borland, gdzie uczestniczył w tworzeniu środowiska Turbo Pascal oraz Delphi. C# jest dość specyficznym językiem, jak wspomniałem — ściśle związanym z platformą .NET. Aplikacje pisane w tym języku wymagają środowiska uruchomieniowego CLR, które zostanie omówione w dalszej części książki.
Instalacja środowiska W trakcie pisania przykładów do tej książki będę korzystał z darmowego środowiska Visual C# 2005 Express Edition, które możesz pobrać ze strony http://msdn.microsoft.com/vstudio/express/visualcsharp/download/. Po pobraniu i uruchomieniu pliku instalacyjnego (o rozmiarach kilku MB) nasz system zostanie przygotowany do ściągnięcia środowiska .NET Framework 2.0 i Visual C# Express Edition oraz systemu pomocy. W sumie będzie to nieco ponad 300 MB. Po ściągnięciu wszystkich potrzebnych plików uruchomiony zostanie instalator, który powinien bezproblemowo zainstalować wszelkie aplikacje potrzebne do tego, aby zacząć programować.
Jak się uczyć
Jak się uczyć programowania? Każdy ma inne sposoby, mniej lub bardziej skuteczne. Wziąłeś tę książkę do ręki zapewne dlatego, że interesujesz się przedstawioną w niej tematyką i dzięki temu nauka będzie przychodziła Ci łatwiej. Gorzej, jeżeli musisz nauczyć się programowania w C#, ponieważ tak nakazał Ci szef — w takich przypadkach nauka będzie trudniejsza. To naturalne — oswojenie się z tym, co się lubi, co sprawia przyjemność, jest w gruncie rzeczy łatwe. Tak samo jest z C#. Jeżeli interesujesz się programowaniem, to nauka tego języka będzie przyjemnym doświadczeniem.
Nie ucz się na pamięć! Na pewno nie należy wkuwać wszystkiego i lepiej nie starać się zapamiętać całego prezentowanego tu materiału — w każdym momencie można przecież cofnąć się do poprzedniego rozdziału!
Początkowe trudności Jeżeli jesteś naprawdę początkujący w tej dziedzinie, to przez jakiś czas może Ci być ciężko. Nie mówię tego, aby kogokolwiek zniechęcać, lecz raczej aby pocieszyć — tak jest w wielu przypadkach, więc nie ma się czym przejmować. Z czasem wiele kwestii stanie się bardziej czytelnych i łatwiejszych. Może się bowiem zdarzyć, iż na początku tej książki będę wspominał o sprawach, które zostaną omówione dopiero w dalszych rozdziałach (aczkolwiek będę się starał unikać niezrozumiałych pojęć), tak więc nie należy się przejmować, jeśli coś wydaje się niejasne — dalej znajdą się odpowiedzi.
Pomoc systemowa Zawsze można skorzystać z systemu pomocy! Najważniejszym klawiszem skrótu jest F1, który powoduje uruchomienie systemu pomocy. Niestety, pomoc jest w całości napisana w języku angielskim, tak więc jest niezbędna jest przynajmniej podstawowa umiejętność czytania w tym języku.
Praktyka Praktyka czyni mistrza! Każdy na pewno nieraz słyszał to przysłowie. Warto się do niego stosować. Po zapoznaniu się z jakąś nową funkcją warto sprawdzić jej działanie w praktyce
— jest to dobry sposób na opanowanie nowego materiału.
Pierwsza aplikacja Nasz pierwszy program nie będzie realizował żadnych konkretnych zadań. Będzie to typowe okno systemu Windows z przyciskiem. Po naciśnięciu przycisku wyświetlone zostanie nowe okno z napisem Hello World. Uruchom środowisko Visual C# 2005 Express Edition (jeżeli chcesz, możesz skorzystać z bardziej zaawansowanych wersji Visual Studio). 1. 2. 3. 4.
Z menu File wybierz New Project. Zaznacz ikonę Windows Application. W polu Name wpisz HelloApp. Naciśnij OK.
W tym momencie został utworzony nowy projekt typu Windows Forms. Okno o nazwie Form1 to okno naszej aplikacji. W trakcie projektowania programu mamy możliwość obserwacji, jak taka aplikacja będzie wyglądała w trakcie działania. Po lewej stronie ekranu znajduje się pozycja Toolbox. Gdy naprowadzimy na nią myszkę, rozwinie się okno Toolbox zawierające spis tzw. komponentów (rysunek 1.2) lub kontrolek (takie określenie również jest często stosowane).
Rysunek 1.2. Lista komponentów Odnajdź, a następnie kliknij raz pozycję Button, aby ją podświetlić. Teraz wystarczy raz kliknąć obszar formularza (Form1), aby umieścić w naszej aplikacji przycisk (rysunek 1.3).
Rysunek 1.3. Przycisk umieszczony na formularzu Komponent (owy przycisk) można przesuwać oraz zmieniać jego rozmiar, chwytając za jego narożniki. Z menu Format wybierz pozycję Center in Form, a następnie Horizontally, co spowoduje umieszczenie przycisku centralnie po środku formularza.
Kompilacja i uruchamianie Na razie nasz program składa się jedynie z przycisku. Poza tym nic nie robi, bo naciśnięcie przycisku nie powoduje żadnej reakcji. Jednak już teraz możesz uruchomić nasz projekt i sprawdzić jego działanie. Jest to bardzo proste. Wystarczy z menu Debug wybrać Start Debugging lub nacisnąć przycisk F5. Środowisko Visual C# Express Edition oprócz edytora kodu i edytora formularzy zawiera również debuger oraz kompilator. Naciśnięcie klawisza F5 spowoduje skompilowanie programu, a następnie jego uruchomienie. Po chwili nasza aplikacja w postaci okna z przyciskiem powinna zostać uruchomiona. Naturalnie jest to bardzo prosty program, który nie realizuje żadnego zadania. Zwróć uwagę, że zaprojektowaliśmy (stworzyliśmy) program, nie pisząc ani jednej linii kodu!
Komponenty W zaprezentowanym przykładzie skorzystaliśmy z komponentu Button. W tym miejscu należą Ci się wyjaśnienia, czym właściwie jest komponent. Typ aplikacji, który przed chwilą
stworzyliśmy, nazywany jest aplikacją typu Windows Forms. Więcej na ten temat dowiesz się z lektury rozdziału 2. oraz 10. Istotne jest to, że projektowanie aplikacji tego typu umożliwia stosowanie podstawowych kontrolek systemu Windows, takich jak przyciski, listy rozwijane, pola edycyjne, panele itp. Umożliwia to projektowanie interfejsu aplikacji. Można więc powiedzieć, że komponenty są klockami umożliwiającymi szybkie projektowanie aplikacji.
Właściwości komponentów Każda kontrolka (komponent) posiada właściwości, które umożliwiają ustalenie jej specyficznych cech, takich jak położenie, kolor, czcionka itp. Spis właściwości komponentów znajduje się w oknie Properties (rysunek 1.4), które standardowo umiejscowione jest w prawym dolnym rogu środowiska Visual C# Express Edition.
Rysunek 1.4. Spis właściwości z zaznaczoną cechą Text Okno Properties podzielone jest na dwie kolumny. Kolumna po lewej zawiera nazwę właściwości, a ta po prawej — jej wartość. Wykonajmy proste ćwiczenie polegające na zmianie wartości właściwości Text komponentu Button. 1. Zaznacz komponent Button (pojedynczym kliknięciem), który w poprzednim ćwiczeniu umieściłeś na formularzu. 2. Na liście właściwości odszukaj Text, która określa tekst wyświetlany na przycisku. 3. Kliknij podwójnie nazwę właściwości. Spowoduje to zaznaczenie kolumny po prawej, która umożliwia wprowadzenie nowej wartości. 4. Wpisz w polu tekst Kliknij mnie i naciśnij Enter.
Jeżeli wszystko wykonałeś zgodnie z opisem, tekst znajdujący się na przycisku został zmieniony.
Piszemy kod Mamy już formularz, przycisk, lecz brakuje nam reakcji na naciśnięcie go. Musimy wobec tego oprogramować odpowiednie zdarzenie (reakcje) na wciśnięcie kontrolki. Kliknij dwukrotnie komponent Button. Zostaniesz przeniesiony do edytora kodu, gdzie należy wpisać kod, który zostanie wykonany, w momencie gdy użytkownik naciśnie przycisk. My chcemy, aby wyświetlone zostało okienko, co realizuje poniższy kod: MessageBox.Show("Hello World");
Cały kod źródłowy naszego programu prezentuje listing 1.1. Listing 1.1. Kod źródłowy pierwszej aplikacji using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace HelloApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { MessageBox.Show("Hello World"); } } }
Teraz możesz już uruchomić program klawiszem F5 i przetestować jego działanie. Rysunek 1.5 prezentuje aplikację w trakcie działania.
Rysunek 1.5. Pierwsza aplikacja Windows Forms
Zapisywanie projektu Moje gratulacje! Właśnie napisałeś swój pierwszy program w C#. Myślę, że warto by było zapisać go na pamiątkę. Aby zapisać projekt, z menu File wybierz Save All. W tym momencie musisz wybrać katalog, w którym zapisane zostaną pliki projektu (przycisk Browse). Zapisanie projektu nastąpi po naciśnięciu przycisku Save. W tym momencie w wybranym katalogu zostanie utworzony szereg plików i folderów. Ich zawartość w tym momencie jest nieistotna. Najważniejsze są pliki z rozszerzeniem *.cs, które zawierają rzeczywisty kod źródłowy naszego programu.
Podsumowanie Po przeczytaniu tego rozdziału powinieneś mieć podstawową wiedzę na temat programowania. Najważniejsze jest, abyś wiedział już, czym jest kompilator, kod źródłowy czy proces kompilacji. Powinieneś mieć zarys tego, jak działają kompilatory. W tym rozdziale zarysowałem jedynie tematykę programowania oraz poruszyłem podstawowe pojęcia, które będę rozwijał w dalszej części książki. [1] Powszechnie nazwę języka zapisuje się jako C#, czyli używa się znaku # (ang. hash), a nie znaku krzyżyka (ang. sharp) używanego w notacji muzycznej. Spowodowane jest to brakiem znaku sharp w wielu czcionkach.
Rozdział 2 Podstawy platformy .NET W poprzednim rozdziale opisałem, na czym polega programowanie. Nadszedł czas, aby zgłębić tę wiedzę i dowiedzieć się, czym jest programowanie dla platformy .NET. Nazwa wydaje się dość dziwna, niewątpliwie kojarzy się z internetem. I prawidłowo! Ta technologia ma sporo wspólnego z globalną siecią. Internet odgrywa coraz większą rolę w naszym życiu. Nie służy już tylko do przeglądania statycznych stron WWW. Teraz internet to głównie komunikatory oraz dynamiczne strony WWW, które umożliwiają większą interakcję z użytkownikiem. Przykładowo, po otwarciu strony traktującej o gospodarce uzyskujemy na bieżąco wgląd w najnowsze notowania akcji, kursy walut czy ciekawostki. Po rejestracji w takim serwisie można otrzymywać codzienne informacje kierowane na wskazany telefon komórkowy czy skrzynkę e-mail. Strony WWW dostarczają teraz coraz bardziej interesującej rozrywki dzięki animacjom czy grom wykonanym za pomocą technologii Flash. Wiele serwisów umożliwia rejestrację, w której można ustalać swoje preferencje, takie jak wygląd strony czy korzystanie z rozmaitych opcji. Obecnie nie istnieje jednak technologia pozwalająca na swobodne komunikowanie się ze sobą serwisów internetowych. Np. przeglądając oferty sklepu muzycznego, chcielibyśmy zamówić bilet na koncert ulubionego zespołu. Takie zamówienia obsługuje jednak inna witryna. Oczywiście sklep muzyczny może oferować swoim klientom kupno biletu na koncert poprzez podanie np. odnośnika do strony, na której można zamówić bilet, lub pośredniczenie w sprzedaży. Nie jest to jednak idealne i wygodne rozwiązanie. Czy nie lepiej byłoby, gdyby witryny mogły się ze sobą komunikować? Wyobraźmy sobie taki scenariusz: nasza strona — sklep muzyczny — wysyła zapytanie do serwisu organizatora koncertu i sprawdza dostępność biletów na występ danego wykonawcy wraz z cenami oraz terminami. W takim przypadku wszystko odbywałoby się automatycznie — nie jest potrzebna jakakolwiek ingerencja człowieka, gdyż informacje są stale pobierane ze strony organizatora koncertu. Między innymi takie scenariusze zakłada platforma .NET. Chodzi o integrację usług, sprzętu i innych urządzeń, takich jak palmtopy i telefony komórkowe. Microsoft chce, aby aplikacje można było uruchamiać w telefonach, aby programy te mogły pobrać aktualne dane z internetu (na przykład aktualne terminy koncertów). O tym jednak opowiem nieco dalej… najpierw przecież należy wyjaśnić, w jaki sposób odbywa się programowanie dla systemu Windows.
Interfejs programistyczny Kolejnym pojęciem, które trzeba znać, jest API, czyli programistyczny interfejs aplikacji
(ang. Application Programming Interface). To pojęcie będzie się często pojawiało w tej książce. Mówiąc o API, mam na myśli interfejs w kontekście systemu Windows. Wyobraźmy sobie, że chcemy w naszym programie pobrać współrzędny wskaźnik myszy. System Windows udostępnia w tym celu funkcję GetCursorPos. Możemy ją wykorzystać w każdej aplikacji, która będzie działać w systemie Windows. Aby w danym momencie pobrać współrzędne wskaźnika myszy, program zgłasza się do systemu operacyjnego z „prośbą” o udostępnienie tych informacji. W takim przypadku sprawa jest prosta — wystarczy jedno polecenie, aby otrzymać współrzędne wskaźnika myszy. Nie trzeba programować własnych funkcji i głowić się nad sposobem pobrania owych współrzędnych. API jest zestawem funkcji, poleceń, które udostępnia aplikacja (w tym przypadku system Windows). Owe funkcje mogą być następnie wykorzystywane przez inne, także pisane przez użytkownika, programy. Przykładowo, API systemu Windows jest zestawem wielu poleceń, które zawarte są w bibliotekach DLL. Nasza aplikacja może wykorzystywać te funkcje, dzięki czemu programowanie staje się prostsze.
API systemu Windows Aby ułatwić nam pisanie programów dla systemu Windows, programiści Microsoftu udostępniają biblioteki DLL (z których najważniejszymi są: user32.dll, kernel32.dll, gdi32.dll). Biblioteki DLL z kolei zawierają funkcje, które możemy wykorzystywać. Oczywiście trzeba też wiedzieć, jakie nazwy noszą owe funkcje oraz w jaki sposób można z nich skorzystać. Informacje te umieszczono w dokumentacji udostępnionej na witrynie firmy Microsoft (http://msdn.microsoft.com). API systemu Windows nosi nazwę WinAPI (ang. Windows Application Programming Interface). Na początku, w systemach Windows 3.1., był to interfejs Win16 (systemy Windows 3.1. były 16-bitowe), a później — Win32 dla systemów Windows 95/98/NT/XP/2000. Interfejs Win32 jest wykorzystywany do dzisiaj przez wielu programistów i wiele aplikacji. Podsumowując:
Windows API (WinAPI) — to programistyczny interfejs (ang. application programming interface) używany w systemie operacyjnym Microsoft Windows. Innymi słowy, jest to specyfikacja procedur i funkcji służących do komunikowania się programów z systemem operacyjnym. Win16 — 16-bitowa wersja WinAPI. Win32 — 32-bitowa wersja WinAPI używana w systemach Windows do dzisiaj. Jest to zbiór funkcji napisanych w języku C, które są umiejscowione w bibliotekach DLL — np. kernel32.dll, user32.dll itd.
Teraz najważniejsze: programując w C#, zasadniczo nie korzysta się z tych funkcji
zgromadzonych w bibliotekach DLL (WinAPI). WinAPI systemu Windows uważane jest obecnie za przestarzałe, w kolejnych systemach operacyjnych Microsoftu pozostanie jedynie ze względów kompatybilności. Wszak aplikacje, które programiści piszą obecnie, korzystają z tych funkcji, zgromadzonych w licznych bibliotekach DLL. Gdyby zabrakło ich w nowych wersjach systemu, takie programy zwyczajnie przestałyby działać. W nowym systemie operacyjnym, nazwanym Longhorn 1 (nazwa kodowa ), obecny WinAPI zostanie zastąpiony nowoczesnym , bardziej przyjaznym i potężniejszym interfejsem WinFX. Platforma .NET ma być łącznikiem pomiędzy obecnym modelem programowania (WinAPI) a nowym interfejsem WinFX. Ma stopniowo, powoli przyzwyczajać programistów do nowego stylu programowania. Skoro w C# nie korzystamy z WinAPI, to czemu o tym wspominam? Chcę, abyś miał odpowiedź na pytanie, skąd się biorą te wszystkie funkcje, które udostępnia język programowania. Jak to jest, że dane polecenie umożliwia pobranie współrzędnych myszy, a inne wyłączenie wygaszacza ekranu. W rzeczywistości nie są to funkcje/polecenia, które udostępnia język programowania, lecz te udostępniane przez system operacyjny! Te same polecenia można wykorzystać w innych językach programowania.
Wizja .NET W lipcu 2000 roku Microsoft zwołał konferencję programistów, na której zademonstrowano .NET. W zamierzeniu .NET jest platformą nowej generacji, przeznaczoną do tworzenia aplikacji Windows. Bill Gates w swoim przemówieniu porównał powstanie platformy .NET do przejścia z systemu DOS do Windows 3.1., a później z przejściem do Windows 95. W rzeczywistości .NET nie jest nowym systemem operacyjnym ani językiem programowania. Jest podstawą, pewna ideą, sposobem budowania aplikacji dla systemu Windows. Platforma .NET to po prostu nowe podejście do tworzenia oprogramowania. Jednym z założeń platformy .NET jest porzucenie dotychczasowego modelu Win32 i zastąpienie go całkowicie nowym, prostszym oraz bardziej zaawansowanym. W poprzednim rozdziale opisałem proces instalowania platformy .NET. W rzeczywistości podczas tej operacji instalator kopiuje na dysk twardy komputera szereg bibliotek DLL, aplikacji dla programistów oraz dokumentację. Dzięki temu można korzystać z nowych narzędzi oferowanych przez Microsoft. Windows 2003 standardowo jest wyposażony w platformę .NET, więc nie ma potrzeby ponownej instalacji. Zgodnie z definicją firmy Microsoft: .NET jest stworzonym przez firmę Microsoft rozwiązaniem dla usług sieciowych, stanowi nową generację oprogramowania łączącego się z naszym światem informacji, urządzeniami i użytkownikami w jednolity, spersonalizowany sposób. Taka definicja zapewne wydaje się trochę niejasna, dlatego w tym rozdziale postaram się
objaśnić kilka kwestii związanych z platformą .NET. Istotnymi założeniami, które zdecydowały o powstaniu platformy .NET, były:
prostsze tworzenie oprogramowania, możliwość komunikowania się aplikacji, uproszczone wdrażanie aplikacji.
Składniki platformy .NET Platforma .NET składa się z kilku składników:
środowiska .NET Framework, narzędzia dla programistów, serwerów .NET Enterprise.
Środowisko .NET Framework Bardzo ważna uwaga: programy pisane w C# wymagają, aby na komputerze, na którym będą uruchamiane, było zainstalowane środowisko .NET Framework. Jeżeli taki program działa na danym komputerze, to znaczy, że zainstalowano pakiet .NET Framework albo pecet pracuje pod kontrolą Windows 2003, gdzie .NET Framework jest zainstalowany domyślnie. Środowisko .NET Framework jest podstawowym składnikiem platformy .NET. Mówiąc najprościej, są to biblioteki, technologie wspierające działanie programu, odpowiedzialne za zarządzanie pamięcią aplikacji itp. Należy w tym miejscu zaznaczyć, że programowanie dla platformy .NET różni się od standardowego modelu programowania dla Win32. Środowisko .NET Framework zawiera nowe biblioteki, funkcje, które znacząco różnią się od tych udostępnianych przez Win32. Można powiedzieć, że programowanie dla .NET jest prostsze od programowania dla Win32, gdyż platforma .NET odciąża programistę od zadań, które wcześniej spoczywały na jego głowie (np. zwalnianie pamięci), lecz o tym będziemy mówili w dalszej części książki. Platforma .NET przyciąga coraz większą liczbę programistów, którzy chcą spróbować swoich sił i modyfikują swoje programy, aby działały pod kontrolą środowiska .NET Framework. Zdobywanie popularności przez środowisko jest jednak procesem długofalowym. Minie jeszcze sporo czasu, zanim większość porzuci standardowy model programowania pod Win32.
Narzędzia dla programistów Wraz z dostarczeniem technologii, jaką jest platforma .NET, firma Microsoft musiała udostępnić programistom także narzędzia, za pomocą których mogli oni „produkować” swoje aplikacje. W związku z tym powstał pakiet dla programistów o nazwie Visual Studio.NET. Dotychczas językiem programowania popieranym przez Microsoft był Visual Basic, przez wielu uważany za prosty i mało profesjonalny. Język ten został wzbogacony o nowe funkcje i możliwość obsługi platformy .NET (teraz Microsoft promuje go pod nazwą Visual Basic.NET). Możliwości języka Visual Basic zostały rozszerzone z myślą o programistach, którzy już od dawna używali tego narzędzia. Nie można było o nich zapomnieć, lecz trzeba było dać im ten sam produkt (poszerzony o nową funkcjonalność ), który pozwalałby na tworzenie programów dla platformy .NET. Językiem programowania, który powstał specjalnie na potrzeby platformy .NET, jest C#. Jest to język zbudowany na bazie C++ oraz Javy (o czym wspominałem w poprzednim rozdziale). Umożliwia obsługę wszystkich technologii oferowanych przez .NET Framework. Podstawy języka C# zostaną opisane w kolejnym rozdziale niniejszej książki. Narzędzia dla programistów to nie tylko języki programowania. To również nowa baza danych — MS SQL Server oraz technologia bazodanowa ADO.NET. Jeżeli chodzi o technologie związane z internetem, do dyspozycji pozostaje technologia tworzenia dynamicznych stron WWW — ASP.NET. Jest to technologia powstała na bazie ASP (ang. Active Server Pages). Strony ASP.NET mogą być pisane przy użyciu C#, Visual Basic.NET oraz jakiegokolwiek języka obsługującego platformę .NET.
Kompilator csc.exe Pamiętasz, jak w poprzednim rozdziale wspominałem o kompilatorze języka C# udostępnianym za darmo wraz ze środowiskiem .NET Framework? Nie miałem jeszcze okazji zaprezentować sposobu działania tego programu. W poprzednim rozdziale wspominałem również, iż do programowania nie jest potrzebny żaden specjalistyczny edytor. Wystarczy zwykły, najprostszy edytor tekstu, jak np. Notatnik z systemu Windows. Uruchom Notatnik i wpisz następującą treść programu: using System; class Welcome { static void Main() { Console.WriteLine("Cześć!"); Console.ReadLine();
} }
Plik o tej treści zapisz w katalogu, w którym znajduje się kompilator csc.exe. W moim wypadku jest to katalog C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727. Plik zapisz pod nazwą welcome.cs. Ten prosty program wyświetla na ekranie konsoli tekst Cześć!. 1. Z menu Start wybierz Programy/Akcesoria/Wiersz poleceń. 2. Wpisz komendę cd C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727, która spowoduje otwarcie katalogu z kompilatorem. 3. Następnie wpisz polecenie csc welcome.cs, które spowoduje skompilowanie programu.
Jeżeli wszystko poszło dobrze, czyli kod źródłowy nie zawiera błędów, na ekranie konsoli powinien pojawić się tekst: Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.42 for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727 Copyright (C) Microsoft Corporation 2001-2005. All rights reserved.
Teraz można w katalogu C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727 odszukać aplikację welcome.exe i spróbować ją uruchomić. Moje gratulacje! Właśnie wspólnie napisaliśmy pierwszy program konsolowy działający w środowisku .NET Framework w języku C# (rysunek 2.1). Program można zamknąć, naciskając klawisz Enter.
Rysunek 2.1. Program konsolowy napisany w C#
Opisując to ćwiczenie, założyłem, że kompilator csc.exe znajduje się w katalogu C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727, jednak wcale nie musi tak być we wszystkich komputerach.
Serwery .NET Enterprise Serwer .NET Enterprise jest oprogramowaniem, które może zostać użyte wraz z aplikacjami .NET. Dobrym przykładem jest baza SQL Server, która pozwala na wymianę informacji pomiędzy aplikacją a serwerem. Dane — np. informacje o klientach — mogą być zgromadzone na jednym komputerze (serwerze). Baza danych będzie obsługiwana przez serwer SQL Server. Klientem jest aplikacja, która łączy się z serwerem i pobiera aktualną listę klientów oraz umożliwia pobranie szczegółowych informacji na temat jednego z nich. Do innych serwerów .NET Enterprise zaliczamy m.in. oprogramowanie BizTalk Server czy Exchange. W tej książce nie będę jednak szczegółowo opisywał serwerów Enterprise.
Konkluzja Podsumujmy pokrótce, czym jest .NET:
.NET jest nową strategią firmy Microsoft. .NET wytycza nowe standardy programowania. .NET bazuje na infrastrukturze związanej z internetem. .NET ma w założeniu działać na każdym systemie lub platformie 2 . .NET to nie system operacyjny.
C# a .NET Podsumujmy więc naszą wiedzę dotyczącą języka C#:
C# jest językiem programowania na platformę .NET. Programując w C#, korzystamy z funkcji, jakie oferuje środowisko .NET Framework. Aplikacje napisane w C# wymagają, aby na komputerze zainstalowane było środowisko .NET Framework.
Chcąc pisać aplikację na platformie .NET, nie jesteś skazany jednie na język C#. Platforma .NET wspiera również inne języki, takie jak C++, Visual Basic.NET, Delphi i inne.
Rodzaje aplikacji W poprzednim rozdziale stworzyłeś swoją pierwszą aplikację w C#. Jak pamiętasz, program składał się z jednego formularza (okna). W takim wypadku mówimy o wykorzystaniu biblioteki Windows Forms. Owa biblioteka, będąca oczywiście częścią platformy .NET, umożliwia użycie tzw. komponentów, takich jak przyciski, listy rozwijane, pola edycyjne. Przed chwilą pokazałem, jak można stworzyć aplikację konsolową.
Aplikacje konsolowe Aplikacje konsolowe to programy uruchamiane w oknie konsoli. Tego rodzaju aplikacje nie posiadają interfejsu użytkownika i są przydatne, jeżeli chodzi o prosty program, który będzie używany przez wąską grupę ludzi posiadających wiedzę potrzebną do uruchomienia go i sterowania nim z linii poleceń. Przykład aplikacji konsolowej został przedstawiony w tym rozdziale wcześniej, gdy zaprezentowałem możliwość skompilowania prostego programu napisanego w C#.
Windows Forms W rozdziale 1. pokazałem, w jaki sposób utworzyć prosty program wizualny na podstawie formularza i z wykorzystaniem komponentu. Po uruchomieniu takiego programu użytkownik widzi standardowe okno Windows wraz z etykietą pośrodku. Tego typu aplikacje będą działać jedynie w przypadku, gdy na komputerze jest zainstalowane środowisko .NET Framework. Windows Forms (często nazywana w skrócie WinForms) jest biblioteką wizualną, umożliwiającą zaprojektowanie graficznego interfejsu użytkownika (GUI, czyli Graphical User Interface). WinForms jest częścią środowiska .NET Framework, tak więc takie komponenty jak Button (użyty w przykładzie z rozdziału 1.) mogą zostać użyte zarówno w Delphi, jak i w C# czy innym języku obsługiwanym przez platformę .NET (np. Visual Basic.NET). O bibliotece Windows Forms będzie mowa w rozdziale 10.
Formularze Web Forms Technologia .NET umożliwia tworzenie dynamicznych stron WWW z użyciem technologii ASP.NET. Chciałbym teraz wspomnieć o czymś takim jak formularze Web Forms. Tworzenie dynamicznych stron ASP.NET odbywa się w C# w sposób wizualny. Do dyspozycji pozostaje formularz (który odzwierciedla przyszłą stronę WWW) oraz zestaw kontrolek. Wykorzystanie owych komponentów opiera się na dynamicznym generowaniu kodu HTML dla strony. Projektowanie strony odbywa się wizualnie — programista umieszcza odpowiedni element na stronie (np. komponent — tabelę), a środowisko Visual Studio w tle generuje odpowiedni kod HTML, który na końcu będzie przetwarzany przez przeglądarkę. W tej książce nie będziemy zajmować się tworzeniem dynamicznych stron WWW przy pomocy ASP.NET. Czytelnika zainteresowanego tą technologią odsyłam do dokumentacji firmy Microsoft lub do książek traktujących o tej technologii.
Składniki .NET Framework W tym miejscu można by się zatrzymać i zapytać: „Zaraz, zaraz… wcześniej była mowa o składnikach platformy .NET, a teraz o składnikach platformy .NET Framework?”. Trzeba więc wyjaśnić, że zagadnienia związane z .NET są bardzo rozległe i całościowe ich omówienie w tej książce jest po prostu niemożliwe. Chciałbym jednak poruszyć kilka najważniejszych kwestii związanych z tą platformą. Nie można przy tym pominąć najważniejszego, czyli środowiska .NET Framework. Środowisko .NET Framework jest podstawową technologią budowania i uruchamiania aplikacji. Szczegółowo omówimy ją w rozdziale 4. Tutaj chciałbym wspomnieć jedynie o kilku jej składnikach:
Class Libraries (CL) — biblioteki klas. Są to gotowe komponenty, funkcje i biblioteki, które można wykorzystać we własnym programie. Wizualna biblioteka WinForms jest także częścią CL, podobnie jak szereg innych funkcji, które stopniowo będę omawiał w tej książce. Common Language Runtime (CLR) — wspólne środowisko wykonywalne (albo wspólne środowisko uruchomieniowe). Technologia CLR odpowiada za uruchamianie aplikacji .NET oraz ich prawidłowe działanie, a także za zwalnianie pamięci czy inne skomplikowane czynności. Szerzej o CLR opowiem w rozdziale 4. Common Type System (CTS) — wspólny system typów. Dzięki tej technologii jest możliwa współpraca pomiędzy różnymi aplikacjami na platformie .NET. Na razie nie musimy zaprzątać sobie tym głowy. Wystarczy tylko wiedzieć, że coś takiego istnieje.
Usługi sieciowe Usługi sieciowe są aplikacjami udostępnianymi poprzez interfejs WWW. Nie chodzi tutaj bynajmniej o strony WWW, gdyż aplikacje sieciowe mogą komunikować się ze sobą za pomocą mechanizmu zdalnego wywoływania procedur (ang. Remote Procedure Calls — RPC), przeważnie używając do tego protokołu HTTP. Dobrym przykładem jest wypożyczalnia DVD. Szef takiej firmy może podpisać umowę z właścicielami sklepów z płytami DVD, którzy na swoich stronach będą oferowali płyty z wypożyczalni. Wówczas serwisy właścicieli sklepów mogą komunikować się z usługą sieciową znajdującą się na serwerze przynależnym do wypożyczalni w celu pobrania aktualnej oferty, sprawdzenia, czy dany tytuł jest już wypożyczony i kiedy ma zostać zwrócony. Podobnie oprogramowanie zainstalowane w placówce/placówkach wypożyczalni może komunikować się z serwerem, pobierając aktualne dane o klientach czy wypożyczonych tytułach. Wszystkie dane znajdują się na jednym serwerze i są udostępniane przez usługę sieciową. Wszelkie zmiany w tytułach bądź produktach będą przeprowadzane jedynie na centralnym serwerze — nie zachodzi wówczas potrzeba aktualizacji oprogramowania w placówkach. Usługi sieciowe można także sprzedawać. Jeżeli ktoś napisze innowacyjną usługę wykorzystującą niepowtarzalny, nowatorski algorytm szyfrujący, może ją udostępniać w internecie. Klienci natomiast mogą dostosować odpowiednio swoje oprogramowanie, tak aby umożliwić wykorzystanie tej nowej usługi. W takim przypadku wszelkie poprawki czy naprawa błędów będą dotyczyły tylko tej jedynej usługi, a klienci nie muszą nawet o tym wiedzieć. Warunkiem jest jedynie zapewnienie dostępności takiej aplikacji przez internet. Inny przykład: autorzy popularnej wyszukiwarki Google udostępnili programistom jej interfejs. Dzięki usługom sieciowym można wykorzystać mechanizmy Google do przeszukiwania zasobów internetu. Następnie wyniki takiego wyszukiwania można zaprezentować w swoim programie lub stronie internetowej. Usługi sieciowe są uniwersalnym sposobem wymiany informacji pomiędzy aplikacjami czy stronami WWW. Teraz aplikacja nie musi składać się z tylko jednego modułu (pliku wykonywalnego .exe), ale może dzielić się na kilka mniejszych, które zostaną rozmieszczone na kilku serwerach. Centralna aplikacja, znajdująca się na komputerze klienta, może bezproblemowo łączyć się z serwerami i wymieniać dane oraz realizować zadania.
Niezależność Bardzo ważnym aspektem usług sieciowych jest ich niezależność od platformy językowej. Ważne jest, aby usługa działała pod kontrolą .NET Framework, jednak nieistotne jest to, w jakim języku została napisana. Może to być więc C#, Visual Basic.NET czy Delphi.
Aplikacja, która korzysta z danej usługi, również może być napisana w jakimkolwiek języku obsługiwanym przez .NET.
Uniwersalność A zatem już wiadomo, że usługa napisana w C# może być wykorzystywana przez aplikację napisaną w Delphi. Jak to się więc dzieje, że pomiędzy usługą a używającą jej aplikacją zachodzi swobodna komunikacja ? Chciałbym zasygnalizować kilka podstawowych pojęć związanych nie tylko z usługami, lecz ogólnie z platformą .NET. Komunikacja z usługami sieciowymi odbywa się poprzez sieć WWW, a konkretniej przez wykorzystywany tutaj protokół HTTP. Aplikacja może wysłać do usługi sieciowej zapytanie w formacie XML, a następnie otrzymać odpowiedź — również w tym formacie. Kluczową rolę w procesie komunikowania się z usługami sieciowymi odgrywa właśnie język XML opracowany przez konsorcjum W3C. Konsorcjum W3C (ang. World Wide Web Consorcium) jest organizacją wyznaczającą standardy technologii związanych z internetem (specyfikacje, wytyczne aplikacji itp.). Organizacja W3C jest odpowiedzialna m.in. za opracowanie standardu HTML, który obowiązuje do dzisiaj. Stanowi niewątpliwy autorytet w dziedzinie definiowania standardów sieciowych. Więcej informacji na temat konsorcjum można znaleźć na stronie www.w3c.org. Z usługami sieciowymi wiąże się kilka następnych pojęć:
XML — ang. eXtensible Markup Language. Jest to uniwersalny język znaczników przeznaczony do publikowania danych. Jego uniwersalność, niezależność od platformy oraz coraz większa popularność czynią go idealnym formatem do przesyłania danych pomiędzy usługami sieciowymi. SOAP — ang. Simple Object Access Protocol. SOAP jest językiem opartym na XML, wykorzystywanym przez usługi sieciowe do wywoływania procedur. Protokół SOAP określa format przesyłanych danych, nazwy parametrów itp. WSDL — ang. Web Service Description Language. WSDL jest również opartym na XML formatem opisu usługi sieciowej. WSDL służy do opisywania usługi, dostarcza użytkownikom informacji o jej przeznaczeniu i sposobie wykorzystania. UDDI — ang. Universal Description, Discovery and Integration. Jest to spis usług sieciowych udostępnianych publicznie.
Podsumowanie Uf! W tym rozdziale zaprezentowałem kolejną dawkę teoretycznej wiedzy z zakresu
platformy .NET. Bardziej szczegółowo o środowisku .NET Framework opowiem w rozdziale 4., teraz jednak powinieneś zapamiętać pewne podstawowe pojęcia, takie jak CLR czy CLS. Trzeba również odróżniać model programowania dla Win32 od modelu .NET.
[1] Nazwy kodowe nadawane są projektom we wczesnym stadium rozwoju, wtedy gdy nie wiadomo jeszcze, jaka będzie rzeczywista nazwa produktu. [2] Odniosę się w tym momencie do starego powiedzenia. Owszem, .NET ma działać na każdej platformie, ale pod warunkiem że będzie to produkt firmy Microsoft. Powstaje jednak darmowy projekt (Open Source), którego założeniem jest stworzenie platformy zintegrowanej z .NET dla systemu Linux. Projekt nosi nazwę dotGNU, a jego strona internetowa to www.dotgnu.org.
Rozdział 3 Podstawy języka C# Zawsze, na każdym kroku staram się podkreślać, iż tworzenie aplikacji nie opiera się jedynie na układaniu „klocków” (komponentów) w oknie formularza. Oczywiście, nowoczesne języki programowania (takie jak C#) oraz środowiska tworzenia aplikacji (Visual C# Express Edition) dają nam możliwość szybkiego oraz przyjemnego projektowania aplikacji, lecz nie na tym polega programowanie! Należy mieć wiedzę na temat podstawowych elementów języka programowania oraz podstawowych poleceń służących do sterowania pracą programu. W tym rozdziale zajmiemy się właśnie językiem C#. Celowo podkreśliłem słowo „język”, gdyż omówię podstawowe terminy programistyczne oraz elementy każdego wysokopoziomowego języka programowania. Odstawimy w tym miejscu na chwilę przyjemne i ładne projektowanie wizualne (przy pomocy komponentów) na rzecz aplikacji konsolowych. Wszystko dlatego, iż moim zdaniem prościej jest nauczyć się danego języka na przykładach zawierających jak najmniejszą ilość kodu, tak jak to ma miejsce w przypadku programów konsolowych. Składnia to specyficzne słowa kluczowe (elementy danego języka służące do sterowania pracą programu) oraz znaki, które muszą zostać zapisane w określonym porządku.
Podstawowa składnia Kod źródłowy musi składać się z poleceń zakończonych określonymi znakami. Nie można pozostawić w kodzie żadnego bałaganu — nawet pominięcie jednego znaku czy zwykła literówka mogą spowodować brak możliwości uruchomienia programu. Tak jak w języku polskim, pominięcie odpowiedniego znaku interpunkcyjnego jest błędem. Takie banalne z pozoru błędy mogą być prawdziwą udręką dla początkującego programisty i w konsekwencji spowodować spowolnienie prac nad programem. Z czasem wyrabia się pewien nawyk, dzięki
któremu popełnia się coraz mniej błędów, a nawet jeśli — to są one dla bardziej zaawansowanego programisty łatwiejsze do wykrycia. Na szczęście nowoczesne kompilatory potrafią bardzo precyzyjnie wskazać źródło błędu wraz ze szczegółowym komunikatem oraz numerem linii, w której on wystąpił. Zacznijmy więc. Utwórz swój pierwszy projekt aplikacji konsolowej. 1. Z menu File wybierz New Project. 2. Zaznacz ikonę Console Application. 3. Kliknij przycisk OK.
W tym momencie środowisko utworzy nowy projekt aplikacji konsolowej (rysunek 3.1). W edytorze kodu zostanie automatycznie wpisany startowy kod naszego programu. Taki kod możesz w tym momencie skompilować — nie zawiera on żadnych błędów i jest to praktycznie punkt startowy do rozwijania aplikacji.
Rysunek 3.1. Środowisko Visual C# z otwartym projektem Jak widzisz, kod źródłowy składa się ze specyficznych słów (np. using, namespace), symboli (np. nawiasy kwadratowe oraz okrągłe). Wszystko tworzy jedną spójną całość i nazywane jest składnią programu.
Najprostszy program Napiszmy najprostszy program w języku C#. W swoim projekcie musimy pozostawić pewne niezbędne elementy programu. Kod źródłowy najprostszego programu może wyglądać następująco: class Foo { static void Main(string[] args)
{ } }
Oczywiście taka aplikacja nic nie robi, zaraz po uruchomieniu (klawisz F5) zostanie zamknięta.
Jak kompilatory czytają kod Podczas kompilowania programu kompilator sprawdza najpierw, czy kod źródłowy nie zawiera błędów. Kod jest „czytany” linia po linii, począwszy od góry, tak więc instrukcje są wykonywane w takiej kolejności, w jakiej zostały zapisane.
Wielkość znaków Kompilator języka C#, podobnie jak C++ czy Javy, rozróżnia wielkość znaków. Przykładowo polecenie Foo nie jest równe poleceniu foo — z punktu widzenia kompilatora to dwa różne polecenia. Jeżeli więc wcześniej programowałeś w Delphi lub Turbo Pascalu (które nie rozróżniały wielkości znaków), musisz przywyknąć do tego, że kompilator C# rozróżnia wielkość znaków, i poświęcać dużo uwagi temu, co piszesz. Dla przykładu doprowadź wygląd naszej prostej aplikacji do takiego kształtu: class Foo { static void main(string[] args) { } }
Po dokładnej analizie możesz zauważyć, że dokonałem jednej małej poprawki. Zamieniłem nazwę funkcji z Main na main. Podczas próby skompilowania takiego programu wyświetlony zostanie błąd: Program 'ConsoleApplication1.exe' does not contain a static 'Main' method suitable for an entry point. Komunikat mówi o tym, że aplikacja nie zawiera metody o nazwie Main, która jest punktem startu programu. Jest to pierwsza reguła charakterystyczna dla programów pisanych w C#. A mianowicie:
Program musi posiadać metodę Main Dla prawidłowej terminologii używam tutaj pojęcia metoda, mimo iż nie wyjaśniłem, czym właściwie jest metoda! Na razie nie przejmuj się tym — zostanie to omówione dalej. Zapamiętaj jedynie, że metoda Main jest obowiązkowym elementem programu. To od niej program rozpoczyna swe działanie i na niej je kończy. Zmodyfikuj nasz program do takiej postaci: class Foo { static void Main(string[] args) { System.Console.WriteLine("Witaj Świecie!"); } }
Po uruchomieniu takiego programu (F5) na ekranie konsoli zostanie wyświetlony tekst: Witaj Świecie!. Jest to potwierdzeniem moich słów o tym, iż to właśnie w metodzie Main rozpoczyna się właściwe działanie programu, czyli wykonanie instrukcji: System.Console.WriteLine("Witaj Świecie!");
Taki kod powoduje wyświetlenie na ekranie konsoli tekstu zapisanego pomiędzy apostrofami. Nie możesz zapominać o obowiązkowych nawiasach, bez których aplikacja nie zostanie skompilowana. Na szybkim komputerze program może zostać skompilowany i uruchomiony z taką prędkością, iż nie dostrzeżemy w ogóle jego działania, gdyż od razu zostanie zamknięty. Abyśmy to my mogli decydować, kiedy aplikacja zostanie zamknięta, należy dodać do programu odpowiednie instrukcje. Jakie? Wskazówki znajdziesz w dalszej części rozdziału.
Średnik kończy instrukcję Bardzo ważna sprawa, często staje się źródłem błędów — chodzi o średnik kończący instrukcję. Kompilator jest tylko programem, który m.in. analizuje nasz kod źródłowy. Pisząc kod, musimy poinstruować kompilator, iż w tym miejscu następuje zakończenie danej instrukcji. Oznaczamy to, stawiając na końcu danego wyrażenia średnik (taka zasada obowiązuje w większości nowoczesnych języków programowania). Zwróć uwagę na powyższą instrukcję wyświetlającą tekst na konsoli. Jest ona zakończona średnikiem. Gdyby go nie było, kompilator w trakcie kompilacji wyświetliłby błąd: ;
expected.
Jest to bardzo ważna zasada i musisz o niej pamiętać. Nie martw się — po jakimś czasie znak średnika na końcu wyrażenia będziesz stawiał automatycznie.
Program musi posiadać klasę Kolejne pojęcie, jakie należy wprowadzić, to klasa. O klasach szczegółowo opowiem w rozdziale 5., jednak aby kontynuować naukę, musisz chociaż wiedzieć, czym one są. Każda klasa musi mieć nazwę poprzedzoną słowem kluczowym class: class Foo
Nazwę klasy od słowa kluczowego musi dzielić co najmniej jedna spacja. Klasy mogą zawierać m.in. metody, takie jak metoda Main. Zawartość klasy musi mieścić się pomiędzy klamrami — { oraz }: class Bar { }
Tak naprawdę każda aplikacja języka C# musi posiadać przynajmniej jedną klasę lub strukturę! O strukturach opowiem w dalszej części książki; teraz jedynie zaznaczam ten fakt, gdyż może on mieć znaczenie dla osób, które programowały wcześniej np. w języku C++.
Wcięcia, odstępy Z punktu widzenia kompilatora nie jest istotne, jak pisany jest kod, czy zawiera odstępy oraz wcięcia. Równie dobrze może być pisany w jednej linii: class Foo { static void Main(string[] args) { System.Console.WriteLine("Witaj Świecie!"); } }
Czytelność takiego kodu pozostawia jednak wiele do życzenia i dlatego taki sposób pisania nie jest zalecany.
Słowa kluczowe
Każdy język programowania posiada specyficzne elementy, tzw. słowa kluczowe. Mogą one oznaczać rozkaz czy instrukcję, które są w dany sposób interpretowane przez kompilator. Standardowo w środowisku Visual C# Express Edition słowa kluczowe wyróżniane są kolorem niebieskim. Do słów kluczowych C# można zaliczyć m.in. class, void, static, string. Przykładowo, słowo class oznacza deklarację klasy o danej nazwie. Deklaracja w kontekście języka programowania może zwyczajnie oznaczać utworzenie danego elementu (np. klasy).
Symbole Symbole to pojedyncze znaki, które wraz ze słowami kluczowymi tworzą składnię. Przykładowo, znak cudzysłowu (") określa początek oraz koniec łańcucha tekstu. Spróbuj usunąć cudzysłowy z instrukcji wypisującej tekst na konsoli: System.Console.WriteLine(Witaj Świecie!);
Podczas próby kompilacji kompilator zasygnalizuje błąd, ponieważ nieznane są dla niego instrukcje Witaj oraz Świecie, które wraz z cudzysłowem tworzą łańcuch, a zwartość łańcucha nie jest przez niego sprawdzana pod względem wystąpienia danych poleceń.
Komentarze Najprostszym elementem każdego języka programowania są komentarze. W kodzie źródłowym możesz dodawać wzmianki — informacje absolutnie niemające wpływu na działanie programu. W trakcie kompilacji są one usuwane przez kompilator i nie znajdują się w pliku wynikowym .exe. Oczywiście kompilator nie usuwa tych komentarzy fizycznie, tzn. ze źródła programu. Komentarze mają ogromny wpływ na proces powstawania aplikacji, szczególnie jeżeli pracujesz w grupie. W kodzie możesz zawrzeć informacje przeznaczone dla innych czytających go programistów, o tym jak on działa i co robi. Podstawowym typem w języku C# są komentarze jednej linii w stylu języka C++. Przykład: class Foo { // to jest komentarz // metoda Main — tutaj zaczynamy działanie! static void Main(string[] args)
{ System.Console.WriteLine("Witaj Świecie!"); } }
Tekst znajdujący się po znakach // nie będzie brany pod uwagę przez kompilator. W nowoczesnych edytorach kodu (takich jak w środowisku Visual C#) komentarze są specjalnie wyróżniane (w moim przypadku — kolorem zielonym). Tego typu komentarze nazywane są komentarzami jednej linii, z tego względu iż obowiązują jedynie w linii, która rozpoczyna się od znaków //: // tu jest komentarz ale tutaj już nie obowiązuje
W C# dostępne są także komentarze w stylu języka C, dzięki którym możesz „skomentować” wiele linii tekstu: /* Tutaj jest komentarz Tutaj również... */
Rozpoczęcie oraz zakończenie bloku komentarza określają znaki /* oraz */. Komentarze mogą być w sobie zagnieżdżane, przykładowo: /* komentarz w stylu C // komentarz jednej linii */ Istnieje jeszcze jeden typ komentarza związanego z dokumentacją języka XML, lecz omówię to w 13. rozdziale książki.
Podzespoły, metody, klasy Powiedzieliśmy sobie już o symbolach oraz słowach kluczowych, którymi można się posłużyć w trakcie pisania programów. Nim przejdziemy dalej, muszę Ci wyjaśnić kilka dodatkowych pojęć. W poprzednim rozdziale wspominałem o środowisku .NET Framework oraz bibliotece klas jako o głównym narzędziu projektowania aplikacji. Chciałbym w tym momencie wyjaśnić pewną kwestię. Otóż środowisko .NET Framework udostępnia programistom szereg klas i bibliotek, które ułatwiają proces programowania. Jest to coś na wzór WinAPI, o którym wspominałem w poprzednim
rozdziale. Główną biblioteką w .NET Framework jest mscorlib.dll. Zawiera ona przestrzeń nazw System, która z kolei zawiera klasę Console! Klasy z kolei zawierają metody, m.in. WriteLine, która służy do wypisywania tekstu na konsoli. Jak widzisz, system zależności jest dość skomplikowany, a wielość pojęć, jakie trzeba opanować, może przyprawić o ból głowy! Dlatego na razie nie będę Cię zadręczał skomplikowanymi definicjami oraz opisami tych pojęć — będziesz je poznawał stopniowo w trakcie czytania tej książki. Na tym etapie ważne jest abyś wiedział, że istnieją metody, które — na wzór funkcji — realizują gotowe zadania takie jak np. wypisywanie tekstu na konsoli. Można powiedzieć, że są to polecenia, które można wykorzystać w aplikacji, chociaż taka terminologia nie jest raczej dopuszczalna. Nie zagłębiając się w szczegóły, postaram się opisać kilka podstawowych elementów języka programowania w C#, które dość często będą wykorzystywane w dalszej części książki.
Funkcje Funkcje jako takie nie istnieją w C#! Zamiast tego mówimy o wspomnianych już w tej książce metodach. Idea jest w zasadzie identyczna, ale aby uniknąć nieporozumień, będę się starał nie używać słowa funkcja. Z tym słowem spotkasz się zapewne nie raz w swojej karierze, gdyż mechanizm funkcji jest obecny w wielu językach programowania. Funkcje to wydzielony blok kodu realizujący jakieś zadanie. Chciałbym w paru słowach przybliżyć ideę tzw. programowania proceduralnego. Idea programowania proceduralnego zaczęła się pojawiać wraz z bardziej zaawansowanymi aplikacjami. Tradycyjny moduł projektowania nie sprawdzał się dobrze, gdy programy zaczęły być bardziej skomplikowane — wówczas ich konserwacja i naprawianie błędów były niezwykle trudne. Ktoś mądry wymyślił wtedy, że można by było dzielić program na mniejsze części — tzw. procedury. Przykładowo, jeżeli napisano kod, który wyświetla pewien komunikat i kończy działanie programu, a ów fragment jest używany wiele razy w tej aplikacji, to należałoby go dublować wiele razy. Powoduje to nie tylko zwiększenie objętości kodu, ale również potęguje podatność na błędy. Bo co się stanie, jeżeli właśnie w tym małym, wielokrotnie powtórzonym w aplikacji fragmencie, wystąpi błąd? Należałoby wówczas przeszukać cały kod i w każdym miejscu poprawiać usterkę. Teraz, w nowoczesnych językach programowania można umieścić pewien fragment kodu w procedurze i wywołać ją za każdym razem, kiedy zajdzie potrzeba jego wykonania! Generalnie w językach takich jak C++, PHP, Java nie istnieją procedury, lecz funkcje. Sama idea jest identyczna, ale z uwagi na to, iż w C# procedury nie istnieją, nie będę o nich więcej wspominał.
Metody Mam nadzieję, że masz już w głowie pewien zarys tego, czym jest metoda. Już wkrótce nauczysz się pisać własne metody oraz klasy. Aktualnie jedyne, co musisz wiedzieć, to to, że metody mogą posiadać tzw. parametry. Spójrz na poprzedni przykład użycia metody WriteLine(). Parametrem tej metody był tekst Hello World!. Innymi słowy — przekazujemy metodzie tekst do wyświetlenia na konsoli. Możesz myśleć o parametrach jak o danych wejściowych — przekazujesz metodzie dane, na których ona operuje. Obowiązkowym elementem każdej metody są nawiasy, w których podaje się parametry. Jednak nie wszystkie metody w środowisku .NET Framework mają parametry — w takim wypadku pozostawiamy pusty nawias, np.: System.Console.Read();
Klasy O klasach można powiedzieć, iż jest to zestaw metod. Przykładowo, klasa Console zawiera zestaw metod służących do operowania na konsoli. Myśl o klasach jak o przyborniku, paczuszce zawierającej przydatne narzędzia. Środowisko .NET Framework udostępnia szereg klas gotowych do użycia. Przykładowo, chcemy napisać program, który obliczy logarytm z danej liczby. Zamiast samemu męczyć się z pisaniem odpowiedniego kodu, możemy wykorzystać klasę Math, która jest częścią środowiska .NET Framework, i udostępniane przez nią mechanizmy.
Przestrzenie nazw Środowisko .NET Framework jest na pierwszy rzut oka dość specyficzne dla osób, które programowały wcześniej na platformie Win32 lub dopiero się uczą. Bo cóż oznacza zapis: System.Console.WriteLine("Witaj Świecie!");
Przede wszystkim jest strasznie długi! Mamy tutaj kilka „instrukcji” oddzielonych znakiem kropki. Czy nie łatwiej i krócej byłoby pisać po prostu samą nazwę metody?
WriteLine("Witaj Świecie!");
Środowisko .NET Framework jest o wiele bardziej rozbudowane od swojego poprzednika — systemu WinAPI. Zawiera tysiące klas i innych typów, każdy posiada inną nazwę. W środowisku Win32 nie mogło się zdarzyć, że istniały dwie funkcje o takiej samej nazwie 1 , co jest możliwe w .NET Framework. Przykładowo, metoda WriteLine() wypisuje tekst, ale istnieje również metoda WriteLine(), która zapisuje go do pliku (metoda klasy TextWriter). Ich jednoczesna obecność jest niewykluczona, ponieważ należą do innych klas. Podobnie jest w przypadku przestrzeni nazw (ang. namespace). W obrębie kilku przestrzeni nazw mogą istnieć klasy o tej samej nazwie. Np.: namespace Bar { class Hello { } } namespace Foo { class Hello { } }
Operator kropki Pojęcie operator zostanie wprowadzone w dalszej części rozdziału. Znak kropki (.) stanowi separator pomiędzy nazwą przestrzeni nazw, klasą a nazwą metody. Przy pomocy tego operatora otrzymujemy dostęp do elementów danej klasy czy przestrzeni nazw. Używając środowiska Visual Studio.NET (jak również Visual C# Express Edition), możesz korzystać z narzędzi wspomagających tworzenie aplikacji. Wykonaj małe doświadczenie. W edytorze napisz słowo System., kończąc je kropką. Po chwili powinna pojawić się rozwijana lista zawierająca listę klas oraz innych zagnieżdżonych przestrzeni nazw, które możemy wykorzystać (rysunek 3.2).
Rysunek 3.2. Lista klas oraz przestrzeni nazw w obrębie przestrzeni System
Słowo kluczowe using Pisanie za każdym razem nazwy przestrzeni nazw, a następnie klasy i metody może być nieco męczące. Dlatego też można wykorzystać słowo kluczowe using, które informuje kompilator, że w programie będziemy korzystali z klas znajdujących się w danej przestrzeni nazw (np. System): using System; class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } }
Zwróć uwagę, że słowo kluczowe using znajduje się jeszcze przed klasą Program.
Zmienne W każdym programie, który ma więcej niż kilkanaście linijek kodu, zachodzi konieczność przechowywania tymczasowych danych w pamięci komputera. Takie dane przechowywane są jedynie w trakcie działania programu. Zmienne definiujemy jako obszar w pamięci komputera, który służy do przechowywania danych tymczasowych (obecnych w pamięci do czasu wyłączenia programu), mających postać liczb, tekstu itp. Zapisywanie danych w pamięci komputera w obecnych językach programowania jest bardzo proste. Należy określić unikalną nazwę, pod którą będziemy uzyskiwać dostęp do danych.
Deklarowanie zmiennych Operacja utworzenia zmiennej nazywana jest deklaracją zmiennej. Musimy określić unikalną nazwę zmiennej, która nie może się powtarzać w obrębie danej klasy czy metody. Musimy również określić typ danej zmiennej, czyli zidentyfikować dane, jakie będziemy przechowywać w pamięci (tekst, liczby itp.). Przykładowe zadeklarowanie zmiennej przedstawiłem poniżej: class Foo { static void Main(string[] args) { string Bar; } }
W powyższym programie „utworzyłem” zmienną o nazwie Bar oraz typie string. Jak widzisz, sposób deklaracji jest bardzo prosty, schemat można przedstawić w ten sposób:
Nazwę od typu musi dzielić co najmniej jedna spacja.
Deklaracja kilku zmiennych Najczęściej bywa tak, iż w aplikacji potrzebujemy wielu zmiennych. Przykładowo, jedna zmienna przechowuje pobrany login użytkownika, druga — imię użytkownika, a trzecia — jego nazwisko. Potrzebujemy wobec tego trzech zmiennych. Jeżeli wszystkie zmienne są tego samego typu (string), możemy zadeklarować je w ten sposób: string Login, FName, LName;
Nazwy zmiennych musimy oddzielić znakiem przecinka. Z punktu widzenia kompilatora nie ma znaczenia to, w jaki sposób deklarujesz zmienne, więc równie dobrze możesz je zadeklarować w ten sposób: string Login; string FName; string LName;
Zwróć uwagę, że instrukcja deklarowania zmiennej zakończona jest znakiem średnika!
Przydział danych Zadeklarowaliśmy już zmienną, lecz nie przypisaliśmy do niej żadnych danych, więc jej zawartość jest pusta. Przypisanie danych do zmiennej jest równie proste jak deklaracja. W tym celu używamy operatora przypisania (=): class Foo { static void Main(string[] args) { string Bar = "Hello World"; } }
W tym momencie zawartość zmiennej Bar to tekst Hello World.
Każdy tekst zadeklarowany w ramach zmiennej musi być ujęty w cudzysłowie. Zmodyfikujemy nieco nasz ostatni program, tak aby tekst wyświetlany w oknie konsoli był odczytywany ze zmiennej. Spójrz na poniższy kod: using System; class Foo { static void Main(string[] args) { string Bar = "Hello World"; Console.WriteLine(Bar); Console.Read(); // czekaj na reakcję użytkownika! } }
W metodzie WriteLine zamiast tekstu umieściliśmy nazwę zmiennej. W trakcie działania programu odczytywana jest zawartość zmiennej Bar (czyli Hello World) i przekazywana jest metodzie WriteLine, co oczywiście skutkuje wypisaniem tekstu na konsoli. Zwróć uwagę, że w programie wykorzystałem metodę Read, która w tym wypadku czeka na reakcję użytkownika. Program zostanie zamknięty wówczas, gdy użytkownik naciśnie klawisz Enter. Jak sama nazwa wskazuje, zmienne (lub raczej ich zawartość) mogą być modyfikowane w trakcie działania programu. To, że przypisaliśmy zawartość zmiennej już w momencie jej deklaracji, nie oznacza, że nie można tego zmienić: string Bar; Bar = "Hello World"; Bar = "Hello my darling!"; Console.WriteLine(Bar);
Jak widzisz, tutaj napisałem zawartość zmiennej; na skutek tego na konsoli wyświetlony zostanie tekst Hello my darling!.
Typy danych Do tej pory deklarowaliśmy zmienne typu string, co oznaczało, iż służą one do
przechowywania tekstu. Język C# oferuje kilka innych typów danych, którymi możemy posługiwać się we własnych aplikacjach. Przykładowo, typ int służy do przechowywania liczb całkowitych: int X = 10;
Jest to bardzo popularny typ danych. Typy danych języka C# różnią się od siebie tzw. zakresem. Np. maksymalna wartość, jaka może zostać przypisana do zmiennej typu int, to 2,147,483,647, natomiast maksymalna możliwa wartość typu byte to 255. Typy danych różnią się od siebie również ilością pamięci, jaką „pochłaniają”. Np. typ int zajmuje w pamięci 4 bajty, a typ byte jedynie 1. W tabeli 3.1 zaprezentowane zostały wbudowane typy danych języka C# wraz z maksymalnym zakresem. Tabela 3.1. Wbudowane typy danych języka C# Typ danych Zakres byte od 0 do 255 sbyte od –128 do 127 short od –32,768 do 32,767 int od –2,147,483,648 do 2,147,483,647 uint od 0 do 4,294,967,295 long od –9,223,372,036,854,775,808 do 9,223,372,036,854,775,807 ulong od 0 do 18,446,744,073,709,551,615 float od –3.402823e38 do 3.402823e38 double od –1.79769313486232e308 do 1.79769313486232e308 decimal od –79228162514264337593543950335 do 79228162514264337593543950335 char Pojedynczy znak string Łańcuch znaków typu char bool true lub false
Jak widzisz, mamy do dyspozycji całą gamę typów, zarówno stało-, jak i zmiennoprzecinkowych. Specyficznym typem danych jest bool, który może przybierać jedynie dwie wartości — true lub false. Jednakże jest on dość często wykorzystywany przez programistów, o czym przekonasz się w trakcie czytania tej książki. Przykład użycia typu bool: bool MyBool; MyBool = true;
Restrykcje w nazewnictwie
Nie jest do końca prawdą, że nazwa zmiennej może być zupełnie dowolna. Niestety, istnieją pewne restrykcje, o których trzeba wiedzieć. Na przykład pierwszym znakiem nazwy zmiennej nie może być cyfra — nazwa ta musi rozpoczynać się od litery. Nazwa zmiennej może jednak zawierać na początku _, ale już inne znaki, takie jak ( ) * & ^ % # @ ! / = + - [ } ] } ' " ; , . czy ?, nie są dozwolone. Platforma .NET oraz sam język C# obsługują standard kodowania znaków Unicode, dlatego w nazwach zmiennych można używać polskich znaków lub jakichkolwiek innych wchodzących w skład Unicode: byte _gżegżółka = 234;
Stałe Stałe od zmiennych odróżnia to, że zawartość przydzielana jest jeszcze w trakcie pisania kodu i nie ulega późniejszym zmianom. Zawartości stałych nie można zmieniać w trakcie działania aplikacji. Raz przypisana wartość pozostaje w pamięci komputera aż do zakończenia działania aplikacji: const double Version = 1.0; Version = 2.0; // w tej linii kompilator wskaże błąd
Stałe deklarowane są prawie identycznie jak zmienne. Jedyna różnica to konieczność poprzedzenia deklaracji słowem kluczowym const. Do czego mogą przydać się stałe? Przykładowo, w stałej Version możesz przechowywać numer wersji swojej aplikacji. Taki numer wersji możesz wyświetlać w oknie „O programie” oraz w innych miejscach kodu źródłowego. Jeżeli uznasz, że należy zmienić numer wersji aplikacji, po prostu zmodyfikujesz zawartość stałej. Nie musisz każdorazowo zmieniać fragmentów kodu, w których wyświetlasz wersję programu.
Operacje na konsoli Wiesz już, czym są aplikacje konsolowe. Nie posiadają one żadnych okien, kontrolek itp., interakcja z użytkownikiem jest więc słaba. Program może jedynie wypisywać tekst na konsoli (WriteLine()) lub odczytać tekst wpisany przez użytkownika. Mogłeś zauważyć, że program, który napisaliśmy wcześniej, zamyka się zaraz po uruchomieniu i wyświetleniu tekstu. To dlatego, że nie nakazaliśmy mu czekać na „pozwolenie” użytkownika.
Metoda ReadLine() umożliwia odczytanie tekstu wpisanego w oknie konsoli. Program zostaje wówczas wstrzymany do czasu naciśnięcia klawisza Enter. Napiszmy prostą aplikację, która pobierze od użytkownika jego imię, a następnie zapisze je w zmiennej. Kod źródłowy takiego programu prezentuje listing 3.1. Listing 3.1. Program pobierający imię użytkownika using System; class Program { static void Main(string[] args) { Console.WriteLine("Cześć, jak masz na imię?"); string name; // deklaracja zmiennej name = Console.ReadLine(); // pobranie tekstu wpisanego przez użytkownika Console.WriteLine("Miło mi " + name + ". Jak się masz?"); Console.ReadLine(); } }
Możesz skompilować, a następnie uruchomić taką aplikację. Umożliwia ona wpisanie imienia, które następnie wyświetli. Przeanalizujmy ten program. Większość zawartych instrukcji powinna być już dla Ciebie znana, objaśnienia wymaga linia pobierająca imię użytkownika: name = Console.ReadLine();
name to nazwa uprzednio zadeklarowanej zmiennej typu string. Taka konstrukcja nakazuje przypisanie do zmiennej wartości tekstu wpisanego i pobranego w oknie konsoli. Innymi słowy, zmienna name zawiera imię pobrane przez użytkownika. Kolejna linia kodu, którą trzeba objaśnić, to: Console.WriteLine("Miło mi " + name + ". Jak się masz?");
Jak widzisz, łańcuch tekstu został tutaj rozdzielony — za pomocą znaków + połączono go w jedną całość. W trakcie wykonywania instrukcji w miejsce name zostanie podstawione imię pobrane od użytkownika.
Metody klasy Console Do tej pory poznałeś dwie metody klasy Console — WriteLine oraz ReadLine. W rzeczywistości klasa udostępnia więcej metod, z których najważniejsze zaprezentowałem w tabeli 3.2. Tabela 3.2. Wybrane metody klasy Console Metoda Opis Beep Umożliwia odegranie dźwięku z głośnika systemowego. Clear Czyści ekran konsoli. ResetColor Ustawia domyślny kolor tła oraz tekstu. SetCursorPosition Ustawia pozycję kursora w oknie konsoli. SetWindowPosition Umożliwia ustawienie położenia okna konsoli. SetWindowSize Umożliwia określenie rozmiaru okna konsoli.
Oto prosta aplikacja demonstrująca wykorzystanie tych metod: using System; class Program { static void Main(string[] args) { // ustaw rozmiar okna Console.SetWindowSize(60, 30); // ustaw położenie tekstu Console.SetCursorPosition(10, 10); Console.WriteLine("Hello World!"); Console.ReadLine(); Console.Clear(); Console.Beep(); } }
Należy zaznaczyć, że parametry tych metod nie oznaczają rozmiaru przedstawionego w pikselach! Przykładowo, pierwszym argumentem metody SetWindowSize jest szerokość okna konsoli wyrażona w ilości kolumn tekstu. Natomiast drugi parametr określa wysokość okna wyrażoną w ilości wierszy (jedna linia tekstu równa się jednemu wierszowi).
Właściwości klasy Console
Poznałeś już pojęcie metoda klasy, potrafisz korzystać z metod, jak również przypisywać oraz odczytywać dane ze zmiennych. Właściwości klasy umożliwiają określenie pewnych zachowań danej klasy. Przykładowo, klasa Console posiada właściwość BackgroundColor, która umożliwia nadanie koloru tła w oknie konsoli. Ta sama klasa posiada właściwość Title, do której możemy przypisać tekst, jaki będzie umieszczony na belce tytułowej okna konsoli. Myśl o właściwościach jak o zwykłych zmiennych, do których można przypisywać dane, a także je odczytywać. Oto przykładowe użycie dwóch wspomnianych właściwości: using System; class Program { static void Main(string[] args) { // nadanie wartości dla właściwości Console.Title = "Hello World"; // określenie koloru tła (ciemny żółty) Console.BackgroundColor = ConsoleColor.DarkYellow; // odczyt wartości właściwości Console.WriteLine("Tytuł tego okna to: " + Console.Title); Console.ReadLine(); } }
Po uruchomieniu takiego programu w oknie konsoli wypisany zostanie tekst: Tytuł tego okna to Hello World. Na samym początku przypisałem wartość do właściwości Title (wartość Hello World), a następnie określiłem tło tekstu. Niezrozumiała dla Ciebie może być operacja określenia tła tekstu: Console.BackgroundColor = ConsoleColor.DarkYellow;
Dlaczego robimy to w ten, a nie inny sposób? Otóż właściwość BackgroundColor wymaga, aby przypisywane do niej dane były typu ConsoleColor. Tak jak w przypadku zmiennych: zmiennej typu string nie możemy przypisać liczby i odwrotnie. ConsoleColor nie jest klasą, a tzw. wyliczeniem. To pojęcie wyjaśnię w dalszej części książki.
Operatory W każdym języku programowania wysokiego poziomu istnieją symbole służące do sterowania programem. Takie znaki nazywane są przez programistów operatorami. W trakcie czytania tej książki miałeś okazję zastosować operator przypisania (=). W rzeczywistości istnieje jednak o wiele więcej operatorów, które należy omówić. Symbole operatorów w języku C# są praktycznie identyczne z tymi z języka C++ oraz Java. Jeżeli więc programowałeś wcześniej w jednym z tych języków, nie będziesz miał problemu z zapamiętaniem poszczególnych symboli.
Operatory porównania Czynność porównywania stosujemy w naszym codziennym życiu. Jesteśmy w stanie na przykład określić, która z dwóch osób jest wyższa. Na podstawie liczby koni mechanicznych silników jesteśmy w stanie ocenić, który z nich ma większą moc. W matematyce również obowiązują takie znaki porównania jak > (znak większości) i < (znak mniejszości). Identyczne symbole są wykorzystywane w językach programowania (patrz tabela 3.3). Tabela 3.3. Operatory porównania Operator Język C# Nierówności != Równości == Większości > Mniejszości < Większe lub równe >= Mniejsze lub równe
Operatory przypisania Sprawa jest dosyć prosta. Przy pomocy tego operatora do zmiennej lub właściwości przypisujemy określone dane: foo = 4; bar = foo;
Po lewej stronie od znaku = musi znajdować się nazwa zmiennej, a po prawej — przypisywana wartość, która również może znajdować się w zmiennej. Oprócz tego prostego operatora język C# udostępnia wiele innych, które umożliwiają przypisanie jakiejś wartości wraz z wykonaniem na niej danej czynności. Przykładowo:
x += 2;
Oznacza to: do wartości zmiennej x dodaj cyfrę 2. Równie dobrze można to wykonać w następujący sposób: x = x + 2;
Jednak poprzednie rozwiązanie jest częściej używane i przejrzystsze. Pozostałe operatory przypisania to: -=, *=, /=, %=, &=, |=, ^=, =.
Inne operatory Naturalnie pokrótce omówione przeze mnie grupy operatorów to nie wszystko, co oferuje język C#. Na tym etapie nauki nie ma jednak sensu prezentowanie bardziej zaawansowanych operatorów, gdyż związane są one z pojęciami, których jeszcze nie omówiłem. Kolejne operatory będę omawiał w dalszej części książki, wraz z prezentowaniem nowych porcji zagadnień.
Instrukcje warunkowe Przed chwilą była mowa o operatorach języka C#, jednak bez sensu byłoby opisywanie ich bez wzmianki o instrukcjach warunkowych. Są to konstrukcje, które służą do sprawdzania, czy dany warunek został spełniony. Jest to praktycznie podstawowy element języka programowania — dzięki instrukcjom warunkowym możemy odpowiednio zareagować na istniejące sytuacje i sterować pracą programu. Przykładowo: użytkownik musi wpisać swoje imię na samym początku działania programu. Może się jednak zdarzyć, że użytkownik specjalnie lub omyłkowo wpisze liczbę. Jeżeli programista nie uwzględni tej możliwości i nie wprowadzi odpowiednich zabezpieczeń, może się to skończyć źle dla programu lub (w przypadku większych aplikacji) spowodować błędy związane z bezpieczeństwem systemu! Ostatnimi czasy w przeglądarce Internet Explorer wykryto poważne błędy związane z bezpieczeństwem, ich powodem był… pasek adresów. Zagrożenie powstawało w chwili wpisania w pasku adresów odpowiedniego ciągu znaków. Oczywiste jest, jak ważne jest sprawdzanie danych dostarczanych przez użytkownika. Podstawowa zasada brzmi: nie wolno ufać danym podawanym aplikacji przez użytkownika — zawsze należy je sprawdzać przed dalszym działaniem programu.
Instrukcja if Podstawową instrukcją warunkową w języku C# jest if. Słowo if oznacza w języku angielskim jeżeli. Ta instrukcja warunkowa umożliwia przetestowanie jakiegoś warunku (np. porównanie, czy wartość zmiennej X jest większa niż zmiennej Y) i zaprogramowanie odpowiedniej reakcji, jeżeli zostanie on spełniony. Programując, używamy instrukcji warunkowych cały czas. Wyobraź sobie, że chcesz napisać program, który pyta użytkownika o imię. Jeżeli imię kończy się literą „a”, możemy z dużym prawdopodobieństwem stwierdzić, że jest ono kobiece. W takim wypadku program wyświetla na ekranie konsoli tekst: Witaj koleżanko!. W przeciwnym wypadku wyświetlamy tekst: Witaj kolego!. Do tego właśnie służą instrukcje warunkowe. Oto przykładowy program wykorzystujący instrukcję warunkową if: using System; class Program { static void Main(string[] args) { int x = 5; if (x == 5) { Console.WriteLine("Zmienna x ma wartość 5!"); } Console.ReadLine(); } }
W tym wypadku instrukcja warunkowa sprawdza, czy wartość zmiennej x równa się 5. Przypomnę, iż operator == służy do sprawdzania, czy obie wartości są sobie równe. Ogólna budowa instrukcji if wygląda następująco: if (warunek do spełnienia) { operacje do wykonania, jeżeli warunek jest prawdziwy; } Obowiązkowym elementem każdej instrukcji if są nawiasy okrągłe, w których musi znaleźć się warunek do sprawdzenia. Warunek może zostać spełniony (czyli wynik pozytywny) albo nie. Od tego zależy, czy wykonany zostanie kod znajdujący się pomiędzy klamrami. Klamry w języku C# są odpowiednikiem bloku begin oraz end z języka Pascal.
Klamry nie są obowiązkowym elementem instrukcji if, aczkolwiek zalecanym ze względu na przejrzystość kodu. Jeżeli pomiędzy klamrami znajduje się tylko jedna instrukcja do wykonania, spokojnie można je usunąć: if (x == 5) Console.WriteLine("Zmienna x ma wartość 5!");
Generalnie jednak zalecam stosowanie klamer niezależnie od tego, ile instrukcji mamy do wykonania. Klamry w języku C# pełnią rolę swego rodzaju pojemnika; określają np. początek oraz zakończenie metody czy klasy. Tak samo oznaczają rozpoczęcie oraz zakończenie bloku instrukcji warunkowej.
Instrukcje zagnieżdżone Instrukcje if można dowoli zagnieżdżać: if (x == 5) { Console.WriteLine("Zmienna x ma wartość 5!"); if (y >= 10) { Console.WriteLine("Drugi warunek również spełniony"); } }
Jeżeli pierwszy warunek zostanie spełniony, wykonany zostanie kod wyświetlający wiadomość na ekranie konsoli. Następnie sprawdzony zostanie kolejny warunek i — po raz kolejny — jeżeli zostanie on spełniony, wykonany zostanie odpowiedni kod. Uwaga! Każda klamra, która zostanie otwarta ({), musi być zakończona w dalszej części programu (}).
Kilka warunków do spełnienia Zaprogramujmy teraz coś trudniejszego. Przypomnij sobie temat operatorów, a konkretnie operatorów logicznych. Umożliwiają one sprawdzanie dwóch warunków w jednej instrukcji warunkowej. Przykładowo, chcemy wyświetlić na konsoli tekst Dobry wieczór koleżanko,
ale tylko wtedy, gdy użytkownik podał kobiece imię. Dodatkowo musimy sprawdzić aktualną godzinę, powiedzmy, witamy się zwrotem dobry wieczór, jeżeli jest po godzinie 18:00. Mamy więc dwa problemy. Po pierwsze musimy sprawdzić, jaka jest ostatnia litera imienia podanego przez użytkownika. Po drugie musimy pobrać aktualną godzinę. Być może wybiegam nieco w przyszłość i nie powinienem na tym etapie prezentować tak trudnych przykładów, ale pragnę urozmaicić Ci naukę. Pierwszy problem można rozwiązać następująco. Otóż przy pomocy symbolu [ oraz ] możemy odwołać się do dowolnego miejsca w łańcuchu tekstowym (np. zmiennej typu string). Przykładowo: string Foo = "Adam"; Console.WriteLine(Foo[1]);
Znaki w łańcuchu tekstowym numerowane są od zera. Tak więc imię Adam ma długość 4 znaków, z tym że pierwszy z nich posiada indeks 0: ----------------Indeks | Numer A | 0 d | 1 a | 2 m | 3
Powyższy kod wyświetli więc na konsoli literę d. W naszym programie musimy więc sprawdzić, jaka jest ostatnia litera łańcucha (naszego imienia). No dobrze, ale skąd program ma wiedzieć, jak długie imię podał użytkownik? Nie wie! Dlatego musimy użyć właściwości Length, która pobiera i zwraca długość danego łańcucha (długość liczona w ilości znaków w tekście). Oto rozwiązanie problemu: string Foo; int FooLength; Foo = "Adam"; FooLength = Foo.Length; // mamy długość łańcucha Console.WriteLine(Foo[FooLength -1]);
W takim programie zmienna FooLength będzie zawierać długość łańcucha Foo. Ponieważ chcemy pobrać ostatni znak w tekście, od tej długości musimy odjąć cyfrę 1. Zajmijmy się drugim problemem. Jak pobrać aktualną godzinę? W tym celu musimy skorzystać z dobrodziejstw klas .NET Framework, a konkretnie z klasy DateTime. Posiada ona właściwość Now, która zawiera informacje o aktualnej godzinie, dniu, tygodniu itd. Ta z kolei posiada kolejną właściwość Hour, która zwraca aktualną godzinę w formie liczby
całkowitej typu int. Jak wspomniałem, w instrukcji warunkowej za jednym razem, możemy porównywać kilka warunków. Spójrz na poniższy przykład: string imie = Console.ReadLine(); if (imie[imie.Length - 1] == 'a' && DateTime.Now.Hour >= 18) { Console.WriteLine("Dobry wieczór koleżanko!"); }
Użyłem tutaj operatora && (logiczne i) do porównania dwóch warunków. Innymi słowy, sprawdzamy, czy ostatnią literą zmiennej imie jest a oraz czy jest po godzinie 18:00. Jeżeli te dwa warunki zostaną spełnione, dopiero wówczas będzie mógł zostać wykonany odpowiedni kod instrukcji (w tym wypadku — wyświetlenie komunikatu w oknie konsoli). Zwróć uwagę, że w ostatnim przykładzie zamiast cudzysłowu użyłem apostrofów, aby sprawdzić, czy ostatnim znakiem tekstu jest litera a. Więcej informacji na temat łańcuchów znajdziesz w rozdziale 9. W taki sposób możemy używać dowolnych operatorów logicznych. Przykładowo, możesz zastąpić && operatorem ||: if (imie[imie.Length - 1] == 'a' || DateTime.Now.Hour >= 18) { // dalszy kod }
Teraz do wykonania kodu z ciała instrukcji wystarczy spełnienie jednego z dwóch warunków. Innymi słowy: albo imię będzie się kończyć literą a, albo jest po godzinie 18:00.
Zastosowanie nawiasów Na lekcjach matematyki w szkole podstawowej uczyłeś się zapewne, że nawiasy okrągłe mają najmniejsze znaczenie i ustępują ważnością nawiasom kwadratowym oraz klamrom. Zapomnij o tej regule w trakcie programowania. W języku C# w instrukcjach warunkowych posługujemy się jedynie nawiasami okrągłymi. Nie oznacza to jednak, że nie możemy sterować priorytetem operacji. Spójrz na przykład: int x, y, z; x = 2; y = 4; z = 10;
if ((x == 2 || y == 4) && z > 20) { Console.WriteLine("Yes, yes, yes!"); }
Zastosowałem tutaj podwójny nawias. Nakazałem tym samym, aby najpierw sprawdzony został warunek z wewnętrznego nawiasu. OK, jest on spełniany, ponieważ zmienna x zawiera cyfrę 2 i to wystarczy. Następnie sprawdzany jest drugi warunek, który nie zostaje spełniony, ponieważ zmienna z ma wartość 10, a wymagane jest, aby była to liczba większa od 20. Ponieważ operator && wymaga, aby obydwa warunki zostały spełnione, kod z ciała instrukcji if nie zostanie wykonany. Podobnie jest w przypadku obliczeń matematycznych. Jeżeli chcemy wymusić ważność danego obliczenia, stosujemy nawiasy: x = (2 + 2) * 2; Console.WriteLine(x);
Wskutek takiego działania najpierw zostanie obliczone działanie w nawiasie (czyli dodawanie), a następnie jego wynik (czyli 4) zostanie pomnożony przez 2.
Słowo kluczowe else Powróćmy jeszcze raz do poprzedniego przykładu, w którym prezentowałem sposób na pobranie ostatniej litery łańcucha oraz aktualnej godziny. Jeżeli warunek został spełniony, wyświetlaliśmy tekst Dobry wieczór koleżanko. Co zrobić w przypadku, gdy chcemy wyświetlić tekst Dzień dobry koleżanko, jeżeli jest przed godziną 18:00? Możemy zastosować drugą instrukcję if: // warunek sprawdza, czy jest po godzinie 18:00 if (DateTime.Now.Hour >= 18) { Console.WriteLine("Dobry wieczór koleżanko!"); } // warunek sprawdza, czy jest przed godziną 18:00 if (DateTime.Now.Hour < 18) { Console.WriteLine("Dzień dobry koleżanko!"); }
Łatwiej w takim wypadku korzystać z instrukcji else, która oznacza w przeciwnym
wypadku. Jest ona dość często wykorzystywana przez programistów: // warunek sprawdza, czy jest po godzinie 18:00 if (DateTime.Now.Hour >= 18) { Console.WriteLine("Dobry wieczór koleżanko!"); } // warunek sprawdza, czy jest przed godziną 18:00 else { Console.WriteLine("Dzień dobry koleżanko!"); }
Jeżeli warunek w instrukcji if nie zostanie spełniony, wykonany zostanie kod z ciała instrukcji else. Na listingu 3.2 zaprezentowany został pełny kod programu. Listing 3.2. Kod programu stosującego instrukcje warunkowe using System; class Program { static void Main(string[] args) { string imie; Console.WriteLine("Cześć! Jak masz na imię?"); imie = Console.ReadLine(); // sprawdzamy, czy imię jest kobiece if (imie[imie.Length - 1] == 'a') { // warunek sprawdza, czy jest po godzinie 18:00 if (DateTime.Now.Hour >= 18) { Console.WriteLine("Dobry wieczór koleżanko!"); } // warunek sprawdza, czy jest przed godziną 18:00 else { Console.WriteLine("Dzień dobry koleżanko!"); } } else { Console.WriteLine("Cześć Kolego!"); }
Console.ReadLine(); } }
Jak widzisz, mamy tutaj kilka instrukcji warunkowych if oraz else zagnieżdżonych w sobie, co jest oczywiście dozwolone. Jeżeli użytkownik poda imię męskie, pierwszy warunek nie zostanie spełniony, czyli zostanie wykonany kod z ciała instrukcji else (na konsoli ujrzymy tekst Cześć Kolego!).
Instrukcja else if Istnieje możliwość połączenia instrukcji if i else. Przypuśćmy, że chcemy wyświetlić wiadomość w oknie konsoli w zależności od aktualnej godziny. Spójrz na poniższy fragment kodu: int X = DateTime.Now.Hour; if (X == 12) { Console.WriteLine("Jest południe!"); } else if (X > 12 && X < 21) { Console.WriteLine("No cóż... już po 12:00"); } else if (X >= 21) { Console.WriteLine("Oj... toż to środek nocy"); } else { Console.WriteLine("Dzień dobry"); }
Jeżeli pierwsza instrukcja nie zostanie spełniona, program przejdzie do sprawdzania kolejnej. Jeżeli i ta nie zostanie spełniona, sprawdzi kolejną i jeszcze następną. W ostateczności program wykona kod z ciała instrukcji else.
Instrukcja switch Jest to kolejna instrukcja warunkowa języka C#. Umożliwia sprawdzanie wielu warunków.
Spójrz przez chwilę na poprzedni przykład. Połączona instrukcja if-else if sprawdzała wartość zmiennej X i wykonywała odpowiedni kod w zależności od jej wartości. Użycie instrukcji switch to dobry pomysł na zastąpienie wielu instrukcji if. Składnia takiej instrukcji jest dość specyficzna. Spójrz na poniższy przykład: i nt X = 10; switch (X) { case 1: // kod nr 1 break; case 5: // kod nr 2 break; }
Sprawdzamy tutaj wartość zmiennej X. Jeżeli ma ona wartość 1, wykonujemy odpowiedni kod i kończymy działanie instrukcji warunkowej. Składnia instrukcji switch jest dość specyficzna. Bardzo ważnym elementem jest słowo kluczowe break, które nakazuje „wyskoczenie” z instrukcji warunkowej, jeżeli warunek zostanie spełniony. Przykładowo: int Mandat = 50; switch (Mandat) { case 10: Console.WriteLine("10 zł mogę zapłacić"); break; case 20: Console.WriteLine("Oj, 20 zł to troszkę dużo"); break; }
Jeżeli wartość zmiennej Mandat wynosi 10, zostanie wykonany odpowiedni kod (w tym wypadku wyświetlenie komunikatu), po czym instrukcja switch zostanie zakończona (pozostałe warunki po słowach kluczowych case nie będą sprawdzane). Usunięcie słowa break spowoduje błąd przy kompilacji: Control cannot fall through from one case label ('case 10:') to another.
Wartość domyślna W ostatnim przykładzie możesz zauważyć, iż zmiennej Mandat przypisałem wartość 50. W takim wypadku żaden z warunków nie zostanie spełniony, ponieważ w instrukcji sprawdzamy jedynie, czy wartością jest 10 lub 20. W języku C# możemy użyć słowa kluczowego default, aby odpowiednio zareagować, gdy żaden ze wcześniejszych warunków nie zostanie spełniony: int Mandat = 50; switch (Mandat) { case 10: Console.WriteLine("10 zł mogę zapłacić"); break;
case 20: Console.WriteLine("Oj, 20 zł to troszkę dużo"); break; default: Console.WriteLine("Niezidentyfikowana suma"); break; }
Po uruchomieniu takiego programu na ekranie konsoli wyświetlony zostanie tekst: Niezidentyfikowana suma.
Instrukcja goto Słowo kluczowe break nakazuje zakończenie instrukcji switch. Istnieje możliwość przeskoczenia do innej etykiety case lub default przy pomocy instrukcji goto. Oto przykład: int Mandat = 50; switch (Mandat) { case 10: Console.WriteLine("10 zł mogę zapłacić");
goto case 20;
case 20: Console.WriteLine("Oj, 20 zł to troszkę dużo"); break; default: Console.WriteLine("Niezidentyfikowana suma"); goto case 10; }
Przeanalizujmy taki kod. Ponieważ wartość zmiennej Mandat to 50, wykonany zostanie blok z etykiety default. Zawarty w niej kod nakazuje przeskok do etykiety case 10, a ten z kolei do case 20. Wskutek tak sformułowanego kodu na ekranie konsoli zostanie wyświetlony tekst: Niezidentyfikowana suma 10 zł mogę zapłacić Oj, 20 zł to troszkę za dużo
Uwaga! W ten sposób można „zapętlić” program. Programiści określają tym mianem sytuację, w której w programie nie znajdzie się instrukcja nakazująca zakończenie danego kodu, co skutkuje ciągłym jego wykonywaniem. Oto przykład prezentujący taką sytuację: case 10: Console.WriteLine("10 zł mogę zapłacić"); goto case 20;
case 20: Console.WriteLine("Oj, 20 zł to troszkę dużo"); goto case 10; default: Console.WriteLine("Niezidentyfikowana suma"); goto case 10;
W każdej etykiecie znajduje się instrukcja goto, która nakazuje skok do innej etykiety case.
Pętle W świecie programistów pod słowem pętla kryje się pojęcie oznaczające wielokrotne
wykonywanie tych samych czynności. Jest to bardzo ważny element każdego języka programowania, dlatego konieczne jest zrozumienie istoty jego działania. Wyobraźmy sobie sytuację, w której trzeba kilka razy wykonać tę samą czynność. Może to być na przykład wyświetlenie kilku linii tekstu. Zamiast wielokrotnie używać Console.WriteLine(), można skorzystać z pętli. Za chwilę przekonamy się, że zastosowanie pętli w programie wcale nie jest trudne.
Pętla while Pętla while umożliwia wielokrotne wykonywanie tego samego kodu, dopóki nie zostanie spełniony warunek jej zakończenia. W tym momencie musimy ponownie posłużyć się operatorami logicznymi oraz porównania. Ogólna budowa pętli while wygląda następująco: while (warunek zakończenia) { // kod do wykonania }
Napiszmy jakiś prosty program, który będzie wyświetlał w pętli dowolny tekst, powiedzmy — dziesięciokrotnie. Spójrz na poniższy kod: using System; class Program { static void Main(string[] args) { int X = 1; while (X 3)) { Console.WriteLine("Nieprawidłowe wartości X lub/i Y"); return false; } // ustawienie znaku na danym polu FField[X, Y] = GetActive().Type; // sprawdzenie, czy należy zakończyć grę CheckWinner(); // jeżeli nikt nie wygrał — zamiana graczy if (!Winner) { FActive = (FActive == 0 ? 1 : 0); } return true; } }
Interfejs aplikacji Na samym początku wykorzystamy utworzoną klasę w aplikacji konsolowej. Aby użytkownik mógł zapełnić określone pole, musi podać jego współrzędną X i Y. Następnie aplikacja wyświetla aktualny stan gry (rysunek 7.3).
Rysunek 7.3. Gra kółkoi krzyżyk w trybie konsoli Kod źródłowy aplikacji korzystającej z wcześniej stworzonego modułu jest dość prosty. Podstawą gry jest menu, dzięki któremu użytkownik steruje pracą programu.
Menu do gry Menu jest dość proste. Zawiera kilka opcji, które można wybrać przy pomocy klawiatury. Operacje „rysowania” menu zawarłem w osobnej metodzie — Menu(): static void Menu() { Console.Clear(); Console.WriteLine("------- Kółko i krzyżyk ------"); Console.WriteLine("------------------------------"); Console.WriteLine(" 1 -- Start gry "); Console.WriteLine(" 2 -- Opcje "); Console.WriteLine(" 3 -- O programie "); Console.WriteLine(" Esc -- Zakończenie "); Console.WriteLine("------------------------------"); }
Wyjaśnienia wymaga właściwie tylko pierwsza linia z ciała tej metody: Console.Clear(). Ponieważ ta metoda będzie wywoływana wiele razy w naszym programie, należy wyczyścić zawartość okna konsoli.
Sterowanie menu Użytkownik będzie mógł sterować menu za pomocą klawiszy klawiatury. Trzeba więc pobrać informacje na temat wciśniętego klawisza. Umożliwia to metoda ReadKey() z klasy Console, zwracająca informacje w postaci typu ConsoleKeyInfo: ConsoleKeyInfo Key; Key = Console.ReadKey(true);
W zależności od wciśniętego klawisza wykonujemy odpowiednie metody naszego programu: switch (Key.KeyChar) { case '1': Start(); break; case '2': Option(); break; case '3': About(); break; case '4': break; }
Warunkiem zakończenia programu jest naciśnięcie klawisza Esc, więc wyświetlanie menu należy kontynuować w pętli. Oto cały kod metody Main(): static void Main(string[] args) { // utworzenie nowej instancji klasy GomokuObj = new GomokuEngine(); ConsoleKeyInfo Key; do { Menu(); // pobranie wciśniętego klawisza
Key = Console.ReadKey(true); // w zależności od wciśniętego klawisza, wykonujemy określoną metodę switch (Key.KeyChar) { case '1': Start(); break; case '2': Option(); break; case '3': About(); break; case '4': break; } } while (Key.Key != ConsoleKey.Escape); }
Kod źródłowy modułu głównego Najważniejsza w naszym programie jest metoda Start(). To ona zawiera główny kod aplikacji, który odpowiada za pobieranie współrzędnych i wywoływanie metody Set() z klasy GomokuEngine. Ona odpowiada również za rysowanie planszy do gry. Pełny kod źródłowy aplikacji przedstawiony jest na listingu 7.4. Listing 7.4. Kod aplikacji Gomoku /************************************************************ * Gomoku (Kółko i krzyżyk) * * Copyright (c) Adam Boduch 2006 * * E-mail: [email protected] * * http://4programmers.net * * * * *********************************************************/ using System; class Program
{ // instancja klasy static GomokuEngine GomokuObj; // metoda wyświetla informacje o autorze static void About() { Console.WriteLine( " Kółko i Krzyżyk v. 1.0 \n" + " Copyrigh (c) Adam Boduch 2006 \n" + " E-mail: [email protected] \n"); Console.ReadLine(); } // metoda wyświetla menu gry static void Menu() { Console.Clear(); Console.WriteLine("------- Kółko i krzyżyk ------"); Console.WriteLine("------------------------------"); Console.WriteLine(" 1 -- Start gry "); Console.WriteLine(" 2 -- Opcje "); Console.WriteLine(" 3 -- O programie "); Console.WriteLine(" Esc -- Zakończenie "); Console.WriteLine("------------------------------"); } // główna metoda — rozpoczęcie gry static void Start() { // jeżeli użytkownik nie podał imion, przekierowujemy go do metody Option if (GomokuObj.Player1 == null || GomokuObj.Player2 == null) { Option(); } // inicjalizacja gry GomokuObj.Start(); // licznik tur int Counter = 1; Console.WriteLine(); int X, Y; char C; // gra będzie kontynuowana, dopóki ktoś nie wygra // jednym z warunków zakończenia jest również remis while (GomokuObj.Winner == false)
{ Console.WriteLine("Tura nr. " + Convert.ToString(Counter) + ", Gracz: " + GomokuObj.Active.Name); // pobranie współrzędnych pola Console.Write("Podaj współrzędną Y: "); X = Convert.ToInt32(Console.ReadLine()); Console.Write("Podaj współrzędną X: "); Y = Convert.ToInt32(Console.ReadLine()); // jeżeli nie można umieścić znaku na polu, nie wykonujemy dalszego kodu // przechodzimy do kolejnej iteracji if (!GomokuObj.Set(X, Y)) { continue; } Counter++; // jeżeli jest to 9 ruch, widocznie jest remis — przerywamy pętlę if (Counter == 9) { break; } // poniższe pętle mają za zadanie rysowanie planszy do gry for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { switch (GomokuObj.Field[i, j]) { case FieldType.ftCross: C = 'X'; break; case FieldType.ftCircle: C = 'O'; break; default: C = '_'; break; } Console.Write(" {0} |", C);
} Console.WriteLine(); } } if (GomokuObj.Winner) { Console.WriteLine("Gratulacje, wygrał gracz " + GomokuObj.Active.Name); } else { Console.WriteLine("Remis!"); } Console.WriteLine("Naciśnij Enter, aby powrócić do menu"); Console.ReadLine(); } // wyświetlanie opcji do gry, czyli możliwości wpisania imion static void Option() { Console.Write("Podaj imię pierwszego gracza: "); GomokuObj.Player1 = Console.ReadLine(); Console.Write("Podaj imię drugiego gracza: "); GomokuObj.Player2 = Console.ReadLine(); } static void Main(string[] args) { // utworzenie nowej instancji klasy GomokuObj = new GomokuEngine(); ConsoleKeyInfo Key; do { Menu(); // pobranie wciśniętego klawisza Key = Console.ReadKey(true); // w zależności od wciśniętego klawisza, wykonujemy określoną metodę switch (Key.KeyChar) { case '1': Start();
break; case '2': Option(); break; case '3': About(); break; case '4': break; } } while (Key.Key != ConsoleKey.Escape); } }
Ćwiczenie dodatkowe W klasie GomokuEngine zaimplementowałem metodę NewGame(), lecz nigdzie w programie nie ma możliwości rozpoczęcia gry od nowa, czyli wyzerowania licznika wygranych. Ba, licznik wygranych nie jest nawet nigdzie w programie prezentowany. To zadanie dla Ciebie!
Mechanizm indeksowania Skoro powiedzieliśmy sobie o tablicach, grzechem byłoby nie wspomnieć o tzw. mechanizmie indeksowania, funkcji języka C#, która pozwala odwoływać się do obiektu tak jak do zwykłych tablic (przy pomocy symboli [ ]). Jest to ciekawy element języka C#, nieco bardziej zaawansowany, lecz myślę, że warto o nim wspomnieć. O indeksatorach możesz myśleć jak o tablicach połączonych z możliwością deklarowania właściwości klas. Wyobraź sobie, że przy pomocy symboli [ oraz ] możesz odwoływać się do pewnych elementów klasy: TranslateList MyList = new TranslateList(); MyList[0] = "Foo";
Dodatkowo możesz kontrolować proces pobierania oraz przypisywania takich elementów.
Indeksatory w klasie deklaruje się podobnie jak właściwości, z tą różnicą, że zamiast nazwy właściwości stosujemy słowo kluczowe this: class TranslateList { public string this[string index] { get { } set { } } }
Jak widzisz, składnia jest dość charakterystyczna:
Indeksatory deklarujemy z użyciem słowa kluczowego this. Indeksator musi posiadać typ zwrotny. Indeksator musi posiadać akcesor get i/lub set. W jednej klasie może być wiele indeksatorów, pod warunkiem że będą miały różne parametry.
Zaprezentuję teraz prosty przykład wykorzystania indeksatorów. Spójrz na poniższą klasę: class TranslateList { private string[,] DayList; private int LangId; public TranslateList(string lang) { DayList = new string[,] { {"Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nd"}, {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} }; LangId = lang == "pl" ? 0 : 1; } public string this[int index] { get { return DayList[LangId, index];
} } }
W konstruktorze klasy wypełniamy danymi dwuwymiarową tablicę, w której znajdują się oznaczenia dni tygodnia w języku polskim oraz angielskim. W indeksatorze istnieje możliwość odczytania nazwy danego dnia w zależności od wartości pola LangId. Teraz przy pomocy nawiasów kwadratowych możemy odwołać się do poszczególnych elementów: TranslateList MyList = new TranslateList("en"); for (int i = 0; i < 7; i++) { Console.Write("{0} ", MyList[i]); }
W zaprezentowanym przeze mnie przykładzie nie ma możliwości przypisania danych do indeksatora. Kompilator zaprotestuje, wyświetlając komunikat o błędzie: Property or indexer cannot be assigned to -- it is read only.
Indeksy łańcuchowe Tablice asocjacyjne, znane zapewne wielu programistom PHP, nie są niestety dostępne w języku C#. Zamiast tego możemy jednak skorzystać z indeksatorów, które dopuszczają użycie łańcuchów w nazwach indeksu: Console.WriteLine(MyList["Pn"]); MyList["Pn"] = "Monday"; Console.WriteLine(MyList["Pn"]);
Oczywiście musimy jeszcze odpowiednio oprogramować nasz indeksator: class TranslateList { private string[] ShortDay; private string[] LongDay; public TranslateList() { ShortDay = new string[] {"Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nd"}; LongDay = new string[] {"Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota", "Niedziela"};
} public string this[string index] { get { int IndexOf = System.Array.IndexOf(ShortDay, index); return LongDay[IndexOf]; } set { int IndexOf = System.Array.IndexOf(ShortDay, index); LongDay[IndexOf] = value; } } }
Takie rozwiązanie jest jednak nieco problematyczne. Lepiej w takich przypadkach używać list i słowników, o których będzie mowa w tym rozdziale. Podsumowując: mechanizm indeksowania można traktować jako rozbudowany system tablic, ponieważ dają one większą kontrolę nad przypisywaniem i odczytywaniem danych. Indeksatory nie mogą być opatrzone modyfikatorem dostępu static.
Kolekcje Pisałem już o tablicach, zgłębiłem temat mechanizmu indeksowania, lecz omawiając tablice języka C#, nie sposób nie wspomnieć jeszcze o mechanizmie kolekcji. Tablice jako takie obecne są w większości języków wysokiego poziomu. Niekiedy mechanizm korzystania z tablic jest naprawdę rozbudowany (jak np. w języku PHP), a niekiedy może okazać się niewystarczający (jak w języku C#). Tablice w języku C# mają wiele ograniczeń, które możemy łatwo ominąć, stosując mechanizm kolekcji. Mówiąc o ograniczeniach, mam na myśli brak możliwości nadawania określonych indeksów dla elementów tablicy (jeżeli nie chcemy, aby elementy były indeksowane od 0) czy trudności w usuwaniu lub dodawaniu kolejnych elementów tablicy. Deklarując tablicę w C#, musimy podać z góry jej rozmiar lub nadać jej elementy. Kolekcje stanowią bardziej zaawansowany mechanizm przechowywania zbioru różnych obiektów. Odpowiednie klasy dostarczają bardziej zaawansowane metody służące do operacji na zbiorze elementów kolekcji. W przestrzeni nazw System.Collections znajduje się wiele klas służących do operowania na zbiorach danych. W dalszej części tego rozdziału omówimy kilka z nich.
Interfejsy System.Collections W przestrzeni nazw System.Collections zadeklarowanych jest wiele klas służących do operowania na zbiorach danych, a ich zależność jest dość duża, co może spowodować pewną dezorientację. Powróćmy jeszcze na chwilę do klasy System.Array. Mimo iż należy ona do przestrzeni nazw System, implementuje metody interfejsów ICloneable, IList, ICollection oraz IEnumerable! Możemy więc o niej powiedzieć, iż jest prostą klasą wykorzystującą mechanizm kolekcji! Można wyróżnić 8 interfejsów zdefiniowanych wewnątrz przestrzeni System.Collections: IEnumerable, ICollection, IList, IDictionary, IEnumerator, IComparer, IDictionaryEnumerator, IHashCodeProvider. W tabeli 7.2 znajduje się krótki opis każdej z nich. Tabela 7.2. Opis interfejsów z przestrzeni System.Collections Interfejs Opis Udostępnia interfejs, który umożliwia przeglądanie elementów IEnumerable kolekcji w jednym kierunku. ICollection Dziedziczy metody IEnumerable. Definiuje rozmiar kolekcji. Definiuje kolekcję, której elementy są dostępne za pośrednictwem IList indeksów. Definiuje kolekcję, w której elementy są dostępne za pośrednictwem IDictionary kluczy. Definiuje metody umożliwiające przeglądanie kolejnych elementów IEnumerator kolekcji. Definiuje właściwości umożliwiające pobieranie kluczy i wartości IDictionaryEnumerator słowników (obiektów klas Dictionary). IComparer Definiuje metodę służącą do porównywania dwóch obiektów. IHashCodeProvider Definiuje metodę zwracającą unikalny kod danego obiektu.
Żaden z tych interfejsów nie może oczywiście działać samodzielnie. One jedynie definiują metody, które są następnie implementowane w klasach reprezentujących. Tym zajmiemy się w dalszej części rozdziału. Teraz omówię pokrótce kilka najważniejszych interfejsów.
IEnumerable Bazowy interfejs. Definiuje właściwie tylko jedną metodę — GetEnumerator(). Zwraca ona egzemplarz klasy, który implementuje interfejs IEnumerable. I to właściwie wszystko, co znajduje się w tym interfejsie i wymaga omówienia.
ICollection Interfejs, który dziedziczy po IEnumerable. Definiuje przydatną właściwość, z której będziesz korzystał nie raz. Jest to właściwość Count, która zwraca ilość elementów w kolekcji. Definiuje również metodę CopyTo(), która umożliwia skopiowanie obiektu zawartego w danej kolekcji do tablicy (System.Array). Metoda ta posiada dwa parametry. Pierwszy z nich to nazwa tablicy, do której zostaną skopiowane dane. Drugi to indeks (liczony od zera), od którego rozpocznie się kopiowanie.
IList Interfejs IList przejmuje właściwości oraz metody po interfejsach ICollection oraz IEnumerable. Jest to bazowy interfejs dla tzw. list. Definiuje metody oraz właściwości umożliwiające dodawanie i kasowanie elementów listy. Na razie przyjrzyj się metodom i właściwościom zdefiniowanym w interfejsie IList (tabela 7.3.). Przykłady wykorzystania klas implementujących ten interfejs zaprezentuję w dalszej części książki. Tabela 7.3. Metody oraz właściwości zdefiniowane w interfejsie IList Właściwość/Metoda Opis IsFixedSize Właściwość określa, czy lista ma stały rozmiar. Właściwość określa, czy dana lista zwraca swoje wartości jedynie do IsReadOnly odczytu. Właściwość umożliwia pobranie lub ustawienie elementów pod danym Item indeksem. Add() Metoda umożliwia dodanie elementów do listy. Clear() Usuwa elementy z danej listy. Contains() Sprawdza, czy lista zawiera daną wartość. IndexOf() Zwraca indeks danej wartości. Insert() Umożliwia wstawienie elementu pod wskazaną pozycję. Remove() Usuwa pierwsze wystąpienie danego elementu. RemoveAt() Usuwa element określony danym indeksem.
IDictionary Interfejs definiujący metody oraz właściwości umożliwiające korzystanie z tzw. słowników. Słowniki umożliwiają przechowywanie danych, do których dostęp uzyskuje się, podając
odpowiedni klucz — nie indeks, jak to jest w tablicach oraz listach. Tym zagadnieniem zajmiemy się nieco później. Interfejs, podobnie jak IList, definiuje właściwości IsFixedSize, IsReadOnly i Items oraz metody Add(), Clear(), Contains(), Remove(). Dodatkowo posiada dwie właściwości:
Keys — właściwość zwraca kolekcję składającą się z kluczy danego słownika. Values — właściwość zwraca kolekcję składającą się z wartości danego słownika.
IEnumerator Interfejs IEnumerator definiuje właściwość Current zwracającą bieżący element kolekcji. Dodatkowo definiuje metody MoveNext() oraz Reset(). Pierwsza z nich służy do przechodzenia do kolejnego elementu kolekcji, a druga — do przejścia do pierwszego elementu.
Stosy Po tym przydługim wstępie czas przejść do rzeczy bardziej praktycznych, czyli obsługi kolejek. Na początek chciałbym wspomnieć o klasie Stack (ang. stos), która służy do przechowywania danych zgodnie z zasadą FILO (ang. First-in Last-out), co oznacza: pierwszy wchodzi, ostatni wychodzi. Wyobraź sobie stos talerzy — jeden leży na drugim. Aby wyciągnąć ten na samym dole, musisz najpierw ściągnąć te leżące na nim, prawda? Identycznie działa klasa Stack. Możesz swobodnie dodawać kolejne elementy do kolekcji, lecz nie ma sposobu na usunięcie tego dodanego na samym początku. Spójrz na listing 7.5, który prezentuje przykład użycia klasy Stack oraz wyświetlania zawartości danego stosu. Listing 7.5. Przykład wykorzystania klasy Stack using System; using System.Collections; namespace FooConsole { class Program { static void Main(string[] args)
{ Stack MyStack = new Stack(); for (int i = 0; i < 20; i++) { MyStack.Push("Pozycja nr " + i); } Console.WriteLine("Ostatni element: " + MyStack.Peek()); Console.WriteLine("Usunięty element: " + MyStack.Pop()); Console.WriteLine(); IEnumerator MyEnum = MyStack.GetEnumerator(); while (MyEnum.MoveNext()) { Console.WriteLine(MyEnum.Current.ToString()); } Console.Read(); } } }
Przed użyciem klasy Stack należy wywołać konstruktor klasy. Można w nim określić przewidywalną ilość elementów lub wywołać konstruktor bez parametrów, tak jak ja to zrobiłem. Wówczas zostanie nadana domyślna ilość elementów, czyli 10. W razie potrzeby ta wartość zostanie automatycznie zwiększona, więc nie musisz się bać, że wystąpi błąd (takie ryzyko istniało przy okazji korzystania z tablic). Korzystając z metody Push(), umieszczam na stosie kolejne elementy. Metoda Peek() służy do pobrania ostatniego elementu stosu, natomiast Pop() zwraca ostatni element, po czym go usuwa. Zwróć uwagę na sposób wyświetlania elementów z kolekcji. W tym celu zadeklarowałem zmienną wskazującą na interfejs IEnumerator. Użyłem również metody GetEnumerator(), która zwraca implementowany obiekt. Dzięki temu, posługując się odpowiednimi metodami, jestem w stanie pobrać kolejne elementy ze stosu i je wyświetlić. Klasa Stack implementuje interfejsy ICollection, IEnumerable, ICloneable. Pamiętaj więc o tym, iż zawiera metody oraz właściwości zdefiniowane w tych interfejsach. Ze względu na to, że klasa Stack nie umożliwia usuwania dowolnego elementu stosu, pewnie nie będziesz z niej często korzystał. Właściwie zależy to tylko od Twoich potrzeb. Jeżeli istnieje konieczność umieszczenia elementów w formie stosu, wydaje mi się, że zastosowanie klasy Stack będzie dobrym rozwiązaniem. Jeżeli musisz mieć możliwość usuwania dowolnego elementu, zapewne będziesz zmuszony skorzystać z list.
Kolejki Klasa Queue działa podobnie jak Stack. Różny jest jednak sposób ich działania. Klasa Queue działa zgodnie z zasadą FIFO (ang. First-in First-out), co można przetłumaczyć jako: pierwszy wchodzi, pierwszy wychodzi. Aby lepiej zrozumieć zasadę działania tej klasy, wyobraź sobie zwykłą kolejkę sklepową. Ten, kto stoi w niej ostatni, obsługiwany jest jako ostatni, prawda? Pierwsza osoba w kolejce wychodzi ze sklepu jako pierwsza. Klasa Queue również nie posiada metody umożliwiającej usunięcie dowolnego elementu. Zamiast tego istnieje możliwość umieszczenia danego elementu jako ostatniego w kolejce, co realizuje metoda Enqueue(). Jeżeli mówimy o usuwaniu, to realizuje to metoda Dequeue(), która usuwa pierwszy element w kolejce. Podsumowując: jeżeli chcemy usunąć ostatni element kolejki, najpierw musimy usunąć wszystkie pozostałe utworzone wcześniej. Na listingu 7.6 znajduje się kod źródłowy zmodyfikowanego programu z listingu 7.5. Listing 7.6. Program prezentujący użycie klasy Queue using System; using System.Collections; namespace FooConsole { class Program { static void Main(string[] args) { Queue MyQueue = new Queue(); for (int i = 0; i < 20; i++) { MyQueue.Enqueue("Pozycja nr " + i); } Console.WriteLine("Pierwszy element: " + MyQueue.Peek()); Console.WriteLine("Usunięty element: " + MyQueue.Dequeue()); Console.WriteLine(); IEnumerator MyEnum = MyQueue.GetEnumerator(); while (MyEnum.MoveNext()) { Console.WriteLine(MyEnum.Current.ToString()); } Console.Read(); } } }
Rysunek 7.4 prezentuje program w trakcie działania.
Rysunek 7.4. Program prezentujący działanie klasy Queue Klasa Queue implementuje te same interfejsy, co klasa Stack.
Klasa ArrayList ArrayList stanowi prawdopodobnie najlepszą alternatywę pomiędzy klasą Stack oraz Queue. Posiada ona również metody obecne w klasie Array, więc operowanie nią jest podobne do obsługi zwykłych tablic. Implementuje interfejsy IList, ICollection, IEnumerable, ICloneable, więc miej na uwadze to, iż zawiera właściwości i metody, o
których wspominałem kilka stron wcześniej, przy okazji omawiania tych interfejsów.
Listy Opisałem już, czym charakteryzują się kolejki oraz stosy. Chciałbym teraz pójść nieco dalej i zająć się tematyką list oraz typami generycznymi. Zostawmy na razie możliwości oferowane przez przestrzeń nazw System.Collections i pójdźmy dalej. Zajmijmy się możliwościami oferowanymi przez .NET 2.0, z którego zapewne teraz korzystasz, a konkretnie klasami znajdującymi się w przestrzeni System.Collections.Generic.
Typy generyczne Podczas omawiania klas nie wspomniałem o jednej właściwości klas szeroko wykorzystywanej przy kolekcjach. Jest to nowość w środowisku .NET Framework 2.0 (poprzednia wersja 1.1 nie posiadała możliwości wykorzystania typów generycznych), wzorowana na technologii templates z języka C++. Spójrz na poniższy kod prezentujący, w jaki sposób możemy dodać elementy do kolekcji typu ArrayList i wyświetlić je: ArrayList Foo = new ArrayList(); Foo.Add("Adam"); Foo.Add("Marta"); Foo.Add(100); Foo.Add(2.34); for (int i = 0; i < Foo.Count; i++) { Console.WriteLine("Indeks: {0} wartość: {1}", i, Foo[i]); }
Przy pomocy Add() dodajemy do listy kolejne elementy, raz typu string, później liczbę stałoprzecinkową i wreszcie — liczbę rzeczywistą. Jest to absolutnie dopuszczalne, gdyż parametr metody Add() jest typu object, a jak wiadomo — wszystkie typy .NET Framework dziedziczą po tej klasie. Przy każdym wywołaniu metody Add() program musi wykonać pakowanie (ang. boxing) typów, a przy wyświetlaniu — odpakowywanie. Przy dużej ilości danych trwa to dość długo. O technice pakowania oraz odpakowywania pisałem w rozdziale 5. Dlatego jeżeli zamierzasz umieścić w kolekcji dane tego samego typu, lepiej wykorzystać klasy oferowane przez przestrzeń nazw System.Collection.Generic. Znajdują się tam klasy, odpowiedniki już wspomnianych Stack oraz Queue, które działają szybciej na danych tego samego typu. Nie ma w przestrzeni nazw System.Collection.Generic klasy ArrayList. Zamiast tego możemy jednak wykorzystać klasę List, która jest właściwie generycznym odpowiednikiem ArrayList. Listing 7.7 prezentuje prosty przykład użycia klasy List: Listing 7.7. Przykład użycia klasy List using System; using System.Collections.Generic; namespace FooConsole { class Program
{ static void Main(string[] args) { List Foo = new List(); Foo.Add(10); Foo.Add(100); Foo.Add(10000); for (int i = 0; i < Foo.Count; i++) { Console.WriteLine("Indeks: {0} wartość: {1}", i, Foo[i]); } Console.Read(); } } }
Przy deklarowaniu i utworzeniu obiektu klasy List musiałem podać typ danych (int), na jakim chcemy operować. Oczywiście istnieje możliwość operowania na dowolnym typie danych — wówczas w miejsce T należy podać jego nazwę.
Tworzenie typów generycznych Istnieje możliwość tworzenia własnych klas generycznych. Jedyne, co musimy zrobić, to na końcu nazwy dodać frazę : class Generic { }
Teraz przy tworzeniu egzemplarza klasy kompilator będzie wymagał, aby podać również typ danych, na których ma ona operować — np.: Generic MyGeneric = new Generic();
Przyjęło się, że przy deklarowaniu typów generycznych stosujemy frazę . Kompilator nie wymusza jednak takiego nazewnictwa, więc równie dobrze możemy napisać: class Generic {}.
Po zadeklarowaniu takiej klasy w jej obrębie typ T będzie oznaczał typ danych podany podczas jej tworzenia. Można go dowolnie wykorzystywać. Np.: class Generic { public void Add(T X) { Console.WriteLine("{0}", X); } }
Obsługa takiej klasy wiąże się z odpowiednim utworzeniem obiektu: Generic MyGeneric = new Generic(); MyGeneric.Add("Hello World!");
Metody generyczne Istnieje również możliwość deklarowania metod generycznych. Ich tworzenie wygląda bardzo podobnie: static void Foo(T Bar) { Console.WriteLine("{0}", Bar); }
Wywołując taką metodę, możesz, aczkolwiek nie musisz, podawać typu danych — np.: Foo("Adam"); // dobrze Foo(12); // dobrze
Kompilator domyśli się typu na podstawie przekazanych parametrów.
Korzystanie z list Klasa List posiada spore możliwości — implementując interfejsy IList, ICollection, IEnumerable, IList, ICollection, IEnumerable, umożliwia dodawanie, usuwanie
dowolnych pozycji list. Nie będę prezentował spisu wszystkich metod oraz właściwości, gdyż to zostało właściwie powiedziane już wcześniej, przy okazji omawiania tablic oraz interfejsów. Zaprezentuję za to prostą aplikację przedstawiającą użycie list. Aplikacja będzie dość prosta. Po uruchomieniu użytkownik będzie mógł dodać do listy nazwę województwa wraz z jego stolicą (rysunek 7.5).
Rysunek 7.5. Aplikacja w trakcie działania Użytkownik dodatkowo będzie mógł mieć możliwość usunięcia zaznaczonej pozycji oraz wyczyszczenia całej listy. W liście będą przechowywane dane odnośnie do województw oraz ich stolic. W tym celu zadeklarowałem odpowiednią strukturę: public struct Location { public string Province; public string Capital; }
Listing 7.8 zawiera kod źródłowy głównego formularza programu. Listing 7.8. Kod źródłowy formularza using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace ListApp
{ // struktura przechowywana w liście public struct Location { public string Province; public string Capital; } public partial class MainForm : Form { private List MyList; public MainForm() { InitializeComponent(); } // metoda uaktualniająca stan komponentu ListBox zgodnie ze stanem faktycznym private void UpdateList() { lbLocation.Items.Clear(); for (int i = 0; i < MyList.Count; i++) { lbLocation.Items.Add( String.Format("{0,-10} {1,10}", MyList[i].Province, MyList[i].Capital )); } } private void MainForm_Load(object sender, EventArgs e) { // utworzenie instancji klasy w momencie załadowania formularza MyList = new List(); lbLocation.Items.Clear(); } private void btnClear_Click(object sender, EventArgs e) { MyList.Clear(); UpdateList(); } private void btnDelete_Click(object sender, EventArgs e) { // usunięcie zaznaczonej pozycji
MyList.RemoveAt(lbLocation.SelectedIndex); UpdateList(); } private void btnAdd_Click(object sender, EventArgs e) { Location MyLocation = new Location(); MyLocation.Province = textProvince.Text; MyLocation.Capital = textCapitol.Text; textProvince.Text = textCapitol.Text = ""; MyList.Add(MyLocation); UpdateList(); } } }
Myślę, że aplikacja jest dość prosta. Za wyświetlanie wszystkich pozycji w komponencie typu ListBox odpowiada metoda UpdateList(). W pętli dodaje ona kolejne pozycje do komponentu ListBox po uprzednim jego wyczyszczeniu. Odpowiednie procedury zdarzeniowe komponentów Button służą do dodawania nowej pozycji, usuwania zaznaczonej oraz czyszczenia wszystkich dodanych rekordów. Po naciśnięciu przycisku służącego do dodawania nowych wpisów tworzona jest instancja struktury, do której przypisujemy dane wpisane w kontrolkach typu TextBox. Następnie przy pomocy metody Add() dodajemy nowy wpis do kolejki.
Słowniki Programistom PHP zapewne znany jest mechanizm tablic, w którym zamiast indeksu używany jest tzw. klucz. Wiesz już, że indeks jest unikalną wartością identyfikującą dany element w tablicy/liście. Omawiając mechanizm indeksowania, pokazałem, w jaki sposób zrobić indeks, który mógłby przyjmować wartości łańcuchowe. W przypadku słowników zamiast indeksu jest klucz (wartość typu string), który również musi posiadać unikalną wartość w obrębie całej kolekcji. Właściwie to, czy będzie on typu string, czy jakiegokolwiek innego, zależy już tylko od nas. To samo dotyczy wartości elementów słownika. Najczęściej słowniki służą mimo wszystko do przechowywania wartości tekstowych, gdzie klucz i wartość są typu string. Klasa Dictionary zadeklarowana jest w przestrzeni nazw System.Collections.Generic w sposób następujący:
public class Dictionary : IDictionary, ICollection, IEnumerable, IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback { }
Jak więc widzisz, ilość interfejsów, które są w klasie implementowane, jest imponująca. Deklarując instancję klasy, należy podać zarówno typ klucza, jak i wartości — np.: Dictionary MyDictionary;
Korzystanie ze słowników jest bardzo podobne do używania zwykłych list. Różnica jest taka, iż słowniki dają nam możliwość określenia typu klucza. Tak więc dla powyższej deklaracji, dane do słownika możemy przypisywać w ten sposób: MyDictionary["Klucz"] = "Wartość";
Jeżeli chcemy korzystać ze słowników tak jak z list, należy utworzyć następującą zmienną wskazującą na klasę: Dictionary MyDictionary;
Wówczas kluczem takiego słownika jest liczba stałoprzecinkowa, a wartością — ciąg znaków typu string.
Przykładowy program Aby zaprezentować możliwości działania słowników, napisałem prosty program umożliwiający tłumaczenie tekstu na podstawie słówek wpisanych do słownika. Działanie jest proste: przy pomocy metody Replace() program zamienia określony tekst znajdujący się w komponencie RichTextBox na wyrazy pobierane ze słownika. Program napisałem na podstawie biblioteki wizualnej Windows Forms. Aplikacja została podzielona na dwie zakładki przy użyciu komponentu TabControl (rysunki 7.6 oraz 7.7).
Rysunek 7.6. Zakładka umożliwiająca dodanie danych do słownika
Rysunek 7.7. Zakładka umożliwiająca zamianę tekstu Na pierwszej zakładce, w komponencie ListView wyświetlana jest zawartość słownika. Lewa kolumna zawiera klucze słownika, a prawa — ich wartości. Program umożliwia modyfikowanie, usuwanie oraz czyszczenie słownika.
Na prawej zakładce istnieje możliwość wprowadzenia tekstu. Po naciśnięciu przycisku program odszukuje wyrazy określone w kluczach słownika i zastępuje je wartościami tych kluczy. Jeżeli chodzi o zastępowanie tekstu, to realizuje to metoda Replace() klasy System.String. Omówienie metod służących do operowania na tekście, znajdziesz w rozdziale 9. W pętli musimy pobierać dane ze słownika, i nie tylko wartości elementów, ale również wartości kluczy. Realizuje to poniższy kod: // utworzenie instancji klasy Dictionary.Enumerator Enumerator = MyDictionary.GetEnumerator(); while (Enumerator.MoveNext()) { // pobierz kolejny klucz i wartość KeyValuePair Value = Enumerator.Current; // zastąp tekst richTextBox.Text = richTextBox.Text.Replace(Value.Key, Value.Value); }
Aby zrealizować to zadanie, musimy zadeklarować zmienną wskazującą na strukturę Dictionary.Enumerator, do której przypiszemy wartość zwracaną przez metodę GetEnumerator(). Pobranie kolejnych kluczy i wartości następuje w pętli przy pomocy struktury KeyValuePair. Dodanie kolejnego elementu do słownika realizuje metoda Add(), która posiada dwa parametry — nazwę klucza oraz wartość. Chcąc usunąć dany element, należy skorzystać z metody Remove() poprzez podanie nazwy klucza.
Podsumowanie Być może na tym etapie nauki języka nie dostrzegasz zalet stosowania tablic czy kolekcji. Być może nie widzisz również potrzeby stosowania tego typu rozwiązań we własnych aplikacjach. Możesz mi wierzyć lub nie, ale z czasem, gdy Twoje programy będą coraz bardziej zaawansowane, zaistnieje konieczność wykorzystania tablic. Jako że tablice w C# nie dają takich korzyści jak w innych językach (m.in. z powodu trudności w deklarowaniu rozmiaru w trakcie działania programu), być może będziesz zmuszony do wykorzystania kolekcji. Wówczas możesz sięgnąć po ten rozdział i przeanalizować prezentowane tutaj przykłady
Rozdział 8 Obsługa wyjątków Trzeba sobie uświadomić, że błędy w aplikacjach są — niczym śnieg na biegunie — elementem nieodłącznym. Wielu programistów, często z pośpiechu, lecz także z braku wystarczających umiejętności, marginalizuje problem błędów w aplikacjach. Wiele firm, mimo ogromnego zaangażowania setek programistów oraz dużych nakładów finansowych, wciąż nie jest w stanie pozbyć się wszystkich błędów (np. Microsoft) i bez przerwy publikuje nowe poprawki do swoich produktów. Tylko program zawierający trzy linie kodu źródłowego może być pozbawiony jakichkolwiek błędów, lecz w przypadku skomplikowanych, rozbudowanych aplikacji uniknięcie niedoskonałości jest niemożliwe. Dzieje się tak dlatego, że programista jest tylko człowiekiem i po prostu się myli. Pozornie aplikacja może zachowywać się normalnie, a błąd może tkwić gdzieś indziej. Nie mówię tutaj bowiem o błędach wykrywanych w czasie kompilacji, najłatwiejszych do usunięcia, które sprowadza się najczęściej do drobnych poprawek, często bardzo śmiesznych — na przykład dopisanie średnika na końcu wyrażenia. Najtrudniejsze do wykrycia są błędy zagnieżdżone w kodzie, kiedy program pozornie działa prawidłowo, lecz nie do końca wykonuje operacje, których oczekuje użytkownik. Warto, abyś w tym momencie dobrze zapamiętał stwierdzenie, iż program zawsze działa prawidłowo, a jeśli nie działa zgodnie z naszymi oczekiwaniami — jest to zwyczajnie wina projektanta. Pomijam tutaj błędy kompilatora, bo te również są tworzone przez ludzi i również mogą zawierać błędy. Kompilator jest jednak produktem podwyższonego ryzyka: nie można pozwolić, aby zawierał choćby najdrobniejsze błędy. Błąd w aplikacji często jest określany mianem bug (z ang. robak, pluskwa). Termin ten wziął się z czasów, gdy komputery zajmowały duże pomieszczenia i pobierały tyle energii co małe osiedle, a ich obsługą zajmował się sztab ludzi. Robaki, które zalęgły się gdzieś w zakamarkach ogromnej maszyny, czasami powodowały zwarcie instalacji elektrycznej. Od tamtej pory błędy nazywa się bugami, a proces ich wykrywania — debugowaniem (z ang. debugging). Częstą przyczyną „uwidocznienia” błędu jest czynnik ludzki. Załóżmy na przykład, że piszesz skomplikowaną aplikację biurową, z której będzie korzystało wielu ludzi. Nie każdy z nich jest informatykiem, nie każdy posiada odpowiednią wiedzę, aby wystarczająco dobrze obsłużyć Twój program. Ty oczywiście powinieneś dostarczyć wraz z aplikacją podręcznik użytkownika oraz projektować interfejsy w sposób przejrzysty, lecz nie jesteś w stanie uniknąć sytuacji, w której użytkownik obsłuży ją nieprawidłowo — na przykład w danym polu tekstowym wpisze tekst zamiast liczby. Taki z pozoru błahy błąd może spowodować niespodziewane efekty i dziwne zachowania Twojego programu, dlatego nowoczesne języki programowania dostarczają odpowiednie mechanizmy, dzięki którym jesteśmy w stanie odpowiednio zareagować na takie przypadki.
Czym są wyjątki
Wyjątkiem nazywamy mechanizm kontroli przepływu występujący w językach programowania i służący do obsługi zdarzeń wyjątkowych. Zdarzenia wyjątkowe to w szczególności błędy, jak np. dzielenie przez zero. Dzielenie liczby przez zero w programowaniu nie jest dopuszczalne. Nowoczesne kompilatory starają się wykryć takie sytuacje i nie dopuścić do kompilacji. Przykład: int X; X = 10 / 0;
Taki kod nie zostanie skompilowany, ponieważ kompilator wykryje próbę dzielenia przez zero. Można go jednak łatwo oszukać, podstawiając wartości pod zmienne: int X = Y = Z =
X, Y, Z; 10; 0; X / Y;
Powyższy kod zostanie skompilowany, lecz już w trakcie działania taki program się „wysypie”, wyświetlając błąd. Mechanizm wyjątków pozwala nam przechwycić takie niedopuszczalne i wyjątkowe sytuacje i odpowiednio zareagować, np. wyświetlając komunikat: Panie, nie wiesz, że nie można dzielić przez zero?. Kompilator jest jednak tylko głupim programem i musimy go odpowiednio uświadomić, jakie miejsce kodu jest narażone na wystąpienie błędu.
Obsługa wyjątków W języku C#, jak i w wielu innych popularnych językach, kod narażony na wystąpienie nieprzewidzianych sytuacji musimy oznaczyć słowem kluczowym try. Kod występujący w bloku try będzie „obserwowany” i w razie wystąpienia nieprzewidzianych sytuacji będzie można odpowiednio zareagować: try { // tutaj kod }
Sam blok try nie wystarczy, musimy gdzieś zawrzeć kod, który będzie wykonywany w razie wystąpienia błędu. W tym celu używamy słowa kluczowego catch:
try { // kod } catch { // kod w razie wystąpienia wyjątku }
Przykładowo, aby odpowiednio zareagować na próbę dzielenia, możemy zastosować następujące instrukcje: int X, Y, Z; X = 10; Y = 0; try { Z = X / Y; } catch { Console.WriteLine("Prosimy nie dzielić przez zero!"); }
Spróbuj uruchomić taki kod. Wskutek jego działania na konsoli wyświetlony zostanie tekst Prosimy nie dzielić przez zero!. Uruchamiając projekt z poziomu środowiska Visual C# Express Edition, możesz nie zaobserwować działania wyjątków. Wszystko dlatego, że to środowisko odpowiada w takim przypadku za obsługę błędów. Aby lepiej zobrazować działanie wyjątków, uruchamiaj swoje programy bez użycia debuggera. W Visual C# Express Edition odpowiada za to skrót klawiaturowy Ctrl+F5.
Blok finally Blok catch jest opcjonalny. Równie dobrze w miejsce słowa kluczowego catch możemy wpisać finally. Różnica jest spora: kod zawarty w bloku finally zostanie wykonany zawsze, bez względu na to, czy wyjątek wystąpił, czy też nie. Jest to dobre miejsce na wykonanie instrukcji, które muszą być wykonane przed zamknięciem programu. Dobrym przykładem są operacje na plikach . Po otwarciu pliku dokonujemy odczytu danych i skomplikowanych operacji. Dodajmy do tego, że plik jest otwarty na wyłączność naszego programu. Chcemy, aby w razie wystąpienia
błędu plik został zamknięty. Dobrą praktyką jest łączenie bloku try-catch-finally — dzięki temu możemy odpowiednio zareagować na wystąpienie wyjątku oraz wykonać kod niezbędny przed zamknięciem aplikacji. Oto przykład: try { Z = X / Y; } catch { Console.WriteLine("Prosimy nie dzielić przez zero!"); } finally { Console.WriteLine("Kod z bloku finally"); }
Po uruchomieniu takiej aplikacji oba komunikaty zostaną wyświetlone tylko wtedy, jeżeli dzielenie spowoduje błąd (w najlepszym przypadku zostanie wyświetlony jeden z nich, z bloku finally).
Zagnieżdżanie wyjątków Bloki wyjątków można w miarę potrzeb dowolnie zagnieżdżać: try { // kod try { // kod try { } finally { // finally } } catch {
// jeszcze inny błąd } } catch { // błąd } finally { // blok finally }
Wystąpienie wyjątku w zagnieżdżonym bloku nie oznacza, że wykonany zostanie również kod z „zewnętrznych” bloków catch.
Klasa System.Exception Nieobsłużone wyjątki, czyli takie, których nie obsługuje nasza aplikacja, są obsługiwane przez system. Owocuje to najczęściej wystąpieniem komunikatu o błędzie, często niejasnym, informującym np. o nieprawidłowym odwołaniu do pamięci. To już jednak są ekstremalne sytuacje. W każdym razie jeśli wystąpi wyjątek, system dostarcza programiście informacji na jego temat, które może on obsłużyć wedle własnego uznania. Informacje oczywiście dostarczone są w formie obiektu klasy, której klasą bazową jest System.Exception. W rzeczywistości środowisko .NET Framework posiada całkiem pokaźną kolekcję wyjątków dziedziczonych po tej właśnie klasie. Poniższy kod prezentuje obsługę wyjątku polegającego na wyświetlaniu dostarczonego komunikatu o błędzie: // kod try { Z = X / Y; } catch (Exception e) { Console.WriteLine(e.Message); }
Jak widzisz, parametr bloku catch jest opcjonalny, aczkolwiek dopuszczalny. Taki zapis oznacza deklarację zmiennej e typu Exception (czyli w rzeczywistości System.Exception) i
przypisanie do niej informacji odnośnie do błędu. Tabela 8.1 zawiera spis najważniejszych właściwości klasy System.Exception. Tabela 8.1. Najważniejsze właściwości klasy System.Exception Właściwość Opis Data Dodatkowe informacje na temat źródła wystąpienia wyjątku. Umożliwia odczytanie lub ustawienie linka (np. do pomocy) związanego z HelpLink błędem. Message Komunikat błędu. Umożliwia odczytanie lub przypisanie nazwy aplikacji lub obiektu, w którym Source wystąpił błąd. TargetSite Umożliwia odczytanie metody, w której wystąpił błąd.
Selektywna obsługa wyjątków Czasami może zaistnieć sytuacja, w której będziemy chcieli odpowiednio zareagować w zależności od rodzaju wyjątku, jaki wystąpił. Język C# umożliwia w takich sytuacjach dodanie kolejnego bloku catch: string[] Foo = new string[5]; try { Foo[10] = "Bar"; } catch (IndexOutOfRangeException e) { Console.WriteLine("Indeks jest zbyt duży!"); } catch { Console.WriteLine("Inny błąd"); }
Jak widzisz, w tym programie popełniłem ewidentny błąd — próbuję przypisać wartość do indeksu tablicy nr 10. Wskutek takiego działania zostanie wykonany wyjątek IndexOutOfRangeException, który obsłuży pierwszy blok catch. Wszelkie inne nieobsłużone jeszcze wyjątki będą obsługiwane przez domyślny blok catch.
Wywoływanie wyjątków
W wielu językach programowania istnieje możliwość wywoływania danego wyjątku w dowolnym miejscu kodu. Jest to zasygnalizowanie aplikacji, iż w tym miejscu dochodzi do nieprzewidzianej sytuacji. Konstrukcja jest dość prosta, służy do tego słowo kluczowe throw. Ponieważ należy użyć tego słowa w połączeniu z obiektem dziedziczonym po klasie System.Exception, będziemy musieli dodatkowo użyć słowa kluczowego new: try { throw new IndexOutOfRangeException(); } catch (Exception e) { Console.WriteLine(e.Message); }
throw jest najczęściej używane wewnątrz bloku try, aczkolwiek dopuszczalne jest jego
użycie poza nim. W takiej sytuacji aplikacja może uruchomić domyślną obsługę wyjątków, co najczęściej kończy się komunikatem o błędzie.
Własne klasy wyjątków Na potrzeby naszego programu możemy zadeklarować w nim własne klasy obsługi wyjątków. Przykładowo, w grze w kółko i krzyżyk, w metodzie Set() przeprowadzaliśmy walidację danych, należało sprawdzić, czy użytkownik podał prawidłowe współrzędne. Dobrym rozwiązaniem byłoby zadeklarowanie wówczas własnej klasy wyjątków, która byłaby wykonywana w razie podania nieprawidłowych danych: try { FField[X, Y] = GetActive().Type; } catch { throw new BadPointException(); }
Jeśli utworzylibyśmy nową klasę BadPointException, moglibyśmy odpowiednio zareagować na tę sytuację, nie tylko poprzez wyświetlenie odpowiedniego komunikatu, ale również poprzez podanie odnośnika (URL) — np. do opisanych zasad gry. No dobrze, być może trochę zbytnio zobrazowałemsytuację, równie dobrze można to rozwiązać w ten sposób:
try { FField[X, Y] = GetActive().Type; } catch { Console.WriteLine("Nieprawidłowe pole! Naucz się grać!"); }
Ale czy koniecznie chcemy, aby komunikat błędu był wyświetlany w oknie konsoli? Może lepszym rozwiązaniem byłoby, gdybyśmy pozwolili decydować klasie wyjątku, co ma zrobić z danym komunikatem? BadPointException("Nieprawidłowe pole! Naucz się grać!");
Niech klasa BadPointException decyduje, co program powinien w takiej chwili zrobić. Może wyświetlić komunikat na konsoli albo w nowym okienku Windows? Jeżeli będziemy dostosowywali nasz program, aby działał nie — jak dotychczas — w oknie konsoli, ale z wykorzystaniem biblioteki WinForms, wymagane poprawki będą kosmetyczne (albo w ogóle ich nie będzie).
Deklarowanie własnej klasy Najlepszym rozwiązaniem jest skorzystanie z tego, co już jest. Po co wywarzać otwarte drzwi? Najlepiej więc będzie, gdy nasza nowa klasa będzie dziedziczyła po System.Exception. Po lekturze poprzednich rozdziałów nie powinno być z tym problemu: public class MediumException : System.Exception { public MediumException(string Message) : base(Message) { this.Source = "FooException"; this.HelpLink = "http://4programmers.net/C_sharp"; } }
Dobrą praktyką jest, aby klasy obsługi wyjątków posiadały w nazwie słówko Exception. W konstruktorze klasy do odpowiednich właściwości przypisywane są dane, które mogą pomóc w ewentualnym odszukaniu i naprawie usterki. Zwróć również uwagę, że konstruktor klasy MediumException dziedziczy po takim samym konstruktorze z System.Array. W bardziej rozbudowanych aplikacjach zalecane jest deklarowanie 3 konstruktorów dla klas wyjątków, każdy z innymi parametrami:
public MediumException() { } public MediumException(string Message) : base(Message) { } public MediumException(string Message, Exception inner) : base(Message, inner) { }
Przykładowa aplikacja Aby usystematyzować wiedzę na temat wyjątków, proponuję napisanie prostej aplikacji z wykorzystaniem biblioteki WinForms. W zależności od wybranej opcji będzie ona generować dany wyjątek, a następnie odpowiednio go obsługiwać. Program w trakcie działania zaprezentowany został na rysunku 8.1.
Rysunek 8.1. Program podczas działania
Do napisania takiej aplikacji użyłem komponentów Button, RadioButton oraz RichTextBox. W zależności od wybranej opcji po naciśnięciu przycisku generowany zostanie dany wyjątek. Kod procedury zdarzeniowej wygląda następująco: private void RunBtn_Click(object sender, EventArgs e) { try { if (lowExceptionRadio.Checked) { throw new LowException("Niegroźny błąd"); } if (mediumExceptionRadio.Checked) { throw new MediumException("Średni błąd"); } if (HighExceptionRadio.Checked) { throw new HighException(); } } catch (Exception ex) { RichBox.Clear(); RichBox.Text += String.Format( "Komunikat: {0}\n" + "Podzespół: {1}\n" + "Metoda: {2}\n" + "Podzespół: {3}", ex.Message, ex.Source, ex.TargetSite, ex.HelpLink); } }
W bloku catch następuje przechwycenie wyjątku i wyświetlenie informacji na jego temat. Pełny kod źródłowy programu znajduje się na listingu 8.1. Właściwość Checked (typu bool) komponentu RadioButton informuje, czy kontrolka jest zaznaczona, czy też nie. Listing 8.1. Obsługa wyjątków w C# using System; using System.ComponentModel; using System.Data; using System.Drawing;
using System.Text; using System.Windows.Forms; namespace ExceptionApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void RunBtn_Click(object sender, EventArgs e) { try { if (lowExceptionRadio.Checked) { throw new LowException("Niegroźny błąd"); } if (mediumExceptionRadio.Checked) { throw new MediumException("Średni błąd"); } if (HighExceptionRadio.Checked) { throw new HighException(); } } catch (Exception ex) { RichBox.Clear(); RichBox.Text += String.Format( "Komunikat: {0}\n" + "Podzespół: {1}\n" + "Metoda: {2}\n" + "Podzespół: {3}", ex.Message, ex.Source, ex.TargetSite, ex.HelpLink); } } } public class LowException : System.Exception { public LowException(string Message) : base(Message) { this.Source = "FooException"; this.HelpLink = "http://4programmers.net"; }
} public class MediumException : System.Exception { public MediumException(string Message) : base(Message) { this.Source = "FooException"; this.HelpLink = "http://4programmers.net/C_sharp"; } } public class HighException : System.Exception
{
}
}
Pamiętaj, aby wszelkie klasy swojego programu umieszczać w kodzie niżej niż klasa obsługi formularza (w moim wypadku — Form1). Inaczej środowisko Visual C# Express Edition ma problem z prawidłowym działaniem w trybie projektowania. Właściwie wszystko w tym kodzie powinno być dla Ciebie zrozumiałe. W konstruktorze przypisujemy wartości właściwościom dziedziczonym po klasie System.Exception. Owe właściwości są później odczytywane w bloku catch.
Przepełnienia zmiennych Na początku tej książki wprowadziłem pojęcie „typu danych”. Tam również wspomniałem o tym, iż każdy typ danych posiada jakiś maksymalny zakres, tj. maksymalną wartość, jaką można przypisać do zmiennej tego typu. Np. maksymalna wartość, jaką można przypisać do zmiennej typu byte, to 255. Próba przypisania większej wartości zakończy się błędem: byte b = 256;
OK, kompilator wykrywa takie próby, lecz nie jest w stanie wykryć próby przypisania wartości większej niż 255 w trakcie działania programu: byte X = 250; byte Y = 50; byte Z = (byte) (X + Y);
W tym przykładzie zmienna Z będzie posiadać wartość 44. Dlaczego? Po przekroczeniu zakresu wartości będą ponownie numerowane od zera. Tak więc: 255+50 = 300–255 = 45.
Ponieważ wartości numerowane są od zera, zmienna Z będzie miała wartość 44, a nie 45, jak mogłoby wynikać z tego rachunku matematycznego. Użycie słowa kluczowego checked spowoduje, iż przy przepełnieniu zmiennej zgłaszany będzie wyjątek OverflowException: using System; namespace FooApp { class Program { static void Main(string[] args) { byte X, Y, Z; X = 250; Y = 50; Z = 0; try { Z = checked((byte)(X + Y)); } catch (Exception e) { Console.WriteLine(e.Message); } Console.Write(Z); Console.Read(); } }
W wyniku zaistnienia takiego kodu na ekranie konsoli wyświetlona zostanie treść komunikatu o błędzie. Słowa kluczowego checked można użyć jako operatora (tak jak to przedstawiono w przykładzie powyżej) lub w formie konstrukcji: checked { // kod narażony na przepełnienie zmiennej }
Język C# posiada również słowo kluczowe unchecked, które powoduje, iż w przypadku przepełnienia zmiennej nie jest zgłaszany wyjątek. Jest to jednak domyślne zachowanie aplikacji pisanej w C# więc nie ma potrzeby jawnego użycia tego słowa.
Podsumowanie Obsługa wyjątków w C# nie jest ani trudna, ani też konieczna, aczkolwiek warto się nad tym zastanowić podczas pisania aplikacji. Jeżeli piszesz kod, który może być narażony na ryzyko wystąpienia błędu, stosuj wywołanie try-catch-finally. Pozwoli Ci ono odpowiednio zareagować na ewentualne błędy oraz zwolnić wszystkie zasoby zadeklarowane w trakcie działania aplikacji.
Rozdział 9 Łańcuchy w C# Łańcuchy w informatyce oznaczają ciąg znaków. Do tej pory pojęcie „ciąg” kojarzyło Ci się zapewne z typem string. Powiedzmy sobie szczerze, że jest to jeden z najczęściej wykorzystywanych typów danych w C#. W tym rozdziale mam zamiar wyłożyć tematykę wykorzystania łańcuchów w C#, również przy użyciu klasy System.StringBuilder. Omówione zostaną podstawowe metody służące do operowania na łańcuchach w środowisku .NET Framework.
Typ System.String Podstawowym typem danych w środowisku .NET Framework, który umożliwia operacje na łańcuchach, jest System.String. W C# odpowiednikiem tego typu jest string i taki zapis stosuje się najczęściej ze względu na skróconą formę. Każdy język obsługujący platformę .NET posiada własny odpowiednik typu System.String, np. w Delphi jest to po prostu String. W zamierzchłych czasach Turbo Pascala, który również posiadał obsługę łańcuchów, długość jednego łańcucha mogła wynosić maksymalnie 255 znaków. Nie jest to zbyt praktyczne, gdy musimy przechować w zmiennej naprawdę dużą ilość tekstu. W nowoczesnych językach programowania długość tekstu, jaki możemy przypisać do łańcucha, jest niemal nieograniczona, a praktycznie ogranicza ją tylko ilość pamięci, jaką posiadamy w komputerze. Ilość pamięci, jaka jest rezerwowana na potrzeby danego łańcucha, zależy od jego długości. Wszystko odbywa się automatycznie, nie musimy się martwić o przydzielanie czy zwalnianie pamięci. Łańcuch w .NET jest właściwie sekwencją (tablicą) znaków typu String.Char połączoną w
jedną całość. Do każdego elementu łańcucha możemy odwoływać się jak do elementu tablicy przy pomocy symboli []: System.String S = "Hello World"; Console.WriteLine(S[0]);
Wspomniana tablica jest indeksowana od zera, co oznacza, że pierwsza litera w łańcuchu posiada indeks 0, druga — 1 itd. Czyli pod konstrukcją S[0] kryje się litera H. W rzeczywistości taką możliwość zapewnia indekser zadeklarowany w klasie System.String. W dalszej części rozdziału, mówiąc o typie System.String, będę się posługiwał skróconą nazwą string.
Unicode w łańcuchach Platforma .NET w całości wspiera system kodowania znaków Unicode. Standard ten w zamierzeniu obejmuje wszystkie języki świata i eliminuje problem tzw. krzaczków. Oznacza to, że łańcuchy, m.in. języka C#, są kodowane w systemie Unicode, co sprawia, że każdy znak zajmuje 2 bajty pamięci. Co więcej — kodowanie Unicode możemy również stosować w kodzie źródłowym, używając np. polskich znaków w nazwie zmiennych: string gżegżółka;
Jest to bardzo dobre rozwiązanie mające ułatwić pisanie aplikacji wielojęzykowych. Dzięki temu unikamy również problemu kodowania i ewentualnej konwersji systemu znaków.
Niezmienność łańcuchów Każdą wartość raz przypisaną łańcuchowi możesz zmienić — to fakt. Co więc oznacza, że łańcuchy są niezmienne? Oznacza to, że jeżeli raz stworzyliśmy egzemplarz typu string, to nie będziemy mogli zmienić jego wartości. Nie oznacza to jednak, że poniższy kod spowoduje błąd: System.String S = "Hello World"; S = "Witaj Świecie";
Jest on jak najbardziej prawidłowy, jednak jego realizacja wymaga zastosowania dwóch egzemplarzy (obiektów) w pamięci komputera. Co prawda kod wygląda tak, jakbyśmy tę
wartość modyfikowali, ale w rzeczywistości w pamięci tworzony jest nowy obiekt z nową wartością, która jest zwracana. Stara wartość znajduje się w pamięci i czeka na usunięcie przez mechanizm platformy .NET — garbage collection. Nie należy się więc tym zbytnio przejmować. Abyś się przekonał, że modyfikowana wartość w rzeczywistości wcale nie ulega modyfikacji, możesz wykonać małe ćwiczenie. Oto prosty fragment kodu: System.String S = "Hello World"; Console.WriteLine(S.ToUpper()); Console.WriteLine(S);
Metoda ToUpper() powoduje zamianę wszystkich znaków w łańcuchu na wielkie i zwrócenie nowej, zmodyfikowanej wartości. Pod zmienną S kryje się jednak oryginalna, pierwotnie zadeklarowana wartość. Aby zastąpić dotychczasową wartość, należy użyć oczywiście operatora przypisania: S = S.ToUpper();
Ze względu na to, iż łańcuchy są niezmienne, nie zaleca się przeprowadzania na nich wielu operacji dodawania tekstu, zamiany czy usuwania fragmentów łańcucha. Taki kod będzie po prostu działał niezwykle wolno. Zamiast tego zaleca się użycie klasy StringBuilder. Klasa System.String jest zaplombowana. Oznacza to, że żadna klasa nie może po niej dziedziczyć. Przeanalizujmy teraz taką sytuację: System.String S = "Adam"; S = S.Insert(4, " Boduch").ToUpper().Replace("CH", "SZEK");
Mimo iż ten kod może wydawać się nieco dziwny, jest jak najbardziej prawidłowy. W pierwszej kolejności pod indeksem nr 4 wstawiany jest nowy tekst. To owocuje powstaniem w pamięci nowego, zamienionego obiektu. Następnie wszystkie znaki w takim łańcuchu są zamieniane na wielkie, co znowu tworzy nowy obiekt. Na końcu następuje zamiana znaków i utworzenie nowego obiektu, który zostaje przypisany do zmiennej S. Pozostałe obiekty będą dostępne w pamięci do czasu ich usunięcia przez odśmiecacz pamięci (ang. garbage collection). Aby bardziej zobrazować całą sytuację, proponuję wkleić do projektu następujący kod: System.String S = "Adam"; Console.WriteLine(S.Insert(4, " Boduch"));
Console.WriteLine(S.ToUpper()); Console.WriteLine(S.Replace("CH", "SZEK")); // wartość oryginalna Console.WriteLine(S);
Wszystkie operacje zawarte w tym kodzie będą przeprowadzane na łańcuchu oryginalnym znajdującym się w zmiennej S.
Konstruktory klasy Aby korzystać z właściwości klasy string, należy jedynie zadeklarować zmienną wskazującą na ten typ danych. Nie ma potrzeby jawnego wywoływania konstruktora tej klasy. Jeżeli jednak zajdzie taka potrzeba, klasa System.String posiada kilka przeciążonych konstruktorów, które mogą przyjąć różne wartości. Oto przykład wywołania konstruktora, który jako argument przyjmuje tablicę znaków char: char[] charArr = new char[] { 'H', 'e', 'l', 'l', 'o' }; System.String S = new System.String(charArr);
Klasa udostępnia wiele konstruktorów. Ciekawych odsyłam do dokumentacji języka C#.
Operacje na łańcuchach Klasa System.String udostępnia kilka ciekawych metod służących do operowania na łańcuchach. Zapewne z wielu z nich będziesz nie raz korzystał w trakcie programowania w C#, więc postanowiłem tutaj opisać kilka najciekawszych.
Porównywanie łańcuchów Zacznijmy od rzeczy najprostszej, czyli od porównywania wartości łańcuchów. Najprościej użyć przeciążonych operatorów != oraz ==, które zadeklarowane zostały w klasie System.String. Czyli porównywanie wartości łańcuchów wygląda tak jak np. porównywanie wartości typów liczbowych: string s1, s2;
s1 = "Hello"; s2 = "hello"; if (s1 != s2) { Console.WriteLine("Wartości są różne"); }
Taka instrukcja warunkowa zwróci wartość true, ponieważ operator != rozróżnia wielkość liter, która jest różna w zmiennych s1 i s2. Jeżeli chcemy, aby aplikacja ignorowała wielkość znaków, należy skorzystać z metody Compare(): s1 = "Hello"; s2 = "hello"; if (String.Compare(s1, s2, true) == 0) { Console.WriteLine("Wartości są równe"); }
Pierwsze dwa parametry metody Compare() to oczywiście nazwy zmiennych. Trzeci parametr określa, czy różnice w wielkości znaków mają być ignorowane (true), czy też uwzględniane (false). Metoda Compare() zwraca wartość liczbową. Jeżeli jest ona mniejsza od zera, oznacza to, że łańcuch s1 jest mniejszy od s2. Wartość większa od zera oznacza, że s1 jest większy od s2, a zero oznacza, iż są one równe. Trzeci parametr metody Compare() jest opcjonalny. Jeżeli go nie określimy, metoda domyślnie będzie rozróżniać wielkość znaków. Porównywanie łańcuchów przy pomocy operatorów == oraz != zapewnia w rzeczywistości mechanizm przeładowania operatorów, który został użyty w klasie System.String. Istnieje również metoda CompareOrdinal(), której z pewnością będziesz rzadziej używał. Porównuje ona ciągi zgodnie z kodami ASCII przypisanymi do danego znaku; nie odzwierciedla porządku alfabetycznego: s1 = "hello"; s2 = "HELLO"; Console.WriteLine(String.Compare(s1, s2, true)); Console.WriteLine(String.CompareOrdinal(s1, s2));
Po uruchomieniu takiego kodu na ekranie konsoli zostanie wyświetlone: 0 32
Wartości zwracane przez metodę CompareOrdinal() oznaczają to samo, co te zwracane przez Compare() (tzn. wartość 0 oznacza, iż ciągi są takie same). Skoro jesteśmy przy temacie porównywania wartości łańcuchów, warto wspomnieć o możliwości porównywania z uwzględnieniem różnych kultur. Służy do tego przeciążona metoda Compare(): s1 = "info"; s2 = "INFO"; Console.WriteLine(String.Compare(s1, s2, true, new CultureInfo("pl-PL"))); Console.WriteLine(String.Compare(s1, s2, true, new CultureInfo("tr-TR")));
Umożliwia ona porównywanie wartości z uwzględnieniem regionu (kultury). W pierwszym wypadku porównywanie odbyło się przy zastosowaniu kultury pl-PL (Polska), a drugie tr-TR (Turcja). Okazuje się, że w Turcji mała i duża litera i ma zupełnie inne znaczenie i takie porównywanie zakończy się wyświetleniem na konsoli: 0 1 Typ CultureInfo() znajduje się w przestrzeni nazw System.Globalization.
Modyfikacja łańcuchów W tabeli 9.1 zawarłem najważniejsze metody klasy System.String służące do modyfikacji tekstu. Tabela zawiera skrótowy opis ich przeznaczenia; w dalszej części rozdziału zawarłem krótkie przykłady prezentujące działanie każdej z nich. Tabela 9.1. Główne metody klasy System.String Funkcja/Procedura Opis Concat() Łączy ze sobą dwie lub więcej instancji klasy string. Contains() Sprawdza, czy część łańcucha nie znajduje się w podanym łańcuchu. Length() Zwraca ilość znaków znajdujących się w ciągu znakowym. Copy() Tworzy nową instancję ciągu znakowego (kopiuje jego zawartość). Insert() Wstawia tekst w określone miejsce ciągu znakowego. Join() Funkcja łączy kilka elementów tekstowych w jeden. Remove() Funkcja umożliwia usunięcie kawałka ciągu znaków. Replace() Zamienia część ciągu znakowego.
Split() SubString() ToLower() ToUpper() Trim() TrimEnd() TrimStart()
Umożliwia rozdzielenie ciągu znaków na mniejsze fragmenty na podstawie podanego znaku. Wycina część ciągu znaków. Zwraca ciąg znaków zapisany małymi literami. Zwraca ciąg znaków zapisany wielkimi literami. Usuwa wszystkie spacje z początku i z końca ciągu znaków. Usuwa wszystkie spacje z końca ciągu znaków. Usuwa wszystkie spacje z początku ciągu znaków.
Concat() Metoda Concat() służy do łączenia ze sobą dwóch lub większej ilości łańcuchów. Jej użycie jest proste, bowiem ogranicza się do podania na liście parametrów łańcuchów, które mają zostać połączone: s1 = "Hello"; s2 = " World"; Console.WriteLine(String.Concat(s1, s2));
Metoda zwraca wartość połączonych ze sobą łańcuchów. Moim zdaniem łatwiejszym i przejrzystszym sposobem jest wykorzystanie do tego celu operatora + (dodawanie), który z równie dobrym skutkiem połączy dane ciągi: Console.WriteLine(s1 + s2);
Contains() Metoda Contains() może być wykorzystana do wyszukiwania danej frazy w łańcuchu. Zwraca true, jeżeli dana fraza została odnaleziona, lub false jeżeli nie albo łańcuch jest pusty. Należy zwracać uwagę na wielkość znaków, gdyż metoda Contains() je rozróżnia. Oto prosty przykład jej wykorzystania: string s1 = "Jaś i Małgosia świetną parą byli!"; if (s1.Contains("Małgosia"))
{ Console.WriteLine("String zawiera słowo \"Małgosia\""); }
Jeżeli chcemy w łańcuchu tekstowym zawrzeć znak cudzysłowu ("), należy poprzedzić go backslashem (\). Znak \ również musi być poprzedzony backslashem, jeżeli ma zostać wyświetlony: s1 = "C:\\Windows\\System";
Length Podobnie jak w przypadku tablic właściwość Length() podaje rozmiar, tak w kontekście użycia z łańcuchami zwraca ilość znajdujących się w nich znaków: s1 = "Jaś i Małgosia świetną parą byli!"; Console.WriteLine(s1.Length);
Właściwość Length jest jedynie do odczytu, nie można przypisywać do niej żadnych wartości.
Copy() Metoda Copy() tworzy nową instancję klasy z identyczną zawartością: s1 = "Jaś i Małgosia świetną parą byli!"; s2 = String.Copy(s1);
Insert() Metoda Insert() umożliwia wstawienie tekstu w dane miejsce łańcucha. Pierwszym parametrem musi być pozycja (indeks), w jakiej zostanie umieszczony nowy tekst. Drugim parametrem musi być sam tekst do wstawienia. Oto przykład: s1 = "Jaś i Małgosia świetną parą byli!"; s1 = s1.Insert(s1.Length, " I co im po tym?"); Console.WriteLine(s1);
Join() Często w trakcie zmagania się z jakimś problemem programistycznym możesz się natknąć na konieczność połączenia elementów tablicy w jeden ciąg. Metoda Join() łączy elementy tablicy w jedną całość na podstawie podanego łącznika (dowolny ciąg znaków). Prosty przykład: string[] Foo = { "Ala", "ma", "kota" }; string s1; s1 = String.Join(" ", Foo); Console.WriteLine(s1);
Zmienna s1 po wykonaniu takiego kodu będzie miała wartość Ala ma kota.
Split() Metoda Split(), znana zapewne programistom Perla czy PHP, wykonuje czynność odwrotną niż Join(). Rozbija mianowicie łańcuch znaków na podstawie podanego separatora i zapisuje do tablicy kolejne elementy ciągu. Parametrem metody Split() jest lista separatorów w postaci tablicy typu char. Metoda zwraca tablicę typu string, w której znajdują się odseparowane elementy: string s1 = "Ala ma kota"; string[] Foo = s1.Split(new char[] { ' ' }); foreach (string Bar in Foo) { Console.WriteLine(Bar); } Console.Read();
Replace()
Metoda Replace() służy do podmieniania pewnych elementów w łańcuchu na nowe. Jej użycie zaprezentowałem na początku tego rozdziału. Pierwszym parametrem musi być tekst, który ma zostać zastąpiony, a drugim — nowa fraza: string s1 = "Ala ma kota"; Console.WriteLine(s1.Replace("Ala", "Bartek"));
W wyniku wykonania takiego kodu na ekranie konsoli zostanie wyświetlony napis Bartek ma kota.
Remove() Czasem może zaistnieć potrzeba usunięcia danej frazy łańcucha. Wówczas z pomocą przychodzi metoda Remove(). Pierwszym parametrem musi być indeks, od którego metoda rozpocznie usuwanie tekstu. To jest właściwie jedyny wymagany przez nią parametr. Wtedy usunie ona wszystko, co znajduje się za podanym indeksem. Istnieje jednak możliwość określenia ilości tekstu, jaka ma zostać skasowana: string s1 = "Ala ma kota"; Console.WriteLine(s1.Remove(0, 4));
Taki kod wyświetli na konsoli napis ma kota.
SubString() Metoda SubString pozwala na skopiowanie części ciągu, począwszy od podanego znaku, i zwrócenie skopiowanej wartości. Jej budowa i zastosowanie są proste. Pierwszym parametrem musi być pozycja, od której rozpocznie się kopiowanie ciągu znaków, a drugim jest liczba znaków do skopiowania: string s1 = "Ala ma kota"; /* wyświetli napis: Ala */ Console.WriteLine(s1.Substring(0, 4));
ToUpper(), ToLower() Zastosowanie tych metod jest proste. Pierwsza z nich (ToUpper()) zamienia wszystkie znaki w łańcuchu na wielkie, a druga (ToLower()) na małe.
Trim(), TrimStart(), TrimEnd() Jeżeli na początku lub/i na końcu łańcucha znajdują się tzw. białe znaki (spacje, znaki nowej linii), możemy je usunąć przy pomocy metody Trim(). Oto przykład: string s1 = " Ala ma kota\t\n"; Console.WriteLine(s1.Trim());
Po wykonaniu metody Trim() wartością łańcucha będzie Ala ma kota. Podobnie TrimStart() usuwa białe znaki na początku łańcucha, a TrimEnd() — na jego końcu. \n w łańcuchu powoduje wstawienie w danym miejscu znaku nowej linii, natomiast \t to znak tabulacji.
Łańcuchy w WinForms Co prawda biblioteka WinForms nie jest tematem tego rozdziału, ale chciałbym tutaj wspomnieć o kilku jej tekstowych kontrolkach, które wykorzystują mechanizmy łańcuchów. Musisz sobie uświadomić, że biblioteka WinForms, tak jak cała biblioteka klas .NET Framework, opiera się na klasach. Komponenty (które również są klasami) posiadają właściwości oraz metody, z których duża część jest właśnie typu System.String. Podstawowe dwa komponenty służące do edycji tekstu to Textbox oraz RichTextBox. Pierwsza z nich jest kontrolką jednoliniową służącą do wpisywania prostych i krótkich notek. Drugi komponent jest rozbudowaną kontrolką tekstową — miniedytorem tekstu. Może przechowywać długie teksty, wyświetlać zawartość plików tekstowych. Maksymalną długość tekstu, jaką może przechowywać komponent RichTextBox, definiuje jego właściwość — MaxLength. Domyślnie jest to 2147483647 znaków. Komponent TextBox może służyć jako kontrolka wieloliniowa, lecz jest to możliwość rzadko używana. Jeżeli mimo wszystko chcesz, aby komponent mógł przechowywać wiele linii tekstu, zmień właściwość Multiline na true.
Za wyświetlanie tekstu w komponencie TextBox odpowiada właściwość Text. Możesz przypisać tekst wyświetlany w kontrolce za pośrednictwem okna Properties lub bezpośrednio w kodzie: TextBox1.Text = "Witam";
Właściwość Text jest typu System.String, więc automatycznie na jej wartości możemy operować tak jak na zwykłych zmiennych typu string. W komponencie RichTextBox sprawa jest nieco bardziej skomplikowana, bo i on sam jest bardziej skomplikowany. Zasadniczo mamy możliwość korzystania z właściwości Lines, która w rzeczywistości jest tablicą typu string. Zasadniczo lepszym rozwiązaniem będzie korzystanie z właściwości Text typu System.String. Należy pamiętać, iż w takim wypadku znak \n jest odpowiedzialny za przejście do nowej linii. Listing 9.1 prezentuje prosty program WinForms służący do wczytywania zawartości pliku tekstowego. Oprócz wczytania zawartości do komponentu RichTextBox program wyświetla ścieżkę do wybranego pliku w komponencie TextBox (rysunek 9.1). Rysunek 9.1. Program wyświetlający zawartość plików tekstowych
Listing 9.1. Program służący do wczytywania zawartości pliku tekstowego using System; using System.ComponentModel;
using System.Data; using System.Windows.Forms; using System.IO; namespace WinForms { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // wyświetlenie okna „Otwórz” openFileDialog1.ShowDialog(); // przypisanie ścieżki do pliku textBox1.Text = openFileDialog1.FileName; // odczytanie zawartości pliku FileStream FileStr = (FileStream)openFileDialog1.OpenFile(); StreamReader Reader = new StreamReader(FileStr); richTextBox1.Clear(); // przypisanie tekstu richTextBox1.Text = Reader.ReadToEnd(); } } }
Do zbudowania interfejsu aplikacji skorzystałem z kontrolki OpenFileDialog, Button, TextBox, Panel oraz oczywiście RichTextBox.
Klasa StringBuilder Klasa StringBuilder (w odróżnieniu od System.String) reprezentuje zmienny łańcuch. Stosowanie klasy StringBuilder jest szczególnie zalecane w sytuacjach, w których musisz często modyfikować łańcuchy w swoim programie (np. w pętli). Klasa StringBuilder została zdefiniowana w przestrzeni nazw System.Text. Do utworzenia obiektu tej klasy można skorzystać z jednego z kilku przeciążonych konstruktorów. W zależności od rodzaju konstruktora będziesz musiał podać wartości dla następujących parametrów:
capacity — początkowy rozmiar tablicy znaków. Domyślnie jest to 16 znaków. Jeżeli okaże się, że rozmiar tablicy jest zbyt mały, by zrealizować dane zadanie, klasa StringBuilder podwaja tę liczbę. Ze względów wydajnościowych warto jest nadać już w konstruktorze rozmiar, który bez zwiększania wystarczy do wykonania danej operacji. length — długość łańcucha przekazanego w konstruktorze (wyrażona w znakach). value — początkowa wartość (łańcuch znaków) przekazana do klasy.
Metody klasy StringBuilder W tabeli 9.2 znajdują się najważniejsze metody klasy StringBuilder. Tabela 9.2. Najważniejsze metody klasy StringBuilder Metoda Opis Metoda służy do dodawania tekstu na końcu łańcucha utrzymywanego Append() przez klasę StringBulider. Metoda o podobnym działaniu do Append(). Jej dodatkowym atutem jest AppendFormat() możliwość przekazania tzw. specyfikatorów formatu. AppendLine() Umożliwia wstawienie znaku końca linii. Umożliwia wstawienie łańcuchowej reprezentacji danego obiektu we Insert() wskazanym miejscu. Remove() Usuwa wskazaną część łańcucha. Replace() Umożliwia podmianę części łańcucha.
Zastosowanie klasy StringBuilder Wspominałem wcześniej o tym, iż klasa StringBuilder (w przeciwieństwie do String) reprezentuje zmienny łańcuch. W sytuacjach gdy musimy często modyfikować wartość danego łańcucha, ze względów wydajnościowych zalecane jest używanie klasy StringBuilder (listing 9.2). Listing 9.2. Dodawanie wartości do ciągu tekstowego using System; using System.Diagnostics; using System.Text; namespace FooConsole {
class Program { static void Main(string[] args) { Stopwatch Timer = new Stopwatch(); string Bar = ""; // rozpoczynamy odliczanie Timer.Start(); for (int i = 0; i < 10000; i++) { Bar += "Ala ma kota "; } Timer.Stop(); Console.WriteLine("Czas wykonywania: {0} ms", Timer.ElapsedMilliseconds); Console.ReadLine(); } } }
Ten prosty program dodaje w pętli wartość do zmiennej Bar. Czas potrzebny na wykonanie 10 000 tys. powtórzeń pętli to ok. 9000 milisekund (w przypadku mojego procesora), czyli 9 sekund. Aby przyspieszyć działanie aplikacji, można skorzystać z klasy StringBuilder, która znajduje się w przestrzeni System.Text. Użycie tej klasy spowoduje przyspieszenie działania aplikacji aż do 3 milisekund! Różnicę w wydajności widać więc bardzo wyraźnie. Aby skorzystać z klasy StringBuilder, należy utworzyć jej instancję i — oczywiście — dodać przestrzeń nazw System.Text. Przerobiony program z listingu 9.2 zaprezentowany został na listingu 9.3. Listing 9.3. Program napisany z użyciem klasy StringBuilder using System; using System.Diagnostics; using System.Text; namespace FooConsole { class Program { static void Main(string[] args) { Stopwatch Timer = new Stopwatch(); StringBuilder Bar = new StringBuilder();
// rozpoczynamy odliczanie Timer.Start(); for (int i = 0; i < 10000; i++) { Bar.Append("Ala ma kota "); } Timer.Stop(); Console.WriteLine("Czas wykonywania: {0} ms", Timer.ElapsedMilliseconds); Console.ReadLine(); } } }
Do sprawdzenia czasu wykonywania danego kodu użyłem klasy Stopwatch, która znajduje się w przestrzeni nazw System.Diagnostics. Podstawowe dwie metody tej klasy to Start(), która rozpoczyna „odliczanie”, oraz Stop() oznaczająca zakończenie pomiaru czasu. Czas potrzebny na wykonanie danego kodu możemy odczytać z właściwości ElapsedMilliseconds (czas w milisekundach).
Formatowanie łańcuchów Nieraz podczas czytania tej książki mogłeś zauważyć, iż stosowałem formatowanie łańcuchów — np.: Console.WriteLine("Wartość: {0}", Bar);
Chciałbym na chwilę zatrzymać się przy tym zagadnieniu i poświęcić nieco więcej uwagi możliwościom i korzyściom wynikającym z użycia formatowania łańcuchów. Korzyścią jest przede wszystkim przejrzystszy kod. Ale nie tylko, bo jak się za chwilę przekonasz, pojęcie formatowania łańcuchów to coś więcej niż tylko podstawianie wartości w określonym miejscu ciągu. Owszem — jest to podstawowe zadanie formatowania. W miejsce symboli zastępczych, znajdujących się pomiędzy klamrami — { oraz } — w procesie wykonywania kodu zostaną podstawione wartości odpowiadające danemu indeksowi. Oto przykład: Console.WriteLine("{0}, lat {1}, {2} cm wzrostu", "Jan Kowalski", 42, 170);
Każdy kolejny parametr metody WriteLine() jest numerowany od zera. Czyli parametr Jan Kowalski ma indeks 0, parametr 42 — indeks 1 itd. Taka instrukcja spowoduje wyświetlenie na ekranie konsoli tekstu Jan Kowalski, lat 42, 170 cm wzrostu. Pomiędzy klamrami należy umieścić numer indeksu, nic nie stoi na przeszkodzie, aby takie dane wyświetlić w dowolnej kolejności: Console.WriteLine("{2} cm wzrostu, {1} lat, {0}, ", "Jan Kowalski", 42, 170);
Jeżeli chcemy wyświetlić w konsoli znak { lub }, musimy te symbole zapisać podwójnie: Console.WriteLine("{{Ten tekst będzie ujęty w klamry}}"); Przeanalizujmy kolejny przykład. Symbol zastępczy może zawierać parametr określający rozmiar wyrównania. Jest to liczba całkowita określająca szerokość danego pola oraz wyrównanie do prawej lub lewej strony. Spójrz na listing 9.4. Listing 9.4. Symbole zastępcze z parametrem wyrównania using System; namespace FooConsole { class Program { public struct User { public string Name; public byte Age; } static void Main(string[] args) { User[] Bar = new User[2]; Bar[0].Name = "Jan Kowalski"; Bar[0].Age = 32; Bar[1].Name = "Piotr Nowak"; Bar[1].Age = 56; for (int i = 0; i < Bar.Length; i++) { Console.WriteLine("{0,-15} | {1,5}", Bar[i].Name, Bar[i].Age ); } Console.ReadLine(); }
} }
Program jest prosty. Zadeklarowałem w nim dwuelementową tablicę struktur, a następnie w pętli wyświetliłem jej zawartość. W symbolach zastępczych, po przecinku określiłem wartości wyrównania. Wartość -15 oznacza, iż pole będzie posiadać rozmiar w wielkości 15 znaków, a tekst będzie wyrównany do lewej. Wartość dodatnia oznacza wyrównanie do prawej (zobacz rysunek 9.2).
Rysunek 9.2. Program prezentujący właściwości wyrównania Do tej pory prezentowałem zastosowanie formatowania na przykładzie metody WriteLine(). Chciałbym zaznaczyć, że taka możliwość jest dostępna dla każdego łańcucha typu string dzięki statycznej metodzie Format(): string Bar = String.Format("{0}, {1}", "Jan Kowalski", 42);
Specyfikatory formatów Symbole zastępcze mają o wiele większe zastosowanie niż wspomniana możliwość wyrównywania wartości. Środowisko .NET Framework definiuje zestaw symboli nazwanych specyfikatorami formatów. Umożliwiają one formatowanie wartości liczbowych, daty i czasu oraz wyliczeniowych, według własnego upodobania. Spójrz na poniższy kod:
int X = 110003242; Console.WriteLine("{0:X}", X);
Oznacza on wyświetlenie liczby, która jest zapisana w zmiennej X, w postaci szesnastkowej. Symbol X jest specyfikatorem nakazującym prezentację danych w postaci szesnastkowej (heksadecymalnej). Kolejny przykład prezentuje wyświetlenie danej wartości w postaci walutowej: int X = 10; Console.WriteLine("{0:C}", X);
Po uruchomieniu takiego kodu na konsoli zostanie wyświetlona wartość walutowa 10,00 zł. W tabeli 9.3 znajdują się specyfikatory dla wartości liczbowych. Symbol waluty (zł) oczywiście zależy od lokalizacji kraju, w jakim się znajdujemy. W USA taki kod spowodowałby wyświetlenie następującej wartości: $10.00.
Tabela 9.3. Specyfikatory formatów dla wartości liczbowych Symbol Opis C lub c Walutowy D lub d Dziesiętny E lub e Naukowy (wykładniczy) F lub f Stałoprzecinkowy G lub g Ogólny N lub n Liczbowy P lub p Procentowy R lub r Zaokrąglony X lub x Szesnastkowy
Przykład programu używającego specyfikatorów liczbowych znajduje się na listingu 9.5. Listing 9.5. Przykład zastosowania specyfikatorów using System; namespace FooConsole { class Program { static void Main(string[] args) { System.Int32 Integer = 10; System.Double Decimal = 10.25; System.Double Percent = 0.50;
Console.WriteLine("Walutowy: {0:C}", Integer); Console.WriteLine("Dziesiętny: {0:D5}", Integer); Console.WriteLine("Naukowy: {0:E}", Integer); Console.WriteLine("Stałoprzecinkowy: {0:F2}", Integer); Console.WriteLine("Ogólny: {0:G2}", Integer); Console.WriteLine("Liczbowy: {0:N2}", Integer); Console.WriteLine("Procentowy: {0:P0}", Percent); Console.WriteLine("Zaokrąglony: {0:R1}", Decimal); Console.WriteLine("Heksadecymalny: {0:X}", Integer); Console.ReadLine(); } } }
W programie zadeklarowałem 3 przykładowe zmienne, którym nadałem odpowiednie wartości. Przy pomocy odpowiednich specyfikatorów mogę spreparować wyświetlane dane według własnych upodobań. Rezultat działania takiego programu prezentuje rysunek 9.3.
Rysunek 9.3. Przykład zastosowania specyfikatorów liczbowych Zwróć uwagę, iż symbol specyfikatora określa się po znaku dwukropka, zaraz po numerze indeksu symbolu zastępczego: {:} Może zauważyłeś, iż w kilku przypadkach po symbolu specyfikatora znajduje się cyfra. Ta cyfra określa precyzję wyświetlanych danych. Np. poniższy kod oznacza, iż liczba będzie wyświetlana z precyzją dwóch miejsc po przecinku:
Console.WriteLine("Stałoprzecinkowy: {0:F2}", Integer);
Własne specyfikatory formatowania Kiedy okaże się, że standardowe specyfikatory formatów nie spełniają naszych oczekiwań w zakresie formatowania wartości liczbowych, możemy stworzyć i zastosować własne łańcuchy formatowania. Przykładowo, przechowujemy w zmiennej liczbę 100230786 i dla zwiększenia jej czytelności chcemy ją wyświetlić w postaci 100 230 786. Umiejętne wykorzystanie niestandardowych specyfikatorów nie powinno sprawić problemu w realizacji tego zadania. Oto kod: System.Int32 Integer = 100230786; Console.WriteLine("{0:### ### ###}", Integer);
Symbol # oznacza dowolną liczbę. Innymi słowy, symbol ten zostanie zastąpiony kolejną cyfrą z liczby przekazanej jako parametr. Kolejne przykłady: System.Double Integer = 123.345; // 123.3 Console.WriteLine("{0:#.#}", Integer); // 123.3450 Console.WriteLine("{0:#.###0}", Integer);
W tabeli 9.4 przedstawiono listę niestandardowych specyfikatorów formatowania. Tabela 9.4. Niestandardowe specyfikatory formatowania Symbol Opis 0 Symbol zastępczy dla zera # Symbol zastępczy dla cyfry . Kropka dziesiętna , Separator tysiąca % Symbol zastępczy dla procentów E0, E+0, E-0, e0, e+0, e-0 Notacja naukowa 'AAA', "AAA" Stały łańcuch ; Separator sekcji
Pozostałe znaki
Wszystkie inne znaki wykorzystywane w łańcuchach formatowania
Tematyka niestandardowych specyfikatorów formatowania wykracza poza ramy tej książki. Po więcej informacji na ten temat odsyłam do dokumentacji firmy Microsoft na temat platformy .NET.
Specyfikatory typów wyliczeniowych Dla typów wyliczeniowych środowisko .NET Framework udostępnia cztery specyfikatory przedstawione w tabeli 9.5. Tabela 9.5. Specyfikatory typów wyliczeniowych Symbol Opis Wyświetla wyliczenie w postaci łańcucha. Jeśli nie jest to możliwe, wyświetla G lub g wartość całkowitoliczbową. Wyświetla wyliczenie w postaci łańcucha. Jeśli nie jest to możliwe, wyświetla F lub f wartość liczbową. Wyświetla sumę tych wartości — jeśli jest to możliwe, łańcuchy są konkatenowane i oddzielane przecinkami. D lub d Wyświetla zmienną typu wyliczeniowego w postaci wartości liczbowej. X lub x Wyświetla zmienną typu wyliczeniowego w postaci wartości szesnastkowej.
Na listingu 9.6 znajduje się przykładowy program prezentujący w praktyce zastosowanie specyfikatorów zaprezentowanych w tabeli 9.4. Listing 9.6. Prezentacja specyfikatorów formatowania dla typów wyliczeniowych using System; namespace FooConsole { class Program { static void Main(string[] args) { DayOfWeek MyDays = DayOfWeek.Friday; Console.WriteLine(MyDays.ToString("G")); // Wyświetli „Friday”. Console.WriteLine(MyDays.ToString("F")); // Wyświetli „Friday”. Console.WriteLine(MyDays.ToString("D")); // Wyświetli „5”. Console.WriteLine(MyDays.ToString("X")); // Wyświetli „00000005”.
Console.ReadLine(); } } }
Przykład z listingu 9.6 operuje na typie wyliczeniowym DayOfWeek, a konkretnie na jego elemencie Friday. Zwróć również uwagę, w jaki sposób zastosowałem funkcję formatowania łańcuchów. Należy wspomnieć, iż jest to możliwe również w metodzie ToStrnig(). Zauważ także, iż w takim przypadku nie jest konieczne wpisywanie numeru indeksu. Jeżeli metodę ToString() wywołam ze zmiennej MyDays, to formatowaną wartością jest ta znajdująca się w tej zmiennej. DayOfWeek jest typem wyliczeniowym znajdującym się w przestrzeni System, reprezentującym dni tygodnia. Pierwszym elementem tego typu jest Sunday. Typ wyliczeniowy jest numerowany od zera.
Typ System.Char Typ char języka C#, będący odpowiednikiem klasy System.Char z CTS, służy do przechowywania pojedynczych znaków typu Unicode. Wartość zmiennej typu char musi być zawarta w apostrofach: char C = 'A';
Zmienna typu char nie może zawierać więcej znaków niż jeden, gdyż kompilator wskaże błąd Too many characters in character literal. Wartość przypisywana do zmiennej typu char może być zapisana w formie heksadecymalnej: char char char char
C1 C2 C3 C4
= = = =
'\x0058'; (char)65; 'A'; '\t'; // znak tabulacji
W tabeli 9.6 zebrano kilka ciekawych metod zawartych w klasie System.Char. Tabela 9.6. Metody klasy System.Char Metoda Opis IsControl() Zwraca true, jeżeli znak zawarty w zmiennej jest znakiem kontrolnym (np.
\t czy \n). IsDigit() Zwraca true, jeżeli znak zawarty w zmiennej jest liczbą. IsLetter() Zwraca true, jeżeli znak zawarty w zmiennej jest literą. IsLower() Zwraca true, jeżeli znak zawarty w zmiennej jest małą literą. IsSymbol() Zwraca true, jeżeli znak zawarty w zmiennej jest symbolem. Zwraca true, jeżeli znak zawarty w zmiennej można zaliczyć do tzw. IsWhiteSpace() białych znaków (spacje, znaki nowej linii itp.).
Podsumowanie Tematyka operowania na łańcuchach wbrew pozorom jest całkiem rozległa. Ja w tym rozdziale opisałem podstawowe mechanizmy operowania na ciągach znaków, w tym metody służące do modyfikacji łańcuchów. Wspomniałem również o klasie StringBuilder oraz o formatowaniu łańcuchów, co z pewnością pomoże Ci efektywnie wykorzystać możliwości platformy .NET w tym zakresie. Bardziej zaawansowanym operowaniem na łańcuchach jest mechanizm wyrażeń regularnych (ang. regular expression), lecz ta tematyka wykracza poza ramy niniejszej publikacji. Po więcej informacji na ten temat odsyłam do dokumentacji środowiska .NET Framework.
Rozdział 10 Biblioteka Windows Forms Z doświadczenia wiem, że początkującym programistom najwięcej frajdy sprawia wizualne projektowanie aplikacji. Umieszczanie w programie wszelkiego rodzaju przycisków, kontrolek, łączenie całości w działającą aplikację przynosi najwięcej satysfakcji. W poprzednich rozdziałach większość przykładów prezentowana była na aplikacjach konsolowych. Było to celowe działanie, gdyż użycie biblioteki wizualnej wymaga zastosowania większej ilości kodu źródłowego, konstrukcji, których wcześniej nie znałeś. Teraz, gdy nieobce jest Ci programowanie obiektowe, wiesz, czym są klasy i tablice, możemy przejść do programowania wizualnego. Odstawimy na bok aplikacje konsolowe, z których mało kto dzisiaj korzysta. Zajmiemy się budową programów na podstawie komponentów, które są w dzisiejszych czasach podstawą do budowania aplikacji okienkowych, działających w systemach Windows. W gruncie rzeczy do tej pory powiedzieliśmy już sobie całkiem sporo o aplikacjach wizualnych. Potrafisz już tworzyć nowe projekty WinForms przy pomocy środowiska Visual C# Express Edition. W rozdziale 5. omówiłem podstawowy kod aplikacji wizualnej, a w rozdziale 6. wyjaśniłem, czym są zdarzenia. Wiesz już, czym są właściwości, w jaki sposób je
modyfikować oraz jak obsługiwać zdarzenia. Brakuje Ci jedynie wiedzy praktycznej, umożliwiającej wykorzystanie podstawowych komponentów biblioteki Windows Forms.
Podzespół System.Windows.Forms Przestrzeń nazw System.Windows.Forms znajduje się w podzespole o tej samej nazwie. Jest to podstawowa biblioteka służąca do projektowania wizualnego. Kompilując dany projekt z użyciem kompilatora csc.exe, w linii komend musisz zaznaczyć, iż ma on być kompilowany wraz z podzespołem System.Windows.Forms.dll. Jest to bardzo niewygodne, dlatego zalecam korzystanie z wizualnych środowisk typu Visual C# Express Edition. Podczas tworzenia nowego projektu WinForms odpowiednie podzespoły umożliwiające jego prawidłową kompilację są włączane automatycznie. Możesz to sprawdzić, rozwijając gałąź References w oknie Solution Explorer (rysunek 10.1).
Rysunek 10.1. Okno Solution Explorer Gdy klikniemy daną gałąź oznaczającą podzespół i wybierzemy pozycję Properties z menu podręcznego, wyświetli się lista właściwości podzespołu. Wyświetlone zostaną m.in. informacje o wersji oraz nazwie pliku danego podzespołu. O podzespołach .NET opowiem w rozdziale 11.
Okno Object Browser Żaden programista nie jest w stanie zapamiętać setek typów, jakie oferuje biblioteka klas platformy .NET Framework. Podejrzewam, że sami twórcy czasami muszą sięgnąć do dokumentacji, aby przypomnieć sobie nazwę danej klasy. Dlatego środowiska takie jak Visual C# Express zapewniają wsparcie, aby programowanie było jeszcze prostsze i szybsze. Skoro jesteśmy przy temacie podzespołów, to warto wspomnieć o oknie Object Browser. Umożliwia
nam ono przeglądanie zawartości danych podzespołów, przestrzeni nazw i typów. Całość jest ładnie przedstawiona w sposób hierarchiczny (rysunek 10.2).
Rysunek 10.2. Okno Object Browser Okno Object Browser możemy wyświetlić, jeśli klikniemy dwukrotnie nazwę podzespołu w gałęzi References lub przejdziemy do menu View/Other Windows/Object Browser.
Przestrzeń System.Windows.Forms Najważniejszą przestrzenią nazw w podzespole System.Windows.Forms.dll jest ta o takiej samej nazwie (czyli System.Windows.Forms), włączana do każdego projektu typu WinForms. Zawiera ona podstawowe klasy obsługi aplikacji, ale również wszystkie klasy komponentów. Musisz zdać sobie sprawę, że każdy komponent jest jednocześnie klasą, taką samą, jaką tworzyliśmy w rozdziale 5. Taka klasa zawiera elementy, o których wspominałem już niejednokrotnie w tej książce, czyli właściwości, metody, zdarzenia, indeksery, typy wyliczeniowe czy struktury. Ogólnie, klasy biblioteki WinForms można zakwalifikować do kilku kategorii, które omówiłem w tabeli 10.1. Tabela 10.1. Kategorie klasy przestrzeni System.Windows.Forms Grupa Opis
Komponenty
Kontrolki
Elementy pozycjonowania
Menu i paski narzędziowe
Okna dialogowe
O komponentach potocznie mówi się jak o kontrolkach wizualnych. W rzeczywistości można powiedzieć, że są to niewidoczne elementy programu, „klocki” działające w tle, które mogą mieć duży wpływ na działanie aplikacji, lecz nie są częścią jej interfejsu. Kontrolki to inaczej komponenty wizualne. Dzięki nim możesz tworzyć interfejs swojego programu. Do tej kategorii można zaliczyć przyciski, listy rozwijane, pola edycyjne itp. Biblioteka WinForms posiada kilka komponentów, które umożliwiają zarządzanie elementami interfejsu. Jest to np. komponent umożliwiający tworzenie zakładek czy komponent Panel, który służy tylko do tego, aby grupować „w sobie” inne kontrolki. W programach wizualnych często zachodzi potrzeba utworzenia menu czy pasków narzędziowych. W bibliotece WinForms mamy do dyspozycji kilka komponentów, dzięki którym jesteśmy w stanie zrealizować to zadanie. Są to ukryte komponenty, które odpowiadają za wyświetlanie okien dialogowych obecnych w wielu aplikacjach biurowych (np. Otwórz, Zapisz jako…, Drukuj itp.).
Podstawowe klasy Oczywiście cała biblioteka WinForms zbudowana jest na mechanizmie dziedziczenia oraz polimorfizmu (jak mogłoby być inaczej?), toteż istnieje pewien zestaw klas bazowych dla wszystkich komponentów, o których powinieneś wiedzieć. Nie jest to wiedza niezbędna w pracy nad komponentami, ale być może ciekawostka dla osób, które chcą wiedzieć, jak to wszystko jest zbudowane.
System.ComponentModel.Component Klasa — o dziwo — nie znajduje się w przestrzeni System.Windows.Forms, lecz w System.ComponentModel. Jest to podstawowa klasa dla wszystkich komponentów z biblioteki WinForms, implementuje interfejs IComponent. Właściwie nigdy nie będziesz miał potrzeby używania tej klasy samodzielnie, służy ona jedynie jako klasa bazowa. Zawiera kilka elementów, m.in. metodę Events() zwracającą listę zdarzeń przypisaną do danego komponentu.
System.Windows.Forms.Control
W przestrzeni nazw System.Windows.Forms znajduje się klasa Control, która jest bazowa dla każdej wizualnej kontrolki biblioteki WinForms. Co oznacza, że kontrolka jest wizualna? Przede wszystkim kontrolki tego typu tworzą interfejs aplikacji, są widoczne podczas jej działania. Dodatkowo mają one możliwość reagowania na zdarzenia użytkownika , takie jak naciskanie klawiszy klawiatury czy myszy.
Właściwości klasy Klasa Control zawiera cale mnóstwo właściwości publicznych oraz chronionych, które dziedziczone są w klasach potomnych. Właściwości te służą do określania podstawowych cech komponentów, takich jak położenie, rozmiar, kolor itp. W tabeli 10.2 zawarłem podstawową listę właściwości, które z pewnością będą przez Ciebie wykorzystywane najczęściej. Tabela 10.2. Podstawowa lista właściwości klasy Control Właściwość Opis Właściwość typu bool określa, czy dany komponent może być rodzicem AllowDrop dla innych, tzn. czy za pomocą techniki „przeciągnij i upuść” (ang. drag & drop) można w nim umieszczać inne kontrolki. Określa, w jaki sposób komponent ma zmieniać swoje rozmiary, jeżeli Anchor kontrolka znajdująca się w nim zmienia swój rozmiar. Właściwość typu bool określa, czy komponent ma mieć rozmiar zgodny z AutoSize wymiarami treści, która się w nim znajduje. BackColor Umożliwia określenie koloru tła dla komponentu. BackgroundImage Określa obrazek, który będzie używany jako tło komponentu. Określa kursor myszy, jaki ma być wyświetlany, jeżeli jej wskaźnik Cursor zostanie naprowadzony nad daną kontrolkę. Właściwość typu bool określa, czy komponent będzie wyłączony Enabled (nieaktywna kontrolka jest specjalnie oznaczana, nie reaguje na zdarzenia). Umożliwia ustawienie czcionki, jaka będzie wyświetlana, jeżeli kontrolka Font umożliwia wyświetlanie tekstu. ForeColor Określa kolor tekstu dla komponentu. Określa, czy w komponencie znajdują się inne kontrolki (tzw. kontrolki HasChildren potomne). Height Umożliwia odczytanie lub przypisanie szerokości kontrolki. Umożliwia określenie (w pikselach) odległości lewej krawędzi od obszaru Left formularza lub innego komponentu. Location Właściwość typu Point umożliwia określenie położenia dla komponentu. Name Bardzo ważna właściwość. Określa nazwę komponentu. Umożliwia określenie lub odczytanie odstępów (minimalnej odległości) Padding wewnątrz danej kontrolki.
Parent Right Size TabIndex Tag Top Visible Width
Umożliwia odczytanie lub przypisanie kontrolki macierzystej dla danego komponentu. Określa odstęp (w pikselach) od prawej krawędzi obszaru formularza lub innego komponentu. Właściwość typu Point umożliwia określenie rozmiaru kontrolki. Umożliwia przypisanie lub odczytanie kolejności przechodzenia do danego komponentu przy pomocy klawisza Tab. Właściwość może być wykorzystana do dowolnych celów. Nie robi nic konkretnego, może przechowywać dowolne wartości. Określa odległość (w pikselach) od górnej krawędzi formularza lub komponentu. Właściwość typu bool umożliwia określenie, czy komponent będzie widoczny, czy też nie. Umożliwia odczytanie lub nadanie szerokości dla komponentu.
Oczywiście chciałbym zaznaczyć, że to tylko niektóre z właściwości. Starałem się zamieścić w tabeli komponenty, które są w większości wspólne dla wszystkich kontrolek wizualnych. Nie wszystkie właściwości prezentowane w tabeli 10.2 muszą być widoczne na liście okna Properties. Niektóre mogą być dostępne jedynie z poziomu kodu programu.
System.Windows.Forms.Application Klasa Application ma duże znaczenie dla naszego programu. Zawiera bowiem bardzo ważną metodę odpowiedzialną za wyświetlenie głównego formularza projektu. Wykonajmy małe ćwiczenie pozwalające napisać najprostszy program, który będzie wykorzystywał bibliotekę WinForms. Nasza aplikacja będzie wyświetlała jedynie puste okno formularza. Formularzem nazywamy okno systemu Windows. 1. Z menu File wybierz pozycję New Project. 2. Zaznacz pozycję Empty project i naciśnij przycisk OK. 3. W oknie Solution Explorer zaznacz, a następnie kliknij prawym przyciskiem pozycję References. 4. Kliknij Add Reference. 5. W oknie, w zakładce .NET odszukaj pozycję System, a następnie kliknij przycisk OK. 6. W ten sam sposób dodaj pozycję System.Windows.Forms. 7. Z menu Project wybierz pozycję Add New Item. 8. Zaznacz pozycję Empty file i kliknij OK.
Utworzyliśmy w ten sposób nowy projekt, ręcznie dodaliśmy odwołania do odpowiednich podzespołów (rysunek 10.3). Mamy przed sobą pustą zawartość pliku źródłowego.
Rysunek 10.3. Okno Solution Explorer z gałęzią prezentującą odwołania Kod odpowiedzialny za utworzenie nowego formularza prezentuje listing 10.1. Listing 10.1. Kod odpowiedzialny za wyświetlenie nowego formularza using System; using System.Windows.Forms; public class WinForms : Form { static void Main() { Application.Run(new WinForms()); } }
Jak widzisz, kod jest prosty i krótki. Wywołujemy metodę Run(), która jako parametru wymaga instancji klasy Form. Klasa Form jest podstawową klasą obsługi formularzy, dlatego WinForms musi po niej dziedziczyć. Jeżeli kod z listingu 10.1 oprócz okna formularza wyświetla również okno konsoli, musisz sprecyzować typ projektu. Z menu Project wybierz Properties. W zakładce Application znajduje się lista rozwijana Output type. Wybierz z niej pozycję Windows Application. Klasa Application ma wiele metod oraz właściwości odpowiadających za obsługę tzw. komunikatów oraz wątków. Ze względu na ograniczenia objętości tej książki, te tematy nie zostaną zaprezentowane. Klasa zawiera jednak dwie metody, które mogą Ci się przydać w dalszej pracy z językiem C#. Mam tu na myśli metodę Exit(), umożliwiającą zakończenie pracy oraz Restart(), która ponownie uruchamia aplikację. Rozbudujmy nasz program z listingu 10.1, dodając do niego przyciski umożliwiające zamknięcie oraz ponowne uruchomienie aplikacji. Aby zrealizować to zadanie, musimy umieścić w programie kod tworzący dwa komponenty
Button oraz przypisać im odpowiednie zdarzenia. Oczywiście moglibyśmy to prosto zrobić w
trybie projektowania w środowisku Visual C#. Aby jednak utrudnić sobie to zadanie, wpiszmy odpowiedni kod ręcznie. Utworzenie nowego komponentu na formularzu polega zwyczajnie na utworzeniu nowej instancji (np. komponentu Button) oraz ustawieniu odpowiednich właściwości: RestartBtn = new Button(); RestartBtn.Parent = this; RestartBtn.Top = 10; RestartBtn.Left = 10; RestartBtn.Text = "Restartuj"; RestartBtn.Click += new System.EventHandler(OnRestareClick);
Kluczowe w tym kodzie jest przypisanie odpowiedniej wartości dla właściwości Parent. Musimy określić komponent macierzysty dla naszej kontrolki (będzie to formularz), co prezentuje listing 10.2. Listing 10.2. Dynamiczne tworzenie komponentów na formularzu using System; using System.Windows.Forms; public class MyForm : Form { private Button RestartBtn; private Button ExitBtn; private void OnRestareClick(object sender, EventArgs e) { Application.Restart(); } private void OnExitClick(object sender, EventArgs e) { Application.Exit(); } public MyForm() { this.Text = Application.StartupPath; RestartBtn = new Button(); RestartBtn.Parent = this; RestartBtn.Top = 10; RestartBtn.Left = 10; RestartBtn.Text = "Restartuj"; RestartBtn.Click += new System.EventHandler(OnRestareClick);
ExitBtn = new Button(); ExitBtn.Parent = this; ExitBtn.Top = 10; ExitBtn.Left = 120; ExitBtn.Text = "Zamknij"; ExitBtn.Click += new System.EventHandler(OnExitClick); } } public class WinForms { static void Main() { Application.Run(new MyForm()); } }
Rysunek 10.4 prezentuje działanie takiego programu. Zwróć uwagę, że pasek tytułowy formularza zawiera ścieżkę do uruchomionego programu. Odpowiada za to linia: this.Text = Application.StartupPath;
Właściwość StartupPath zwraca pełną ścieżkę do programu. W klasie Application znajduje się podobna właściwość — ExecutablePath — która również wyświetla ścieżkę do programu, włączając w to jego nazwę.
Rysunek 10.4. Działanie aplikacji WinForms
Przykład działania Zostawmy na chwilę suchą teorię, a zajmijmy się tworzeniem graficznego interfejsu użytkownika (GUI — ang. Graphical User Interface). Pamiętasz, jak w rozdziale 6. pisaliśmy grę „Kółko i krzyżyk”? Wówczas cała gra odbywała się w trybie konsolowym. Ponieważ całe engine gry jest już gotowe, wystarczy zaprojektować interfejs, który obsługiwałby jej klasę. Do roboty!
Przygotowanie klasy Utwórz nowy projekt aplikacji Windows Forms. Zapisz go na dysku, po czym skopiuj moduł, w którym znajdowała się klasa GomokuEngine, do katalogu z projektem. Należy dodać ten moduł do projektu: 1. Prawym przyciskiem kliknij w obrębie okna Solution Explorer. 2. Wybierz pozycję Add/Existing Item. 3. Wskaż plik, w którym znajduje się kod gry, i naciśnij OK.
Projektowanie interfejsu Zacznijmy od tytułu okna głównego. Domyślny napis Form1 nie odpowiada nazwie naszej gry. W oknie Properties możesz zmienić wartość właściwości Text, wpisując własną nazwę okna — np. Gomoku Win. Jeżeli okno Properties nie wyświetla prawidłowo właściwości formularza, być może nie jest on zaznaczony. Kliknij więc okno formularza, aby wyświetlić listę jego właściwości. Kolejna rzecz, jakiej potrzebujemy, to plansza do gry. W celu jej utworzenia proponuję umieścić na formularzu 9 komponentów typu Button. Na razie umieść jeden komponent, gdyż musimy ustalić właściwości jego wyglądu. Zacznijmy od rozmiaru. W oknie Properties odnajdź właściwość Size i rozwiń jej gałąź. Wartość Width zmień na 50, a Height na 30. Teraz przyszedł czas na zmianę czcionki. Rozwiń gałąź Font i zmień następujące pozycje:
Name (nazwa czcionki) — Arial Black; Size (rozmiar czcionki) — 14.
Teraz — używając kombinacji klawiszy Ctrl+C (kopiuj) oraz Ctrl+V (wklej) — możesz
dziewięć razy wkleić skopiowany wcześniej komponent Button. Oszczędzi Ci to czasu na modyfikację właściwości kolejnych przycisków. Potrzebujemy jeszcze dwóch kontrolek tekstowych, w których użytkownicy będą mogli wpisać swe imiona. Moja wersja interfejsu użytkownika zaprezentowana została na rysunku 10.5.
Rysunek 10.5. Interfejs gry Ramkę otaczającą podstawowe elementy uzyskałem, umieszczając na formularzu komponent GroupBox. Dodatkowo ustawiłem właściwość Text tego komponentu na wartość pustą. Etykiety, w których znajdują się napisy Gracz #1 oraz Gracz #2, to komponenty Label — ich jedynym zadaniem jest przechowywanie krótkich tekstów.
Rozwiązania programistyczne W każdym projekcie tworzenie interfejsu aplikacji to dopiero początek. Naszym zadaniem jest odpowiednia obsługa klasy Gomoku, gdyż cały „silnik” gry mamy już gotowy. Zacznijmy od rzeczy najprostszej, czyli oprogramowania zdarzenia Click komponentu btnStart (start gry): private void btnStart_Click(object sender, EventArgs e) { if (textPlayer1.Text.Length > 0 && textPlayer2.Text.Length > 0) { GomokuObj.Player1 = textPlayer1.Text; GomokuObj.Player2 = textPlayer2.Text; GomokuObj.Start(); Ready = true; } }
W instrukcji warunkowej sprawdzamy, czy użytkownik wpisał imiona graczy. Następnie imiona przypisujemy do odpowiednich właściwości klasy, po czym wywołujemy metodę Start(). Na samym końcu znajduje się instrukcja zmieniająca wartość pola Ready na true. To pole jest używane na wewnętrzny potrzeby naszego programu (wykorzystywane jest w dalszej części kodu). Zakładam, że nie masz problemu z generowaniem i obsługą zdarzeń. Gdybyś jednak miał — odsyłam do wcześniejszych fragmentów książki. Teraz pomyślmy chwilę nad planszą do gry. Mamy 9 przycisków, których zdarzenia trzeba oprogramować. Ale żeby nie generować 9 procedur zdarzeniowych, wygenerujemy jedną i przypiszemy ją do pozostałych przycisków. Skoro jedna metoda będzie obsługiwać 9 przycisków, to skąd mamy wiedzieć, który przycisk został przez użytkownika wciśnięty. Po lekturze poprzednich rozdziałów powinieneś się domyślić, że wykorzystamy do tego parametr sender. Pamiętaj jednak, że do metody Set() klasy Gomoku musimy przekazać współrzędne pola (przycisku), które wybrał użytkownik. Do tego wykorzystamy właściwość Tag, która obecna jest we wszystkich komponentach. Właściwość Tag typu System.Object może przechowywać dowolne dane na użytek programu. W naszym przypadku będzie przechowywać „współrzędne” danego przycisku. Otwórz plik przechowujący ustawienia formularza (u mnie jest to Form1.Designer.cs) i odnajdź kod, który odpowiedzialny jest za utworzenie przycisków: this.button1.Font = new System.Drawing.Font("Arial Black", 14F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238))); this.button1.Location = new System.Drawing.Point(12, 12); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(50, 30); this.button1.TabIndex = 0; this.button1.TextAlign = System.Drawing.ContentAlignment.TopCenter; this.button1.UseVisualStyleBackColor = true; this.button1.Click += new System.EventHandler(this.button1_Click); this.button1.Tag = new System.Drawing.Point(1, 1);
Pogrubiona linia oznacza przypisanie wartości do właściwości Tag. Wpisałem ją ręcznie. Zrób tak z każdym przyciskiem, zmieniając jednak współrzędne, czyli następnie będzie to: (1, 2), (1, 3), (2, 1), (2, 2) itd. Oto kod metody zdarzeniowej dla komponentów typu Button: private void button1_Click(object sender, EventArgs e) { if (!Ready)
{ MessageBox.Show("Podaj imiona graczy!"); return; } Point p = new Point(); p = (Point)(sender as Button).Tag; (sender as Button).Text = GomokuObj.Active.Type == FieldType.ftCircle ? "O" : "X"; GomokuObj.Set(p.X, p.Y); if (GomokuObj.Winner) { MessageBox.Show(String.Format("Brawo dla {0}", GomokuObj.Active.Name), "Wygrana!"); } }
Pierwsza instrukcja sprawdza, czy pole Ready ma wartość false. Jeżeli tak, wyświetla ostrzeżenie, iż nie podano imion graczy. Następnie tworzymy nową strukturę Point i przypisujemy do niej informacje z właściwości Tag naciśniętego przycisku. Kolejna instrukcja umieszcza na przycisku symbol odpowiadający danemu graczowi, po czym wywołuje metodę Set() z odpowiednimi parametrami. Na samym końcu instrukcja sprawdza, czy nie zakończyć gry, jeżeli któryś z użytkowników wygrał. Omówienia wymaga również metoda zdarzeniowa przycisku btnNewGame: private void btnNewGame_Click(object sender, EventArgs e) { for (int i = 0; i < Controls.Count; i++) { if (Controls[i] is Button && (Controls[i] as Button).Tag != null) { (Controls[i] as Button).Text = ""; } } GomokuObj.NewGame(); }
Jej zadaniem jest wywołanie metody NewGame(), lecz wcześniej czyści wartości Text przycisków planszy. Właściwość Controls zwraca informacje na temat kontrolek umieszczonych — w tym przypadku — na formularzu. W pętli sprawdzamy każdą kontrolkę i jeżeli jest typu Button oraz jej właściwość Tag nie jest pusta, czyścimy zawartość jej właściwości Text. Kod źródłowy gry (głównego modułu aplikacji) zawarty jest na listingu 10.3.
Rysunek 10.6 prezentuje grę w trakcie działania. Listing 10.3. Kod źródłowy gry Gomoku using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace Gomoku { public partial class Form1 : Form { private GomokuEngine GomokuObj; private bool Ready = false; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { GomokuObj = new GomokuEngine(); } private void btnStart_Click(object sender, EventArgs e) { if (textPlayer1.Text.Length > 0 && textPlayer2.Text.Length > 0) { GomokuObj.Player1 = textPlayer1.Text; GomokuObj.Player2 = textPlayer2.Text; GomokuObj.Start(); Ready = true; } } private void btnNewGame_Click(object sender, EventArgs e) { for (int i = 0; i < Controls.Count; i++) { if (Controls[i] is Button && (Controls[i] as
Button).Tag != null) { (Controls[i] as Button).Text = ""; } } GomokuObj.NewGame(); } private void button1_Click(object sender, EventArgs e) { if (!Ready) { MessageBox.Show("Podaj imiona graczy!"); return; } Point p = new Point(); p = (Point)(sender as Button).Tag; (sender as Button).Text = GomokuObj.Active.Type == FieldType.ftCircle ? "O" : "X"; GomokuObj.Set(p.X, p.Y); if (GomokuObj.Winner) { MessageBox.Show(String.Format("Brawo dla {0}", GomokuObj.Active.Name), "Wygrana!"); } } } }
Rysunek 10.6. Gra Gomoku w trakcie działania
Technika przeciągnij i upuść
Technika „przeciągnij i upuść” (ang. Drag & Drop) z powodzeniem stosowana jest w wielu programach komputerowych. Sam z pewnością nieraz z niej skorzystałeś. Przykładowo: przenoszenie plików za pomocą kliknięcia i przesuwania kursora myszy jest już wykorzystaniem techniki przeciągnij i upuść! Dzięki temu obsługa aplikacji jest przyjemniejsza i bardziej intuicyjna. Ponieważ obsługa tej techniki w środowisku .NET Framework nie należy do najprostszych, postanowiłem zaprezentować przykłady jej wykorzystania. W naszej przykładowej aplikacji użytkownik będzie miał możliwość przenoszenia elementów pomiędzy dwoma komponentami ListBox. Kontrolka ListBox służy do przechowywania kolejnych linii tekstu (pozycji). Rysunek 10.7 prezentuje działanie takiej aplikacji.
Rysunek 10.7. Aplikacja prezentująca działanie techniki przeciągnij i upuść Pierwsze, co musimy zrobić, to zmienić wartość właściwości AllowDrop na true w obu kontrolkach ListBox. Właściwość ta określa, czy dany komponent może obsługiwać dane przychodzące przy pomocy techniki drag & drop. Ponieważ zależy nam na tym, aby można było dowolnie przemieszczać dane, procedury zdarzeniowe będą takie same dla obydwu kontrolek. Zacznijmy od oprogramowania zdarzenia MouseDown, które występuje w momencie kliknięcia klawiszem myszy w obrębie kontrolki: private void lb_MouseDown(object sender, MouseEventArgs e) { DragDropEffects effects = (sender as ListBox).DoDragDrop((sender as ListBox).SelectedItem, DragDropEffects.Move);
if (effects != DragDropEffects.None) { (sender as ListBox).Items.RemoveAt((sender as ListBox).SelectedIndex);
} }
Najważniejsza w tym kodzie jest metoda DoDragDrop(), która uruchamia proces przeciągania danych. Pierwszym parametrem są dane, które będą przeciągane, czyli w naszym przypadku — zaznaczony element w ListBox. Drugim parametrem jest tryb przenoszenia. Metoda zwraca obiekt klasy DragDropEffects. Sami musimy zadbać o usuwanie elementu z komponentu źródłowego. Realizuje to metoda RemoveAt() z właściwości Items. W parametrze tej metody musimy podać numer pozycji do usunięcia. Usuwamy zaznaczoną pozycję, więc skorzystamy z właściwości SelectedIndex, która dostarcza nam informacji o zaznaczonej pozycji. Właściwość Items komponentu typu ListBox zwraca listę pozycji (elementów) komponentów w formie kolekcji. Kolejnym krokiem będzie obsłużenie zdarzenia DragEnter, które wykonywane jest w momencie, gdy kursor myszy z przeciąganym elementem wejdzie w obszar kontrolki: private void lb_DragEnter(object sender, DragEventArgs e) { StatusBar.Text = "Przesyłam dane..."; if (e.Data.GetDataPresent(typeof(System.String))) { e.Effect = DragDropEffects.Move; } }
Parametr e tej procedury zdarzeniowej zawiera informacje o przesyłanych danych. Akceptujemy jedynie dane typu System.String, więc musimy upewnić się, czy mamy do czynienia z prawidłowym typem. Operator typeof zwraca informacje o typie danych w postaci obiektu System.Type. Dodatkowo umieszczamy na pasku statusu tekstową informację o wykonywanych czynnościach. Skorzystałem z komponentu StatusStrip, do którego dodałem etykietę (rysunek 10.8) i nazwałem ją StatusBar.
Rysunek 10.8. Dodawanie etykiety w komponencie StatusStrip Ostatnie zdarzenie, które musimy obsłużyć, to DragDrop, wykonywane w momencie zwolnienia klawisza myszy. Procedura zdarzeniowa musi zawierać kod informujący aplikację o tym, co zrobić z dostarczonymi danymi: private void lb_DragDrop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(typeof(System.String))) { Object item = (object)e.Data.GetData(typeof(System.String)); (sender as ListBox).Items.Add(item); StatusBar.Text = ""; } }
Pierwsza instrukcja służy do sprawdzenia poprawności danych, podobnie jak w procedurze zdarzeniowej lb_DragEnter(). Następnie odczytujemy dane i przypisujemy je do obiektu item, którego zawartość wstawiania jest do listy w komponencie typu ListBox. Listing 10.4 zawiera kod źródłowy aplikacji. Listing 10.4. Przykładowy program korzystający z techniki Drag & Drop using System; using System.Collections.Generic;
using using using using using
System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms;
namespace DockApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnAdd_Click(object sender, EventArgs e) { int selectedItem = lbSource.SelectedIndex; object item = lbSource.SelectedItem; if (selectedItem > 0) { lbSource.Items.RemoveAt(selectedItem); lbDest.Items.Add(item); } } private void btnRemove_Click(object sender, EventArgs e) { int selectedItem = lbDest.SelectedIndex; object item = lbDest.SelectedItem; if (selectedItem > 0) { lbDest.Items.RemoveAt(selectedItem); lbSource.Items.Add(item); } } private void lb_DragEnter(object sender, DragEventArgs e) { StatusBar.Text = "Przesyłam dane..."; if (e.Data.GetDataPresent(typeof(System.String))) { e.Effect = DragDropEffects.Move; } }
private void lb_DragLeave(object sender, EventArgs e) { StatusBar.Text = ""; } private void lb_MouseDown(object sender, MouseEventArgs e) { DragDropEffects effects = (sender as ListBox).DoDragDrop((sender as ListBox).SelectedItem, DragDropEffects.Move);
if (effects != DragDropEffects.None) { (sender as ListBox).Items.RemoveAt((sender as ListBox).SelectedIndex); } } private void lb_DragDrop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(typeof(System.String))) { Object item = (object)e.Data.GetData(typeof(System.String)); (sender as ListBox).Items.Add(item); StatusBar.Text = ""; } } } }
Tworzenie menu W wielu aplikacjach, nie tylko tych przeznaczonych dla systemu Windows, obecne jest menu główne. Nie muszę Ci chyba tłumaczyć, czym jest menu, ale warto wspomnieć o tym, jak się je tworzy w środowisku Visual C# Express Edition. Wbrew pozorom, w środowisku wizualnym projektowanie menu jest bardzo proste i przyjemne. Za wyświetlanie i obsługę menu odpowiada komponent MenuStrip. Umieść ten komponent na formularzu. Zostanie on umieszczony na pasku podręcznym (rysunek 10.9), gdyż należy do komponentów niewizualnych.
Rysunek 10.9. Komponent MenuStrip umieszczony na pasku podręcznym Po zaznaczeniu tego komponentu u góry formularza wyświetlona zostanie pozycja z napisem Type here, służąca do wpisania etykiety pierwszego menu (rysunek 10.10).
Rysunek 10.10. Projektowanie menu Po kliknięciu tej pozycji kursor zostanie zmieniony, co da nam możliwość wpisania etykiety. Po naciśnięciu klawisza Enter środowisko da nam możliwość dodania kolejnego elementu menu. Rysunek 10.11 prezentuje moją propozycję menu, które zawiera podstawowe elementy każdej aplikacji biurowej (menu Plik, Edycja, Pomoc).
Rysunek 10.11. Menu w trakcie edycji Separator w menu można utworzyć, wpisując znak -.
Właściwości menu Menu możesz również edytować, jeśli klikniesz prawym przyciskiem ikonę komponentu i wybierzesz pozycję Edit Items. Okno Items Collection Editor daje możliwość określenia szczegółowych właściwości dla menu, z których najważniejsze opisałem w tabeli 10.3. Tabela 10.3. Właściwości pozycji menu Właściwość Opis BackColor Umożliwia określenie koloru tła pozycji menu. BackgroundImage Umożliwia określenie obrazka tła. BackgroundImageLayout Określa pozycję i zachowanie obrazka tła. Zmiana tej właściwości na true spowoduje pojawienie się przy Checked pozycji zaznaczenia (charakterystycznego „ptaszka”). Określa sposób wyświetlania pozycji (tekst i obrazek, tekst lub DisplayStyle obrazek). Image Umożliwia określenie ikony, która będzie wyświetlana w pozycji. ImageAlign Określa położenie obrazka. ImageTransparent Umożliwia określenie koloru tła obrazka (jeżeli jest przezroczysty). RightToLeft Określa, czy pozycje w menu będą wyrównane do prawej. TextAlign Umożliwia szczegółowe określenie pozycji tekstu w menu. TextDirection Określa kierunek wyświetlania tekstu (domyślnie poziomy). TextImageRelation Umożliwia określenie położenia tekstu względem obrazka. AutoSize Dopasowanie rozmiaru elementu do jego zawartości.
AutoToolTip CheckOnClick ToolTipText DropDownItems ShortcutKeys ShowShortcutKeys
Automatyczne wyświetlanie dymków podpowiedzi po naprowadzeniu kursora nad pozycję. Kliknięta pozycja zostanie zaznaczona, jeżeli wartość tej właściwości to true. Zawartość dymka podpowiedzi. Umożliwia edycję elementów podmenu. Umożliwia określenie skrótu klawiaturowego dla elementu. Określa, czy obok pozycji menu wyświetlać skrót klawiaturowy (jeżeli został określony).
Ikony dla menu Aby nasze menu było bardziej intuicyjne i ładne, dodamy do niego ikony, które będą zdobić jego pozycje. Obrazek może być zapisany w formacie *.bmp, *.gif, *.jpeg lub *.png. Nowy obrazek możemy dodać bardzo prosto, korzystając z właściwości Image. Po jej zaznaczeniu wyświetlona zostanie mała ikona — gdy ją naciśniemy otwarte zostanie okno Select Resource (rysunek 10.12).
Rysunek 10.12. Okno Select Resource Zaznacz pozycję Project resource file, a następnie kliknij przycisk Import. Zostaniesz poproszony o wskazanie pliku z obrazem, który zostanie dodany do listy. Po naciśnięciu OK dany obrazek zostanie przypisany do pozycji menu. Prostszym rozwiązaniem będzie skorzystanie z opcji podręcznego menu. Po kliknięciu danej
pozycji menu w trakcie projektowania wybierz pozycję Set Image, która również wyświetli okno Select Resource (rysunek 10.13).
Rysunek 10.13. Opcje podręcznego menu w trakcie projektowania Obrazki przypisane do menu będą zapisane w tzw. zasobach programu. Zasoby (takie jak np. obrazy, teksty, dźwięki) są dołączane do pliku wykonywalnego w trakcie kompilacji. Nie ma więc potrzeby dołączania do aplikacji dodatkowych plików graficznych czy dźwiękowych. Informacje o zasobach są przechowywane w formacie XML, w pliku Resources.resx, który znajduje się w podkatalogu Properties Twojego projektu. Jeżeli zajdzie taka potrzeba, możesz odczytać informacje związane z obrazkami przypisanymi do menu. Oczywiście przy pomocy właściwości, które mogą zostać wyświetlone w oknie Properties. W trakcie projektowania wybierz dla przykładu menu Plik/Nowy (tak jak to pokazałem na rysunku 10.13), kliknij go prawym przyciskiem myszy i wybierz pozycję Properties. W oknie Properties, znajduje się właściwość Image (to już wiesz), której pozycje mogą zostać rozwinięte. Uzyskasz wówczas informacje o ikonie przypisanej do danego menu (rysunek 10.14).
Rysunek 10.14. Informacje o ikonie przypisanej do pozycji menu Pozycje w oknie Properties oznaczone kolorem szarym przeznaczone są jedynie do odczytu, nie można ich modyfikować.
Skróty klawiaturowe Aby nadać naszemu menu bardziej profesjonalny charakter, należy niektórym pozycjom przypisać skróty klawiaturowe. Umożliwi to szybkie wybranie danej pozycji przy pomocy klawiatury, co znacznie ułatwi pracę z aplikacją. Do ustawiania skrótu klawiaturowego służy właściwość ShortcutKeys. Dzięki niej możemy wybrać dowolną kombinację klawiaturową, która spowoduje wybranie danej pozycji (rysunek 10.15).
Rysunek 10.15. Ustawianie skrótu klawiaturowego Właściwość ShowShortcutKeys określa, czy skrót będzie wyświetlony obok etykiety pozycji w menu. Zalecane jest pozostawienie wartości domyślnej (czyli true). Inną zalecaną techniką jest umieszczenie w etykiecie znaku &. Przykładowo, zaznacz menu Plik i przejdź do właściwości Text. Zmień jej wartość na &Plik. Spowoduje to dodanie pod literą P charakterystycznego podkreślenia. Dzięki temu użytkownik będzie mógł przejść do tej pozycji menu, używając skrótu Lewy Alt+P.
Menu podręczne
Opcje menu podręcznego wyświetlane są w momencie, gdy użytkownik kliknie prawym przyciskiem myszy w obrębie danego elementu. Do tworzenia menu podręcznego służy komponent ContextMenuStrip. Pozycje tego menu tworzone są na takiej samej zasadzie jak we wspomnianym komponencie MenuStrip. Jedyne, o czym warto wspomnieć, to przypisywanie menu podręcznego do danego elementu. Większość kontrolek wizualnych posiada rozwijaną właściwość ContextMenuStrip, która służy do kojarzenia danego elementu z menu podręcznym. Jeżeli więc chcesz, aby menu podręczne było wyświetlane po kliknięciu prawym przyciskiem myszy w obrębie formularza, zaznacz formularz i odszukaj właściwość ContextMenuStrip. Z listy wybierz menu, które ma obsługiwać formularz.
Paski narzędziowe Paski narzędziowe (ang. tool bar) to również często spotykany element interfejsu aplikacji. Zawiera przyciski z najczęstszymi opcjami programu ozdobione ikoną. Podczas projektowania aplikacji w menu powinieneś umieszczać wszystkie opcje, jakie oferuje program, a na pasku jedynie te najczęściej używane. W bibliotece WinForms do tworzenia pasków narzędziowych możemy wykorzystać komponent ToolStrip. Umieszczony na formularzu, automatycznie dopasowuje się do jego górnej krawędzi. Zwróć uwagę na małą ikonę wyświetlaną na pasku w trybie projektowania (rysunek 10.16).
Rysunek 10.16. Pasek narzędziowy w trybie projektowania Rozwinięcie jego pozycji wyświetla spis komponentów, które możemy umieścić na pasku. Wybór jest na tyle spory, że powinien zaspokoić Twoje wymagania. Najczęściej bowiem na pasku umieszczamy przyciski (Button) oraz separatory (Separator).
Przypisywanie obrazków do przycisków wygląda tak samo jak w przypadku menu. Po kliknięciu przycisku prawym klawiszem myszy należy wybrać pozycję Set Image. Projektowanie pasków narzędziowych i menu jest podobne, a to za sprawą właściwości, które są bardzo zbliżone w obu przypadkach. Wierzę, że poradzisz sobie z zaprojektowaniem menu pasującego do aplikacji, więc postanowiłem pominąć ponowne opisywanie właściwości komponentu ToolStrip.
Pasek statusu Pasek statusu również często gości we wszelkiego rodzaju aplikacjach okienkowych. Jest to pasek, który automatycznie dopasowuje się do dolnej krawędzi okna i służy do wyświetlania podstawowych informacji w trakcie działania programu. W Windows Forms za wyświetlanie paska statusu odpowiada komponent StatusStrip. Po jego wyświetleniu, podobnie jak w przypadku pasków narzędziowych, możemy umieścić na nim wiele elementów, w tym etykietę czy pasek postępu (rysunek 10.17).
Rysunek 10.17. Pasek statusu w trakcie projektowania Oczywiście najczęstszym zastosowaniem paska jest wyświetlanie informacji tekstowych, stąd pewnie pozycja StatusLabel będzie tą, którą będziesz wybierać najczęściej.
Zakładki Często gdy mamy rozbudowany interfejs, zachodzi potrzeba podzielenia go na kilka części. Świetnie nadaje się do tego mechanizm zakładek, które możemy tworzyć dzięki kontrolce TabControl. TabControl (rysunek 10.18) jest komponentem-rodzicem. Oznacza to, że w jego wnętrzu
możemy umieszczać inne komponenty.
Rysunek 10.18. Komponent TabControl TabControl domyślnie posiada dwie zakładki, które możemy dodawać lub usuwać.
Wystarczy je kliknąć i wybrać z menu podręcznego pozycję Add lub Remove. Jeżeli chcesz edytować właściwości danej zakładki, musisz wybrać właściwość TabPages z okna właściwości Properties. Właściwość TabPages oferuje wiele opcji, dzięki którym będziesz mógł ustawić cechy każdej zakładki z osobna. Kilka interesujących właściwości komponentu TabControl prezentuje tabela 10.4. Tabela 10.4. Najważniejsze właściwości komponentu TabControl Właściwość Opis Określa położenie zakładek. Domyślnie są one położone u góry (Top), ale Alingment możliwe są również wartości: Bottom (dół), Left (lewa strona), Right (prawa strona). Określa sposób prezentacji zakładek. Można wybrać wartość Button (przyciski) Appearance lub Flat Buttons (płaskie przyciski). Jeżeli właściwość ma wartość true, zakładka zostanie podświetlona, gdy HotTrack naprowadzimy na nią kursor. Określa, czy tytuł zakładki może mieścić się w więcej niż jednej linii (domyślnie Multiline false — nie może). SizeMode Tryb rozmiaru zakładek. Domyślnie szerokość zakładki dopasowywana jest do
treści. Można ustawić wartości: FillToRight (wyrównanie do prawej) lub Fixed (każda zakładka będzie miała taką samą szerokość). Kontrolka TabControl jest najczęściej dopasowywana do szerokości formularza albo komponentu-rodzica. Jeżeli chcesz, aby rozmiar komponentu był dopasowany do obiektu macierzystego, wystarczy zmienić właściwość Dock na Fill.
Kontrolki tekstowe Osobiście dzielę kontrolki tekstowe na jednowierszowe i wielowierszowe. Wielowierszową kontrolką edycyjną jest komponent RichTextBox. Jednowierszową kontrolką edycyjną jest np. TextBox. Do często używanych kontrolek można zaliczyć również:
ComboBox, ListBox.
Ta pierwsza prezentuje kolejne linie tekstu w formie listy rozwijanej. Komponent ListBox działa bardzo podobnie, tyle że kolejne linie prezentowane są w formie listy nierozwijanej. Na nowej zakładce komponentu TabControl umieściłem listę rozwijaną (ComboBox) oraz komponent WebBrowser (rysunek 10.19). Ten drugi służy do wyświetlania stron internetowych.
Rysunek 10.19. Komponent ComboBox umieszczony na zakładce Komponent ComboBox będzie umożliwiał wpisanie adresu strony, która ma zostać wyświetlona w komponencie typu WebBrowser. Wpisane adresy stron WWW będą zapamiętywane i umieszczane na liście rozwijanej. Oprogramujmy więc zdarzenie KeyDown komponentu ComboBox. Musimy wychwycić moment naciśnięcia klawisza Enter. Jeżeli to nastąpi, ładujemy stronę o określonym URL, a sam adres
dodajemy do listy rozwijanej (rysunek 10.20).
Rysunek 10.20. Lista adresów WWW w komponencie ComboBox Procedura zdarzeniowa zdarzenia KeyDown wygląda następująco: private void comboBox1_KeyDown(object sender, KeyEventArgs e) { if (e.KeyValue == (int)Keys.Enter) {
IE.Navigate(comboBox1.Text); comboBox1.Items.Add(comboBox1.Text); } }
Jeżeli warunek zostanie spełniony, wywołana będzie metoda Navigate(), która nakazuje przejść do określonego URL wpisanego w komponencie ComboBox. Kolejna instrukcja dodaje wpisany adres do listy. Musimy się w niej odwołać do właściwości Items, która reprezentuje kolekcję. Za dodanie nowej pozycji odpowiada metoda Add(). Kilka interesujących właściwości komponentu ComboBox zostało zaprezentowanych w tabeli 10.5. Tabela 10.5. Właściwości komponentu ComboBox Właściwość Opis Reprezentuje styl listy rozwijanej. Wartość Simple oznacza usunięcie DropDownStyle ikony służącej do rozwijania listy. Wartość DropDownList uniemożliwia nadanie wartości właściwości Text. DropDownHeight Reprezentuje wysokość listy (w pikselach). DropDownWidth Reprezentuje szerokość listy (w pikselach). Oznacza maksymalną liczbę pozycji, jaka może znaleźć się na liście MaxDropDownItems rozwijanej. Sorted Wartość true oznacza, że pozycje na liście będą sortowane.
Właściwości oraz użycie komponentu ListBox są bardzo podobne, więc pozostawię to zagadnienie bez opisu.
Komponent RichTextBox Można powiedzieć, że komponent RichTextBox jest miniedytorem tekstu. Zawiera metody służące do kopiowania, wycinania czy wklejania tekstu, jak również takie, które umożliwią zmianę koloru zaznaczonego tekstu czy ilości wcięć. Komponent RichTextBox idealnie nadaje się do reprezentowania dużych porcji tekstu — np. zawartości plików tekstowych. Skoro pokazałem już, w jaki sposób tworzyć menu, paski narzędziowe czy zakładki, może warto by było przeobrazić nasz program w coś więcej niż tylko pusty interfejs. Oprogramujmy zdarzenia Click poszczególnych pozycji menu. Zacznijmy od pozycji Wytnij, Kopiuj, Wklej oraz Zaznacz wszystko. Wygeneruj zdarzenia Click dla tych pozycji.
Kod procedur zdarzeniowych powinien wyglądać tak: private void mmCut_Click(object sender, EventArgs e) { MyRichBox.Cut(); } private void mmCopy_Click(object sender, EventArgs e) { MyRichBox.Copy(); } private void mmPaste_Click(object sender, EventArgs e) { MyRichBox.Paste(); } private void mmSelectAll_Click(object sender, EventArgs e) { MyRichBox.SelectAll(); }
Jak widzisz, proces kopiowania czy wycinania tekstu jest bardzo prosty. Wystarczy wywołać metodę Cut(), aby wyciąć zaznaczony tekst, Copy(), aby go skopiować, i Paste(), aby wkleić tekst ze schowka. Za zaznaczenie całego tekstu w komponencie RichTextBox odpowiada metoda SelectAll(). Ponieważ na pasku narzędziowym również mamy ikony odpowiadające za kopiowanie, wycinanie i wklejanie, należy przypisać im odpowiednie procedury zdarzeniowe. Inne ciekawe metody komponentu RichTextBox wymieniłem w tabeli 10.6. Tabela 10.6. Interesujące metody komponentu RichTextBox Właściwość Opis AppendText() Umożliwia dodanie tekstu na końcu aktualnej linii. Zwraca wartość true, jeżeli w schowku są dane, CanPaste() które użytkownik może wkleić. Clear() Czyści zawartość komponentu RichTextBox. Czyści informacje na temat ostatnio cofanych ClearUndo() operacji. DeselectAll() Usuwa ewentualne zaznaczenie tekstu. Zwraca znak znajdujący się w określonej pozycji GetCharFromPosition() tekstu. GetFirstCharIndexFromLine() Zwraca indeks pierwszego znaku z podanej linii. GetFirstCharIndexOfCurrentLine() Zwraca indeks pierwszego znaku aktualnej linii. LoadFile() Umożliwia wczytanie zawartości pliku.
Redo() SaveFile() Select() Undo()
Powtarza operację, która została uprzednio cofnięta. Zapisuje tekst wpisany w kontrolce do określonego pliku. Zaznacz określony tekst znajdujący się w kontrolce. Cofnij ostatnio wykonaną operację.
Okna dialogowe Zapomnijmy na chwilę o komponencie RichTextBox. Nasza przykładowa aplikacja ma pozycje w menu służące do otwierania, zapisywania pliku. Aby zrealizować to zadanie i nadać naszej aplikacji więcej profesjonalizmu, możemy skorzystać z okien dialogowych Otwórz… oraz Zapisz… Są to standardowe okna systemu Windows, które zapewne nieraz widziałeś. Umożliwiają one wskazanie pliku, który zostanie otwarty lub zapisany. Komponenty umożliwiające wyświetlanie okien dialogowych znajdują się w kategorii Dialogs okna Toolbox. Utwórz na formularzu komponenty OpenFileDialog oraz SaveFileDialog. Są to komponenty niewizualne, więc reprezentujące je ikony zostaną umieszczone w panelu podręcznym zakładki projektowania. Wygeneruj zdarzenie Click pozycji Otwórz z menu głównego. Kod metody zdarzeniowej może wyglądać tak: private void mmOpen_Click(object sender, EventArgs e) { if (openFileDialog1.ShowDialog() == DialogResult.OK && openFileDialog1.FileName.Length > 0) { MyRichBox.LoadFile(openFileDialog1.FileName); Text = AppName + " [" + openFileDialog1.FileName + "]"; } }
Jak widzisz, sposób otwarcia nowego dokumentu jest prosty. Przede wszystkim należy wyświetlić okno dialogowe (metoda ShowDialog()). Jeżeli użytkownik wskaże plik i naciśnie przycisk OK, zmieniony zostanie tytuł okna dialogowego, ale przede wszystkim zawartość pliku zostanie załadowana do komponentu (metoda LoadFile()). Właściwość FileName komponentu OpenFileDialog zwraca ścieżkę do pliku, który wybrał użytkownik. Oprogramujmy teraz zdarzenie Click pozycji odpowiadającej za zapisanie dokumentu (pozycja Zapisz oraz Zapisz jako):
private void mmSave_Click(object sender, EventArgs e) { if (FileName == null) { mmSaveAs_Click(sender, e); } else { MyRichBox.SaveFile(FileName); Modified = false; } } private void mmSaveAs_Click(object sender, EventArgs e) { if (saveFileDialog1.ShowDialog() == DialogResult.OK && saveFileDialog1.FileName.Length > 0) { MyRichBox.SaveFile(saveFileDialog1.FileName); Text = AppName + " [" + saveFileDialog1.FileName + "]"; FileName = saveFileDialog1.FileName; Modified = false; } }
W programie zadeklarowałem pole prywatne FileName typu string. Jeżeli wartość tego pola jest równa null, należy wywołać okno dialogowe, w którym użytkownik musi podać ścieżkę oraz nazwę pliku. Zapis zawartości komponentu RichTextBox realizuje metoda SaveFile(), w której należy podać ścieżkę do zapisu. Metoda SaveFile() zapisuje plik w formacie RTF (ang. Rich Text Format), który akceptuje potem także przy odczycie.
Właściwości okien dialogowych Zarówno komponent OpenFileDialog, jak i SaveFileDialog posiada kilka interesujących właściwości, o których warto wspomnieć. Kilka z nich omówiłem w tabeli 10.7. Tabela 10.7. Właściwości okien dialogowych Właściwość Opis Title Tytuł okna dialogowego. Właściwość określa, czy automatycznie dodawać rozszerzenie, jeżeli AddExtension użytkownik je pominął.
Określa, czy generować ostrzeżenie (true), jeżeli plik wskazany przez użytkownika nie istnieje. Określa, czy generować ostrzeżenie (true), jeżeli wskazana ścieżka nie CheckPathExists istnieje. DefaultExt Domyślnie rozszerzenie (np. txt, rtf). Filtry plików, jakie wyświetlone są w liście rozwijanej okna dialogowego Filter (np. Pliki RTF|*.rtf). ShowHelp Określa, czy wyświetlać przycisk pomocy (domyślnie false). Domyślna nazwa dla pliku (wyświetlana w kontrolce tekstowej w oknie FileName dialogowym). Domyślny katalog, którego zawartość zostanie wyświetlona zaraz po InitialDirectory otwarciu okna dialogowego. CheckFileExists
Aplikacja — edytor tekstów Na podstawie tego, co zaprezentowałem w tym rozdziale, stworzyliśmy prosty edytor tekstów. Zaprezentowałem również procedury zdarzeniowe dla niektórych zdarzeń Click. Czas zaprezentować kod pozostałych procedur zdarzeniowych, których zresztą nie ma zbyt wiele. Zacznijmy od pozycji Nowy w naszym menu: private void mmNew_Click(object sender, EventArgs e) { FileName = null; Text = AppName; MyRichBox.Clear(); Modified = true; }
Pierwsza instrukcja nadaje wartość null dla pola FileName (określającego ścieżkę aktualnie edytowanego pliku). Kolejna instrukcja przypisuje do właściwości Text wartość pola AppName, które określa nazwę programu: private const string AppName = "Notepad Extra";
Ostatnia instrukcja czyści zawartość komponentu typu RichTextBox. Idąc dalej, należy również oprogramować zdarzenie Click pozycji Zakończ: private void mmExit_Click(object sender, EventArgs e) {
if (Modified) { DialogResult dr = MessageBox.Show("Plik nie został zapisany. Zapisać?", "Plik niezapisany", MessageBoxButtons.YesNoCancel); if (dr == DialogResult.Cancel) { return; } else if (dr == DialogResult.Yes) { mmSave_Click(sender, e); } Application.Exit(); } }
Po wybraniu tej pozycji aplikacja zostanie zamknięta. Uprzednio jednak program zapyta użytkownika, czy zapisać zmiany w pliku, jeżeli został on zmodyfikowany. Metoda Show() z klasy MessageBox (która notabene była często używana przeze mnie w tej książce) zwraca informacje o naciśniętym klawiszu w formie typu wyliczeniowego DialogResult. W naszym przykładzie zawartość pliku zostanie zapisana tylko wtedy, gdy użytkownik wybierze opcję Yes.
Tworzenie nowego formularza Powróćmy raz jeszcze do naszego przykładowego programu — edytora tekstu. Zapomnieliśmy oprogramować jeszcze jednej opcji z menu, a mianowicie — O programie. Chcielibyśmy, aby po wybraniu tej opcji wyświetlony został nowy formularz informacji o autorze aplikacji i adres e-mail autora. Pracując w środowisku Visual C# Express Edition, z menu Project wybierz Add Windows Form. W oknie Add New Item wskaż pozycję Windows Form. Na kolejnej zakładce zostanie wyświetlony formularz. Możesz umieścić na nim etykiety informujące o autorze programu oraz dane kontaktowe (rysunek 10.21).
Rysunek 10.21. Okno O programie… Aby uniemożliwić zmiany rozmiarów okna formularza, zmieniłem właściwość FormBorderStyle na FixedToolWindow. Zwróć uwagę, że formularz zaprezentowany na rysunku 10.21 nie ma ikon minimalizacji oraz maksymalizacji okna. Aby wyświetlić formularz O programie, należy zwyczajnie utworzyć nowy egzemplarz jego klasy, a następnie wywołać metodę Show(): private void mmAbout_Click(object sender, EventArgs e) { AboutForm About = new AboutForm(); About.Show(); }
Podsumowanie Opisanie całej biblioteki WinForms wymaga odrębnej publikacji. Ja w tym rozdziale opisałem jedynie kilka podstawowych komponentów. Mam nadzieję, że na tej podstawie będziesz w stanie wykorzystać użyteczność pozostałych komponentów, jakie udostępnia biblioteka Windows Forms. Uwierz mi, że w poznaniu tej biblioteki duże znaczenie odgrywa doświadczenie. Im częściej będziesz wykorzystywał w swoich aplikacjach bibliotekę WinForms, tym lepiej ją zgłębisz i wkrótce nie będzie ona kryła przed Tobą żadnych tajemnic.
Rozdział 11 Podzespoły .NET
Pojęcie „podzespół .NET” zostało wprowadzone w rozdziale 2. Podzespoły są fizycznymi plikami zapisanymi w formacie PE. Mogą być plikami wykonywalnymi (z rozszerzeniem .exe) albo współużytkowanymi bibliotekami .dll. Tak więc każdy program tworzony w środowisku .NET jest podzespołem. Czasami można się spotkać z określeniem komponenty .NET. Nim rozpocznę omawianie podzespołów, pragnę przybliżyć Ci tematykę modeli COM, które również stanowiły komponenty przenośne w środowisku Win32. W dalszej części tego rozdziału nauczysz się pisać własne podzespoły oraz przeprowadzać komunikację pomiędzy poszczególnymi aplikacjami.
Czym jest COM Pierwowzorem idei komponentów .NET jest COM (ang. Component Object Model), COM+, DCOM. COM zapewniał niezależność języka programowania, lecz .NET wprowadza dodatkowo jego integrację oraz niezależność od platformy. Rozwinięciem angielskiego skrótu COM jest Component Object Model (obiektowy model komponentów). Jest to specyfikacja firmy Microsoft, która w założeniu dotyczy tworzenia obiektów wielokrotnego użytku, niezależnych od języka programowania. Aby zrozumieć ActiveX, trzeba poznać COM — postaram się zwięźle wytłumaczyć to pojęcie. Otóż firma Microsoft stworzyła model obiektów, które mogą być wykorzystywane w każdym środowisku programistycznym Windows. Wynikiem powstania obiektu COM jest kontrolka — plik binarny z rozszerzeniem .ocx. Kontrolka taka może być wykorzystywana zarówno w Delphi, jak i w językach Visual C++, C++ Builder czy Visual Basic. Na razie obiekty COM działają jedynie w różnych środowiskach Windows — wykorzystanie ich poza tym systemem jest niemożliwe.
Kontrolka w rozumieniu COM Na tym etapie będę używał słowa „kontrolka” w znaczeniu obiektu COM. Można powiedzieć, że obiekty COM są takim uniwersalnym komponentem podobnym do biblioteki DLL. Raz utworzona kontrolka może być wykorzystywana wiele razy, przez wielu programistów oraz w różnych środowiskach programowania. Jeżeli ktoś już napisał kontrolkę spełniającą daną funkcję, to powtórne tworzenie takiego samego elementu nie ma sensu. Przykładem może być przeglądarka WWW. Napisanie programu analizującego kod HTML jest niezwykle czasochłonnym i żmudnym zadaniem. Niekiedy jednak w tworzonym programie konieczne staje się wyświetlenie jakiegoś dokumentu w formie strony WWW. Dzięki technologii COM i ActiveX (o ActiveX opowiem nieco później) możemy zaimportować udostępnione przez
twórców przeglądarki obiekty COM i wykorzystać je w Delphi w bardzo prosty sposób.
Odrobinę historii COM jest technologią stosunkowo nową, bo powstałą kilka lat temu. Wprowadzenie jej miało na celu zapewnienie jednolitego standardu komunikowania się, tak aby np. (by jeszcze raz posłużyć się wcześniejszym przykładem) programiści mogli korzystać z możliwości przeglądania stron WWW w swoich aplikacjach. Firma Microsoft wyszła naprzeciw tym potrzebom i utworzyła moduł obiektów (COM), który umożliwia udostępnianie innym aplikacjom swoich metod.
ActiveX ActiveX jest technologią opartą na COM. Pozwala na tworzenie kontrolek .ocx lub .dll. W rzeczywistości ActiveX to obiekt COM, tyle że posiadający własny interfejs (okna, kontrolki itp.). Tak więc mogliśmy tworzyć kontrolki ActiveX, wykorzystując np. Delphi oraz jego zalety projektowania wizualnego. Można było korzystać ze wszystkich komponentów i, ogólnie rzecz biorąc, projektowanie było łatwiejsze niż w przypadku zwykłych obiektów COM. Dodatkowo ActiveX pozwala na wygenerowanie kodu umożliwiającego umieszczenie aplikacji na stronie WWW. Platforma .NET jest następczynią COM, która zakłada integralność pomiędzy programami. Do tej pory programiści mogli budować osobne kontrolki, które później dawało się wykorzystywać w innych aplikacjach. Wiązało się to z rejestracją tej kontrolki i dodawaniem odpowiednich wpisów w rejestrze Windows. W .NET komunikacja między aplikacjami będzie ułatwiona — dany program będzie mógł dziedziczyć po klasie z innego, obsługiwać jego wyjątki itp.
DCOM DCOM jest akronimem słów Distributed Component Object Model. Technologia ta, również opracowana przez firmę Microsoft, zakłada możliwość komunikowania się pomiędzy poszczególnymi kontrolkami COM za pośrednictwem internetu. Ta technologia również została uznana za przestarzałą w stosunku do platformy .NET.
Podstawowy podzespół Jak już wiesz, podzespoły zawierają przestrzenie nazw, a te z kolei — kolejne klasy, struktury czy typy wyliczeniowe. Wszystkie aplikacje .NET wykorzystują podzespół mscorlib.dll. Podzespół ten zawiera nie tylko wszystkie podstawowe typy wykorzystywane w tej platformie, ale także klasę bazową Exception oraz wiele innych ważnych elementów. Plik mscorelib.dll znajduje się w katalogu, w którym zainstalowane jest środowisko .NET Framework. Na moim komputerze jest to C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll.
Deasembler .NET Deasembler jest programem działającym odwrotnie niż asembler — przekształca formę binarnej aplikacji do kodu pośredniego IL. Wraz z pakietem .NET Framework SDK jest dostarczany deasembler, który umożliwia analizę kodu binarnego PE i dokonanie konwersji do kodu IL. SDK to skrót od Software Developer Kit. Pakiet .NET Framework SDK zawiera narzędzia, dokumentację oraz wiele przydatnych rzeczy, które mogą przydać się każdemu programiście .NET. Pakiet ten nie jest wymagany do prawidłowego funkcjonowania aplikacji .NET. Możesz go ściągnąć za darmo ze strony http://msdn.microsoft.com/netframework/downloads/updates/default.aspx. Deasembler, o którym mowa, znajduje się w katalogu C:\Program Files\Microsoft.NET\SDK\v2.0\Bin (w zależności od konfiguracji) pod nazwą Ildasm.exe (rysunek 11.1).
Rysunek 11.1. Deasembler w trakcie działania Program jest dość prosty w użyciu. Wystarczy z menu File wybrać Open i wskazać plik, który następnie zostanie otwarty przez deasembler. Rysunek 11.1 przedstawia zawartość podzespołu mscorlib.dll. Aby obejrzeć kod IL danego fragmentu aplikacji, wystarczy kliknąć odpowiednią pozycję. Przykładowy fragment takiego kodu IL wyświetlonego przez deasembler pokazano na rysunku 11.2.
Rysunek 11.2. Podgląd kodu IL Pewnie chciałbyś teraz zapytać, w jaki sposób można zabezpieczyć się przed deasemblacją programu. Prawda jest taka, że nie można. Nikt nie będzie jednak w stanie przekształcić kodu IL do rzeczywistego kodu źródłowego C#, więc nie ma powodów do zmartwień. Jedyne ryzyko jest takie, że każdy może dowiedzieć się, z jakich modułów korzystał projektant. Może także poznać strukturę klas aplikacji. W menu File znajduje się ciekawa pozycja Dump, która umożliwia zapisanie zrzutu (kodu IL i metadanych) do pliku tekstowego.
Komponenty .NET Główną zaletą .NET Framework jest niezależność od platformy oraz od języka. W rozdziale 2. wspominałem o technologii Common Type System (wspólny system typów). Pisałem wówczas, iż dzięki CTS możliwe jest komunikowanie się pomiędzy poszczególnymi podzespołami. W środowisku Win32 możliwe jest podzielenie aplikacji na mniejsze jednostki. W tym celu stosuje się biblioteki DLL. Biblioteki DLL mogą eksportować funkcje i procedury, które z kolei mogą być wykorzystywane w aplikacji EXE. Innym rozwiązaniem jest zastosowanie kontrolek COM. Użycie takiej kontrolki w programie także nie jest łatwe, gdyż przed wykorzystaniem obiekt COM musi zostać zarejestrowany.
W .NET sytuacja wygląda zupełnie inaczej: poszczególne podzespoły mogą wykorzystywać się wzajemnie. Podzespół A może odwoływać się do metody z klasy, która znajduje się w podzespole B. W przeciwieństwie do bibliotek DLL, można korzystać z całych klas umieszczonych w danym podzespole (a nie tylko z procedur i funkcji). W tej części rozdziału zaprezentuję, w jaki sposób aplikacja C# może wykorzystywać komponent napisany w Delphi 1. Delphi jest obiektowym, profesjonalnym językiem programowania, stworzonym przez firmę Borland. Umożliwia projektowanie aplikacji zarówno na platformę Win32, jak i .NET. Zdaję sobie sprawę, że tematyka programowania w Delphi może Cię nie interesować i możesz nie znać tego języka. Dlatego prezentowane tutaj przykłady kodów źródłowych umieszczone są w formie źródłowej oraz skompilowanej na płycie CD dołączonej do książki.
Przygotowanie komponentu w Delphi Nasz przykładowy program może być bardzo prosty, nie musi wykonywać żadnych wyspecjalizowanych działań. Odgrywa jedynie rolę demonstracyjną. Nie jest w tej chwili istotne, czy nasza aplikacja będzie podzespołem .exe, czy .dll — jest skompilowana do kodu IL, więc można ją wykorzystać w ten sam sposób. Na listingu 11.1 znajduje się przykładowy program zawierający klasę Vehicle. Program jest prosty, zawiera kilka metod, które wyświetlają określony tekst na konsoli. Jak już wspominałem, nie jest to program zbyt użyteczny, bowiem jedynie demonstruje współdziałanie komponentów .NET. Listing 11.1. Przykładowy podzespół napisany w Delphi library Assembly; {$APPTYPE CONSOLE} { Copyright (c) 2004 by Adam Boduch } type Vehicle = class private procedure TurnLeft; procedure TurnRight; procedure Breaks; public procedure SendMsg(const Message : String); end;
{ Vehicle } procedure Vehicle.Breaks; begin Console.WriteLine('Włączam hamulce.'); end; procedure Vehicle.SendMsg(const Message: String); begin { sprawdzenie, jaki parametr został przekazany do procedury } if Message = 'Left' then TurnLeft else if Message = 'Right' then TurnRight else if Message = 'Breaks' then Breaks else Console.WriteLine('Nieprawidłowa komenda.'); end; procedure Vehicle.TurnLeft; begin Console.WriteLine('Skręcam w lewo.'); end; procedure Vehicle.TurnRight; begin Console.WriteLine('Skręcam w prawo.'); end; begin { empty } end.
Jeżeli masz na swoim komputerze zainstalowane środowisko Delphi 8 lub nowsze, możesz skompilować w nim poniższy kod. Dla wszystkich tych, którzy nie mają zainstalowanego środowiska Delphi, na płycie CD dołączonej do książki zamieściłem wersję skompilowaną. Parametr przekazany do metody SendMsg() decyduje o tym, jaka procedura zostanie wywołana z klasy Vehicle.
Przygotowanie komponentu C# Przede wszystkim uruchom środowisko Visual C# Express Edition i utwórz nowy projekt aplikacji konsolowej. W oknie Solution odnajdź gałąź References, kliknij ją prawym przyciskiem myszy i wybierz Add Reference. Dzięki oknu Add Reference możemy dodać do programu odwołanie do innego komponentu .NET lub kontrolki COM. Nas interesuje zakładka Browse — pozwala ona wskazać skompilowany podzespół, do którego zostanie utworzone odwołanie.
Odszukaj i wskaż skompilowany podzespół Assembly.dll, który możesz znaleźć na płycie CD dołączonej do książki. Po dodaniu nowa pozycja zostanie wyświetlona w oknie Solution. Teraz czas na wykorzystanie klasy Vehicle z naszego podzespołu. Rozwiązanie znajduje się na listingu 11.2. Listing 11.2. Program korzystający z podzespołu using System; using Assembly; /* Copyright (c) 2004 by Adam Boduch */ class MainClass { public static void Main() { try { Vehicle Car = new Vehicle(); // utworzenie obiektu String S; // deklaracja łańcucha Console.WriteLine("Przykładowa aplikacja korzystająca z podzespołu Assembly"); Console.WriteLine("Podaj komendę do wysłania:"); Console.WriteLine(" -- Left"); Console.WriteLine(" -- Right"); Console.WriteLine(" -- Breaks"); Console.WriteLine(); Console.Write("Komenda: "); S = Console.ReadLine(); // pobranie komendy Car.SendMsg(S); // przesłanie do podzespołu } catch (Exception e) { Console.WriteLine(e.ToString()); } Console.Read(); } }
Zwróć uwagę, iż przy pomocy instrukcji using musimy włączyć do programu odpowiednią przestrzeń nazw. Dalsza część kodu powinna być dla Ciebie jasna. Rysunek 11.3 prezentuje działanie takiego programu.
Rysunek 11.3. Prezentacja możliwości wykorzystania podzespołu
Kompilacja z poziomu linii poleceń Oczywiście do kompilacji takiej aplikacji można również użyć kompilatora csc.exe. Ja skorzystałem ze środowiska Visual C# Express Edition jedynie ze względu na wygodę . Aby kompilacja takiego programu przy pomocy kompilatora csc.exe przebiegła bez żadnych przeszkód, należy kompilować w ten sposób: csc /r:C:\katalog\Assembly.dll /out:C:\demo.exe C:\katalog\demo.cs
Powyższe polecenie odniesie oczekiwany efekt, pod warunkiem że plik Assembly.dll oraz demo.cs znajdują się w katalogu C:\katalog. Przełącznik /r: określa podzespół, z jakim zostanie skompilowany nasz program. Dzięki temu kompilator rozpoznaje typ Vehicle w kodzie C#. Z kolei przełącznik /out: określa ścieżkę, w której zostanie umieszczona wersja wynikowa naszego programu. Jeżeli kompilator nie wyświetli żadnego błędu, można spróbować uruchomić aplikację demo.exe.
Zalety stosowania podzespołów W poprzednich przykładach zaprezentowałem sposób na wykorzystanie możliwości danego
podzespołu z poziomu drugiego programu. Taki przykład powinien zobrazować Ci pewną zaletę platformy .NET, a mianowicie niezależność. Jak widzisz, jeden program (podzespół) został napisany w języku Delphi, a drugi — w C#. Niezależnie od tego bez żadnych problemów mogliśmy wykorzystać klasę Vehicle. W ten sposób skonstruowana jest cała biblioteka klas .NET Framework. Plik mscorelib.dll jest skompilowanym podzespołem zawierającym setki klas. Programując na platformie .NET, możemy ten plik włączyć do swojej aplikacji i wykorzystywać znajdujące się w nim klasy. Będąc programistom, możesz pisać swoje aplikacje w języku C#, a następnie sprzedawać. Wyobraź sobie, że napisałeś program zawierający klasę służącą do szyfrowania tekstu. Jest ona tak rewolucyjna, że bez problemów znajdujesz kupców na swój program. Nie chcesz jednak udostępniać swoich kodów źródłowych, sprzedajesz więc wersję skompilowaną. Firma, która kupiła od Ciebie ów program, może wykorzystywać klasy zawarte w podzespole, nie znając przy tym zawartości kodu! Takiej firmie wystarczy jedynie dokumentacja klasy. Przy pomocy mechanizmu dziedziczenia oraz polimorfizmu firma, która kupiła od Ciebie podzespół, może napisać nową klasę, dziedziczącą po Twojej, znajdującej się w osobnym podzespole! Inną zaletą dzielenia aplikacji na kilka podzespołów jest podział funkcjonalności. Możesz podzielić aplikację na kilka mniejszych modułów według ich funkcjonalności. Przypuszczam, że już niedługo będziesz (jeżeli jeszcze nie jesteś) zawodowym programistą. Możesz pisać aplikacje i udostępniać je na zasadach licencji shareware. W każdej aplikacji zamieszczasz formularz służący do rejestracji programu i uzyskiwania klucza produktu. Po co ten sam kod kopiować do wielu programów? Nie lepiej przenieść go do osobnego podzespołu, który będziemy ładować, kiedy będzie potrzebny? Dzielenie aplikacji na kilka modułów (podzespołów) ma też inną zaletę. Jeżeli w jednym z modułów wykryjesz błąd, możesz udostępnić swoim klientom poprawkę w postaci jednego podzespołu, a nie całej aplikacji. Możesz uniemożliwić innym podzespołom dziedziczenie lub wręcz wykorzystywanie Twoich klas lub ich elementów. Należy wówczas deklarować elementy z użyciem modyfikatora dostępu internal.
Budowa podzespołu Powiedzieliśmy sobie, że podzespół to nie tylko pośredni kod IL. W rzeczywistości jego zawartość można podzielić na cztery elementy:
kod pośredni IL, zasoby aplikacji,
manifest, metadane.
Te wszystkie elementy są umieszczane w jednym pliku wykonywalnym (.exe lub .dll). Kod pośredni jest zapisany w języku IL, który przypomina nieco Asemblera. Wspominałem o nim w rozdziale 4. i wydaje mi się, że to pojęcie nie wymaga dalszego wyjaśnienia. O zasobach wspominałem w poprzednim rozdziale. Zasoby to pliki dźwiękowe, graficzne, tekstowe oraz inne pliki wykonywalne. Zasoby mogą być umieszczone w zewnętrznym pliku, ale najczęściej są umieszczane w aplikacji wykonywalnej. Metadane to informacje o kodzie źródłowym. Są one generowane w trakcie kompilacji i opisują klasy, metody czy inne typy wykorzystywane w kodzie. Dzięki metadanym jesteśmy w stanie stwierdzić, jaka jest zawartość danego podzespołu, lepiej wykorzystać jego funkcjonalność. Wykonaj małe doświadczenie. W kodzie źródłowym kliknij prawym przyciskiem nazwę jakiejś klasy biblioteki FCL. Z menu podręcznego wybierz pozycję Go to definition. Na podstawie metadanych zawartych w podzespole środowisko Visual C# Express Edition jest w stanie wyświetlić informację o zawartości danej klasy czy przestrzeni nazw. Manifest to informacje o metadanych podzespołu. Manifest zawiera informacje odnośnie do podzespołu, jego relacji z innymi komponentami .NET. W szczególności jest to:
Nazwa atrybutu — ciąg znaków określający nazwę podzespołu. Wersja podzespołu — numer wersji podzespołu. Lokalizacja — informacje o lokalizacji lub o języku interfejsu podzespołu. Ścisła kontrola nazw — publiczny klucz podzespołu. O tym opowiem w dalszej części rozdziału. Lista plików — informacje o plikach wykorzystywanych w projekcie.
Atrybuty podzespołu W podzespołach można zadeklarować określone atrybuty opisujące dany podzespół. Mogą one być później odczytywane przez inne programy w celu weryfikacji numeru wersji aplikacji oraz nazwy producenta. Innymi słowy, atrybuty są narzędziem programisty, który może decydować o zawartości manifestu. Jeżeli masz otwarty projekt aplikacji C#, nieważne, czy Windows Forms, czy konsolowy, przejdź do okna Solution w gałęzi Properties, wybierz pozycję AssemblyInfo.cs i kliknij ją: using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify
the information // associated with an assembly. [assembly: AssemblyTitle("AssemblyApp")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("AssemblyApp")] [assembly: AssemblyCopyright("Copyright © 2006")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("a0bf0a73-54d8-4b7b-9472-3a2c0980654f")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")]
Zawartość tego modułu jest również kompilowana wraz z pozostałym kodem. Moduł ten zawiera informacje o naszej aplikacji w formie atrybutów. Te informacje zostaną skompilowane do kodu pośredniego IL. Jak widzisz, umieszczanie atrybutów w kodzie programu jest dość specyficzne. Atrybuty umieszczane są w nawiasach kwadratowych, z użyciem słowa określającego przeznaczenie atrybutu (assembly): [assembly: nazwa atrybutu]
Atrybuty są zwykłymi klasami, które dziedziczą po Attribute (która to zadeklarowana jest w przestrzeni System.Reflection). Przykładowo, deklaracja klasy AssemblyFileVersionAttribute w przestrzeni System.Reflection wygląda następująco: public sealed class AssemblyFileVersionAttribute : Attribute {
public AssemblyFileVersionAttribute(string version); public string Version { get; } }
W przypadku tego atrybutu do klasy należy przekazać jeden parametr w postaci typu string. Jeżeli chcesz, możesz zmienić zawartość atrybutów, m.in. podając prawidłową nazwę aplikacji oraz wersję czy autora.
Mechanizm refleksji Powiedziałem już, że dzięki metadanym mamy możliwość odczytania informacji na temat elementów umieszczonych w podzespole. Pokazywałem, jak to zrobić, korzystając z opcji Go to definition w menu podręcznym środowiska Visual C# Express Edition. Mechanizm refleksji umożliwia odczytanie informacji odnośnie do metadanych. Z tego mechanizmu korzysta m.in. środowisko Visual C# Express Edition przy odczytywaniu informacji z podzespołu. Mamy możliwość przeglądania zawartości biblioteki klas FCL lub jakiegokolwiek innego podzespołu przy pomocy okna Object Browser (rysunek 11.4).
Rysunek 11.4. Okno Object Browser Okno Object Browser można wywołać, wybierając z menu View pozycję Other Windows/Object Browser. Korzystając z przycisku ... znajdującego się obok listy rozwijanej Browse, mamy możliwość wyboru podzespołu, z którego dane zostaną wyświetlone w oknie. Rysunek 11.4 przedstawia informacje o metadanych podzespołu Assembly.dll (napisanego w Delphi). Po zaznaczeniu wybranej pozycji w prawym oknie wyświetlona zostanie lista elementów danej klasy wraz z ewentualnymi parametrami metod. Okno Object Browser umożliwia odczytywanie informacji z wybranego podzespołu i równocześnie stanowi prosty i szybki sposób na poznanie budowy programu. Istnieje możliwość zastosowania mechanizmu refleksji w naszej aplikacji dzięki przestrzeni nazw System.Reflection, która jest udostępniana przez pakiet .NET
Framework. Mechanizm refleksji pozwala na odczyt dowolnego podzespołu .NET. Nie jest więc istotne, czy program został napisany w C#, Delphi czy w C++. Po prostu program napisany w Delphi będzie zawierał więcej informacji, gdyż kompilator Delphi dodatkowo włącza w aplikację wykonywalną zawartość przestrzeni Borland.Delphi.System.
Funkcja GetType Każda klasa .NET posiada metodę GetType(), która jest dziedziczona po klasie głównej — System.Object. Owa metoda zwraca rezultat w postaci klasy System.Type. Dzięki niej można odczytywać takie informacje jak nazwa podzespołu, jego wersja, a także przestrzeń adresowa, w jakiej się on znajduje. Tym zajmiemy się nieco później — teraz pozostaniemy jedynie przy odczytaniu nazwy danego typu: using System; namespace FooApp { class Program { static void Main(string[] args) { int I = 10; double D = 10.5; Console.WriteLine("Zmienna I jest typu: " + I.GetType().ToString()); Console.WriteLine("Zmienna D jest typu: " + D.GetType().ToString()); Console.Read(); } } }
Typy int czy double w C# są odpowiednikami typów .NET, również są klasami, zatem posiadają metodę GetType(), która zwraca informacje o obiekcie. W powyższym przykładzie użyłem metody ToString() do znakowego przedstawiania typu zmiennej. Działanie takiego programu spowoduje wyświetlenie na ekranie tekstu: Zmienna I jest typu System.Int32 Zmienna D jest typu System.Double
Klasa System.Type Jak powiedziałem wcześniej, metoda GetType() zwraca informacje w postaci obiektu System.Type, z którego można wyczytać więcej informacji na temat konkretnego obiektu .NET. Na formularzu WinForms umieśćmy teraz komponent ListBox oraz Button. Nazwijmy je, odpowiednio, lbInfo oraz btnGetInfo (nazwy komponentów nie odgrywają większej roli). Zdarzenie Click przycisku powinno wyglądać tak jak na listingu 11.3. Listing 11.3. Wykorzystanie klasy System.Type using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace WinForms { public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void btnGetInfo_Click(object sender, EventArgs e) { System.Type MyType; MyType = sender.GetType(); lbInfo.Items.Add("Nazwa klasy: " + MyType.FullName); lbInfo.Items.Add("Przestrzeń nazw: " + MyType.Namespace); lbInfo.Items.Add("Nazwa podzespołu: " + MyType.Assembly.FullName); lbInfo.Items.Add("Wersja podzespołu: " + MyType.Assembly.GetName().Version.ToString()); lbInfo.Items.Add("Nazwa pliku: " + MyType.Module.Name); } } }
Po naciśnięciu przycisku aplikacja pobiera informację na temat komponentu Button. Wyświetla m.in. pełną nazwę klasy (System.Windows.Forms.Button) oraz podzespołu. Należy zapamiętać, że nazwa, jaką nadaje się komponentowi, nie ma większego znaczenia dla działania programu. Nie można jednak przepisywać bezmyślnie kodu z tej książki, gdyż w poszczególnych przypadkach nazwa komponentu może się różnić od tej, którą ja nadałem. W takiej sytuacji program nie zostanie skompilowany. Na samym początku do zadeklarowanej zmiennej MyType jest przypisywany rezultat działania metody GetType(). Od tego momentu zmienna MyType zawiera informację na temat przycisku. Następnie informacje te są umieszczane w komponencie typu ListBox.
Ładowanie podzespołu Program przedstawiony w poprzednim przykładzie pobierał informacje o komponencie typu Button. Jest to dość proste, lecz niezbyt przydatne. Okno Object Browser umożliwia odczytanie informacji na temat dowolnego podzespołu. Aby to wykonać, można wykorzystać klasę System.Reflection.Assembly. Dla przypomnienia: dla kompilatora nie ma znaczenia, czy zadeklarowana zmienna będzie typu Assembly czy System.Reflection.Assembly. Załadowanie podzespołu wiąże się zaledwie z jedną linią kodu — wywołaniem metody LoadFrom() z klasy System.Reflection.Assembly (w skrócie będę nazywał tę klasę po prostu Assembly): System.Reflection.Assembly AFile; AFile = System.Reflection.Assembly.LoadFrom("Assembly.exe");
Taki zapis umożliwia następnie odczytanie klas znajdujących się w podzespole oraz ich właściwości, zdarzeń itd.
Przykład działania — program Reflection Rysunek 11.5 prezentuje przykładowy program Reflection, który analizuje zawartość podzespołu. Zadaniem aplikacji jest wyświetlanie listy typów zawartych w podzespole wraz z ich właściwościami, polami, metodami oraz zdarzeniami.
Rysunek 11.5. Przykładowy program Reflection Na rysunku 11.5 program odczytuje zawartość biblioteki Assembly.dll, którą opisywałem w trakcie prezentowania możliwości wspólnego modelu programowania. Interfejs aplikacji składa się z komponentów TreeView (komponent nazwałem tvAssembly), Button oraz niewidoczny OpenFileDialog (przypomnę — służy on do wyświetlania standardowego okna Windows Otwórz plik). Komponent TreeView został użyty z uwagi na możliwość tworzenia gałęzi, co daje wrażenie hierarchicznej struktury. W programie wykorzystałem również komponent ProgressBar. Pokazuje on postęp w trakcie analizowania i wczytywania podzespołu. Cały kod źródłowy został przedstawiony na listingu 11.4. Najpierw mu się przyjrzyj, później przeczytasz opis jego budowy. Listing 11.4. Kod źródłowy programu Reflection using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace Reflection
{ public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void btnLoad_Click(object sender, EventArgs e) { System.Reflection.Assembly AFile; TreeNode ClassTree, PropTree, MethodTree, FieldTree, EventTree; // usunięcie wszystkich gałęzi (jeżeli są) tvAssembly.Nodes.Clear(); // wyświetlenie okna dialogowego openFileDialog1.ShowDialog(); try { // pobranie nazwy wybranego podzespołu oraz załadowanie go AFile = System.Reflection.Assembly.LoadFrom(openFileDialog1.FileName); Text = "Reflection [" + openFileDialog1.FileName + "]"; } catch { MessageBox.Show("Nie można załadować podzespołu!"); return; } // załadowanie informacji o klasach znajdujących się w podzespole System.Type[] MyType = AFile.GetTypes();
progressLoad.Maximum = MyType.Length; for (int i = 0; i < MyType.Length; i++) { // utworzenie nowego węzła o nazwie klasy z podzespołu ClassTree = tvAssembly.Nodes.Add(MyType[i].FullName); // utworzenie nowej gałęzi o nazwie „Właściwości”
PropTree = ClassTree.Nodes.Add("Właściwości"); for (int subI = 0; subI < MyType[i].GetProperties().Length; subI++) { PropTree.Nodes.Add(MyType[i].GetProperties().GetValue(subI).To String()); } // utworzenie nowej gałęzi o nazwie „Metody” MethodTree = ClassTree.Nodes.Add("Metody"); for (int subI = 0; subI < MyType[i].GetMethods().Length; subI++) { MethodTree.Nodes.Add(MyType[i].GetMethods().GetValue(subI).ToS tring()); } // utworzenie nowej gałęzi o nazwie „Pola” FieldTree = ClassTree.Nodes.Add("Pola"); for (int subI = 0; subI < MyType[i].GetFields().Length; subI++) { FieldTree.Nodes.Add(MyType[i].GetFields().GetValue(subI).ToStr ing()); } // utworzenie nowej gałęzi o nazwie „Zdarzenia” EventTree = ClassTree.Nodes.Add("Zdarzenia"); for (int subI = 0; subI < MyType[i].GetEvents().Length; subI++) { MethodTree.Nodes.Add(MyType[i].GetEvents().GetValue(subI).ToSt ring()); } progressLoad.Value = i; } progressLoad.Value = 0; } } }
Po załadowaniu wybranego przez użytkownika podzespołu następuje odczytanie znajdujących się w nim typów (metoda GetTypes()) do tablicy MyTypes. Następnie pętla for
analizuje po kolei wszystkie elementy, dodając za każdym razem nową pozycję w komponencie TreeView: ClassTree = tvAssembly.Nodes.Add(MyType[i].FullName);
Tworzenie nowej pozycji w komponencie TreeView przypomina nieco tworzenie nowej linii w kontrolce ListBox. Zmienna ClassTree stanowi niejako „uchwyt”, który będzie wykorzystywany do tworzenia kolejnych odgałęzień. Kolejnym krokiem w działaniu programu jest odczytanie właściwości, metod, zdarzeń, pól oraz metod każdej z klas. W każdym przypadku proces ten jest niemal identyczny (różne są jedynie nazwy służących do tego funkcji): PropTree = ClassTree.Nodes.Add("Właściwości"); for (int subI = 0; subI < MyType[i].GetProperties().Length; subI++) { PropTree.Nodes.Add(MyType[i].GetProperties().GetValue(subI).T oString()); }
Na samym początku tworzone jest odgałęzienie Właściwości, do którego będą doklejane kolejne gałęzie zawierające nazwy właściwości. W tym celu kolejna pętla for pobiera nazwy kolejnych właściwości klasy. Jak zapewne zdążyłeś się zorientować, do pobierania listy właściwości danego typu służy metoda GetProperties(), która również zwraca listę w postaci tablicy.
Odczyt atrybutów z podzespołu W naszym programie Reflection odczytywaliśmy jedynie drzewo klas i metod danego podzespołu. Teraz zajmiemy się odczytem niestandardowych atrybutów podzespołu, takich jak jego opis, tytuł itp. W tym celu .NET Framework posiada klasy umożliwiające odczyt np. tytułu podzespołu (klasa AssemblyTitleAttribute). Naszym zadaniem jest załadowanie wybranego podzespołu (to już opisywałem), a następnie wywołanie metody GetCustomAttributes(), która pobierze nazwy wszystkich atrybutów znajdujących się w podzespole i zapisze je do tablicy. Rozbudujmy więc naszą aplikację Reflection, dodając do niej nową funkcjonalność. Do formularza dodałem kilka komponentów typu TextBox, które będą przechowywać nazwę podzespołu, jego opis, opis praw autorskich oraz wersję. Odpowiada za to prywatna metoda:
private void loadAttributes(System.Reflection.Assembly AFile) { System.Object[] Attrs = AFile.GetCustomAttributes(true); for (int i = 0; i < Attrs.Length; i++) { /* W tych instrukcjach następuje sprawdzenie, czy dany obiekt należy do szukanej przez nas klasy. Pozwala to uniknąć błędów w trakcie działania programu. Inaczej mówiąc, instrukcja if sprawdza, czy w podzespole znajduje się dany atrybut (przykładowo: „AssemblyTitleAttribute” */ if (Attrs[i] is AssemblyTitleAttribute) { edtTitle.Text = (Attrs[i] as AssemblyTitleAttribute).Title; } if (Attrs[i] is AssemblyDescriptionAttribute) { edtDescription.Text = (Attrs[i] as AssemblyDescriptionAttribute).Description; } if (Attrs[i] is AssemblyCopyrightAttribute) { edtCopyright.Text = (Attrs[i] as AssemblyCopyrightAttribute).Copyright; } if (Attrs[i] is ComCompatibleVersionAttribute) { edtVersion.Text = Convert.ToString( (Attrs[i] as ComCompatibleVersionAttribute).MajorVersion) + '.' + Convert.ToString((Attrs[i] as ComCompatibleVersionAttribute).MinorVersion); } } }
Przed próbą kompilacji programu nie można zapomnieć o włączeniu do programu przestrzeni nazw System.Reflection i System.Runtime.InteropServices.
W powyższym przykładzie posłużyłem się mechanizmem rzutowania za pomocą operatorów is i as: if (Attrs[i] is AssemblyTitleAttribute) { edtTitle.Text = (Attrs[i] as AssemblyTitleAttribute).Title; }
Najpierw program sprawdza, czy element tablicy określony zmienną i jest typu AssemblyTitleAttribute. Jeżeli tak, następuje rzutowanie tego elementu tablicy oraz wyciągnięcie wartości właściwości Title. Kontrolka o nazwie edtText w rzeczywistości jest komponentem TextBox umieszczonym na formularzu. Zastosowanie takiego mechanizmu było konieczne ze względu na to, iż funkcja GetCustomAttributes zwraca elementy w postaci tablicy typu System.Object. Należało więc ustalić, czy rzutowanie na daną klasę (np. AssemblyTitleAttribute) powiedzie się — stąd konieczność użycia operatorów is i as.
Własne atrybuty Powiedziałem, że atrybuty są danymi opisującymi dane. Oprócz standardowych atrybutów — określających np. prawa autorskie do podzespołu — można zadeklarować własne, które tak samo będą odczytywane przez programy typu Reflection. Do czego mogą się przydać własne atrybuty? Możliwości są nieograniczone. Przykładowo: umieszczanie w swoich programach takich informacji jak odnośnik do strony WWW zawierającej kod XML danego programu. Gdy wszystkie Twoje aplikacje będą zawierały odpowiedni atrybut z odnośnikiem do pliku XML, to będą mogły zacieśnić współpracę — w pliku XML może znajdować się dodatkowa informacja dotycząca programu. Inny przykład: notowanie błędów aplikacji. Można oczywiście umieszczać komentarz przy klasie, w której poprawiono kod. Można także użyć atrybutu, np. w taki sposób: [BugFixedAttribute("Adam", "19-05-2006", "Dodałem metodę")] [BugFixedAttribute("Janek", "21-06-2006", "Poprawka")] class Program { static void Main(string[] args) { } }
Własny atrybut BugFixAttribute opisuje dane dotyczące błędu — nazwisko programisty, który go naprawił, datę naprawy oraz komentarz. Takie informacje może zawierać także ID błędu. Następnie odpowiedni moduł aplikacji odczyta wszystkie wartości atrybutu TBugFixAttribute, w tym ID błędu, oraz pozwoli na połączenie z bazą danych, która na podstawie ID ujawni więcej informacji na temat poprawki.
Deklaracja własnego atrybutu Aby dany atrybut był rozpoznawany przez kompilator, trzeba w aplikacji utworzyć klasę o nazwie odpowiadającej atrybutowi: class BugFixedAttribute : Attribute { public BugFixedAttribute(string Programmer, string Date, string Comment) { } }
Taka klasa musi dziedziczyć po Attribute. Jak widzisz, klasa BugFixedAttribute posiada konstruktor z trzema parametrami. Jest to ważny element programu, gdyż kiedy użyjemy atrybutu, jego wartości zostaną przypisane do parametrów konstruktora: [BugFixedAttribute("Adam", "19-05-2006", "Dodałem metodę")]
Użycie atrybutu w kodzie programu przypomina wywołanie metody klasy. Mamy również możliwość przekazania do niego określonych parametrów oddzielonych znakiem przecinka. Istnieje możliwość jawnego przypisania parametrów atrybutu do danych właściwości — np.: [BugFixedAttribute(Programmer = "Janek", Date = "21-06-2006", Comment = "Poprawka")]
Wówczas w klasie BugFixedAttribute należy zadeklarować właściwości Programmer, Date oraz Comment: class BugFixedAttribute : Attribute { private string FProgrammer; private string FDate; private string FComment; public string Programmer
{ get { return FProgrammer; } set { FProgrammer = value; } } public string Date { get { return FDate; } set { FDate = value; } } public string Comment { get { return FComment; } set { FComment = value; } }
public BugFixedAttribute(string AProgrammer, string ADate, string AComment) { FProgrammer = AProgrammer; FDate = ADate; FComment = AComment; } public BugFixedAttribute() { } }
Wydaje mi się, że zapis z konstruktorem jest szybszy i przejrzystszy, aczkolwiek nie jest zbyt czytelny dla programisty nieznającego parametrów atrybutu.
Odczyt wartości atrybutu W poprzednim przykładzie pokazywałem, w jaki sposób można odczytać atrybuty znajdujące się w innym podzespole. Aby odczytać atrybuty znajdujące się we własnym programie, nie trzeba korzystać z typu Assembly — kod stanie się bardziej przejrzysty po użyciu następującej instrukcji: System.Object[] Attrs; Attrs = typeof(Foo).GetCustomAttributes(typeof(BugFixedAttribute), true);
Operator typeof jest tutaj ważny, gdyż zwraca typ danych (w tym przypadku klasy) w postaci zmiennej typu System.Type. Po uzyskaniu typu można wywołać metodę GetCustomAttributes(), pobierającą atrybuty programu. Pierwszy parametr funkcji, GetCustomAttributes(), informuje o tym, że atrybuty mają być jedynie typu BugFixAttributes. Jeśli mamy tablicę danych typu Object (Attrs), odczyt atrybutów jest taki sam jak w poprzednio prezentowanym przykładzie. Listing 11.5 zawiera pełny kod źródłowy programu odczytującego atrybuty użyte w programie. using System; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace AttrsApp { [AttributeUsage(AttributeTargets.All, AllowMultiple=true)] class BugFixedAttribute : Attribute { private string FProgrammer; private string FDate; private string FComment; public string Programmer { get { return FProgrammer; } set { FProgrammer = value; }
} public string Date { get { return FDate; } set { FDate = value; } } public string Comment { get { return FComment; } set { FComment = value; } }
public BugFixedAttribute(string AProgrammer, string ADate, string AComment) { FProgrammer = AProgrammer; FDate = ADate; FComment = AComment; } public BugFixedAttribute() { } } [BugFixedAttribute("Adam", "19-05-2006", "Dodałem metodę")] [BugFixedAttribute(Programmer = "Janek", Date = "21-062006", Comment = "Poprawka")] class Foo { static void Bar() { } } class Program { static void Main(string[] args)
{ System.Object[] Attrs; Attrs = typeof(Foo).GetCustomAttributes(typeof(BugFixedAttribute), true); for (int i = 0; i < Attrs.Length; i++) { Console.WriteLine("-------"); Console.WriteLine("Programista: " + (Attrs[i] as BugFixedAttribute).Programmer); Console.WriteLine("Data: " + (Attrs[i] as BugFixedAttribute).Date); Console.WriteLine("Komentarz: " + (Attrs[i] as BugFixedAttribute).Comment); } Console.Read(); } } }
Rysunek 11.6 prezentuje program w trakcie działania.
Rysunek 11.6. Odczyt atrybutów użytych w programie
Użycie atrybutu
Przypatrz się dokładnie listingowi 11.5. Klasa BugFixedAttribute została opatrzona atrybutem AttributeUsage: [AttributeUsage(AttributeTargets.All, AllowMultiple=true)]
Takie użycie oznacza, że nasza klasa BugFixedAttribute będzie mogła być użyta w połączeniu z dowolnym elementem programu. Parametr AllowMultiple informuje, że nasz atrybut BugFixedAttribute może być użyty w wielu miejscach aplikacji. Wartość false spowodowałaby błąd kompilacji: Duplicate 'BugFixedAttribute' attribute w przypadku wielokrotnego użycia tego samego atrybutu.
Aplikacje .NET Framework SDK Pakiet .NET Framework Software Developer Kit zawiera aplikacje przydatne każdemu programiście .NET. Pakiet ten nie jest wymagany do prawidłowego działania aplikacji, lecz może się okazać przydatny, gdybyśmy chcieli np. wykorzystać deasemblera .NET czy inne programy związane z tą platformą. Chciałbym w tym miejscu wspomnieć o kilku użytecznych aplikacjach dostępnych w pakiecie SDK. Więcej informacji na temat omawianych tu programów (np. dodatkowe parametry czy opcje) można znaleźć w dokumentacji .NET Framework. Spod adresu http://msdn.microsoft.com/netframework/downloads/updates/default.aspx możesz ściągnąć najnowszy pakiet .NET Framework SDK.
Global Assembly Cache Tool Obiekty COM należało zarejestrować przed użyciem, co powodowało tworzenie odpowiednich wpisów w rejestrze. Dopiero później można było użyć takiej kontrolki. Jeżeli chodzi o komponenty .NET, zabiegi takie nie są konieczne — stosowny przykład zaprezentowałem wcześniej w tym rozdziale, gdzie klasa z aplikacji napisanej w Delphi była wykorzystana w programie C#. W tamtym przykładzie skorzystałem z tzw. komponentów prywatnych (ang. private components). Oznaczało to, że tylko jedna nasza aplikacja mogła korzystać z owego podzespołu, a przy tym należało uważać na ścieżki (katalogi), w których się ona znajdowała. Przykładowo, w opisywanym przykładzie podzespół, z którego korzystamy, zawsze będzie musiał znajdować się w tym samym katalogu co korzystający z niego program. .NET zakłada możliwość tworzenia podzespołów współużytkowanych (ang. shared
components), czyli takich, które raz zarejestrowane zostaną umieszczone w jednym głównym katalogu. Z takiego katalogu będzie mogła korzystać każda aplikacja, która zechce użyć naszego podzespołu. Przestrzeń, w której są rejestrowane takie podzespoły, nazywa się Global Assembly Cache (w skrócie GAC) i znajduje się w katalogu C:\WINDOWS\assembly (w moim przypadku). Każdy podzespół powinien posiadać opis, nazwę, informację o prawach autorskich oraz — co bardzo ważne — numer wersji. Na jednej maszynie może istnieć kilka takich samych podzespołów o różnych numerach wersji. Program Global Assembly Cache Tool, kryjący się pod nazwą gacutil.exe, umożliwia rejestrację danego podzespołu jako globalnego, współużytkowanego. Program gacutil jest wywoływany z poziomu wiersza poleceń. Aby zainstalować dany podzespół, należy wywołać program z opcją /i, tak jak to zrobiłem poniżej: C:\Program Files\Microsoft.NET\SDK\v1.1\Bin>gacutil /i c:\csharpexample\Assembly.dll Microsoft (R) .NET Global Assembly Cache Utility. Version 1.1.4322.573 Copyright (C) Microsoft Corporation 1998-2002. All rights reserved. Assembly successfully added to the cache
Przed zainstalowaniem podzespołu jako GAC zaleca się wygenerowanie specjalnego klucza, który stanowi o unikalności jego oraz numeru jego wersji. Do tego celu należy użyć narzędzia Strong Name Tool (sn.exe), które wygeneruje odpowiedni plik będący kluczem. Plik ten z kolei należy dołączyć jako atrybut do programu: C:\Program Files\Microsoft.NET\SDK\v1.1\Bin>sn -k Assembly.snk Microsoft (R) .NET Framework Strong Name Utility Version 1.1.4322.573 Copyright (C) Microsoft Corporation 1998-2002. All rights reserved. Key pair written to Assembly.snk
Program sn.exe także jest aplikacją wywoływaną z poziomu wiersza poleceń. W celu wygenerowania klucza należy uruchomić ją z parametrem –k oraz podać nazwę pliku, do którego klucz zostanie zapisany (tak jak to przedstawiłem powyżej). Koncepcja strong names jest podobna do 128-bitowego klucza GUID (ang. Globally Unique Identifier) w COM. GUID zapewnia unikalność klucza. Tymczasem wersja oraz nazwa podzespołu nie gwarantują unikalności tak jak klucz. Dlatego też używamy narzędzia sn.exe. Włączenie klucza do podzespołu może nastąpić za sprawą użycia programu al.exe. Można także włączyć klucz bezpośrednio w kodzie programu, używając atrybutu AssemblyKeyFileAttribute.
Wygenerowany tym sposobem plik można skopiować do katalogu z kodem źródłowym programu, a następnie włączyć go, korzystając z atrybutu AssemblyKeyFileAttribute. [assembly: AssemblyKeyFileAttribute("Assembly.snk")]
Rysunek 11.7 przedstawia podzespół zainstalowany w przestrzeni GAC.
Rysunek 11.7. Podzespół zainstalowany w przestrzeni GAC Zalecane jest odpowiednie nazewnictwo podzespołów w postaci NazwaFirmy.NazwaPodzespołu, np. Boduch.Foo itd. Łatwiej jest się wtedy zorientować, czyjego autorstwa jest podzespół, oraz wprowadza się pewną zasadę nadawania nazw, która powinna być respektowana przez większość programistów.
WinCV Nazwa programu jest skrótem od słów Windows Class Viewer (ang. podgląd klas). Jest to przydatny i prosty program służący do odnajdywania i podglądu zawartości klas .NET (rysunek 11.8). {{Image:csharp11.8.jpg}} Rysunek 11.8. Program WinCV w trakcie działania Głównym elementem programu jest pole, w którym należy wpisać słowo kluczowe do wyszukania. Na liście po lewej stronie zostaną wyświetlone klasy zawierające to słowo kluczowe. Po zaznaczeniu danej klasy w głównym oknie zostanie wyświetlona lista znajdujących się w niej właściwości, pól, zdarzeń oraz metod wraz z nazwami oraz typem parametrów.
Narzędzie konfiguracji .NET Framework Narzędzie konfiguracji .NET Framework kryje się pod nazwą mscorcfg.msc. Jest to program, dzięki któremu można zarządzać GAC poprzez dodawanie lub usuwanie wybranych podzespołów. Istnieje możliwość ustawienia wielu opcji uprawnień aplikacji, wersji, jaka ma być ładowana itp.
PEVerify — narzędzie weryfikacji Narzędzie PEVerify.exe służy do weryfikacji, czy kod MSIL i metadane spełniają warunki bezpiecznego kodu. Aby upewnić się, że dana aplikacja jest bezpieczna, można wywołać PEVerify.exe z poziomu wiersza poleceń: C:\Program Files\Microsoft.NET\SDK\v1.1\Bin>PEVerify C:\P6_1.exe Microsoft (R) .NET Framework PE Verifier Version 1.1.4322.573 Copyright (C) Microsoft Corporation 1998-2002. All rights reserved. All Classes and Methods in C:\P6_1.exe Verified Jeżeli po wywołaniu aplikacji zostanie wyświetlony komunikat All Classes and Methods in C:\P6_1.exe Verified, można być pewnym, że jest ona bezpieczna i zweryfikowana.
.NET a COM Pokazałem, w jaki sposób poszczególne podzespoły mogą komunikować się ze sobą oraz wykorzystywać klasy z innych podzespołów. Jednak cały ten proces odbywał się w obrębie kodu zarządzanego (managed code). W tym podrozdziale zaprezentuję sposób stosowania kodu niezarządzanego (kontrolki COM) w języku C# na .NET. Import obiektu COM do aplikacji .NET jest dosyć prosty. Obiekty Win32 COM mogą zostać zaimportowane do .NET za pomocą narzędzia o nazwie Tlbimp.exe, dołączonego do .NET Framework SDK. Zasady importowania obiektów COM do .NET przedstawię na przykładzie kontrolki SAPI (Microsoft Speech API). Najpierw odszukajmy na dysku plik sapi.dll. Potem z poziomu wiersza poleceń trzeba uruchomić program Tlbimp.exe, który zaimportuje kontrolkę COM do .NET: tlbimp "C:\Scieżka do pliku\sapi.dll" /verbose /out:C:\Interop.SAPI.dll
Takie użycie programu spowoduje utworzenie podzespołu Interop.SAPI.dll, który można w pełni wykorzystać w środowisku .NET. Normalne jest, że podczas konwersji na konsoli jest wyświetlana masa komunikatów ostrzegających o możliwej niekompatybilności owej kontrolki z .NET. Gdy utworzymy podzespół, trzeba skopiować go do katalogu, w którym następnie należy umieścić nowy projekt WinForms. W oknie Solution należy dodać odwołanie do pliku Interop.SAPI.dll. Interfejs aplikacji składa się z komponentu typu RichTextBox oraz Button (rysunek 11.9). Po kliknięciu przycisku ciąg znakowy z pola tekstowego zostanie przekazany do biblioteki, co spowoduje wywołanie lektora, który przeczyta tekst.
Rysunek 11.9. Przykład użycia kontrolki COM w aplikacji .NET Użycie kodu z biblioteki COM wiąże się z dodaniem odpowiedniej przestrzeni nazw Interop.SAPI. Kod źródłowy aplikacji wykorzystującej tę bibliotekę COM wygląda tak: using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms; Interop.SAPI;
namespace SAPI { public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void btnTalk_Click(object sender, EventArgs e) {
SpVoice Voice; Voice = new SpVoice(); Voice.Speak(richTextBox1.Text, SpeechVoiceSpeakFlags.SVSFDefault); } } }
PInvoke .NET jest nową platformą programistyczną. Minie jeszcze sporo czasu, zanim programiści przystosują swoje aplikacje do nowej platformy oraz obdarzą ją zaufaniem. Mimo iż .NET udostępnia dziesiątki klas umożliwiających łatwiejsze programowanie, w niektórych przypadkach nadal konieczne może okazać się wykorzystanie funkcji udostępnionych przez WinAPI. W wielu przypadkach użycie funkcji Win32 stanie się wręcz niezastąpione, tak więc w tej części rozdziału zajmiemy się wykorzystaniem bibliotek Win32 DLL w aplikacjach .NET. W tym celu będziemy korzystać z mechanizmu zwanego Platform Invocation Service, czyli Platform Invoke, zwanego w skrócie PInvoke (lub P/Invoke). Mechanizm ów pozwala na importowanie funkcji z bibliotek Win32 DLL za pomocą atrybutu [DllImport]. Dla przypomnienia powiem, iż system Windows udostępnia interfejs programistyczny zwany WinAPI (pisałem o tym w rozdziale 2.), który zawiera setki funkcji możliwych do wykorzystania w naszych aplikacjach. Funkcje te zawarte są w tzw. bibliotekach DLL. Biblioteki DLL to pliki z rozszerzeniem .dll, zawierające skompilowany kod. Nie są to jednak aplikacje wykonywalne (takie jak .exe).
Użycie funkcji Win32 API Aby skorzystać z funkcji Win32 API w kodzie zarządzanym, należy przede wszystkim utworzyć prototyp funkcji w kodzie zarządzanym. Dla przykładu pokażę sposób użycia funkcji GetUserName(), która znajduje się w bibliotece Advapi32.dll. Zwraca ona nazwę użytkownika zalogowanego w systemie. Aby utworzyć prototyp funkcji, należy wiedzieć, jak wygląda ich budowa w bibliotekach DLL. Windows był pisany w języku C, stąd też przykłady w dokumentacji WinAPI są zapisane w tym języku. Wiele funkcji zwracało rezultaty do parametru referencyjnego —
warto tu wspomnieć chociażby o funkcjach GetUserName czy GetComputerName. Ich deklaracja w C wyglądała tak: BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD nSize ); BOOL GetUserName( LPTSTR lpBuffer, LPDWORD nSize );
// address of name buffer // address of size of name buffer
// address of name buffer // address of size of name buffer
Dla przykładu pokażę, w jaki sposób pobrać nazwę użytkownika (funkcja GetUserName()) oraz ścieżkę do systemu Windows (funkcja GetWindowsDirectory()). Ich prototyp w kodzie zarządzanym wygląda tak: [DllImport("kernel32.dll")] static extern bool GetWindowsDirectory(StringBuilder lpBuffer, ref int nSize); [DllImport("Advapi32.dll")] static extern bool GetUserName(StringBuilder lpBuffer, ref int nSize);
Obie metody zostały opatrzone słowem kluczowym extern. Informuje ono kompilator, że metody będą ładowane z bibliotek zewnętrznych. Metody zostały opatrzone atrybutem DLLImport, określającym nazwę biblioteki, z której ładowane będą funkcje. Listing 11.6 zawiera program, który wykorzystuje funkcje Win32API. Listing 11.6. Przykład użycia funkcji Win32 API using System; using System.Text; using System.Runtime.InteropServices; namespace PInvoke { class Program { [DllImport("kernel32.dll")] static extern bool GetWindowsDirectory(StringBuilder lpBuffer, ref int nSize); [DllImport("Advapi32.dll")] static extern bool GetUserName(StringBuilder lpBuffer, ref int nSize); static void Main(string[] args) { StringBuilder Buffer = new StringBuilder(64);
int nSize = 64; GetWindowsDirectory(Buffer, ref nSize); Console.WriteLine("Ścieżka do katalogu Windowsa: {0}", Buffer.ToString()); GetUserName(Buffer, ref nSize); Console.WriteLine("Nazwa użytkownika: {0}", Buffer.ToString()); } } }
Interesujące nas dane (ścieżka do systemu Windows oraz nazwa użytkownika) zostaną przypisane do zmiennej Buffer typu StringBuilder. Aby wykorzystać funkcje Win32 API, należy znać ich budowę (parametry oraz typy). Tego możesz dowiedzieć się z dokumentacji firmy Microsoft, znajdującej się pod adresem http://msdn.microsoft.com.
Użycie atrybutu DLLImport Przed skorzystaniem z atrybutu DllImport trzeba dołączyć do programu przestrzeń nazw System.Runtime.InteropServices. Użycie tego atrybutu w najprostszym wydaniu prezentuje poprzedni przykład. Jego budowa jest dość prosta, gdyż w takim przypadku należy podać jedynie nazwę biblioteki DLL: [DllImport("Nazwa biblioteki DLL")];
Taki atrybut nie posiada żadnych dodatkowych parametrów. Normalnie jest możliwe określenie konwencji wywołania parametrów (cdecl, stdcall itp.), nazwy ładowanej procedury bądź funkcji oraz kodowania ciągów znakowych (Unicode, ANSI String). Parametry atrybutu DllImport można określać tak: [DllImport("SimpleDLL.dll", CallingConvention = CallingConvention.Stdcall, EntryPoint="About")]
W powyższym przykładzie sprecyzowałem sposób wywołania parametrów funkcji (parametr CallingConvention) oraz określiłem dokładnie nazwę importowanej funkcji (EntryPoint). Istnieje jeszcze jeden parametr używany tylko w przypadku, gdy w parametrze funkcji lub
procedury znajduje się ciąg znaków (String). Tym parametrem jest CharSet, który określa kodowanie:
Ansi — ciąg znakowy ANSI, Unicode — ciągi znakowe Unikodu, None — oznacza to samo co parametr Ansi.
W przypadku Win32 API wiele funkcji miało dwie odmiany — jedną z parametrem typu PAnsiChar, a drugą z PWideChar. Dla przypomnienia: wszystkie ciągi znakowe w .NET są zapisane w Unicode, także typ String. Wspomniałem tutaj o konwencji wywołania. Jest to zaawansowane zagadnienie związane z przekazywaniem wartości do parametrów funkcji, a konkretnie ich umieszczaniem w pamięci. Wyjaśnienie tych pojęć wykracza poza ramy niniejszej publikacji. Zasadniczo technika PInvoke ma o wiele większe zastosowanie. Ja pokazałem jedynie prosty przykład wywołania funkcji Win32 API, lecz ze względu na różnice pomiędzy systemem Win32 a .NET niekiedy użycie funkcji API może okazać się o wiele trudniejsze. Jeżeli jesteś zainteresowany technologią PInvoke, odsyłam do dokumentacji platformy .NET.
Podsumowanie Możliwość integracji poszczególnych podzespołów to wielka zaleta platformy .NET, dająca ogromne możliwości. Należy uświadomić sobie, że środowisko .NET Framework to nie tylko biblioteka klas czy biblioteka WinForms, ale również CLS, czyli wspólny język programowania. Dzięki temu nieważne jest, w jakim języku piszemy swoje aplikacje, ponieważ wygenerowany kod pośredni zawsze będzie taki sam, niezależnie od języka. Mam nadzieję, że po przeczytaniu niniejszego rozdziału masz pewną świadomość możliwości płynących ze współdzielenia podzespołów. [1] W tej części rozdziału terminem komponent .NET będę określał podzespół (ang. assembly), czyli zwykłą aplikację skompilowaną do kodu IL i posiadającą rozszerzenie .exe lub .dll.
Rozdział 12 Pliki i obsługa strumieni Czym jest plik
? Tego chyba nie trzeba wyjaśniać żadnemu użytkownikowi komputera. Istnieje kilka rodzajów plików: tekstowe, binarne, typowane itp. Pliki są wykorzystywane przez programy do pobierania lub przechowywania informacji. Mogą też zawierać binarne fragmenty programu. Ten rozdział będzie poświęcony plikom i ich obsłudze w C#. Zaczniemy od rzeczy najprostszych, przechodząc do coraz bardziej zaawansowanych aspektów. Biblioteka klas środowiska .NET Framework zawiera szereg klas umożliwiających obsługę plików oraz katalogów. Klasy te znajdują się w przestrzeni nazw System.IO. IO to potoczne określenie operacji wejścia-wyjścia (ang. input-output).
Czym są strumienie Strumienie są specjalną formą wymiany i transportu danych, obsługiwaną przez klasy przestrzeni System.IO. To określenie może nie jest zbyt precyzyjne, ale zaraz postaram się wyjaśnić to szczegółowo. Dzięki strumieniom można w prosty sposób operować na danych znajdujących się w pamięci komputera, w plikach itp. Przykładowo, strumień może być plikiem, pamięcią operacyjną lub współdzielonym zasobem sieciowym.
Klasy przestrzeni System.IO Do operowania na plikach i katalogach możemy wykorzystać klasy opisane w tabeli 12.1. Niektóre klasy zawierają metody statyczne, więc nie jest konieczne tworzenie ich instancji. Tabela 12.1. Podstawowe klasy operowania na plikach i katalogach Klasa Opis Udostępnia metody służące do operowania na katalogach (przenoszenie, Directory kopiowanie). Klasa udostępnia podstawowe mechanizmy pozwalające na tworzenie, File usuwanie oraz przenoszenie plików. Klasa służy do przetwarzania informacji o ścieżkach katalogów oraz Path plików. Ma działanie podobne do klasy Directory. Jeżeli dokonujemy wielu działań DirectoryInfo na katalogach, jest to optymalna klasa, gdyż jej metody nie wykonują tzw. testów bezpieczeństwa. Ma działanie podobne do klasy File. Jeżeli dokonujemy wielu działań na FileInfo plikach, jest to optymalna klasa, gdyż jej metody nie wykonują testów bezpieczeństwa. FileSystemInfo Klasa bazowa dla klas DirectoryInfo oraz FileInfo.
Operacje na katalogach Do operowania na katalogach wykorzystujemy klasę Directory lub DirectoryInfo. Directory może być wygodnym sposobem prostego operowania na katalogach, gdyż nie wymaga tworzenia egzemplarza klasy. Jeżeli jednak musimy wykonać wiele operacji na katalogach, wydajniejszym sposobem będzie skorzystanie z klasy DirectoryInfo.
Tworzenie i usuwanie katalogów Jeżeli chcemy utworzyć nowy katalog, należy skorzystać z metody CreateDirectory() z klasy Directory: if (!Directory.Exists("C:\\Foo")) { Directory.CreateDirectory("C:\\Foo"); }
Zwróć uwagę, że przed utworzeniem katalogu następuje sprawdzenie, czy przypadkiem on już nie istnieje (metoda Exists()). Nie jest to jednak konieczne, gdyż w przypadku gdy tworzony katalog istnieje na dysku, żaden wyjątek nie zostanie wygenerowany. Przy pomocy metody CreateDirectory() możemy utworzyć katalog, nawet wówczas gdy nie istnieje katalog macierzysty. Np.: Directory.CreateDirectory("C:\\Foo\\Bar");
Ścieżka C:\Foo\Bar zostanie utworzona, nawet gdy na dysku nie ma katalogu Foo. Pamiętaj o tym, aby w trakcie podawania ścieżek w łańcuchu korzystać z podwójnych backslashów (\\). Podobnie jak w językach C/C++, kompilator po znaku \ oczekuje symbolu specjalnego, takiego jak np. \n, który oznacza nową linię. Alternatywny kod korzystający z klasy DirectoryInfo, również tworzący nowy katalog, wygląda następująco: DirectoryInfo dInfo = new DirectoryInfo("C:\\Foo");
if (!dInfo.Exists) { dInfo.Create(); }
Jak widzisz, w konstruktorze klasy DirectoryInfo musimy podać ścieżkę do katalogu, na jakim będziemy operowali. Za tworzenie nowego folderu odpowiada metoda Create(), natomiast właściwość Exists zwraca wartość true, jeżeli ścieżka przekazana w konstruktorze istnieje. Jeżeli chcemy usunąć dany katalog, możemy skorzystać z metody Delete() klasy DirectoryInfo: if (dInfo.Exists) { dInfo.Delete(); }
Klasa DirectoryInfo udostępnia przeciążoną metodę Delete(). Jej druga wersja może posiadać parametr typu bool, który określa, czy katalog będzie usuwany nawet wówczas, gdy posiada inne pliki i foldery (wartość true).
Kopiowanie i przenoszenie Podczas tworzenia aplikacji przetwarzających z katalogami może zaistnieć konieczność skopiowania i (lub) przeniesienia pewnych folderów. Okazuje się, że klasy Directory i DirectoryInfo udostępniają tylko metody do przenoszenia katalogów. Zaprezentuję również technikę umożliwiającą kopiowanie całych katalogów, gdyż — nie wiedzieć czemu — takiej możliwości brakuje w bibliotece klas FCL. Przenoszenie katalogów jest proste. Klasa Directory udostępnia metodę Move(), która przyjmuje dwa parametry — katalog źródłowy oraz docelowy: Directory.Move("C:\\Foo", "C:\\Bar");
Przenoszenie jest proste. Należy jednak zastanowić się nad metodą kopiowania katalogów, którą trzeba napisać samodzielnie. Metoda, którą za chwilę zaprezentuję, wykorzystuje elementy nieomówione do tej pory — omówię je w dalszej części książki. Metoda, która realizuje kopiowanie katalogu, przedstawiona została poniżej:
private void CopyDir(string SourceDir, string TargetDir) { string[] Files; FileAttributes Attr; string srcFile; // sprawdzenie, czy na końcu ścieżki znajduje się separator \ if (!SourceDir.EndsWith(Path.DirectorySeparatorChar.ToString())) { SourceDir += Path.DirectorySeparatorChar; } if (!TargetDir.EndsWith(Path.DirectorySeparatorChar.ToString())) { TargetDir += Path.DirectorySeparatorChar; } // pobranie listy plików z katalogu Files = Directory.GetFileSystemEntries(SourceDir); // utworzenie katalogu docelowego Directory.CreateDirectory(TargetDir); for (int i = 0; i < Files.Length; i++) { // pobranie atrybutu pliku Attr = File.GetAttributes(Files[i]); // pobranie nazwy pliku srcFile = Path.GetFileName(Files[i]); // warunek sprawdza, czy plik jest katalogiem if (FileAttributes.Directory == Attr) { if (!Directory.Exists(TargetDir + srcFile)) { Directory.CreateDirectory(TargetDir + srcFile); } // wywołanie rekurencyjne CopyDir(Files[i], TargetDir + srcFile); } else { // skopiowanie pliku z katalogu File.Copy(Files[i], TargetDir + srcFile); } } }
Prywatna metoda ma dwa parametry — ścieżkę katalogu źródłowego oraz docelowego. Pierwsze dwie instrukcje warunkowe sprawdzają, czy na końcu ścieżki znajduje się znak ukośnika (\ w systemie Windows lub / w Linuksie). W kolejnych wierszach kodu pobieramy listę katalogów i plików znajdujących się w danym folderze. Dalej w pętli są pobierane atrybuty pliku, gdyż trzeba sprawdzić, czy kopiowany plik nie jest katalogiem. Jeżeli jest, stosujemy rekurencję i kopiujemy zawartość podkatalogu. Jeżeli nie — po prostu kopiujemy plik z katalogu źródłowego do docelowego. Metoda rekurencyjna to taka, która wywołuje samą siebie. Wykorzystanie takiej metody jest bardzo proste. Możemy pobrać od użytkownika ścieżki katalogu źródłowego oraz docelowego i wywołać metodę: CopyDir(edtSrc.Text, edtDst.Text);
Odczytywanie informacji o katalogu Klasa DirectoryInfo posiada kilka użytecznych metod służących do odczytu informacji o katalogach. Opis najważniejszych właściwości znajduje się w tabeli 12.2. Tabela 12.2. Właściwości klasy DirectoryInfo Właściwość Opis Attributes Atrybuty katalogu CreationTime Czas utworzenia katalogu Czas utworzenia katalogu w formacie UTC (ang. Coordinal Universal CreationTimeUtc Time — UTC) FullName Pełna ścieżka do katalogu LastAccessTime Czas ostatniego dostępu do katalogu LastAccessTimeUtc Czas ostatniego dostępu do katalogu w formacie UTC LastWriteTime Czas ostatniego zapisu do katalogu LastWriteTimeUtc Czas ostatniego zapisu do katalogu Name Zwraca nazwę katalogu FullName Zwraca pełną ścieżkę do katalogu Parent Zwraca macierzysty katalog
Listing 12.1 prezentuje przykład odczytu informacji o katalogu, w którym znajduje się uruchamiany program (rysunek 12.1). Listing 12.1. Przykład odczytu informacji o katalogu
using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms; System.IO;
namespace WindowsApplication1 { public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void btnLoad_Click(object sender, EventArgs e) { DirectoryInfo dirInfo = new DirectoryInfo(Application.StartupPath); lbDirectory.Items.Add( "Pełna ścieżka: " + dirInfo.FullName); lbDirectory.Items.Add( "Katalog macierzysty: " + dirInfo.Parent); lbDirectory.Items.Add( "Data utworzenia: " + dirInfo.CreationTime.ToShortDateString()); lbDirectory.Items.Add( "Data ostatniego dostępu: " + dirInfo.LastAccessTime.ToShortDateString()); lbDirectory.Items.Add( "Data zapisu: " + dirInfo.LastWriteTime.ToShortDateString()); lbDirectory.Items.Add( "Atrybuty: " + dirInfo.Attributes.ToString()); } } }
Rysunek 12.1. Przykład odczytu informacji o katalogu Metody zwracające czas ostatniej modyfikacji katalogu (ewentualnie dostępu) zwracają informacje o dacie i czasie, w formacie DateTime. Ta klasa posiada metodę ToShortDateString(), która służy do łańcuchowego prezentowania daty w krótkim formacie.
Obsługa plików Tak jak omówione w poprzednim podrozdziale klasy Directory i DirectoryInfo służą do obsługi katalogów, tak File oraz FileInfo służą do obsługi plików. Z klasy File skorzystałem już wcześniej, podczas prezentowania sposobu na skopiowanie katalogu. Teraz chciałbym skupić się na podstawowych operacjach, jakich dokonujemy na plikach.
Tworzenie i usuwanie plików Chyba najprostszym sposobem na utworzenie nowego pliku jest użycie metody CreateText() z klasy File: if (!File.Exists("C:\\foo.txt")) { File.CreateText("C:\\foo.txt"); }
Tworzy ona nowy plik gotowy do zapisu tekstu z kodowaniem UTF-8. Oczywiście
parametrem musi być ścieżka do tworzonego pliku. Metoda Exists() zwraca informację, czy plik, którego ścieżka przekazana jest w parametrze, istnieje (wówczas zwraca wartość true). Alternatywnym rozwiązaniem jest użycie klasy FileInfo: FileInfo F = new FileInfo("C:\\foo.txt"); if (!F.Exists) { F.CreateText(); }
Oczywiście utworzony w ten sposób plik będzie pusty. Ani klasa File, ani FileInfo nie oferuje metod służących do zapisu lub odczytu danych z pliku. W tym celu można skorzystać z klasy StreamWriter, której obiekt jest zwracany przez metodę CreateText(): if (!File.Exists("C:\\foo.txt")) { StreamWriter sw = File.CreateText("C:\\foo.txt"); sw.WriteLine("Hello World!"); sw.Close(); }
W tym przykładzie metoda WriteLine() zapisuje nową linię do pliku tekstowego. Taki plik należy koniecznie zamknąć, korzystając z metody Close(). Usuwanie plików jest równie proste jak usuwanie katalogów. Służy do tego metoda Delete(): File.Delete("C:\\foo.txt");
Wersja klasy FileInfo: FileInfo F = new FileInfo("C:\\foo.txt"); F.Delete();
Kopiowanie i przenoszenie plików
Poprzednio w rozdziale używałem już metody Copy() z klasy File. Oczywiście, jak się domyślasz, realizuje ona kopiowanie pliku. W parametrach metody należy podać ścieżkę źródłową oraz docelową: string srcPath = "C:\\foo.txt"; string dstPath = "C:\\bar.txt"; if (!File.Exists(dstPath)) { File.Copy(srcPath, dstPath); }
Przenoszenie realizuje metoda Move(): string srcPath = "C:\\foo.txt"; string dstPath = "C:\\bar.txt"; if (!File.Exists(dstPath)) { File.Move(srcPath, dstPath); }
Dobrą praktyką jest sprawdzanie (przy pomocy metody Exists()), czy pod ścieżką docelową nie znajduje się jakiś plik.
Odczytywanie informacji o pliku Podobnie jak klasa DirectoryInfo udostępnia informacje o katalogu, do odczytania informacji o plikach możemy użyć klasy FileInfo. Kilka z nich omówiłem w tabeli 12.3. Tabela 12.3. Właściwości klasy FileInfo Właściwość Opis Name Nazwa pliku Length Rozmiar pliku (ilość bajtów) DirectoryName Nazwa katalogu, w którym znajduje się plik Extension Rozszerzenie pliku Attributes Atrybuty pliku
CreationTime Czas utworzenia pliku LastAccessTime Czas ostatniego dostępu do pliku LastWriteTime Czas zapisu do pliku
Rysunek 12.2 prezentuje aplikację, która odczytuje informacje o pliku i dodaje je kolejno do komponentu typu ListBox.
Rysunek 12.2. Odczytywanie informacji o pliku Listing 12.2 zawiera kod źródłowy programu zaprezentowanego na rysunku 12.2. Listing 12.2. Analizowanie informacji o pliku using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.IO; namespace WinForms { public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { if (!File.Exists("C:\\Foo.txt"))
{ File.CreateText("C:\\Foo.txt"); } FileInfo F = new FileInfo("C:\\Foo.txt"); lbFile.Items.Add( "Nazwa pliku: " + F.Name); lbFile.Items.Add( "Rozmiar pliku: " + F.Length); lbFile.Items.Add( "Nazwa katalogu: " + F.DirectoryName); lbFile.Items.Add( "Rozszerzenie: " + F.Extension); lbFile.Items.Add( "Atrybuty: " + F.Attributes.ToString()); lbFile.Items.Add( "Czas utworzenia pliku: " + F.CreationTime.ToShortDateString()); lbFile.Items.Add( "Czas dostępu: " + F.LastAccessTime.ToShortDateString()); lbFile.Items.Add( "Czas ostatniej modyfikacji: " + F.LastWriteTime.ToShortDateString()); File.Delete("C:\\Foo.txt"); } } }
Strumienie O strumieniach możesz myśleć jako o ciągach danych. W środowisku .NET Framework dostępnych jest wiele klas obsługujących strumienie, w zależności od medium przechowującego te dane. I tak do obsługi strumieni plików wykorzystujemy klasę FileStream, natomiast klasa MemoryStream reprezentuje strumienie znajdujące się w pamięci operacyjnej. Pomimo tych różnic wykorzystywanie tych klas jest niemalże identyczne. Wspomniałem o klasach reprezentujących dane. Jednakże do odczytywania i zapisywania danych do strumieni używamy odrębnych klas — StreamReader oraz StreamWriter. W przypadku danych binarnych są to odpowiednio klasy BinaryWriter i BinaryReader. Możliwości stosowania tych klas w praktyce zostaną zilustrowane przykładami w kolejnych punktach.
Obsługa plików tekstowych Podstawowe operacje na plikach tekstowych, jakie wykonujemy, programując, to: zapisywanie, odczytywanie oraz — ewentualnie — przemieszczanie się w pliku tekstowym. Zacznijmy od utworzenia egzemplarza klasy FileStream. Klasa posiada wiele przeciążonych konstruktorów. Jeden z nich wymaga podania trzech parametrów:
ścieżki do pliku, trybu otwarcia pliku, trybu dostępu do pliku.
Typowe utworzenie nowego egzemplarza może wyglądać tak: FileStream fs = new FileStream("C:\\Foo.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);
W drugim i trzecim parametrze określony został tryb otwarcia oraz dostępu do pliku. Są to typy wyliczeniowe, których wartości opisałem w tabeli 12.4 oraz 12.5. Tabela 12.4. Wartości typu wyliczeniowego FileMode Wartość Opis Tworzy lub otwiera plik i przechodzi na jego koniec. Wymaga utworzenia Append obiektu z parametrem FileAccess.Write. Tworzy plik, a w razie gdy on już istnieje, zastępuje jego dotychczasową Create zawartość. CreateNew Tworzy plik, a w razie gdy on już istnieje, generuje odpowiedni wyjątek. Otwiera nowy plik do odczytu. Jeżeli plik nie istnieje, generowany jest Open wyjątek. OpenOrCreate Otwiera plik, a jeżeli ten nie istnieje — tworzy nowy. Truncate Otwiera plik i czyści jego zawartość.
Tabela 12.5. Wartości typu wyliczeniowego FileAccess Wartość Opis Read Dane mogą być jedynie odczytywane. Write Dane mogą być tylko zapisywane. ReadWrite Dane mogą być zarówno zapisywane, jak i odczytywane.
Powyższy kod tworzący obiekt klasy FileStream próbuje otworzyć plik. Jeżeli ten nie istnieje — tworzy nowy. Plik jest otwierany zarówno do odczytu, jak i do zapisu. Aby zapisać wartość w pliku tekstowym, należy utworzyć również egzemplarz klasy StreamWriter. W parametrze konstruktora tej klasy należy przekazać obiekt klasy FileStream (patrz listing 12.3). Listing 12.3. Przykład tworzenia oraz zapisania wartości do pliku using System; using System.IO; namespace ConsoleApp { class Program { static void Main(string[] args) { FileStream fs = new FileStream("C:\\Foo.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite); try { StreamWriter sw = new StreamWriter(fs); sw.WriteLine("Hello World!"); sw.WriteLine("Bye!"); sw.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } } }
Podobnie jak metoda WriteLine() z klasy Console wyświetla nową linię w oknie konsoli, tak metoda WriteLine() z klasy StreamWriter zapisuje nową linię w pliku tekstowym. Należy wspomnieć o tym, że wywołanie metody WriteLine() nie równa się bezpośredniemu zapisaniu do pliku, lecz do bufora pamięci. Należy pamiętać o wywołaniu metody Close() lub Flush(), która czyści bufor. Aby odczytać zawartość pliku tekstowego, możemy skorzystać z klasy StreamReader. Jej użycie jest podobne jak w przypadku wcześniej wspominanej klasy StreamWriter. Listing 12.4 prezentuje przykład załadowania do komponentu typu RichtextBox zawartości pliku tekstowego wskazanego przez użytkownika. Listing 12.4. Przykład załadowania zawartości pliku
using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms; System.IO;
namespace WinForms { public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void MainForm_Load(object sender, EventArgs e) { openDialog.ShowDialog(); FileStream fs = new FileStream(openDialog.FileName, FileMode.Open, FileAccess.Read); try { StreamReader sr = new StreamReader(fs); richText.Text = sr.ReadToEnd(); sr.Close(); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } } }
Do operacji odczytu zawartości pliku służy metoda ReadToEnd(). Odczytuje ona zawartość całego pliku, zwracając ją w formie łańcucha string. Tak więc przy większych plikach użycie tej metody może wpłynąć na zawartość pamięci, jaka jest używana przez naszą aplikację. W przypadku większych plików można skorzystać z metody ReadLine(), która odczytuje kolejne linie pliku tekstowego. Tę metodę należy odtwarzać w pętli: while (!sr.EndOfStream) {
richText.Text += sr.ReadLine(); }
Właściwość EndOfStream zwraca true, jeżeli napotkano koniec pliku. W przypadku strumieni istnieje pojęcie bieżącej pozycji wewnątrz reprezentowanego bloku danych, z której dane są odczytywane lub na której są zapisywane. Może to być albo początek strumienia, albo koniec, albo wybrany punkt pomiędzy jego początkiem a końcem. Do przesuwania wewnętrznej pozycji strumienia służy metoda Seek(), w której możemy podać ilość bajtów, o jaką zostanie przesunięty wskaźnik: sr.BaseStream.Seek(10, SeekOrigin.Begin);
Pozycja odczytu-zapisu w strumieniu jest reprezentowana za pomocą wartości typu wyliczeniowego SeekOrigin (patrz tabela 12.6). Tabela 12.6. Wartości typu wyliczeniowego SeekOrigin Wartość Opis Begin Reprezentuje początek strumienia. Current Reprezentuje bieżącą pozycję w strumieniu. End Reprezentuje koniec strumienia.
Czyli np. wywołanie: se.BaseStream.Seek(0, SeekOrigin.End);
oznacza ustawienie wskaźnika na końcu pliku. Rozwiązaniem alternatywnym do zaprezentowanej przed chwilą właściwości EndOfStream jest metoda Peek(). Zwraca ona numer kolejnego znaku (typ int), ale nie zmienia dotychczasowego położenia wskaźnika. Jeżeli wartość zwrócona przez tę metodę jest równa 1, oznacza to, że mamy do czynienia z końcem pliku: while (sr.Peek() != -1) { // kod }
Klasa StreamReader udostępnia również metodę ReadBlock(), która umożliwia odczytanie konkretnego fragmentu pliku. Pierwszy parametr musi wskazywać na zmienną tablicową (typu char), do której zostanie przypisana odczytana wartość. Kolejny parametr oznacza wartość początkową, od której rozpocznie się kopiowanie znaków, a ostatni — ilość znaków do skopiowania. Np.:
char[] buff = new char[100]; sr.ReadBlock(buff, 0, 100);
Operacje na danych binarnych Praca z danymi binarnymi jest bardzo podobna do pracy ze zwykłymi plikami tekstowymi. Różnica jest taka, iż w tych drugich do odczytu oraz zapisu stosujemy klasy BinaryRead oraz BinaryWrite. Przy pomocy klasy BinaryWrite możemy zapisać do pliku dane wielu rodzajów, począwszy od łańcuchów tekstowych, na liczbach i tablicach typu char skończywszy. Wszystko dzięki przeciążonej metodzie Write(). Oto przykład: char[] MyChar = { 'H', 'e', 'l', 'l', 'o' }; FileStream fs = new FileStream("C:\\Data.dat", FileMode.OpenOrCreate, FileAccess.ReadWrite); BinaryWriter bw = new BinaryWriter(fs); bw.Write("Hello World!"); bw.Write(12.23); bw.Write(true); bw.Write(MyChar); bw.Close();
Konstruktor klasy BinaryWriter jako parametru również wymaga obiektu klasy FileStream. Do odczytu danych z pliku binarnego musimy użyć odpowiednich metod, których nazwa tworzona jest ze słów Read oraz typu danych. Np. za odczyt danych typu string odpowiada metoda ReadString(). Poniżej zaprezentowałem kod odpowiadający za załadowanie i wyświetlenie danych binarnych: FileStream fs = new FileStream("C:\\Data.dat", FileMode.OpenOrCreate, FileAccess.ReadWrite); BinaryReader bw = new BinaryReader(fs); Console.WriteLine("String: {0}", bw.ReadString()); Console.WriteLine("Double: {0}", bw.ReadDouble());
Console.WriteLine("Boolean: {0}", bw.ReadBoolean()); Console.WriteLine("Char array: {0}", bw.ReadChars(5)); Console.Read();
Metoda ReadChars(), w odróżnieniu od pozostałych, jako parametru wymaga ilości znaków, które zostaną odczytane.
Serializacja Serializacja jest procesem umożliwiającym zapisywanie informacji o obiektach klas w plikach tekstowych lub binarnych. Ściślej mówiąc, jest procesem przekształcania klas w strumień bajtów, który może być utrwalany na nośniku danych, przesyłany do innego procesu lub nawet na inny komputer. Procesem odwrotnym jest deserializacja (ang. deserialization), która polega na odczytaniu strumienia bajtów z pliku lub zdalnego komputera i zrekonstruowaniu zawartej w nim klasy włącznie z jej zapisanym stanem. Dzięki serializacji możemy w każdej chwili zapisać stan, w jakim znajduje się obiekt, wraz z wartościami jego elementów. Kiedy obiekt jest serializowany, środowisko uruchomieniowe CLR wewnętrznie buduje graf obiektów. Umożliwia on temu środowisku obsługę utrwalanych obiektów wraz ze wszystkimi obiektami, które są z nimi powiązane. Seralizowana klasa musi być opatrzona atrybutem [Serializable]. Serializowane są wszystkie elementy klasy, pod warunkiem że nie zostały opatrzone atrybutem [NonSerialized]: [Serializable] class Foo { int i; string s; [NonSerialized] double d; }
Formaty zapisu danych Platforma .NET Framework udostępnia dwa formaty zapisu serializowanych danych — binarny i SOAP. Należy pamiętać, że platforma .NET Framework nie ogranicza liczby tych formatów — w razie potrzeby możemy więc tworzyć własne formaty. Aby zapisać klasy w formacie binarnym, musimy użyć klasy BinaryFormatter, należącej do przestrzeni nazw
System.Runtime.Serialization.Formatters.Binary. Użycie formatu SOAP wymaga wykorzystania klasy SoapFormatter, należącej do przestrzeni nazw System.Runtime.Serialization.Formatters.Soap.
SOAP — ang. Simple Object Access Protocol — jest językiem opartym na XML, wykorzystywanym przez usługi sieciowe do wywoływania procedur. Protokół SOAP określa format przesyłanych danych, nazwy parametrów itp. Język XML zostanie omówiony w kolejnym rozdziale książki. Klasa SoapFormatter nie jest zalecana jako format przechowywanych danych. W wersji 2.0 biblioteki klas FCL ta klasa może być nieobecna.
Przykład serializacji Listing 12.5 zawiera przykładowy kod źródłowy, który dokonuje procesu serializacji, a następnie deserializacji danych. Serializowane dane są zapisywane na dysku C: pod nazwą data.dat. Listing 12.5. Przykład serializacji using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace ConsoleApp { public enum GenreEnum { Fish, Mammal }; [Serializable] public struct Animal { public string Name; public int Age; public GenreEnum Genre; } class Program { static void Main(string[] args) { // deklaracja tablicy struktur Animal[] MyPet = new Animal[2]; // wypełnienie tablicy struktur MyPet[0].Name = "Pies"; MyPet[0].Age = 10; MyPet[0].Genre = GenreEnum. Mammal;
MyPet[1].Name = "Ryba"; MyPet[1].Age = 1; MyPet[1].Genre = GenreEnum.Fish; FileStream MyStream; // utworzenie pliku, który będzie zawierał strumienie danych MyStream = new FileStream("C:\\data.dat", FileMode.Create);
BinaryFormatter MyFormatter = new BinaryFormatter(); // próba serializacji MyFormatter.Serialize(MyStream, MyPet); // zamknięcie strumienia MyStream.Close(); // ponowne otwarcie strumienia MyStream = new FileStream("C:\\data.dat", FileMode.Open); // deserializacja MyPet = (Animal[])MyFormatter.Deserialize(MyStream); foreach (Animal Pet in MyPet) { Console.WriteLine("{0} {1} {2}", Pet.Name, Pet.Age, Pet.Genre ); } Console.Read(); } } }
Serializowanymi danymi jest struktura Animal. Struktura służy do przechowywania informacji o zwierzętach. W programie zadeklarowałem dwuelementową tablicę typu Animal, którą wypełniam danymi. Następnie tworzona jest instancja klasy FileStream oraz BinaryFormatter. Serializowania dokonuje metoda Serialize(). W tym samym programie dokonujemy również deserializacji przy pomocy metody Deserialize(). Ponieważ metoda ta zwraca dane typu object, konieczne jest rzutowanie typów na tablicę Animal, którą wyświetlamy w oknie konsoli.
Podsumowanie Obsługa plików we własnej aplikacji wciąż, z uwagi na większą popularność systemów bazodanowych, jest rzadziej wykorzystywana. Do przechowywania danych dotyczących aplikacji używa się baz danych czy plików XML (będzie o nich mowa w kolejnym rozdziale). Strumienie są jednak wykorzystywane dosyć często w bibliotece klas FCL w wielu klasach, dlatego warto znać choćby ich podstawową obsługę. Niekiedy może się również nadarzyć okazja do użycia plików w formie bazy danych — do przechowywania informacji. Wówczas możesz powrócić do tego rozdziału i przypomnieć sobie sposoby obsługi różnych typów strumieni.
Rozdział 13 Obsługa formatu XML XML jest stosunkowo nową technologią zaproponowaną przez największe firmy informatyczne (Microsoft i Borland) jako nowy standard przechowywania i wymiany informacji. XML jest skrótem od słów eXtensible Markup Language, co oznacza rozszerzalny język znaczników. Został opracowany przez W3C (ang. World Wide Web Consorcium). HTML (ang. HyperText Markup Language) jest językiem znaczników opisującym strukturę dokumentu WWW. Projektant, aby utworzyć stronę WWW, musi zapisać pewne z góry określone polecenia, tak aby były rozpoznawane przez przeglądarkę oraz odpowiednio interpretowane. Dzięki temu, korzystając z przeglądarek, możemy obserwować dokumenty pełne odnośników, obrazków oraz animacji. XML jest nieco inny — tutaj to programista ustala nazwy znaczników oraz całą strukturę dokumentów. Dzięki temu tworzy unikalny format dokumentów, który może być odczytywany za pomocą dowolnego edytora tekstu. XML jest często mylony z bazą danych. Należy jednak podkreślić, że XML — chociaż działa podobnie — nie jest bazą danych. W przypadku baz danych każda platforma (MySQL, PostgreSQL, Oracle) posiada własną strukturę danych i własne formaty plików, w których są gromadzone informacje. Co prawda uniwersalny jest tutaj język zapytań — SQL, za pomocą którego komunikujemy się z bazą danych, ale sam zapis i odczyt informacji z plików zapewnia aplikacja — np. MySQL. Plik XML jest natomiast zwykłym plikiem tekstowym, dzięki czemu każdy może modyfikować jego zawartość. Obecnie istnieje wiele tzw. parserów, które pozwalają na prostą modyfikację plików XML. Służą one do odczytywania struktury takiego pliku, dzięki czemu odciążają programistę od
mozolnego procesu edycji kodu XML. XML nie został stworzony przez żadną firmę, lecz przez niezależne konsorcjum (W3C również utworzyło standard HTML), co gwarantuje mu niezależność oraz brak praw patentowych. Popularność XML ciągle rośnie, a dowodem tego są nowe standardy, które go wykorzystują (np. SOAP).
Niezależność XML XML jest niezależny od platformy, języka programowania i sprzętu (PC, Palmtop), jest więc doskonałym i łatwym formatem do przechowywania danych. Weźmy jako przykład notowania spółek giełdowych przedstawiane na różnych portalach. Notowania te są zapisane na stronie w formie tabel HTML, tak więc jedynym problemem w tym momencie jest odpowiednia edycja kodu HTML, tak aby pozyskać z niego potrzebne dane. Wystarczy jednak mała zmiana w tym kodzie, aby wykorzystane narzędzie przestało poprawnie funkcjonować. Jeżeli takie dane byłyby przedstawiane jedynie w formie XML, znika problem niekompatybilności. Wynika to z tego, że XML prezentuje jedynie treść, a nie formę — jak ma to miejsce w HTML.
XHTML XHTML jest nowym standardem, opracowanym także przez W3C, który w zamierzeniach ma zastąpić HTML i stanowić pewien pośrednik pomiędzy językiem HTML a XML. Rozwój HTML został wstrzymany i teraz do projektowania stron WWW zaleca się używanie XHTML. XHTML jest bardziej uporządkowany niż HTML. Usunięto stąd wiele znaczników HTML, stawiając na rozwój arkuszy stylów CSS. Innymi słowy, projektant powinien zrezygnować z wielu znaczników HTML na rzecz CSS.
Budowa dokumentu W tym podrozdziale mam zamiar opisać, jak wygląda budowa plików XML i jakie są ich wymagane elementy. Jako edytora można tutaj użyć dowolnego programu pozwalającego na zapisywanie tekstu, ja będę korzystał z edytora Visual C# Express Edition.
Jeżeli masz otwarty nowy projekt, z menu Project wybierz Add New Item. W oknie wybierz pozycję XML File i naciśnij Add. Do projektu dodany zostanie nowy plik, którego zawartość wygląda tak:
Taki dokument można zapisać pod dowolną nazwą z rozszerzeniem *.xml. Pliki XML otwiera się w przeglądarce — np. Internet Explorer — aby sprawdzić poprawność dokumentu. Jeśli dokument jest poprawny, Internet Explorer powinien wyświetlić jego treść, w przeciwnym razie wskaże błąd w dokumencie. Należy w tym momencie zaznaczyć, że XML nie jest tak elastyczny jak HTML. Przeglądarki obecnie są w stanie zaakceptować nieprawidłowy zapis kodu HTML i mimo błędu poprawnie zinterpretować dany znacznik. Najmniejszy błąd w XML spowoduje, że przeglądarka wyświetli błędy w dokumencie.
Prolog Specyficzna instrukcja dodana automatycznie przez środowisko Visual C# Express Edition do dokumentu to tzw. prolog. Jest to opcjonalna informacja dla parsera, informująca o typie dokumentu oraz kodowaniu. Powinno się przykładać dużą wagę do owego nagłówka i zawsze stosować go w swoich dokumentach. Standardowa budowa prologu jest następująca:
Podstawowym warunkiem jest umieszczenie go między znacznikami . Opis poszczególnych parametrów prologu znajduje się w tabeli 13.1. Tabela 13.1. Elementy składowe prologu
Podstawowe instrukcje używane przy deklaracji nagłówka XML. Wersja specyfikacji XML. Obecnie jedyną wersją owej specyfikacji jest version="1.0" 1.0. Ważny nagłówek, mówiący o kodowaniu dokumentu. Zalecam używanie kodowania Unicode (UTF-8), gdyż zapewnia on poprawne wyświetlanie encoding="UTF-8" większości znaków. Godnym polecenia jest również standard ISO-88592. Dodatkowa wartość określająca, czy dokument zawiera jakieś odwołania standalone="yes" do innych plików (yes).
Znaczniki Budowa dokumentów HTML, XHTML oraz XML opiera się na znacznikach, zwanych także tagami. W przypadku HTML oraz XHTML znaczniki muszą mieć z góry określoną nazwę. W przypadku XML nie ma takich restrykcji, tak więc nazwa danego znacznika zależy już od programisty. Listing 13.1 przedstawia przykładową zawartość pliku XML. Listing 13.1. Przykładowa zawartość pliku XML
Hawajska 14,50
Peperoni 15,50
Taką zawartość możesz wkleić do swojego edytora i zapisać. Po otwarciu takiego pliku w przeglądarce Internet Explorer powinien zostać wyświetlony obraz taki jak na rysunku 13.1.
Rysunek 13.1. Zawartość dokumentu XML wyświetlona przez Internet Explorer Jeśli wpisany kod będzie niepoprawny, Internet Explorer wyświetli informację o tym, w której linii wystąpił błąd.
Budowa znacznikowa Znaczniki są elementami o określonej nazwie, umieszczonymi pomiędzy nawiasami (< oraz >). Wyróżniamy dwa rodzaje znaczników — otwierający oraz zamykający. W takim przypadku w znaczniku zamykającym musi się znaleźć znak /:
Pomiędzy znacznikami można umieścić określoną wartość, pewne dane. Istnieje kilka ważnych reguł, jak np. nazewnictwo. Nazwy znacznika otwierającego oraz zamykającego muszą być identyczne. Rozróżniana jest także wielkość znaków. Tak więc poniższy zapis spowoduje zgłoszenie błędu: dane
Podczas edycji znaczników XML należy także zwrócić uwagę na ich nazwy — nie mogą się zaczynać od cyfr oraz nie mogą zawierać znaków $#%&*(/. Przykładowo, poniższa nazwa znacznika nie jest poprawna: dane
Znacznik otwierający i zamykający oraz informacja umieszczona między nimi to węzeł.
Elementy HTML Format XML jest stworzony jedynie do przechowywania informacji, tak więc upiększające tekst znaczniki HTML nie mają dla XML znaczenia. Przykładowo, znacznik służył w HTML do pogrubiania tekstu. W XML taki znacznik będzie zwykłym tagiem, niewpływającym na sposób wyświetlania tekstu: Pizza Peperoni jest dobra
Znaczniki zagnieżdżone W związku z powyższym przykładem chciałbym wspomnieć o znacznikach zagnieżdżonych. Element będzie głównym znacznikiem zawierającym tekst Pizza jest dobra. Natomiast w powyższym przypadku znacznikiem zagnieżdżonym będzie , zawierający tekst Peperoni.
Znaczniki zagnieżdżone są podstawowym element projektowania dokumentów XML — ten mechanizm wykorzystałem również na listingu 13.1. W tamtym przypadku głównym znacznikiem jest . Znacznik ten zawiera z kolei dwa elementy . W elementach znalazły się znaczniki określające nazwę pizzy oraz cenę.
Znaki specjalne Podczas wpisywania informacji pomiędzy znacznikami nie można używać znaków < oraz >. Aby umieścić te znaki pomiędzy tagami, należy zastosować pewien trik, polegający na zastąpieniu ich innymi wartościami (tabela 13.2). Tabela 13.2. Znaki specjalne XML Znak specjalny Należy zastąpić < > & “ ’
< > & " '
Po małych zabiegach taki kod XML będzie prawidłowy: Pizza jest dobra
I co najważniejsze — znaki < oraz > zostaną poprawnie wyświetlone.
Atrybuty Każdy element może dodatkowo wykorzystywać atrybuty przybierające określone wartości: Peperoni
W tym przykładzie pizza Peperoni posiada atrybut id o wartości 101. Atrybuty należy wpisywać między apostrofami lub w cudzysłowie. W danym elemencie można umieścić dowolną liczbę parametrów. Tak samo jak w przypadku nazw elementów, nazwy atrybutów nie mogą zawierać niedozwolonych znaków (!@#^&*), nawiasów itp.
Znaczniki puste Istnieje możliwość deklarowania elementów w sposób specyficzny — uwzględniając jedynie atrybuty, bez zamykania danego znacznika. W takim przypadku na końcu nazwy elementu musi znaleźć się znak /.
Powyższy element posiada atrybuty id oraz nazwa_pizzy i jest znacznikiem pustym.
Podstawowa terminologia Czasami wyrazy tag, znacznik, element czy węzeł są używane zamiennie, gdyż człowiek jest w stanie domyślić się ich znaczenia. Pragnę jednak sprecyzować pewne pojęcia. Znacznik inaczej możemy nazywać tagiem — jest to specjalnie sformatowany ciąg znaków, np.: