You are on page 1of 382

Spis treści

Co znajdziesz w tej książce? ............................................................. 9


Rozdział 1. Podstawy środowiska Visual C++ 2012 Professional ....................... 11
Opis środowiska ............................................................................................................. 11
Język C++ a .NET Framework ....................................................................................... 12
Pobieranie i instalacja środowiska .................................................................................. 12
Kilka pojęć na początek .................................................................................................. 14
Zmienne ................................................................................................................... 14
Funkcja ..................................................................................................................... 14
Klasy ........................................................................................................................ 15
Przestrzenie nazw ..................................................................................................... 16
Z czego składa się aplikacja Windows ........................................................................... 16
Główne okno VC++ 2012 RC ........................................................................................ 17
Zaginiony projekt ........................................................................................................... 18
Tworzenie projektu nowej aplikacji w VC++ 2012 ........................................................ 19
Wygląd środowiska w trybie budowy aplikacji .............................................................. 22
Struktura projektu ........................................................................................................... 24
Efektywna praca w środowisku ...................................................................................... 25
Rozdział 2. Struktura programów C++ i C++/CLI .............................................. 29
Programy korzystające z konsoli w VC++ 2012 ............................................................ 29
Ogólna postać programu pisanego w C++ ...................................................................... 29
Dyrektywy ...................................................................................................................... 31
Dyrektywa #include ................................................................................................. 31
Dyrektywa #define ................................................................................................... 33
Dyrektywa #ifdef — kompilacja warunkowa ........................................................... 34
Typy zmiennych ............................................................................................................. 37
Zmienne typu int ...................................................................................................... 37
Zmienne typu float ................................................................................................... 38
Typ double ............................................................................................................... 38
Typ char ................................................................................................................... 38
Modyfikatory typów ................................................................................................. 38
Rzutowanie (konwersja) typów ...................................................................................... 39
Rzutowanie static_cast ............................................................................................. 39
Rzutowanie const_cast ............................................................................................. 40
Rzutowanie safe_cast ............................................................................................... 41
Rzutowanie dynamic_cast ........................................................................................ 41
4 Microsoft Visual C++ 2012. Praktyczne przykłady

Typ wyliczeniowy .......................................................................................................... 41


Silnie typowane wyliczenia ...................................................................................... 41
Słowo kluczowe auto, czyli dedukcja typu ..................................................................... 45
L-wartości i R-wartości .................................................................................................. 46
Operatory ........................................................................................................................ 46
Zapis danych do plików i odczyt z nich za pomocą operatorów << i >> ................. 48
Wskaźniki i referencje .................................................................................................... 50
Wskaźniki ................................................................................................................. 50
Referencje ................................................................................................................ 50
Referencje do r-wartości .......................................................................................... 51
Wskaźniki do stałej i rzutowanie const_cast ............................................................ 51
Tablice ............................................................................................................................ 52
Operatory new i delete .................................................................................................... 55
Instrukcje ........................................................................................................................ 55
Instrukcje warunkowe .............................................................................................. 56
Instrukcje iteracji ...................................................................................................... 57
Rozdział 3. Funkcje ......................................................................................... 59
Tradycyjny zapis funkcji ................................................................................................ 59
Przeciążanie funkcji ........................................................................................................ 60
Niejednoznaczność ................................................................................................... 60
Przekazywanie argumentów przez wartość i adres ................................................... 61
Wskaźniki do funkcji, delegaty ................................................................................ 62
Wyrażenia lambda .......................................................................................................... 65
Funkcja main() ............................................................................................................... 67
Przekazywanie parametrów do funkcji main() ......................................................... 68
Szablony funkcji ............................................................................................................. 70
Rozdział 4. Struktury, klasy, obiekty ................................................................ 73
Struktury ......................................................................................................................... 73
Klasy ............................................................................................................................... 75
Statyczne metody i pola klasy .................................................................................. 78
Wskaźnik zwrotny this ............................................................................................. 79
Dziedziczenie ................................................................................................................. 80
Funkcje wirtualne ........................................................................................................... 83
Wskaźniki na klasy bazowe i pochodne, rzutowanie ...................................................... 85
Przeciążanie operatorów ................................................................................................. 88
Szablony klas .................................................................................................................. 89
Wyjątki ........................................................................................................................... 92
Przestrzenie nazw ........................................................................................................... 94
Rozdział 5. Konstruowanie i usuwanie obiektów klas ........................................ 97
Konstruktory i destruktory .............................................................................................. 97
Przeciążanie konstruktorów ............................................................................................ 99
Konstruktor kopiujący .................................................................................................. 100
Konstruktor przenoszący .............................................................................................. 102
Konstruktory definiowane w klasach dziedziczonych .................................................. 104
Konstruktor kopiujący w klasie potomnej .................................................................... 105
Konstruktor definiowany w szablonie klasy ................................................................. 107
Struktury a klasy — porównanie .................................................................................. 110
Rozdział 6. Interface win32, główne okno aplikacji ......................................... 113
Części składowe podstawowego kodu okienkowej aplikacji win32 ............................. 113
Funkcja główna programu win32 ................................................................................. 115
Klasa okna głównego .................................................................................................... 115
Spis treści 5

Tworzymy nowe okno .................................................................................................. 118


Procedura okna ............................................................................................................. 120
Pętla komunikatów ....................................................................................................... 122
Zasoby ikon .................................................................................................................. 123
Zasoby menu ................................................................................................................ 128
Okna dialogowe w zasobach ........................................................................................ 131
Rozdział 7. Obsługa komunikatów .................................................................. 139
Komunikaty w aplikacji Windows ............................................................................... 139
WinAPI a standard Unicode ......................................................................................... 140
Przycisk i okno tekstowe, czyli budujemy warsztat ...................................................... 140
Komunikat WM_COMMAND ..................................................................................... 142
Odmalowywanie okna — komunikat WM_PAINT ..................................................... 145
Ruch myszy sygnalizuje WM_MOUSEMOVE ............................................................ 146
WM_CREATE kończy tworzenie okna ........................................................................ 149
SendMessage() prześle każdy komunikat ..................................................................... 150
Rozdział 8. Podstawowe kontrolki w działaniu aplikacji WinAPI ......................... 153
Wszechstronny przycisk Button ................................................................................... 153
Obsługa przycisków Button jako pól wyboru ............................................................... 154
Kontrolka ComboBox ................................................................................................... 155
Rozdział 9. Budowa aplikacji .NET w trybie wizualnym .................................... 165
Od WinAPI do .NET Framework ................................................................................. 165
Okno w trybie wizualnym ............................................................................................ 165
Przyciski ....................................................................................................................... 171
Etykiety ........................................................................................................................ 173
Pola tekstowe ................................................................................................................ 175
Wprowadzanie danych do aplikacji za pomocą pól tekstowych ................................... 176
Wprowadzanie danych z konwersją typu ..................................................................... 178
Wyświetlanie wartości zmiennych ............................................................................... 179
Pole tekstowe z maską formatu danych ........................................................................ 180
Pola wyboru, przyciski opcji, kontenery grupujące ...................................................... 183
Rozdział 10. Menu i paski narzędzi .................................................................. 187
Rodzaje menu ............................................................................................................... 187
Komponent MenuStrip ................................................................................................. 187
Menu podręczne ........................................................................................................... 193
Skróty klawiaturowe w menu ....................................................................................... 195
Paski narzędzi ............................................................................................................... 197
Rozdział 11. Tablice, uchwyty i dynamiczne tworzenie obiektów ....................... 203
Tablice .......................................................................................................................... 203
Dostęp do elementów tablicy za pomocą enumeratora ................................................. 206
Uchwyty ....................................................................................................................... 208
Dynamiczne tworzenie obiektów — operator gcnew ................................................... 209
Dynamiczna deklaracja tablic ....................................................................................... 210
Rozdział 12. Komunikacja aplikacji z plikami .................................................... 213
Pliki jako źródło danych ............................................................................................... 213
Wyszukiwanie plików .................................................................................................. 214
Odczyt własności plików i folderów ............................................................................ 215
Odczyt danych z plików tekstowych ............................................................................ 216
Zapisywanie tekstu do pliku ......................................................................................... 220
Zapis danych do plików binarnych ............................................................................... 222
Odczyt z plików binarnych ........................................................................................... 223
6 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozdział 13. Okna dialogowe ........................................................................... 225


Okno typu MessageBox ................................................................................................ 225
Okno dialogowe otwarcia pliku .................................................................................... 227
Okno zapisu pliku ......................................................................................................... 230
Okno przeglądania folderów ......................................................................................... 231
Okno wyboru koloru ..................................................................................................... 233
Wybór czcionki ............................................................................................................ 234
Rozdział 14. Możliwości edycji tekstu w komponencie TextBox ........................ 237
Właściwości pola TextBox ........................................................................................... 237
Kopiowanie i wklejanie tekstu ze schowka .................................................................. 239
Wyszukiwanie znaków w tekście ................................................................................. 240
Wstawianie tekstu między istniejące linie .................................................................... 241
Wprowadzanie danych do aplikacji .............................................................................. 242
Prosta konwersja typów — klasa Convert .................................................................... 242
Konwersja ze zmianą formatu danych .......................................................................... 243
Konwersja liczby na łańcuch znakowy ......................................................................... 246
Rozdział 15. Komponent tabeli DataGridView ................................................... 249
Podstawowe właściwości komponentu DataGridView ................................................. 249
Zmiana wyglądu tabeli ................................................................................................. 253
Dopasowanie wymiarów komórek tabeli do wyświetlanego tekstu .............................. 255
Odczytywanie danych z komórek tabeli ....................................................................... 257
Zmiana liczby komórek podczas działania aplikacji .................................................... 261
Tabela DataGridView z komórkami różnych typów .................................................... 265
Przyciski w komórkach — DataGridViewButtonCell .................................................. 268
Komórki z polami wyboru — DataGridViewCheckBoxCell ....................................... 269
Grafika w tabeli — komórka DataGridViewImageCell ............................................... 270
Komórka z listą rozwijaną — DataGridViewComboBoxCell ...................................... 272
Odnośniki internetowe w komórkach — DataGridViewLinkCell ................................ 274
Rozdział 16. Aplikacja bazy danych .................................................................. 277
Baza danych i aplikacja ................................................................................................ 277
Instalacja PostgreSQL .................................................................................................. 277
Wyłączenie usługi bazy ................................................................................................ 281
Inicjalizacja bazy .......................................................................................................... 281
Organizacja i typy danych w bazach PostgreSQL ........................................................ 283
Język SQL .................................................................................................................... 284
Utworzenie bazy danych .............................................................................................. 285
Interfejs użytkownika ................................................................................................... 286
Włączenie sterowników bazy PostgreSQL do projektu ................................................ 288
Łączenie z bazą i odczyt danych ................................................................................... 290
Dodawanie danych do bazy .......................................................................................... 292
Zmiana danych w bazie ................................................................................................ 295
Kasowanie danych ........................................................................................................ 297
Obsługa bazy ................................................................................................................ 298
Rozdział 17. Metody związane z czasem — komponent Timer ........................... 299
Czas systemowy ........................................................................................................... 299
Komponent Timer ........................................................................................................ 301
Rozdział 18. Grafika w aplikacjach .NET Framework ......................................... 303
Obiekt Graphics — kartka do rysowania ...................................................................... 303
Pióro Pen ...................................................................................................................... 308
Pędzle zwykłe i teksturowane ....................................................................................... 310
Spis treści 7

Rysowanie pojedynczych punktów — obiekt Bitmap .................................................. 313


Rysowanie trwałe — odświeżanie rysunku .................................................................. 314
Animacje ...................................................................................................................... 316
Rozdział 19. Podstawy aplikacji wielowątkowych ............................................. 319
Wątki ............................................................................................................................ 319
Komunikacja z komponentami z innych wątków — przekazywanie parametrów ........ 321
Przekazywanie parametrów do metody wątku .............................................................. 323
Klasa wątku — przekazywanie parametrów z kontrolą typu ........................................ 324
Kończenie pracy wątku ................................................................................................ 326
Semafory ...................................................................................................................... 328
Sekcje krytyczne — klasa Monitor ............................................................................... 331
Komponent BackgroundWorker ................................................................................... 334
Rozdział 20. Połączenie aplikacji z siecią Internet ............................................ 339
Komponent WebBrowser ............................................................................................. 339
Przetwarzanie stron Web — obiekt HtmlDocument ..................................................... 342
Uruchamianie skryptów JavaScript z poziomu aplikacji .............................................. 345
Protokół FTP ................................................................................................................ 347
Pobieranie zawartości katalogu z serwera FTP ............................................................. 348
Pobieranie plików przez FTP ........................................................................................ 350
Wysyłanie pliku na serwer FTP .................................................................................... 351
Klasa do obsługi FTP ................................................................................................... 352
Pobieranie plików w oddzielnym wątku ....................................................................... 356
Wysyłanie plików w wątku .......................................................................................... 357
Rozdział 21. Dynamiczne tworzenie okien i komponentów ................................. 359
Wyświetlanie okien — klasa Form ............................................................................... 359
Komponenty w oknie tworzonym dynamicznie ........................................................... 361
Przesyłanie danych z okien dialogowych ..................................................................... 362
Okno tytułowe aplikacji ................................................................................................ 363
Obsługa zdarzeń dla komponentów tworzonych dynamicznie ..................................... 364
Aplikacja zabezpieczona hasłem .................................................................................. 365
Rozdział 22. Prosty manager plików ................................................................. 367
Interfejs managera ........................................................................................................ 367
Wyświetlanie zawartości folderów ............................................................................... 367
Formatowanie prezentacji folderu .......................................................................... 369
Przechodzenie w dół i w górę drzewa plików ............................................................... 372
Idziemy w górę ....................................................................................................... 372
Idziemy w dół ......................................................................................................... 373
Kopiowanie plików między panelami .......................................................................... 374
Kasowanie plików ........................................................................................................ 375
Skorowidz .................................................................................... 377
8 Microsoft Visual C++ 2012. Praktyczne przykłady
Co znajdziesz
w tej książce?
Jeśli chcesz w miarę szybko i sprawnie zacząć poruszać się w Visual C++, ta książka
jest dla Ciebie. Jeśli masz już doświadczenie w C++, a chcesz programować dla .NET
Framework, również znajdziesz tu wiele interesujących informacji. Zebrane tu wiadomo-
ści pozwolą Ci wejść w świat programowania w C++ od zera. Pierwsza część książki
to opis standardowego C++. Jeśli zupełnie nic nie wiesz o tym języku, koniecznie zacznij
od tej części. Kiedy już opanujesz podstawy, dalsze rozdziały pokażą Ci, o co chodzi
w Windows z WinAPI. To najbardziej tradycyjny sposób programowania w Windows
i moim zdaniem każdy, kto chce tak naprawdę poznać ten temat, powinien znać choć
podstawy. Po opisie WinAPI przyjdzie kolej na C++/CLI i .NET Framework. Każdy
rozdział o .NET Framework opisuje konkretne zadania, takie jak operacje na łańcuchach
znakowych, bazy danych, grafikę w systemie, operacje na plikach, programowanie
wielowątkowe. Przedstawiłem też elementy nowego standardu C++11, ale tylko te, które
są obsługiwane przez wersję RC VC++ 2012.

Wszystkie aplikacje napisane są przy użyciu wersji RC (Release candidate) Visual C++ 2012.
To środowisko można pobrać nieodpłatnie ze strony internetowej firmy Microsoft. Książka
w żadnym razie nie wyczerpuje możliwości tego środowiska, ale daje solidne podstawy.
Całość napisana jest zgodnie z zasadą, że na przykładach można utrwalić wiedzę z pod-
ręcznika szybciej i efektywniej, niż jedynie powtarzając materiał teoretycznie.
10 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 1.
Podstawy środowiska
Visual C++ 2012
Professional

Opis środowiska
Visual C++ 2012 jest częścią większej całości, określanej jako MS Visual Studio 2012.
Od wersji 2012 związek z Visual Studio jest niemal nierozerwalny. Inne komponenty
Visual Studio to między innymi kompilator Visual Basica i Visual C#. Nas w tej
książce interesuje tylko Visual C++. Niestety, właśnie od wersji beta 2011 nie można
zainstalować samego IDE Visual C++, ale trzeba instalować całe Visual Studio. Ma-
my co prawda możliwość wyboru najczęściej używanego kompilatora, ale i tak zapo-
trzebowanie na miejsce na dysku jest rzędu 10 GB. VC++ 2012 można pobrać w kil-
ku wersjach, różniących się rodzajem licencji i możliwościami. Ja skupię się na wersji
Professional. Jest to wersja odpowiednia do nauki, a jednocześnie pozwala tworzyć
bardzo skomplikowane programy. Na razie są to wersje testowe i wszystkie można
pobrać bezpłatnie. Po pojawieniu się wersji finalnej bezpłatna pozostanie zapewne
tylko wersja Express Edition. Jak na razie istnieje ona tylko dla Windows 8, a więc syste-
mu będącego też w fazie testów. Czy Microsoft wyda wersję bezpłatną dla Windows 7,
pozostaje w chwili pisania tego tekstu sprawą otwartą. Windows XP jak na razie nie
jest wspierany w ogóle. Ani środowisko VC++ 2012 nie działa na tym systemie, ani nie
można tworzyć dla niego aplikacji. Musisz więc, Czytelniku, zaopatrzyć się co najmniej
w Windows 7. Wydaje mi się, że książka jest napisana na tyle uniwersalnie, że można
z niej skorzystać też przy posługiwaniu się Visual C++ 2010, jednak w takim przypadku
czasem będzie potrzebna własna inwencja, bo nie wszystko zadziała od razu.

Za pomocą VC++ 2012 można tworzyć programy w WinAPI, zarządzane aplikacje .NET
Framework, a także nowy rodzaj aplikacji nazwany Stylem Metro. Aplikacje Metro
działają tylko w Windows 8 i nie opisuję ich w tej książce. Mamy więc do wyboru jakby
12 Microsoft Visual C++ 2012. Praktyczne przykłady

dwa języki w jednym kompilatorze. Są to standardowy C++ i C++/CLI. Pierwszy z nich


daje możliwość pisania „tradycyjnych” programów win32, a C++/CLI oferuje pisanie
programów zarządzanych z .NET Framework. Są one częściowo podobne, jeśli chodzi
o składnię, ale już reprezentacja w pamięci programu zarządzanego i niezarządzanego
jest inna.

Język C++ a .NET Framework


Na początek kilka zdań do osób, które już znają trochę C++ i chcą pisać programy z .NET
Framework. Środowisko .NET Framework jest opracowanym przez Microsoft narzę-
dziem programistycznym, które umożliwia pisanie wieloplatformowych (przynajmniej
teoretycznie) aplikacji. System współpracuje z wieloma językami programowania, nie
mogło zabraknąć wśród nich także C++. Aby wykorzystać możliwości .NET Framework,
stworzono najpierw rozszerzenie C++ zwane Managed C++. Niestety, składnia progra-
mów pisanych w Managed C++ była mało czytelna, a całość była tylko „łatką” na C++.
Microsoft postanowił opracować całkowicie nową specyfikację języka, upraszczającą
programowanie dla środowiska .NET. Tak powstał C++/CLI. Kod pisany w tym języku
może zawierać zarówno elementy zarządzane (managed), jak i ze „starego” języka C++
(unmanaged).

Kod zarządzany podlega działaniu mechanizmu zwanego garbage collector (najczęściej


określanego po polsku trochę niepoprawnym słowem odśmiecacz). Działanie „odśmieca-
cza” polega na okresowym przeglądaniu zawartości pamięci używanej przez program
i usuwaniu nieużywanych obiektów. W celu rozróżnienia obiektu (zmiennej, obiektu
klasy itp.) używanego od nieużywanego garbage collector stosuje różne metody, których
nie będę tu opisywał.

Zaletą takiego rozwiązania jest to, że programista nie musi się martwić o zwalnianie pamięci
przez obiekty tworzone dynamicznie na stercie. Oczywiście, można zażądać destrukcji
takiego obiektu, ale nie jest to wymagane, może zająć się tym automatycznie odśmiecacz.

W odróżnieniu od zwykłych kompilatorów, kod uruchamiany w .NET Framework nie


jest bezpośrednio tłumaczony na instrukcje procesora, ale najpierw przekształcany na
kod w języku CIL (od ang. Common Intermediate Language), a ten kod jest następnie
kompilowany przed wykonaniem przez środowisko .NET. W ten sposób program może
być wykonywany nie tylko w systemie Windows, ale w każdym systemie wspierającym
platformę .NET. Jest to rozwiązanie podobne do języka Java.

Pobieranie i instalacja środowiska


Microsoft udostępnia środowisko na stronie http://www.microsoft.com/visualstudio/
11/en-us.

Kliknij Download an RC version, na następnej stronie kliknij Visual Studio, a dalej


Professional.
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 13

Po lewej stronie masz kolumnę Visual Studio Professional 2012 RC. Poniżej jest lista
wyboru języka. Niestety, nie ma polskiego, więc zostawiłem angielski. Kliknij pierwszą
ramkę poniżej listy wyboru języka z napisem Visual Studio Professional 2012 RC – English
Install. Powinno zacząć się pobieranie instalatora o nazwie vs_professional.exe. Nie jest to
oczywiście całe środowisko, tylko instalator sieciowy. Po zapisaniu uruchom ten plik.

Po uruchomieniu instalatora pojawi się ekran powitalny, jak na rysunku 1.1.

Rysunek 1.1.
Ekran powitalny
instalatora Visual
Studio 2012 RC

Instalator ma postać znanych kreatorów, do kolejnych kroków instalacji przesuwamy


się przyciskiem Next, ewentualny powrót do poprzedniego kroku uzyskamy przyciskiem
Previous. Mamy tu okno z nazwą folderu, do którego będzie zainstalowane środowisko,
oraz z odnośnikiem do warunków umowy, które warto przeczytać. Całość zajmuje
ponad 9 GB. Szkoda, że nie ma opcji instalowania samego Visual C++, ale trzeba in-
stalować też inne kompilatory.

Aby przejść dalej, musisz wyrazić zgodę na warunki licencji, zaznaczając kwadrat
przy I agree to the License terms and conditions. W następnym oknie instalator prosi
o zatwierdzenie dodatków, standardowo zaznaczone są wszystkie pozycje, tak też zo-
stawiłem. Kliknij przycisk INSTALL. Rozpocznie się pobieranie i instalacja środowiska.
Na moim średniej klasy komputerze instalacja trwała około 30 minut. Po instalacji można
uruchomić środowisko, klikając na pojawiający się napis Launch.

Przy pierwszym uruchomieniu mamy możliwość wyboru narzędzi, które będą najbardziej
potrzebne. Od tego zależy konfiguracja środowiska. Ja wybrałem oczywiście Visual C++
Development Settings. Klikamy Start Visual Studio i zaczyna się automatyczny proces
wewnętrznej konfiguracji. Po około minucie zobaczysz IDE środowiska. Pojawia się
14 Microsoft Visual C++ 2012. Praktyczne przykłady

też zapytanie o pozwolenie na pobranie z Internetu plików pomocy. Jeśli spotkałeś się
już z VC++, to na pierwszy rzut oka wszystko wygląda znajomo — zastosowano nową
kolorystykę i menu, ale ogólny rozkład jest podobny do VC++ 2010.

Kilka pojęć na początek


Programowanie, jak każda dziedzina, ma swoje pojęcia i definicje. Spróbuję wytłumaczyć
te, które będą nam potrzebne od samego początku. Opiszę je w możliwie najprostszy
sposób.

Zmienne
Na pewno spotkałeś się już ze zmienną, rozwiązując jakiekolwiek równanie. Litera x,
którą zwykle oznacza się niewiadome w równaniach, stała się już synonimem czegoś
ukrytego i tajemniczego. W matematyce zmienna to liczba ukryta pod nazwą, symbolem.
W implementacji komputerowej liczba ta jest zapisana w ustalonej komórce pamięci.
Nazwa zmiennej może składać się z jednej lub więcej liter.

Funkcja
Funkcja to pewien algorytm zamknięty w całość i mający nazwę. Z funkcjami też się
spotkałeś, ucząc się matematyki. Weźmy funkcję sinus. Mówimy, że ta funkcja ma argu-
ment (zwany parametrem), którym jest kąt. Jest to dana wejściowa. Kiedy chcemy
obliczyć sinus kąta, to wiemy, co robić: najprościej posłużyć się kalkulatorem. Można
opisać taki proces krok po kroku:

Chcemy obliczyć y = sin(x).


1. Włącz kalkulator.
2. Wprowadź kąt o wartości x.
3. Naciśnij przycisk sin.
4. Odczytaj wynik z ekranu i podstaw go pod y.

Wartość y otrzymywana w wyniku działania funkcji to wartość zwracana. Tak samo


pisze się funkcje w C++ i innych językach programowania. Oczywiście, zamiast zdań
po polsku mamy instrukcje języka. Raz napisana funkcja może być użyta wielokrotnie,
skracając program.
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 15

Klasy
Spójrzmy ogólnie na kąty podstawiane do funkcji sinus. Oznaczyliśmy je przez x, czyli
zmienna ta jest pewną ogólną reprezentacją kąta. Można więc powiedzieć, że mamy
pewną zbiorowość (klasę) kątów. Każdy z tych konkretnych kątów jest obiektem
klasy kąt. Klasa jest zbiorem cech, natomiast każdy z obiektów klasy posiada konkretne
wartości tych cech. Rozróżnienie między klasą a obiektem klasy jest bardzo ważne dla
zrozumienia sposobu pisania programów zwanego programowaniem obiektowym.
Pomyślmy nad formalnym zapisem takiej klasy. Mógłby on wyglądać tak:
klasa kąt
x

Obudowaliśmy zmienną x zawierającą wartość kąta. Teraz można utworzyć obiekt


klasy kąt, który w swoim wnętrzu będzie zawierał x. Może pomyślisz, że jakoś nie
widać walorów użytkowych takiego przedsięwzięcia. Pojawią się one, gdy rozszerzymy
pojęcie klasy. Oprócz zmiennych (które nazywamy polami — może być ich wiele)
klasa może zawierać funkcje operacji na tych zmiennych. Funkcje składowe klasy na-
zywają się metodami. W klasie kąt metodami mogłyby być funkcje trygonometryczne.
klasa kąt
x
y = sin(x)
y = cos(x)
y = tg(x)
y = ctg(x)

Teraz, tworząc obiekt klasy, można wywoływać na nim metody. Dla przykładu zobacz
taki algorytm:
1. Utwórz obiekt klasy kąt o nazwie alfa.
2. Przyporządkuj zmiennej x we wnętrzu obiektu wartość 30o.
3. Wykonaj na obiekcie alfa metodę sin(), a jej wynik zapisz w zmiennej a.

W skrócie można to zapisać tak:


kąt alfa
alfa.x = 30
a = alfa.sin()

Zauważ, że dostęp do wnętrza klasy uzyskujemy za pomocą kropki. Jest to operator do-
stępu. Funkcja sin() nie ma teraz żadnych parametrów. Wiadomo bowiem, że jej parame-
trem jest zmienna x w obiekcie klasy. Wróćmy jeszcze do różnicy między obiektem
a klasą. Możesz rozumieć klasę jako projekt obiektu. Programista, pisząc aplikację,
tworzy klasy (w „programie” wyżej klasa kąt) i deklaruje obiekty tych klas, tak jak
my zadeklarowaliśmy obiekt alfa. W momencie wykonania programu zadeklarowane
obiekty zostaną utworzone w pamięci.
16 Microsoft Visual C++ 2012. Praktyczne przykłady

Przestrzenie nazw
Jeśli kilka osób pracuje nad dużym projektem, to może się zdarzyć, że dwie osoby nazwą
tak samo zmienną, funkcję lub klasę. Spowoduje to konflikt i program się nie uruchomi.
Aby tego uniknąć, można pogrupować nazwy funkcji i klas właśnie w przestrzeniach
nazw. Później, wywołując na przykład funkcję, trzeba zaznaczyć, z jakiej przestrzeni
nazw ona pochodzi.

Z czego składa się aplikacja Windows


Do zrozumienia tego, co jest wyświetlane w oknach VC++ 2012, potrzebne jest wyjaśnie-
nie, z czego składa się aplikacja Windows. Łatwiej będzie to zrozumieć, znając pojęcia
przedstawione wyżej.

Aplikacje Windows składają się z obiektów klas. Wiesz już, że taki obiekt może zawie-
rać w sobie zmienne. Standardowo zwane są one polami, ale w nazewnictwie Visual
C++ mamy nazwę właściwości (properties). Obiekty te komunikują się ze sobą poprzez
komunikaty lub zdarzenia. Działająca aplikacja jest zbiorem komunikujących się ze
sobą obiektów. Obiekty te nie są niezależne, ale najczęściej związane w jakąś hierarchię.
Może to wydać Ci się trochę mało precyzyjną definicją. Spróbuję pokazać to na przy-
kładzie.

Na dole ekranu Windows masz pasek zadań. Doskonale wiesz, że jeśli klikniesz przycisk
Start, to pokaże się menu. Tyle widać na zewnątrz, ale spróbujmy się zastanowić, co się
stało „pod maską” systemu. Przycisk Start (i każdy inny) jest obiektem. I nie jest nieza-
leżny, ale jest obiektem podrzędnym w stosunku do samego paska. Kiedy go kliknąłeś,
wysłał on do swojego „rodzica”, czyli paska, wiadomość „kliknięto mnie”. Pasek zarea-
gował, wyświetlając menu. Zarówno pasek, jak i menu też są obiektami i razem tworzą
aplikację systemową, którą widzisz jako pasek zadań.

Ponieważ obiekty takie jak przycisk czy okno są stałym składnikiem aplikacji, byłoby
to nieco kłopotliwe, gdyby każdy pisał klasy tych obiektów od nowa. Ich klasy są zdefi-
niowane przez autora systemu, gotowe i można ich użyć w programie. Obiekty wi-
doczne w oknie aplikacji nazywane są kontrolkami.

Wrócę do paska zadań i spróbuję wyjaśnić, na czym polegałoby napisanie takiej aplikacji.
Myślę, że doświadczeni programiści wybaczą mi skrótowe określenia. Najpierw trze-
ba mieć klasę samego paska, menu i przycisku, bo takie obiekty chcemy mieć w na-
szej aplikacji. Na szczęście, nie trzeba ich pisać od początku, ale można skorzystać
z gotowych. Weźmy klasę przycisku. Polami tej klasy będą na przykład wymiary
przycisku czy ikona Windows do wyświetlenia na nim. Jak widzisz, pola mogą mieć
różne typy; może to być liczba, ale może też być rysunek. Kliknięcie przycisku spo-
wodowało wysłanie komunikatu do paska zadań. Klasa paska zadań też ma pola, na
przykład wymiary i kolor obiektu, który z niej powstanie. Ma również co najmniej
jedną metodę, która jest wywoływana, kiedy pasek otrzyma komunikat od przycisku
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 17

Start. Metoda ta wyświetla menu. Menu też jest obiektem pewnej klasy. W skrócie
przy pisaniu aplikacji naszym zadaniem jest ustalenie zależności między obiektami,
określenie wartości właściwości i napisanie metod reakcji na zdarzenia.

Główne okno VC++ 2012 RC


Po uruchomieniu programu ukazuje się główne okno podzielone na kilka części, jak na
rysunku 1.2.

Rysunek 1.2. Okno IDE Visual Studio 2012 Professional

Całą środkową i prawą stronę zajmuje okno Start Page z odnośnikami do tworzenia no-
wego projektu (New Project) lub otwierania istniejącego (Open Project). Poniżej ma-
my możliwość otwarcia jednego z ostatnio utworzonych projektów (Recent projects).
Prawa strona okna Start Page to dwie zakładki w poziomie: Get Started i Latest News.
Latest News to kanał RSS o nowościach Microsoftu. W zakładce Get Started mamy
natomiast okno Welcome, a w nim cztery grupy odnośników. W pierwszej grupie, What’s
New in Visual Studio, są opisy nowych możliwości Visual Studio i .Net Framework. Druga
grupa odnośników, Getting Started, zawiera pomoce do nauki programowania. Trzecia
część, Manage your projects in the cloud, dotyczy programowania w chmurze. Ostatnia,
Learning Resources, to odnośniki do wiadomości przydatnych do nauki Visual Studio.
18 Microsoft Visual C++ 2012. Praktyczne przykłady

Z prawej strony okna Start Page mamy trzy zakładki. Górna zakładka, Server Explorer,
jest czymś nowym dla użytkowników przyzwyczajonych do wersji Express Visual
C++. To bardzo mocne narzędzie, za pomocą którego można połączyć się z serwerem
bazy danych na komputerze lokalnym lub zdalnym, jeśli mamy odpowiednie uprawnienia.
W wersji Express podobną funkcjonalność znajdziemy w zakładce Database Explorer.
Druga zakładka, Toolbox, to „magazyn” kontrolek i obiektów do użycia w aplikacjach.
Trzecia zakładka, SQL Server Object Explorer, oferuje też zarządzanie serwerem ba-
zy danych, ale otwierane przez nią narzędzie jest przeznaczone w zasadzie tylko do
Microsoft SQL Server.

Z lewej strony okna Start Page mamy panel z sześcioma zakładkami na dole. Za pomocą
tego panelu można zarządzać projektem aplikacji, ale o tym dalej.

Zaginiony projekt
Po zainstalowaniu Visual C++ w wersji RC okazało się, że znikła możliwość tworzenia
aplikacji Windows Form Application. Jest to aplikacja okienkowa bazująca na C++/CLI
i .NET Framework. Będziemy z niej wielokrotnie korzystać. Trochę mnie to zdziwiło,
bo w wersji beta środowiska taka możliwość normalnie była. Mam nadzieję, że to jakiś
błąd, który zostanie naprawiony. Na szczęście, można sobie z tym poradzić, budując
własny szablon.

Do budowy szablonu będzie potrzebny projekt aplikacji właśnie typu Windows Form
Application utworzony za pomocą innego środowiska VC++, najlepiej wersji beta o nu-
merze VC2011. Przygotowałem taki projekt w pliku z przykładami. Można go znaleźć
w podkatalogu WindowsFormApplication1. Ponieważ jest to przykład, którego być może
nie będziesz musiał wykonywać, zaznaczyłem go poza normalną numeracją, jako 1.A.

Przykład 1.A

Otwórz Projekt WindowsFormApplication1. Z menu File wybierz opcję Export Template.


W oknie Export Template Wizard pozostaw zaznaczone Project Template. Kliknij
Next>. W następnym oknie w polu Template name wpisz Windows Form Application.
W Template Description umieść opis, na przykład Aplikacja okienkowa .NET. Oba pola
wyboru na dole pozostaw zaznaczone, aby szablon został automatycznie dodany do
środowiska. Kliknij Finish>.

Powstanie plik .zip z szablonem. Nie musisz się nim przejmować, bo szablon już zo-
stał dołączony do VC++ 2012. Kliknij File/New/Project. Szablon aplikacji okienkowej
.NET znajdziesz w oknie New project w gałęzi Templates/Visual C++, tak jak pokazuje
rysunek 1.3.
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 19

Rysunek 1.3. Szablon projektu okienkowego C++/CLI w nowym miejscu

Tworzenie projektu nowej aplikacji


w VC++ 2012
Mamy dwie możliwości stworzenia nowego projektu aplikacji. Pierwsza to wybór opcji
z menu File/New/Project, a druga to kliknięcie odnośnika New Project... w środko-
wym oknie, które już opisywałem. Dalsze postępowanie zależy od tego, jakiego rodzaju
biblioteki zamierzasz użyć. Wersje Express VC++ oferowały dwie możliwości: apli-
kacje z rozszerzeniem win32 lub C++/CLI. W obu przypadkach można było tworzyć
aplikację konsolową lub okienkową. VC++ 2012 w wersji Professional daje dodatko-
wo możliwość pisania aplikacji korzystającej z ATL lub MFC. W tej książce z trzech
powodów zdecydowałem się pozostać przy opisywaniu aplikacji w win32 i C++/CLI.
Po pierwsze, moim celem jest głównie opisywanie standardowego C++ z Visual C++
jako kompilatorem. Po drugie, z C++/CLI i programowaniem z .NET Framework mam
największe doświadczenie. Po trzecie, książka jest pisana głównie z myślą o użytkow-
nikach wersji Express. Jeśli nie zetknąłeś się do tej pory z językiem C++, to zanim
nauczysz się pisać programy okienkowe, powinieneś poznać zasady i strukturę tego
języka. Najszybciej osiągniesz to, zaczynając od programów uruchamianych w konsoli.
Nie musisz się wtedy martwić o zaprogramowanie spraw „okienkowych”, a możesz
się skupić na sednie programu. W VC++ 2012 masz do wyboru dwa rodzaje projektów
konsolowych. Jeden wykorzystuje rozszerzenie win32, a drugi C++/CLI. Polega to na
tym, że możesz uruchamiać programy pisane w czystym C++, ale jeśli chcesz, możesz
20 Microsoft Visual C++ 2012. Praktyczne przykłady

odwołać się do bibliotek win32 lub CLI. W kolejnych rozdziałach zaczniemy od pro-
jektów konsolowych win32, następnie przejdziemy do projektów okienkowych z tą
biblioteką i wreszcie do C++/CLI. W kolejnych czterech przykładach pokażę, jak
tworzyć te rodzaje projektów i co się w nich znajduje po utworzeniu.

Przykład 1.1

Utwórz nowy projekt działający w konsoli win32.

Rozwiązanie
Z menu wybierz opcję File/New/Project. Pojawi się okno jak na rysunku 1.4.

Rysunek 1.4. Okno tworzenia nowego projektu aplikacji

W lewym panelu zaznacz opcję Win32 i sprawdź, czy w prawym panelu podświetlona
jest opcja Win32 Console Application. Teraz w pole Name na dole okna wpisz dowolną
nazwę projektu. Zwróć uwagę na pole wyboru Create directory for solution w prawej
dolnej części okna. Jeśli jest ono zaznaczone, to dla nowego projektu będzie założony
folder o nazwie takiej jak wpisana w polu Name. Folder ten będzie założony na ścieżce
z pola Location. Jeśli ta opcja nie jest zaznaczona, to pliki projektu zostaną utworzone
bezpośrednio w miejscu określonym przez Location. Kliknij OK. Pojawi się kreator
aplikacji. Ponieważ wystarczą ustawienia standardowe, możesz od razu kliknąć przy-
cisk Finish.
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 21

Przykład 1.2

Utwórz nowy projekt działający w konsoli C++/CLI.

Rozwiązanie
Kliknij opcję File/New/Project. W znanym już oknie wyboru rodzaju projektu z lewej
strony zaznacz opcję CLR. W środkowym oknie wybierz CLR Console Application.
Teraz w polu Name poniżej okna wyboru rodzaju projektu należy wpisać nazwę aplikacji
i ewentualnie wybrać folder w polu Location. Zasady dotyczące nazwy i miejsca utworze-
nia plików projektu są takie same jak w przykładzie 1.1. Po ustawieniu opcji okno powinno
wyglądać jak na rysunku 1.5.

Rysunek 1.5. Okno tworzenia nowego projektu konsolowego C++/CLI

Kliknij OK i projekt zostanie utworzony.

Przykład 1.3

Utwórz projekt okienkowy win32.

Rozwiązanie
Kliknij opcję File/New/Project. W oknie New Project (rysunek 1.3) w lewym panelu
wybierz win32, jak w przykładzie 1.1. Tym razem jednak w środkowym oknie wybierz
Win32 Project, następnie wpisz nazwę projektu w pole Name. Jeśli chodzi o nazwy i fol-
dery projektów, to zasady są takie same jak w przykładzie 1.1.
22 Microsoft Visual C++ 2012. Praktyczne przykłady

Kliknij OK. Przy projektach okienkowych Win32 czeka Cię jeszcze przejście kreatora
aplikacji, który zostanie uruchomiony właśnie teraz. Kreator jest prosty i ma tylko dwa
kroki. W pierwszym kroku możesz tylko zaakceptować fakt tworzenia aplikacji Win32,
kliknij więc Next>. Drugi krok daje możliwość wybrania, czy tworzysz zwykłą aplikację,
bibliotekę DLL czy aplikację konsolową. Tworzymy aplikację, więc zostaw przycisk
opcji tak jak jest, czyli z zaznaczonym Windows Application, i naciśnij Finish.

Projekt zostanie utworzony. Niestety, nie zobaczysz tu graficznie przedstawionego


okna aplikacji, a magazyn ToolBox jest pusty. Pisanie programów Win32 wymaga
bezpośredniego pisania kodu i jest nieco trudniejsze od C++/CLI, ale za to program
nie ma bagażu w postaci części biblioteki .NET Framework i jest dzięki temu mniej-
szy objętościowo po skompilowaniu.

Przykład 1.4

Utwórz projekt okienkowy C++/CLI.

Rozwiązanie
Kliknij opcję File/New/Project. W znanym już oknie wyboru rodzaju projektu z lewej
strony zaznacz gałąź CLR, tak jak na rysunku 1.4. W środkowym oknie wybierz Windows
Forms Application. Jeśli sam tworzyłeś ten szablon, to znajdziesz go w gałęzi Visual
C++, jak pokazałem w przykładzie 1.A. W polu Name wpisz nazwę projektu; zasady
dotyczące nazwy i miejsca utworzenia plików projektu są takie same jak w przykła-
dzie 1.1. Kliknij OK. Zostanie utworzony projekt C++/CLR. W takim projekcie mamy
graficzne wsparcie w postaci kontrolek pobieranych z magazynu Toolbox i pewnej
automatyzacji tworzenia aplikacji.

Wygląd środowiska w trybie budowy


aplikacji
Środowisko typu IDE ułatwia zarządzanie klasami podczas budowy aplikacji. Dzięki
niemu wiemy, jak będą wyglądać okna przyszłej aplikacji, a właściwości klas i zda-
rzenia związane z daną klasą są zestawione w tabelach. W ten sposób łatwiej nadać warto-
ści polom klasy i przyporządkować metody reakcji do zdarzeń. Dokładniej wyjaśnię
to w następnych rozdziałach. Na razie opiszę, gdzie znajdują się okna środowiska, których
będziemy używać. Wszystkie objaśnienia odnoszą się do rysunku 1.6.

W prawym oknie znajduje się kod źródłowy budowanej aplikacji oraz widok jej okna,
jeśli rodzaj projektu pozwala na graficzną budowę okna. Po lewej stronie głównego
okna jest panel zawierający kilka zakładek pomagających zarządzać projektem apli-
kacji. Na rysunku 1.6 widać jeden z trybów działania tego panelu, zwany Solution
Explorer. Pokazuje on pliki wchodzące w skład projektu w postaci drzewa zależności.
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 23

Rysunek 1.6. Okno VC++ 2012 Professional podczas pisania aplikacji

Pierwsza gałąź tego drzewa, External Dependencies, grupuje pliki zewnętrzne, które
są niezbędne do działania projektu. Są to tak zwane pliki nagłówkowe rozszerzenia win32
lub C++/CLI (w zależności od rodzaju projektu) lub na przykład biblioteki DLL. Ga-
łąź Header Files to pliki nagłówkowe należące bezpośrednio do projektu i najczęściej
umieszczone w jego folderze. Resource Files to pliki zasobów, zawierające na przykład
ikonę aplikacji. Wreszcie Source Files to pliki z kodem źródłowym programu. Plika-
mi nagłówkowymi zajmiemy się w następnym rozdziale; na razie wystarczy informa-
cja, że kod aplikacji w C++ nie musi być w jednym pliku, ale może być podzielony.

Z prawej strony okna z kodem źródłowym mamy aż cztery zakładki pionowe. Dwie z nich,
Toolbox i Server Explorer, już omawiałem. Zakładka Properties otwiera panel z wła-
ściwościami klasy aktualnie wybranego komponentu aplikacji. Tu właśnie wprowadzamy
wartości właściwości opisane wcześniej. Panel ten można przełączyć w tryb zdarzeń
za pomocą ikony błyskawicy w jego górnej części. Wtedy mamy listę zdarzeń, do których
przyporządkujemy metody reakcji. Za pomocą zakładki SQL Server Object Explorer
zarządzamy ewentualną bazą danych podłączoną do aplikacji. Ma ona zastosowanie,
jeżeli budowana aplikacja korzysta z bazy danych.

Poniżej okna z kodem aplikacji znajduje się okno komunikatów. Wyświetlane są tu ko-
munikaty o przebiegu kompilacji i ewentualnych błędach.

W górnej części ekranu znajdują się menu i paski narzędzi, jak w wielu aplikacjach Win-
dows. Z przycisków na paskach warto wymienić trójkąt, który służy do rozpoczęcia
kompilacji, a następnie uruchomienia pisanego programu. Po prawej stronie trójkąta
24 Microsoft Visual C++ 2012. Praktyczne przykłady

znajdują się trzy listy wyboru trybu kompilacji i tak zwanego debugowania, czyli
usuwania usterek w aplikacji. W pierwszej ustawiamy, czy informacje debugera mają
dotyczyć całej aplikacji czy tylko jej części, na przykład kodu zarządzanego. W drugiej
mamy opcje Debug, Release i Configuration Manager. Opcja Debug kompiluje pro-
gram z dodatkowymi informacjami ułatwiającymi debugowanie, a opcja Release po-
woduje, że informacje te nie są załączane, przez co plik wykonywalny jest mniejszy.

Trzecia lista rozwijalna pozwala na zdefiniowanie konfiguracji platformy, dla jakiej


nasza aplikacja będzie kompilowana.

Ustawienie Debug należy stosować tylko na etapie testowania aplikacji. Po usunięciu


usterek program trzeba skompilować w trybie Release.

Struktura projektu
Projekt w VC++ 2012 jest częścią tak zwanego Solution, czyli Rozwiązania. Może
ono zawierać jeden lub kilka projektów, każdy w osobnym podfolderze. Rozwiązania
pozwalają na sprawniejsze zarządzanie projektami, na przykład tryb kompilacji kodu usta-
wia się na poziomie rozwiązania dla wszystkich wchodzących w jego skład projektów.

Po utworzeniu nowe rozwiązanie składa się z kilku folderów. Najpierw mamy folder
o nazwie takiej jak podana w polu Solution Name okna z rysunku 1.3. Nazwijmy go folde-
rem rozwiązania. W tym folderze niezależnie od rodzaju projektu znajdują się nastę-
pujące pliki:
nazwa_rozwiązania.sdf — plik bazy danych systemu IntelliSense
ułatwiającego pisanie programów. Jest on odpowiedzialny na przykład
za podpowiadanie nazw metod przy wpisywaniu.
nazwa_rozwiązania.sln — główny plik Rozwiązania, zawierający informacje
o projektach wchodzących w jego skład.
nazwa_rozwiązania.suo — binarny, ukryty plik zawierający prawdopodobnie
konfigurację opcji Rozwiązania.

W folderze rozwiązania znajdują się co najmniej dwa podfoldery. Jeden z nich nazywa
się ipch i zawiera pliki potrzebne do tzw. prekompilacji nagłówków. Jest to mecha-
nizm przyspieszający kompilację, o którym będziemy mówić później. Ten folder zawiera
też dane wykorzystywane do generowania podpowiedzi do pisanego kodu. Drugi folder
ma taką nazwę projektu, jak wpisana w pole Name na rysunku 1.3. Tu mamy pliki
źródłowe aplikacji:
nazwa_projektu.ico — plik typu .ico, zawierający jedną lub więcej ikon
budowanej aplikacji. Będziemy się tym zajmować przy omawianiu edytora
zasobów.
nazwa_projektu.rc — plik opisujący zasoby aplikacji (skrypt zasobów).
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 25

nazwa_projektu.cpp — „program główny”, czyli funkcja main(); tu zaczyna


się wykonywanie programu. Jest to plik generowany automatycznie, zawiera
tylko instrukcje utworzenia głównego okna aplikacji, czyli obiektu klasy
zdefiniowanej w pliku Form1.h.
nazwa_projektu.vcxproj — plik opisu projektu.
nazwa_projektu.vcxproj.filters — opis drzewa plików wyświetlanego w oknie
Solution Explorer.

Oprócz tego folder zawiera kilka plików nagłówkowych, tekstowy plik z opisem, a także
pewne pliki zależne od rodzaju projektu.

Po kompilacji w folderze rozwiązania automatycznie tworzone są foldery Debug lub Rele-


ase, w zależności od trybu kompilacji. W tych folderach znajdują się pliki wykonywalne.

Efektywna praca w środowisku


Edytor kodu w VC++ 2012 zawiera kilka ułatwień bardzo przydatnych dla programisty.
Jeśli zaczynasz od początku, to w pełni je docenisz, kiedy nabędziesz już umiejętności
pisania podstawowych programów, jednak zdecydowałem się opisać je już na początku
książki.

Pierwsza funkcja, która rzuca się w oczy, to podświetlanie wszystkich wystąpień


zmiennej lub obiektu po kliknięciu na nim.

Przykład 1.5

Podświetl wszystkie wystąpienia dowolnego obiektu w kodzie programu.

Rozwiązanie
Uruchom VC++ 2012 i wybierz menu File/New Project. W lewym panelu okna New
Project rozwiń gałąź Visual C++, kliknij na Win32, w środkowym panelu kliknij Win32
project. Zostawimy domyślną lokalizację projektu, więc kliknij OK. W razie problemów
pomoże Ci przykład 1.3; do tej pory jest to jego powtórzenie.

Pojawi się kreator aplikacji. Nie będziemy w nim nic zmieniać, więc kliknij Finish.

Pojawi się podstawowy kod aplikacji winAPI; na razie nie będziemy się w niego zagłę-
biać. Kliknij na dowolną nazwę zmiennej, ja kliknąłem na hInstance. Wszystkie wystą-
pienia hInstance zostaną podświetlone, jak na rysunku 1.7.
26 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 1.7.
Podświetlone
wystąpienia zmiennej
hInstance

Zapisz aplikację lub jeszcze lepiej na razie jej nie zamykaj. Następne przykłady są krótkie
i będziemy je wykonywać na tym samym kodzie.

Inną opcją ułatwiającą pracę jest wyszukiwanie definicji i deklaracji funkcji. Wiesz już,
że funkcja to pewien fragment programu, który jest zamkniętą całością i może być użyty
wielokrotnie, dzięki czemu nie trzeba go ciągle pisać do nowa. Ten fragment trzeba gdzieś
w programie zdefiniować i nadać mu nazwę, podobnie jak wyżej definiowaliśmy obli-
czanie funkcji sinus. Czasem jednak z jakichś powodów chcemy go zdefiniować dopiero
na końcu programu. Logiczne jest w takim wypadku, że będziemy go używać już przed
definicją. Jeśli tak, to kompilator, napotykając nazwę funkcji, nie będzie wiedział, o co
chodzi, bo jeszcze nie napotkał definicji. Zatem na początku programu wymienimy tylko
nazwę funkcji i jej parametry. Jest to deklaracja, która mówi, że definicja będzie gdzieś
później.

Deklaracja funkcji w teorii wygląda tak:


wartość_ zwracana nazwa_funkcji(parametry)

a w praktyce na przykład tak:


BOOL InitInstance(HINSTANCE, int);

Definicja wygląda bardzo podobnie, tylko po pierwszej linii następuje kod funkcji (ciało)
ujęty w nawiasy klamrowe.

Przykład 1.6

Znajdź deklarację i definicję wybranej funkcji w kodzie.

Rozwiązanie
Operujemy na programie z poprzedniego przykładu. Ponieważ znasz już funkcję InitIn-
stance(), znajdź jej deklarację na początku kodu programu. Pomoże Ci w tym rysu-
nek 1.8.
Rozdział 1. ♦ Podstawy środowiska Visual C++ 2012 Professional 27

Rysunek 1.8.
Deklaracja funkcji
InitInstance()

Najedź kursorem myszy na nazwę InitInstance i kliknij prawym klawiszem. Rozwinie


się menu, w którym wybierz opcję Go To Definition. Zostaniesz przeniesiony do definicji
tej funkcji na koniec programu. Jeśli teraz klikniesz prawym przyciskiem na nazwie
InitInstance w definicji i wybierzesz Go To Declaration, wrócisz do deklaracji.

Kolejnym ułatwieniem jest możliwość wyszukania wszystkich odwołań do danej zmiennej,


funkcji czy klasy. Innymi słowy, jest to wyszukanie wszystkich linii, w których występuje
na przykład dana zmienna. Jest to bardzo przydatne, kiedy chcemy prześledzić, w jakich
wyrażeniach czy działaniach występuje ta zmienna w toku wykonywania programu

Przykład 1.7

Wyszukaj wszystkie linie kodu z daną zmienną.

Rozwiązanie
Na początku kodu, z którym pracujemy już od kilku przykładów, jest linia:
TCHAR szTitle[MAX_LOADSTRING];

Oznacza ona deklarację zmiennej tablicowej szTitle, w której ukrywa się tytuł okna
naszej aplikacji. Załóżmy, że chcemy znaleźć wszystkie linie kodu, w których ta zmienna
jest użyta. Nic prostszego: kliknij na nazwę szTitle myszą, następnie kliknij prawym
przyciskiem i ze znanego już menu wybierz Find all references. W oknie komunikatów
poniżej okna z kodem zostaną wyświetlone wszystkie linie programu zawierające tę
nazwę.

Ostatnim ułatwieniem, jakie chciałem tu pokazać, jest wyświetlanie tak zwanej hierarchii
wywołań, czyli zapisu, jaka funkcja używała danej funkcji i jakie funkcje ona z kolei wy-
woływała. Będzie to jak pytanie, kto mnie wołał i kogo ja wołałem.
28 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 1.8

Ustal hierarchię wywołań funkcji InitInstance().

Rozwiązanie
W dalszym ciągu posługujemy się kodem z poprzednich przykładów. Znajdź jeszcze
raz funkcję InitInstance(), kliknij prawym przyciskiem jej nazwę i wybierz View Call
Hierarchy. W oknie komunikatów zobaczysz drzewo, jak na rysunku 1.9.

Rysunek 1.9.
Okno hierarchii
wywołań

Można z niego wyczytać, że funkcja InitInstance() była wołana przez funkcję wWinMain().
InitInstance() z kolei wywołała CreateWindow(), ShowWindow() i UpdateWindow().
Rozdział 2.
Struktura programów C++
i C++/CLI

Programy korzystające z konsoli


w VC++ 2012
Do tej pory mówiliśmy o programowaniu ogólnie, natomiast od tego rozdziału wejdziemy
już w strukturę języka C++. Środowiska typu IDE (ang. Integrated Development
Environment) ułatwiają pisanie aplikacji poprzez jej budowę w trybie graficznym z goto-
wych komponentów. Jednak nawet budując aplikacje w ten sposób, nie unikniesz pisania
części kodu. Zresztą myślę, że każdy programista powinien zacząć od poznania pod-
staw składni języka, a później pisania najpierw prostych aplikacji. Dlatego w pierwszych
rozdziałach spróbuję przedstawić podstawy języka C++ i C++/CLI. Tam, gdzie wy-
stępują różnice między tymi językami, starałem się wyjaśnić, na czym one polegają.
Zapomnijmy o graficznym środowisku i spróbujmy sami stworzyć strukturę programów.
Będziemy tworzyć projekty typu CLR Console Application i win32 Console Application.
Są to projekty tworzone w C++/CLI lub win32 API, ale bez wykorzystania środowiska
graficznego. Projekty CLI wykorzystamy do prezentacji struktury programów wpisanych
w C++/CLI, a win32 do standardowego C++.

Ogólna postać programu pisanego


w C++
Każdy program C++ zawiera pewien stały układ elementów. Nawet w przypadku naj-
prostszego projektu konsolowego podstawowa struktura programu jest tworzona przez
środowisko VC++ 2012. Zacznijmy od projektu win32. Aby go utworzyć, wykonaj
przykład 1.1 z poprzedniego rozdziału. VC++ 2012 wygeneruje następujący kod:
30 Microsoft Visual C++ 2012. Praktyczne przykłady

// test.cpp Defines the entry point for the console application.


//
#include "stdafx.h"

int _tmain(int argc, _TCHAR* argv[]) {


return 0;
}

Program rozpoczyna się nagłówkiem, w którym znajdują się dyrektywy określające


konieczność dołączenia do programu dodatkowych plików, a także definiujące inne
wartości niezbędne do jego działania, na przykład makra. Potrzeba załączania do pro-
gramu innych plików wynika ze schematu programowania w C++. Program może
składać się z wielu plików. Pliki z rozszerzeniem .h to tak zwane pliki nagłówkowe,
zawierające deklaracje zmiennych oraz definicje funkcji i klas używanych w programie.
Właściwy plik z programem ma rozszerzenie .cpp i zawiera program korzystający z defi-
nicji i deklaracji w plikach nagłówkowych. Aby plik .cpp „wiedział”, że istnieje po-
trzebny mu plik .h, potrzebna jest dyrektywa #include. W jednym pliku nagłówkowym
mogą być dyrektywy załączające inne pliki .h. Tu mamy tylko jeden plik nagłówkowy
stdafx.h. Jest to plik, który załącza inne często stosowane pliki nagłówkowe. Pliki te
są już skompilowane, dzięki czemu Twój program skompiluje się szybciej. Jest to tak
zwany mechanizm precompiled headers.

Dalej mamy funkcję main() (tu _tmain()). Jest to specjalna funkcja, która musi istnieć
w każdym programie C++. Od funkcji main() rozpoczyna się wykonywanie programu.
Najprostszy program w C++ składa się z funkcji main(), która nie przyjmuje żadnych
argumentów i nic nie robi.
int main() {}

Jedyną pracą, którą wykonuje funkcja main() wygenerowana przez kompilator, jest
jawne zwrócenie wartości zero za pomocą instrukcji return. Jest to sygnalizacja prawi-
dłowo zakończonego programu. Zgodnie ze standardem C++ funkcja main() nie musi
zawierać jawnej instrukcji return.

Przeanalizujmy teraz wynik wykonania przykładu 1.2 z poprzedniego rozdziału, czyli naj-
prostszy program w C++/CLI. Po utworzeniu projektu główny plik aplikacji z funkcją
main()jest również tworzony przez środowisko. Znajduje się on w pliku o nazwie
takiej jak nazwa projektu i rozszerzeniu .cpp. Jeżeli założymy, że mamy projekt aplikacji
o nazwie PR_1_2, to zawartość tego pliku będzie następująca:
// PR_1_2.cpp main project file.

#include "stdafx.h"

using namespace System;

int main(array<System::String ^> ^args)


{
Console::WriteLine(L"Hello World");
return 0;
}
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 31

Widzimy już, że program wygląda podobnie do programu konsolowego win32. Naj-


pierw są nagłówki, ale następnie występuje linia, której w poprzednim przykładzie nie
było. Jest to tak zwana dyrektywa using, mówiąca kompilatorowi, że będziemy uży-
wać wymienionej przestrzeni nazw. W tym przykładzie jest to przestrzeń nazw System.
Zawiera ona bardzo wiele często wykorzystywanych funkcji i klas. Możesz w tym miej-
scu zapytać, czym w takim razie różni się przestrzeń nazw od pliku nagłówkowego —
on przecież też zawierał na przykład definicje funkcji. Otóż przestrzeń nazw jest tylko
zbiorem na przykład funkcji i musi być gdzieś zadeklarowana. Plik nagłówkowy na-
tomiast jest plikiem z kodem, zawarte w tym kodzie funkcje mogą być pogrupowane
właśnie w przestrzenie nazw. Innymi słowy, jest to tylko sposób na systematykę funkcji
i klas zadeklarowany w obrębie istniejących plików. W dalszej części pokażę to na przy-
kładzie. Następnie mamy funkcję main(), która pisze w konsoli napis „Hello World” za
pomocą metody WriteLine().Wiesz już, że metoda to funkcja, która jest częścią klasy.
Jeśli tak, to powinna być wywoływana na obiekcie klasy, a tutaj tak nie jest. Jest to spe-
cjalna metoda, zwana statyczną. Co to jest metoda statyczna, wytłumaczę w dalszej części
książki.

Po ekspresowym omówieniu struktury programu zajmiemy się jeszcze raz po kolei jego
częściami dokładniej, zaczynając od nagłówka.

Dyrektywy
Dyrektywa #include
Dyrektywa #include mówi kompilatorowi, że w tym miejscu ma zostać dołączony plik
z dodatkowym kodem, na przykład z definicjami funkcji użytych w programie.

Nazwy plików nagłówkowych pochodzących z języka C należy poprzedzić literą c. I tak


na przykład plik nagłówkowy math.h załączamy dyrektywą:
#include <cmath>
W przypadku załączania własnych plików nagłówkowych ich nazwy piszemy w cudzy-
słowie i z rozszerzeniem.
#include "plik.h"

W VC++ 2012 mamy standardowo dołączone dwa pliki nagłówkowe: Form1.h (w apli-
kacjach okienkowych) oraz stdafx.h (zarówno w aplikacjach okienkowych, jak i kon-
solowych). Pierwszy z nich to plik z definicją klasy głównego okna programu. Na tym
pliku będziemy najczęściej operować, pisząc aplikacje.

Drugi plik, stdafx.h, jest plikiem, w którym można umieścić wszystkie pliki nagłówkowe,
z jakich korzysta nasza aplikacja. Umieszczamy je tam za pomocą dyrektyw #include,
tak jak w programie głównym. Korzyść z takiego rozwiązania jest taka, że plik ten
implementuje technikę prekompilowanych nagłówków (ang. precompiled headers) —
32 Microsoft Visual C++ 2012. Praktyczne przykłady

w skrócie chodzi o to, że kod z tego pliku kompilowany jest tylko raz, a później załączany
do plików aplikacji w postaci już skompilowanej, co przyspiesza proces kompilacji
w dużych projektach.

Jeżeli mechanizm prekompilowanych nagłówków jest włączony i plik stdafx.h jest


ustawiony jako plik zawierający nagłówki do prekompilacji, to musi on być załączony
jako pierwszy do wszystkich plików .cpp w projekcie. Kompilator nie kompiluje bo-
wiem żadnego kodu występującego wcześniej niż dyrektywa #include <stdafx.h>.
Mechanizm prekompilowanych nagłówków można włączać i wyłączać w menu Project\
Properties. W lewym panelu wybieramy ścieżkę Configuration Properties\C/C++\
Precompiled Headers.

Przykład 2.1

Sprawdź mechanizm dołączania pliku nagłówkowego iostream na przykładzie strumienia


do konsoli cout.

Rozwiązanie
W tym przykładzie chodzi po prostu o to, żeby napisać tekst w okienku konsoli. Żeby to
zrobić, wysyłamy tekst do strumienia cout za pomocą operatora <<. Strumień cout zde-
finiowany jest w pliku iostream. Na razie brzmi to trochę tajemniczo, ale za chwilę zo-
baczysz, jak tego dokonać, a w dalszej części książki te pojęcia staną się zrozumiałe.

Zacznij od utworzenia aplikacji konsolowej win32 według przykładu 1.1.

W utworzoną funkcję t_ main() wpisz na początku dwie linie. Pierwsza z nich będzie
wypisywała tekst na ekranie.
std::cout<<"To chcę napisać"<<std::endl;

Omówię tę linię po kolei: std oznacza, że strumień cout znajduje się w standardowej
przestrzeni nazw. Podwójny dwukropek to tak zwany operator zasięgu. Mówi on o tym,
że cout znajduje się w przestrzeni nazw std. Zgodnie ze standardem C++ musi być to
jawnie wskazane. Operator << wysyła do strumienia to, co jest po jego prawej stronie,
czyli napis „To chcę napisać”. Manipulator endl wysyła do strumienia znak końca linii.
On też należy do przestrzeni std; dzięki niemu nasz napis będzie w oddzielnej linii.

Druga instrukcja zatrzymuje okno konsoli, aby nie zamknęło się zaraz po wykonaniu
programu.
system("pause");

Cała funkcja t_main() powinna teraz wyglądać tak:


int _tmain(int argc, _TCHAR* argv[])
{
std::cout<<"To chcę napisać"<<std::endl;
system("pause");

return 0;
}
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 33

Skompiluj i uruchom program. Możesz to zrobić na trzy równoważne sposoby. Pierwszy


to kliknięcie zielonego trójkąta w górnym pasku narzędziowym. Ten trójkąt wygląda
jak znak Odtwarzanie (Play) w sprzęcie audio/wideo. Drugi sposób to wybranie z menu
opcji Debug/Start debugging, trzeci to naciśnięcie F5. Niestety, program się nie uruchomi.
Zamiast konsoli zobaczysz w oknie komunikatów kilka błędów, a wśród nich taki:
error C2065: 'cout' : undeclared identifier

Oznacza on, że kompilator nie wie, co znaczy cout, ponieważ nie ma odpowiedniego pliku
nagłówkowego. Załączmy go więc dyrektywą #include, umieszczając ją po istniejącej dy-
rektywie (patrz wyżej uwaga o prekompilowanych nagłówkach). Nasz mały blok
#include powinien wyglądać tak:
#include "stdafx.h"
#include <iostream>

Skompiluj program jeszcze raz. Teraz wszystko będzie działać, a napis pojawi się w oknie
konsoli. Naciśnięcie dowolnego klawisza zakończy program.

Dyrektywa #define
Następną dyrektywą jest #define. Definiuje ona tzw. makrodefinicję. W najprostszym
przypadku jest to identyfikator, za który każdorazowo będzie podstawiany określony
fragment kodu, na przykład:
#define liczba_dwa 2

Ile razy napiszemy w kodzie programu liczba_dwa, słowa te podczas wstępu do kom-
pilacji, tak zwanej prekompilacji preprocesora, zostaną zastąpione cyfrą 2.

Makrodefinicja może mieć również parametry, można przekazać do niej na przykład


liczbę. Ta liczba zostanie podstawiona we wskazanym miejscu makrodefinicji i całe
działanie będzie wpisane do kodu programu.

Przykład 2.2

Napisz makro dodające dwie liczby.

Rozwiązanie
Zacznij od utworzenia aplikacji konsolowej win32 według przykładu 1.1. Znowu bę-
dziemy pisali wyniki w okienku konsoli, więc w bloku nagłówków wpisz dyrektywę:
#include <iostream>

Poniżej tej linii wpisz definicję makra, które nazwałem dodaj.


#define dodaj(a,b) a+b
34 Microsoft Visual C++ 2012. Praktyczne przykłady

W makro z parametrami wpisujemy najpierw nazwę z listą parametrów w nawiasie,


a następnie treść makra. W funkcji _tmain() powołamy się na to makro. Zmień funkcję
_tmain(), aby wyglądała jak niżej:
int _tmain(int argc, _TCHAR* argv[])
{
std::cout<<dodaj(2,3)<<std::endl;
system("pause");
return 0;
}

Uruchom program. Pojawi się okno konsoli z napisem:


5
Aby kontynuować, naciśnij dowolny klawisz . . .

Jak łatwo zrozumieć, jest to wynik dodawania liczb 2 i 3. Pewnie już widzisz, że wywoła-
nie makra dodaj znajduje się w linii:
std::cout<<dodaj(2,3)<<std::endl;

Działanie polega na tym, że w pierwszej fazie kompilacji linia ta zostanie zapisana jako:
std::cout<<2+3<<std::endl;

Odpowiedzialny jest za to tak zwany preprocesor. Przetwarza on plik przed rozpoczęciem


właściwej kompilacji.

Ponieważ omawiam makra na samym początku, możesz dojść do wniosku, że jest to


często stosowany mechanizm w C++. Nie jest to prawdą. Makra to czasami skuteczne,
ale zawsze bardzo niebezpieczne narzędzie, choćby dlatego że preprocesor mechanicznie
zastępuje nazwę treścią makra. Jako parametr można więc wpisać cokolwiek. Nasze
makro z przykładu 2.2 można wywołać na przykład tak:
dodaj(5,”mandarynki”)

Program się wykona, ale wynik będzie teraz reprezentował coś innego niż dodawanie
liczb. Nie zachęcam więc do stosowania makr — C++ oferuje inne mechanizmy do
tych samych celów.

Dyrektywa #ifdef — kompilacja warunkowa


Ponieważ w C++ mamy możliwość kompilacji tego samego projektu za pomocą róż-
nych kompilatorów, bardzo przydatne są dyrektywy kompilacji warunkowej. Umożli-
wia ona kompilację różnych fragmentów kodu w zależności od wartości pewnej stałej
lub zdefiniowania pewnego makra. Jeżeli chcemy tylko sprawdzić, czy dane makro
jest zdefiniowane, piszemy:
#ifdef nazwa_makra
kod jaki należy skompilować jeśli makro istnieje
#endif
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 35

W ten sposób kompilujemy tylko te fragmenty, które są „zrozumiałe” dla danego kom-
pilatora. Jest to jedna z niewielu sytuacji, kiedy makra się przydają. Szczególnie w dobie
programów wieloplatformowych kompilacja warunkowa jest bardzo na miejscu. Zwykle
kompilator czy też platforma (system operacyjny) posiadają zdefiniowane makra, po
których można rozpoznać, jaki to system, i określić jego wersje. Identyfikacja wyłącznie
systemu jest bardziej „zgrubna” i można jej używać, jeśli mamy pewność, że dany kod
skompiluje się za pomocą wszystkich kompilatorów w danym systemie. Jeśli pewno-
ści nie mamy, to lepiej identyfikować dany kompilator. Visual C++ 2012 posiada zde-
finiowanych kilka użytecznych makr. Zestawiłem je dalej w tabeli, ale zacznijmy od
przykładu. Załóżmy, że chcesz napisać program, który wydasz w stanie źródłowym.
Końcowy użytkownik będzie mógł go skompilować za pomocą VC++ 2012 lub kom-
pilatora GNU C++. Podczas pisania okazało się jednak, że miejscami musisz użyć
kodu, który jest zrozumiały tylko dla jednego kompilatora. Aby pokonać tę trudność,
musiałbyś wydać dwie wersje kodu: jedną przeznaczoną dla VC++ 2012 i jedną dla
GNU C++. Jest to nieco niewygodne. I tu właśnie można posłużyć się kompilacją warun-
kową. Jednym z predefiniowanych makr VC++ 2012 jest _MSC_VER, które nie tylko identy-
fikuje ten kompilator, ale podaje też jego wersję. W GNU C++ wersję programu mamy
w dwóch makrach: __GNUC__ podaje tak zwany numer główny wersji, a __GNUC_MINOR__
numer dodatkowy wersji.

Przykład 2.3

Napisz program podający wersję kompilatora VC++ 2012 lub GNU C++, którym go
skompilowano.

Rozwiązanie
W przykładzie zakładam, że masz dostęp do poprawnie zainstalowanego kompilatora
GNU C++. Instalacja i opis tego kompilatora przekracza zakres tej książki. Zacznij od
utworzenia aplikacji konsolowej win32 według przykładu 1.1. Znowu będziemy wpi-
sywali wyniki w okienku konsoli, więc w bloku nagłówków wpisz dyrektywę:
#include <iostream>

Przechodzimy do funkcji _tmain(). Jak już pisałem, standardowa nazwa głównej funkcji
to main(). Kompilator VC++ 2012 nazywa ją _tmain(), niestety GNU C++ nie rozpo-
zna takiej nazwy. Jeśli chcemy, żeby program był naprawdę wieloplatformowy, to
oryginalna składnia powinna być zachowana dla obydwu kompilatorów. Napiszemy
więc dwie funkcje main() zamknięte w swoich blokach #ifdef/#endif.

Najpierw za pomocą #ifdef zbadamy, czy jest zdefiniowane makro _MSC_VER. Jeśli tak,
to mamy do czynienia z kompilatorem Visual C++. Funkcja main() z parametrami dla
tego kompilatora wyświetli odpowiedni napis w konsoli, a następnie wypisze zawartość
makra _MSC_VER, czyli wersję kompilatora.
#ifdef _MSC_VER
int _tmain(int argc, _TCHAR* argv[])
{
36 Microsoft Visual C++ 2012. Praktyczne przykłady

std::cout<<"Visual C++ wersja ";


std::cout<<_MSC_VER<<std::endl;
system("pause");
return 0;
}
#endif

Podobnie postąpimy, aby wykryć kompilator GNU C++. Tutaj sprawdzamy definicję
makra __GNUC__. Jeśli takowe istnieje, to funkcja main() w postaci akceptowanej przez ten
kompilator zapisuje na konsoli informacje o wykryciu kompilatora GNU i jego wersji.
#ifdef __GNUC__
int main(int argc, char* argv[])
{
std::cout<<"GNU C++ wersja ";
std::cout<<__GNUC__<<"."<<__GNUC_MINOR__<<std::endl;
system("pause");
return 0;
}
#endif

Wszystko już gotowe. Jeśli skompilujesz program, używając VC++ 2012, w oknie
konsoli zobaczysz:
Visual C++ wersja 1700
Aby kontynuować, naciśnij dowolny klawisz . . .

Liczba 1700 oznacza wersję kompilatora 17.00. Teraz skompilujemy przykład z użyciem
kompilatora gcc. Niestety, trzeba ręcznie oznaczyć komentarzem dyrektywę #include
"stdafx.h". Dopiero w takiej postaci kod skompiluje się bez błędów.
//#include "stdafx.h" //tu wstaw znaki komentarza
#include <iostream>

#ifdef _MSC_VER
int _tmain(int argc, _TCHAR* argv[])
{
std::cout<<"Visual C++ wersja ";
std::cout<<_MSC_VER<<std::endl;
system("pause");
return 0;
}
#endif

#ifdef __GNUC__
int main(int argc, char* argv[])
{
std::cout<<"GNU C++ wersja ";
std::cout<<__GNUC__<<"."<<__GNUC_MINOR__<<std::endl;
system("pause");
return 0;
}
#endif
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 37

Nagłówka stdafx.h nie można zamknąć w bloku kompilacji warunkowej. Nie pozwala na
to mechanizm prekompilowanych nagłówków. Jeśli ten mechanizm jest włączony, VC++
ignoruje wszystko przed linią:
#include "stdafx.h"

Dyrektywa #ifdef _MSC_VER wstawiona przed #include "stdafx.h" zostanie pominięta,


natomiast #endif kończące blok jest brane pod uwagę. Spowoduje to błąd kompilacji
i niespodziewane #endif.

Po wstawieniu komentarza kompilacja przykładu pod GNU C++ spowoduje wyświetlenie


napisu:
GNU C++ wersja 3.4
Aby kontynuować, naciśnij dowolny klawisz . . .

Inne użyteczne makra zdefiniowane w Visual C++ 2012:

Makra standardowe, zdefiniowane w większości kompilatorów C++


Makro Opis
__LINE__ Numer linii, w której wywołano makro.
__FUNCTION__ Nazwa funkcji, w której wywołano makro.
__DATE__, __TIME__ Odpowiednio data i czas ostatniej kompilacji.
Makra dostępne tylko w VC++ 2012
Makro Opis
_MFC_VER Aktualna wersja biblioteki Microsoft Foundation Classes.
_WIN64 Zdefiniowane, jeśli aplikacja jest kompilowana w trybie 64-bitowym.
_DEBUG Zdefiniowane, jeśli aplikacja jest kompilowana w trybie debugowania.

Typy zmiennych
W C++ istnieje wiele typów danych. Są to typy proste, takie jak arytmetyczne i wylicze-
niowe, oraz typy złożone (klasy, struktury). Poniżej opisuję typy proste zmiennych.

Zmienne typu int


int to podstawowy typ całkowity. Jest to 32-bitowa liczba całkowita, która może przybie-
rać wartości w przedziale od –2 147 483 648 do 2 147 483 647. Istnieją także typy:
__int8, __int16, __int32, __int64 o reprezentacji 8, 16, 32, 64 bitów i odpowiednio więk-
szym lub mniejszym zakresie wartości. Zmienne podstawowego typu int deklarujemy tak:
int zmienna;
38 Microsoft Visual C++ 2012. Praktyczne przykłady

Możemy taką zmienną też od razu inicjalizować wartością, na przykład:


int zmienna = 20;

Zmienne typu float


float to 32-bitowy typ zmiennoprzecinkowy. Może przybierać wartości rzeczywiste z prze-
działu od 1,2⋅(10–38) do 3,4⋅(1038), symetrycznie dla liczb dodatnich i ujemnych.

Typ double
Jeżeli obliczenia na zmiennych float mają niewystarczającą dokładność, możemy użyć
typu double. Jest to 64-bitowa liczba zmiennoprzecinkowa. Jej dozwolone wartości to
2,2⋅(10–308) do 1,7⋅(10308), symetrycznie dla liczb dodatnich i ujemnych.

Typ char
Jest to 8-bitowa zmienna całkowita o dozwolonych wartościach od –128 do +127. Zwykle
służy ona do przechowywania pojedynczych znaków ASCII.

W .NET Framework mamy inne typy danych, z których można korzystać w C++/CLI;
ich funkcjonalność odpowiada tym ze standardowego C++. Porównanie typów zawiera
tabela 9.5 z rozdziału 9.

Modyfikatory typów
Jeżeli z jakichś względów typ podstawowy nie odpowiada naszym oczekiwaniom, może-
my zastosować modyfikator typu, zmieniający reprezentację liczby. Mamy trzy modyfika-
tory podstawowe: short, long i unsigned.

Modyfikator long wydłuża reprezentację zmiennej (zwiększa liczbę bitów pamięci prze-
znaczonych na tę cyfrę). Skutkuje to oczywiście zwiększeniem zakresu zmiennej i do-
kładności obliczeń. I tak na przykład zmienna long double będzie miała 80 bitów za-
miast 64.

Modyfikator short zmniejsza liczbę bitów. I tak na przykład typ short int ma 16 bitów
zamiast 32 dostępnych dla typu int.

Modyfikator unsigned powoduje, że liczba może przybierać tylko wartości dodatnie.


Przedział wartości nie jest symetryczny, ale za to dwa razy dłuższy w stronę liczb do-
datnich. I tak na przykład typ char może przybierać wartości od –128 do +127, a typ
unsigned char od 0 do 255.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 39

Rzutowanie (konwersja) typów


Konwersja typów jest przepisaniem wartości ze zmiennej jednego typu do innego.
W praktyce numerycznej występują na przykład przypadki podstawienia liczby całko-
witej do zmiennej zmiennoprzecinkowej. Uruchamia to mechanizm konwersji i może
być stosowane wtedy, kiedy nie powoduje błędu. Kiedy przesuwamy wartości ze
zmiennej węższej do szerszej, prawdopodobnie nic się nie stanie. Na przykład, jeśli pod-
stawimy wartość typu int a=3 do zmiennej float b, otrzymamy b=3.0 i żadne dane
nie zostaną stracone. Jednak odwrotnie, kiedy ze zmiennej float zostanie wzięta tylko
ta część, która „zmieści się” w zmiennej int, część liczby po przecinku zostanie utracona.
Czasem takie konwersje są stosowane (powiemy o tym niżej). Częściej jednak stosuje
się rzutowanie z typu mniej ogólnego na bardziej ogólny, na przykład z int na float
bądź z float na double.

Programista może sam nakazać rzutowanie do wybranego typu — mamy wtedy konwer-
sję jawną. W języku C++ służy do tego operator (). I tak na przykład:
zm_float=(float) zmienna_int;

konwertuje zmienną zmienna_int na typ float.

Standard C++ posiada kilka rodzajów jawnego rzutowania. Dzięki temu dokładniej wia-
domo, w jaki sposób rzutowanie zostało wykonane. Tutaj pokażę trzy rodzaje, w tym
jeden specyficzny dla Visual C++.

Rzutowanie static_cast
Ten rodzaj powinien być stosowany, kiedy mamy jasno określone typy, między którymi
stosujemy rzutowanie. Tak jest na przykład w sytuacji, gdy chcesz zamienić typ zmien-
noprzecinkowy na całkowity w celu redukcji rozmiaru danych. Jak już pisałem, taka
zamiana grozi utratą danych, więc musisz wiedzieć, co robisz. Można też użyć tego rzu-
towania do konwersji typów w obrębie hierarchii klas. Wrócimy do rzutowania w odpo-
wiednich rozdziałach. Składnia static_cast prezentuje się następująco:
typ_docelowy ZmiennaPoKonwersji = static_cast<typ_docelowy>(ZmiennaDoKonwersji);

Przykład 2.4

Wykonaj rzutowanie kilku zmiennych za pomocą static_cast.

Rozwiązanie

Kolejny raz zaczynamy od utworzenia aplikacji konsolowej win32 według przykładu 1.1.
Będziemy zapisywali wyniki w okienku konsoli, więc w bloku nagłówków wpisz dyrek-
tywę
#include <iostream>
40 Microsoft Visual C++ 2012. Praktyczne przykłady

Przechodzimy do _tmain(). W tym przykładzie pokażę najprostsze konwersje między ty-


pami danych. Od razu napiszę całą funkcję, a później omówimy kolejne linie.
int _tmain(int argc, _TCHAR* argv[])
{
//rzutowanie jawne
double a=2.6;
float b;
b=3.4;
int c = static_cast<int>(a);
int d = static_cast<int>(3.4);
std::cout<<"double a=2.6 na int c="<<c<<std::endl;
std::cout<<"wartość 3.4 na int d="<<d<<std::endl;

//rzutowanie niejawne
int f=a;
int g=3.4;
std::cout<<"To samo niejawnie:"<<std::endl;
std::cout<<"double a=2.6 na int f="<<f<<std::endl;
std::cout<<"wartość 3.4 na int g="<<g<<std::endl;
system("pause");
return 0;
}

Program składa się z dwóch bloków. W pierwszym konwertujemy jawnie zmienną typu
double na int, a następnie typ float również na int. W obu przypadkach przychodzi
nam z pomocą static_cast. W drugim bloku robimy to samo, ale za pomocą niejawnego
rzutowana, które wykonuje konwersję w sposób niewidoczny. Po uruchomieniu mamy:
double a=2.6 na int c=2
wartość 3.4 na int d=3
To samo niejawnie:
double a=2.6 na int f=2
wartość 3.4 na int g=3
Aby kontynuować, naciśnij dowolny klawisz . . .

Jak widać, wyniki są takie same. Na tym etapie trudno pokazać to na przykładzie, jednak
static_cast powoduje lepsze wykorzystanie pamięci w wewnętrznych procesach kon-
wersji i jest bezpieczniejsze.

Rzutowanie const_cast
Jest to sposób na pozbycie się identyfikatora const, czyli w pewnym sensie zamiany stałej
na zmienną. Takie zmiany są niebezpieczne, więc znowu obowiązuje zasada, że powinie-
neś dokładnie wiedzieć, co chcesz osiągnąć. Użycie const_cast jest bezpieczne w zasa-
dzie tylko w jednym przypadku. Aby to pokazać, musimy najpierw nauczyć się trochę
o funkcjach. Aby w ogóle uruchomić jakiś przykład z tą konwersją, potrzebne jest zrozu-
mienie tak zwanych wskaźników, więc na razie zostańmy przy teorii.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 41

Rzutowanie safe_cast
Jest to rzutowanie dostępne tylko w C++/CLI. Jego funkcjonalność jest podobna jak
static_cast, z tym że oferuje lepszą obsługę błędów.

Rzutowanie dynamic_cast
Ten sposób rzutowania wymaga znajomości mechanizmu dziedziczenia klas wraz z funk-
cjami wirtualnymi; zajmiemy się nim w rozdziale o klasach.

Typ wyliczeniowy
Dzięki typowi wyliczeniowemu możemy zdefiniować kilka stałych i przyporządkować im
kolejne liczby naturalne. Na przykład przy pisaniu programu kalendarza moglibyśmy
potrzebować stałych oznaczających nazwy miesięcy i przyporządkowanych im kolejnych
numerów.

Możemy to zrobić tak:


const int styczen = 0;
const int luty = 1;
const int marzec = 2;
.....
const int grudzien = 11;

Słowo const przed deklaracją zmiennej oznacza, że jej wartość nie będzie się zmieniała
w ciągu działania programu, czyli że jest to stała. Kod, jaki napisaliśmy wyżej, jest jednak
dosyć długi. To samo możemy osiągnąć poprzez typ wyliczeniowy enum:
enum miesiace
{styczen,luty,marzec,kwiecien,maj,czerwiec,lipiec,sierpien,wrzesien,pazdziernik,
listopad,grudzien);

Od teraz możemy używać nazw miesięcy jako stałych o wartości od 0 do 11. Możemy
także zadeklarować zmienną typu miesiace, która będzie mogła przybierać tylko wartości
od 0 do 11:
miesiace miesiac = styczen;

Silnie typowane wyliczenia


Po lekturze działu o typie wyliczeniowym możesz zadać sobie pytanie, jakiego typu
są składowe wyliczenia. Pomyślisz, że są typu całkowitego i rzeczywiście są one niejaw-
nie konwertowane do tego typu. Udowodnimy to na przykładzie. Przy okazji zajmiemy się
zasięgiem (widocznością) elementów wyliczeń w programie.
42 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 2.5

Utwórz dwa wyliczenia i porównaj wartości ich składowych.

Rozwiązanie
Zacznij od utworzenia aplikacji konsolowej win32 według przykładu 1.1. Konieczność pi-
sania wyników w okienku konsoli wymaga załączenia w bloku nagłówków dyrektywy:
#include <iostream>

Poniżej zadeklarujemy dwa wyliczenia: pierwsze to już znane miesiace, a drugie nazwiemy
owoce. Nie ma to tutaj większego znaczenia.
enum miesiace
{styczen,luty,marzec,kwiecien,maj,czerwiec,lipiec,sierpien,wrzesien,pazdziernik,
listopad,grudzien};

enum owoce
{jablka,gruszki,sliwki,wisnie,pomarancze};

Przechodzimy do funkcji _tmain(). Porównamy element jablka z wyliczenia owoce z ele-


mentem kwiecien z wyliczenia miesiace. Oczywiście, zdaję sobie sprawę, że jest to trochę
surrealistyczne porównanie, ale w tym wypadku nie chodzi o sens. Następnie przepiszemy
zawartość elementu sliwki wyliczenia owoce do zmiennej typu całkowitego. Zatem
w funkcji _tmain() napiszmy:
int _tmain(int argc, _TCHAR* argv[])
{
int a;
if (jablka<kwiecien) std::cout<<"jabłka < kwiecień - cokolwiek to znaczy "<<std::endl;
a=sliwki;
std::cout<<"a= "<<a<<std::endl;
system("pause");
return 0;
}

Uruchom aplikację. W oknie konsoli otrzymasz:


jabłka < kwiecień — cokolwiek to znaczy
a= 2
Aby kontynuować, naciśnij dowolny klawisz . . .

W tym przykładzie udowodniliśmy aż trzy właściwości wyliczeń. Po pierwsze, elementy


różnych wyliczeń są porównywalnego typu. Druga właściwość może sprawić więcej kło-
potów. Zauważ, że na elementy wyliczeń powołujemy się bez oznaczenia, do którego
wyliczenia należą. To znaczy, że gdyby wyliczenia miesiace i owoce zawierały element
o tej samej nazwie, byłby konflikt. Programiści mówią na taką sytuację, że zasięg wyli-
czeń jest globalny. Po trzecie, elementy wyliczeń są niejawnie konwertowane do zmiennej
całkowitej i bez problemu ich zawartość została do takiej zmiennej przepisana.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 43

Konwersje, jak w poprzednim przykładzie, sprzyjają różnym błędom w programach.


Lepiej, żeby elementy wyliczenia miały ściśle zdefiniowany typ, a ich zasięg ograniczał
się do samego obiektu wyliczenia. Temu mają służyć silnie typowane wyliczenia wpro-
wadzone w standardzie C++11. Do definicji wyliczenia dochodzi słowo class, następnie
nazwa wyliczenia, operator zasięgu : i typ. Używając nowej składni, nasze wyliczenia
można zapisać następująco:
enum class miesiace : unsigned long
{styczen,luty,marzec,kwiecien,maj,czerwiec,lipiec,sierpien,wrzesien,pazdziernik,
listopad,grudzien};
enum class owoce : unsigned long
{jablka,gruszki,sliwki,wisnie,pomarancze};

Próba porównania elementu jablka do elementu kwiecien spowoduje błąd. Kompi-


lator nie wie, co to są jablka czy kwiecien. Elementy te są znane tylko w nawiasach
klamrowych wyliczenia; chcąc się na nie powołać w programie, trzeba napisać miesiace::
kwiecien lub owoce::jablka. Niestety, nawet z operatorem nie uda nam się porównać
elementów różnych wyliczeń. Prawidłowe jest jedynie porównanie w obrębie jednego
wyliczenia, na przykład:
if (owoce::jablka<owoce::wisnie) {warunek_spelniony}

Próba przepisania zawartości elementu sliwki (nawet z operatorem) do zmiennej int


też nie spotka się ze zrozumieniem kompilatora. Możemy to poćwiczyć tylko w teorii,
bo Visual C++ nie wspiera typowanych wyliczeń w ujęciu standardowym. Mamy nato-
miast w C++/CLI klasę enum, która ma podobną funkcjonalność.

Przykład 2.6

Napisz przykład w C++/CLI z wyliczeniami stosującymi klasę enum.

Rozwiązanie
Będziemy potrzebować projektu konsolowego C++/CLI; przyrządź go według przepisu
z przykładu 1.2. Składnia wyliczeń tworzonych klasą enum jest taka sama jak w standar-
dzie C++11.
public enum class miesiace
{styczen,luty,marzec,kwiecien,maj,czerwiec,lipiec,sierpien,wrzesien,pazdziernik,
listopad,grudzien};

public enum class owoce


{jablka,gruszki,sliwki,wisnie,pomarancze};

W funkcji main() porównamy dwa elementy, ale teraz można to zrobić tylko w obrębie
jednego wyliczenia. Podstawienie uda się tylko między elementami tego samego typu.
Zmienna a musi być typu miesiace.
int main(array<System::String ^> ^args)
{
miesiace a;
if (owoce::jablka<owoce::wisnie) Console::WriteLine("jabłka < wiśnie -
cokolwiek to znaczy ");
44 Microsoft Visual C++ 2012. Praktyczne przykłady

a=miesiace::luty;
Console::WriteLine(a);
Console::ReadLine();
return 0;
}

Oto wynik w konsoli:


jabłka < wiśnie — cokolwiek to znaczy
luty

Zauważ, że polecenie wypisania elementu wyliczenia nie wypisało liczby, ale nazwę
elementu wyliczenia.

Typizację wyliczeń można obejść za pomocą jawnego rzutowania static_cast lub


safe_cast. Dzięki temu wyliczenia będące obiektami klasy enum mogą zachowywać
się jak te sprzed wprowadzenia C++11.

Przykład 2.7

Napisz program z jawną konwersją między wyliczeniem enum a zmienną int.

Rozwiązanie
Jeszcze raz będziemy potrzebować projektu konsolowego C++/CLI według przepisu
z przykładu 1.2. Przepisz jeszcze raz wyliczenia z poprzedniego przykładu.
public enum class miesiace
{styczen,luty,marzec,kwiecien,maj,czerwiec,lipiec,sierpien,wrzesien,pazdziernik,
listopad,grudzien};

public enum class owoce


{jablka,gruszki,sliwki,wisnie,pomarancze};

W funkcji main() utworzymy dwie zmienne: jedną klasy wyliczeniowej enum miesiace
i drugą typu int. Następnie do zmiennej typu miesiace podstawimy tymczasową wartość
int, a do zadeklarowanej zmiennej int podstawimy element wyliczenia owoce. Skorzy-
stamy z rzutowań static_cast i safe_cast. Oto cała funkcja main():
int main(array<System::String ^> ^args)
{
miesiace a;
int b;
a=static_cast<miesiace>(2);
b=safe_cast<int>(owoce::wisnie);
Console::WriteLine(a);
Console::WriteLine("b="+b);
Console::ReadLine();
return 0;
}
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 45

Po uruchomieniu mamy:
marzec
b=3

Zmienna miesiace przybrała wartość dwa, której odpowiada element marzec; zmienna
b ma wartość 3, której odpowiada element wisnie.

Rzutowania posłużyły do zdjęcia zabezpieczenia zapewnionego przez klasę enum, widać


więc, że trzeba używać ich bardzo ostrożnie.

Słowo kluczowe auto,


czyli dedukcja typu
Wraz z wprowadzeniem C++11 pojawiła się możliwość samodzielnego „domyślania”
się kompilatora, jakiego typu zmienną chcemy zadeklarować. Na czym te domysły są
oparte, wyjaśni prosty przykład.

Przykład 2.8

Zadeklaruj zmienną z automatycznym rozpoznawaniem typu.

Rozwiązanie
Zaczynamy od projektu konsolowego win32 według przepisu z przykładu 1.1. Kolejny
raz będzie potrzebny nagłówek umożliwiający używanie strumienia cout, a więc w bloku
nagłówków po dyrektywie:
#include "stdafx.h" //ta linia istnieje

wpisujemy:
#include <iostream>

W funkcji _tmain() zdefiniujemy zmienną a typu int i nadamy jej wartość. Następnie
na podstawie tej zmiennej zadeklarujemy zmienną b i nadamy jej wartość zmiennej a.
Teraz za pomocą słowa kluczowego typeid rozpoznamy i wypiszemy w konsoli typ
zmiennej b. Ostatnia instrukcja zapobiegnie zamknięciu okna i da nam czas na prze-
czytanie jego zawartości.
int _tmain(int argc, _TCHAR* argv[])
{
int a=5;
auto b=a;
std::cout<<"b jest typu "<<typeid(b).name()<<" i b="<<b<<std::endl;
system("pause");
return 0;
}
46 Microsoft Visual C++ 2012. Praktyczne przykłady

Skompiluj i uruchom program. W konsoli zobaczysz:


b jest typu int i b=5
Aby kontynuować, naciśnij dowolny klawisz . . .

Teraz już widać, że zmienna b ma taki typ jak zmienna, z pomocą której jest inicjalizo-
wana. Wynika z tego, że deklaracja zmiennej słowem auto musi być połączona z jej
inicjalizacją. Na przykład taki zapis:
auto b;
jest błędny, bo kompilator nie ma żadnej wskazówki co do typu zmiennej b.

L-wartości i R-wartości
W poprzednim dziale mówiliśmy o typach danych. Każda z tych danych może mieć jakąś
wartość. Ta wartość była ukryta pod nazwą zmiennej i miała swoje miejsce w pamięci.
Zmienna może zawierać w sobie różne wartości z dozwolonego zbioru. Taka wartość
nazywa się l-wartością; za chwilę wyjaśnię znaczenie tej nazwy. Z drugiej strony,
można wyobrazić sobie też wartość, która jest „bezpośrednia”, na przykład kiedy pi-
szemy gdzieś w kodzie programu jawnie liczbę 2. Liczba 2 nie jest zmienną, ale kon-
kretną liczbą, nie można pod nią podstawić nic innego. Nazywamy ją r-wartością.
Taką wartością jest również wynik jakiegoś wyrażenia, na przykład a+b. Możesz po-
myśleć, że l-wartość to „zmienna”, a r-wartość to stała, nie jest to jednak reguła. L-wartość
może być przecież stałą, wystarczy modyfikator const.

Różnica między l-wartością a r-wartością polega na tym, że ta pierwsza istnieje pod


pewną określoną nazwą i zajmuje stale jakąś komórkę pamięci. Natomiast r-wartość
jest zwykle wynikiem jakiegoś wyrażenia, a jej występowanie jest miejscowe. Wy-
stępuje zwykle tylko w miejscu obliczenia wartości tego wyrażenia.

Nazwy „l-wartość” i „r-wartość” pochodzą od stron, po których mogą one występować


względem operatora przypisania =. L-wartość może stać po lewej lub prawej stronie.
Weźmy na przykład wyrażenie a=b. Przypisuje ono wartość zmiennej b do zmiennej a.
Zarówno a, jak i b są l-wartościami. R-wartość może stać tylko po prawej stronie operato-
ra przypisania. Zapis a=2 jest poprawny, ale 2=a już nie ma sensu, ponieważ w C++
operator przypisania nie jest równocześnie operatorem porównania, jak to znamy z mate-
matyki. Dokładniej opisuje to następny dział.

Operatory
Operatory są reprezentacją operacji na zmiennych. W C++ mamy kilka typów operatorów
— są operatory arytmetyczne, logiczne, porównania zasięgu i inne. Możemy je także
podzielić na jednoargumentowe i dwuargumentowe. Na przykład operator dodawania
jest dwuargumentowy, bo wynik uzyskiwany jest z dwóch liczb. Oznaczenia i funkcje
najczęściej używanych operatorów przedstawia tabela 2.1.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 47

Tabela 2.1. Zestawienie operatorów


Symbol operatora Znaczenie Przykład
Operatory arytmetyczne
+ Dodawanie c=a+b
- Odejmowanie c=a-b
* Mnożenie c=a*b
/ Dzielenie c=a/b
% Reszta z dzielenia c=a%b
Operatory porównania
== Równy a==b
!= Nierówny a!=b
> Większy a>b
< Mniejszy a<b
>= Większy lub równy a>=b
<= Mniejszy lub równy a<=b
Operatory przypisania
= Przypisanie a=b+c
+= Dodawanie z przypisaniem a+=b
-= Odejmowanie z przypisaniem a-=b
*= Mnożenie z przypisaniem a*=b
/= Dzielenie z przypisaniem a/=b
Operatory logiczne
&& Koniunkcja (AND) (a==c)&&(b==c)
|| Alternatywa (OR) (a==c)||(b==c)
<< Przesunięcie w lewo zmienna << ilość_bitów
>> Przesunięcie w prawo zmienna >> ilość_bitów
Operatory jednoargumentowe
* Operator dostępu do wartości *wskaźnik=a
& Operator adresowy adres_zmiennej=&a
! Negacja logiczna !a
++ Inkrementacja a++
-- Dekrementacja a--
Operatory dostępu
:: Zakres dostępu klasa::metoda()
. Dostęp bezpośredni instancja_klasy.zmienna
-> Dostęp pośredni wsk_na_inst_klasy->zmienna
Operator uzyskania wielkości obiektu
sizeof(obiekt) Wielkość obiektu w pamięci sizeof(zmienna)
48 Microsoft Visual C++ 2012. Praktyczne przykłady

Zwróćmy uwagę na operatory = i ==. Pierwszy z nich powoduje przypisanie, na przy-


kład wyniku działania do zmiennej. I tak c=a+b oznacza „dodaj a do b i wynik zapisz
w c”. Natomiast drugi to operator relacji, sprawdzający równość dwóch zmiennych,
na przykład c==a+b oznacza „czy c równa się a plus b”. Drugi operator wykorzystamy na
przykład w instrukcjach warunkowych, opisanych w podrozdziale noszącym tytuł
„Instrukcje”. Trzeba pamiętać o rozróżnieniu tych dwóch operatorów, ponieważ w przy-
padku ich zamiany kompilator nie generuje błędu, a jedynie ostrzeżenie.

Zapis danych do plików i odczyt z nich


za pomocą operatorów << i >>
Operator przekierowania używany jest wielokrotnie przy wypisywaniu tekstu do strumie-
nia cout, czyli na ekran konsoli. Składnia wypisania tekstu na ekran jest następująca:
cout<<"Tekst do wypisania";

Strumień cin pobiera dane z konsoli, czyli w naszym przypadku z klawiatury. Wpisanie
wartości zmiennej z klawiatury realizujemy tak:
cin>>zmienna;

W ten sam sposób możemy odczytywać tekst i wartości zmiennych z pliku i zapisywać do
niego. W tym przypadku zamiast strumieni cin i cout musimy zadeklarować strumienie
powiązane z plikami. Zobaczmy to na przykładzie.

Przykład 2.9

W standardowym C++ napisz aplikację zapisującą zmienną do pliku dyskowego.

Rozwiązanie
Utwórz aplikację konsolową win32 jak w przykładzie 1.1. W części nagłówkowej załącz
nagłówek fstream odpowiedzialny za metody operacji na plikach. Załącz go dyrektywą:
#include <fstream>

bezpośrednio po załączeniu nagłówka stdfx.h. W funkcji _tmain() najpierw tworzy-


my obiekt typu ofstream (tak zwana klasa strumieniowa do zapisu plików). Mamy tu
nowe pojęcie — konstruktor klasy. Jest to specjalna metoda wykonywana przy tworzeniu
obiektu. Może ona na przykład wpisywać jakieś wartości do pól obiektu. Tu parametrem
konstruktora jest nazwa pliku. Za pomocą operatora << zapisujemy wartość zmiennej
do pliku. Oto cały program:
#include "stdafx.h"
#include <fstream> // nagłówek zawierający funkcje operacji na plikach

using namespace std;

int _tmain(int argc, _TCHAR* argv[]) {


int zmienna_do_pliku=24;
ofstream plik("dane.dat"); // utworzenie strumienia wyjściowego (typu ofstream)
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 49

// stowarzyszonego z plikiem dane.dat


plik<<zmienna_do_pliku; // zapisanie wartości zmiennej do pliku
plik.close(); // zamknięcie pliku

return 0;
}

Po uruchomieniu w folderze roboczym programu zostanie utworzony plik dane.dat


z zapisaną wartością 24.

Przykład 2.10

W standardowym C++ napisz aplikację odczytującą zmienną z pliku na dysku.

Rozwiązanie
Początek jest taki sam jak poprzednio. Utwórz aplikację konsolową win32, jak w przykła-
dzie 1.1. W części nagłówkowej załącz nagłówek fstream odpowiedzialny za metody
operacji na plikach. Załącz go dyrektywą:
#include <fstream>

bezpośrednio po załączeniu nagłówka stdfx. W tym przypadku będą potrzebne jeszcze


dwa nagłówki. Pierwszy (conio.h) powinien zawierać funkcję getch(), która poczeka
z zamknięciem okna, aż naciśniesz klawisz, a drugi (iostream ) klasę strumieniową
cout do wypisania zmiennej na ekran po odczytaniu.
#include <iostream>
#include <conio.h>

Czekanie z otwartym oknem na naciśnięcie klawisza za pomocą getch jest alternatywnym


sposobem do instrukcji system(„pause”). Dalej jest podobnie jak przy zapisie zmiennej.
Deklarujemy obiekt klasy strumieniowej, tym razem ifstream, która jest klasą do czytania
z pliku, a następnie odczytujemy wartość operatorem >>. Cały kod wygląda jak niżej:
#include "stdafx.h"
#include <fstream> // nagłówek zawierający funkcje operacji na plikach
#include <iostream>
#include <conio.h>
using namespace std;
int _tmain(int argc, _TCHAR* argv[]) {
int zmienna_z_pliku;
ifstream plik("dane.dat"); // utworzenie strumienia wejściowego (typu ifstream)
// stowarzyszonego z plikiem dane.dat
plik>>zmienna_z_pliku; // wczytanie wartości zmiennej z pliku
plik.close(); // zamknięcie pliku
cout<<zmienna_z_pliku; //wypisanie wartości zmiennej
_getch();
return 0;
}

Za pomocą Notatnika utwórz w folderze programu plik dane.dat zawierający jedną do-
wolną liczbę całkowitą. Po uruchomieniu programu wartość z pliku pojawi się na ekranie.
50 Microsoft Visual C++ 2012. Praktyczne przykłady

Sposób zapisu i odczytu z plików w C++/CLI podany jest w rozdziale 12., „Komuni-
kacja aplikacji z plikami”.

Wskaźniki i referencje
Wartości zmiennych i stałych przechowywane są w komórkach pamięci. Dostęp do
tych komórek w C++ jest możliwy nie tylko poprzez wartość zmiennej, ale też po-
przez odwołanie do konkretnego adresu pamięci.

Wskaźniki
W C++ każdy typ zmiennej posiada skojarzony z nim typ wskaźnikowy. Typ wskaźniko-
wy tworzymy za pomocą operatora *. I tak wskaźnik do typu int będzie miał postać:
int* wsk;

Wartością zmiennej wskaźnikowej jest adres pamięci wskazywanej zmiennej. Jeżeli


chcemy, aby wskaźnik nie wskazywał na żaden adres, ustawiamy go na zero:
wsk=0;

lub
wsk=NULL;

Referencje
Aby ustawić wskazanie na istniejącą zmienną, należy do wskaźnika podstawić jej adres
uzyskany za pomocą operatora &.
int a;
wsk=&a;

Jest to operator referencji. Zwraca on adres komórki pamięci, w której przechowywa-


na jest zmienna.

Aby uzyskać dostęp do wartości zmiennej, na którą wskazuje wskaźnik, stosujemy ope-
rator *. Nazywa się on operatorem wyłuskania. I tak na przykład, jeśli wskaźnik wsk
wskazuje na zmienną a, to instrukcje:
a=5;

oraz
*wsk=5;

spowodują wpisanie liczby 5 w tę samą komórkę pamięci.

W C++/CLI wskaźniki mogą być używane tylko do typów niezarządzanych. Obiekty


zarządzane zmieniają bowiem swój adres w pamięci i wskaźnik może je zgubić. Dlatego
do obiektów zarządzanych mamy tak zwane uchwyty. Mówi o tym rozdział 11.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 51

Referencje do r-wartości
Jak już pisałem, r-wartości są wyrażeniami „miejscowymi”, a ich adres w pamięci jest
przypadkowy. Bardzo często jednak występuje konieczność przypisania r-wartości do
l-wartości. Taką sytuację mamy choćby w przypadku wyrażenia a=3. Żeby wykonać
takie działanie, aplikacja musi wykonać kopię liczby 3 i wpisać ją w adres pamięci zmien-
nej a. Jeśli jest to jedna liczba, trwa to ułamek sekundy, ale jeśli mamy tablicę z wielu
liczb, to można pomyśleć o jakimś sposobie przyspieszenia tej operacji. A gdyby tak
nie kopiować r-wartości, tylko pobrać jej adres w pamięci i zapisać go jako adres
zmiennej a? Jednak pobieranie adresów obiektów tymczasowych poprzez operator &
jest zabronione. Łatwo się zresztą domyślić dlaczego: skoro są to twory tymczasowe,
to operowanie na ich adresach jest ryzykowne. Standard C++11 wprowadza nowy typ
operatora referencji: &&, czyli referencje do r-wartości. Umożliwi nam to opisane wy-
żej przyspieszenie działania programu, trzeba tylko używać tego mechanizmu uważnie.
Na tym etapie trudno podać konkretny przykład, ponieważ wymaga on programowa-
nia obiektowego, zostańmy więc na razie tylko z tym wyjaśnieniem teoretycznym.

Wskaźniki do stałej i rzutowanie const_cast


Typ wskaźnikowy można poprzedzić modyfikatorem const, otrzymując wskaźnik do
stałej. Elementu wskazywanego przez taki wskaźnik nie można normalnie modyfikować.
Za pomocą rzutowania const_cast można pozbyć się blokady stałej i zmienić jej wartość.
Przykład, który za chwilę pokażę, nie jest normalną praktyką programistyczną i ab-
solutnie nie polecam robienia takich rzeczy w swoich programach. Mam na celu tylko
pokazanie rzutowania.

Przykład 2.11

Za pomocą const_cast konwertuj wskaźnik do stałej na zwykły wskaźnik do zmiennej.

Rozwiązanie
Początek jest taki sam jak poprzednio. Utwórz aplikację konsolową win32, jak w przykła-
dzie 1.1. Po raz już nie wiem który załączymy nagłówek pozwalający na działania z konsolą:
#include <iostream>

Podam całą funkcję _tmain(), a później ją omówimy:


int _tmain(int argc, _TCHAR* argv[])
{

const double a=4.2;


const double* b=&a;

std::cout<<"stała a="<<a<<std::endl;
double* d = const_cast<double*>(b);
std::cout<<"po const_cast d="<<*d<<std::endl;
*d=7.6;
52 Microsoft Visual C++ 2012. Praktyczne przykłady

std::cout<<"po *d=7.6 d="<<*d<<std::endl;


std::cout<<"a="<<a<<std::endl;
system("pause");
return 0;
}

W pierwszej linii deklarujemy stałą typu zmiennoprzecinkowego a o wartości 4.2, w dru-


giej wskaźnik do stałej, który ustawiamy na adres stałej a. Wartość stałej wypisujemy na
ekran. Teraz następuje clue programu. Deklarujemy wskaźnik do zmiennej o nazwie d ,
tak aby wskazywał na ten sam obszar pamięci co wskaźnik do stałej b. Ponieważ jeden
wskaźnik jest stały, a drugi zmienny, nie można tak zwyczajnie przypisać wskaźnika
b do d. Należy użyć konwersji cont_cast. Próba zmiany wartości wskazywanej przez
wskaźnik b skończy się błędem.
*b=8; //błąd

Jednak zmiana wartości, na którą wskazuje wskaźnik d, jest możliwa. Jednak obydwa
wskazują na to samo miejsce w pamięci. Co zatem się stanie? Zobaczmy. Po urucho-
mieniu programu mamy:
stała a=4.2
po const_cast d=4.2
po *d=7.6 d=7.6
a=7.6
Aby kontynuować, naciśnij dowolny klawisz . . .

A zatem wartość wskazywana przez d (czyli stała) została zmieniona.

Znam tylko jeden przypadek bezpiecznego użycia tej konwersji: kiedy potrzebujemy
pozbyć się identyfikatora const ze względu na zgodność typów akceptowanych przez
jakąś funkcję, ale wiemy, że wartość stałej nie zostanie w tej funkcji zmodyfikowana.

Tablice
Tablice w C++ są ściśle związane ze wskaźnikami. Za pomocą wskaźników można wyko-
nać wszystkie operacje na tablicach. Tablicę jednowymiarową deklarujemy następująco:
typ_tablicy nazwa_tablicy[liczba_elementów];

Dodawanie do wskaźnika (nie do jego wartości) pewnej liczby powoduje przesunięcie


wskazywanego przez ten wskaźnik adresu o wielkość tylu zmiennych wskazywanego
typu. Ponieważ pola tablicy zajmują kolejne miejsca w pamięci, wskaźnik będzie
wskazywał kolejną wartość zapisaną w tablicy. Na przykład po instrukcji wsk=tablica
wskaźnik jest ustawiony na tablica[0], a po dodaniu jedności operatorem ++ usta-
wiamy go na tablica[1] itd.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 53

Należy uważać, aby nie przestawić wskaźnika poza obszar tablicy, gdyż w takim
przypadku będzie on wskazywał na adresy pamięci, w których zapisane są inne dane.
Odczyt wartości wskazywanej przez taki wskaźnik spowoduje najwyżej uzyskanie
przypadkowych wartości, jednak zapis może skutkować nieoczekiwanym zresetowa-
niem systemu lub innymi niespodziankami.

Jeśli deklarujemy tablicę i od razu wpisujemy do niej wartości, to jej rozmiar może
być obliczony automatycznie na podstawie liczby tych wartości. W takim przypadku
nawiasy kwadratowe pozostawiamy puste, a wartości początkowe wpisujemy w na-
wiasach klamrowych. Na przykład:
int tablica[] = {1,2,3,4]

deklaruje tablicę tablica o czterech polach wypełnionych cyframi od 1 do 4. Ten sposób


jest wygodny, jeśli inicjalizujemy tablicę znaków char. Wtedy można napisać jeszcze
prościej:
char tablica[]=”Jakis tekst”

Teraz kolejne wartości tablicy tablica będą zawierały kolejne litery napisu. Tablice wie-
lowymiarowe deklarujemy następująco:
typ_tablicy nazwa_tablicy[wymiar_x][wymiar_y];

Jest to w istocie tablica jednowymiarowa, której elementami są też tablice jedno-


wymiarowe. Tutaj także nazwa tablicy jest konwertowana na wskaźnik do pierwszej
z wymiar_x tablic o rozmiarze wymiar_y. Chcąc odczytywać i zapisywać tablice wie-
lowymiarowe za pomocą wskaźników, musimy zadeklarować wskaźnik do tablicy
jednowymiarowej. Deklaracja takiego wskaźnika ma postać:
typ_wskazywany (*nazwa_wskaźnika)[ilość_pól];

Można też deklarować tablicę z inicjalizacją wartości. W takim przypadku podajemy


wartości w nawiasach klamrowych, rozdzielone przecinkami.
typ_tablicy nazwa_tablicy[liczba_elementów]= {wartość1,wartość2,…,wartośćn};

Przykład 2.12

Zadeklaruj tablicę typu int bez inicjalizacji, tablicę zmiennych float z inicjalizacją
i tablicę dwuwymiarową z inicjalizacją.

Rozwiązanie
Kolejny raz utwórz aplikację konsolową win32 jak w przykładzie 14.1 i załącz nagłówek:
#include <iostream>

Jak poprzednio, najłatwiej będzie podać całą funkcję _tmain(). Objaśnię ją w dalszej
części rozdziału.
int _tmain(int argc, _TCHAR* argv[])
{
54 Microsoft Visual C++ 2012. Praktyczne przykłady

int tablica[2];
float tablica1[3] = {2.3,1.2,4.5};
int tablica2[2][3] = {1,2,3,4,5,6};
std::cout<<"tablica[2]="<<tablica[1]<<std::endl;
std::cout<<"tablica1[2]="<<tablica1[2]<<std::endl;
std::cout<<"tablica2[0][0]="<<tablica2[0][0]<<std::endl;
std::cout<<"tablica2[0][1]="<<tablica2[0][1]<<std::endl;
std::cout<<"tablica2[0][2]="<<tablica2[0][2]<<std::endl;
std::cout<<"tablica2[1][0]="<<tablica2[1][0]<<std::endl;
std::cout<<"tablica2[1][1]="<<tablica2[1][1]<<std::endl;
std::cout<<"tablica2[1][2]="<<tablica2[1][2]<<std::endl;
system("pause");
return 0;
}

W pierwszej linii funkcji _tmain() deklarujemy tablicę tablica, która zawiera dwa ele-
menty typu całkowitego. Ponieważ numerowanie indeksów zaczyna się od zera, mają
one numery 0 i 1. Wartości tych elementów są nieokreślone. W drugiej linii tworzymy
tablicę o trzech elementach float i od razu przypisujemy im wartości. W trzeciej linii
mamy deklarację tablicy dwuwymiarowej z inicjalizacją. Dalsze linie to wypisywanie
danych z wybranych elementów tablic. Po uruchomieniu programu kompilator wy-
świetli ostrzeżenie o próbie wypisania wartości z niezainicjalizowanej tablicy. Należy
kliknąć na Continue. W oknie konsoli dostaniemy:
tablica[2]=-858993460
tablica1[2]=4.5
tablica2[0][0]=1
tablica2[0][1]=2
tablica2[0][2]=3
tablica2[1][0]=4
tablica2[1][1]=5
tablica2[1][2]=6
Aby kontynuować, naciśnij dowolny klawisz . . .

Widać, że pierwsza tablica zawiera przypadkowe wartości, druga i trzecia natomiast takie,
jak wpisaliśmy. Przy tablicy dwuwymiarowej kolejne wartości przypisywane są tak,
że ostatni indeks z prawej zmienia się najszybciej.

Tablice w C++/CLI deklarujemy inaczej niż w zwykłym C++. Opis tego mechanizmu
znajduje się w rozdziale 11.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 55

Operatory new i delete


Operatory new i delete służą do dynamicznego przydzielania pamięci na tzw. stercie.
Dostęp do przydzielonego obszaru pamięci uzyskujemy za pomocą wskaźników. Na
przykład dla zmiennej typu float składnia jest następująca:
float* wsk;
wsk=new float(wartość_początkowa);

Wskaźnik wsk wskazuje teraz na obszar pamięci o rozmiarze pojedynczej zmiennej typu
float. Obszar ten będzie zarezerwowany, dopóki nie zwolnimy go operatorem delete.
delete wsk;

W celu skrócenia zapisu programiści często deklarują wskaźnik i rezerwują pamięć w tej
samej linii.
float* wsk=new float(wartość_początkowa);

Po wykonaniu delete wciąż możemy odczytać wartość zapisaną w pamięci. Nie jest ona
już jednak w żaden sposób chroniona; system w każdej chwili może przydzielić ten
obszar do zapisu innych danych. W praktyce operacje na wskaźniku po delete są poważ-
nym błędem.

Za pomocą new i delete możemy także alokować (tworzyć) tablice dynamicznie.

Deklaracja dynamiczna tablicy jednowymiarowej typu int o dziesięciu elementach ma


postać:
int* wsk = new int[10];

W C++/CLI mamy operator gcnew do tworzenia obiektów zarządzanych. Nie wymaga on


operatora delete, bowiem obiekt jest usuwany automatycznie. Opisuje to rozdział 11.

Instrukcje
Instrukcja lub operator jest najmniejszą wykonywalną częścią programu. W tym dziale
zapoznamy się z instrukcjami. W C++ mamy trzy rodzaje instrukcji:
 warunkowe,
 iteracji,
 przerwania działania programu lub opuszczenia funkcji (skoku).

Omówimy je kolejno.
56 Microsoft Visual C++ 2012. Praktyczne przykłady

Instrukcje warunkowe
Często podczas używania programu stajemy przed koniecznością dokonania wyboru
dalszego toku jego działania, w zależności na przykład od wartości pewnej zmiennej.
Służą do tego instrukcje warunkowe.

Instrukcja if()
Jest to chyba najprostsza instrukcja warunkowa. W przypadku gdy spełniony jest wa-
runek zawarty w nawiasie tej instrukcji, zostanie wykonana jedna lub wiele innych in-
strukcji znajdujących się bezpośrednio po if().
if (a==3) {cout<<"A równa się 3";}

Jeżeli po if() ma być wykonana nie jedna instrukcja, ale cały blok, musimy go za-
mknąć w klamrach {}.

Instrukcja if() else


Jest to rozszerzenie instrukcji if(). Jeżeli warunek w nawiasie nie jest spełniony, wy-
kona się blok instrukcji następujących po słowie else.
if (a==3) {cout<<"A równa się 3";} else {cout<<"A nie równa się 3";}

No dobrze, a co zrobić, jeżeli mamy wiele wartości danej zmiennej i wiele wariantów
działania programu? Czy trzeba każdą wartość sprawdzać za pomocą instrukcji if()?

Na szczęście nie. W takich przypadkach na odsiecz przychodzi nam instrukcja switch().

Instrukcja switch()
Umożliwia ona przejrzyste pisanie programów podejmujących wielowariantowe de-
cyzje. Składnia jest następująca:
switch(zmienna)
{ case wartosc1: instrukcje1; break;
case wartosc2: instrukcje2; break;
(...)
case wartoscn: instrukcjen; break;
default: instrukcje; break;
}

W przypadku gdy zmienna przyjmuje wartość wartosc1, zostaną wykonane instrukcje1,


jeżeli wartosc2 — instukcje2 itd. Jeżeli zmienna przyjmie inną wartość, wykonywa-
ne są instrukcje po słowie default. Przypadek default nie jest obowiązkowy, możemy
go pominąć. Wtedy w przypadku innej wartości zmiennych żadne instrukcje nie zostaną
wykonane.
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 57

Instrukcje iteracji
Instrukcje iteracji umożliwiają tworzenie pętli wykonujących określoną liczbę sekwencji
instrukcji określoną liczbę razy bądź aż do spełnienia jakiegoś warunku.

Instrukcja for
Jeżeli chcemy uzyskać pętlę z określoną z góry liczbą przebiegów, najłatwiej zastosować
instrukcję for. Na przykład, jeśli chcemy powtórzyć pętlę dziesięć razy, napiszemy:
for(int i=0;i<10;i++) {instrukcje_pętli}

Jak widać, w nawiasach tej instrukcji znajdują się trzy wyrażenia oddzielone średni-
kami. Pierwsze z nich to początkowa wartość zmiennej pętli. W tym miejscu możemy
zdefiniować tę zmienną. W takim przypadku będzie ona istnieć tylko w pętli. Drugie wy-
rażenie to warunek działania pętli. W tym przypadku będzie ona działała, dopóki i<10.
Trzecie to instrukcja wykonywana w każdej iteracji, w tym przypadku inkrementacja
wartości zmiennej pętli. Operator ++ oznacza dodanie jedności, czyli i++ oznacza tyle
samo, co i=i+1. Jeżeli w pętli for znajduje się następna pętla, mówimy o zagnieżdżeniu
pętli.

Można w ten sposób napisać program, który będzie wypisywał w kolejnych wierszach
liczby od 0 do n–1, a n będzie wzrastało w każdym wierszu o 1.

Zdarzają się przypadki, kiedy nie znamy potrzebnej liczby przebiegów pętli, ale chcemy ją
wykonywać, dopóki nie zostanie spełniony jakiś warunek. Taka sytuacja występuje
często w obliczeniach numerycznych. Wtedy stosujemy pętlę while lub do...while.

Instrukcja while
Instrukcja while ma następującą składnię:
while(wyrażenie) {instrukcje}

Przed wykonaniem bloku instrukcji program wartościuje wyrażenie i sprawdza, czy jest
ono prawdziwe. Jeżeli tak, zostają wykonane instrukcje pętli. Jeśli przy pierwszym spraw-
dzeniu wyrażenie jest nieprawdziwe, pętla nie wykona się ani razu.

Instrukcja do...while
Funkcjonalność tej pętli jest podobna do pętli while. Istnieje między nimi jednak pewna
istotna różnica. Warunek wyjścia z pętli jest tutaj sprawdzany po jednorazowym wy-
konaniu instrukcji w pętli, a więc niezależnie od początkowego warunku pętla będzie
wykonana przynajmniej raz.

Struktura pętli jest następująca:


do {instrukcje } while (warunek);
58 Microsoft Visual C++ 2012. Praktyczne przykłady

Instrukcja break
Zarówno pętla for, while jak i do...while może być natychmiast przerwana poprzez
instrukcję break. Po napotkaniu tej instrukcji pętla zostanie zakończona, a program będzie
wykonywany dalej od następnej instrukcji po pętli. Tę instrukcję trzeba ostrożnie stoso-
wać w pętlach for, ponieważ nagłe przerwanie tego typu pętli nie jest dobrą praktyką
programistyczną.
Rozdział 3.
Funkcje
Już wiesz, że funkcje stanowią pewien fragment kodu programu, do którego możemy się
wielokrotnie odwoływać. Mogą posiadać parametry, czyli dane wejściowe, mogą wyko-
nywać na nich działania, zmieniać wartości tych parametrów i wreszcie zwracać wyniki.
W każdym języku programowania występują funkcje standardowe, tworzące bibliotekę,
z której można korzystać. Wiele z nich jest realizacją procedur opisujących standar-
dowe funkcje matematyczne, inne są częścią standardowej biblioteki klas i operują na
obiektach tych klas. Mamy też możliwość definiowania własnych funkcji. W progra-
mach języka C++ rolę funkcji głównej pełni funkcja main(), od niej rozpoczyna się
wykonywanie programu. Standard C++11 znacznie polepszył elastyczność funkcji,
wprowadzając na przykład funkcje anonimowe.

Tradycyjny zapis funkcji


Tradycyjnie w języku C++ deklarację funkcji zapisujemy następująco:
typ_zwracany_przez_funkcje nazwa(typ_zmiennej1 zmienna1,
typ_zmiennej2 zmienna2,...typ_zmiennejn zmiennan) {
kod funkcji
return wartość_zwracana;
}

Funkcja pobiera wartości zmienna1, zmienna2, …, zmiennan i zwraca wartość wartość_


zwracana. Instrukcja return nie musi występować na końcu funkcji; może istnieć kil-
ka instrukcji zwracających różne wartości w zależności od wykonania programu. Jest
to bardzo podobne do deklaracji funkcji z pierwszego rozdziału, tylko że tamta jest
napisana w języku naturalnym, a tutaj mamy prawdziwą funkcję w C++.

Warto tu zaznaczyć, że funkcja może zwracać wyniki swoich działań nie tylko przez
instrukcję return.
W posługiwaniu się funkcjami ważną rolę odgrywa typ zmiennej void. Jest to typ
pusty, nie przechowuje on żadnej wartości. Służy do deklaracji funkcji, które nie
zwracają wartości lub ich nie pobierają.
60 Microsoft Visual C++ 2012. Praktyczne przykłady

Zdefiniowaną funkcję wywołujemy:


wynik=nazwa(wartość_zmiennej1,wartość_zmiennej2, …,wartość_zmniennejn);

Wartość zwrócona zostanie zapisana w zmiennej wynik. Jeżeli definicję funkcji umieścimy
po funkcji main(), wymagana jest jej deklaracja przed funkcją main(), na przykład:
int funkcja(int, float); // deklaracja
int main() {
// instrukcje funkcji main()
}

int funkcja(int zmienna1, float zmienna2) {


// ciało funkcji
}

Jeżeli definicja znajduje się przed main(), deklaracja nie jest wymagana.

Przeciążanie funkcji
Wyobraźmy sobie, że piszemy program, który będzie wykonywał to samo działanie na
zmiennych różnych typów. Na przykład będziemy obliczali pole trójkąta, którego bo-
ki będą mogły być dane jako liczby całkowite typu int bądź liczby zmiennoprzecinkowe
float. Moglibyśmy napisać w tym celu dwie oddzielne funkcje, jednak jeżeli byłoby
ich więcej, szybko pogubilibyśmy się w ich nazwach, a program wyglądałby mało przej-
rzyście. Zamiast tego w C++ mamy możliwość zadeklarowania tylko jednej funkcji
i przeciążania jej dla różnych typów argumentów. Po prostu w jednym programie de-
klarujemy funkcje o tej samej nazwie, ale akceptujące i zwracające inne elementy.
int pole_tr(int a, int b) {
return a*b;
}
float pole_tr(float a, float b) {
return a*b;
}

Niejednoznaczność
Mechanizm przeciążania funkcji jest bardzo przydatny, ale można się tu spotkać z pułapką
niejednoznaczności. Chodzi o to, że na podstawie argumentów czasem nie sposób
określić, którą funkcję przeciążoną należy wywołać. W takim przypadku trzeba zastoso-
wać jawną konwersję typów, tak aby typ danych przekazywanych do funkcji nie pozosta-
wiał wątpliwości.
Rozdział 3. ♦ Funkcje 61

Przekazywanie argumentów przez wartość i adres


Istnieją różne sposoby przekazywania argumentów do funkcji: przez przypisanie wartości
zmiennej lub jej adresu. Przekazywanie za pomocą adresu może się odbywać poprzez
wskaźniki lub referencje.

Przekazywanie argumentów przez wartość nie zmienia oryginalnych wartości zmien-


nych przekazywanych do funkcji, czyli zwracanie wartości z funkcji może się odby-
wać tylko przez instrukcję return. Dzieje się tak dlatego, że funkcja operuje tak na-
prawdę na kopiach parametrów przekazanych przez wartość. Jeżeli chcemy napisać
funkcję, która będzie zmieniała wartości przekazanych do niej zmiennych, trzeba to
zrobić w inny sposób. Wyjaśnię to na przykładzie.

Przykład 3.1

Poćwiczymy przekazywanie argumentów z i do funkcji.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Napisz kod
jak niżej:
#include "stdafx.h"
#include <iostream>
#include <conio.h>

using namespace std;

void podstaw(int a,int b) {


a=5;
b=5;
}

int _tmain(int argc, _TCHAR* argv[]) {


int a=0,b=0;
podstaw(a,b);
cout<<"a="<<a<<"\n";
cout<<"b="<<b<<"\n";
getch();
return 0;
}

W zamiarze ten program ma podstawiać liczby 5 pod parametry a i b przekazywane


jako parametry funkcji podstaw(). Niestety, po jego wykonaniu otrzymamy:
a=0
b=0

A więc mimo wykonania funkcji nic się nie podstawiło. Aby program działał, parametry
należy przekazać przez adres. Można do tego celu wykorzystać wskaźniki. Zmień
program jak niżej:
62 Microsoft Visual C++ 2012. Praktyczne przykłady

#include "stdafx.h"
#include <iostream>
#include <conio.h>
using namespace std;
void podstaw(int* a,int* b) {
*a=5;
*b=5;
}
int _tmain(int argc, _TCHAR* argv[]) {
int a=0,b=0;
podstaw(&a, &b);
cout<<"a="<<a<<"\n";
cout<<"b="<<b<<"\n";
getch();
return 0;
}

Teraz po wykonaniu mamy:


a=5
b=5

A więc program działa. Lepszą czytelność programu uzyskamy, stosując referencje:


#include "stdafx.h"
#include <iostream>
#include <conio.h>
using namespace std;
void podstaw(int& a,int& b) {
a=5;
b=5;
}
int _tmain(int argc, _TCHAR* argv[]) {
int a=0,b=0;
podstaw(a, b);
cout<<"a="<<a<<"\n";
cout<<"b="<<b<<"\n";
getch();
return 0;
}

Teraz zmienne podajemy do funkcji przez wartość, ale zostaną one zamienione na adresy
już w funkcji podstaw() dzięki operatorowi &. Zauważmy, że w samej funkcji nie mu-
simy używać operatorów * do działań na wartościach zmiennych.

Wskaźniki do funkcji, delegaty


Podobnie jak w przypadku zmiennych, możliwe jest również deklarowanie wskaźników
do funkcji. Można wyobrazić sobie sytuację, kiedy funkcja może być argumentem in-
nej funkcji, np. gdy piszemy program rysujący wykresy podanej funkcji. Taki mechanizm
istnieje w C++ i został też rozwinięty w .NET Framework.
Rozdział 3. ♦ Funkcje 63

W C++ mamy od tego wskaźniki do funkcji. Są to podobne twory jak wskaźniki na


zmienne, tylko nieco inaczej deklarowane. Deklaracja wygląda tak:
typ_zwracany_funkcji (*nazwa_wskaźnika)(parametry_funkcji);

Wskaźnik określonego typu może wskazywać tylko na funkcję o podanym typie zwraca-
nym i parametrach. Funkcję wywołujemy poprzez wskaźnik, posługując się operatorem *.
Pamiętasz, że w przypadku „zwykłych” wskaźników użycie tego operatora pozwalało
dostać się do wartości wskazywanej. Tu jest podobnie — poprzez gwiazdkę uzyskujemy
dostęp do wskazywanej funkcji. Wskaźnik do funkcji może być parametrem innej —
w ten sposób można przekazać jedną funkcję do innej.

Przykład 3.2

Napisz program wywołujący funkcję przekazaną przez wskaźnik z wnętrza drugiej.

Rozwiązanie
Utwórz projekt konsolowy win32 według przykładu 1.1. Podłącz nagłówek iostream.
#include "stdafx.h" //ta linia istnieje
#include<iostream>

Napisz prostą funkcję drukującą przekazany do niej parametr.


void funk(int a) {
std::cout<<"Parametr = "<<a<<std::endl;
}

Przejdź do funkcji main(), w niej zadeklaruj wskaźnik do funkcji funk().


void (*fwsk)(int);

Jak widzisz, konwencja jest jak wyżej. Funkcja zwraca wartość void (czyli nic), a pobiera
parametr typu int. Wskaźnik nazywa się fwsk. Teraz ustawimy wskaźnik na adres funkcji
funk().
fwsk=&funk;

Tak naprawdę, operator pobrania adresu & nie jest tu konieczny, nazwa funkcji bez nawia-
sów jest bowiem konwertowana do jej adresu. Wywołajmy funkcję poprzez wskaźnik,
na razie w main().
std::cout<<"Wywołuje w funkcji main ";
(*fwsk)(4);

Tak wygląda wywołanie funkcji przez wskaźnik. Mamy tu operator * i parametr podany
w nawiasach. Ale to nam nie wystarczy — chcemy przekazać tę funkcję do innej. Wyjdź
zatem z main() i pod funkcją funk() napisz funk1():
void funk1(void (*fwsk1)(int))
{ std::cout<<"Wywołuje w funkcji funk1 ";
(*fwsk1)(3);
}
64 Microsoft Visual C++ 2012. Praktyczne przykłady

Na pierwszy rzut oka lista parametrów tej funkcji wygląda bardzo dziwnie. Jednak jeśli
się przyjrzeć, to jest to deklaracja wskaźnika do funkcji, którą już napisałeś w main().
Wywołanie wskaźnika w funk1() też jest identyczne jak w main(). Wróć teraz do main()
i przed instrukcją return napisz:
funk1(&funk);
system("pause");

Funkcja funk1() pobrała jako parametr adres funkcji funk(). Druga linia zapobiega za-
mknięciu okna i nie ma nic wspólnego ze wskaźnikami. Skompiluj i uruchom program,
a otrzymasz:
Wywołuje w funkcji main Parametr = 4
Wywołuje w funkcji funk1 Parametr = 3
Aby kontynuować, naciśnij dowolny klawisz . . .

Czyli wszystko działa. Może zapytasz, po co tyle zachodu, skoro funkcję funk() można
w tym przypadku wywołać z funk1() bezpośrednio. Owszem, można, ale jeśli funkcji
takich jak funk() byłoby kilka, to można przekazywać je jako parametry przy kolejnych
wywołaniach funk1().

W .NET Framework mamy podobny mechanizm, zwany delegatami. Deklaracja delegata


jest bardziej podobna do deklaracji funkcji niż wskaźnika. Właściwie różni się tylko
słowem delegate. Dla ułatwienia zachowam postacie funkcji z przykładu o wskaźnikach.
delegate void fdel(int b)

Przykład 3.3

Przekaż funkcję jako parametr do innej za pomocą delegata.

Rozwiązanie
Utwórz konsolowy projekt C++/CLI według przykładu 1.2. Funkcję delegata zadekla-
rujemy globalnie:
using namespace System; // ta linia istnieje
delegate void fdel(int b);

Następnie definiujemy funkcję funk(), jak w przykładzie o wskaźnikach. Funkcja ma


to samo działanie, inny jest jedynie sposób wypisywania parametru.
void funk(int a) {
Console::WriteLine(L"Parametr = "+a);}

Przechodzimy do funkcji main(). Inaczej niż przy wskaźnikach delegat będzie obiek-
tem, a funkcję funk przekazujemy jako parametr konstruktora.
fdel^ delegat = gcnew fdel(funk);
Rozdział 3. ♦ Funkcje 65

Od teraz obiektu delegat można używać jak funkcji funk(). Napisz:


Console::WriteLine(L"Wywołanie z funkcji main()");
delegat(3);
Console::ReadLine();

Jeśli skompilujesz program, otrzymasz


Wywołanie z funkcji main()
Parametr = 3

To było wywołanie z main(), teraz przekażemy delegat jako parametr funkcji funk1().
Najpierw napisz funkcję funk1(). Definiujemy ją oczywiście poza main(), proponuję
pod funkcją funk().
void funk1(fdel^ deleg) {
Console::WriteLine(L"Wywołanie z funkcji funk1()");
deleg(6);
}

Parametrem funk1() jest uchwyt do obiektu delegata. Wróć teraz do main() i napisz
pod wskazaną linią wywołanie funk1(). Nowy obiekt delegata tworzymy bezpośred-
nio w wywołaniu:
delegat(3); //ta linia istnieje
funk1(gcnew fdel(funk));

Po kompilacji w oknie konsoli dostaniesz:


Wywołanie z funkcji main()
Parametr = 3
Wywołanie z funkcji funk1()
Parametr = 6

Wyrażenia lambda
Wyrażenia lambda to funkcje, które nie mają nazwy nadawanej przez programistę
(anonimowe). Kompilator sam nadaje im nazwy, ale są one ukryte przed użytkownikiem.
Wywołanie takiej funkcji następuje zwykle zaraz po jej definicji, a właściwie wywo-
łanie i definicja łączą się w jedną całość. W poprzednim standardzie C++03 nie było
w ogóle takiej konstrukcji w języku C++, wprowadza ją dopiero C++11.

Wyrażenie lambda, podobnie jak funkcja, może posiadać parametry, wykonywać na


nich działania i zwracać wartości. Jeżeli użyjemy tych samych nazw co przy opisie
tradycyjnych funkcji, składnia wyrażenia lambda będzie następująca:
[sposób przechwytywania] (typ_zmiennej1 zmienna1, typ_zmiennej2 zmienna2, …
typ_zmiennejn zmiennan) [mutable] -> typ_zwracany_przez_wyrażenie {kod wyrażenia return
wartość zwracana;}(wartość_zmiennej1,wartość_zmiennej2, …,wartość_zmniennejn);
66 Microsoft Visual C++ 2012. Praktyczne przykłady

Z powodu dużej liczby parametrów może to wyglądać na trochę skomplikowane, ale


za chwilę postaram się sprawę objaśnić.

Wyrażenie lambda, podobnie zresztą jak zwykła funkcja, nie ma dostępu do zmiennych
spoza funkcji. Wyjątkiem są tylko zmienne globalne dla całego programu. W tradycyjnej
funkcji zmienne, które miały być użyte, były przekazywane jako parametry. W wyra-
żeniach lambda można je „przechwycić”. Wyrażenie w nawiasach kwadratowych określa,
jakie zmienne mają być przechwycone do wnętrza wyrażenia. Tych zmiennych moż-
na używać pod niezmienionymi nazwami. Można je przechwycić przez referencje lub
wartość. Podobnie jak to było przy przekazywaniu parametrów do tradycyjnej funkcji,
przechwycenie przez wartość powoduje, że zmienna nie będzie zmodyfikowana poza
zakresem wyrażenia. Przechwycenie przez referencje umożliwia modyfikacje.

Po bloku przechwytywania mamy tradycyjną listę parametrów funkcji. Mamy więc


dwa niezależne sposoby przekazywania danych do wyrażenia: przez przechwytywanie lub
przez parametry. Po tej liście może wystąpić słowo mutable, które umożliwia modyfikacje
we wnętrzu wyrażenia zmiennej przechwyconej przez wartość. Wyrażenie mutable
dotyczy zmiennych przechwyconych, nie parametrów. Bez tego słowa nawet taka mody-
fikacja wygeneruje błąd już podczas kompilacji. Tak naprawdę, modyfikowana jest
kopia zmiennej, więc po opuszczeniu wyrażenia wartość zmiennej w programie nie
zmieni się. Dalej mamy typ zwracany przez wyrażenie. To jest element, który w tradycyj-
nych funkcjach występuje jako pierwszy. Później postaram się opowiedzieć, dlaczego
tutaj jest inaczej. Następnie występuje blok kodu wyrażenia i wreszcie w nawiasach
wartości parametrów przekazywane w chwili wywołania.

Oddzielnego komentarza wymagają jeszcze zasady przechwytywania zmiennej przez


wartość lub referencje. W skrócie znak & (operatora referencji) przed zmienną ozna-
cza, że chcemy ją przechwycić przez referencje. Znak równości oznacza natomiast
przechwycenie przez wartość. Każda zmienna, którą wypiszemy w bloku przechwycenia,
będzie przechwycona przez wartość. Nie ma potrzeby wstawiania znaku równości. Nato-
miast jeśli chcemy przechwycić przez referencje, musimy postawić znak &. Można prze-
chwycić wszystkie zmienne dostępne w chwili wywołania wyrażenia lambda, wystarczy
w nawiasach umieścić tylko odpowiedni znak: [&] lub [=]. W pierwszym przypadku
zmienne przechwycimy przez referencje, w drugim przez wartość. Zobaczmy to na
przykładzie.

Przykład 3.4

Napisz program z wyrażeniami lambda wykonującymi dodawanie liczb oraz inkrementację


w instrukcji for wraz z wypisywaniem zmiennej pętli.

Rozwiązanie
Ponieważ potrzebujemy programu konsolowego win32 z możliwością pisania do konsoli,
zacznij od utworzenia aplikacji konsolowej win32 według przykładu 1.1. W bloku
nagłówków wpisz dyrektywę:
#include <iostream>
Rozdział 3. ♦ Funkcje 67

W funkcji _tmain() zadeklarujemy zmienną c typu int i ustawimy jej wartość na 3. Na-
stępnie wypiszemy tę wartość.
int c=3;
std::cout<<"c="<<c<<std::endl;

Czas na pierwsze (trochę pozbawione sensu) wyrażenie lambda. W nawiasach kwadrato-


wych przechwycimy wszystkie zmienne przez referencje; u nas i tak pobieramy tylko
zmienną c, bo innych nie ma. Wyrażenie doda do c jedność. Po wyjściu z wyrażenia
wypisz znowu wartość zmiennej.
[&] {c++;}();
std::cout<<"Po wyrażeniu lambda c="<<c<<std::endl;

Następne wyrażenie przechwyci zmienną c przez wartość i dodatkowo będzie posiadało


dwa parametry. Obliczy sumę tych trzech wartości i zwróci ją instrukcją return jako licz-
bę typu int. Wynik wyrażenia od razu wydrukujemy w konsoli. Oto odpowiedni kod:
std::cout<<[c] (int x, int y) mutable -> int {return x + y + c;}(2,3)<<std::endl;

Właściwie słowo mutable jest tu niepotrzebne, bo nie modyfikujemy przechwyconej


zmiennej c. Ostatnia pętla for pokazuje zastosowanie wyrażeń lambda jako sposobu
na umieszczenie kodu wyznaczającego na bieżąco wartości potrzebne w innej instrukcji.
Za pomocą takiego wyrażenia będziemy inkrementować zmienną w pętli i wypisywać
jej wartość.
for (int n=0; n<10;[&n] {n++;std::cout<<n<<" ";}());

Zauważ, że tu zmienna n musi być znowu przechwycona przez referencje, ponieważ


chcemy, aby jej wartość pozostała zmieniona po zakończeniu wyrażenia lambda.

Pozostaje jeszcze zadbać, aby okno konsoli nie znikło od razu po zakończeniu pro-
gramu, trzeba przecież odczytać wyniki.
system("pause");

Po uruchomieniu mamy:
c=3
Po wyrażeniu lambda c=4
9
1 2 3 4 5 6 7 8 9 10 Aby kontynuować, naciśnij dowolny klawisz . . .

Funkcja main()
Napisaliśmy już kilkanaście przykładów w C++. Jak widać, w tym języku, w przeciwień-
stwie na przykład do Pascala, nie ma czegoś takiego jak „program główny”. W uprosz-
czeniu, cały kod źródłowy składa się z nagłówka, po którym następują definicje
zmiennych, funkcji i klas. Jedna z tych funkcji musi nosić nazwę main() i to od niej
68 Microsoft Visual C++ 2012. Praktyczne przykłady

rozpoczyna się wykonywanie programu. Funkcja ta musi istnieć w każdym programie,


najprostszy program w C++ będzie zatem wyglądał tak:
int main(){}

Jest to pojedyncza funkcja main(), która nie pobiera żadnych argumentów i nic nie robi.
Wartość zwracana przez funkcję main() powinna być liczbą całkowitą. Ponieważ zakoń-
czenie wykonywania main() jest równoznaczne z zakończeniem działania programu,
wartość zwracana przez funkcję przekazywana jest do systemu.

Bezbłędne prawidłowe zakończenie programu powinno zwracać wartość zero. Każda


inna wartość oznacza błąd. Standard C++ mówi, że funkcja main() powinna być typu int,
ale może nie zwracać żadnej wartości i nie musi zawierać instrukcji return.

Przekazywanie parametrów do funkcji main()


W aplikacjach uruchamianych z konsoli czasem zachodzi potrzeba podania parametrów
wywołania programu. W standardowym C++ w tym celu funkcja main() posiada listę
parametrów.
int _tmain(int argc, _TCHAR* argv[])

argc to liczba parametrów przekazywanych, a argv to tablica wskaźników do łańcuchów


znakowych zawierających te parametry. Podajemy je, uruchamiając program z linii
poleceń konsoli.
> nazwa_programu arg1 arg2

Jako argument 0 podana jest pełna ścieżka do programu. Jeżeli nie podamy żadnego
parametru, to argc=1, a argv[0] zawiera ścieżkę do katalogu roboczego programu.

Przykład 3.5

Napisz program wyświetlający argumenty przekazane do funkcji main().

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. W programie
wypiszemy do konsoli zawartość zmiennej argc i tablicy argv[]. Oto kod:
#include "stdafx.h"
#include <iostream>
#include <conio.h>
using namespace std;
int _tmain(int argc, _TCHAR* argv[]) {
cout<<"Ilość argumentów:"<<argc<<"\n";
for (int arg=0;arg<argc;arg++)
cout<<arg<<" argument "<<argv[arg]<<"\n";

_getch();
return 0;
}
Rozdział 3. ♦ Funkcje 69

Aby program działał jak należy, trzeba wyłączyć kodowanie Unicode. Można to zrobić
w menu Project\<nazwa_projektu> Properties. W lewym panelu wybierz opcje Configu-
ration Properties\General, a następnie w prawym panelu znajdź opcję Character Set
i ustaw jej wartość na Not Set.

Otwórz teraz systemowy wiersz poleceń, przejdź do folderu, w którym znajduje się skom-
pilowany program, i wywołaj go z linii komend, na przykład tak:
nazwa_projektu arg1 arg2

Przy wywołaniu programu jak wyżej mamy:


Ilość argumentów:3
0 argument E:\programy\test\nazwa_projektu.exe
1 argument arg1
2 argument arg2

Funkcja main() w C++/CLI akceptuje tablice parametrów typu System::String. Dostęp


do tych parametrów uzyskujemy za pomocą funkcji GetCommandLineArgs() z klasy
Environment. Zwykle najpierw przepisujemy je do tablicy typu System::String, a następ-
nie ich używamy. Pole o indeksie 0 w tablicy argumentów zawiera nazwę aplikacji,
zaś następne pola zawierają kolejne argumenty podane przy uruchomieniu.

Przykład 3.6

Napisz program w C++/CLI wyświetlający argumenty linii komend.

Rozwiązanie
Tym razem utwórz nowy projekt aplikacji konsolowej C++/CLI, jak w przykładzie 1.2.
Aby wyświetlić argumenty, pobieramy je metodą GetCommandLineArgs() i zapisujemy
do tablicy, a następnie wypisujemy tę tablicę. Oto pełny kod:
// PR_3_6.cpp main project file.

#include "stdafx.h"

using namespace System;

int main(array<System::String ^> ^args)


{

Console::WriteLine(L"Hello World");
array<System::String^>^ argu = System::Environment::GetCommandLineArgs();
Console::WriteLine(argu[0]);
for (System::Int32 i=1;i<argu->Length;i++)
Console::WriteLine(argu[i]);

return 0;
}
70 Microsoft Visual C++ 2012. Praktyczne przykłady

Podobnie jak w poprzednim przykładzie, otwórz teraz wiersz poleceń, przejdź do folderu,
w którym znajduje się skompilowany program, i wywołaj go z linii komend, na przy-
kład tak:
nazwa_projektu arg1 arg2

Wynik działania programu:


Hello World
PR_3_6
arg1
arg2

Na etapie budowy aplikacji parametry linii komend można zadawać w opcji Project\
<nazwa_projektu> Properties. W lewym panelu wybieramy ścieżkę Configuration
Properties\Debugging. Parametry wpisujemy w pole Command Arguments.

Szablony funkcji
Mówiliśmy już o przeciążaniu funkcji. Można je wykorzystać, jeśli chcemy mieć jed-
ną funkcję operującą na kilku typach danych. Jest jednak pewien warunek: musi być to
zbiór typów określony w chwili pisania programu. Można sobie wyobrazić bardziej ogól-
ny mechanizm, taki, który pozwoli napisać funkcję działającą z parametrami dowol-
nego typu. Oczywiście, przy pisaniu tak ogólnego kodu trzeba uważać, żeby nie wywołać
szablonu dla typu, przy którym działania w funkcji nie będą miały sensu.

Tak zdefiniowana ogólna funkcja nazywa się szablonem funkcji. Składnia definicji
jest następująca:
template <class T> T fun(T a)

Wszystko zaczyna się słowem kluczowym template, co oznacza właśnie szablon. Dalej
mamy słówko class, po którym następuje znacznik typu występującego w funkcji. Nie
jest to konkretny typ, ale miejsce, do którego zostanie podstawiony typ argumentu prze-
kazanego w momencie wywołania funkcji. Tego typu może być wartość zwracana i/lub
parametry funkcji, co widać dalej w deklaracji. Powyższa deklaracja odnosi się do
szablonu, który zarówno zwraca wartość, jak i akceptuje parametry. Deklaracja sza-
blonu funkcji, który nic nie zwraca (a więc z typem zwracanym void), wygląda tak:
template <class T> void fun(T a)

Deklaracja szablonu, który nie ma parametrów, nie jest możliwa, bo typ użyty w szablonie
dla konkretnego wywołania jest ustalany po parametrach. Oto przykład szablonu, który
dodaje dwie zmienne lub obiekty dowolnego typu.
Rozdział 3. ♦ Funkcje 71

Przykład 3.7

Napisz szablon funkcji dodający parametry dowolnego typu.

Rozwiązanie
Potrzebny będzie projekt aplikacji konsolowej win32 z przykładu 1.1. W bloku nagłów-
ków wpisz dyrektywę:
#include <iostream>

Następnie zdefiniujemy szablon funkcji próbujący wykonać operator dodawania dla


podanych typów.
template <class T> T dodaj(T par1, T par2)
{
return par1+par2;

Niestety, nie zawsze będzie to miało sens, a szablon nie ma żadnej kontroli błędów, jest to
tylko przykład.

W funkcji _tmain() wywołamy funkcję dodaj() dla liczb całkowitych, zmiennoprzecin-


kowych i zmiennych typu char.
int _tmain(int argc, _TCHAR* argv[])
{
std::cout<<dodaj(1,2)<<std::endl;;
std::cout<<dodaj(2.4,3.7)<<std::endl;;
std::cout<<dodaj('a','b')<<std::endl;
system("pause");
return 0;
}

Skompiluj i uruchom program. W oknie konsoli otrzymasz:


3
6.1

Aby kontynuować, naciśnij dowolny klawisz . . .

Jak widać, funkcja nie ma kłopotów z dodawaniem liczb całkowitych i z przecinkiem.


Nieoczekiwane zachowanie pojawia się przy dodawaniu znaków char, ale to skutek
takiego zdefiniowania operatora + dla tego typu.
72 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 4.
Struktury, klasy, obiekty

Struktury
Struktury i klasy są w C++ bardzo podobnymi typami danych. Mogą zawierać zmienne
i funkcje (metody), które operują na tych zmiennych. Składnia definicji struktury jest
następująca:
struct nazwa_struktury {
typ_zmiennej1 zmienna1;
typ_zmiennej2 zmienna2;
...
typ_zmiennejn zmiennan;
};

Po zdefiniowaniu struktury możemy deklarować obiekty typu nazwa_struktury. Dostęp


do składowych struktur otrzymamy za pomocą operatorów dostępu . lub ->.

Przykład 4.1

Napisz program wyświetlający informacje o samochodzie przechowywane w strukturze.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Zaczniemy
od dopisania potrzebnych plików nagłówkowych i zadeklarowania użycia przestrzeni
nazw std.
#include <iostream>
#include <conio.h>
using namespace std;

Bezpośrednio powyżej funkcji main() deklarujemy strukturę samochod, w której umie-


ścimy dane opisujące pojazd.
74 Microsoft Visual C++ 2012. Praktyczne przykłady

struct samochod {
char *rodzaj;
char *marka;
int ile_osob;
float poj_silnika;
};

Teraz w main() zadeklarujemy strukturę pojazd typu samochod oraz wskaźnik do tej
struktury. Następnie będziemy odczytywać i zapisywać jej składowe. Zawartość pól tek-
stowych struktury wpisujemy przez pomocnicze tablice zawierające tekst. Oto program:
int _tmain(int argc, _TCHAR* argv[]) {
samochod pojazd;
samochod *wsk;
char rodzaj_sam[]="Osobowy";
char marka_sam[]="Fiat";
wsk=&pojazd;
pojazd.rodzaj=rodzaj_sam;
pojazd.marka=marka_sam;
wsk->ile_osob=5;
wsk->poj_silnika=1.4;
cout<<wsk->rodzaj<<"\n";
cout<<wsk->marka<<"\n";
cout<<pojazd.ile_osob<<"\n";
cout<<pojazd.poj_silnika<<"\n";
_getch();
return 0;
}

Bezpośredni dostęp do składowej struktury uzyskujemy poprzez operator ., natomiast


dostęp poprzez wskaźnik wymaga operatora ->. W tym programie wskaźnik ustawiono na
strukturę pojazd, mamy więc możliwość dostępu do tej samej struktury na dwa sposoby.

Oto wynik działania programu:


Osobowy
Fiat
5
1.4

Można teraz deklarować obiekt struktury z inicjalizacją wartości. W tradycyjnym ujęciu


przypomina to deklaracje tablicy.

Przykład 4.2

Zadeklaruj strukturę z inicjalizacją wartości.

Rozwiązanie
Początek pisania programu będzie taki sam jak w poprzednim przykładzie. Tworzymy nowy
projekt konsolowy win32 i załączamy nagłówek umożliwiający pisanie w oknie konsoli:
#include <iostream>
Rozdział 4. ♦ Struktury, klasy, obiekty 75

Następnie powyżej funkcji _tmain() napiszemy definicję prostej struktury:


struct zbior1 {
int a1;
float b1;
};

Przechodzimy do wnętrza _tmain(), zadeklarujemy tu obiekt dane1 typu naszej struktury


zbior1. Deklaracja zawiera inicjalizacje pól struktury.
int _tmain(int argc, _TCHAR* argv[])
{

zbior1 dane1={4,5.6};
std::cout<<"dane1.a1="<<dane1.a1<<std::endl;
system("pause");
return 0;
}

Po uruchomieniu programu otrzymasz:


dane1.a1=4
Aby kontynuować, naciśnij dowolny klawisz . . .

Składnia inicjalizacji wartości jest identyczna jak w tablicy.

Pierwotnie w języku C podział między strukturami i klasami był wyraźny. Struktura


była tylko zbiorem danych (pól), ale nie posiadała metod. Klasy zaś w języku C nie było
wcale. Obecnie różnica polega w zasadzie tylko na tym, że pola struktury są domyślnie
dostępne spoza struktury (są publiczne), natomiast pola klasy nie (są prywatne). Co to
dokładnie oznacza, spróbuję wyjaśnić w następnym dziale, poświęconym klasom.
Później, uzbrojeni w tę wiedzę, wrócimy jeszcze na chwilę do struktur.

Klasy
Klasy czynią z C++ elastyczny język programowania obiektowego. Jak już wiesz,
klasy są zbiorem danych i metod (funkcji) operacji na tych danych. Możemy regulo-
wać prawa dostępu do składowych klasy. Mogą być one deklarowane jako prywatne
(private), publiczne (public) i chronione (protected). Dostęp do składowych pry-
watnych mają tylko i wyłącznie metody wchodzące w skład klasy. Składowe publicz-
ne są dostępne z innych funkcji programu, na przykład z funkcji main(). Znaczenie
składowych chronionych wyjaśnię przy omawianiu dziedziczenia klas. Klasy definiujemy
za pomocą słowa kluczowego class.
class nazwa_klasy {
private:
// deklaracje składowych prywatnych (metod, pól)
public:
// deklaracje składowych publicznych (metod, pól)
76 Microsoft Visual C++ 2012. Praktyczne przykłady

protected:
//deklaracje składowych chronionych (metod, pól)
};

Definicje metod klasy umieszcza się zwykle poza deklaracją klasy w takiej postaci:
nazwa_klasy::nazwa_metody(parametry) { ciało_metody }

Poniżej przytoczono program wykorzystujący niewielką klasę z dwoma polami i dwiema


metodami.

Przykład 4.3

Napisz program wykorzystujący prostą klasę.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Zadeklarujemy
klasę prosta z dwoma polami typu int x i y. Pod te pola podstaw wartości i wypisz ją
na ekran.
// PR_4_3.cpp Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <conio.h>

using namespace std;

class prosta {
public:
int x,y;
};

int _tmain(int argc, _TCHAR* argv[]) {


prosta obiekt;
obiekt.x=10;
obiekt.y=5;
cout<<"Wartości pól: x="<<obiekt.x<<" y="<<obiekt.y<<"\n";
_getch();
return 0;
}

Po kompilacji otrzymamy:
Wartości pól: x=10 y=5

Program z poprzedniego przykładu działa, ale niestety nie jest poprawny. Nie realizuje
ważnej zasady hermetyzacji danych. Poprawnie napisany program nie powinien mieć
możliwości modyfikacji pól klasy bezpośrednio, ale przez jej funkcje składowe, czyli
metody. Zablokowanie bezpośredniego dostępu do pól uzyskujemy przez zadeklaro-
wanie ich jako prywatne.
Rozdział 4. ♦ Struktury, klasy, obiekty 77

Przykład 4.4

Napisz program wykorzystujący prostą klasę z hermetyzacją danych.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Tak jak poprzed-
nio, zadeklarujemy klasę prosta z dwoma polami typu int x i y, lecz teraz będą one
prywatne. Oprócz pól klasa będzie miała metodę ustaw() podstawiającą wartości pod
pola. Ponieważ pól prywatnych nie można również odczytać poza klasą, potrzebne
będą metody podajX() i podajY() zwracające te wartości.
// PR_4_4.cpp Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <conio.h>

using namespace std;

class prosta {
public:
void ustaw(int,int);
int podajX();
int podajY();
private:
int x,y;
};
void prosta::ustaw(int a,int b) {
x=a;y=b;
}

int prosta::podajX() {
return x;
}

int prosta::podajY() {
return y;
}
int _tmain(int argc, _TCHAR* argv[]) {
prosta obiekt;
obiekt.ustaw(3,7);
cout<<"Wartości pól: x="<<obiekt.podajX()<<" y="<<obiekt.podajY()<<"\n";
_getch();
return 0;
}

Zarówno metody, jak i pola klasy wywołujemy dla danej instancji klasy za pomocą ope-
ratora . (jeżeli jest to zmienna statyczna) lub -> (jeżeli jest to zmienna wskaźnikowa).

Wynik działania programu:


Wartości pól: x=3 y=7
78 Microsoft Visual C++ 2012. Praktyczne przykłady

W C++/CLI istnieją tak zwane klasy wartościowe i referencyjne. W klasie wartościowej


słówko class jest poprzedzone słowem value, a w referencyjnej słowem ref. Generalnie
klasa wartościowa jest silnie związana z obiektem przechowującym wartości, a refe-
rencyjna z uchwytem do obiektu. Nie będziemy się tym szczegółowo zajmować, gdyż
jest to materiał na oddzielną książkę.

Statyczne metody i pola klasy


Statyczne metody i pola są deklarowane ze słowem kluczowym static. Elementy te
są związane z klasą, a nie z jej obiektami. Powoduje to, że pola i metody klasy mają
dwie ważne właściwości.

Jest tylko jedna kopia pola statycznego. Każdy obiekt klasy z elementami statycznymi
ma dostęp do tej samej kopii, czyli pole statyczne ma tę samą wartość dla wszystkich
obiektów tej klasy.

Zarówno metody, jak i pola statyczne mogą być używane bez tworzenia obiektu klasy.
Dostęp do metod lub pola statycznego uzyskujemy za pomocą operatora zasięgu ::.

Przykład 4.5

Napisz klasę probna zawierającą metodę statyczną i pole statyczne.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Zaczniemy
od załączenia potrzebnych nagłówków bezpośrednio pod nagłówkiem stdafx.h oraz
użycia przestrzeni std.
#include <iostream>
#include <conio.h>

using namespace std;

Napiszemy klasę probna zawierającą metodę statyczną podaj_a() i pole statyczne a.


Zarówno pola, jak i metody statyczne wymagają definicji poza deklaracją klasy.
class probna {
public:
static float a;
static float podaj_a();
float b;
};
float probna::a;

float probna::podaj_a() {
return a;
}

W funkcji main() wpisujemy wartość do pola a klasy probna za pomocą operatora :: bez
tworzenia obiektu tej klasy. Teraz dopiero tworzymy dwa obiekty tej klasy. Łatwo się
Rozdział 4. ♦ Struktury, klasy, obiekty 79

przekonać, że każdy z tych obiektów ma własną kopię pola niestatycznego b, nato-


miast oba odwołują się do tej samej kopii pola a. Metodę statyczną podaj_a() można
wywołać w kontekście obiektu lub za pomocą operatora ::.

Metody statyczne klasy mogą operować jedynie na polach statycznych, a więc metoda nie
mogłaby zwracać na przykład wartości pola b.
int _tmain(int argc, _TCHAR* argv[]) {
probna obiekt1;
probna obiekt2;
probna::a=2.34;
obiekt1.b=3.45;
obiekt2.b=6.78;
cout<<"obiekt1.b="<<obiekt1.b<<endl;
cout<<"obiekt2.b="<<obiekt2.b<<endl;
cout<<"obiekt1.podaj_a()="<<obiekt1.podaj_a()<<endl;
cout<<"obiekt2.podaj_a()="<<obiekt2.podaj_a()<<endl;
cout<<"probna::podaj_a()="<<probna::podaj_a()<<endl;
_getch();
return 0;
}

Wskaźnik zwrotny this


Czasem w metodzie klasy zachodzi potrzeba odwołania się do obiektu klasy, na którym
wywołana jest metoda. Można wtedy skorzystać z niejawnego wskaźnika this, który
jest przekazywany do metody klasy.

Przykład 4.6

Odwołaj się do pól klasy za pomocą wskaźnika this.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Zaczniemy od
załączenia potrzebnych nagłówków bezpośrednio pod nagłówkiem stdafx.h oraz użycia
przestrzeni std.
#include <iostream>
#include <conio.h>

using namespace std;

Poniżej napiszemy klasę probna. Metoda podstaw() w tej klasie będzie korzystała ze
wskaźnika this do podstawienia wartości pola.
class probna {
public:
void podstaw(float);
float podaj();
private:
80 Microsoft Visual C++ 2012. Praktyczne przykłady

float a;
};

void probna::podstaw(float wartosc) {


this->a=wartosc;
}
float probna::podaj() {
return a;
}
int _tmain(int argc, _TCHAR* argv[]) {
probna obiekt;
obiekt.podstaw(8);
cout<<obiekt.podaj();
_getch();
return 0;
}

W metodzie podstaw() odwołujemy się do pola a obiektu klasy, na którym wywołujemy


tę metodę. Wskaźnik this reprezentuje obiekt klasy, na którym metoda została wywołana.

Dziedziczenie
Dana klasa w C++ nie istnieje tylko i wyłącznie dla siebie. Na jej podstawie możemy
zadeklarować tzw. klasy potomne (pochodne), które przejmą część składowych klasy
bazowej. Klasa potomna może dziedziczyć po jednej klasie bazowej lub po kilku z nich.
Składnia dziedziczenia pojedynczego jest następująca:
class pochodna : tryb_dziedziczenia klasa_bazowa {
składowe klasy potomnej };

Tryb dziedziczenia może być prywatny, publiczny lub chroniony. Jeżeli tryb jest publiczny,
to składowe publiczne klasy bazowej będą publiczne w klasie pochodnej. Elementy
prywatne klasy bazowej nie będą dostępne w klasie pochodnej. Jeżeli klasa dziedziczy
w trybie prywatnym, to elementy publiczne klasy bazowej będą prywatne w klasie
pochodnej, a składowe prywatne również nie będą dostępne.

Istnieje jeszcze typ składowych chronionych (protected). Jest on nieco bardziej skompli-
kowany. W klasie bazowej możemy zadeklarować pola tego typu. Będą one w tej klasie
polami prywatnymi, ale będą dostępne w klasach potomnych na następujących zasadach:
 Jeżeli klasa będzie dziedziczyła w trybie publicznym, to pola chronione klasy
bazowej będą chronione w klasie pochodnej.
 Jeżeli dziedziczenie będzie w trybie prywatnym, to pola chronione staną się
prywatnymi.
 Jeżeli klasa dziedziczy w trybie chronionym, to chronione i publiczne
elementy klasy bazowej staną się składowymi chronionymi klasy pochodnej.
Rozdział 4. ♦ Struktury, klasy, obiekty 81

Wyobraźmy sobie, że dostaliśmy zadanie napisania bazy danych dla sklepu fotograficz-
nego. Sklep ten sprzedaje zarówno aparaty na klisze, jak i cyfrowe. Oba rodzaje aparatów
mają cechy wspólne — powiedzmy, że będą to waga, maksymalna ogniskowa obiektywu
i fakt posiadania lampy błyskowej. Każdy z rodzajów aparatów ma także cechy właściwe
tylko danej grupie. Załóżmy, że dla aparatów na kliszę będzie to posiadanie czytnika
kodu DX oraz funkcji przewijania filmu (rewind), a dla aparatów cyfrowych będzie to
liczba pikseli i możliwość nagrywania filmów.

Przykład 4.7

Napisz klasę do programu opisanego wyżej.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Zaczniemy od
załączenia potrzebnych nagłówków bezpośrednio pod nagłówkiem stdafx.h oraz użycia
przestrzeni std.
#include <iostream>
#include <conio.h>

using namespace std;

Napiszemy klasę bazową aparat, która będzie zawierała cechy wspólne, i dwie klasy po-
chodne, których składowe będą cechami indywidualnymi. Każda klasa będzie zawierała
metody ustaw() i odczyt(), umożliwiające zmianę wartości składowych i wyświetlenie
ich wartości dla danego obiektu.

Schemat programu jest następujący:


1. Najpierw utworzymy klasę bazową aparat i klasy pochodne aparat_klisza
i aparat_cyfra.
2. Następnie zdefiniujemy metody ustaw() i odczyt() odpowiednich klas.
Ponieważ obie klasy dziedziczą w trybie publicznym, pola chronione klasy
aparat będą dostępne dla klas potomnych.
3. Teraz w funkcji main() możemy utworzyć obiekt odpowiadający pojedynczej
sztuce aparatu odpowiedniego rodzaju i zapisać jego dane, a następnie
odczytać je.

Zamieszczam od razu cały listing programu:


#include "stdafx.h"
#include <iostream>
#include <conio.h>
using namespace std;

class aparat {
protected:
int masa;
float ognisko;
bool flash;
};
82 Microsoft Visual C++ 2012. Praktyczne przykłady

class aparat_cyfra : public aparat {


public:
void ustaw(float,bool,int,float,bool);
void odczyt();
private:
float piksel;
bool film;
};
class aparat_klisza : public aparat {
public:
void ustaw(bool,bool,int,float,bool);
void odczyt();
private:
bool koddx;
bool rewind;
};

void aparat_cyfra::ustaw(float a,bool b,int c,float d,bool e) {


piksel=a;film=b;masa=c;ognisko=d;flash=e;
}

void aparat_cyfra::odczyt() {
cout<<"Aparat cyfrowy"<<"\n";
cout<<"Ile Mpikseli:"<<piksel<<"\n";
cout<<"Nagrywa film:"<<film<<"\n";
cout<<"Masa aparatu:"<<masa<<"\n";
cout<<"Max ogniskowa:"<<ognisko<<"\n";
cout<<"Lampa:"<<flash<<"\n\n";
}

void aparat_klisza::ustaw(bool a,bool b,int c,float d,bool e) {


koddx=a;rewind=b;masa=c;ognisko=d;flash=e;
}

void aparat_klisza::odczyt() {
cout<<"Aparat na klisze"<<"\n";
cout<<"Czyta kody dx:"<<koddx<<"\n";
cout<<"Funkcja rewind:"<<rewind<<"\n";
cout<<"Masa aparatu:"<<masa<<"\n";
cout<<"Max ogniskowa:"<<ognisko<<"\n";
cout<<"Lampa:"<<flash<<"\n\n";
}
int _tmain(int argc, _TCHAR* argv[]) {
aparat_klisza aparat1;
aparat1.ustaw(1,1,350,130,1);
aparat1.odczyt();
aparat_cyfra aparat2;
aparat2.ustaw(13.1,1,300,120,1);
aparat2.odczyt();
_getch();
return 0;
}

A oto wynik działania:


Aparat na klisze
Czyta kody dx:1
Rozdział 4. ♦ Struktury, klasy, obiekty 83

Funkcja rewind:1
Masa aparatu:350
Max ogniskowa:130
Lampa:1

Aparat cyfrowy
Ile Mpikseli:13.1
Nagrywa film:1
Masa aparatu:300
Max ogniskowa:120
Lampa:1

W C++/CLR klasy typu wartościowego deklarowane słowem value class nie mogą być
klasą bazową do dziedziczenia. Są one tak zwanymi klasami zamkniętymi.

Funkcje wirtualne
Czasami może istnieć metoda klasy, która ma sens dla całej hierarchii klas. W przykładzie
„fotograficznym” takimi metodami były ustaw() i odczyt(). Weźmy może inny przykład
zastosowania funkcji wirtualnych, który jest często przytaczany, ale moim zdaniem do-
brze oddaje ich potrzebę. Piszemy program, który oblicza różne parametry wielu figur
geometrycznych. Można sobie wyobrazić, że zdefiniujemy klasę bazową nazwaną figura
i klasy pochodne dla poszczególnych figur (kwadrat, kolo itp.). Niech każdy parametr
figur będzie obliczany przez inną metodę klasy. I tak na przykład metoda obliczająca
powierzchnię będzie się nazywała pole(). Tę metodę możemy zdefiniować już właśnie
w klasie bazowej figura, jako funkcję wirtualną. Następnie będziemy redefiniować ją
w każdej klasie potomnej i wypełniać kodem do obliczania pola konkretnej figury. Jeśli
teraz wywołamy funkcję pole() na obiekcie jakiejś figury, to program sam wybierze
metodę z klasy tej figury. Jeśli chodzi o wybór tej metody, to mamy dwie możliwości.
Jeśli obiekt klasy jest zdefiniowany bezpośrednio, to jej wybór jest prosty, bo wiadomo,
z jakiego typu obiektem mamy do czynienia. Wtedy dowiązanie odpowiedniej metody
może się odbyć już w trakcie kompilacji programu; mówimy, że jest to wiązanie wczesne.
Trudniej jest, jeśli do obiektu klasy odwołujemy się poprzez wskaźnik. W C++
wskaźnik na typ podstawowy może bezpiecznie wskazywać na typ pochodny; więcej
o tym w następnym punkcie. Zatem wskaźnik na obiekt klasy figura może wskazywać na
dowolny obiekt klasy kolo, kwadrat, czyli klas konkretnych figur. Jeśli teraz wywołamy
metodę pole na takim wskaźniku, to na etapie kompilacji nie wiadomo, na jaki obiekt
jakiej klasy będzie on wskazywał. Będzie to wiadome dopiero po uruchomieniu pro-
gramu i dopiero wtedy będzie można wybrać (dowiązać) odpowiednią metodę. Taki typ
wiązania nazywamy wiązaniem późnym. Metodę wirtualną definiujemy słowem kluczo-
wym virtual przed typem funkcji. Zróbmy przykład z funkcjami wirtualnymi i wią-
zaniem późnym.
84 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 4.8

Utwórz klasę podstawową o nazwie pierwsza i pochodną druga. Niech zawierają one
tylko jedną metodę wirtualną, która zawiadamia o swoim wywołaniu. Wywołaj ją na
obiekcie klas pierwsza i druga, posługując się wskaźnikiem do klasy pierwsza.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Zaczniemy
od załączenia potrzebnego nagłówka bezpośrednio pod nagłówkiem stdafx.h.
#include <iostream>

Teraz zdefiniujemy klasę pierwsza z wirtualną funkcją podaj().


class pierwsza
{
public:
virtual void podaj()
{std::cout<<"podaj z klasy pierwsza"<<std::endl;}
};

Następnie napiszemy klasę pochodną druga z redefinicją podaj(). Tu już nie jest potrzeb-
ny specyfikator virtual.
class druga : public pierwsza
{
public:
void podaj()
{std::cout<<"podaj z klasy druga"<<std::endl;}
};

Przechodzimy do funkcji _tmain(). Zadeklarujemy w niej wskaźnik do klasy pierwsza,


następnie obiekt klasy pierwsza i klasy druga. Wskaźnik ustawimy na adres obiektu
klasy pierwsza (pobrany operatorem &) i wywołamy metodę podaj(). Następnie usta-
wimy wskaźnik na obiekt klasy pochodnej i znowu wywołamy metodę podaj(). Zna-
czenie dwóch ostatnich linii nie wymaga komentarza. Oto cała funkcja _tmain():
int _tmain(int argc, _TCHAR* argv[])
{
pierwsza* wsk;
pierwsza obiekt1;
druga obiekt2;
wsk=&obiekt1;
wsk->podaj();
wsk=&obiekt2;
wsk->podaj();
system("pause");
return 0;
}
Rozdział 4. ♦ Struktury, klasy, obiekty 85

Po uruchomieniu programu mamy:


podaj z klasy pierwsza
podaj z klasy druga
Aby kontynuować, naciśnij dowolny klawisz . . .

W zależności od obiektu, na który wskazywał wskaźnik, uruchomiła się odpowiednia


metoda.

Wskaźniki na klasy bazowe


i pochodne, rzutowanie
Klasa pochodna dziedziczy wszystkie cechy klasy bazowej i dodaje swoje. Jest więc
większym obiektem niż klasa bazowa. Co się stanie, jeśli utworzymy wskaźnik na
klasę bazową i ustawimy go na adres obiektu klasy pochodnej? Wskaźnik obejmujący
pewien obszar pamięci jest ustawiony na adres obiektu, który zajmuje większy ob-
szar. Zatem nie ma niebezpieczeństwa, że wskazanie wyjdzie poza obiekt. Jest to tak
zwane rzutowanie w górę (hierarchii klas). Jest ono zawsze bezpieczne; wykorzysta-
łeś je już w poprzednim przykładzie. Gorzej jest z rzutowaniem w dół. Jeśli wskaźnik
do większego obiektu (pochodnego) ustawimy na adres mniejszego obiektu (bazowego),
to wskaźnik ten, mówiąc skrótowo, wskaże cały obiekt i jeszcze coś. Aby program działał
poprawnie, trzeba mieć pewność, że ten dodatek nie spowoduje problemów. Rzutowanie
w dół musi być przeprowadzane co najmniej za pomocą operatorów static_cast lub
dynamic_cast. Rzutowanie statyczne wymaga większej ostrożności, ponieważ nie zabez-
piecza przed błędami tak jak dynamiczne. Z kolei mechanizm rzutowania dynamicz-
nego jest wolniejszy.

Przykład 4.9

Napisz program rzutujący obiekty w dół hierarchii klas za pomocą static_cast i dynamic_
cast. Wskaż na niebezpieczeństwa błędnego przydziału pamięci.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Jak zwykle
potrzebujemy pliku nagłówkowego:
#include <iostream>

umieszczonego bezpośrednio pod nagłówkiem stdafx.h. Będziemy operować na dwóch


klasach podobnych do przykładu z funkcjami wirtualnymi. Hierarchia klas musi mieć
funkcję wirtualną, gdyż inaczej nie działa rzutowanie dynamic_cast. Od poprzedniego
przykładu klasy różnią się tylko tym, że każda z nich zawiera metodę właściwą tylko
sobie. Myślę, że nie wymaga to szerszego komentarza i mogę od razu zamieścić kod
klas do wpisania po sekcji nagłówków.
86 Microsoft Visual C++ 2012. Praktyczne przykłady

class pierwsza
{

public:

virtual void podaj()


{
std::cout<<"podaj z klasy pierwsza"<<std::endl;
}
void funP()
{
std::cout<<"Metoda klasy bazowej"<<std::endl;
}

};

class druga : public pierwsza


{
public:

void podaj()
{
std::cout<<"podaj z klasy druga"<<std::endl;
}
void funD()
{
std::cout<<"Metoda klasy pochodnej"<<std::endl;
}

};

Czas na funkcję _tmain(). Będę omawiał linie kodu w tej funkcji kolejno, zaczynając
od początku. A więc najpierw klamra otwierająca, która już istnieje, bo została utworzona
przez kompilator. Następnie zadeklarujemy dwa wskaźniki na obiekty klasy pierwsza
i druga i niejako do kompletu dwa obiekty tych klas.
pierwsza* pierwszaWsk;
druga* drugaWsk;

pierwsza pierwszaObj;
druga drugaObj;

Wskaźnik do obiektu klasy bazowej ustawimy na obiekt tej klasy, na razie nie ma tu
więc żadnego rzutowania. Wskaźnik na obiekt klasy pochodnej wyzerujemy.
pierwszaWsk=&pierwszaObj;
drugaWsk=NULL;

Teraz za pomocą static_cast przyporządkujemy obiekt klasy bazowej wskaźnikowi


na obiekt klasy potomnej. Jest to rzutowanie w dół. Spróbujemy wywołać metodę
klasy potomnej na obiekcie wskazywanym przez wskaźnik.
drugaWsk = static_cast<druga*>(pierwszaWsk);
drugaWsk->funD();
Rozdział 4. ♦ Struktury, klasy, obiekty 87

W zasadzie taka operacja nie powinna się udać, bo wskaźnik drugaWsk wskazuje na
obiekt klasy bazowej, który nie zawiera metody funD(). Sam wskaźnik jest jednak ty-
pem wskaźnikowym klasy potomnej. Przygotowując się do drugiego rzutowania, ze-
rujemy wskaźnik drugaWsk.
drugaWsk=NULL;

Tym razem skorzystamy z dynamic_cast. To rzutowanie nie zostanie wykonane, jeśli


brak zgodności typów. W przypadku błędu wskaźnik drugaWsk będzie nadal wskazywał
wartość NULL. Niezależnie od rezultatu spróbujemy wywołać metodę funD().
drugaWsk = dynamic_cast<druga*>(pierwszaWsk);

if (drugaWsk==NULL)
std::cout<<"Pierwsza konwersja z dynamic_cast nieudana."<<std::endl;
drugaWsk->funD();

Przy trzeciej konwersji wskaźnik pierwszaWsk ustawimy na obiekt klasy potomnej, a więc
mamy rzutowanie w górę; drugaWsk wyzerujemy. Za pomocą dynamic_cast rzutujemy
wskaźnik pierwszaWsk na wskaźnik do klasy druga. Robimy to, mając pewność, że
wskaźnik pierwszaWsk wskazuje na obiekt klasy druga. Sprawdzimy, czy rzutowanie
się wykonało, a następnie wywołamy metodę funD().
drugaWsk=NULL;
pierwszaWsk=&drugaObj;

drugaWsk = dynamic_cast<druga*>(pierwszaWsk);

if (drugaWsk==NULL)
std::cout<<"Druga konwersja z dynamic_cast nieudana."<<std::endl;

drugaWsk->funD();

Ostatnia linia przed instrukcją return nie wymaga komentarza.


system("pause");

Uruchom program. Powinieneś otrzymać komunikaty jak niżej, choć nie gwarantuję
działania:
Metoda klasy pochodnej
Pierwsza konwersja z dynamic_cast nieudana.
Metoda klasy pochodnej
Metoda klasy pochodnej
Aby kontynuować, naciśnij dowolny klawisz . . .

Program się wykonał, choć tak naprawdę tylko ostatnia konwersja jest poprawna. Po
pierwszej wskaźnik drugaWsk wskazuje na obiekt klasy bazowej będący niepełnym
obiektem klasy pochodnej. Mimo że rzutowanie statyczne zostało wykonane i nawet
można wywołać na obiekcie metodę klasy pochodnej, to tak naprawdę nie wiadomo,
co znajduje się w części pamięci wskazywanej przez wskaźnik drugaWsk. Absolutnie
nie jest to bezpieczny sposób programowania. Można się o tym przekonać, widząc, że
88 Microsoft Visual C++ 2012. Praktyczne przykłady

rzutowanie dynamiczne w identycznej sytuacji nie zostało wykonane, niejako zadziałało


zabezpieczenie. W ostatniej konwersji wskaźnik pierwszaWsk wskazuje na obiekt klasy
pochodnej. Nie jest istotne, że widzi on tylko część tego obiektu odnoszącą się do klasy
bazowej, cały obiekt istnieje przecież w pamięci. Konwersja dynamiczna się udaje,
a drugaWsk wskazuje na kompletny obiekt klasy druga.

Z powyższego przykładu wypływają dwa wnioski. Po pierwsze, działanie programu nie


znaczy, że został on poprawnie napisany. Po drugie zaś, do stabilnego działania konieczne
jest zastanowienie się nad organizacją obiektów w pamięci podczas projektowania
aplikacji.

Przeciążanie operatorów
Przeciążanie operatorów daje użytkownikowi bardzo duże możliwości, szczególnie przy
programowaniu czysto obiektowym. Możliwe jest przeciążanie istniejących operatorów
do wykonywania działań na typach lub obiektach zdefiniowanych przez użytkownika.
Jest to tak naprawdę przeciążanie funkcji operatorowej, która składa się ze słowa ope-
rator i symbolu. Na przykład w przypadku mnożenia mamy funkcję operator*.
Składnia definicji przy przeciążaniu jest następująca:
typ_zwracany nazwa_klasy::operator*(argumenty) {
instrukcje
}

typ_zwracany oznacza typ wartości zwracanej jako wynik, a nazwa_klasy to klasa, dla
której przeciążamy funkcję operatorową.

Przykład 4.10

Napisz klasę wektor określającą liczby zespolone, która będzie miała dwie składowe x
i y, oznaczające część rzeczywistą i urojoną. Niech klasa ta zawiera operator dodawania
wektorów.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Zaczniemy
od załączenia potrzebnych nagłówków bezpośrednio pod nagłówkiem stdafx.h oraz
użycia przestrzeni std.
#include <iostream>
#include <conio.h>

using namespace std;

Klasa wektor będzie miała dwie składowe prywatne x i y, dwa konstruktory, metodę
wyswietl() i operator +. Przeciążony operator + dla klasy wektor będzie zwracał również
obiekt tej klasy, którego zmienne x i y będą sumą dwóch liczb zespolonych typu wektor.
Deklaracje klasy napisz w dalszym ciągu programu.
Rozdział 4. ♦ Struktury, klasy, obiekty 89

class wektor {
public:
wektor() {};
wektor(int a,int b) {x=a;y=b;}
wektor operator+(wektor);
void wyswietl() {cout<<"x="<<x<<"\n"<<"y="<<y<<"\n";}
private:
int x,y;
};

Teraz zdefiniujemy funkcję operatorową.


wektor wektor::operator+(wektor w) {
return wektor(x+w.x,y+w.y);
}

W funkcji main() można już dodawać obiekty klasy wektor.


int _tmain(int argc, _TCHAR* argv[]) {
wektor wektor1(2,3);
wektor wektor2(3,4);
wektor wektor3;
wektor3=wektor1+wektor2;
wektor3.wyswietl();
_getch();
return 0;
}

Szablony klas
W rozdziale o funkcjach zajmowaliśmy się między innymi szablonami funkcji. Było
to definiowanie funkcji, która mogła operować na parametrach różnego typu. Szablo-
ny są podobnym mechanizmem, ale jeszcze bardziej ogólnym. Wyobraź sobie, że
chcesz napisać klasę, która będzie przetwarzać elementy różnych typów. W zależności od
sytuacji chcesz, żeby klasa miała pola typu liczbowego, tekstowego albo inne, na
przykład pola typu obiektów innej klasy. Okazuje się, że nie trzeba tworzyć oddzielnej
klasy z polami każdego typu. Zamiast tego można (i jest dużo sensowniej) posłużyć
się szablonem klasy. Szablon klasy operuje na danych nieokreślonego typu. Określenie
typu danych następuje na etapie tworzenia obiektu klasy. Szablon klasy deklarujemy
za pomocą słowa kluczowego template. Nazwijmy klasę szablon. Definicja będzie
wyglądała tak:
template <class T> class szablon
{ //wnętrze szablonu klasy
};

Jak widać, sposób definiowania szablonu jest podobny do definicji zwykłej klasy. Uzu-
pełnieniem jest słowo template, a następnie <class T>, gdzie T jest typem parametru
klasy. Załóżmy, że chcemy utworzyć klasę z parametrem typu int. Utworzenie takiego
obiektu z szablonu klasy wygląda tak:
szablon<int> a;
90 Microsoft Visual C++ 2012. Praktyczne przykłady

Wszędzie tam, gdzie w definicji klasy użyliśmy parametru T, będzie on zamieniony na typ
int. Podobnie jak „zwykłe” klasy, szablony również mogą posiadać swoje metody.
Oto przykład:

Przykład 4.11

Napisz program z szablonem klasy, jednym polem o typie parametryzowanym i dwiema


metodami. Jedna z nich będzie wpisywała zawartość pola z podanego parametru, a druga
tę zawartość zwracała.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Do działania stru-
mienia cout będzie potrzebny nagłówek iostream. Załącz go pod nagłówkiem stdafx.h.
#include "stdafx.h" //ta linia istnieje
#include <iostream>

Szablon klasy nazwałem bardzo odkrywczo, po prostu szablon; oczywiście, możesz wy-
myślić dowolną inną nazwę. Ma on dwie publiczne metody. Pierwsza z nich, podajDana(),
zwraca zawartość jedynego pola klasy, o nazwie _dana. Druga, wpiszDana(), akcep-
tuje parametr o typie T i wpisuje go w pole _dana. Samo pole jest prywatne i ma typ T.
template <class T>
class szablon
{
public:
T podajDana()
{ return _dana;}

void wpiszDana(T wpis)


{
_dana=wpis;
}

private:
T _dana;
};

Jak widać, jest to zwykła klasa, tyle że występuje w niej typ nazwany T, który jest para-
metrem. Pod ten parametr można podstawić konkretny typ, na przykład int, float
czy dowolny inny, także taki, który sami stworzymy. Tutaj na razie podstawimy
int. W funkcji _tmain() tworzymy obiekt a, który jest obiektem klasy szablon z parame-
trem int. Parametr podajemy w nawiasach ostrych (znaki mniejszości i większości).
Metoda wpiszDana() wpisuje do pola kasy liczbę 2. Następnie wypisujemy zawartość
tego pola w konsoli. Teraz za pomocą tego samego szablonu tworzymy obiekt b, który bę-
dzie miał pole typu standardowej klasy string. Jego pole _dana będzie więc mogło prze-
chować tekst. Za pomocą metody wpiszDana() wypełniamy pole _dana tekstem, a w na-
stępnej linii wyprowadzamy ten tekst do konsoli. W ten sposób metody wpiszDana()
i podajDana() operują na różnych typach w zależności od parametru szablonu.
Rozdział 4. ♦ Struktury, klasy, obiekty 91

int _tmain(int argc, _TCHAR* argv[])


{
szablon<int> a;
a.wpiszDana(2);
std::cout<<a.podajDana()<<std::endl;
szablon<std::string> b;
b.wpiszDana("Tutaj tekst");
std::cout<<b.podajDana().c_str()<<std::endl;
system("pause");
return 0;
}

Po uruchomieniu w konsoli ukaże się:


2
Tutaj tekst
Aby kontynuować, naciśnij dowolny klawisz . . .

Powstał program niezbyt efektowny, wyświetlający po prostu liczbę 2 i napis Tutaj tekst.
Chodziło mi jednak o pokazanie zawartości pola klasy w zależności od parametru.

W ostatnim przykładzie metody szablonu były definiowane w jego wnętrzu. Wiesz już, że
metody klasy można definiować też na zewnątrz jej ciała. Niestety, w przypadku sza-
blonu sprawa jest trudniejsza. Na zewnątrz parametr klasy jest wielkością, której kompi-
lator nie rozpoznaje. Metody klasy trzeba deklarować jako szablony funkcji. Składnia
jest identyczna jak przy zwykłych szablonach funkcji, tyle że tu podajemy dodatkowo
nazwę klasy wraz z parametrem, następnie operator zasięgu :: i nazwę funkcji. Jest to
jakby połączenie definicji szablonu funkcji ze zwykłą metodą klasy. W naszym przypadku
definicję metody podajDana() rozpoczniemy tak:
template <class T> T szablon<T>::podajDana()

Przepiszemy teraz program z poprzedniego przykładu, ale definiując metody poza


ciałem klasy.

Przykład 4.12

Napisz prosty szablon klasy z jednym polem, definiując metody odczytu i zapisu pola
na zewnątrz klasy.

Rozwiązanie
Najprościej zacząć od kodu z poprzedniego przykładu. W samej klasie będą teraz tylko
deklaracje metod. Usuń definicje wraz z otaczającymi je nawiasami klamrowymi i dodaj
średniki na końcu deklaracji.
template <class T>
class szablon
{
public:
T podajDana();
92 Microsoft Visual C++ 2012. Praktyczne przykłady

void wpiszDana(T wpis);


private:
T _dana;
};

Po klamrze ze średnikiem zamykającej ciało klasy piszemy definicje metod jako szablony
funkcji:
template <class T> T szablon<T>::podajDana()
{
return _dana;
}

template <class T> void szablon<T>::wpiszDana(T wpis)


{
_dana=wpis;
}

Funkcja _tmain() pozostaje taka sama jak w przykładzie 4.18. Uruchom program, jego
działanie nie zmieni się.
2
Tutaj tekst
Aby kontynuować, naciśnij dowolny klawisz . . .

Wyjątki
Wyjątki są mocnym narzędziem, umożliwiającym raportowanie i obsługę błędów w pro-
gramie. Zdecydowałem się opisać je w rozdziale o klasach, ponieważ do ich sprawnego
wykorzystania wygodnie jest użyć właśnie klas. Do standardowej obsługi błędów służą
trzy instrukcje: try, throw i catch. W przypadku gdy przy wykonywaniu jakiejś instrukcji
dojdzie do błędu, na przykład do próby otwarcia nieistniejącego pliku lub próby dzie-
lenia przez zero, program bez obsługi wyjątków zakończy się i przekaże kod błędu do
systemu. Program z obsługą wyjątków nie zostanie zakończony, ale wykona instrukcję
przewidzianą na tę okazję przez programistę, a następnie będzie działał dalej. Po słowie
try występuje blok instrukcji, które program „próbuje” wykonać. W tym bloku powinni-
śmy przewidzieć, jakie błędy mogą wystąpić przy wykonywaniu naszych instrukcji
(na przykład przy dzieleniu należy sprawdzić mianownik, przy wykonaniu jakieś
funkcji to, czy zwraca ona oczekiwaną wartość itd.). Sprawdzenie to wykonujemy za
pomocą instrukcji if. Jeżeli wartość wskazuje na błąd, „wyrzucamy” wyjątek za pomocą
instrukcji throw. Wyjątek jest to zmienna lub obiekt ustalonego typu. Instrukcja throw
powoduje, że program przerywa wykonywanie bloku try, wychodzi z niego i szuka in-
strukcji catch(parametr), gdzie parametr jest typem zmiennej wyrzuconej przez throw. Po
znalezieniu odpowiedniego bloku catch wykonywane są instrukcje w tym bloku, a na-
stępnie program wykonuje się dalej od następnej instrukcji po bloku catch (nie ma już
powrotu do bloku try).
Rozdział 4. ♦ Struktury, klasy, obiekty 93

Przykład 4.13

Napisz program z obsługą wyjątku przy dzieleniu przez zero.

Rozwiązanie
Jak zwykle utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Za-
czniemy od załączenia potrzebnych nagłówków bezpośrednio pod nagłówkiem stdafx.h
oraz użycia przestrzeni std.
#include <iostream>
#include <conio.h>

using namespace std;

Oto przykład programu z obsługą wyjątków napisaną według schematu opisanego wyżej:
int _tmain(int argc, _TCHAR* argv[]) {
float a=2;
float b=0;
float c=-2;
float wynik;
int blad=0; //definicja zmiennej wyjątku; wartość nie jest istotna
float blad_f;
try {
for(b=5;b>-5;b--) {
cout<<"a="<<a<<" b="<<b;
if (b==0)
throw blad;
else
wynik=a/b;
cout<<" a/b="<<wynik<<"\n";
}
}
catch(int) {
cout<<" Błąd - dzielenie przez zero!!"<<"\n";
}
cout<<"Po catch program działa dalej, ale pętla została przerwana."<<"\n";
_getch();
return 0;
}

Po uruchomieniu mamy:
a=2 b=5 a/b=0.4
a=2 b=4 a/b=0.5
a=2 b=3 a/b=0.666667
a=2 b=2 a/b=1
a=2 b=1 a/b=2
a=2 b=0 Błąd - dzielenie przez zero!!
Po catch program działa dalej, ale pętla została przerwana.
94 Microsoft Visual C++ 2012. Praktyczne przykłady

Jeśli używamy standardowych typów, nie mamy możliwości wyszczególniania wielu


typów wyjątków, ponieważ szybko skończą nam się standardowe typy zmiennych.
Rozwiązaniem jest potraktowanie wyjątków jako obiektu i napisanie odpowiednich
klas. Możemy w ten sposób tworzyć hierarchię wyjątków, od ogólnych do najbardziej
szczegółowych.

Przestrzenie nazw
Przestrzenie nazw dają możliwość grupowania nazw zmiennych, funkcji i klas w za-
mknięte bloki niepowodujące konfliktu nazw. Dzięki temu w jednym programie mogą
istnieć na przykład dwie zupełnie różne klasy o tej samej nazwie, zamknięte w oddziel-
nych przestrzeniach nazw. Deklaracje dla danej przestrzeni rozpoczynamy słowem
kluczowym namespace i nazwą przestrzeni.
namespace nazwa {
deklaracje zmiennych, funkcji, klas
}

Dostęp do funkcji czy klas poszczególnych przestrzeni uzyskujemy za pomocą ope-


ratora ::. Wszystkie funkcje standardowe C++ należą do przestrzeni std. Poprzez dyrek-
tywę using namespace std jawnie deklarujemy, że będziemy używali nazw z przestrzeni
standardowej, jeśli nie określono inaczej. Można też używać operatora :: każdorazowo
przy korzystaniu ze składowej danej przestrzeni nazw, na przykład std::cout.

Przykład 4.14

Utwórz dwie klasy o identycznych nazwach w jednym programie.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Zaczniemy
od załączenia potrzebnych nagłówków bezpośrednio pod nagłówkiem stdafx.h oraz
użycia przestrzeni std.
#include <iostream>
#include <conio.h>

using namespace std;

Przed funkcją main() utworzymy dwie przestrzenie nazw, a w każdej z nich zadekla-
rujemy klasę taka.
namespace moja {
class taka {
public:
taka() {
cout<<"moja.taka"<<endl;
}
};
Rozdział 4. ♦ Struktury, klasy, obiekty 95

}
namespace twoja {
class taka {
public:
taka() {
cout<<"twoja.taka"<<endl;
}
};
}

W funkcji main() będziemy tworzyć obiekty klas taka z dwóch przestrzeni. Oto cały
program:
int _tmain(int argc, _TCHAR* argv[]) {
moja::taka k;
twoja::taka a;
_getch();
return 0;
}

Po kompilacji i uruchomieniu otrzymamy:


moja.taka
twoja.taka

Widać, że mimo identycznych nazw nie ma konfliktu i możliwe jest wywołanie od-
powiednich konstruktorów.

Przestrzenie nazw są przydatne w dużych projektach, nad którymi równocześnie pracuje


wielu programistów. Przydzielając każdemu programiście inną przestrzeń nazw, nie
mamy kłopotu z uzgadnianiem nazw zmiennych i funkcji, klas, a także innych obiektów.

W C++/CLI przestrzenie nazw są bardzo ważne i szeroko wykorzystywane.


96 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 5.
Konstruowanie
i usuwanie obiektów klas

Konstruktory i destruktory
Każda klasa posiada specjalną metodę zwaną konstruktorem. Jest ona wykonywana
zawsze w chwili deklaracji obiektu klasy. Konstruktor może wykonywać różne in-
strukcje, ale najczęściej jego zadaniem jest inicjalizacja pól, przydzielanie pamięci
czy konwersja typów. Nawet jeśli nie zdefiniujemy go jawnie, jest on dodawany przez
kompilator i służy tylko do tworzenia obiektu klasy. Taki konstruktor nazywa się kon-
struktorem domyślnym. Istnieje także metoda zwana destruktorem, która usuwa obiekt
klasy i może wykonywać różne metody. Zarówno konstruktor, jak i destruktor mają
taką samą nazwę jak klasa, z tym że destruktor poprzedzony jest znakiem ~. Składnia
konstruktora jest następująca:
nazwa_klasy::nazwa_klasy(parametry) {}

Zaś składnia destruktora wygląda tak:


nazwa_klasy::~nazwa_klasy() {}

Przykład 5.1

Napisz program wykorzystujący klasę z jawnym konstruktorem i destruktorem.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Zaczniemy
od załączenia potrzebnych nagłówków bezpośrednio pod nagłówkiem stdafx.h.
#include <iostream>
#include <conio.h>
98 Microsoft Visual C++ 2012. Praktyczne przykłady

Poniżej zadeklarujemy klasę prosta o właściwościach jak poprzednio, ale z konstruktorem


i destruktorem. Niech pola x i y klasy będą teraz wskaźnikami do typu int.
class prosta {
public:

prosta(int,int);
~prosta();
int podajX();
int podajY();

private:
int* x;
int* y;
};

Definicje konstruktora i destruktora oraz metod podajX() i podajY() piszemy pod de-
klaracją klasy, jak poprzednio. W konstruktorze pola klasy będą tworzone za pomocą
operatora new, adresy wskaźników x i y będą wskazywały na zawartość pól klasy. De-
struktor zwolni pamięć za pomocą operatora delete.
prosta::prosta(int a,int b) {
std::cout<<"Konstruktor obiektu klasy prosta\n";
x = new int(a);
y = new int(b);
}

prosta::~prosta() {
std::cout<<"Destruktor obiektu klasy prosta\n";
delete x;
delete y;
}

int prosta::podajX() {
return *x;
}

int prosta::podajY() {
return *y;
}

Teraz w funkcji main() możemy już deklarować obiekty klasy prosta z inicjalizacją
wartości; zrobimy to również w sposób dynamiczny, za pomocą operatora new. Powstały
obszar pamięci z obiektem jest wskazywany przez wskaźnik wsk. Metody wyświetla-
jące współrzędne wywołujemy za pomocą operatora ->. Instrukcja delete usuwa
obiekt i wywołuje destruktor.
int _tmain(int argc, _TCHAR* argv[])
{
prosta* wsk = new prosta(30,70);
std::cout<<"Wartości pól: x="<<wsk->podajX()<<" y="<<wsk->podajY()<<"\n";
delete wsk;

_getch();
return 0;
}
Rozdział 5. ♦ Konstruowanie i usuwanie obiektów klas 99

Wynik działania programu:


Konstruktor obiektu klasy prosta
Wartości pól: x=30 y=70
Destruktor obiektu klasy prosta

Jak widać, destruktor został wywołany w momencie usuwania obiektu klasy prosta,
na który wskazywał wskaźnik wsk.

Teraz musimy podawać wartości przy każdym utworzeniu obiektu klasy prosta. Wy-
godnie byłoby mieć możliwość wyboru między tworzeniem obiektu bez inicjalizacji
i z nią. Taką możliwość (a także o wiele więcej) daje nam przeciążanie konstruktorów.

Przeciążanie konstruktorów
Konstruktory, podobnie jak inne metody, można przeciążać. Ten mechanizm pozwala
zadeklarować dwa konstruktory, z których na przykład jeden będzie inicjalizował wartości
pól klasy, a drugi nie. Klasa może zawierać też inne specjalnie nazwane rodzaje kon-
struktorów, o których powiemy za chwilę.

Do poprzedniego programu dodamy drugi konstruktor, który będzie tworzył obiekt


bez inicjalizacji.

Przykład 5.2

Napisz program wykorzystujący klasę z konstruktorem przeciążonym.

Rozwiązanie
Zacznij od wykonania jeszcze raz przykładu 5.1, jeżeli nie zachowałeś jego kodu. Do
klasy prosta dodaj konstruktor bezparametrowy. Wnętrze tego konstruktora tworzy
zmienne w pamięci, ale nie nadaje im wartości.
class prosta {
public:

prosta(int,int);
prosta() {
std::cout<<"Konstruktor obiektu klasy prosta bez parametrów \n";
x = new int;
y = new int;
} //tu nowy konstruktor
~prosta();
int podajX();
int podajY();
private:
int* x;
int* y;};
100 Microsoft Visual C++ 2012. Praktyczne przykłady

Teraz w funkcji main() możemy tworzyć obiekty na dwa sposoby: za pomocą pierw-
szego (z inicjalizacją) lub drugiego (bez inicjalizacji) konstruktora. Dopisz do main()
nowe linie, jak zaznaczono w komentarzach. W nowych liniach tworzymy obiekt klasy
prosta, na który wskazuje wskaźnik wsk1. Ten drugi obiekt jest tworzony za pomocą
konstruktora bez parametrów. Ponieważ wartości nie są inicjalizowane, zawierają
przypadkowe dane, a ich wydruk mija się z celem. Dowodem wywołania konstruktora
bezparametrowego będą wyświetlane przez niego napisy.
int _tmain(int argc, _TCHAR* argv[])
{
prosta* wsk = new prosta(30,70);
std::cout<<"Wartości pól: x="<<wsk->podajX()<<" y="<<wsk->
podajY()<<"\n";
delete wsk;

//tu początek nowych linii


prosta* wsk1 = new prosta();
delete wsk1;
//tu koniec

_getch();
return 0;

Oto wynik:
Konstruktor obiektu klasy prosta
Wartości pól: x=30 y=70
Destruktor obiektu klasy prosta
Konstruktor obiektu klasy prosta bez parametrów
Destruktor obiektu klasy prosta

Konstruktor kopiujący
Konstruktor kopiujący tworzy kopię istniejącego obiektu klasy. Jego parametrem jest
stała referencja do obiektu, który ma zostać skopiowany. Stała referencja zapewnia, że
konstruktor nie może zmienić kopiowanego obiektu. Samo kopiowanie polega na utwo-
rzeniu nowego obiektu operatorem new i przepisaniu do niego zawartości parametru
konstruktora, czyli obiektu kopiowanego.

Przykład 5.3
Skopiuj obiekt konstruktorem kopiującym.

Rozwiązanie
Dopiszemy konstruktor kopiujący dla klasy prosta z poprzednich przykładów. Po-
trzebujemy kodu z przykładu 5.2. Mamy już klasę, więc możemy zająć się dopisaniem
Rozdział 5. ♦ Konstruowanie i usuwanie obiektów klas 101

konstruktora kopiującego. Proponuję dopisać go po klamrze zamykającej konstruktor


bezparametrowy.
prosta() {
std::cout<<"Konstruktor obiektu klasy prosta bez parametrów \n";
x = new int;
y = new int;
} //istniejący konstruktor bezparametrowy

//konstruktor kopiujący — tu piszemy


prosta(const prosta& _param)
{

Pierwsza linia mówi, że konstruktor akceptuje jako parametr stałą referencję do obiektu
klasy prosta, czyli obiekt do skopiowania. Dalej mamy klamrę otwierającą. We wnętrzu
konstruktora najpierw mamy komentarz wypisywany w konsoli. Po tym komentarzu
będziemy poznawać, że wywołany został ten właśnie konstruktor.
std::cout<<"Konstruktor kopiujący \n";

Dalej tworzymy pola klasy i inicjalizujemy je wartościami obiektu kopiowanego.


Wreszcie zamykamy klamrę i mamy gotowy konstruktor.
x=new int(*_param.x);
y=new int(*_param.y);
}

Funkcję _tmain() przerobimy całkowicie, eksponując kopiowanie obiektów. Utworzymy


obiekt obj1, a następnie obj2, będący kopią obj1. Jako trzeci utworzymy obiekt obj3
i od razu podstawimy do niego obiekt obj1 za pomocą operatora =.
int _tmain(int argc, _TCHAR* argv[])
{
prosta obj1(30,70);
std::cout<<"Wartości pól obj1: x="<<obj1.podajX()<<" y="<<obj1.podajY()<<"\n";

std::cout<<"Wywołanie bezpośrednie"<<"\n";
prosta obj2(obj1);
std::cout<<"Wartości pól obj2: x="<<obj1.podajX()<<" y="<<obj1.podajY()<<"\n";

std::cout<<"Operator ="<<"\n";
prosta obj3 = obj1;
std::cout<<"Wartości pól obj3: x="<<obj1.podajX()<<" y="<<obj1.podajY()<<"\n";

_getch();
return 0;
}

Uruchom program, otrzymasz następujące wyniki:


Konstruktor obiektu klasy prosta
Wartości pól obj1: x=30 y=70
Wywołanie bezpośrednie
Konstruktor kopiujący
102 Microsoft Visual C++ 2012. Praktyczne przykłady

Wartości pól obj2: x=30 y=70


Operator =
Konstruktor kopiujący
Wartości pól obj3: x=30 y=70

Obiekt obj2 jest kopią obj1, a jego pola zawierają te same wartości. Operator przypisania
również wywołał konstruktor kopiujący. Zachowaj program, ponieważ będzie jeszcze
potrzebny.

Konstruktor przenoszący
Przed wprowadzeniem standardu C++11 jedynym sposobem utworzenia obiektu na
podstawie obiektu tymczasowego było użycie konstruktora kopiującego. Nowy obiekt
zawierał kopię obiektu tymczasowego. Skopiowanie obiektu, szczególnie jeśli zawiera on
dużo danych, jest czasochłonne. W przypadku elementu tymczasowego kopiowanie
jest szczególnie mało efektywne. Jeśli obiekt jest tymczasowy, to może lepiej przenieść
jego adres do nowego obiektu, tak jak ustawia się wskaźnik na adres. Skoro obiekt jest
tymczasowy, to i tak zostanie za chwilę zniszczony. Weźmy zatem jego adres i „uratujmy”
zawartość, przypisując go do nowego, stałego obiektu. Do tej pory nie było takiej możli-
wości, bo nie było można pobrać adresu obiektu tymczasowego. Po wprowadzeniu re-
ferencji do r-wartości mamy już taką możliwość. Jej realizacją jest konstruktor prze-
noszący, a cała składnia procedury nazywa się semantyką przenoszenia. Chyba
najlepiej zobaczyć to na przykładzie.

Przykład 5.4

Napisz program z konstruktorem przenoszącym.

Rozwiązanie
Znowu potrzebujemy klasy prosta. Zacznij od wykonania jeszcze raz przykładu 5.3, jeżeli
nie zachowałeś jego kodu. Konstruktor przenoszący możemy napisać zaraz za kopiują-
cym. Wzajemne położenie metod klasy może być dowolne, możesz więc wpisać go
gdziekolwiek. Ja staram się zachować jakiś porządek w przykładach zamieszczanych
w książce.
y=new int(*_param.y);
} //koniec konstruktora kopiującego — ten kod istnieje

// konstruktor przenoszący — tu zaczynamy pisanie


prosta(prosta&& _param) :x(NULL), y(NULL)
{

Rozpoczynamy tak jak każdy konstruktor: nazwą taką jak nazwa klasy, parametrem zaś
jest nowy typ w C++, czyli referencja do r-wartości. Na liście inicjalizacyjnej ustawiamy
Rozdział 5. ♦ Konstruowanie i usuwanie obiektów klas 103

adresy nowego obiektu na wartość NULL, aby mieć pewność, że nie wskazują przypad-
kowej wartości. Rozpoczynamy konstruktor klamrą i piszemy:
std::cout<<"Konstruktor przenoszący \n";
x=_param.x;
y=_param.y;

Pierwsza linia to zgłoszenie konstruktora w konsoli. Druga i trzecia są najważniejsze.


Ustawiamy adres pól nowego obiektu na pola obiektu tymczasowego. Zauważ, że niczego
tu nie tworzymy, nie ma operatora new, a jedynie przepisujemy adresy istniejącego
obiektu tymczasowego do nowego obiektu. Co się stanie teraz z obiektem tymczasowym?
Zostanie za chwilę zniszczony i nie można temu zapobiec. Wraz z nim utracimy zawar-
tość znajdującą się pod jego adresami. My tego nie chcemy, bo na tę zawartość już
wskazuje nasz nowy obiekt. Wobec tego ustawimy wskaźniki obiektu tymczasowego
tak, aby wskazywały na adres zero (NULL). Teraz zniszczenie obiektu tymczasowego
nam już nie zaszkodzi, bo nie będzie powiązany z cenną zawartością.
_param.x=NULL;
_param.y=NULL;
}

Klamra zamykająca kończy konstruktor.

Aby wywołać konstruktor, potrzeba tymczasowego obiektu klasy prosta. Napiszemy


więc funkcję tymczas() poza klasą, która taki obiekt zwróci. Będzie ona miała typ zwra-
cany prosta, a w jej wnętrzu zostanie utworzony obiekt i zwrócony za pomocą return.
prosta tymczas()
{
prosta p(20,30);
return p;
}

W funkcji _tmain() utworzymy obiekt klasy prosta z tymczasowego obiektu zwróconego


przez funkcję tymczas(). Argumentem konstruktora jest bezpośrednio wywołanie
funkcji, wartość zwracana nie ma więc nazwy.
int _tmain(int argc, _TCHAR* argv[])
{
prosta obj1(tymczas());
std::cout<<"Wartości pól wsk: x="<<obj1.podajX()<<" y="<<obj1.podajY()<<"\n";

_getch();
return 0;
}

Po uruchomieniu programu w konsoli pojawią się napisy:


Konstruktor obiektu klasy prosta
Konstruktor przenoszący
Destruktor obiektu klasy prosta
Wartości pól wsk: x=20 y=30
104 Microsoft Visual C++ 2012. Praktyczne przykłady

Pierwsza linia to utworzenie tymczasowego obiektu przez funkcję tymczas(), druga to


przeniesienie tego obiektu do stałego obiektu obj1 za pomocą konstruktora przenoszące-
go. Dalej mamy usunięcie obiektu tymczasowego. Ostatnia linia wyświetla zawartość pól
obj1. Widać, że jest ona taka sama jak obiektu z funkcji tymczas, a więc wszystko działa.

Konstruktory definiowane
w klasach dziedziczonych
Klasy dziedziczone mogą mieć oczywiście konstruktory definiowane. Prześledźmy
kolejność wywoływania konstruktorów klas dziedziczonych i przekazywanie para-
metrów.

Przykład 5.5

Napisz klasę bazową z konstruktorem inicjującym wartość pola tej klasy. Następnie
dopisz do niej klasę pochodną. Niech przy tworzeniu obiektu klasy pochodnej będzie
możliwość inicjacji pola klasy bazowej.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Będziemy
korzystać ze strumienia cout, więc załączymy odpowiedni nagłówek.
#include <iostream.h>

Zaczynamy od napisania klasy bazowej, którą nazwiemy podstawowa. Będzie ona miała
jedno pole typu całkowitego int oraz konstruktor i destruktor. Konstruktor akceptuje
parametr i wpisuje jego wartość do pola klasy podstawowa. Zarówno konstruktor, jak
i destruktor zaznaczają swoje wywołanie w konsoli.
class podstawowa
{
public:
podstawowa(int _dana) {
std::cout<<"Konstruktor klasy podstawowa"<<std::endl;
_danapod=_dana;
std::cout<<"_danapod="<<_danapod<<std::endl;
}
~podstawowa() {std::cout<<"Destruktor klasy podstawowa"<<std::endl;}
private:
int _danapod;
};

Czas teraz na klasę dziedziczącą. Nazwałem ją dziedzic, będzie potomkiem w trybie


publicznym. Konstruktor tej klasy wywołuje konstruktor klasy podstawowa na liście ini-
cjalizacyjnej. Konstruktor klasy pochodnej jawnie wywołuje więc konstruktor klasy
bazowej.
Rozdział 5. ♦ Konstruowanie i usuwanie obiektów klas 105

class dziedzic : public podstawowa


{
public:
dziedzic(int _danaDoPod) : podstawowa(_danaDoPod) {std::cout<<"Konstruktor
klasy dziedzic"<<std::endl;}
~dziedzic() {std::cout<<"Destruktor klasy dziedzic"<<std::endl;}
private:
int _danadzie;
};

W funkcji _tmain() tworzymy, a następnie niszczymy obiekt klasy dziedzic:


int _tmain(int argc, _TCHAR* argv[])
{
dziedzic* a = new dziedzic(3);
delete a;
system("pause");
return 0;
}

Po uruchomieniu programu mamy w konsoli:


Konstruktor klasy podstawowa
_danapod=3
Konstruktor klasy dziedzic
Destruktor klasy dziedzic
Destruktor klasy podstawowa
Aby kontynuować, naciśnij dowolny klawisz . . .

Najpierw tworzony jest obiekt klasy bazowej i inicjalizowane jest jego pole, następnie
konstruowany jest obiekt klasy potomnej. Dzieje się tak dlatego, że obiekt klasy potomnej
nie może być skonstruowany bez obiektu klasy bazowej. Dlatego konstruktor klasy
bazowa jest wywoływany na liście inicjalizacyjnej, aby zadziałał jeszcze przed rozpoczę-
ciem wykonywania konstruktora klasy pochodnej. Usuwanie obiektów przebiega
w odwrotnej kolejności. Gdyby było inaczej, usuwanie obiektu klasy podstawowa znisz-
czyłoby część obiektu klasy potomnej. Usunięcie później niepełnego obiektu klasy
dziedzic doprowadziłoby do błędu.

Konstruktor kopiujący
w klasie potomnej
Co robić, jeśli potrzebujemy możliwości tworzenia obiektu klasy potomnej z już istnie-
jących? Można napisać konstruktor kopiujący w klasie pochodnej. Kiedy jednak zaczniesz
pisać taki konstruktor, dojdziesz do wniosku, że trzeba wywołać też konstruktor kopiu-
jący klasy bazowej, który skopiuje obiekt bazowy. I mamy problem: co przekazać do tego
konstruktora? Zgodnie z zasadami należałoby przekazać obiekt klasy bazowej, tylko
106 Microsoft Visual C++ 2012. Praktyczne przykłady

jak to zrobić? Okazuje się, że wystarczy jako parametr konstruktora kopiującego klasy
bazowej podstawić obiekt klasy potomnej. Dzięki odpowiedniej organizacji pamięci
obiekt zostanie niejako zrzutowany na obiekt klasy bazowej.

Przykład 5.6

Do klasy potomnej dziedzic z poprzedniego przykładu dopisz konstruktor kopiujący.


Konstruktor powinien kopiować cały obiekt klasy potomnej wraz ze składowymi klasy
bazowej.

Rozwiązanie
Posłużymy się przykładem 5.5. Do klasy dziedzic dopisujemy konstruktor kopiujący.
Jest on bardzo prosty, możesz napisać go zaraz po istniejącym konstruktorze. Akceptuje
stałą referencję do obiektu klasy dziedzic, ta referencja staje się parametrem dla kon-
struktora klasy podstawowa. Następnie w konsoli wypisywany jest tekst zawiadamiający,
że konstruktor został wywołany.
std::cout<<"Konstruktor klasy dziedzic"<<std::endl;} // ta linia istnieje
//od tego momentu nowy konstruktor
dziedzic(const dziedzic& _param): podstawowa(_param)
{
std::cout<<"Konstruktor kopiujący klasy dziedzic\n";

Wywoływany konstruktor klasy podstawowa również jest kopiujący. Nie chcemy kon-
struktora generowanego przez kompilator, należy zatem samemu go napisać. Jego pa-
rametrem będzie stała referencja do obiektu klasy podstawowa. We wnętrzu mamy ko-
piowanie zawartości pola tej klasy do nowego obiektu.
podstawowa(const podstawowa& _param)
{
std::cout<<"Konstruktor kopiujący klasy podstawowa\n";
_danapod=_param._danapod;
std::cout<<"_danapod="<<_danapod<<std::endl;
}

W funkcji _tmain() utworzymy obiekt b jako kopię obiektu a.


int _tmain(int argc, _TCHAR* argv[])
{
dziedzic* a = new dziedzic(3);
dziedzic* b = new dziedzic(*a);
delete a;
delete b;
system("pause");
return 0;
}
Rozdział 5. ♦ Konstruowanie i usuwanie obiektów klas 107

Po uruchomieniu programu w konsoli mamy:


Konstruktor klasy podstawowa
_danapod=3
Konstruktor klasy dziedzic
Konstruktor kopiujący klasy podstawowa
_danapod=3
Konstruktor kopiujący klasy dziedzic
Destruktor klasy dziedzic
Destruktor klasy podstawowa
Destruktor klasy dziedzic
Destruktor klasy podstawowa
Aby kontynuować, naciśnij dowolny klawisz . . .

W pierwszych trzech liniach mamy budowę obiektu a, w następnych trzech kopiowa-


nie tego obiektu do obiektu b. Najpierw uruchamia się konstruktor kopiujący klasy
bazowej, a następnie klasy dziedziczącej. Konstrukcja obiektu kopiowanego przebiega
więc w podobny sposób jak wcześniej konstrukcja wzorca.

Konstruktor definiowany
w szablonie klasy
Konstruktor w szablonie klasy konstruuje obiekt o typie określanym w chwili wywołania
tego konstruktora. Pola klasy o typie parametru szablonu mogą być wskaźnikami, a pa-
mięć może być przydzielana operatorem new, tak jak w „zwykłych” typach. Napiszemy
szablon klasy z polem w postaci wskaźnika, konstruktor będzie inicjalizował tablicę,
a dostęp do tej tablicy będzie poprzez ten wskaźnik. Drugim polem klasy będzie rozmiar
tablicy. Będzie to więc tablica elementów o parametryzowanym typie. Oprócz kon-
struktora niech zawiera dwie metody: jedna będzie wpisywała zawartość do podanego
elementu tablicy, a druga zwracała taką wartość. Takie tablice oferuje na przykład stan-
dardowa klasa vector. Oczywiście, mają one nieporównywalnie większe możliwości.

Przykład 5.7

Napisz szablon klasy z tablicą o parametryzowanym typie.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1. Będziemy korzy-
stać ze strumienia cout, więc załączymy odpowiedni nagłówek.
#include "iostream"
108 Microsoft Visual C++ 2012. Praktyczne przykłady

Zaczynamy od napisania pustego szablonu klasy.


template <class T>
class szablon
{
};

Następne linie kodu aż do odwołania wpisuj między nawiasami klamrowymi szablonu.

Rozpoczynamy od publicznego konstruktora z parametrem. Parametr będzie rozmiarem


inicjalizowanej tablicy. Dodatkowo jego wartość podstawimy do pola _rozmiar.
public:
szablon(int _rozmiar) {
_dana = new T[_rozmiar];
rozmiar = _rozmiar;
}

Teraz metoda wpisująca zadaną wartość w odpowiednią komórkę tablicy:


void wpiszDana(T wpis, int _indeks)
{
if (_indeks>=rozmiar) {std::cout<<"Za mały rozmiar tablicy"<<std::endl; return;}
_dana[_indeks]=wpis;
}

Jak widać, dodałem prostą kontrolę błędu: próba wpisania do pola tablicy wartości
poza zakresem określonym jej rozmiarem powoduje wyjście z metody. Następna jest me-
toda odczytu. Jako parametr przyjmuje numer pola tabeli. Tu też mamy kontrolę, czy
nie próbujemy odczytać komórki poza rozmiarem tabeli. Jeśli tak, zwracana jest wartość
komórki zero. W zasadzie w takim wypadku nic nie powinno być zwracane. Ponieważ
metoda musi zwrócić parametr typu parametryzowanego, zwrócenie jakiegoś rekordu
tablicy było najprostszym wyjściem z sytuacji.
T podajDana(int _indeks)
{
if (_indeks>=rozmiar) {std::cout<<"Indeks poza tablica - zracam zawartość
komórki 0"<<std::endl; return _dana[0];}
return _dana[_indeks];
}

Przyda się jeszcze destruktor likwidujący tablicę.


~szablon() { delete[] _dana;}

Prywatnymi składowymi są wskaźnik do elementu typu T wskazujący na komórkę tablicy


i rozmiar tablicy.
private:
T* _dana;
int rozmiar;

Szablon już skończony, przenosimy się teraz do funkcji _tmain(). Tutaj będziemy ten
szablon jakoś użytkować. Zacznijmy od doprowadzenia funkcji _tmain() do postaci
jak niżej.
Rozdział 5. ♦ Konstruowanie i usuwanie obiektów klas 109

int _tmain(int argc, _TCHAR* argv[])


{
// tu będziemy wpisywać dalsze linie
system("pause");
return 0;
}

Jeżeli teraz uruchomimy aplikację, to otrzymamy puste okno konsoli z oczekiwaniem


na naciśnięcie klawisza. Kolejne linie będziemy wpisywać, począwszy od miejsca zazna-
czonego komentarzem.

Najpierw stworzymy dwuelementową tablicę liczby typu int. Ponieważ w C++ indeksy
numerowane są od zera, tablica będzie miała indeksy 0 i 1. Tak będzie wyglądać pierw-
sza linia w funkcji _tmain():
szablon<int> a(2);

Wpiszemy teraz liczbę 2 w pole tablicy a o indeksie 1.


a.wpiszDana(2,1);

Dalej zapomnieliśmy, jaki rozmiar ma tablica a, i spróbujemy wpisać liczbę 5 w pole


tablicy a o indeksie 2.
a.wpiszDana(5,2);

Sprawdźmy, jaka wartość jest w polu nr 1 tablicy a.


std::cout<<a.podajDana(1)<<std::endl;

A teraz skonstruujemy tablicę łańcuchów tekstowych typu string z biblioteki stan-


dardowej. Tablica będzie miała pięć elementów.
szablon<std::string> b(5);

Do pola numer zero podstawimy łańcuch znakowy "Tutaj tekst 0", a do pola numer
jeden "Tutaj tekst 1".
b.wpiszDana("Tutaj tekst 0",0);
b.wpiszDana("Tutaj tekst 1",1);

Jak widać, metoda wpiszDana() operuje teraz na typie string. Spróbujemy wpisać
zawartość pola nr 0, 1 i 6. Jak się domyślasz, pierwsza i druga operacja powinny się udać,
a trzecia nie.
std::cout<<b.podajDana(0).c_str()<<std::endl;
std::cout<<b.podajDana(1).c_str()<<std::endl;
std::cout<<b.podajDana(6).c_str()<<std::endl;

Po uruchomieniu aplikacji mamy w konsoli:


Za mały rozmiar tablicy
2
Tutaj tekst 0
Tutaj tekst 1
110 Microsoft Visual C++ 2012. Praktyczne przykłady

Indeks poza tablicą - zwracam zawartość komórki 0


Tutaj tekst 0
Aby kontynuować, naciśnij dowolny klawisz . . .

Pierwsza i druga linia funkcji _tmain() nie wyprodukowała żadnego tekstu, trzecia sygna-
lizuje błąd pisania poza tabelą. Czwarta wypisuje zawartość pola 1 tablicy a. Linie 5,
6 i 7 wykonują operacje zgodnie z tym, co napisaliśmy, linia numer 8 i 9 wypisuje
teksty z pól tablicy i wreszcie linia 9 sygnalizuje błąd odczytu. Program działa zgodnie
z przewidywaniami.

Struktury a klasy — porównanie


Wiemy już trochę o klasach, zobaczmy teraz, jak te wiadomości mają się do struktur.
W następnym przykładzie zbudujemy strukturę, która będzie wyglądała zupełnie jak
klasa. Będzie miała jawny konstruktor i prywatne składowe.

Przykład 5.8

Napisz strukturę z jawnym konstruktorem i hermetyzacją (kapsułkowaniem) danych.

Rozwiązanie
Utwórz nowy projekt aplikacji konsolowej win32 jak w przykładzie 1.1 Zaczniemy
od załączenia potrzebnego nagłówka bezpośrednio pod nagłówkiem stdafx.h.
#include <iostream>

Poniżej budujemy naszą strukturę; nazwiemy ją zbior. Zaczynamy słowem kluczowym


struct, następnie nazwa struktury i klamra otwierająca. A dalej zupełnie jak w klasie:
konstruktor pobierający parametry i inicjalizujący pola struktury na liście inicjalizacyjnej.
Następnie funkcja podająca wartość jednego z pól klasy (bezpośredni dostęp nie będzie
już możliwy). Na końcu dwa prywatne pola klasy a i b. Całość wygląda jak niżej:
struct zbior {
zbior(int _a, float _b) : a(_a),b(_b) {};
int podajA() {return a;};
private:
int a;
float b;
};

Zauważ, że definicja konstruktora w klasie musiała być poprzedzona specyfikatorem


public:, ponieważ domyślnie każda składowa klasy jest prywatna. W strukturze składowe
są domyślnie publiczne. W funkcji _tmain zadeklarujemy obiekt struktury, przy czym teraz
skorzystamy z konstruktora. Następnie wypiszemy zawartość pola a.
Rozdział 5. ♦ Konstruowanie i usuwanie obiektów klas 111

int _tmain(int argc, _TCHAR* argv[])


{
zbior dane(1,2.3);
std::cout<<"dane.a="<<dane.podajA()<<std::endl;
system("pause");
return 0;
}

Po uruchomieniu programu mamy:


dane.a=1
Aby kontynuować, naciśnij dowolny klawisz . . .

Składnia definicji obiektu wraz z inicjalizacją dla struktury zawierającej elementy typowe
dla klasy jest identyczna jak w przypadku klasy. Tymczasem definicja i inicjalizacja
obiektu struktury w poprzednim rozdziale przypominała inicjalizację tablicy. Tę różnicę
znosi standard C++11. Od teraz można oba rodzaje struktur definiować tak samo. W na-
szym przypadku inicjalizacja struktury dane wyglądałaby następująco:
zbior dane{1,2.3}

niezależnie od tego, czy struktura miałaby postać taką jak w poprzednim przykładzie,
czy taką:
struct zbior {
int a;
float b;
};

VC++ 2012 nie wspiera tej części standardu, więc nie możemy wykonać przykładu.
112 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 6.
Interface win32,
główne okno aplikacji
Interface win32 (WinAPI) jest 32-bitową bazą programistyczną do pisania aplikacji
Windows. Jest to podstawowy mechanizm pisania aplikacji dla Windows. Inne roz-
szerzenia (jak na przykład.NET Framework) odwołują się często do WinAPI. Aby
opisać całość, można spokojnie napisać książkę grubszą, niż trzymasz teraz w ręku,
ale warto poznać choć podstawy tego zagadnienia, zanim zaczniemy używać .NET.
Programy pisane wyłącznie z użyciem WinAPI są długie i dosyć zawiłe, stąd pewnie
rozwój rozszerzeń, takich jak choćby wspomniany już .NET Framework.

W tym rozdziale zajmiemy się podstawą aplikacji, czyli zaprogramowaniem główne-


go okna. Kiedy już zrozumiesz, jak działają mechanizmy podstawowe, łatwiej będzie
zgłębiać tajniki win32, wychodząc nawet poza materiał opisany w tej książce. Taka
jest zresztą moja intencja: opisać podstawy, bo całość jest bardzo obszerna. Kiedy
wybierasz utworzenie projektu okienkowego win32, środowisko pisze kod głównego
okna za Ciebie. Skupimy się więc w tym rozdziale raczej na analizie tego, co podaje
nam Visual C++, niż na pisaniu własnego programu.

Części składowe podstawowego kodu


okienkowej aplikacji win32
Zanim przejdziemy do szczegółów, spróbujemy spojrzeć szeroko na cały projekt pod-
stawowej aplikacji i zobaczyć, jakie bloki i mechanizmy ze sobą współdziałają. Przy-
kłady w tym rozdziale będą z konieczności raczej do czytania i analizy gotowego ko-
du niż do pisania.
114 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 6.1

Objaśnienie części składowych aplikacji win32.

Rozwiązanie
Utwórz nowy projekt win32 według przykładu 1.3. Nazwa jest w tej chwili nieistotna,
może nazwij go okno. Rzut oka na plik okno.cpp, jaki przed chwilą zbudował kompi-
lator, przekona Cię, że jest tam całkiem sporo kodu. Dzięki temu nie musimy zaczynać
wszystkiego od początku. W pierwszej kolejności mamy blok nagłówkowy z kilkoma
instrukcjami #include, następnie deklaracje kilku zmiennych, a właściwie obiektów
globalnych, i wreszcie pięć funkcji. Pierwszą funkcją jest _tWinMain() i jest to główna
funkcja, od której zaczyna się wykonywanie programu. Żeby wyjaśnić, jaką rolę peł-
nią te elementy, trzeba opisać strukturę aplikacji win32.

Zaczynamy od słowa instancja (ang. instance) — po polsku inaczej wystąpienie.


Mówiąc w uproszczeniu, instancja to reprezentacja działającego programu w pamięci.
W momencie, kiedy uruchamiamy nowy program, system widzi go jako instancję.
Zauważ, że kiedy uruchomimy jedną aplikację kilka razy w jednej sesji, to będzie ona
miała wiele instancji. Następną sprawą jest okno, czyli to, z czym mamy głównie do
czynienia w systemach okienkowych. O oknach wygodnie myśleć jako o obiektach —
każde z nich żyje przecież własnym życiem na pulpicie, choć mogą być ze sobą powią-
zane. A skoro są obiektami, to ich reprezentacja w programie jest klasą. Znamy więc
już wystąpienie aplikacji i wiemy, czym jest okno. Przydałoby się jeszcze, żeby ist-
niała jakaś komunikacja z tymi oknami i żeby wykonywały polecenia oraz przesyłały
i odbierały dane. I rzeczywiście tak jest. Te polecenia nazywają się komunikatami
(ang. messages). Ich przechwytywaniem z systemu zajmuje się pętla komunikatów
(ang. message loop). Działa ona przy instancji każdej aplikacji i oprócz przechwytywa-
nia rozsyła je też do okien swojej aplikacji. Sposób, w jaki okno reaguje na otrzymany
komunikat, opisany jest w funkcji zwrotnej okna (callback function). I to już wszystkie
podstawowe składniki. Teraz przyrządzimy z nich aplikację.

Na razie opiszę ten proces schematycznie, a w kolejnych przykładach pokażę, jak zapro-
gramować każdy z kroków. Rozpoczynamy od tak zwanej rejestracji klasy okna. Przed
rejestracją wypełniamy pola klasy, które opisują właściwości okna. Za pomocą tych pól
ustawiamy wygląd okna, ale — co ważniejsze — także podajemy nazwę jego funkcji
zwrotnej. Następnie inicjujemy instancję aplikacji, która będzie związana z tym oknem.
W końcu uruchamiamy pętlę wiadomości nasłuchującej komunikatów dla okna.

Skompiluj i uruchom kod napisany przez środowisko. Pojawi się puste okno główne
aplikacji. Posiada ono nawet menu (jak je zaprogramować, opowiem dalej), za pomo-
cą którego można zakończyć działanie aplikacji. Zapisz projekt, ponieważ przyda się
do następnych przykładów.

Pierwszy przykład był raczej opisowy; teraz zajmiemy się już programowaniem ko-
lejnych przedstawionych w nim działań.
Rozdział 6. ♦ Interface win32, główne okno aplikacji 115

Funkcja główna programu win32


Zgodnie z konwencją wszystkie programy C++ zaczynamy od funkcji głównej. Tutaj nie
może być inaczej; funkcja główna nazywa się _tWinMain(). I tak samo, jak to było w apli-
kacjach konsolowych, ma ona określoną liczbę parametrów. W programach win32 liczba
i znaczenie tych parametrów są inne niż w konsoli.
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,int nCmdShow)

Pierwszy parametr jest uchwytem do wystąpienia (instancji aplikacji), drugi teoretycznie


powinien zawierać uchwyt do poprzedniego wystąpienia (jeśli takie by istniało), jednak
zawsze wynosi NULL. Parametr lpCmdLine to argumenty przekazane przy wywołaniu
aplikacji z linii komend, tak jak argv „konsolowej” funkcji main(). Ostatnia zmienna
nCmdShow to określenie cech okna głównego aplikacji. Jest ona potrzebna w momencie
wyświetlania tego okna.

Klasa okna głównego


Okno jest obiektem klasy WNDCLASSEX, która jest już zdefiniowana w pliku nagłówkowym
windows.h. Jest to główny plik nagłówkowy interfejsu win32. W celu rejestracji okna
deklarujemy obiekt tej klasy, następnie wypełniamy jego pola i wreszcie rejestrujemy.

Przykład 6.2

Utwórz obiekt klasy okna i zarejestruj go.

Rozwiązanie
Możesz otworzyć projekt z poprzedniego przykładu lub otworzyć nowy według przy-
kładu 1.3. Podobnie jak w poprzednim przykładzie, niewiele tu napiszesz, ale będziemy
tym razem analizować kod szczegółowo. Rejestrację klasy można by zapisać bezpo-
średnio w funkcji _tWinMain(), jednak w celu zachowania przejrzystości kodu została
ona umieszczona w oddzielnej funkcji, wywoływanej z _tWinMain(). Ta funkcja nazy-
wa się MyRegisterClass(), a jej wywołanie w _tWinMain() znajdziesz w postaci:
MyRegisterClass(hInstance);

Parametrem tej funkcji jest instancja aplikacji. Na razie zostawmy kwestię, skąd ona się
wzięła, skoro jeszcze jej nie inicjowaliśmy. Poszukaj w kodzie funkcji MyRegisterClass().
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;

wcex.cbSize = sizeof(WNDCLASSEX);
116 Microsoft Visual C++ 2012. Praktyczne przykłady

wcex.style = CS_HREDRAW | CS_VREDRAW;


wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_OKIENKO));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_OKIENKO);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

return RegisterClassEx(&wcex);
}

Przeanalizujmy kolejno linie programu. Pierwsza to deklaracja obiektu klasy WNDCLASSEX.


Kolejno będziemy wypełniać pola tego obiektu. Za pomocą operatora sizeof() po-
bieramy rozmiar struktury okna i wpisujemy go w pole cbSize. A więc struktura zna swój
własny rozmiar. Wypiszę każdą linię jeszcze raz, aby nie było wątpliwości, o której
linii mowa.
WNDCLASSEX wcex;

wcex.cbSize = sizeof(WNDCLASSEX);

Pole style definiuje różne style okna i zachowania się kursora w oknie. Przedstawione tu
wartości oznaczają, że okno będzie odrysowywane (odświeżane) przy zmianie jego wy-
sokości CS_VREDRAW lub szerokości CS_HREDRAW.
wcex.style = CS_HREDRAW | CS_VREDRAW;

Składowa lpfnWndProc wskazuje na funkcję, która będzie przetwarzać komunikaty nad-


syłane do tego okna. Jest to funkcja zwrotna, nazywana też po prostu funkcją okna (już
o niej wspominałem).
wcex.lpfnWndProc = WndProc;

Pole cbClsExtra zawiera liczbę dodatkowych bajtów dodanych do każdej klasy, a cbWndExtra
liczbę dodatkowych bajtów dodanych do każdej działającej kopii okna. Obie te wartości
ustawiamy na zero.
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;

Pole hInstance wskazuje na uchwyt do wystąpienia aplikacji, której częścią jest dane
okno. W naszym przypadku uchwyt ten znajduje się w zmiennej _hInstance. Dlatego
ta zmienna musiała być przekazana do funkcji, aby można było z niej w tym momencie
skorzystać.
wcex.hInstance = hInstance;

Następna składowa określa ikonę widoczną w pasku aplikacji. Ta składowa musi być
inicjalizowana uchwytem do ikony.
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_OKIENKO));
Rozdział 6. ♦ Interface win32, główne okno aplikacji 117

Tutaj posłużymy się funkcją LoadIcon(). Funkcja ta pobiera ikonę z pliku exe lub z tak
zwanego skryptu zasobów (resource script). Posiada ona dwa parametry. Pierwszy to
uchwyt do instancji aplikacji, która zawiera ikonę, a drugi to nazwa ikony. U mnie aplika-
cja i cały projekt nazywa się okienko. W naszym przypadku ikona będzie zdefinio-
wana w skrypcie zasobów, który opiszę na końcu tego rozdziału. Na razie zaznaczmy
tylko, że ikona jest dana identyfikatorem IDI_OKIENKO. Ten identyfikator jest liczbą
całkowitą, musi więc być konwertowany na typ LPCTSTR, ponieważ takiego typu wy-
maga funkcja LoadIcon(). Istnieją też ikony standardowe. Dla nich parametr pierwszy
musi mieć wartość NULL. Identyfikatory ikon standardowych przedstawiłem niżej. Nie
wymagają one konwersji za pomocą makra MAKEINTRESOURCE:
IDI_APPLICATION Domyślna ikona aplikacji
IDI_ASTERISK Asterisk (Informacje)
IDI_EXCLAMATION Wykrzyknik (Ostrzeżenia)
IDI_HAND Ikona ręki (Poważne ostrzeżenia)
IDI_QUESTION Znak zapytania (Zapytania)
IDI_WINLOGO Logo Windows

Pole hCursor identyfikuje kursor, jaki będzie się pojawiał po najechaniu w obszar okna.
Funkcja LoadCursor() ma takie same parametry jak LoadIcon().
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);

Tu również występują standardowe kursory (pierwszy parametr NULL):


IDC_APPSTARTING Strzałka i mała klepsydra
IDC_ARROW Strzałka
IDC_CROSS Krzyżyk
IDC_IBEAM Kursor tekstowy
IDC_NO Koło ukośnie przekreślone
IDC_SIZEALL To samo co IDC_SIZE, ale dla innych systemów
IDC_SIZENESW, IDC_SIZENS, Podwójne strzałki w różnych kierunkach
IDC_SIZENWSE, IDC_SIZEWE
IDC_UPARROW Pionowa strzałka
IDC_WAIT Klepsydra

Kolejne pole to hbrBackground. Określa ono sposób wypełnienia tła okna. Inicjalizujemy
je nazwą jednego z kolorów systemowych określonych dla składowych interfejsu syste-
mów. Innymi słowy, możemy zażądać, aby okno miało kolor taki jak np. paski okien,
przyciski czy menu. W naszym przykładzie kolor jest ustawiony na standardowy ko-
lor okna. Kolor ten jest pobierany z tablicy kolorów w pliku winuser.h, przy czym do
identyfikatora trzeba dodać jedność. Nazwy kolorów muszą być konwertowane na typ
HBRUSH. Po zmianie schematu kolorów zmieni się też kolor tła naszego okna.
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
118 Microsoft Visual C++ 2012. Praktyczne przykłady

Składowa lpszMenuName wskazuje na menu stowarzyszone z oknem. Menu naszej aplikacji


jest zdefiniowane w skrypcie zasobów, podobnie jak ikona. Na razie przyjmij, że określa
je identyfikator IDC_OKIENKO.
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_OKIENKO);

Pole lpszClassName określa nazwę klasy okna. Kolejny raz korzystamy z zasobów, nazwa
aplikacji jest tam zapisana. Została ona wcześniej skopiowana do zmiennej globalnej
szWindowClass. Żeby zobaczyć, gdzie to zostało przeprowadzone, musisz się cofnąć
do początku funkcji _tWinMain() i znaleźć wywołanie funkcji.
LoadString(hInstance, IDC_OKIENKO, szWindowClass, MAX_LOADSTRING);

To właśnie za pomocą funkcji LoadString() nazwa klasy została przeniesiona z identy-


fikatora IDC_OKIENKO do szWindowClass. Wracamy do wypełniania pól struktury okna.
wcex.lpszClassName = szWindowClass;

Ostatnie ustawione pole służy do określania wyglądu małej ikony w lewym górnym rogu
okna. Znowu korzystamy z funkcji LoadIcon. Zauważ, że teraz jako pierwszy argument
podajemy instancję aplikacji zapisaną w polu hInstance klasy okna.
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

Po inicjalizacji składowych klasy należy ją zarejestrować funkcją RegisterClassEx().


return RegisterClassEx(&wcex);

Wartość zwracana przez funkcję RegisterClassEx() jest identyfikatorem zarejestrowa-


nej klasy okna. Jeśli rejestracja się nie uda, zwracana jest wartość zerowa. Zauważ, że
MyRegisterClass() zwraca tę wartość do _tWinMain(), choć później nie jest ona nigdzie
wykorzystywana. To już koniec funkcji rejestrującej klasę. W następnym przykładzie
będziemy tworzyć okno na bazie tej klasy. Jako wynik tego przykładu możesz skompi-
lować projekt z różnymi dozwolonymi wartościami pól hIcon, hCursor i hbrBackground
i zobaczyć, jak zmienia się wygląd okna. Zapisz projekt, ponieważ będziemy się do
niego odwoływać w kolejnych przykładach.

Tworzymy nowe okno


Mamy już zarejestrowaną klasę okna, czas na jego utworzenie. Okno tworzymy za pomo-
cą funkcji CreateWindow(). Funkcja oprócz klasy okna przyjmuje sporo parametrów,
jednym z nich są tak zwane style okna. Są to stałe (makra) zaczynające się literami
WS (window style). Dokumentacja Windows podaje ich 27; ja tu pokażę takie, które
powodują „standardowy” wygląd okna. Są one na tyle często stosowane razem, że
wprowadzono identyfikator, który określa je łącznie. I tak mamy:
WS_OVERLAPPED — okno ma ramkę i pasek tytułowy.
WS_CAPTION — okno ma pasek tytułowy.
Rozdział 6. ♦ Interface win32, główne okno aplikacji 119

WS_SYSMENU — okno w prawym górnym rogu ma przycisk do zamknięcia.


Przyciski do maksymalizacji i ukrycia w pasku wymagają podania dwóch
dodatkowych stylów: WS_MAXIMIZEBOX i WS_MINIMIZEBOX.
WS_THICKFRAME — możliwa jest zmiana wymiaru okna przez przesuwanie
ramki myszą.

Style mogą być stosowane łącznie. Suma wszystkich powyższych stylów jest zawarta
w stylu WS_OVERLAPPEDWINDOW.

Pozostałe parametry funkcji CreateWindow() wyjaśnię na przykładzie.

Przykład 6.3

Na podstawie zarejestrowanej klasy utwórz nowe okno.

Rozwiązanie
Otwórz projekt z poprzedniego przykładu. Tworzenie okna mogłoby się odbyć
w _tWinMain(), jednak znowu ze względu na przejrzystość programu zostało przeniesione
do oddzielnej funkcji o nazwie InitInstance().

Funkcja ta tworzy okno i je wyświetla. Tak więc w _tWinMain() mamy wywołanie:


if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}

Zauważ, że jeśli funkcja InitInstance() zwróci wartość zerową, to poprzez negację zo-
stanie ona zamieniona na jedność i konsekwencją będzie wyjście z _tWinMain() z warto-
ścią FALSE. Mówiąc wprost, jeśli okna nie uda się utworzyć, to aplikacja zakończy się,
a do systemu zostanie zwrócona wartość FALSE.

Przejdźmy do InitInstance(). Jak widzisz, funkcja ma dwa parametry: pierwszym


jest uchwyt do wystąpienia aplikacji, a drugim zmienna określająca sposób wyświetlania
okna. W przypadku okna głównego oba te parametry są otrzymywane przez _tWinMain()
z systemu. Porównaj listę parametrów głównej funkcji opisaną w tym rozdziale. Możesz
zobaczyć, że przekazane są właśnie zmienne z listy parametrów _tWinMain(). W pierwszej
linii InitInstance() mamy deklarację uchwytu do okna, które za chwilę utworzymy.
HWND hWnd;

Następnie globalna zmienna, w której będzie przechowywany uchwyt do wystąpienia


aplikacji, jest wypełniana tym właśnie uchwytem. Komentarz, który przytaczam, w ory-
ginale głosi: „Uchwyt do wystąpienia aplikacji zapisujemy w zmiennej globalnej”.
hInst = hInstance; // Store instance handle in our global variable

Przyszedł czas na utworzenie okna. Zajmuje się tym wspomniana już CreateWindow().
Listę parametrów prześledzimy na tym konkretnym przykładzie:
120 Microsoft Visual C++ 2012. Praktyczne przykłady

hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,


CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

Pierwszy parametr to nazwa klasy okna; w naszym przypadku użyta została globalna
zmienna szWindowClass. W jaki sposób ta zmienna weszła w posiadanie nazwy klasy
okna, pisałem już przy wypełnianiu pola pszClassName tej klasy. Druga zmienna to
nazwa aplikacji, skopiowana ze skryptu zasobów w identyczny sposób jak nazwa klasy.
Następne pięć parametrów reguluje wielkość i styl okna. Styl okna, o którym już pisałem,
został ustawiony na WS_OVERLAPPEDWINDOW.

Następne dwa parametry stanowią współrzędne lewego górnego rogu okna. Wartość
CW_USEDEFAULT, podana jako x okna, powoduje umieszczenie go w domyślnej pozycji,
wartość y jest ignorowana. Podobnie jest z wymiarami okna. Mamy tu dwa parametry:
długość i szerokość, ale podanie CW_USEDEFAULT jako pierwszego powoduje przyjęcie
domyślnych wartości obydwu z nich. Dalej mamy uchwyt do okna nadrzędnego (rodzica).
Ponieważ jest to pierwsze okno aplikacji, podajmy NULL. Kolejny parametr ma dwa zna-
czenia: dla okna posiadającego pasek i ramkę (tak jak nasze główne okno) oznacza
menu stowarzyszone z oknem, natomiast dla okna potomnego (ang. child window)
oznacza uchwyt do tego okna. Ostatni to dodatkowy parametr, którego na razie nie
opisuję, aby nie komplikować wyjaśnień; tu ma wartość NULL.

Następna linia sprawdza, czy okno zostało utworzone. Jeśli uchwyt jest pusty, funkcja
kończy działanie. W przypadku sukcesu należy jeszcze okno wyświetlić. Odpowiada
za to funkcja ShowWindow().
ShowWindow(hWnd, nCmdShow);

Ma ona dwa parametry: pierwszy to uchwyt okna do wyświetlenia, drugi to cechy wy-
świetlanego okna, które już trochę omawialiśmy. W przypadku okna głównego wartość
tego parametru otrzymuje funkcja tWinMain(). W przypadku innych okien cechy te
jeszcze będziemy omawiać. Po wyświetleniu okna od razu wywołujemy funkcję, która
odświeży jego zawartość, jeśli jest taka potrzeba.
UpdateWindow(hWnd);

Jeśli szczęśliwie przeszliśmy przez te etapy, to funkcja InitInstance() kończy swoje


działanie pozytywnie, zwracając wartość TRUE.
return TRUE;

I to już koniec tworzenia okna; niewiele dzieli już nas od funkcjonującej aplikacji win32.
Zapisz projekt, ponieważ jeszcze się przyda.

Procedura okna
Okno powinno jakoś przetwarzać nadsyłane do niego komunikaty. Będzie się to odby-
wało w specjalnej funkcji zwanej procedurą okna. Jej nazwę już zdefiniowaliśmy w polu
lpfnWndProc klasy okna. Nazwa może być dowolna, natomiast typ i lista parametrów
tej funkcji są z góry określone.
Rozdział 6. ♦ Interface win32, główne okno aplikacji 121

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

Pierwszy parametr pewnie rozpoznajesz — to uchwyt do okna. Pozostałe parametry


określają komunikat oraz dane, jakie on niesie. Sam rodzaj komunikatu jest ukryty
w zmiennej message. Komunikat może, ale nie musi przenosić dane. Na przykład komuni-
kat o kliknięciu myszą zawiera współrzędne tego kliknięcia. Dane te są przechowywane
w zmiennych wParam i lParam. Rodzaje komunikatów zostawmy sobie na następny rozdział.

Przykład 6.4

Prześledź strukturę procedury okna w domyślnym projekcie aplikacji win32.

Rozwiązanie
Otwórz projekt aplikacji win32 z poprzedniego przykładu. Poszukaj funkcji WndProc()
— jest przy końcu pliku z kodem źródłowym aplikacji.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;

switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO Add any drawing code here...
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
122 Microsoft Visual C++ 2012. Praktyczne przykłady

Po zadeklarowaniu kilku zmiennych, których funkcje opiszę później, mamy sedno


funkcji, czyli obsługę komunikatów. Jest to instrukcja switch(), która wykonuje odpo-
wiednie działania w zależności od komunikatu otrzymanego w zmiennej message. Ta
funkcja obsługuje trzy komunikaty: WM_COMMAND, WM_PAINT i WM_DESTROY. Komunikaty
zaczynają się literami WM (Windows Message). WM_COMMAND jest wysyłany w różnych sy-
tuacjach związanych z działaniem kontrolek. W tym przypadku wysyła go menu w mo-
mencie wybrania opcji. Zauważ, że procedura obsługi tego komunikatu zawiera zagnież-
dżoną instrukcję switch, w której zmienną sterującą jest wParam. Jak się można domyślić,
wParam zawiera identyfikator wybranej opcji menu. Komunikat WM_PAINT sygnalizuje
konieczność odświeżania okna. Użyjemy go na przykład do wypisania tekstu w oknie lub
umieszczenia rysunku. Wreszcie, WM_DESTROY oznacza, że okno za chwilę zostanie znisz-
czone i trzeba na przykład zwrócić jakąś wartość do systemu po zakończeniu działania
aplikacji.

Istnieje wiele rodzajów komunikatów. Aby nie było niespodzianki, stosuje się opcję
default, która zadziała w przypadku otrzymania niespodziewanego identyfikatora. Powo-
duje ona uruchomienie domyślnej funkcji okna, która przetwarza komunikaty zapew-
niające takie opcje jak maksymalizacja, zwijanie i inne systemowe zachowania okna.

Wiesz już trochę o tym, w jaki sposób działa procedura okna i jak obsługuje się pod-
stawowe komunikaty. Żeby jednak docierały one do okna, musimy napisać tak zwaną
pętlę komunikatów.

Pętla komunikatów
Pętla komunikatów w najprostszej postaci składa się z trzech funkcji wykonywanych
w pętli:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

Na początku GetMessage() pobiera komunikat przeznaczony dla naszej aplikacji z sys-


temowej kolejki. Następnie TranslateMessage() wykonuje pewne wewnętrzne prze-
twarzanie, na razie dla nas nieistotne. Wreszcie DispatchMessage() rozsyła komunikaty
do okien. W projekcie win32 tworzonym przez kompilator ta pętla wygląda tak:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
Rozdział 6. ♦ Interface win32, główne okno aplikacji 123

Dodatkowa funkcja TranslateAccelerator() umożliwia przetwarzanie kombinacji klawiszy


skrótów menu. Jeśli chcemy, aby naciśnięcie jakiejś kombinacji klawiszy miało skutek
taki jak wybór opcji w menu, to jednym ze sposobów jest zamiana komunikatu odpo-
wiadającego tej kombinacji na komunikat oznaczający wybór opcji menu. To właśnie
realizuje TranslateAccelerator(). Parametry tej funkcji to uchwyt do okna, którego
komunikaty przetwarzamy, tabela klawiszy dla poszczególnych opcji menu i komunikat
z kolejki przechwycony funkcją GetMessage(). Więcej o komunikatach w następnym
rozdziale.

Dotarliśmy do końca opisu najprostszej aplikacji win32. Taka aplikacja nie robi nic oprócz
wyświetlania okna, jednak jest podstawą do konkretnych aplikacji, a zrozumienie, jak
została napisana, jest niezbędne do dalszego poznawania WinAPI.

Pozostał jeszcze do opisania plik zasobów, zawierający opis ikon, strukturę menu, nazwę
aplikacji i inne parametry. Opiszę kolejno zawartość tego pliku.

Zasoby ikon
Plik zasobów to plik tekstowy. Nie zawiera on samych zasobów, jest to raczej spis
treści. Przyporządkowuje na przykład nazwy ikonom. Na te nazwy powołujemy się
później w programie. Skrypt ma nazwę taką samą jak projekt i rozszerzenie .rc; znaj-
dziesz go w folderze projektu. Ponieważ mój projekt nazywa się okienko, plik zasobów
będzie miał nazwę okienko.rc. Struktura tego pliku jest taka, że identyfikatorom przy-
pisuje się ikony, opcje menu, klawisze skrótów i inne. Identyfikatory te muszą być wcze-
śniej zdefiniowane za pomocą dyrektywy #define. Są to więc makra. Ikonę oznaczoną
IDI_SMALL można zdefiniować tak:
#define IDI_SMALL 108
IDI_SMALL ICON "small.ico"

Dyrektywy #define są zapisane w oddzielnym pliku resource.h, załączonym do skryptu


jako nagłówek. W podobny sposób definiujemy menu i okna dialogowe, z tym że sa-
ma definicja może być bardziej rozbudowana, na przykład w przypadku okien dialo-
gowych. Zmiany w pliku możemy wprowadzać na dwa sposoby. Po pierwsze, można go
otworzyć w edytorze tekstów w stylu Notatnika i tam edytować bezpośrednio. Drugim
sposobem jest posłużenie się specjalnym edytorem, który jest częścią Visual C++.
Niestety, w wersji Express Edition brak jest edytora zasobów. Ponieważ w tej książce
korzystam z bety wersji Professional VC++ 2012, edytor jest dostępny. Jednak jeśli
będziesz przy czytaniu korzystał z wersji Express, to edytora zasobów może nie być.
W przykładach opisałem więc oba sposoby edycji pliku: w Notatniku, tak jakby edytora
nie było, a późnej za pomocą edytora.

Przykład 6.5

Dołącz ikonę do aplikacji, korzystając ze skryptu zasobów.


124 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Na razie nie będziemy korzystać z VC++ 2012, choć będzie potrzebny projekt z poprzed-
niego przykładu. Moja aplikacja nazywa się okienko, stąd skrypt zasobów nazywa się
okienko.rc. Jeśli nazwałeś swoją aplikację inaczej, to Twój skrypt nazywa się jak aplikacja
i ma rozszerzenie .rc. Będziemy potrzebować dodatkowo pliku .ico z dowolną ikoną.
Taki plik możesz utworzyć na przykład za pomocą darmowego programu GIMP, który
można pobrać z Internetu. Ikonę rysuje się jak każdy rysunek w GIMP-ie. Ja nie jestem
grafikiem i nie mam dużego doświadczenia w tym programie. Po uruchomieniu GIMP-a
rysowanie swojej ikony rozpocząłem od utworzenia nowego pliku graficznego z menu
Plik/Nowy. Jako wymiary obrazu podałem 48×48. Na obrazku narysuj, co tylko chcesz; ja
napisałem literę A (jak aplikacja). Następnie wybrałem opcje Plik/Zapisz jako. Pojawiło
się okno wyboru miejsca i nazwy zapisywanego pliku, jak na rysunku 6.1.

Rysunek 6.1.
Okno zapisu obrazu
w programie GIMP

W pole Nazwa wpisałem mojaikona.ico. Rozwinąłem opcję Przeglądaj inne katalogi.


Wybrałem katalog projektu — ten, w którym znajduje się plik okienko.rc — i kliknąłem
Zapisz. W oknie Zapisz jako ikonę Windows zaakceptowałem ustawienia domyślne,
klikając Zapisz. Plik z ikoną jest już gotowy. Pozostaje uwzględnić go w skrypcie zasobów,
a następnie w aplikacji. Zacznij od otwarcia w Notatniku pliku resource.h, który jest
w tym samym folderze co skrypt zasobów okienko.rc. W plik nagłówkowym resource.h
trzeba zdefiniować makro dla ikony. Takie makra zaczynają się w VC++ 2012 litera-
mi IDI. W tej chwili są dwa: IDI_OKIENKO i IDI_SMALL. Możesz wpisać następne makro
w dowolnym miejscu pliku, ale może dla porządku dodaj nową linię po linii:
#define IDI_SMALL 108

Do makra musisz przypisać liczbę; znowu może być w zasadzie dowolna, pod warunkiem
że nie będzie się powtarzała w tym pliku. W programowaniu warto zachować systema-
tyczność, aby się nie pogubić. Popatrz na liczby z prawej strony wszystkich makr i zo-
bacz, jaka jest największa, ale niższa niż 200. W standardowym projekcie utworzonym
przez środowisko ostatnia powinna być liczba 130. Następna jest liczba 1000, więc mamy
sporo do zagospodarowania. Naszej ikonie przyporządkujemy makro o wartości 131.
W nowej linii, którą utworzyłeś, wpisz:
#define IDI_MOJAIKONA 131

Zapisz i zamknij resource.h i otwórz plik okienko.rc. Odnajdź w pliku część Icon.
/////////////////////////////////////////////////////////////////////////////
//
// Icon
Rozdział 6. ♦ Interface win32, główne okno aplikacji 125

//

// Icon with lowest ID value placed first to ensure application icon


// remains consistent on all systems.

IDI_OKIENKO ICON "okienko.ico"


IDI_SMALL ICON "small.ico"

Dodaj linię:
IDI_MOJAIKONA ICON "mojaikona.ico"

Teraz dopiero otwórz projekt aplikacji w VC++ 2012. W funkcji rejestracji klasy okna My-
RegisterClass() odnajdź linię:
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

i w drugim parametrze funkcji LoadIcon() wpisz identyfikator naszej ikony:


wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_MOJAIKONA));

Uruchom program i przypatrz się paskowi tytułowemu okna. Obok nazwy znajdziesz
swoją ikonę. U mnie wygląda to tak jak na rysunku 6.2.

Rysunek 6.2.
Pasek okna aplikacji
z własną ikoną

W następnym przykładzie zmienimy ikonę, używając edytora zasobów VC++ 2012. Jest
to o wiele łatwiejsze.

Przykład 6.6

Zmień ikonę w aplikacji, używając edytora zasobów VC++ 2012.

Rozwiązanie
Zacznij od utworzenia zupełnie nowego projektu VC++ 2012 według przykładu 1.3,
ponieważ nie chcemy, aby była tam ikona dodana w poprzednim przykładzie. Po
utworzeniu projektu w lewym panelu mamy widok RESOURCE VIEW. Jeśli jest inny
widok, należy go przełączyć zakładkami na dole okna, jak na rysunku 6.3. Zakładkę
RESOURCE VIEW zaznaczyłem strzałką.

Rysunek 6.3.
Zakładki w lewym
panelu projektu VC++
2012
126 Microsoft Visual C++ 2012. Praktyczne przykłady

Jeśli coś przestawiłeś z oknami i nie widzisz lewego panelu jak na rysunku 6.3, to wy-
bierz z głównego menu opcje View/Resource View.

Teraz, klikając na trójkąty po lewej stronie gałęzi, trzeba dotrzeć do zasobów ikon, jak
pokazuje rysunek 6.4.

Rysunek 6.4.
Drzewo edytora
zasobów VC++ 2012

Masz tu te same identyfikatory ikon co w pliku okienko.rc. Nie jest to dziwne, bo jest to
właśnie ten plik przedstawiony w formie drzewa. Kliknij prawym przyciskiem myszy na
słowie Icon i wybierz Insert Icon. W drzewie pojawi się nowa ikona, nazwana IDI_ICON1.
W środkowym panelu zobaczysz edytor ikon z przykładową ikoną. Nie ma więc potrzeby
używania programu GIMP. Ogólny widok edytora przedstawia rysunek 6.5.

Rysunek 6.5.
Edytor ikon

Pośrodku jest rysowana ikona. Z lewej strony masz wybór rozmiarów i głębi kolorów
ikon, z prawej dostępne kolory, a nad nimi narzędzia rysowania. W panelu z lewej widzisz
kilka możliwych rozmiarów ikon.

Jeśli zostawisz wszystko tak jak jest, to do jednego pliku .ico zostaną zapisane wszystkie
te typy ikon. Gdy powołamy się później na tę ikonę w klasie okna, zostanie wyświe-
tlona pierwsza ikona w pliku. Ja chciałem mieć jedną ikonę, więc usunąłem resztę.
Rozdział 6. ♦ Interface win32, główne okno aplikacji 127

Usuwanie polega na kliknięciu na typ pliku w lewym panelu i wybraniu opcji Delete
image type. Zostawiłem tylko typ 32x32, 4 bit, BMP. Narzędzia rysownicze zazna-
czyłem na rysunku 6.5 ramką, podobne występują w wielu programach graficznych.
Po najechaniu kursorem na narzędzie pojawi się podpowiedź. Ja rysowanie swojej
ikony zacząłem od wybrania Erase Tool (gumki), następnie wymazałem punkty ikony,
tak że stały się białe. W palecie kolorów kliknąłem na seledyn, a z paska narzędzi wybra-
łem Text Tool. Pojawiło się okno wprowadzania tekstu, więc wpisałem A. Było trochę za
małe, więc kliknąłem przycisk Font... ponad wpisaną literą. Wybrałem czcionkę Times
New Roman i rozmiar 20. Następnie zatwierdziłem, klikając OK. Litera w ikonie powięk-
szyła się i zmieniła krój. Ikonę zapisujemy, wybierając z menu File/Save icon1.ico.

Zmienimy nazwę ikony na IDI_MOJAIKONA, tak jak w poprzednim przykładzie. Aby to


zrobić, kliknij prawym przyciskiem nazwę IDI_ICON1 w drzewie zasobów. Wybierz
opcję Properties. Z prawej strony pojawi się panel Properties. Znajdź właściwość ID
i zamiast IDI_ICON1 wpisz IDI_MOJAIKONA. Pozostało tylko przyporządkować ją do
klasy okna. Zrobimy to identycznie jak w poprzednim przykładzie. W prawym panelu
na dole kliknij zakładkę Solution Explorer, porównaj rysunek 6.3. Jeśli nie widzisz zakła-
dek, to z menu głównego wybierz View/Solution Explorer. Następnie w drzewie pro-
jektu kliknij podwójnie plik okienko.cpp znajdujący się w gałęzi Source Files, jak na
rysunku 6.6.

Rysunek 6.6.
Struktura projektu
VC++ 2012

Pojawi się kod programu. W funkcji MyRegisterClass() znajdź linię:


wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

i zamień na:
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_MOJAIKONA));
128 Microsoft Visual C++ 2012. Praktyczne przykłady

Jeżeli ikona była taka sama, to wynik będzie identyczny jak w przykładzie z bezpo-
średnią edycją skryptu zasobów.

W następnym punkcie nauczysz się budować zasoby menu.

Zasoby menu
Po bloku z ikonami mamy blok opisujący menu. Jest to definicja menu aplikacji, więc
struktura jest już bardziej skomplikowana.
/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

IDC_OKIENKO MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "E&xit", IDM_EXIT
END
POPUP "&Help"
BEGIN
MENUITEM "&About ...", IDM_ABOUT
END
END

Opis każdego panelu menu rozpoczyna się słowem kluczowym POPUP i nazwą panelu
wyświetlaną w pasku menu, następnie mamy definicje opcji w panelu zamknięte mię-
dzy słowami BEGIN i END. Każda opcja rozpoczyna się słowem MENUITEM, po którym
następuje jej nazwa, a następnie identyfikator zdefiniowany podobnie jak w przypad-
ku ikony w pliku resource.h. Znak & przy literze w opcji oznacza, że będzie można ją
wywołać za pomocą klawisza skrótu Alt+litera po znaku &. Definiowanie klawiszy
skrótów w menu jest więc bardzo wygodne, bo wymaga tylko dopisania jednego znaku.
Można też definiować klawisze skrótów dla innych funkcji programu.

Przykład 6.7

Wykonaj w aplikacji menu o strukturze:


Panel Plik
Opcje: Zapisz, Otwórz, Wyjście
Panel: Edycja
Opcje: Wytnij, Kopiuj, Wklej
Rozdział 6. ♦ Interface win32, główne okno aplikacji 129

Rozwiązanie
Utwórz aplikację WinAPI według przykładu 1.3. Zakładam, że aplikacja nazywa się
okienko. Zaprojektujemy menu poprzez bezpośrednią edycję skryptu zasobów. Za pomocą
Notatnika otwórz plik okienko.rc z katalogu projektu, który przed chwilą utworzyłeś.
Znajdź sekcję menu rozpoczynającą się od:
/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

Jest już tam zapisane menu dodawane przez środowisko. Aby uzyskać strukturę opcji,
o jaką nam chodzi, trzeba je zmienić następująco:
IDC_OKIENKO MENU
BEGIN
POPUP "&Plik"
BEGIN
MENUITEM "Zapisz" IDM_ZAPISZ
MENUITEM "Otwórz" IDM_OTWORZ
MENUITEM "Wyjście", IDM_EXIT
END
POPUP "&Edycja"
BEGIN
MENUITEM "Wytnij", IDM_WYTNIJ
MENUITEM "Kopiuj" IDM_KOPIUJ
MENUITEM "Wklej" IDM_WKLEJ
END
END

W menu przybyło identyfikatorów (makr), zatem nowe trzeba zdefiniować w pliku


resource.h. Otwórz ten plik w Notatniku; pamiętaj o zapisaniu pliku okienko.rc. Najlepiej
otwórz plik resource.h, uruchamiając drugą kopię (instancję) Notatnika. Przy okazji
widzisz w praktyce, co oznacza instancja programu. W pliku resource.h znajdź linię:
#define IDM_EXIT 105

Tu kończą się istniejące definicje makr związanych z menu, więc to dobre miejsce na
wpisanie nowych. Sprawdź, jaki identyfikator liczbowy poniżej 200 jest ostatni; u mnie
jest to 130. Kolejnym makrom przypiszemy identyfikatory od 131. Po linii definiującej
IDM_EXIT wpisz:
#define IDM_ZAPISZ 131
#define IDM_OTWORZ 132
#define IDM_WYTNIJ 133
#define IDM_KOPIUJ 134
#define IDM_WKLEJ 135

Zapisz i zamknij pliki w obydwu instancjach Notatnika. Menu już jest gotowe, mo-
żesz kompilować i uruchamiać program. Powinieneś otrzymać okno główne z menu,
jak na rysunku 6.7.
130 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 6.7.
Gotowe menu
w aplikacji

Zauważ, że definiując panel Plik, poprzedziłeś literę P znakiem &.


POPUP "&Plik"

Jeśli przy działającym programie naciśniesz Alt+P, to otworzy się ten panel menu.

Teraz utworzymy takie samo menu przy użyciu edytora zasobów.

Przykład 6.8

Wykonaj w aplikacji menu o strukturze jak w poprzednim przykładzie przy użyciu


edytora zasobów.

Rozwiązanie
Utwórz aplikację WinAPI według przykładu 1.3. Zakładam, że projekt nazywa się okienko1.
W lewym panelu przejdź do zakładki RESOURCE VIEW. Otwórz drzewo menu, jak na
rysunku 6.8.

Rysunek 6.8.
Gałąź menu w drzewie
zasobów aplikacji

Kliknij podwójnie identyfikator IDC_OKIENKO1. W widoku okna budowanej aplikacji


zobaczysz menu, jak pokazuje rysunek 6.9.

Rysunek 6.9.
Menu budowane
w edytorze zasobów
Rozdział 6. ♦ Interface win32, główne okno aplikacji 131

Kliknij na nazwę File w pasku budowanego menu. Pojawi się kursor. Zmień nazwę na
&Plik, jak na rysunku 6.10.

Rysunek 6.10.
Edycja menu

Teraz kliknij Exit w rozwiniętym panelu pod opcją Plik. Słowo Exit zastąp słowem
Zapisz. Kliknij słowa Type here pod słowem Zapisz i wpisz Otwórz, jak na rysunku 6.11.

Rysunek 6.11.
Dodawanie opcji menu

Kliknij Type here poniżej i wpisz Wyjście. Pierwszy panel już gotowy, wracamy do paska
menu. Kliknij Help i wpisz &Edycja. Następnie w panelu pod opcją Edycja zamień
About na Wytnij i dopisz opcję Kopiuj i Wklej, tak samo jak to zrobiłeś w panelu Plik.

Skompiluj program. Powinieneś otrzymać takie samo menu jak w poprzednim przy-
kładzie.

Wiesz już, jak tworzyć w pliku zasobów ikony i menu, czas na okna dialogowe. Opis okien
jest najbardziej skomplikowany z tego, co do tej pory robiliśmy, ale myślę, że i tu sobie
poradzimy.

Okna dialogowe w zasobach


Okna dialogowe mogą być definiowane w zasobach lub w samym programie. Tutaj
zajmiemy się definicją w zasobach. Kluczem do opisu okien jest słowo DIALOGEX.

Definicja zasobu okna ma sporo parametrów, bo i możliwości wyglądu i zachowania


okna jest wiele. Ogólny prototyp wygląda jak niżej:
nazwaID DIALOGEX x, y, szerokość, wysokość, STYLE zawartość_sekcji, CAPTION
zawartość_sekcji, FONT zawartość_sekcji
BEGIN
zawartość wnętrza okna (kontrolki)
END
132 Microsoft Visual C++ 2012. Praktyczne przykłady

Pod etykietą nazwaID kryje się nazwa makra identyfikującego okno dialogowe, na to
makro powołamy się przy jego wyświetlaniu. Liczby całkowite x i y to współrzędne
lewego górnego rogu okna; współrzędne liczy się od lewego górnego rogu okna nad-
rzędnego. Wymiary okna w punktach (pikselach) określają szerokość i wysokość. Dalej
mamy sekcję STYLE, czyli styl okna. Style okna dialogowego mają trochę wspólnego ze
stylami okna, o których już pisałem przy okazji budowy głównego okna aplikacji. Niektóre
z tamtych stylów, zaczynających się literami WS_, można zastosować i tutaj. Etykiety
stylów specyficznych dla okna dialogowego zaczynają się literami DS_ (dialog style).

W projekcie WinAPI tworzonym przez kompilator jest jedno okno dialogowe About.
Ma ono kombinacje stylów:
DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU

Ze stylami WS_CAPTION i WS_SYSMENU już się spotkałeś. Zobaczmy, co oznaczają pozostałe


wartości:
 DS_SETFONT — możliwa jest zmiana kroju czcionki w oknie.
 DS_MODALFRAME — przygotowuje okno do bycia oknem modalnym, czyli
wymagającym zamknięcia przed kontynuowaniem aplikacji. Dzięki temu
stylowi można użyć stylów WS_CAPTION i WS_SYSMENU, które dostarczą
przycisków do takiego zamknięcia.
 DS_FIXEDSYS — czcionka w oknie kompatybilna ze starszymi wersjami
Windows.

Wracamy do definicji okna. Po definicji stylu jest sekcja CAPTION; napis po słowie
CAPTION będzie wyświetlony w górnym pasku okna. Sekcja FONT w najprostszym przypad-
ku zawiera rozmiar czcionki i jej nazwę. Ponieważ, jak już pisałem, parametrów jest dużo,
zdecydowałem się przedstawić najprostsze formy, tak żebyś w miarę łatwo poznał
podstawy.

Pomiędzy BEGIN i END mamy definicje kontrolek w oknie. W oknie można wstawić do-
wolną kontrolkę. Ja pokażę użycie trzech: ikony, tekstu i przycisku. Na razie będzie-
my w nich wyświetlać zawartość statyczną, to znaczy niezmienną w czasie działania
programu.

Ikonę w oknie dialogowym zapisujemy następująco:


ICON IkonaID,KontrolkaID, x,y,szerokość, wysokość

IkonaID to identyfikator ikony, ten sam, który omawiałem już w dziale o ikonach.
KontrolkaID to z kolei identyfikator kontrolki. Zauważ, że osadzamy istniejącą ikonę
w nowej kontrolce. Właśnie pierwsze dwa parametry to wcześniej utworzony identyfi-
kator ikony i identyfikator, który będzie miała tworzona kontrolka.

W przypadku obiektów statycznych pod parametr KontrolkaID podstawiamy IDC_STATIC.


Następne cztery parametry to współrzędne lewego górnego rogu ikony oraz jej szero-
kość i wysokość. Szerokość i wysokość można ustawić na zero, bo kompilator i tak je
zignoruje. Ikona wyświetlana jest w swoich oryginalnych wymiarach.
Rozdział 6. ♦ Interface win32, główne okno aplikacji 133

Tyle o ikonie. Kontrolkę tekstową w oknie dialogowym definiujemy następująco:


LTEXT Tekst, KontrolkaID,x,y,szerokość, wysokość

Jak widać, konwencja jest tu podobna. Jedyną różnicą jest to, że zamiast ikony mamy
tekst to wypisania.

Przyda się jeszcze definicja przycisku. Przycisk w oknie może mieć status domyślnego,
wtedy jest aktywny po wyświetleniu okna i aby go wcisnąć, można po prostu nacisnąć
Enter. Do utworzenia przycisku domyślnego służy:
DEFPUSHBUTTON Napis, KontrolkaID,x,y,szerokość, wysokość

Natomiast przycisk bez statusu domyślnego definiujemy tak:


PUSHBUTTON Napis, KontrolkaID,x,y,szerokość, wysokość

Oba wyrażenia są identyczne pod względem liczby parametrów, a ich filozofia nie różni
się od definicji etykiety tekstowej. Parametr Napis jest tekstem do wyświetlania na
przycisku. W przycisku ważny jest identyfikator KontrolkaID; będziemy z niego korzy-
stać przy obsłudze naciśnięcia.

Po zdefiniowaniu okna należy je wyświetlić. Najłatwiej wykonać to za pomocą funkcji:


DialogBox(hInst, MAKEINTRESOURCE(IDD_RESOURCE), hWnd, FunDial);

Parametry tej funkcji to kolejno: uchwyt do instancji aplikacji, w której zasobach zde-
finiowane jest okno, identyfikator tego okna, uchwyt do okna, które będzie oknem
nadrzędnym, i wskaźnik do funkcji obsługi okna dialogowego (procedury obsługi).

Jako przykłady wykonamy okno dialogowe, jak zwykle na dwa sposoby, czyli z edyto-
rem i bez edytora zasobów.

Przykład 6.9

Zaprojektuj i zaprogramuj bez edytora zasobów okno dialogowe zawierające pytanie:


„Czy zakończyć działanie aplikacji?” i dwa przyciski Tak i Nie.

Rozwiązanie
Zabierając się do pracy nad oknem bez graficznego edytora, warto narysować je na kartce.
Ja rozplanowałem elementy okna (kontrolki) jak na rysunku 6.12. Podane są tam
współrzędne lewych górnych rogów i wymiary kontrolek. W górnym rzędzie mamy
ikonę i etykietę z tekstem, a w dolnym dwa przyciski.

Rysunek 6.12.
Plan okna dialogowego
134 Microsoft Visual C++ 2012. Praktyczne przykłady

Teraz, według przykładu 1.3, tworzymy nową aplikację WinAPI, którą nazwałem
okienko2. W folderze tej aplikacji otwórz za pomocą Notatnika plik okienko2.rc.
Znajdź sekcję opisującą okno dialogowe About.
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

Sekcja ta kończy się słowem END po bloku definicji kontrolek. W następnych liniach
zdefiniujemy nasze okno. W celu zachowania przejrzystości opatrzyłem je takim samym
komentarzem z nazwą okna.
/////////////////////////////////////////////////////////////////////////////
//
// Mój Dialog
//

Teraz definicja. Najpierw ogólny opis okna DIALOGEX:


IDD_MOJDIALOG DIALOGEX 0,0,200,100
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Pytanie o wyjście"
FONT 8, "MS Shell Dlg"

Następnie opis kontrolek — porównaj współrzędne z rysunkiem 6.12:


BEGIN
ICON IDI_SMALL, IDC_STATIC, 15,25, 0,0
LTEXT "Czy zakończyć działanie aplikacji?",IDC_STATIC, 45,25,110,8PUSHBUTTON
"Tak",IDTAK,15,75,50,20
DEFPUSHBUTTON "Nie",IDNIE,115,75,50,20
END

Kontrolki ICON i LTEXT mają identyfikatory IDC_STATIC zdefiniowane standardowo.


Przyciski natomiast muszą mieć własne identyfikatory, bo będziemy się na nie powoły-
wać. Są to makra, które nazwałem IDTAK i IDNIE. Trzeba je zapisać w pliku resource.h
umieszczonym też w folderze projektu.

W tym samym pliku trzeba też zdefiniować makro IDD_MOJDIALOG identyfikujące całe
okno. Otwórz więc ten plik Notatnikiem. Można zapisać nowe makra w dowolnym
miejscu pliku, ale ja odnalazłem linię definiującą okno About:
#define IDD_ABOUTBOX 103 //ta linia istnieje

i zapisałem nowe definicje w następnych liniach, pamiętając o sprawdzeniu, czy etykiety


133, 134 i 135 nie należą już do jakiegoś makra.
#define IDD_MOJDIALOG 133
#define IDTAK 134
#define IDNIE 135

Okno jest gotowe. Trzeba je jeszcze wywołać. Do tego wykorzystamy funkcję wyświetlają-
cą okno About. Zamknij Notatnik, pamiętając o zapisaniu plików, i wróć do VC++ 2012.
Rozdział 6. ♦ Interface win32, główne okno aplikacji 135

W pliku okienko2.cpp, w funkcji okna głównego WndProc(), znajdź linię:


DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);

Funkcja DialogBox() wyświetla okno zdefiniowane w zasobach. Jest ona wywoływana po


wybraniu z menu aplikacji opcji About.

Wystarczy tylko zamienić identyfikator IDD_ABOUTBOX w drugim parametrze tej funkcji


na IDD_MOJDIALOG i zamiast okna About wyświetli się Twoje okno.
DialogBox(hInst, MAKEINTRESOURCE(IDD_MOJDIALOG), hWnd, About);

Uruchom aplikację, a następnie z menu wybierz opcje Help/About. Zobaczysz okno


według projektu; powinno wyglądać jak na rysunku 6.13.

Rysunek 6.13.
Okno dialogowe
z przykładu

W VC++ 2012 Professional okno dialogowe można też wykonać za pomocą edytora
zasobów, czyli zaprojektować wizualnie, graficznie.

Przykład 6.10

Utwórz okno dialogowe jak w poprzednim przykładzie za pomocą edytora zasobów.

Rozwiązanie
Po raz kolejny będzie potrzebna aplikacja WinAPI według przykładu 1.3. Nazwałem
ją okienko3. Kliknij zakładkę RESOURCE VIEW w lewym panelu i rozwiń drzewo
edytora zasobów tak, aby zobaczyć gałąź Dialog jak na rysunku 6.14.

Rysunek 6.14.
Drzewo zasobów
z gałęzią Dialog
136 Microsoft Visual C++ 2012. Praktyczne przykłady

Kliknij prawym przyciskiem na napis Dialog i z menu kontekstowego wybierz Add


Resource.... Pojawi się okno Add Resource, wyglądające jak na rysunku 6.15.

Rysunek 6.15.
Okno dodawania zasobu

W oknie kliknij New. Utworzy się nowe okno dialogowe z identyfikatorem IDD_DIALOG1.
W prawym (środkowym) panelu zobaczysz to okno. Wygląd graficznego edytora z pu-
stym oknem przedstawia rysunek 6.16.

Rysunek 6.16.
Okno edytora okien
dialogowych

Zaczniemy od zmiany napisu w pasku okna. Przyjrzyj się zakładkom na prawym boku
ekranu, obok zakładki Toolbox. Będzie tam zakładka Properties. Po jej kliknięciu poja-
wi się panel właściwości, jak na rysunku 6.17.

W tym panelu w lewej kolumnie masz parametry i właściwości okna, a w prawej ich
wartości. Znajdź właściwość Caption — jest to napis w pasku okna. Zmień go z Dialog na
Pytanie o wyjście.

Ustawmy wymiary okna 200×100. W tym celu najedź myszką na czarny kwadracik
w środku prawego pionowego boku okna i naciśnij lewy klawisz myszy. W prawym dol-
nym rogu ekranu zobaczysz dwie liczby rozdzielone znakiem × (np. 300×400). Jest to
szerokość i wysokość okna. Przesuwając mysz z wciśniętym klawiszem, zmieniasz wymiar
Rozdział 6. ♦ Interface win32, główne okno aplikacji 137

Rysunek 6.17.
Panel z właściwościami
okna

okna — w przypadku poziomego boku będzie to szerokość. Zmienia się pierwsza liczba
w prawym dolnym rogu. Ustaw ją na 200.

Teraz kliknij myszą na podobny kwadracik na środku dłuższego boku okna. Przytrzymu-
jąc lewy klawisz i przesuwając mysz, ustaw drugą liczbę w prawym dolnym rogu ekranu
na 100. Okno ma wymiary 200 na 100.

Kontrolki do okna wstawimy z zakładki Toolbox przy prawym brzegu ekranu. Kliknij tę
zakładkę, a wysunie się panel z kontrolkami, jak na rysunku 6.18.

Rysunek 6.18.
Fragment panelu
Toolbox z kontrolkami

Ze względu na oszczędność miejsca przedstawiłem tylko część panelu z kontrolkami


Button i Static Text. Odpowiadają one wpisom LTEXT i PUSHBUTTON w skrypcie zasobów.
Kliknij napis Static Text, a następnie kliknij w oknie dialogowym mniej więcej tam,
gdzie chcesz umieścić tekst z pytaniem Czy zakończyć działanie aplikacji?. Następ-
nie kliknij na napis Button w panelu Toolbox i kliknij okno dialogowe mniej więcej
w miejscu przycisku Tak. Ponieważ jeden przycisk już jest w oknie, to mamy komplet
kontrolek, oprócz ikony. Niestety, ikony nie ma wśród kontrolek dostępnych w Toolbox.
Można zamiast niej użyć kontrolki Picture Control, ale jest to inny obiekt, choć o podob-
nych możliwościach. Ja zdecydowałem się nie używać ikony.
138 Microsoft Visual C++ 2012. Praktyczne przykłady

Kliknij napis Static tak, aby otoczyły go kwadraty, jak na rysunku 6.19.

Rysunek 6.19.
Zaznaczanie
komponentu etykiety
myszką

Wciśnij klawisz myszy na obiekcie Static. W dolnym pasku środowiska, ponad paskiem
zadań, zobaczysz teraz dwie pary liczb. Ta bardziej na prawo to znane Ci już wymiary
(tym razem kontrolki), a para trochę po lewej wymiarów to współrzędne lewego gór-
nego rogu kontrolki. Zgodnie z tym, co zaplanowaliśmy, przesuń kontrolkę tekstu tak,
aby miała współrzędne 45,25. Teraz puść klawisz myszy, kontrolka pozostanie zazna-
czona kwadracikami.

Otwórz znany już panel Properties. Będzie on zawierał właściwości wybranej kontrolki.
Poszukaj właściwości Caption zmień Static na Czy zakończyć działanie aplikacji?.
Naciśnij Enter, napis pojawi się w oknie dialogowym. Teraz chwyć myszą przycisk
Button1 i przesuń na pozycję 15,77 (na 15,75 nie da się bez zmiany rozstawu siatki).
Przy zaznaczonym przycisku rozwiń panel Properties i w jego właściwości Caption
wpisz Tak, naciśnij Enter. We właściwość ID wpisz IDTAK i też zatwierdź za pomocą
przycisku Enter. Kliknij przycisk Cancel, który został umieszczony w oknie automa-
tycznie podczas tworzenia projektu. Rozwiń panel Properties, zmień wartość właściwości
Caption z Cancel na Nie, a ID z IDCANCEL na IDNIE.

Nasze okno jest już gotowe; wyświetlimy je identycznie jak w poprzednim przykładzie.
Okno ma nadany automatycznie identyfikator IDD_DIALOG1. Aby wszystko było
jak w poprzednim przykładzie, zmienimy ten identyfikator na IDD_MOJDIALOG.

Możesz to zrobić, klikając identyfikator IDD_DIALOG1 w gałęzi Dialog okna Resource


view. Otwórz teraz zakładkę Properties, jak na rysunku 6.19. Identyfikator jest we
właściwości ID zamiast IDD_DIALOG1. Wpisz IDD_MOJDIALOG i naciśnij Enter. Teraz
wyświetlenie okna niczym nie różni się od poprzedniego przykładu.

W pliku okienko3.cpp, w funkcji okna głównego WndProc(), znajdź linię:


DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);

Zmień identyfikator IDD_ABOUTBOX w drugim parametrze tej funkcji na IDD_MOJDIALOG.


Zamiast okna About wyświetli się Twoje okno.
DialogBox(hInst, MAKEINTRESOURCE(IDD_MOJDIALOG), hWnd, About);

Skompiluj i uruchom program. Wybierz opcje Help/About. Okno powinno być takie
samo jak w poprzednim przykładzie.
Rozdział 7.
Obsługa komunikatów

Komunikaty w aplikacji Windows


Aplikacja w systemie Windows składa się z okien. Jest ich nawet więcej, niż przypusz-
czasz, ponieważ właściwie każda kontrolka umieszczona w oknie też jest oknem. Mamy
więc zbiór obiektów połączonych w hierarchię. Jednak pożytek z takiego zbioru byłby
niewielki, jeśli obiekty nie komunikowałyby się ze sobą. Grupie ludzi trudno jest pra-
cować nad wspólnym projektem bez komunikacji. Tak samo jest z aplikacją. Komuni-
kację między oknami, a także między systemem a daną aplikacją zapewniają komuni-
katy. Jest wiele ich rodzajów, a każdy niesie inną wiadomość i może być specyficzny
dla danego okna.

Ogólnie komunikat jest przenoszony w trzech zmiennych. Pierwsza to zmienna typu UINT,
niosąca typ wiadomości. Zmienna jest typu całkowitego bez znaku i tak naprawdę typ
komunikatu jest liczbą, której przyporządkowano makra tekstowe w celu łatwiejszego
rozpoznawania. Nazwy makr komunikatów zaczynają się od liter WM, co jest skrótem od
Windows Message. Sam typ komunikatu nie zawsze wystarczy, czasem musi on przenosić
jakieś dane. Te dane są przenoszone w dwóch zmiennych: wParam i lParam. Przeno-
szone dane mogą być różne — raz będzie to wybrana opcja menu, kiedy indziej tekst do
wypisania w oknie czy współrzędne klikniętego właśnie punktu. Jak już pisałem w po-
przednim rozdziale, komunikaty dla każdego okna są przetwarzane w specjalnej funkcji
związanej z tym oknem.

W tym rozdziale chcę przedstawić rodzaje komunikatów, zasady ich rozsyłania i przeno-
szone przez nie dane. Trudno jest ustalić, który komunikat jest ważniejszy, a który
mniej ważny, dlatego kolejność prezentowania ustaliłem subiektywnie.
140 Microsoft Visual C++ 2012. Praktyczne przykłady

WinAPI a standard Unicode


Unicode jest standardem kodowania znaków. Każdy znak w standardzie jest oznaczany
literą U+numer znaku. Daje to możliwość zakodowania praktycznie nieograniczonej
liczby znaków. Sam standard Unicode nie ma nic wspólnego z zapisywaniem znaków
w pamięci komputera; jest to tylko tabela znaków z przyporządkowanymi numerami.
Zapis w pamięci może być realizowany różnymi metodami. Najbardziej znane to
UTF-32/UCS-4, UTF-16, UTF-8. Każda litera jest zapisana w zmiennej o długości
dwóch lub więcej bajtów. Oznacza to, że zmienna typu char jest zbyt mała, aby prze-
chowywać literę; zamiast niej musimy użyć zmiennej typu WCHAR. Tak samo łańcuchy
znakowe będą zabierały więcej miejsca, ponieważ reprezentacja każdego znaku będzie co
najmniej dwubajtowa. W celu zachowania zgodności ze standardem ANSI i Unicode
należałoby więc przy każdym wystąpieniu zmiennej łańcucha znaków stosować kom-
pilację warunkową i odpowiednie rodzaje zmiennych. Taki zapis byłby bardzo nie-
praktyczny, ale na szczęście nie jest konieczny. Zamiast char lub WCHAR można użyć
typu TCHAR, który staje się jednym z dwóch wymienionych wcześniej typów, w zależno-
ści od tego, czy kompilujemy w trybie ANSI czy Unicode. Do konwersji stałych łańcu-
chów znakowych służy makro _T(). W przypadku trybu ANSI nie robi ono nic, zaś
w przypadku trybu Unicode dodaje L przed łańcuchem, co oznacza zmienną long,
która ma większą liczbę bajtów, a o to nam chodzi. I tak na przykład zapis:
TCHAR znaki[]=_T("Łańcuch znakowy");

będzie dla trybu ANSI równoważny zapisowi:


char znaki[] = "Łańcuch znakowy";

a dla Unicode:
WCHAR znaki[] = L"Łańcuch znakowy";

Większość funkcji WinAPI operujących na łańcuchach znakowych ma tak naprawdę


dwie wersje: jedną dla Unicode i jedną dla ANSI. Niestety, nie wszystkie są napisane
w ten sposób. Czasem trzeba operować na zmiennych typu char, a następnie konwertować
je do WCHAR.

Jednak podsumowując, w celu zachowania zgodności z obydwoma standardami należy


używać typu TCHAR i pamiętać o stosowaniu do wszystkich stałych znakowych i łańcu-
chowych makra _T().

Przycisk i okno tekstowe,


czyli budujemy warsztat
Aby pokazać na przykładach, jak działają komunikaty, trzeba je jakoś wysyłać i odbierać.
Przygotujemy sobie w tym celu dwie kontrolki: przycisk i okno tekstowe. Przycisk posłuży
do wysyłania komunikatów, a w oknie tekstowym będziemy wyświetlać efekty ich
działania.
Rozdział 7. ♦ Obsługa komunikatów 141

Zarówno przycisk, jak i okno tekstowe są oknami o zdefiniowanych wcześniej klasach.


Klasa przycisku nazywa się button, a klasa okna edycji edit. Możesz pomyśleć, że
skoro są to okna, to ich tworzenie w oknie aplikacji przebiega identycznie jak tworzenie
samego okna. Rzeczywiście tak jest. Ponieważ klasy tych specjalnych okien są już goto-
we, można od razu wyświetlić je funkcją CreateWindow(). Będą one oknami podrzędnymi
do okna głównego aplikacji.

Przykład 7.1

Wyświetl w oknie aplikacji przycisk i okno tekstowe.

Rozwiązanie
Zacznij od utworzenia nowego projektu WinAPI według przykładu 1.3. Wygodnie
będzie zadeklarować globalny uchwyt do okna edycji, ponieważ będziemy go potrze-
bować w różnych miejscach kodu. Zadeklaruj go na początku pliku okienko3.cpp, za-
raz po bloku deklaracji zmiennych globalnych utworzonych przez kompilator.
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);//ta linia istnieje
//globalny uchwyt do okna tekstowego
HWND hEdit;

Obie kontrolki utworzymy zaraz po funkcji CreateWindow() okna głównego. Nie jest
to być może najlepsze miejsce, ale do tego dojdziemy. W pliku okienko3.cpp odnajdź ten
fragment kodu:
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}

To znana już funkcja tworząca okno główne; za jej pomocą powstanie też przycisk.
Wpisz po podanym wyżej kodzie następującą linię:
CreateWindow(_T("button"),_T("Przycisk"),WS_CHILD | WS_VISIBLE, 10,10,70,30,hWnd,
(HMENU)1000,hInstance,NULL);

Pierwszy parametr to nazwa klasy przycisku, drugi określa wyświetlany na nim napis,
trzeci natomiast to styl i sposób wyświetlania okna. WS_CHILD oznacza okno potomne,
a VS_VISIBLE oznacza, że okno będzie widoczne zaraz po uruchomieniu aplikacji. Na-
stępne parametry to współrzędne i wymiary okna. Kolejny argument to uchwyt do
okna nadrzędnego, w tym wypadku okna głównego aplikacji.

Warto zwrócić uwagę na parametr trzeci od końca. Jak już pisałem, ma on dwa znaczenia,
zależnie od tego, czy definiujemy okno z paskiem i ramką (typu OVERLAPPED) czy
okno potomne. Tutaj mamy tę drugą sytuację: ten parametr oznacza identyfikator da-
nego okna. Za pośrednictwem tego identyfikatora będziemy się komunikowali z oknem.
Możemy nadać mu dowolną wartość, pod warunkiem że ta liczba nie była już gdzieś
zdefiniowana. W tym przypadku będzie to 1000.
142 Microsoft Visual C++ 2012. Praktyczne przykłady

Okno edycji również można utworzyć za pomocą CreateWindow(), ale będzie „płaskie” —
jego krawędzie nie będą dawały wrażenia wklęsłości. Ponieważ standardem jest okno
z wklęsłymi krawędziami, musimy posłużyć się rozszerzoną funkcją CreateWindowEx().
Funkcja ta ma takie same parametry jak CreateWindow() i dodatkowo pierwszy parametr
pozwalający na uzyskanie dodatkowych efektów. Tutaj parametr WS_EX_CLIENTEDGE
pozwoli na uzyskanie wrażenia wklęsłej ramki. Bezpośrednio po linii tworzącej przy-
cisk napisz:
hEdit=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),_T(""),WS_CHILD | WS_VISIBLE |
ES_MULTILINE | ES_AUTOVSCROLL ,10,50,300,150,hWnd,(HMENU)1001,hInstance,NULL);

Nowe parametry są związane z tym, że okno ma wiele linii. ES_MULTILINE powoduje


właśnie, że okno ma kilka wierszy, a ES_AUTOVSCROLL umożliwia przewijanie tekstu
w oknie w pionie. Możesz skompilować i uruchomić program. Powinieneś zobaczyć
okno z przyciskiem i oknem tekstowym. Wszystko jest w porządku, ale przydałoby się
trochę przystosować wymiary okna do kontrolek. W tym celu zmienimy jego wymiary
podane w funkcji CreateWindow() utworzonej przez środowisko. Pokazałem ją na począt-
ku tego przykładu. Zamiast domyślnych wymiarów, podanych jako CW_USEDEFAULT,
użyjemy własnych. Zmień wywołanie funkcji następująco:
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, 350, 300, NULL, NULL, hInstance, NULL);

Uruchom aplikację, a okno będzie wyglądało jak na rysunku 7.1.

Rysunek 7.1.
Okno aplikacji do
testowania komunikatów

Zapisz projekt aplikacji, ponieważ będzie bardzo potrzebny w następnych przykładach.

Komunikat WM_COMMAND
Komunikat WM_COMMAND jest wysyłany, kiedy użytkownik wybierze opcję z menu, na-
ciśnie klawisze skrótu lub zmieni status jakiejś kontrolki. W tym komunikacie ważne
są wartości parametrów wParam i lParam — niosą one informacje o przesyłanej ko-
mendzie lub statusie kontrolki.
Rozdział 7. ♦ Obsługa komunikatów 143

Przykład 7.2

Zmodyfikuj aplikację z przykładu 7.1, aby wyświetlała informację o pojawiających się


komunikatach WM_COMMAND.

Rozwiązanie
Otwórz aplikację z przykładu 7.1. Zanim zaczniemy obsługę komunikatów, potrzeba
nam małej funkcji pomocniczej. Chodzi o to, aby w prosty sposób dopisywać kolejne
linie do okna edycji. Normalnie okno nie przewiduje takiej opcji, ale można ją dosyć
łatwo zaprogramować. W ogólności pisanie w oknie polega na wysyłaniu do niego
odpowiednich komunikatów. Do ich wysyłania służy funkcja SendMessage(). O tym,
jakie to komunikaty, powiemy później, na razie po prostu skopiuj do aplikacji funkcję
podaną niżej. Blisko początku kodu aplikacji znajdź sekcję:
// Forward declarations of functions included in this code module
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);

i bezpośrednio pod nią wpisz funkcję, którą nazwałem DodajTekst():


void DodajTekst(HWND _hEdit, LPCTSTR newText)
{
DWORD l,r;
SendMessage(_hEdit, EM_GETSEL,(WPARAM)&l,(LPARAM)&r);
SendMessage(_hEdit, EM_SETSEL, -1, -1);
SendMessage(_hEdit, EM_REPLACESEL, 0, (LPARAM)newText);
SendMessage(_hEdit, EM_SETSEL,l,r);
}

Ma ona dwa parametry: uchwyt okna edycji, w którym chcemy pisać, i tekst do wypisania.
Teraz już można zaczynać rejestrację komunikatów. Jak już pisałem, są one przechwytywa-
ne w instrukcji switch w procedurze (funkcji) danego okna. Procedura głównego okna
naszej aplikacji nazywa się WndProc(). Znajdź więc w kodzie najpierw ową funkcję:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

a w niej instrukcję switch:


switch (message)
{

Na razie nic nie wpisuj, ponieważ tylko analizujemy kod. Wiadomość WM_COMMAND jest
przetwarzana od razu w pierwszym paragrafie instrukcji switch.
case WM_COMMAND:

Przechwycenie samej komendy to za mało. Trzeba jeszcze podjąć odpowiednie działa-


nia w zależności od rodzaju komendy i tego, jaka kontrolka ją wysłała. Te informacje
zapisane są w bardziej i mniej znaczącym słowie parametru wParam towarzyszącego
WM_COMMAND.
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
144 Microsoft Visual C++ 2012. Praktyczne przykłady

Mniej znaczące słowo, zapisane teraz w zmiennej wmId, zawiera identyfikator (nie
uchwyt do okna) klikniętej opcji menu lub kontrolki. Mamy więc kolejną instrukcję
switch, która podejmie odpowiednie działania, zależnie od tego, co nacisnęliśmy.
switch (wmId)
{

I tak, jeśli wybierzemy z menu opcję About, to wmId będzie zawierał identyfikator zde-
finiowany w pliku resource.h.
case IDM_ABOUT:

W tym przypadku jest wyświetlane okno, a my dodamy jeszcze stosowny komunikat


w oknie tekstowym. Po
case IDM_ABOUT:

dopisz linię:
DodajTekst(hEdit, _T("Komunikat WM_COMMAND About - okno główne\r\n"));

Cały przypadek wygląda teraz tak:


case IDM_ABOUT:
DodajTekst(hEdit, _T("Komunikat WM_COMMAND About - okno główne\r\n"));
DialogBox(hInst, MAKEINTRESOURCE(IDD_MOJDIALOG), hWnd, About);
break;

Zaprogramujemy teraz przechwycenie naciśnięcia przycisku, które też wysyła komu-


nikat WM_COMMAND. Nasz przycisk ma identyfikator 1000 (porównaj trzeci od końca pa-
rametr funkcji CreateWindow() dla przycisku). Taką też wartość będzie miało mniej
znaczące słowo parametru wParam, a w konsekwencji zmienna wmId. Zatem bezpośred-
nio po przypadku IDM_ABOUT dodaj receptę na działanie, kiedy wmId równa się 1000.
case 1000:
DodajTekst(hEdit, _T("Komunikat WM_COMMAND Przycisk - okno główne\r\n"));
break;

Uruchom aplikację, wybierz opcję menu About, naciśnij przycisk. W oknie aplikacji
pojawią się komunikaty jak na rysunku 7.2.

Rysunek 7.2.
Okno aplikacji
z komunikatami
Rozdział 7. ♦ Obsługa komunikatów 145

Już wiesz, w jaki sposób użytkownik może sterować aplikacją. Nie wszystkie komuni-
katy są wynikiem bezpośredniego działania użytkownika. Niektóre są wysyłane przez
system. Chyba najbardziej znanym takim komunikatem jest WM_PAINT.

Odmalowywanie okna
— komunikat WM_PAINT
WM_PAINT wskazuje na potrzebę odrysowania okna, więc jest ważnym komunikatem. Może
być wysłany przez aplikację, choć nie za pomocą SendMessage(), ale pośrednio, przez
funkcje odświeżające okno. Komunikat odświeżania okna jest wysyłany na przykład
przy wykonaniu funkcji InvalidateRect() służącej do odświeżania okna, na przykład
w celu narysowania grafiki. Funkcja ma trzy parametry:
BOOL InvalidateRect(HWND hWnd,const RECT *lpRect,BOOL bErase);

Pierwszy z nich, hwnd, to uchwyt do okna, które ma być odświeżone, lpRect to wskaźnik
do struktury kryjącej obszar odświeżany, a bErase określa, czy obszar ma być wyczysz-
czony przed odświeżeniem.

Komunikat WM_PAINT bywa też wysyłany przez system, na przykład przy zmianie wymia-
rów okna lub w przypadku innych sytuacji wymagających odświeżenia okna z powodu
działania systemu.

Zróbmy przykład:

Przykład 7.3

Dodaj do aplikacji sygnalizację wystąpienia komunikatu WM_PAINT.

Rozwiązanie
Otwórz aplikację z projektu 7.1. Znowu potrzebna będzie funkcja DodajTekst(). In-
strukcja jej pisania podana jest w przykładzie 7.2. Tym razem sytuacja jest prosta, bo
mamy tylko jedno miejsce przechwycenia komunikatu. W omawianej już instrukcji
switch (message) znajdź:
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO Add any drawing code here...

EndPaint(hWnd, &ps);
break;

Zaraz po case WM_PAINT: wpisz polecenie wypisania wiadomości w oknie tekstowym:


case WM_PAINT:
DodajTekst(hEdit, _T("Komunikat WM_PAINT - okno główne\r\n"));
hdc = BeginPaint(hWnd, &ps);
146 Microsoft Visual C++ 2012. Praktyczne przykłady

// TODO Add any drawing code here...


EndPaint(hWnd, &ps);
break;

Komunikat WM_PAINT możemy wysłać poprzez wywołanie funkcji InwalidateRect().


Można to zrobić w momencie naciśnięcia przycisku.

Do instrukcji switch wstaw paragraf z identyfikatorem przycisku, podobnie jak to było


w poprzednim przykładzie. Możesz go dodać po case IDM_ABOUT, tak jak w poprzednim
przykładzie.
case 1000:
InvalidateRect(hWnd, NULL, TRUE);
//DodajTekst(hEdit, _T("Komunikat WM_COMMAND Przycisk - okno główne\r\n"));
break;

Skompiluj i uruchom program. Bezpośrednio po uruchomieniu w oknie pojawi się


komunikat o wystąpieniu WM_PAINT. Jest on przesyłany przez system w celu naryso-
wania okna po utworzeniu. Teraz, poruszając i zakrywając okno, spróbuj określić sy-
tuację, kiedy system wysyła ten komunikat. Jest on wysyłany na przykład przy zmianie
wymiarów okna.

Naciskając przycisk, również wysyłasz WM_PAINT za pośrednictwem funkcji Invalidate-


Rect().

Wiesz już coraz więcej o tym, w jaki sposób okna i kontrolki (które też są oknami)
przesyłają wiadomości między sobą a systemem. Rodzajów tych wiadomości jest bar-
dzo wiele; zajmiemy się teraz przesyłanymi w związku z ruchem myszy.

Ruch myszy sygnalizuje


WM_MOUSEMOVE
Kiedy przesuwasz wskaźnik myszy w obrębie danego okna aplikacji, to okno otrzymuje
komunikat WM_MOUSEMOVE. Byłby on średnio przydatny, gdyby nie zawierał współrzęd-
nych wskaźnika. Tak też się dzieje: parametr wParam to identyfikator naciśniętego
przycisku, a parametr lParam zawiera współrzędne wskaźnika. Zmienna wParam w za-
leżności od naciśniętego przycisku może przybierać następujące wartości: MK_LBUTTON
oznacza lewy przycisk, MK_RBUTTON prawy, a MK_MBUTTON środkowy.

Mniej znaczące słowo to współrzędna x, a bardziej znaczące — współrzędna y. Jeśli


mysz nie jest przesuwana, to komunikat nie jest wysyłany, zatem za pomocą obsługi
WM_MOUSEMOVE niemożliwe jest wykrycie naciśnięcia przycisku nieruchomej myszy.

W naszej aplikacji wyświetlimy powiadomienie o napływającym do okna komunikacie


WM_MOUSEMOVE wraz ze współrzędnymi. Będzie to przy okazji sposobność do pokazania,
jak wyświetlać wartości zmiennych, nie tylko dotyczących komunikatów.
Rozdział 7. ♦ Obsługa komunikatów 147

Przykład 7.4

Wyświetl w oknie edycji powiadomienie o otrzymaniu komunikatu WM_MOUSEMOVE przez


główne okno aplikacji. Dodatkowo wypisz aktualne współrzędne myszy.

Rozwiązanie
Potrzebujemy aplikacji według przykładu 7.1, wraz z funkcją DodajTekst(). W związku
z wyświetlaniem współrzędnych konieczne jest zdefiniowanie kilku zmiennych. Zde-
finiujemy je w funkcji okna WndProc(). Na początku tej funkcji już jest zdefiniowanych
kilka zmiennych.
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;

Bezpośrednio po nich zdefiniujemy zmienne do przechowywania współrzędnych wmX i wmY;


status przycisku zapiszemy w wmPrzycisk. Aby wyświetlać zmienne, trzeba będzie zamie-
nić je na łańcuch znakowy, zdefiniujemy więc tabelę znaków TCHAR. Ponieważ program
jest kompilowany w trybie szerokich znaków, będących implementacją tabeli Unicode,
zmienna TCHAR to w rzeczywistości wchar_t.
int wmX,wmY,wmPrzycisk;
TCHAR wsp[80];

Rozmiar tablicy wsp powinien być tak duży, aby pomieścić cały napis wyświetlany
w oknie. Wartość 80 przyjąłem w drodze prób i błędów. Oczywiście, można było to zrobić
bardziej uniwersalnie, wyliczając potrzebny rozmiar tablicy z rozmiaru tekstu do wpisa-
nia, ale dla uproszczenia przyjąłem stałą wartość.

Przejdź do znanego już bloku switch.


switch (message)

W tym bloku wpiszemy przypadek reagowania na komunikat WM_MOUSEMOVE. Możesz


wpisać go w dowolnym miejscu między istniejącymi przypadkami. Ja wpisałem go
po bloku komunikatu WM_PAINT.
break; //linia kończąca przypadek dla WM_PAINT — istnieje
case WM_MOUSEMOVE: // tu rozpoczyna się nowy przypadek
//kod wykonywany po odebraniu WM_MOUSEMOVE
break; //a tu kończy

We wnętrzu bloku po pierwsze odczytamy współrzędne myszy z parametru lParam i stan


jej klawiszy z wParam.
wmPrzycisk = wParam;
wmX = LOWORD(lParam);
wmY = HIWORD(lParam);

Makra LOWORD i HIWORD odczytują odpowiednio mniej lub bardziej znaczące słowo zmien-
nej. Następnie zamienimy odczytane liczby wraz z komentarzem na zmienną znakową
i wyświetlimy ją w oknie. Konwersję liczby na łańcuch znakowy można zrealizować
148 Microsoft Visual C++ 2012. Praktyczne przykłady

różnie. Ja użyłem funkcji swprintf_s(). Za jej pomocą można wstawiać więcej niż jedną
wartość liczbową do wspólnego łańcucha znaków i dlatego ma ona zmienną liczbę pa-
rametrów. Pierwszy to bufor, drugi to liczba znaków w buforze, trzeci to docelowy łań-
cuch znakowy wraz z maską znaków, a od czwartego parametru zaczynają się zmienne
liczbowe do wstawienia do łańcucha. Zmiennych musi być tyle, ile masek znaków.
Maski są kolejno zastępowane zmiennymi. Typ maski zależy od typu zmiennej. My
posłużymy się maską %d, oznaczającą, że trzeba ją zastąpić zmienną całkowitą. Myślę,
że przykład wiele tu wyjaśni. W dalszym ciągu wpisujemy:
swprintf_s(wsp, _countof(wsp), _T("WM_MOUSEMOVE X=%d Y=%d\r\n"), wmX,wmY );
DodajTekst(hEdit, wsp);

Współrzędne myszy mamy już wypisane, pozostała jeszcze reakcja na przyciski. To,
który przycisk myszy został naciśnięty, mamy już wpisane do zmiennej wmPrzycisk. Bę-
dzie to zmienna decyzyjna dla zagnieżdżonej instrukcji switch. Przypadki case będą od-
powiadały przyciskom.
switch(wmPrzycisk)
{
case MK_LBUTTON:
DodajTekst(hEdit, _T("Wciśnięty przycisk lewy\r\n"));
break;
case MK_RBUTTON:
DodajTekst(hEdit, _T("Wciśnięty przycisk prawy\r\n"));
break;
case MK_MBUTTON:
DodajTekst(hEdit, _T("Wciśnięty przycisk środkowy\r\n"));
break;
}

Skompiluj i uruchom program. Przy każdym przesunięciu myszy w obrębie okna aplikacji
będą raportowane współrzędne. Aplikacja w działaniu wygląda jak na rysunku 7.3.

Rysunek 7.3.
Wyświetlanie
parametrów ruchu
myszy

Zauważ, że kiedy wskaźnik znajduje się nad oknem edycji, komunikat nie jest przesyłany,
mimo że okno edycji jest oknem podrzędnym w stosunku do okna aplikacji. Przy
przesuwaniu myszy z naciśniętym przyciskiem będzie sygnalizacja przycisku.
Rozdział 7. ♦ Obsługa komunikatów 149

WM_CREATE kończy tworzenie okna


Kiedy powiedzie się wykonanie funkcji CreateWindow(), do nowo utworzonego okna
przesyłany jest komunikat WM_CREATE. Jeśli okno jest utworzone, można umieścić w nim
kontrolki. Zatem bezpieczniej jest wywoływać funkcję CreateWindow() kontrolek w pro-
cedurze obsługi tego komunikatu niż bezpośrednio po utworzeniu okna głównego. Nie
jest to jasno unormowane, ale wielu programistów postępuje w ten sposób. Zamieńmy
miejsce tworzenia kontrolek w naszym programie.

Przykład 7.5
Zmień aplikację z przykładu 7.1 tak, aby przycisk i okno tekstowe były wstawiane do
okna głównego po otrzymaniu WM_CREATE.

Rozwiązanie
Otwórz aplikację z przykładu 7.1. Po kilku przykładach znasz już instrukcję switch
w funkcji okna WndProc(), która przechwytuje komunikaty. Dodaj do niej przypadek:
case WM_CREATE:
break;

Miejsce umieszczenia tego przypadku między innymi paragrafami case jest dowolne.
Możesz to zrobić na przykład po obsłudze WM_PAINT, jak w poprzednim przykładzie.
Ponieważ wywołania funkcji CreateWindow() będą prawie identyczne jak te, które
napisaliśmy wcześniej, najprościej będzie je przenieść. Obecnie znajdują się w funkcji
InitInstance(), trochę wyżej w kodzie programu.
CreateWindow(_T("button"),_T("Przycisk"),WS_CHILD | WS_VISIBLE, 10,10,70,30,hWnd,
(HMENU)1000,hInst,NULL);
hEdit=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),_T(""),WS_CHILD | WS_VISIBLE |
ES_MULTILINE | ES_AUTOVSCROLL ,10,50,300,150,hWnd,(HMENU)1001,hInst,NULL);

Zaznacz te dwie linie, a następnie wytnij (Ctrl+X) do schowka i wklej w utworzony przed
chwilą przypadek obsługi WM_CREATE. Aby wywołania funkcji działały, trzeba zmienić
przedostatni parametr, oznaczający instancję aplikacji. Zasięg zmiennej hInstance nie
obejmuje WndProc(), ale mamy globalną zmienną hInst utworzoną przez środowisko.
Instrukcję podstawienia możesz znaleźć w InitInstance() (tej linii nigdzie nie wpisuj,
ona już jest).
hInst = hInstance; // Store instance handle in our global variable

Zatem skorzystamy z hInst, podstawiając ją jako przedostanie parametry obydwu funkcji


CreateWindow().
CreateWindow(_T("button"),_T("Przycisk"),WS_CHILD | WS_VISIBLE , 10,10,70,30,hWnd,
(HMENU)1000,hInst,NULL);
hEdit=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),_T(""),WS_CHILD | WS_VISIBLE |
ES_MULTILINE | ES_AUTOVSCROLL ,10,50,300,150,hWnd,(HMENU)1001,hInst,NULL);

I gotowe. Po kompilacji i uruchomieniu program będzie wyglądał identycznie, ale moim


zdaniem teraz jest napisany lepiej.
150 Microsoft Visual C++ 2012. Praktyczne przykłady

Wspomniałem już o funkcji SendMessage() jako uniwersalnej metodzie wysyłania dowol-


nego komunikatu do okna. Rozdział o przesyłaniu komunikatów byłby moim zdaniem
niepełny bez dokładniejszego opisu tej funkcji.

SendMessage() prześle każdy


komunikat
Tytuł działu jest trochę na wyrost, bo SendMessage() może przesłać każdy komunikat,
ale nie zawsze jest to zalecane. Przykładem jest WM_PAINT, który nie powinien być przesy-
łany bezpośrednio, a tylko za pomocą wywołania funkcji odświeżającej okno. Składnia
SendMessage()jest intuicyjna.
LRESULT WINAPI SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);

Parametr hWnd to uchwyt okna, które jest adresatem wiadomości, Msg to komunikat,
a wParam i lParam to parametry, jakie mają mu towarzyszyć. Wartość zwracana przez
funkcję jest różna, w zależności od wysłanego komunikatu. Spróbujmy wysłać komuni-
katy do okien naszej aplikacji w sposób bezpośredni. Posłużymy się jeszcze nieznanym
rodzajem, mianowicie WM_SETTEXT.

Komunikat WM_SETTEXT jest bardzo często stosowany w oknach edycji. My go jeszcze nie
stosowaliśmy, ponieważ powoduje on wymazanie całej zawartości okna przed napisaniem
tekstu, a nam chodziło o dopisanie do istniejącego tekstu. Komunikat WM_SETEXT można
wysłać do dowolnego okna, nie tylko do okna edycji. Nie zawsze uzyskamy zadowala-
jące efekty, ale można próbować. Tekst do wypisania w oknie podajemy w zmiennej
lParam. Wykonajmy prosty przykład.

Przykład 7.6

Wyślij tekst do okna głównego i okna edycji za pomocą SendMessage() i komunikatu


WM_SETTEXT.

Rozwiązanie
Potrzebna będzie aplikacja według przykładu 7.5. Tym razem nie potrzeba nam funkcji
DodajTekst(). Funkcję SendMessage() wywołamy po naciśnięciu przycisku. Powtórzmy
wiadomości z przykładu 7.2 lub 7.3. Naciśnięcie przycisku generuje komunikat
WM_COMMAND, który w mniej znaczącym słowie parametru wParam zawiera identyfikator (nie
uchwyt) przycisku. Nasz przycisk ma identyfikator 1000. Tak więc w funkcji okna głów-
nego odnajdź blok instrukcji switch (message) i niżej przypadek case WM_COMMAND:
obsługujący interesujący nas komunikat. Instrukcja ta posiada w swoim wnętrzu kolejną
instrukcję switch, o której mówimy że jest zagnieżdżona. Już pisałem w przykładzie
7.2, że mniej znaczące słowo parametru wParam (czyli identyfikator naciśniętego przyci-
sku) jest przepisywane do zmiennej wmId. Zmienna ta staje się zmienną decyzyjną za-
gnieżdżonej instrukcji switch (wmId) reagującej na identyfikatory poszczególnych
Rozdział 7. ♦ Obsługa komunikatów 151

przycisków (gdyby było ich więcej) lub menu. W tym bloku umieszczamy odpowiedni
blok case. Kod w tym bloku będzie wykonany po otrzymaniu komunikatu WM_COMMAND
z mniej znaczącym słowem wParam równym 1000, a więc po naciśnięciu naszego przycisku.
case 1000:
SendMessage(hEdit,WM_SETTEXT,0,(LPARAM)_T("Wiadomość dla okna edycji"));
SendMessage(hWnd,WM_SETTEXT,0,(LPARAM)_T("Wiadomość dla okna głównego"));
break;

Naciśnięcie przycisku wyśle dwa komunikaty WM_SETTEXT — jeden do okna edycji, a drugi
do głównego okna aplikacji. Skompiluj i uruchom program. Po naciśnięciu przycisku
zobaczysz sytuację jak na rysunku 7.4.

Rysunek 7.4.
Komunikaty tekstowe
wysyłane do okien

Zachowanie się okna edycji jest przewidywalne, ale napis wysłany do okna głównego
wylądował w pasku, a nie w samym oknie. Jak widać, reakcja konkretnego rodzaju
(klasy) okna na ten sam komunikat może być różna.

Jestem Ci jeszcze winien wyjaśnienie działania funkcji DodajTekst() podanej na po-


czątku rozdziału. Jak widzisz, składa się ona z czterech wywołań SendMessage(). Po
wykonaniu ostatniego przykładu wiesz już, że do okna edycji zostały tym samym wysłane
cztery komunikaty. Pierwszy zapamiętuje aktualny stan podświetlonego tekstu w oknie.
Nie chodzi tu o podświetlony tekst, a raczej o pozycję kursora. Później będzie możli-
we przywrócenie tej pozycji po zakończonej operacji. Komunikat EM_GETSEL zapa-
mięta pozycję kursora. Zauważ, że w tym wypadku zmienne wParam i lParam nie niosą
parametru do komunikatu, ale zwracają wynik jego działania.
SendMessage(_hEdit, EM_GETSEL,(WPARAM)&l,(LPARAM)&r);

Druga linia wysyła do okna komunikat EM_SETSEL, który normalnie zaznacza tekst
w oknie. Parametry przekazywane z tym komunikatem to numer pierwszego i ostatniego
znaku do zaznaczenia. Kursor jest ustawiany na pozycję ostatniego wybranego znaku.
Wartości –1 w obydwu parametrach to żądanie wyczyszczenia aktualnej selekcji i wyboru
całego tekstu w oknie. Taka kombinacja nie spowoduje zaznaczenia niczego, a tylko
przesunięcie kursora na koniec istniejącego tekstu.
SendMessage(_hEdit, EM_SETSEL, -1, -1);
152 Microsoft Visual C++ 2012. Praktyczne przykłady

Teraz zastępuję wybrany tekst nowym za pomocą komunikatu EM_REPLACESEL. Specyfika


tego komunikatu jest taka, że jeśli nic nie jest wybrane — a tak jest u nas — to nowy
tekst zostanie dopisany w miejscu kursora. Kursor zaś jest na końcu aktualnego tekstu.
SendMessage(_hEdit, EM_REPLACESEL, 0, (LPARAM)newText);

Wreszcie sprzątam po sobie, przywracając pozycję kursora sprzed operacji.


SendMessage(_hEdit, EM_SETSEL,l,r);

W następnym rozdziale zajmę się bardziej szczegółowo podstawowymi kontrolkami.


Rozdział 8.
Podstawowe kontrolki
w działaniu aplikacji WinAPI

Wszechstronny przycisk Button


Kontrolka przycisku ma więcej zastosowań, niż mogłoby się wydawać. Może być
polem wyboru lub przyciskiem opcji, kontenerem grupującym, a nawet przyciskiem o
specjalnym wyglądzie. Wszystko zależy od stylu podanego przy tworzeniu przycisku.
Wiemy już z poprzedniego rozdziału, że styl jest trzecim parametrem funkcji Create-
Window(). Aby uzyskać domyślny widok przycisku, podawaliśmy tam sumę dwóch
parametrów: WS_CHILD | WS_VISIBLE. Tak samo jest i tu, z tym że dodatkowo docho-
dzi trzecia wartość, określająca rodzaj kontrolki zbudowanej na bazie przycisku. Wy-
gląd kontrolki w zależności od tej trzeciej wartości stylu przedstawia tabela 8.1.

Tabela 8.1 Kontrolki możliwe do uzyskania jako style przycisku Button


Kontrolka Styl Opis
BS_PUSHBUTTON Domyślny styl zwykłego przycisku.

BS_DEFPUSHBUTTON Przycisk domyślnie aktywny przy utworzeniu okna. Przycisk


aktywny może być wciśnięty poprzez naciśnięcie Enter.
BS_CHECKBOX, Pole wyboru. Pozwala na zaznaczenie jakiejś opcji. Styl
BS_AUTOCHCECKBOX BS_AUTOCHCECKBOX tworzy przycisk, który sam wyświetla
zaznaczenie. W zwykłym przycisku wyświetlenie stanu
zaznaczonego po kliknięciu trzeba zaprogramować samemu.
BS_CHECKBOX | Styl BS_LEFTTEXT powoduje wyświetlenie tekstu po lewej
BS_LEFTTEXT stronie pola wyboru. Działa też z przyciskiem opcji.
BS_RADIOBUTTON, Przycisk opcji zwykle stosuje się do wyboru jednej opcji
BS_AUTORADIOBUTTON z kilku.
BS_GROUPBOX Kontener grupujący kontrolki.
154 Microsoft Visual C++ 2012. Praktyczne przykłady

Obsługa przycisków Button


jako pól wyboru
Pola wyboru powstałe z przycisku z użyciem stylu BS_CHECKBOX mogą być w jednym
z dwóch stanów: zaznaczonym lub niezaznaczonym. W każdej chwili możemy sprawdzić,
w jakim stanie znajduje się pole, wysyłając do niego komunikat BM_GETCHECK. Samo
wysłanie komunikatu można zrealizować na dwa sposoby: posługując się znaną już
funkcją SendMessage() lub specjalną Button_GetCheck(). Obie te funkcje zwrócą jedną
z trzech wartości
 BST_CHECKED — pole zaznaczone,
 BST_INTERMEDIATE — pole w stanie pośrednim, dla pól trzystanowych,
 BST_UNCHECKED — pole niezaznaczone.

Jak zwykle zróbmy przykład.

Przykład 8.1

Zaprogramuj wyświetlanie w oknie tekstowym komunikatu o stanie przycisku stylu


BS_CHECKBOX.

Rozwiązanie
Napiszemy aplikację trochę podobną do tej z rozdziału 7. W miarę poznawania kolejnych
kontrolek będziemy ją rozwijać. Może nie będzie bardzo użyteczna, ale za to edukacyjna
i w miarę efektowna. Zaczniemy od aplikacji WinAPI według przykładu 1.3. Będą po-
trzebne dwa uchwyty: do przycisku i okna tekstowego. Zadeklarujemy je jako zmienne
globalne. Na początku pliku .cpp z kodem aplikacji znajdź deklaracje funkcji.
// Forward declarations of functions included in this code module
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);

Niżej dopisz deklaracje dwóch uchwytów typu HWND.


//globalny uchwyt do przycisku i okna tekstowego
HWND hEdit;
HWND hButton;

Pole wyboru i okno tekstowe zamontujemy w oknie głównym. Zgodnie z tym, co pisałem
w poprzednim rozdziale, odpowiednie funkcje CreateWindow() umieścimy w obsłudze
komunikatu WM_CREATE w funkcji WndProc() okna głównego. Powtórzmy, że komunikaty
są obsługiwane w instrukcji swith(message). Ponieważ w kodzie napisanym przez
środowisko nie ma obsługi WM_CREATE, trzeba dopisać cały przypadek. Użyjemy stylu
BS_AUTOCHECKBOX i nie będziemy się martwić o wyświetlanie zaznaczenia pola.
Rozdział 8. ♦ Podstawowe kontrolki w działaniu aplikacji WinAPI 155

case WM_CREATE:
hButton=CreateWindow(_T("button"),_T("Przycisk"),BS_AUTOCHECKBOX|WS_CHILD |
WS_VISIBLE , 10,10,80,30,hWnd, (HMENU)1000,hInst,NULL);
hEdit=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),_T(""),WS_CHILD | WS_VISIBLE |
ES_MULTILINE | ES_AUTOVSCROLL ,10,50,300,150,hWnd,(HMENU)1001,hInst,NULL);
break;

Do tej pory nie potrzebowaliśmy uchwytu przycisku; teraz będzie potrzebny, ponieważ
będziemy wysyłać do tej kontrolki komunikaty. Komunikat o stanie przycisku będziemy
wyświetlać w momencie jego kliknięcia. Obsługa samego kliknięcia jest identyczna jak
w zwykłym przycisku. W dalszym ciągu jesteśmy w funkcji okna głównego WndProc(),
przy instrukcji switch (message). Komunikat WM_COMMAND obsługuje przypadek, który
już znasz:
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);

Przypomnijmy, że mniej znaczące słowo parametru wParam zawiera identyfikator przy-


cisku, który przesłał komunikat WM_COMMAND. Poniżej mamy zagnieżdżoną instrukcję
switch, przetwarzającą komunikaty z poszczególnych kontrolek identyfikowanych
właśnie przez przesłany identyfikator.
switch (wmId)
{

Przycisk ma identyfikator 1000, a więc kiedy wmId przybierze wartość 1000, będzie to
znaczyło, że komunikat pochodzi od tego przycisku. W ostatnio wymienionym bloku
switch dopisujemy przypadek.
case 1000:
if (SendMessage(hButton, BM_GETCHECK, 0,0)==BST_CHECKED)
SendMessage(hEdit,WM_SETTEXT,0,(LPARAM)_T("Zaznaczony"));
if (SendMessage(hButton, BM_GETCHECK, 0,0)==BST_UNCHECKED)
SendMessage(hEdit,WM_SETTEXT,0,(LPARAM)_T("Nie zaznaczony"));
break;

Za pomocą SendMessage() wysyłamy komunikat BM_GETCHECK do przycisku. Jeśli w od-


powiedzi przycisk zwraca wartość BST_CHECKED, to znaczy, że jest zaznaczony; wartość
BST_UNCHECKED to brak zaznaczenia. W zależności od wartości zwracanej przez przycisk
w oknie wypisywany jest odpowiedni tekst. Do wypisania też używamy SendMessage().
Skompiluj i uruchom program, zaznacz i wyczyść pole wyboru poprzez klikanie myszą.
Działanie aplikacji pokazuje rysunek 8.1. Zachowaj aplikację na później.

Kontrolka ComboBox
Jest to element z dużymi możliwościami, składający się z rozwijanej listy w postaci
linii tekstu. Jedna z tych linii może być wybrana i jest wyświetlana w kontrolce. Ma
wiele zastosowań, na przykład jako wybór opcji działania programu lub wpisywanie
156 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 8.1.
Pole wyboru w działaniu

zdefiniowanych wcześniej tekstów do formularza. W tabeli 8.2 mamy trzy komunikaty


przesyłane do tej kontrolki. Pierwszy wpisuje linię tekstu, drugi ją kasuje, a trzeci po-
biera aktualnie wybrany tekst.

Tabela 8.2. Wybrane komunikaty kontrolki ComboBox


Komunikat Parametr wParam Parametr lParam Opis
CB_ADDSTRING 0 Tekst do dodania Dodaje linię tekstu do
kontrolki.
CB_GETCURSEL 0 0 Funkcja SendMessage()
zwraca numer aktualnie
wybranego tekstu.
CB_DELETESTRING Numer linii do skasowania. 0 Kasuje linię o podanym
Pierwsza linia ma numer zero. numerze.

Do naszej aplikacji dodamy listę ComboBox z trzema opcjami opisującymi figury geo-
metryczne — trójkąt, koło i kwadrat.

Przykład 8.2

Do aplikacji z poprzedniego przykładu dodaj kontrolkę ComboBox z trzema opcjami:


„Trójkąt”, „Koło” i „Kwadrat”.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu. Potrzebna będzie kolejna zmienna glo-
balna: uchwytu do kontrolki. Najlepiej dla porządku dopisać ją obok istniejących de-
finicji uchwytów na początku pliku programu.
HWND hEdit; // ta linia istnieje
HWND hButton; //ta linia istnieje
HWND hComboBox; //dopisz definicję ComboBox
Rozdział 8. ♦ Podstawowe kontrolki w działaniu aplikacji WinAPI 157

Teraz znajdź funkcję WndProc(), a w niej instrukcję switch(message). W przypadku


obsługi WM_CREATE dopisz tworzenie kontrolki ComboBox.
hComboBox=CreateWindow(_T("combobox"),_T(""),WS_CHILD | WS_VISIBLE | CBS_DROPDOWN,
200,10,150,150,hWnd,(HMENU)1002,hInst,NULL);

W dalszym ciągu za pomocą CR_ADDSTRING i niezastąpionej funkcji SendMessage() do-


dajemy kolejne wyrazy.
SendMessage(hComboBox,CB_ADDSTRING, 0, (LPARAM)_T("Kwadrat"));
SendMessage(hComboBox,CB_ADDSTRING, 0, (LPARAM)_T("Koło"));
SendMessage(hComboBox,CB_ADDSTRING, 0, (LPARAM)_T("Trójkąt"));

I to już wszystko. Skompiluj program. Kiedy klikniesz na przycisk w kontrolce ComboBox,


zobaczysz opcje, jak na rysunku 8.2. Klikając na którąś z nich, możesz ją wybrać.

Rysunek 8.2.
Lista wyboru ComboBox
z opcjami

W następnym etapie pisania naszej przykładowej aplikacji WinAPI zrezygnujemy z pola


tekstowego, a zamiast niego w oknie aplikacji będzie rysowana figura wybrana w liście
rozwijalnej. Z rysowaniem wiąże się pojęcie kontekstu. Rysując figury, rysujemy je „na
kontekście”. Jest to trochę abstrakcyjny obiekt, który może być stowarzyszony z do-
wolnym urządzeniem. Sama nazwa „urządzenie” też jest trochę abstrakcyjna, bo nie
zawsze oznacza coś w plastykowej obudowie stojące na biurku. Urządzeniem może
być zarówno okno aplikacji, jak i drukarka. Rysowanie na kontekście okna będzie
„zwykłą grafiką”, a rysowanie na kontekście drukarki to drukowanie grafiki. Jest to
więc wygodny pośrednik, umożliwiający zastosowanie tych samych procedur do obsługi
różnych urządzeń. Zbiór funkcji do rysowana WinAPI nazywa się GDI (Graphical
Device Interface). Na początku obsługi WM_PAINT jest funkcja BeginPaint(). Zwraca
ona kontekst połączony z oknem, na którym będziemy rysować. Procedura obsługi koń-
czy się funkcją EndPaint(). Całe rysowanie musi się odbyć pomiędzy BeginPaint()
i EndPaint()

Do rysowania w naszej aplikacji wykorzystamy trzy funkcje GDI, zestawione w ta-


beli 8.3.
158 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 8.3. Funkcje GDI wykorzystywane w aplikacji


Funkcja Parametry Opis
BOOL MoveToEx(HDC hdc, hdc — kontekst do rysowania. Przesuwa aktualny punkt
int X,int Y, początkowy na kontekście
x,y — współrzędne punktów,
do współrzędnych x, y. Może
LPPOINT lpPoint do których przesuwamy punkt.
zachować pozycję punktu przed
); lpPoint — struktura do zapisania przesunięciem.
aktualnego punktu przed wykonaniem
przesunięcia.
BOOL LineTo(HDC hdc, hdc — kontekst do rysowania. Rysuje odcinek od aktualnej
int nXEnd,int nYEnd); pozycji do współrzędnych x, y.
x,y — współrzędne końca odcinka.
BOOL Ellipse( hdc — kontekst do rysowania. Rysuje elipsę wpisaną w zadany
__in HDC hdc, prostokąt. Aby narysować koło,
nLeftRect, nTopRect — współrzędne
należy podać kwadrat.
__in int nLeftRect, lewego górnego rogu prostokąta
__in int nTopRect,
opisanego.
__in int nRightRect, nRightRect, nBottomRect — współrzędne
prawego dolnego rogu prostokąta
__in int nBottomRect opisanego.
);

Przykład 8.3

Zmień aplikację z poprzednich przykładów tak, aby wyświetlała figurę, której nazwa
jest wybrana w liście.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu. Najpierw pozbędziemy się okna edycji.
Zakładam, że już wiesz, jak znaleźć miejsce w pliku źródłowym, gdzie obsługiwany
jest komunikat WM_CREATE. W kodzie obsługi znajdź linię:
hEdit=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),_T(""),WS_CHILD | WS_VISIBLE |
ES_MULTILINE | ES_AUTOVSCROLL ,10,50,300,150,hWnd,(HMENU)1001,hInst,NULL);

i po prostu ją skasuj w całości. Wracamy teraz na początek kodu, do definicji globalnych


uchwytów do okien. Dopiszemy w tym miejscu zmienne do przechowywania współrzęd-
nych figury i jej wymiaru.
HWND hComboBox; //ta linia istnieje — deklaracja uchwytu do listy ComboBox
//współrzędne i wymiar figury — te linie dopisujemy
int x1,y1;
int bok;

Zmienna z wymiarem figury nazywa się bok, ale w przypadku koła będzie to średnica.
Początkowe wartości nadamy tym zmiennym w funkcji _tWinMain(). Odnajdź w tej
funkcji podaną niżej linię, a poniżej dopisz inicjalizację zmiennych przykładowymi
wartościami.
HACCEL hAccelTable; //ta linia istnieje
x1=200,y1=200; //tu dopisujemy współrzędne
bok=100; //i długość boku
Rozdział 8. ♦ Podstawowe kontrolki w działaniu aplikacji WinAPI 159

Figury będą rysowane po otrzymaniu komunikatu WM_PAINT. Kwadrat i trójkąt będą


składane z linii rysowanych funkcją LineTo(). Najpierw przesuwamy aktualny punkt do
pierwszego wierzchołka za pomocą MoveToEx(), a następnie rysujemy linie pomiędzy
wierzchołkami. Do narysowania koła chyba najłatwiej wykorzystać funkcję rysującą
elipsę, przy czym obie półosie są równe sobie. Odpowiednia figura jest rysowana po
sprawdzeniu opcji wybranej na liście rozwijalnej.

Cały kod obsługi WM_PAINT, jaki należy napisać, podałem niżej:


case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO Add any drawing code here...

if (SendMessage(hComboBox,CB_GETCURSEL, 0,0)==0) {
MoveToEx(hdc,x1,y1,NULL);
LineTo(hdc,x1+bok,y1);
LineTo(hdc,x1+bok,y1+bok);
LineTo(hdc,x1,y1+bok);
LineTo(hdc,x1,y1);
}
if (SendMessage(hComboBox,CB_GETCURSEL,0,0)==1) {
MoveToEx(hdc,x1,y1,NULL);
Ellipse(hdc,x1-bok,y1-bok,x1+bok,y1+bok);
}
if (SendMessage(hComboBox,CB_GETCURSEL, 0,0)==2) {
MoveToEx(hdc,x1,y1,NULL);
LineTo(hdc,x1+bok,y1+bok);
LineTo(hdc,x1+bok,y1);
LineTo(hdc,x1,y1);
}
EndPaint(hWnd, &ps);
break;

Jak wiemy, komunikat WM_PAINT jest wysyłany przez system w określonych przypadkach.
My jednak nie chcemy czekać, zatem wyślemy go sami po każdej zmianie opcji w liście.
Nasza lista ma identyfikator 1002. Zmiana opcji wysyła komunikat WM_COMMAND , tak
jak w przypadku przycisku. W obsłudze tego komunikatu jest zagnieżdżona instrukcja
switch(), obsługująca naciśnięcia poszczególnych kontrolek. W tej chwili jest tam przy-
padek case 1000:, obsługujący wypisywanie komunikatów w polu tekstowym. Pod tym
przypadkiem dopisz wysyłanie komunikatu WM_PAINT za pomocą funkcji InvalidateRect()
po każdej zmianie opcji w liście.
case 1002:
InvalidateRect(hWnd, NULL, TRUE);
break;

Skompiluj i uruchom program. Po wybraniu nazwy figury w liście pojawi się ona w oknie.
Przykładowy wygląd okna po wybraniu kwadratu przedstawia rysunek 8.3.

Mamy już narysowaną figurę; przydałoby się naszą aplikację ożywić. Zrobimy prostą
animację, w której figura będzie się odbijała od boków okna. Kolejne rysowanie figury
wymaga cyklicznych wywołań WM_PAINT. Zrealizujemy to za pomocą timera. Timer to taki
160 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 8.3.
Rysowanie wybranych
figur

mechanizm, który może co pewien czas wykonywać zadaną funkcję lub generować
zdarzenie WM_TIMER. My wybierzemy tę drugą opcję. W obsłudze zdarzenia WM_TIMER
wpiszemy funkcję InvalidateRect(), która wywoła z kolei komunikat WM_PAINT. W kolej-
nych wywołaniach WM_PAINT figura będzie odpowiednio przesuwana po ekranie. Ale
po kolei. Wróćmy do timera. Może być on powiązany z konkretnym oknem aplikacji,
u nas z oknem głównym. Tworzenie timera umożliwia funkcja SetTimer().
UINT_PTR WINAPI SetTimer(HWND hWnd,UINT_PTR nIDEvent,UINT uElapse,TIMERPROC
lpTimerFunc);

Pierwszy parametr funkcji to uchwyt okna, z jakim połączony będzie timer, drugi to
jego identyfikator. Trzeci parametr to czas między wysłaniem kolejnych komunika-
tów WM_TIMER w milisekundach, wreszcie ostatni oznacza funkcję wywoływaną w kolej-
nych cyklach timera. Niestety, utworzonego timera nie można zatrzymać. Jeśli chcemy
zakończyć jego działanie, to trzeba go zniszczyć.

Do kasowania timera służy funkcja KillTimer().


BOOL WINAPI KillTimer(HWND hWnd,UINT_PTR uIDEvent);

Akceptuje ona uchwyt okna z timerem i jego identyfikator. Aby odbijać figurę od brze-
gów okna, musimy znać ich współrzędne. To z kolei umożliwi nam funkcja:
BOOL WINAPI GetClientRect(HWND hWnd,LPRECT lpRect);

Pobiera ona wymiary wnętrza okna o uchwycie hWnd i wpisuje do specjalnej struktury
typu RECT, która ma cztery składowe oznaczające boki. O strukturach pisałem już w tej
książce. Składowe te nazywają się top, bottom, left i right. Jest tylko jedna rzecz, na
którą trzeba zwrócić uwagę. Prostokąt okna pobrany tą funkcją będzie określony we współ-
rzędnych całego ekranu. Mówiąc prościej, tak naprawdę struktura RECT będzie zawie-
rała współrzędne lewego górnego i prawego dolnego rogu okna zapisane w układzie
współrzędnych związanym z pulpitem. Punkt (0,0) tego układu znajduje się w lewym
Rozdział 8. ♦ Podstawowe kontrolki w działaniu aplikacji WinAPI 161

górnym rogu pulpitu. Figurę do animacji wykreślamy w układzie współrzędnych, który


ma punkt (0,0) w lewym górnym rogu okna aplikacji. Spróbuj sam wymyślić, jak przejść
z jednego układu do drugiego; ja później pokażę to na programie. Tyle teorii, a teraz
przykład.

Przykład 8.4

Dodaj do aplikacji z poprzedniego przykładu animację figury w postaci odbijania jej od bo-
ków okna. Niech animacja włącza się, jeśli zaznaczone jest pole wyboru.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu. Będziemy potrzebowali kolejnych dwóch
zmiennych globalnych, oznaczających prędkość figury wzdłuż osi x i y. Proponuję
dodać je po deklaracji zmiennej bok.
int bok; //ta linia istnieje
//prędkość kształtu
int dx=2,dy=2;

Na początku funkcji okna głównego WndProc() dodajemy strukturę typu RECT, przecho-
wującą wymiary okna.
HDC hdc; // ta linia istnieje
RECT okno; //tę dodajemy

Sama zasada odbijającej się piłki jest dosyć prosta. Pozycja figury jest określona
przez współrzędne jednego z jej punktów. Już to mamy z poprzedniego przykładu, fi-
gura w procedurze obsługi komunikatu WM_PAINT jest rysowana tylko przez podawanie
współrzędnych x1 i y1 oraz zmiennej bok. Za każdym wystąpieniem komunikatu
WM_PAINT figura będzie przesuwana o dx punktów po osi x i dy po osi y. Zatem będzie
poruszała się po linii ukośnej. Jeśli figura znajdzie się przy dolnej krawędzi okna, to zmie-
niamy znak prędkości dy — wtedy figura zacznie się od tej krawędzi oddalać w górę,
również po linii ukośnej. Jeśli figura znajdzie się przy prawej krawędzi, to zmieniamy
znak prędkości dx, zatem figura zacznie się od tej krawędzi oddalać w lewo. Podobna
sytuacja zachodzi, jeśli figura znajdzie się przy górnej lub lewej krawędzi okna.

Oto kod obsługi WM_PAINT z poprzedniego przykładu uzupełniony o przemieszczanie i wa-


runki odbicia od krawędzi.
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO Add any drawing code here...
GetClientRect(hWnd,&okno);
if (SendMessage(hComboBox,CB_GETCURSEL, 0,0)==0) {
MoveToEx(hdc,x1,y1,NULL);
LineTo(hdc,x1+bok,y1);
LineTo(hdc,x1+bok,y1+bok);
LineTo(hdc,x1,y1+bok);
LineTo(hdc,x1,y1);
}
if (SendMessage(hComboBox,CB_GETCURSEL,0,0)==1) {
162 Microsoft Visual C++ 2012. Praktyczne przykłady

MoveToEx(hdc,x1,y1,NULL);
Ellipse(hdc,x1-bok,y1-bok,x1+bok,y1+bok);
}
if (SendMessage(hComboBox,CB_GETCURSEL, 0,0)==2) {
MoveToEx(hdc,x1,y1,NULL);
LineTo(hdc,x1+bok,y1+bok);
LineTo(hdc,x1+bok,y1);
LineTo(hdc,x1,y1);
}
//przemieszczanie figury
x1=x1+dx;y1=y1+dy;
//odbicie od krawędzi
if ((x1+bok>(okno.right-okno.left)) || (x1<0)) dx=-dx;
if ((y1+bok>(okno.bottom-okno.top)) || (y1<0)) dy=-dy;

EndPaint(hWnd, &ps);
break;

Zauważ, że rozmiar prawej i dolnej krawędzi okna musi być w układzie współrzędnych
związanych z oknem; pisałem już o tym przed przykładem. Współrzędne okna pobieramy
funkcją GetClientRect(). Struktura RECT zawiera współrzędne lewego górnego rogu okna
(składowe top i left) i prawego dolnego rogu (składowe bottom i right) w układzie
współrzędnych o początku w lewym górnym rogu pulpitu. Na szczęście, łatwo wyli-
czyć z tego rozmiary okna w pikselach. Rozmiar poziomego boku okna otrzymamy,
odejmując współrzędne początku poziomego od współrzędnych końca. Podobnie
rozmiar poziomego boku otrzymamy, odejmując współrzędne początku pionowego
boku od współrzędnych końca.

A zatem każde wywołanie tak napisanego kodu obsługi powinno dać przesunięcie figury
w oknie i sprawdzenie warunków odbicia. Pozostaje jeszcze zaprogramowanie cykliczne-
go generowania zdarzenia WM_PAINT. Za to będzie odpowiedzialny timer.

Chcemy, aby timer włączał się po zaznaczeniu pola wyboru. Znajdź więc w obsłudze ko-
munikatu WM_COMMAND kod, który generował odpowiedni komunikat w oknie tekstowym
po zaznaczeniu pola.
case 1000:
if (SendMessage(hButton, BM_GETCHECK, 0,0)==BST_CHECKED)
SendMessage(hEdit,WM_SETTEXT,0,(LPARAM)_T("Zaznaczony"));
if (SendMessage(hButton, BM_GETCHECK, 0,0)==BST_UNCHECKED)
SendMessage(hEdit,WM_SETTEXT,0,(LPARAM)_T("Nie zaznaczony"));
break;

Zamiast komunikatów o zaznaczeniu pola będziemy tworzyć lub kasować timer.


Zmień powyższy kod następująco:
case 1000:
if (SendMessage(hButton, BM_GETCHECK, 0,0)==BST_CHECKED)
SetTimer(hWnd,1,20,NULL);
if (SendMessage(hButton, BM_GETCHECK, 0,0)==BST_UNCHECKED)
KillTimer(hWnd,1);
break;
Rozdział 8. ♦ Podstawowe kontrolki w działaniu aplikacji WinAPI 163

Pozostało tylko zaprogramowanie tego, że przy każdym wystąpieniu zdarzenia WM_TIMER


należy generować zdarzenie WM_PAINT. Nie robimy tego bezpośrednio (np. funkcją Send-
Message()), tylko za pomocą funkcji InvalidateRect().

W instrukcji switch(message) funkcji okna WndProc() umieścimy procedurę obsługi


nowego zdarzenia. Możesz to zrobić na przykład przed obsługą WM_PAINT.
case WM_TIMER:
{
InvalidateRect(hWnd, NULL, TRUE);
}
case WM_PAINT: // ta linia istnieje

Skompiluj i uruchom program. Na liście wyboru możesz wybrać figurę, a pole wyboru
uruchamia animację. Zauważ, że jeśli zmienisz wymiary okna, animacja będzie działała
poprawnie.
164 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 9.
Budowa aplikacji .NET
w trybie wizualnym

Od WinAPI do .NET Framework


Poznałeś podstawy programowania Windows z WinAPI. Jest to podstawowy schemat
pisania programów dla tego systemu. Z różnych powodów dobudowano na nim dodat-
kowe biblioteki, które mają upraszczać programowanie. Takich bibliotek jest wiele (np.
QT, wxWidgets) i chyba można powiedzieć, że .NET Framework też jest taką biblioteką.
Ponieważ jednak został napisany przez producenta systemu, zajmuje specjalne miejsce
wśród innych bibliotek.

Korzyścią ze stosowania tych dodatkowych bibliotek jest zwykle prostsze i bardziej intu-
icyjne pisanie programów. Dodatkowo wiele środowisk programowania typu IDE jest
przygotowanych pod konkretną bibliotekę i umożliwia budowanie aplikacji z gotowych
elementów. W Visual C++ taką możliwość mamy w trybie budowy aplikacji .NET
Framework. Używamy wtedy języka C++/CLI, który ma być oddzielnym językiem,
choć ma bardzo wiele wspólnego z C++.

Okno w trybie wizualnym


Już wiadomo z rozdziału 1. (przykład 1.4), że po uruchomieniu Visual C++ i utworzeniu
nowego projektu w trybie wizualnym główną część ekranu zajmuje puste okno aplikacji.
W oknie będziemy osadzać komponenty, z których będzie się składała aplikacja. Jest
to więc kolejne ułatwienie: już nie tylko mamy do dyspozycji gotowe klasy obiektów
kontrolek, ale nie trzeba nawet definiować tych obiektów. Wystarczy przenieść kon-
trolki z panelu Toolbox, a odpowiednie definicje same się napiszą. Kontrolki będą się
komunikowały ze sobą i z systemem za pomocą zdarzeń. Zdarzenia mogą powstawać
w samych komponentach lub być generowane przy udziale użytkownika, na przykład
166 Microsoft Visual C++ 2012. Praktyczne przykłady

wtedy, kiedy użytkownik kliknie komponent. Porównując to z WinAPI, zdarzenia są ja-


kąś obudową mechanizmu komunikatów. Główna praca programisty w trybie wizualnym
Visual C++ polega na napisaniu metod, które są wywoływane przez zdarzenia. Jeżeli
na przykład użytkownik kliknie przycisk, powstaje zdarzenie kliknięcia — zadaniem pro-
gramisty jest napisanie, jakie instrukcje program ma wykonać, gdy zajdzie to zdarzenie.

Zarówno główne okno, jak i komponenty są obiektami klas. Wynika z tego, że mają pew-
ne właściwości, np. kolor, wielkość i inne, zależne od rodzaju komponentu. Można
także wykonywać na nich pewne funkcje, czyli działania, również zależne od rodzaju
komponentu. Okno jest obiektem klasy Form; może to być główne okno aplikacji, ale
inne są również obiektami tej klasy. Nie jest to dla nas nic nowego — w WinAPI okna
też były obiektami klas i miały właściwości, na przykład style okna były właściwościami.

Przykład 9.1

Utwórz nową aplikację i zmień wielkość i tytuł okna głównego.

Rozwiązanie
Utwórz nowy projekt C++/CLI, jak w przykładzie 1.4. Kliknij górny pasek głównego
okna aplikacji, którą przed chwilą utworzyłeś, jak na rysunku 9.1.

Rysunek 9.1.
Puste główne okno
aplikacji

W pionowym pasku z zakładkami przy prawej krawędzi okna środowiska kliknij Pro-
perties. Pojawi się panel z właściwościami okna. Jest ich sporo; te najczęściej wykorzy-
stywane zestawiłem w tabeli 9.1. W tym przykładzie interesują nas dwie właściwości:
Size do zmiany wielkości okna i Text do zmiany tytułu okna. Size znajdziesz w dziale
Layout, a Text w Appearance.

Tabela 9.1. Podstawowe właściwości okna aplikacji


Właściwość Opis
AcceptButton Przycisk, który zostanie naciśnięty, kiedy użytkownik naciśnie klawisz Enter.
Wybór za pomocą listy rozwijanej spośród przycisków umieszczonych w oknie.
AutoScaleMode Ustawienie trybu automatycznego skalowania okna. Wartość Font oznacza skalowanie
w zależności od czcionki systemowej, wartość Dpi oznacza skalowanie według
rozdzielczości ekranu, None oznacza brak skalowania, natomiast Inherit powoduje,
że okno jest skalowane tak jak okno rodzica, do którego ono należy. Ponieważ
okna główne nie mają rodzica, ustawienie opcji Inherit powoduje wyłączenie
automatycznego skalowania.
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 167

Tabela 9.1. Podstawowe właściwości okna aplikacji (ciąg dalszy)


Właściwość Opis
AutoSize Wartość true oznacza, że okno będzie dopasowywało swoje wymiary tak,
aby zmieściły się wszystkie osadzone w nim elementy.
AutoSizeMode Tryb działania funkcji AutoSize. GrowOnly oznacza, że okno może samo zwiększać
swoje rozmiary, ale nie może się zmniejszać poniżej pierwotnych rozmiarów.
GrowAndShrink oznacza, że okno może się zwiększać lub zmniejszać.
BackColor Kolor tła okna.
BackgroundImage Ta właściwość umożliwia ustawienie dowolnego obrazu (bitmapy) jako tła okna.
CancelButton Podobnie jak AcceptButton, z tym że tu można w oknie zdefiniować przycisk, który
zostanie wciśnięty po naciśnięciu klawisza Esc na klawiaturze.
ContextMenuStrip Zawiera odnośnik do menu, jakie zostanie wyświetlone po kliknięciu okna prawym
klawiszem myszy.
ControlBox Kontroluje, czy okno będzie zawierało na pasku przyciski Minimalizuj,
Maksymalizuj, Zamknij oraz ikonę aplikacji.
Cursor Określa rodzaj kursora po wejściu wskaźnika myszy nad okno.
FormBorderStyle Rodzaj obramowania okna.
Icon Ikona w lewym górnym rogu okna. Musi być w formacie .ICO.
IsMdiContainer Określa, czy okno jest oknem głównym aplikacji MDI.
Location Współrzędne górnego lewego rogu okna.
MainMenuStrip Określa główne menu aplikacji.
MaximizeBox Decyduje o wyświetlaniu przycisku Maksymalizuj w prawym górnym rogu okna.
MaximumSize Rozmiar okna po wciśnięciu przycisku Maksymalizuj.
MinimizeBox Decyduje o wyświetlaniu przycisku Minimalizuj w prawym górnym rogu okna.
MinimumSize Rozmiar okna po wciśnięciu przycisku Minimalizuj.
Opacity Przezroczystość okna w procentach: 100% — okno nieprzezroczyste, 0% — okno
całkowicie przezroczyste (niewidoczne).
ShowIcon Włącza lub wyłącza ikonę w lewym górnym rogu okna.
ShowInTaskbar Określa, czy aplikacja jest wyświetlana na pasku zadań.
Size Szerokość i wysokość okna.
StartPosition Pozycja okna w momencie uruchomienia aplikacji. Możliwe ustawienia to: Manual
— pozycja zapisana we właściwości Location, CenterScreen — okno będzie
wyświetlane na środku ekranu, CenterParent — okno wyświetlane w środku okna
nadrzędnego aplikacji, WindowsDefaultLocation — wyświetla okno w domyślnej
pozycji określonej w systemie, WindowsDefaultBounds — okno jest wyświetlane
w domyślnej pozycji i ma domyślne wymiary.
Text Tytuł okna.
TopMost Ustawienie tej właściwości na true powoduje, że okno będzie zawsze wyświetlane
na wierzchu innych okien.
TransparencyKey Definiuje kolor, który powoduje miejscową przezroczystość okna. Mapę
przezroczystości nakładamy, posługując się własnością BackgroundImage. Ta
właściwość może służyć do tworzenia okien o kształtach innych niż prostokąt.
168 Microsoft Visual C++ 2012. Praktyczne przykłady

Jak widać, wiele z tych właściwości odpowiada stylom okna, jakie mieliśmy w WinAPI.
Tak też okno jest prawdopodobnie tworzone. Ustawienie właściwości w panelu po-
woduje generację odpowiedniej postaci funkcji CreateWindow(), tyle że wszystko to
robi za nas środowisko i tego nie widać.

Wyszukaj w panelu po prawej wpis Size. W prawej rubryce widać dwie liczby rozdzielo-
ne średnikiem — jeżeli nic nie zmieniałeś, będzie to 300;300. Jest to szerokość i wy-
sokość okna w pikselach. Kliknij te liczby i wpisz dowolne inne wartości, pamiętając
o średniku. Po zatwierdzeniu wpisu klawiszem Enter zmienią się rozmiary okna.

Wpis Size ma obok znak plus; klikając go, rozwijamy pola, w których można od-
dzielnie wpisywać wysokość i szerokość okna.

Teraz szukamy wpisu Text — w prawej rubryce znajdziemy napis Form1, można go
zmienić na dowolny inny. Po zatwierdzeniu klawiszem Enter Twój napis pojawi się jako
tytuł okna aplikacji.

Program można skompilować i uruchomić poprzez Debug/Start Debugging lub F5 —


uruchomi się wówczas puste okno aplikacji zmienione według Twojego życzenia.

Okno aplikacji wcale nie musi być prostokątne. Choć w większości poważnych progra-
mów lepiej pozostać przy klasycznym kształcie, możliwe jest stworzenie okien bardzo
fantazyjnych. Aby zmienić kształt, potrzebna będzie maska w postaci mapy bitowej,
którą można stworzyć w dowolnym programie graficznym; ja posłużę się systemowym
Paintem.

Przykład 9.2

Utwórz okno aplikacji o owalnym kształcie.

Rozwiązanie
Utwórz nowy projekt, jak w przykładzie 1.4. Zapamiętaj (lub zapisz) nazwę, którą
nadałeś projektowi w polu Name okna New Project.

Otwórz aplikację Paint: Start/Programy/Akcesoria/Paint. Utwórz nowy obraz, wybie-


rając polecenie Nowy z zakładki oznaczonej numerem 1 na rysunku 9.2.

Rysunek 9.2. Pasek narzędzi programu Paint


Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 169

Teraz zmienimy rozdzielczość obrazka, tak aby odpowiadała rozmiarom okna. Załóżmy,
że chcemy mieć okno o wymiarach 600×400 pikseli. Kliknij zakładkę Start, następnie
w panelu Obraz kliknij opcję Zmień rozmiar, zaznaczoną strzałką 2 na rysunku 9.2.
W oknie Zmiana rozmiaru i pochylanie kliknij Zmiana rozmiaru na Piksele. Poniżej
wyczyść okienko Zachowaj współczynnik proporcji. Teraz wpisz w pole W poziomie 600,
a do pola W pionie wpisz 400.

Aby narysować elipsę, wybierz owal z panelu Kształty (strzałka 3 na rysunku 9.2), na-
stępnie narysuj kształt, wypełniając nim możliwie całą szerokość rysunku. Kliknij na
ikonkę kubełka w środku górnego paska w panelu Narzędzia (strzałka 4 na rysunku 9.2).
Kliknij w środku kształtu, wypełniając go kolorem.

Rysunek kolorowej elipsy na białym tle zapisujemy w katalogu utworzonego projektu


Visual C++. Plik zapisujemy jako obraz BMP — maska.bmp. Plik należy zapisać jako
obraz BMP bez kompresji, ponieważ artefakty z kompresji spowodują, że efekt będzie
nieestetyczny.

Teraz w środowisku Visual C++ kliknij pasek głównego okna budowanej aplikacji. We
właściwościach okna odszukaj BackgroundImage, w dziale Apperance kliknij tę właści-
wość, a następnie przycisk po prawej stronie jej wartości. W oknie odszukaj plik
maska.bmp i podwójnie kliknij jego nazwę. Obraz elipsy pojawi się w oknie aplikacji.

Teraz musimy dopasować wymiary okna do wymiarów obrazu. W tym celu odszukaj
właściwość Size i zmień jej wartość na 600;400.

Teraz decydujący moment — będziemy chcieli, aby wszystko, co jest białym obszarem,
stało się przezroczyste. Odszukujemy właściwość TransparencyKey (dział Window Style)
i używając listy rozwijanej, ustawiamy jej wartość na White.

Niestety, przy próbie kompilacji projektu napotkamy problem.

Jest to związane z własnym szablonem projektu wygenerowanym na podstawie innego


projektu w podrozdziale „Zaginiony projekt” w rozdziale 1. Chodzi o to, że przestrzeń
nazw, w której znajduje się klasa formularza, musi mieć taką samą nazwę jak projekt
(w pliku z przykładami PR_9_2). Inaczej występują problemy z dostępem do zasobów.
Ponieważ mapa bitowa z elipsą znajduje się w zasobach, projekt nie będzie działał.
Zmienimy więc ręcznie nazwę przestrzeni nazw.

Z menu File środowiska VC++ 2012 wybierz opcje Open/File. Pojawi się okno otwarcia
pliku. Jeśli utworzyłeś szablon projektu według instrukcji z rozdziału 1., to w folderze
projektu znajdziesz plik WindowsFormApplication1.cpp (patrz przykład 1.A). Jest to
główny plik aplikacji z funkcją main().

Otwórz go. Początkowe linie zawartości powinny wyglądać tak:


// WindowsFormApplication1.cpp : main project file.

#include "stdafx.h"
#include "Form1.h"

using namespace WindowsFormApplication1;


170 Microsoft Visual C++ 2012. Praktyczne przykłady

W dyrektywie using namespace trzeba zmienić nazwę tak, aby odpowiadała nazwie
projektu, którą miałeś zapamiętać, przykładowo:
using namespace PR_9_2;

W pliku Form1.h trzeba będzie zmienić nazwę przestrzeni. Zamiast


#pragma once
namespace WindowsFormApplication1 {

należy wpisać nazwę projektu, tę samą co w dyrektywie using.


#pragma once
namespace PR_9_2 {

Po kompilacji ukaże się główne okno w postaci elipsy. Niestety, zawiera ono ramkę,
która psuje efekt.

Ramkę można wyłączyć poprzez ustawienie właściwości FormBorderStyle na None,


jednak wtedy stracimy możliwość przesuwania okna kursorem myszy, a nawet zamykania
okna. Przycisk do zamykania okna może być jednak w prosty sposób dodany; zrobimy
to w dalszym ciągu rozdziału.

Komponenty umieszczane w oknie mogą być widoczne lub niewidoczne. Widoczne to ta-
kie, które widzi użytkownik, na przykład przycisk czy okno tekstowe. Niewidoczne to
takie, które są w aplikacji, ale wie o nich tylko programista; może to być na przykład
Timer. Komponenty często zwane są kontrolkami. Zaczniemy od podstawowych kompo-
nentów widocznych. W tym rozdziale opiszę przyciski Button, pola tekstowe TextBox,
etykiety Label i przycisk opcji RadioButton oraz pole wyboru CheckButton.

W tabeli 9.2 są zestawione właściwości kontrolek, które będziemy wykorzystywać,


a w tabeli 9.3 najczęściej wykorzystywane zdarzenia generowane przez te kontrolki.
Wybrałem te właściwości, które są wspólne dla wielu typów kontrolek.

Tabela 9.2. Najczęściej wykorzystywane właściwości komponentów


Właściwość Opis
BackColor Kolor tła komponentu.
Font Czcionka używana w kontrolce.
Enabled Decyduje, czy kontrolka jest aktywna. W przypadku ustawienia na wartość false
komponent będzie widoczny, ale nie będzie można wykonać na nim żadnych
operacji.
Image Służy do ustawienia bitmapy jako tła. Za pomocą tej właściwości można na przykład
zrobić przyciski z rysunkami.
ImageAlign Sposób wyświetlania bitmapy określonej przez właściwość Image.
Size Wymiary kontrolki.
Text Treść napisu na komponencie.
Visible Decyduje, czy komponent jest widoczny w oknie.
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 171

Tabela 9.3. Podstawowe zdarzenia


Zdarzenie Moment wystąpienia
Click Generowane przy kliknięciu komponentu.
Paint Zachodzi, kiedy Windows odświeża kontrolkę lub okno, na przykład po zakryciu
kontrolki przez inne okno i ponownym jej odkryciu.
Resize Występuje przy zmianie wymiarów kontrolki lub okna.
TextChanged Generowane przy zmianie zawartości kontrolek, które mogą przyjmować tekst,
na przykład TextBox.

Przyciski
Przyciski są obiektami klasy Button. W momencie naciśnięcia przycisku wywoływane jest
zdarzenie Click. Zwykle wykorzystujemy je do uruchomienia jakiejś metody. Zdarzenia
dotyczące jakiegoś komponentu zestawione są w tabeli po prawej stronie, podobnie
jak właściwości. Aby przejść do tabeli zdarzeń, należy kliknąć ikonę błyskawicy nad
tabelą właściwości.

Przykład 9.3

Utwórz aplikację z pojedynczym przyciskiem kończącym jej działanie.

Rozwiązanie
Utwórz projekt jak w przykładzie 1.4.

W prawym oknie, które pokazuje kod programu lub graficzny widok okna, przejdź do
zakładki Form1.h [Design], czyli do znanego już widoku okna tworzonej aplikacji.

Kliknij pionową zakładkę Toolbox przy prawej krawędzi ekranu lub z menu View wybierz
opcję Toolbox — ukaże się okno z komponentami. W dziale Common Controls
znajdź komponent Button.

Kliknij ten komponent, a następnie kliknij w oknie budowanej aplikacji, tam, gdzie
chcesz umieścić przycisk. W oknie pojawi się przycisk.

Teraz trzeba powiązać naciśnięcie tego przycisku z zamknięciem aplikacji. Kliknij po-
dwójnie wstawiony przed chwilą przycisk. Środowisko przeniesie Cię do kodu aplikacji,
a kursor znajdzie się w metodzie:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
}

W momencie gdy podwójnie kliknąłeś przycisk, została utworzona metoda button1_Click()


i przypisana do zdarzenia Click. Oznacza to, że będzie ona wykonywana zawsze, kiedy
w aplikacji użytkownik naciśnie ten przycisk.
172 Microsoft Visual C++ 2012. Praktyczne przykłady

W prosty sposób wykonałeś dwie czynności. Po pierwsze, utworzyłeś przycisk, co


w WinAPI wymagało funkcji CreateWindow() z odpowiednimi parametrami lub odwoła-
nia się do pliku zasobów. Po drugie, zaprogramowałeś wywołanie funkcji button1_Click()
w chwili naciśnięcia przycisku. W WinAPI wywołanie to umieściłbyś w procedurze
obsługi komunikatu WM_COMMAND, jeśli parametr wParam towarzyszący temu komunikatowi
wskazywałby, że został naciśnięty przycisk.

Działanie aplikacji kończymy przez zamknięcie jej głównego okna. Robimy to za pomocą
metody Close() klasy Form. Wpisujemy odpowiednie wywołanie do funkcji button1_
Click().
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Close();
}

Dodamy jeszcze odpowiedni napis na przycisku. Przejdź do zakładki Form1.h [Design]


i kliknij raz nasz przycisk. Kliknij na znaną już zakładkę Properties przy prawej kra-
wędzi okna Visual C++. Z prawej strony okna środowiska pojawią się właściwości
kontrolki, podobnie jak omawiane wcześniej właściwości okna. Znajdź właściwość
Text, wpisz w polu jej wartości słowo Koniec i naciśnij Enter. Budowana aplikacja po-
winna wyglądać tak, jak na rysunku 9.3. Położenie przycisku może być inne, jednak
nie jest to istotne.

Rysunek 9.3.
Aplikacja z jednym
przyciskiem

Aplikacja jest już gotowa, teraz ją skompiluj i uruchom za pomocą Debug/Start Debugging
lub F5. Po uruchomieniu kliknij przycisk Koniec, a aplikacja zakończy działanie. W ten
sposób możesz dodać przycisk zamykania aplikacji w przypadku braku ramki, jak
w przykładzie z owalnym oknem.

Po zakończeniu działania aplikacji możesz się przekonać, że metoda button1_Click()


jest rzeczywiście przypisana do zdarzenia Click. Aby to zrobić, kliknij przycisk w oknie
budowy aplikacji, a następnie rozwiń jeszcze raz zakładkę Properties i kliknij ikonę
błyskawicy nad tabelą właściwości komponentu. Przejdziesz do tabeli zdarzeń dla nasze-
go przycisku. Odszukaj zdarzenie Click, a zobaczysz przyporządkowaną do niego
metodę button1_Click().
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 173

Etykiety
Wyświetlanie tekstu w oknach aplikacji najłatwiej zrealizować poprzez etykiety (Label).
Są to obiekty klasy Label. Najczęściej wykorzystywaną właściwością etykiet jest wła-
ściwość Text; to za jej pomocą ustawiamy tekst wyświetlany przez kontrolkę. Za po-
mocą właściwości Font możemy natomiast kontrolować rodzaj i wielkość czcionki
użytej do wyświetlania napisu.

Przykład 9.4

Napisz program, w którym po naciśnięciu przycisku wyświetli się napis Visual C++.

Rozwiązanie
Utwórz nowy projekt według przykładu 1.4 i wstaw do okna przycisk Button z panelu
Toolbox.

Teraz przenieś na powierzchnię okna etykietę Label — znajdziesz ją w dziale Common


Controls, pięć kontrolek poniżej komponentu Button.

Kliknij wstawioną przed chwilą etykietę, przejdź do prawego panelu z właściwościami


i odnajdź właściwość Text.

Zmień jej wartość z label1 na puste pole i naciśnij Enter.

Podobnie jak w poprzednim przykładzie, musimy napisać metodę obsługującą zdarzenie


Click przycisku, tym razem jednak metoda nie ma powodować zakończenia progra-
mu, tylko wyświetlenie tekstu w etykiecie. Tekst do wyświetlenia trzeba podstawić
pod właściwość Text.

Kliknij podwójnie wstawiony przycisk i wpisz w metodę button1_Click() instrukcję


podstawienia do właściwości Text komponentu label1.
label1->Text="Visual C++";

Cała metoda powinna wyglądać tak:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
label1->Text="Visual C++";
}

Uruchom program. Po naciśnięciu przycisku pojawi się napis Visual C++.

Mamy tu operator ->; w C++ jest to operator odwołania do składowych klas typu wskaź-
nikowego. Ponieważ komponenty są obiektami klas, ich właściwości są składowymi
klas i będziemy się do nich odwoływać, używając tego operatora.
174 Microsoft Visual C++ 2012. Praktyczne przykłady

Odmianą zwykłej etykiety jest etykieta przeznaczona specjalnie do wyświetlania i prze-


chowywania adresów URL, czyli odnośników (LinkLabel). Jej klasa to LinkLabel. Umoż-
liwia ona wstawianie odnośników do aplikacji i zaznaczanie kolorami tych, które użyt-
kownik odwiedził. Kolory odnośnika można przypisać dowolnie. Kolor odnośnika przed
odwiedzeniem zawarty jest we właściwości LinkColor, a kolor odnośnika odwiedzonego
we właściwości VisitedLinkColor.

To, czy użytkownik odwiedził dany odnośnik, jest natomiast przechowywane we wła-
ściwości LinkVisited, przy czym wartość true oznacza odnośnik odwiedzony.

Przykład 9.5

Umieść w oknie aplikacji link do strony www.helion.pl. Po kliknięciu tego odnośnika


strona ma się otwierać w domyślnej przeglądarce, a odnośnik ma się zaznaczać jako
odwiedzony.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw w okno etykietę do odno-
śników typu LinkLabel; znajdziesz ją jak zwykle w oknie Toolbox.

Znajdź właściwość Text wstawionej etykiety i nadaj jej wartość www.helion.pl.

Teraz kliknij podwójnie etykietę w oknie, tak samo jak klikałeś przycisk — zostaniesz
przeniesiony do pliku Form1.h z kodem programu, gdzie już została utworzona metoda
linkLabel1_LinkClicked(). Metoda ta już została przypisana do zdarzenia LinkClicked
etykiety. To zdarzenie wystąpi zawsze, kiedy przyszły użytkownik Twojej aplikacji
kliknie odnośnik. Metoda linkLabel1_LinkClicked() powinna zatem uruchamiać prze-
glądarkę z odpowiednim adresem.

Aby uruchomić domyślną przeglądarkę, trzeba skorzystać z klasy Process, kontrolującej


programy uruchomione w systemie. Użyjemy metody Start() tej klasy, która uruchamia
procesy. Jej wywołanie będzie wyglądało tak:
System::Diagnostics::Process::Start(linkLabel1->Text);

Wpisujemy je między nawiasy klamrowe metody linkLabel1_LinkClicked(). Całość


wygląda tak:
private: System::Void linkLabel1_LinkClicked(System::Object^ sender,
System::Windows::Forms::LinkLabelLinkClickedEventArgs^ e) {
System::Diagnostics::Process::Start(linkLabel1->Text);
}

Argumentem metody Start() jest tekst z etykiety linkLabel1, czyli adres URL. To wy-
starczy do uruchomienia domyślnej przeglądarki i wczytania odpowiedniej strony.

Chcemy jeszcze, aby odnośnik po kliknięciu zaznaczył się jako odwiedzony. Osiągniesz
to przez ustawienie właściwości LinkVisited na true bezpośrednio po otwarciu prze-
glądarki. Kompletna metoda linkLabel1_LinkClicked() będzie miała postać:
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 175

private: System::Void linkLabel1_LinkClicked(System::Object^ sender,


System::Windows::Forms::LinkLabelLinkClickedEventArgs^ e) {
System::Diagnostics::Process::Start(linkLabel1->Text);
linkLabel1->LinkVisited=true;
}

Możesz teraz uruchomić aplikację i kliknąć odnośnik — otworzy się odpowiednia


strona, a tekst odnośnika zmieni kolor.

Pola tekstowe
Etykiety służyły wyłącznie do wyświetlania tekstu, natomiast za pomocą pól teksto-
wych (TextBox) można zarówno wprowadzać tekst z klawiatury, jak i wyświetlać go
w jednej lub wielu liniach. Pola tekstowe są obiektami klasy TextBox. Komponent ten,
oprócz standardowych własności, ma dużo właściwości specyficznych i ułatwiających
pracę z tekstem. Najważniejsze właściwości tej klasy przedstawia tabela 9.4.

Tabela 9.4. Podstawowe właściwości kontrolki TextBox


Właściwość Opis
Text Zawiera cały tekst aktualnie znajdujący się w polu.
Lines Tabela łańcuchów znakowych, z których każdy zawiera jedną linię tekstu.
Pierwsza linia ma indeks 0.
Multiline Kontroluje, czy okno zawiera jedną czy więcej linii.
PasswordChar Znak, który jest wstawiany zamiast litery, kiedy okno służy do wpisywania hasła.
TextAlign Wyrównanie tekstu w polu. Wartość Left oznacza wyrównanie do lewej, Right do
prawej, a Center do środka.
SelectedText Zawiera zaznaczony tekst.

Z poziomu programu tekst można wprowadzać, podstawiając go pod właściwość Text lub
korzystając z metody AppendText(), operującej na obiekcie pola tekstowego. Metoda
AppendText() dopisuje tekst na końcu już istniejącego, natomiast podstawiając pod
właściwość Text, zamieniamy istniejący tekst na podstawiany. Wyświetlanie napisu przez
zmianę wartości właściwości Text jest identyczne jak dla komponentu etykiety. W przy-
kładzie zajmiemy się wpisywaniem tekstu za pomocą metody AppendText().

Przykład 9.6

Wyświetl tekst w kontrolce typu TextBox.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 , a następnie wstaw w okno przycisk
Button oraz komponent TextBox. Komponent TextBox znajdziesz w tej samej kategorii
Common Controls co przycisk Button, tylko nieco niżej.
176 Microsoft Visual C++ 2012. Praktyczne przykłady

Pole tekstowe wklejone w okno ma tylko jedną linię, można jednak zmienić je na pole
z wieloma liniami. Po kliknięciu pola odszukaj właściwość Multiline i zmień jej wartość
na true. Następnie „złap” myszą za biały kwadracik pod wybranym polem tekstowym
i pociągnij w dół. Pole rozszerzy się tak, że będzie możliwe wpisanie kilku linii.

Kliknij podwójnie komponent przycisku. Otworzy się okno kodu programu ze znaną
już metodą button1_Click(), która od teraz będzie obsługiwać zdarzenie naciśnięcia
przycisku button1. Ponieważ chcemy, aby naciśnięcie wpisywało tekst, metoda but-
ton1_Click() powinna z kolei powodować działanie na kontrolce metody AppendText().
Argumentem metody AppendText() jest tekst do wpisania. Tekst to łańcuch znakowy
typu System::String, który może być sumą kilku łańcuchów. Na przykład jeśli chcemy
uzyskać dodawanie każdego tekstu w nowej linii, to do łańcucha z tekstem dodamy
stałą System::Environment::NewLine, która jest znakiem przejścia do nowej linii. Oto linia
do wpisania między nawiasy klamrowe button1_Click():
textBox1->AppendText("Visual C++"+ System::Environment::NewLine);

Teraz można już uruchomić aplikację. Każde naciśnięcie przycisku dopisze do pola
tekstowego napis Visual C++.

Wprowadzanie danych do aplikacji


za pomocą pól tekstowych
Tekst do pól TextBox można wprowadzać z klawiatury podczas działania aplikacji. Wpi-
sany tekst najprościej jest pobrać poprzez odczytanie właściwości Text pola tekstowego
po wpisaniu z klawiatury.

Przykład 9.7

Napisz program wyświetlający w etykiecie Label tekst wpisany przez użytkownika.

Rozwiązanie
Utwórz projekt aplikacji według przykładu 1.4 i wstaw do okna jeden przycisk Button,
jedno pole tekstowe TextBox oraz jedną etykietę Label.

Kliknij pole tekstowe i w tabeli właściwości ustaw właściwość Multiline na true.

Teraz, podobnie jak w poprzednim przykładzie, rozszerz wstawione pole tekstowe


tak, aby obejmowało kilka linii.

Teraz zajmiemy się przyciskiem; trzeba napisać metodę, która w momencie jego naci-
śnięcia przepisze tekst z pola tekstowego do etykiety. Po prostu musimy przepisać wła-
ściwość Text z pola tekstowego do właściwości Text etykiety.
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 177

Kliknij podwójnie komponent Button, a następnie wpisz w metodę button1_Click()


linię:
label1->Text=textBox1->Text;

Cała metoda będzie wyglądała tak:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
label1->Text=textBox1->Text;
}

Teraz uruchom aplikację. Wpisz cokolwiek do pola tekstowego, a następnie naciśnij


przycisk. Napis pojawi się w etykiecie.

Zauważ, że cały tekst w etykiecie jest wyświetlany w jednej linii. Jeśli wpiszesz długi tekst,
to wyjdzie poza okno. Aby tego uniknąć, można wyłączyć opcję AutoSize etykiety.

Zakończ działanie aplikacji przez naciśnięcie przycisku X w prawym górnym rogu


okna. Kliknij etykietę label1 w widoku projektowania okna aplikacji, w panelu Pro-
perties ustaw właściwość AutoSize na False. Teraz możesz określić wymiary etykiety
we właściwości Size.

Uruchom program powtórnie. Wpisz dowolny tekst w okno i naciśnij przycisk. Tekst
będzie zawijany tak, aby nie przekroczyć podanych wymiarów etykiety. Jeśli tekst się
nie zmieści, to zostanie zwiększona wysokość etykiety.

Za pomocą opcji File/SaveAll zapisz projekt; posłuży on do następnych ćwiczeń.

Poprzez właściwość Text można pobrać jedynie całą zawartość pola tekstowego. Możli-
wości oferowane przez tę kontrolkę są jednak znacznie większe — można na przykład
pobrać tekst z określonej linii.

Przykład 9.8

Wyświetl w etykiecie tekst z wybranej linii pola tekstowego.

Rozwiązanie
Otwórz aplikację z przykładu 9.7 za pomocą opcji File/Open/Project Solution.

Aby wyświetlić zawartość pojedynczej linii, należy skorzystać z właściwości Lines,


tak jak to napisałem w tabeli 9.4. Aby przejść do kodu programu, kliknij zakładkę
Form1.h. W metodzie button1_Click() zamiast kodu wyświetlającego całą zawartość
pola tekstowego:
label1->Text=textBox1->Text;

wpisz kod wyświetlający pierwszą linię:


label1->Text=textBox1->Lines[0];
178 Microsoft Visual C++ 2012. Praktyczne przykłady

Liczba w nawiasie kwadratowym jest indeksem tablicy Lines zawierającej kolejne li-
nie, przy czym numeracja linii zaczyna się od zera.

Po uruchomieniu programu wpisz do pola kilka linii tekstu, kończąc każdą klawiszem
Enter, następnie naciśnij przycisk. Zawartość pierwszej linii pojawi się w etykiecie.

Zmień indeks we właściwości Lines i zobacz efekty.

Zachowaj aplikację do następnego przykładu.

Wprowadzanie danych z konwersją typu


Wszystkie znaki wprowadzone w pole tekstowe są typu System::String. Jest to łań-
cuch znakowy. Również liczby traktowane są jako tekst, co oznacza, że nie można wyko-
nywać na nich działań arytmetycznych. Jeżeli chcemy wykorzystać pole tekstowe do
wprowadzania danych liczbowych, musimy dokonać konwersji.

Co ciekawe, metody konwertujące nie są częścią klasy komponentu TextBox, ale czę-
ścią klasy typu, do którego chcemy konwertować dane. W .NET mamy bardzo silnie
zaznaczone podejście obiektowe, nawet typy danych takie jak liczba całkowita czy
zmiennoprzecinkowa to obiekty odpowiednich klas. Tabela 9.5 przedstawia odpowiadają-
ce sobie typy danych w .NET Framework i „zwykłym” C++.

Tabela 9.5. Typy danych w .NET Framework i odpowiadające im typy w C++


Typ w .NET Typ w C++ Opis
Framework
System::Int32 int Liczba całkowita 32-bitowa ze znakiem.
System::Boolean bool Wartość logiczna prawda lub fałsz.
System::Single float Liczba zmiennoprzecinkowa o pojedynczej precyzji.
System::Double double Liczba zmiennoprzecinkowa o podwójnej precyzji.
System::Char char Typ znakowy, 8-bitowa liczba całkowita wartości od –128 do 127.
System::UInt32 unsigned int Liczba całkowita 32-bitowa bez znaku.
System::Void void Wartość nieokreślona.

Do konwersji z łańcucha znakowego służy metoda Parse(), która jest funkcją skła-
dową wszystkich klas typów numerycznych przedstawionych w tabeli 9.5. Metody tej
nie wywołujemy na obiekcie klasy, jakim jest zmienna, ale jako metodę statyczną.

Oto przykład:

Przykład 9.9

Uzupełnij aplikację z przykładu 9.8 tak, aby można było wybrać linię do odczytu podczas
działania programu.
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 179

Rozwiązanie
Otwórz aplikację z przykładu 9.8.

Przenieś na okno dodatkowe pole tekstowe TextBox — otrzyma ono nazwę textBox2.
W to pole będziemy wpisywać numer linii do wyświetlenia. Ten numer trzeba zamienić
na zmienną całkowitą typu System::Int16 i podać jako indeks tablicy Lines pola text-
Box1.

Jeśli w polu textBox1 podamy liczbę większą niż liczba linii, czyli rozmiar tablicy Lines[],
to nastąpi błąd. Zabezpieczymy się przed tym za pomocą instrukcji if. Porównamy
wartość z pola textBox2 z liczbą linii w polu textBox1 pobraną za pomocą właściwości
Length tabeli Lines.

Znajdź w kodzie programu znaną już metodę button1_Click() i zmień jej kod z:
label1->Text=textBox1->Lines[0];

na:
System::Int16 ShowLine = System::Int16::Parse(textBox2->Text);
if (ShowLine<textBox1->Lines->Length)
label1->Text=textBox1->Lines[ShowLine];

Uruchom aplikację, wpisz kilka linii tekstu w duże pole tekstowe, następnie wpisz
dowolną liczbę mniejszą od liczby linii, które wpisałeś; zawartość wybranej linii po-
jawi się w etykiecie.

Metoda statyczna to metoda związana nie z obiektem klasy, ale z samą klasą. Ozna-
cza to, że można jej używać, nie tworząc obiektu klasy.

Metody statyczne są bardzo rozpowszechnione w .NET i zapewne będziesz z nich korzy-


stał wiele razy. Dostęp do metod statycznych klasy zapewnia operator ::. Z lewej strony
operatora wymieniamy nazwę klasy, a z prawej nazwę metody. Ten operator stosujemy
też, jeśli odwołujemy się do klasy w pewnej przestrzeni nazw; wtedy nazwa przestrzeni
jest po lewej stronie operatora, a klasy po prawej. Po nazwie klasy może być drugi ope-
rator :: i nazwa metody statycznej.

Wyświetlanie wartości zmiennych


Równie często jak potrzeba konwersji typu znakowego na liczbowy zachodzi potrzeba
operacji odwrotnej. Typowym przykładem jest wyświetlenie wartości zmiennej w polu
tekstowym lub etykiecie. Właściwość Text etykiety lub pola tekstowego jest typu
System::String, nie można więc bezpośrednio podstawić do niej liczby.

Do zamiany liczby na łańcuch znakowy służy metoda ToString(), dostępna w każdej


klasie zmiennych numerycznych.
180 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 9.10

Napisz program dzielący dwie liczby przez siebie i wyświetlający wynik.

Rozwiązanie
Utwórz projekt aplikacji według przykładu 1.4. Wstaw w okno etykietę Label i przycisk
Button.

Kliknij podwójnie przycisk i napisz metodę button1_Click().


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e)
{
System::Double i=1.2345678/3;
label1->Text=i.ToString();
}

Teraz uruchom aplikację. Po naciśnięciu przycisku zostanie wyświetlony wynik dzielenia.

Do tej pory mieliśmy do czynienia wyłącznie z obiektami deklarowanymi przez kompila-


tor, gdyż takimi obiektami są komponenty. Wszystkie komponenty są deklarowane
dynamicznie. Teraz zadeklarowałeś zmienną i w sposób statyczny. Metody operujące na
zmiennych zadeklarowanych w sposób statyczny wywołujemy operatorem . zamiast ->,
stąd takie wywołanie metody ToString(). O statycznej i dynamicznej deklaracji można
przeczytać w dalszej części książki.

Można sterować metodą ToString(), aby zamiana przebiegła w określony sposób.


Więcej o tym w dalszych rozdziałach.

Pole tekstowe
z maską formatu danych
Podczas wpisywania tekstu w pole TextBox może zdarzyć się pomyłka. Można na przy-
kład wpisać literę zamiast cyfry, co spowoduje błąd w metodzie konwertującej Parse()
i nieoczekiwane przerwanie działania programu. Jednym ze sposobów zabezpieczenia
się przed takimi niespodziankami jest wprowadzenie maski danych, czyli zezwolenia
na akceptowanie przez pole tekstowe tylko określonego porządku liter i (lub) cyfr.

Pole tekstowe z maską jest odmianą zwykłego pola TextBox i nosi nazwę MaskedTextBox.
W porównaniu ze zwykłym polem ma ono kilka dodatkowych właściwości, opisanych
w tabeli 9.6.
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 181

Tabela 9.6. Specyficzne właściwości komponentu MaskedTextBox


Właściwość Znaczenie
Mask Najważniejsza właściwość komponentu. Określa rodzaj maski do wprowadzania
znaków. Maska składa się z określonych symboli; znaczenie najczęściej
stosowanych jest następujące:
 Cyfra 0 oznacza, że w danym miejscu będzie można wprowadzić tylko cyfrę.
 Cyfra 9 oznacza, że w danym miejscu będzie można wprowadzić cyfrę lub spację.
 Litera L oznacza miejsce na literę.
 Znak ? oznacza miejsce na literę lub spację.
 Litera A daje możliwość wprowadzania litery lub cyfry.
Oprócz tego maska może zawierać znaki specjalne, takie jak -, /, ;, :, które są
wyświetlane jako separatory.
I tak na przykład maska na kod pocztowy to 00-000, a maska na wprowadzenie
daty może wyglądać tak: 00 LLL 0000 (dzień, 3 litery nazwy miesiąca i rok).
PromptChar Znak zastępczy, który pojawia się, gdy nie ma wpisanego znaku. Jeżeli na przykład
znakiem zastępczym będzie #, to podana wyżej maska do kodu pocztowego przed
wpisaniem cyfr będzie wyświetlana jako ##-###.
TextMaskFormat Określa, w jaki sposób zapisywany jest tekst do właściwości Text pola. Include
Literals oznacza, że wpisywane są tekst i znaki specjalne maski, takie jak w kodzie
pocztowym. Wartość Include Prompt powoduje, że do tekstu wpisywane są znaki
zastępcze maski. Exclude PromptAndLiterals powoduje, że nie są wpisywane ani znaki
specjalne, ani znaki zastępcze. Include PromptAndLiterals wpisuje wszystkie znaki.

Przykład 9.11

Napisz program wyświetlający teksty wpisywane w maski z różnymi opcjami.

Rozwiązanie
Utwórz projekt aplikacji według przykładu 1.4. Wstaw w okno cztery maskowane pola
tekstowe MaskedTextBox, cztery etykiety Label i jeden przycisk Button. Aplikacja może
wyglądać na przykład tak jak na rysunku 9.4.

Rysunek 9.4.
Aplikacja z polami
MaskedTextBox
182 Microsoft Visual C++ 2012. Praktyczne przykłady

Teraz w każdym polu MaskedTextBox ustawimy inną wartość właściwości TextMask-


Format i zobaczymy, jak wpływa to na wyświetlanie tekstu. Za każdym razem, kiedy
będę pisał o zmianie jakiejś właściwości kontrolki lub okna, powinieneś najpierw rozwi-
nąć panel Properties. Robi się to za pomocą pionowej zakładki w pasku przy prawej kra-
wędzi okna VC++. Kliknij pierwsze pole tekstowe od góry i w prawym panelu właściwo-
ści przestaw wartość jego właściwości TextMaskFormat na ExcludePromptAndLiterals.

Kliknij drugie pole i przestaw właściwość TextMaskFormat na IncludePrompt. W trzecim


polu jako wartość tej właściwości ustaw IncludeLiterals, a w czwartym IncludePrompt-
AndLiterals.

Czas na ustawienie w każdym polu maski wprowadzania liter. Załóżmy, że we wszyst-


kich polach chcemy wprowadzić datę w formacie: numer dnia, skrót nazwy miesiąca
i rok, na przykład 15 maj 2012, przy czym kolejne pola należy rozdzielić myślnikami tak,
że całość ma mieć postać: 15-maj-2012.

Zgodnie z tabelą 9.6 odpowiednia maska, którą musimy podstawić za właściwość Mask
wszystkich pól, będzie miała postać 00-LLL-0000. Klikając myszą kolejne pola, wpisz
ciąg znaków maski we właściwości Mask wszystkich pól.

Zauważ, że po wpisaniu maski i zatwierdzeniu klawiszem Enter w polu pojawia się


ciąg znaków __-___-____. Jest to reprezentacja maski za pomocą znaków zastępczych.
Domyślnie znak zastępczy (właściwość PromptChar) ustawiony jest na _.

Ostatnią rzeczą jest takie zaprogramowanie przycisku, aby przy naciśnięciu tekst z pól
był przepisywany do etykiet z boku każdego pola. Tekst pola pobierzemy za pomocą
właściwości Text.

Kliknij podwójnie przycisk w oknie i wpisz w metodę button1_Click() następujące


linie:
label1->Text=maskedTextBox1->Text;
label2->Text=maskedTextBox2->Text;
label3->Text=maskedTextBox3->Text;
label4->Text=maskedTextBox4->Text;

Aplikacja jest gotowa. Uruchom ją i wpisz datę w przewidzianej postaci we wszystkie


okna. W drugim i czwartym oknie możesz zostawić jedno lub więcej pól pustych (lub
zostawić znaki zastępcze), na przykład 15-maj-__12. Po wpisaniu naciśnij przycisk
w aplikacji.

W etykietach pojawią się dane z maskowanych pól tekstowych w różnych postaciach.


Tekst z górnej etykiety nie zawiera znaków dodatkowych ani zastępczych, a wyłącznie
datę, nierozdzieloną nawet spacją. W drugiej etykiecie też nie ma znaków dodatkowych,
ale pozostawiono znaki zastępcze tam, gdzie nic nie wpisałeś. Trzecie pole tekstowe
przekazało do etykiety tekst wraz ze znakami dodatkowymi (myślnikami), a czwarte
zarówno ze znakami dodatkowymi, jak i zastępczymi.

Użycie maskowanego pola tekstowego nie chroni przed wszystkimi błędami — można
na przykład podstawić bezsensowny numer lub wyraz z błędem.
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 183

Pola wyboru, przyciski opcji,


kontenery grupujące
Często zdarza się, że w aplikacjach potrzeba wyboru z kilku opcji działania programu
bądź zaznaczenia jakiegoś warunku. Służą do tego komponenty RadioButton i CheckBox.
Oba komponenty mają podobną funkcjonalność: składają się z opisu opcji i miejsca
na zaznaczenie myszą — w postaci kółka lub kwadratu. Przyjęło się, że RadioButton
stosuje się do wyboru jednej z kilku opcji, natomiast CheckBox do zaznaczania poje-
dynczego warunku lub opcji. Z kontrolką CheckBox mieliśmy już do czynienia w roz-
dziale o WinAPI. W trybie wizualnego projektowania zamiast wysyłać do kontrolki
komunikaty, można kontrolować jej stan za pomocą właściwości. Najczęściej używane
właściwości obu tych kontrolek przedstawia tabela 9.7.

Tabela 9.7. Właściwości kontrolek RadioButton i CheckBox


Wspólne właściwości
Checked Określa, czy zaznaczono daną opcję. Wartość true oznacza zaznaczoną opcję.
AutoCheck Po ustawieniu wartości true każde kliknięcie kontrolki w działającej aplikacji
zmienia jej stan na przeciwny (przełącznik).
CheckAlign Zmienia położenie pola zaznaczenia wyboru względem tekstu opcji.
Text Tekst kontrolki.

Ponieważ komponenty RadioButton (przycisk opcji) i CheckBox (pole wyboru) często


występują w grupach, duże znaczenie mają dla nich tak zwane kontenery. Są to kon-
trolki, dzięki którym zyskujemy wzajemne powiązanie elementów. Kontenery występują
w postaci kontrolek Panel i GroupBox, przedstawionych na rysunku 9.5.

Rysunek 9.5.
Porównanie kontrolek
GroupBox i Panel

Mają one podobne właściwości, jedynie GroupBox umożliwia wstawienie nazwy grupy
w górnej części kontrolki. Obie kontrolki umożliwiają uzyskanie różnych stylów ra-
mek oraz kolorów tła. Grupując elementy RadioButton w kontenerze, można uzyskać
zaznaczenie zawsze tylko jednego elementu bez pisania dodatkowego kodu. Kontenerem
jest także samo okno aplikacji, a więc jedną grupę przycisków opcji można uzyskać
bez użycia kontenerów. Zachęcam jednak do ich używania, nawet w przypadku jednej
grupy, bo aplikacja wygląda estetyczniej.
184 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 9.12

Napisz prosty kalkulator czterodziałaniowy, używając pól tekstowych TextBox, przyci-


sków Button i przycisków opcji RadioButton.

Rozwiązanie
Utwórz projekt aplikacji według przykładu 1.4 i umieść w nim dwa pola tekstowe TextBox
i jeden przycisk Button. Będą potrzebne jeszcze cztery przyciski opcji RadioButton, które
zgrupujemy w kontenerze GroupBox. Dodaj najpierw komponent GroupBox, a do niego
wstaw cztery przyciski RadioButton. Wynik działania będzie prezentowany w etykiecie
Label. Po włączeniu kalkulatora powinno się w niej ukazać zero, taką cyfrę ustaw
więc w jej właściwości Text za pomocą panelu Properties.

Aby wynik był lepiej wyeksponowany, zmienimy czcionkę etykiety na większą. Znajdź
właściwości Font etykiety i kliknij napis Font, tak aby się podświetlił. Teraz kliknij
przycisk z trzema kropkami po prawej stronie właściwości zaznaczony strzałką na ry-
sunku 9.6.

Rysunek 9.6.
Przycisk do
wywoływania okna
zmiany czcionki

Zobaczysz standardowe okno wyboru czcionki. Zmień rozmiar na 16 i kliknij OK. Wła-
ściwość Font mają też kontrolki RadioButton. Tam w taki sam sposób zwiększyłem
rozmiar czcionki z 8 na 12.

Opisy opcji, przycisku, kontenera oraz okna aplikacji uzyskasz, zmieniając odpowiednio
właściwości Text komponentów i okna głównego. Całość powinna wyglądać jak na
rysunku 9.7.

Rysunek 9.7.
Kalkulator
Rozdział 9. ♦ Budowa aplikacji .NET w trybie wizualnym 185

Oczywiście, kalkulator umożliwia wykonanie tylko jednego działania w danej chwili,


czyli tylko jeden z przycisków opcji RadioButton może być zaznaczony. Ponieważ
jednak są w jednej grupie, nie musimy się tym martwić, bo tak właśnie będzie.

Pozostało tylko zaprogramowanie tego, aby aplikacja wykonała działania w momencie


naciśnięcia przycisku Licz. Przejdź z powrotem do widoku projektowania aplikacji,
a następnie naciśnij podwójnie przycisk Licz. Do metody obsługującej zdarzenie Click
przycisku wpisz kod, który w zależności od tego, jaka kontrolka RadioButton jest zazna-
czona, pobiera liczby z okien TextBox, wykonuje działanie i wpisuje wynik do etykiety
Label.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
if (radioButton1->Checked)
label1->Text=
(Double::Parse(textBox1->Text)+Double::Parse(textBox2->Text)).ToString();
if (radioButton2->Checked)
label1->Text=
(Double::Parse(textBox1->Text)-Double::Parse(textBox2->Text)).ToString();
if (radioButton3->Checked)
label1->Text=
(Double::Parse(textBox1->Text)*Double::Parse(textBox2->Text)).ToString();
if (radioButton4->Checked)
label1->Text=
(Double::Parse(textBox1->Text)/Double::Parse(textBox2->Text)).ToString();
}

Kalkulator jest gotowy; po uruchomieniu aplikacji wpisz liczby do pól tekstowych,


następnie wybierz działanie i naciśnij klawisz Licz.
186 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 10.
Menu i paski narzędzi

Rodzaje menu
W Visual C++ 2012 mamy do dyspozycji dwa rodzaje menu. Pierwszy jest reprezentowa-
ny przez klasę MenuStrip, za pomocą której możemy budować główne menu. Jest to menu
na górze okna, dobrze znane z każdej aplikacji Windows. Drugi rodzaj menu reprezen-
tuje klasa ContextMenuStrip. Jest to menu kontekstowe, które może być stowarzyszo-
ne z jakąś kontrolką. Normalnie jest ono niewidoczne, a pojawia się dopiero po kliknięciu
kontrolki prawym przyciskiem myszy. Środowisko VC++ daje użytkownikowi naprawdę
duże możliwości projektowania niekonwencjonalnych i funkcjonalnych menu. Menu
zawiera elementy określające poszczególne opcje. Elementy te są polami wspomnianych
już klas MenuStrip lub ContextMenuStrip. Choć są polami klasy, to same też są typu klasy.
Jeśli reprezentują zwykłe opcje, jest to klasa ToolStripMenuItem. Menu może jednak
zawierać także pola tekstowe TextBox i listy rozwijane ComboBox. Menu w VC++ buduje
się w trzech etapach. Najpierw umieszczamy w oknie obiekt MenuStrip lub Context-
MenuStrip, następnie dodajemy kolejne opcje menu, a na końcu przypisujemy do zdarze-
nia kliknięcia każdej z opcji metody, podobnie jak to było w przypadku przycisków.

Komponent MenuStrip
Zarówno sam obiekt MenuStrip, jak i pojedyncza opcja menu ToolStripMenuItem mają
określone właściwości. Tabele 10.1 i 10.2 przedstawiają podstawowe właściwości tych
komponentów. Elementy TextBox i ComboBox, które też mogą być składnikami menu,
już opisywałem.
188 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 10.1. Wybrane właściwości komponentu MenuStrip


Właściwość Opis
BackColor Określa kolor paska menu.
Dock Miejsce wyświetlania paska menu. Może on być wyświetlany klasycznie
na górze, ale także na dole lub z boków, a jeżeli wybierzemy opcję None,
pasek można przesunąć w dowolne miejsce okna aplikacji.
Items Ta właściwość opisuje wszystkie opcje w pasku menu. Kliknięcie
przycisku z prawej strony tej właściwości otwiera edytor, w którym
można dodawać elementy menu.
ShowItemToolTips Wartość true oznacza, że po najechaniu kursorem myszy na opcje menu
będą wyświetlane objaśnienia. Tekst objaśnienia dla danej opcji zawiera
właściwość ToolTipText w obiekcie ToolStripMenuItem.
TextDirection Określa sposób wyświetlania tekstu opcji w menu. Horizontal oznacza
tekst poziomy, a Vertical90 i Vertical270 obrócony o dany kąt.

Tabela 10.2. Niektóre właściwości kontrolki ToolStripMenuItem


Właściwość Opis
Checked Ustawienie na true powoduje, że opcja w menu jest zaznaczona znakiem 9.
CheckOnClick Wartość true określa, że przy każdym wybraniu tej opcji zmieniany jest
jej stan zaznaczenia 9.
DisplayStyle Mówi o wyglądzie opcji menu. Text oznacza, że wyświetlany jest tekst
opcji, Image powoduje wyświetlenie tylko obrazka przyporządkowanego
do opcji, a ImageAndText obu elementów.
DropDownItems Zawiera listę wszystkich komponentów w menu podrzędnym w stosunku
do aktualnej opcji. Podobnie jak w pasku głównym, w menu podrzędnym
też mogą znajdować się kontrolki ToolStripMenuItem, TextBox i ComboBox
oraz separator w postaci poziomej linii.
Font Określa czcionkę menu.
Image Definiuje obrazek z lewej strony opcji menu.
ShortcutKeyDisplayString Opis skrótu klawiszowego danej opcji do wyświetlenia w menu.
ShortcutKeys Definicja skrótu klawiszowego
ShowShortcutKeys Wartość true oznacza, że opis skrótu klawiszowego ma być wyświetlany.
TextDirection Kierunek wypisywania tekstu w opcji. Horizontal oznacza tekst poziomy,
a Vertical90 i Vertical270 obrócony o dany kąt.

Przykład 10.1

Zbuduj aplikację z menu z następującymi opcjami:


Plik\Otwórz Narzędzia\Ołówek Pomoc\O programie…
Plik\Zamknij Narzędzia\Gumka
Plik\Wyjdź Narzędzia\Pędzel
Rozdział 10. ♦ Menu i paski narzędzi 189

Rozwiązanie
Utwórz nowy projekt aplikacji.

W oknie umieść komponent MenuStrip, znajdziesz go jak zwykle w zakładce Toolbox,


w dziale Menus & Toolbars.

W pierwsze okno w pasku menu wpisz słowo Plik, następnie myszką rozwiń menu
w dół, tak jak w każdej aplikacji. Pojawi się nowe miejsce na wpisanie opcji; wpisz Otwórz
(rysunek 10.1).

Rysunek 10.1.
Tworzenie menu
głównego

W taki sam sposób utwórz kolejne opcje w menu Plik, a następnie pozostałe.

Tutaj, niestety, pojawia się pewien problem: nazwy komponentów składowych menu
tworzone są od opisu opcji. I tak na przykład komponent opisujący opcję Wyjdź nazywa
się wyjdźToolStripMenuItem. Nazwy obiektów i zmiennych w C++ nie powinny zawierać
polskich liter, gdyż prowadzi to do błędów. Trzeba więc zmienić nazwę każdego elementu
menu, którego opis zawiera polskie litery.

Nazwa komponentu jest zapisana we właściwości (Name). Po kliknięciu na opcji Wyjdź


w trybie projektowania aplikacji znajdź tę właściwość i zmień wartość na wyjdzTool-
StripMenuItem („z” zamiast „ź”).

Pozostaje jeszcze przyporządkowanie opcjom w menu metod. W tym programie będzie


działać tylko opcja Plik/Wyjdź. Kliknij podwójnie opcję Wyjdź. Otworzy się metoda
wyjdzToolStripMenuItem_Click(), która została już przypisana do zdarzenia naciśnięcia
tej opcji w menu. Działa to identycznie, jak w przypadku przycisków.

W tę metodę wpisz nazwę metody kończącej działanie programu, czyli


Close();

Całość wygląda tak:


private: System::Void wyjdzToolStripMenuItem_Click(System::Object^ sender,
System::EventArgs^ e) {
Close();
}

Teraz skompiluj aplikację. Menu powinno rozwijać się prawidłowo, z tym że działa
tylko opcja Wyjdź, ponieważ tylko do niej przypisaliśmy metodę.
190 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 10.2

Napisz aplikację, w której nazwy wybranych opcji menu będą wyświetlane w etykiecie.
Wszystkie opcje mają być obsługiwane przez tę samą metodę.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4. W pustym oknie aplikacji umieść
pasek menu MenuStrip oraz etykietę Label.

Pierwszą opcję menu nazwij Napisz, a jako jej podmenu utwórz opcje Opcja1 i Opcja2.
Stosując nazewnictwo z przykładu 10.1, powinieneś mieć menu o opcjach:
Napisz/Opcja1
Napisz/Opcja2

Teraz kliknij opcję Opcja1 podwójnie, tak samo jak klikałeś opcję Wyjdź w przykładzie
10.1. Utworzy się metoda opcja1ToolStripMenuItem_Click() i zostanie przyporządkowa-
na do zdarzenia Click dla tej opcji menu (czyli dla tego komponentu ToolStripMenuItem).
private: System::Void opcja1ToolStripMenuItem_Click(System::Object^ sender,
System::EventArgs^ e) {
}

Metoda ta ma dwa argumenty, którymi się do tej pory nie zajmowaliśmy. Pierwszy z nich,
sender, identyfikuje komponent, który wywołał tę metodę, czyli w tym przypadku
komponent Opcja1. Ponieważ parametr sender jest typu object, trzeba go przekonwer-
tować na typ ToolStripMenuItem. W hierarchii klas .NET klasa Object jest bazową klasą
dla ToolStripMenuItem. Będzie to więc rzutowaniem w dół. Z takim rzutowaniem
trzeba zawsze uważać, jednak tutaj wiemy, między jakimi typami rzutujemy, więc jest
to względnie bezpieczne. Ja użyłem tu rzutowania static_cast. Problem z rzutowaniem
w dół parametru sender występuje wielokrotnie przy obsłudze zdarzeń w .NET Frame-
work. Spędza on sen z oczu wielu programistom, a niektórzy starają się go unikać, jak
potrafią. Po konwersji z obiektu ToolStripMenuItem można pobrać właściwość Text,
w której ukryta jest jego nazwa, i wyświetlić ją w etykiecie. Oto odpowiedni kod:
private: System::Void opcja1ToolStripMenuItem_Click(System::Object^ sender,
System::EventArgs^ e) {
label1->Text=static_cast<ToolStripMenuItem^>(sender)->Text;
}

Aby wyświetlić nazwę opcji Opcja2, wystarczy przyporządkować do jej zdarzenia Click
tę samą metodę. Aby to zrobić, przejdź do zakładki budowy aplikacji Form1.h[Design],
a następnie rozwiń budowane menu Napisz i kliknij jeden raz opcję Opcja2.

Otwórz panel Properties za pomocą pionowej zakładki z prawej strony okna VC++,
przełącz się na widok zdarzeń (ikona błyskawicy) i odnajdź zdarzenie Click. Z listy roz-
wijanej z prawej strony nazwy zdarzenia wybierz metodę opcja1ToolStripMenuItem_
Click(). Teraz, kiedy klikniesz odpowiednią opcję, parametr sender przekaże, która
opcja została wybrana, i pojawi się jej nazwa.
Rozdział 10. ♦ Menu i paski narzędzi 191

Możesz już uruchomić aplikację i zobaczyć, jak działa. Zapisz aplikację, rozwiniemy
ją w następnym przykładzie.

Oprócz elementów ToolStripMenuItem menu może zawierać także listy rozwijane Combo-
Box i tekstowe pola TextBox. Dodawanie tych elementów do menu odbywa się za pomocą
specjalnego edytora. Liczba i rodzaj komponentów podrzędnych dla danego kompo-
nentu znajdują się we właściwości DropDownItems.

Właściwość Items głównego paska menu opisuje wszystkie komponenty znajdujące


się na tym pasku. Właściwość DropDownItems każdego z tych komponentów opisuje
elementy podrzędne w stosunku do niego.

Edytor menu dla danego komponentu włączamy poprzez wybranie tego komponentu
myszką, otwarcie panelu Properties, odnalezienie właściwości Items dla paska menu
lub DropDownItems dla komponentu i kliknięcie przycisku z prawej strony tej właści-
wości. Wygląd edytora przedstawia rysunek 10.2. W lewej górnej części okna znaj-
duje się lista rozwijana z komponentami, które można zainstalować w menu. Dodanie
komponentu odbywa się przez wybranie go z listy i naciśnięcie przycisku Add. Poni-
żej mamy aktualną listę komponentów w menu. Po wybraniu komponentu jego wła-
ściwości ukazują się w prawym oknie.

Rysunek 10.2.
Edytor menu

Przykład 10.3

Utwórz teraz bardziej skomplikowane menu, zawierające komponenty TextBox i ComboBox,


tak jak na rysunku 10.3. Niech wybranie pozycji z listy rozwijanej ComboBox powoduje
wypisanie nazwy tej pozycji w etykiecie, a wpisanie tekstu do pola TextBox również
wypisanie tego tekstu.
192 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 10.3.
Menu z różnymi
komponentami

Rozwiązanie
Do wykonania tego zadania będzie potrzebna aplikacja z poprzedniego przykładu.

Najpierw do istniejącego menu Napisz dodamy komponent ComboBox, pasek podziału


i pole TextBox. Aby to zrobić, kliknij raz opcję Napisz w projektowanym menu głównym,
a następnie w panelu właściwości znajdź w dziale Data właściwość DropDownItems,
wybierz ją myszką i kliknij przycisk z prawej strony.

Otworzy się edytor menu. Z górnej listy rozwijanej wybierz ComboBox i naciśnij Add, na-
stępnie wybierz Separator i naciśnij Add, a na końcu TextBox i wciśnij Add. Odpowied-
nie kontrolki zostaną dodane do menu. Teraz kliknij OK, zamykając edytor menu.
Kontrolkę listy ComboBox zapełnimy teraz opcjami tak, aby lista nie była pusta i żeby
było w czym wybierać.

Lista w ComboBox znajduje się we właściwości Items tej kontrolki, kliknij więc kontrolkę,
a później w panelu właściwości znajdź Items i przyciskiem wielokropka ( ) otwórz
edytor listy. Pozycje na liście to łańcuchy tekstowe (String). Każda linia w oknie edyto-
ra to nowa pozycja; wpisz kilka wyrazów, za każdym razem przechodząc do nowej linii
klawiszem Enter. Treść i liczbę pozycji na liście pozostawiam Twojej wyobraźni.

Teraz „podepniemy” istniejącą już metodę pod nowe pozycje w menu tak, żeby wszystkie
korzystały z tego samego kodu. Podstawowym zastosowaniem „zwykłej” opcji menu
jest kliknięcie na nią, dlatego metoda obsługi wyzwalana była zdarzeniem Click. Dla
listy rozwijalnej ComboBox taką podstawową operacją jest wybór opcji z listy, natomiast
dla pola tekstowego wpisanie tekstu. Metodę obsługi dla listy podłączymy do zdarzenia
SelectedIndexChnaged, zachodzącego przy zmianie wybranej opcji na liście. Rozwiń me-
nu w widoku projektowania aplikacji, kliknij pojedynczo na kontrolkę ComboBox w menu,
następnie jeśli potrzeba przełącz się w prawym panelu na widok zdarzeń i znajdź zdarze-
nie SelectedIndexChanged. Kliknij na nazwę zdarzenia i rozwiń listę po prawej. Zobaczysz
tam metodę opcja1ToolStripMenuItem_Click(), obsługującą już zdarzenia kliknięcia
w zwykłe opcje menu. Wybierz ją z listy. Dla pola tekstowego sytuacja jest bardziej
skomplikowana. Metodę obsługi można podłączyć do zdarzenia TextChanged, ale ono
zachodzi po każdej zmianie tekstu, a więc będzie zachodziło w trakcie pisania. Spowoduje
Rozdział 10. ♦ Menu i paski narzędzi 193

to szybką reakcję na wpisywany tekst, ale nie zawsze jest pożądane. Czasem lepiej,
aby metoda była wyzwalana dopiero po napisaniu całego tekstu. Można to zrobić,
podłączając ją pod zdarzenie Click, jak dla zwykłej opcji menu. Wtedy najpierw wpisu-
jemy tekst, a później klikamy myszką na pole. Ja proponuję takie rozwiązanie. Roz-
wiń zatem menu w widoku projektowania aplikacji, kliknij kontrolkę pola tekstowego
i w prawym panelu zdarzeń podłącz metodę opcja1ToolStripMenuItem_Click() do jej
zdarzenia Click.

Teraz musimy rozwinąć metodę opcja1ToolStripMenuItem_Click(). Do tej pory wszyst-


kie elementy menu były typu ToolStripMenuItem. Teraz jednak parametr sender może
być także typu listy lub pola tekstowego. Musimy dodać procedurę rozpoznawania
typu tego parametru i wypisywania właściwości Text z komponentu określonego typu.
Do określenia parametru obiektu sender użyjemy metody GetType(). Metoda ta zwraca
obiekt typu System::Type; we właściwości Name tego obiektu znajduje się łańcuch zna-
kowy z nazwą typu. Oto zmodyfikowana metoda opcja1ToolStripMenuItem_Click():
private: System::Void opcja1ToolStripMenuItem_Click(System::Object^ sender,
System::EventArgs^ e) {
if ((sender->GetType())->Name=="ToolStripMenuItem")
label1->Text=static_cast<ToolStripMenuItem^>(sender)->Text;

if ((sender->GetType())->Name=="ToolStripComboBox")
label1->Text=static_cast<ToolStripComboBox^>(sender)->Text;

if ((sender->GetType())->Name=="ToolStripTextBox")
label1->Text=static_cast<ToolStripTextBox^>(sender)->Text; }

Po kompilacji można wybrać z listy rozwijanej opcje do wypisania w etykiecie lub wpisać
w pole tekst, który również zostanie wypisany w etykiecie.

Menu podręczne
Menu podręczne jest to menu odnoszące się do konkretnego komponentu. Jest ono nie-
widoczne po uruchomieniu aplikacji i pojawia się dopiero po kliknięciu prawym przy-
ciskiem danego komponentu. Przyporządkowanie tego menu do komponentu odbywa
się za pomocą właściwości ContextMenuStrip. Tę właściwość zawiera większość kompo-
nentów, więc menu podręczne można przyporządkować prawie do każdego z nich.

Przykład 10.4

Napisz aplikację z menu podręcznym przyporządkowanym do pola TextBox. Po wy-


braniu opcji w menu nazwa tej opcji ma pojawiać się w polu. Po kliknięciu opcji Wyjdź
program powinien zakończyć działanie.
194 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do niego pole tekstowe
TextBox oraz komponent menu podręcznego ContextMenuStrip.

Wybierz myszką pole tekstowe i znajdź w panelu Properties pozycję ContextMenuStrip.


Lista rozwijana przy tej pozycji powinna zawierać menu podręczne, które dodałeś:
contextMenuStrip1. Wybierz tę pozycję z listy. Od tej chwili menu podręczne context-
MenuStrip1 jest przyporządkowane do pola tekstowego. Chcemy, aby pole TextBox mo-
gło wyświetlać wiele linii tekstu. Przy zaznaczonym polu znajdź w panelu Properties
właściwość Multiline i ustaw jej wartość na true. Możesz rozciągnąć pole przy uży-
ciu myszy, aby pomieściło więcej tekstu.

Teraz trzeba jeszcze zaprojektować samo menu i oprogramować wyświetlanie jego


opcji w polu. Pod oknem projektowanej aplikacji znajduje się pasek, gdzie wyświetlone
są komponenty, które nie są widoczne w oknie. Kliknij komponent contextMenuStrip1
(rysunek 10.4). Pojawi się menu, do którego możesz dodawać opcje, tak jak to robiłeś
w głównym menu aplikacji.

Rysunek 10.4.
Projektowanie
menu podręcznego

Dodaj do menu opcje Opcja1, Opcja2 i Opcja3 oraz Koniec, jak na rysunku 10.4.

Kliknij podwójnie opcję Opcja1, tworząc metodę jej obsługi: opcja1ToolStripMenuItem_


Click(). Podobnie jak w menu głównym, wykorzystamy jedną metodę do obsługi
wszystkich opcji w menu, posługując się parametrem sender. Metoda opcja1Tool-
StripMenuItem_Click() będzie zawierała tylko jedną linię kodu:
private: System::Void opcja1ToolStripMenuItem_Click(System::Object^ sender,
System::EventArgs^ e) {
textBox1->AppendText(static_cast<ToolStripMenuItem^>(sender)->Text);
}

Teraz wróć do zakładki projektowania aplikacji Form1.h [Design]. Kliknij pasek niewi-
docznych komponentów contextMenuStrip1, a następnie wybierz opcję Opcja2, przejdź
Rozdział 10. ♦ Menu i paski narzędzi 195

do panelu Properties, włącz widok zdarzeń i przyporządkuj do zdarzenia Click tę samą


metodę opcja1ToolStripMenuItem_Click().

To samo zrób z opcją Opcja3.

Teraz zajmiemy się opcją Koniec. Kliknij ją podwójnie, tworząc metodę koniecTool-
StripMenuItem_Click(). W tę metodę wpisz polecenie wyjścia z aplikacji Close();.
private: System::Void koniecToolStripMenuItem_Click(System::Object^ sender,
System::EventArgs^ e) {
Close();
}

Aplikacja jest już gotowa. Uruchom ją, a następnie kliknij pole tekstowe prawym przyci-
skiem myszy. Pojawi się menu, które zaprojektowałeś. Jeżeli wybierzesz teraz jakąś
opcję, jej nazwa zostanie doklejona do tekstu znajdującego się w polu.

Skróty klawiaturowe w menu


Bardzo łatwo przyporządkować opcjom menu skróty klawiaturowe. W tabeli 10.2 ma-
my trzy właściwości komponentu ToolStripMenuItem odnoszące się do skrótów menu.
Właściwości ShortcutKeyDisplayString i ShowShortcutKeys to, odpowiednio, tekst
skrótu do wyświetlenia, na przykład Alt+S, i flaga, której ustawienie decyduje, czy
ten tekst ma być wyświetlany. Właściwości te nie mają wpływu na działanie skrótu,
a jedynie na jego opis. Właściwa definicja klawiszy skrótu znajduje się we właściwości
ShortcutKeys.

Przykład 10.5

Napisz aplikację, w której będzie można zmieniać wielkość głównego okna poprzez
naciskanie klawiszy Alt+E (zwiększanie okna) i Alt+W (zmniejszanie okna).

Rozwiązanie
Utwórz projekt nowej aplikacji według przykładu 1.4 i wstaw do niej komponent menu
głównego MenuStrip.

Menu będzie miało tylko jedną opcję: Okno, zawierającą podmenu z opcjami Zwiększ
i Zmniejsz, jak na rysunku 10.5.

Stwórz więc strukturę menu:


Okno/Zwiększ
Okno/Zmniejsz

Ponieważ nazwa opcji Zwiększ zawiera literę „ę”, zmień jej nazwę we właściwości
Name na zwiekszToolStripMenuItem — bez polskiej litery (porównaj przykład 10.1).
196 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 10.5.
Menu ze skrótami
klawiaturowymi

Teraz w widoku projektowania aplikacji zaznacz opcję Zwiększ i otwórz panel Properties.
Do właściwości ShortcutKeyDisplayString wpisz ciąg znaków Alt+E, a następnie
sprawdź, czy właściwość ShowShortcutKeys jest ustawiona na true.

W dalszym ciągu jesteśmy we właściwościach opcji Zwiększ. Teraz zdefiniujemy skrót


klawiaturowy za pomocą własności ShortcutKeys. Wybierz ją myszą i kliknij przycisk
rozwijania listy z prawej strony własności. Pojawi się okno do definicji skrótów. Aby
zdefiniować skrót Alt+E, zaznacz pole wyboru przy Alt, a następnie z listy Key na dole
okna wybierz literę E i naciśnij Enter. Skrót jest przypisany.

Teraz powtórz ostatnie dwa kroki dla opcji Zmniejsz. Właściwość ShortcutKeyDisplay-
String ustaw na Alt+W, a w ShortcutKeys zaznacz Alt i wybierz z listy Key literę W.
Po tych operacjach okno aplikacji powinno wyglądać jak na rysunku 10.5.

Poprzez podwójne kliknięcie opcji Zwiększ utwórz metodę zwiekszToolStripMenuItem_


Click() obsługującą zdarzenie Click. Wpisz w nią polecenie zwiększania wielkości i sze-
rokości okna.
private: System::Void zwiekszToolStripMenuItem1_Click(System::Object^ sender,
System::EventArgs^ e) {
this->Height++;
this->Width++;
}

Użyliśmy tu obiektu this: jest to wskaźnik ustawiony na obiekt klasy, na którym wy-
wołujemy metodę. W tym przypadku menu jest składową klasy Form1 okna głównego,
a metoda zwiekszToolStripMenuItem_Click() metodą tej klasy. Zatem this wskazuje
na obiekt klasy Form1, czyli okno główne.

Utwórz teraz metodę obsługującą zdarzenie Click dla opcji Zmniejsz. Będzie ona
zmniejszała wymiary okna.
private: System::Void zmniejszToolStripMenuItem_Click(System::Object^ sender,
System::EventArgs^ e) {
this->Height--;
this->Width--;
}

Uruchom aplikację. Naciśnięcie Alt+E zwiększa okno, a Alt+W zmniejsza.


Rozdział 10. ♦ Menu i paski narzędzi 197

Paski narzędzi
Paski narzędzi są to znane z wielu aplikacji elementy grupujące najczęściej używane
opcje w celu ułatwienia dostępu. Zawierają one głównie przyciski w postaci ikon, ale
także listy rozwijane, etykiety i inne elementy. W VC++ 2012 paski są komponentami
typu ToolStrip. W pasku można umieszczać następujące elementy:
 Button — przycisk w postaci ikony.

 Label — etykieta.

 DropDownButton — przycisk, który po kliknięciu rozwija się w dodatkowe


menu z opcjami.
 SplitButton — połączenie przycisku Button i DropDownButton.

 Separator — pionowa kreska oddzielająca sekcje paska.

 ComboBox — lista rozwijana.

 TextBox — pole tekstowe.

 ProgressBar — pasek postępu wyświetlający stopień zaawansowania jakiegoś


procesu w skali od 0 do 100%.

Najważniejsze właściwości paska ToolStrip przedstawia tabela 10.3.

Tabela 10.3. Najważniejsze właściwości paska ToolStrip


Właściwość Opis
AutoSize Wartość true powoduje, że wielkość paska będzie dostosowana do rozmiarów
znajdujących się w nim elementów.
Dock Określa położenie paska: Top oznacza pasek u góry okna, Left, Right,
odpowiednio, z lewej lub prawej strony, a Bottom pasek u dołu okna. Opcja Fill
powoduje, że pasek wypełnia całe okno. None oznacza brak dokowania;
swobodny pasek w oknie.
ImageScalingSize Właściwość określająca, do jakiego rozmiaru mają być skalowane ikony
w komponentach (głównie chodzi o przyciski w pasku). Ponieważ wielkość
komponentów jest dostosowywana do wielkości ikon, a wielkość paska z kolei
do wielkości komponentów, właściwość tę można wykorzystać do zmiany
wielkości całego paska wraz z komponentami.
Items Zawiera wszystkie komponenty znajdujące się w pasku. Wybranie tej opcji
i kliknięcie przycisku wielokropka ( ) otwiera edytor komponentów, podobnie
jak w przypadku menu. Za pomocą tego edytora można dodawać i odejmować
komponenty.
Image Obiekt Bitmap z rysunkiem do wyświetlania na przycisku.
198 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 10.6

Za pomocą paska zadań przełączaj kolory tekstu w etykietach. Etykieta, w której prze-
łączany jest kolor, niech będzie wybierana za pomocą listy rozwijanej.

Rozwiązanie
Utwórz nową aplikację według przykładu 1.4 i wstaw do niej pasek ToolStrip. Kiedy
będziesz tworzył projekt, zapamiętaj (lub zapisz) jego nazwę wpisaną w pole Name okna
New Project, ponieważ będzie jeszcze potrzebna.

W pasku, po jego kliknięciu, domyślnie znajduje się obiekt typu SplitButton, składają-
cy się z przycisku po lewej stronie i trójkątnej strzałki po prawej. Każde kliknięcie
przycisku powoduje dodanie do paska komponentu toolStripButton, natomiast kliknięcie
strzałki rozwija listę wyboru z pozostałymi komponentami. Dodaj do paska trzy kom-
ponenty toolStripButton i jeden ComboBox. Niestety, przy próbie kompilacji projektu
napotykamy ten sam problem z dostępem do zasobów co przy oknie z elipsą w przy-
kładzie 9.2.

Tak samo jak tam zmienimy więc ręcznie nazwę przestrzeni nazw. Z menu File środo-
wiska VC++ 2012 wybierz opcje Open/File. Pojawi się okno otwarcia pliku. Jeśli utwo-
rzyłeś szablon projektu według instrukcji z rozdziału 1, to w folderze projektu znajdziesz
plik WindowsFormApplication1.cpp (patrz przykład 1.A). Jest to główny plik aplikacji
z funkcją main(). Otwórz go, początkowe linie zawartości powinny wyglądać tak:
// WindowsFormApplication1.cpp : main project file.

#include "stdafx.h"
#include "Form1.h"

using namespace WindowsFormApplication1;

W dyrektywie using namespace trzeba zmienić nazwę tak, aby odpowiadała nazwie
projektu, którą miałeś zapamiętać, na przykład:
using namespace PR_10_6;

W pliku Form1.h trzeba będzie zmienić nazwę przestrzeni. Zamiast


#pragma once
namespace WindowsFormApplication1 {

należy wpisać nazwę projektu, tę samą co w dyrektywie using.


#pragma once
namespace PR_10_6 {

Teraz przenieś z Toolbox na okno dwa komponenty Label.

Przyciski w pasku narzędzi będą służyły do zmiany koloru tekstu na czerwony, zielony
lub niebieski. Aby je oznaczyć, pokolorujemy je odpowiednio. Pierwszy przycisk
od lewej będzie do koloru czerwonego. Zaznacz go, a następnie wybierz jego właściwość
Rozdział 10. ♦ Menu i paski narzędzi 199

BackColor. Kliknij ikonę strzałki po prawej stronie tej właściwości, a następnie w oknie
wyboru koloru zakładkę Web i wybierz kolor czerwony (Red).

Do drugiego i trzeciego przycisku przyporządkuj kolory zielony (Green) i niebieski


(Blue).

Najpierw zaprogramujemy zmianę koloru jednej etykiety. Kliknij podwójnie pierwszy


przycisk w pasku (czerwony). Do utworzonej metody obsługującej zdarzenie Click
wpisz kod:
private: System::Void toolStripButton1_Click(System::Object^ sender,
System::EventArgs^ e) {
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton1")
label1->ForeColor=System::Drawing::Color::Red;
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton2")
label1->ForeColor=System::Drawing::Color::Green;
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton3")
label1->ForeColor=System::Drawing::Color::Blue;
}

Ta metoda będzie obsługiwała wszystkie przyciski; wykorzystamy znowu parametr


sender — rozpoznawanie przycisku odbywa się na podstawie jego nazwy.

Przyporządkuj tę metodę do obsługi zdarzeń Click dla pozostałych przycisków.

Możesz już uruchomić aplikację — pierwsza etykieta będzie zmieniała kolory zgodnie
z przyciskami.

Teraz zaprogramujemy zmianę etykiety za pomocą listy. W widoku projektowania aplikacji


kliknij listę na pasku, a następnie w panelu właściwości (Properties) znajdź właściwość
Items. Zaznacz ją, a następnie kliknij przycisk wielokropka ( ) z jej prawej strony.

Pojawi się okno definicji zawartości listy. Wpisz w pierwszej linii Etykieta 1, następnie
naciśnij Enter i w drugiej linii wpisz Etykieta 2.

Teraz pozostaje już tylko zmodyfikowanie metody toolStripButton1_Click() tak, aby


przełączała kolor odpowiedniej etykiety w zależności od pozycji wybranej na liście. Aktu-
alną pozycję wybraną na liście rozwijanej odczytamy z jej właściwości SelectedIndex.
Właściwość ta przechowuje numer aktualnie wybranej pozycji na liście. Pozycja na
górze listy ma numer zero.
private: System::Void toolStripButton1_Click(System::Object^ sender,
System::EventArgs^ e) {
if (toolStripComboBox1->SelectedIndex==0) {
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton1")
label1->ForeColor=System::Drawing::Color::Red;
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton2")
label1->ForeColor=System::Drawing::Color::Green;
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton3")
label1->ForeColor=System::Drawing::Color::Blue;
}
200 Microsoft Visual C++ 2012. Praktyczne przykłady

if (toolStripComboBox1->SelectedIndex==1) {
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton1")
label2->ForeColor=System::Drawing::Color::Red;
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton2")
label2->ForeColor=System::Drawing::Color::Green;
if (static_cast<ToolStripButton^>(sender)->Name=="toolStripButton3")
label2->ForeColor=System::Drawing::Color::Blue;
}
}

Po uruchomieniu aplikacja będzie działała zgo7dnie z założeniami. Zapisz ją, ponieważ


będzie jeszcze potrzebna.

Na przycisku toolStripButton można wyświetlać rysunek. Jest on skalowany do roz-


miarów określonych przez właściwość ImageScalingSize.

Przykład 10.7

Na przyciskach aplikacji z poprzedniego przykładu wyświetl rysunki z pliku graficznego.

Rozwiązanie
Zanim zaczniemy cokolwiek wyświetlać, potrzebujemy rysunków. Założyłem sobie,
że przycisk na pasku zwiększymy do rozmiarów 32×32 punkty. Rysunek, który będziemy
na nim wyświetlać, musi mieć ten sam rozmiar lub zostanie przeskalowany. Narysujemy
więc obrazek 32×32 punkty za pomocą Painta. Uruchom zatem Painta, rozwiń za-
kładkę zaznaczoną strzałką na rysunku 10.6, w której są opcje programu.

Rysunek 10.6.
Zakładka opcji
w aplikacji Paint

Wybierz opcję Właściwości; pojawi się okno Właściwości obrazu. Na dole masz dwa
pola: Szerokość i Wysokość, w oba wpisz liczbę 32. Naciśnij OK. Rysunek ma już po-
trzebne wymiary, możesz narysować na nim cokolwiek. Ponieważ jest mały, nie ma tu
zbyt wielkiego pola na rozwijanie talentu. Po narysowaniu kliknij jeszcze raz zakładkę
z rysunku 10.6 i wybierz opcję Zapisz jako. W pole Nazwa wpisz przyc1, a w polu Zapisz
jako typ wybierz TIFF (*.TIF; *.TIFF). Kliknij OK.

Jako folder zapisu trzeba wybrać folder, w którym znajduje się projekt aplikacji. Musi to
być ten sam folder, w którym znajduje się plik .cpp z kodem programu.

Teraz, kiedy mamy już obrazek, wstawimy go do przycisku. Otwórz aplikację z poprzed-
niego przykładu i kliknij pojedynczo na pasek toolStrip poza obszarem kontrolek,
zaznaczając go. Teraz otwórz panel Properties i sprawdź, czy w górnym pasku tego
panelu jest wybrany toolStrip1. Bardzo łatwo jest wybrać którąś z kontrolek na pasku,
a chodzi o właściwości samego paska. Znajdź właściwość ImageScalingSize i wpisz jako
Rozdział 10. ♦ Menu i paski narzędzi 201

jej wartość 32;32. Zamiast wpisywać dwie wartości, możesz kliknąć na nazwę właści-
wości, a następnie przyciskiem z plusem obok liczb rozwinąć oddzielne pola dla wy-
sokości (Height) i szerokości (Width).

Sam rysunek moglibyśmy podpiąć na stałe do przycisku za pomocą właściwości Image,


jednak proponuję wyświetlić go programowo. Tak czy inaczej, też skorzystamy z tej wła-
ściwości, ale jej wartość podstawimy w kodzie programu. Zróbmy to w funkcji urucha-
mianej zdarzeniem utworzenia okna aplikacji. W WinAPI musielibyśmy oprogramować

obsługę komunikatu WM_CREATE, natomiast w wizualnym trybie wystarczy, że klikniesz


podwójnie na oknie, a odpowiednia funkcja zostanie utworzona i podłączona do zdarze-
nia Load okna aplikacji. Od razu wpiszmy do niej instrukcję wczytania obrazka przyc1.tif:
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
toolStripButton1->Image=Bitmap::FromFile("przyc1.tif");
}

Funkcja FromFile() jest funkcją statyczną klasy Bitmap. Zwraca obiekt tej klasy, który
podstawiamy do właściwości Image przycisku. Możesz już uruchomić aplikację. Przycisk
będzie odpowiednio powiększony i będzie miał na sobie grafikę, którą wykonałeś.
Przykładowy wygląd masz na rysunku 10.7.

Rysunek 10.7.
Przykładowy wygląd
paska z graficznymi
przyciskami

W identyczny sposób możesz upiększyć pozostałe przyciski, uzyskując efektowny pasek.


202 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 11.
Tablice, uchwyty
i dynamiczne tworzenie
obiektów

Tablice
Składnia deklaracji tablic w C++/CLI jest inna niż w standardowym C++. Tablice są
obiektami szablonu klasy array. Szablon jest tu konieczny, ponieważ w różnych sytu-
acjach chcemy mieć tablice różnych obiektów. Tablice można tworzyć na dwa sposoby,
zależnie od tego, czy chcemy inicjalizować nową tablicę wartościami zaraz po utwo-
rzeniu, czy nie. Jeżeli tablica ma być wypełniona wartościami już na etapie tworzenia,
to definiujemy ją jak obiekt klasy z szablonem. Dla jednowymiarowej czteroelemento-
wej tablicy z liczbami typu System::Int32 będzie to:
array<int>^tabl = {2,3,4,5};

Tablica od razu została wypełniona liczbami; pierwszy jej element tabl[0]=2, drugi
tabl[1]=3, tabl[2]=4, a tabl[3]=5. Indeksowanie elementów w tablicach w C++ za-
czyna się od zera. Dla tablicy dwuwymiarowej z inicjalizacją:
array<int,2>^wymiar = {{2,3},{4,5}};

tabl[0,0]=2, tabl[0,1]=3, tabl[1,0]=4 i tabl[1,1]=5. Zauważ, że tablica dwuwymiarowa


jest tablicą jednowymiarową, której elementy są również tablicami jednowymiarowymi.

Deklarując tablice statycznie bez inicjalizacji (czyli zawierające zera), nie korzystamy
bezpośrednio z konstruktora klasy, ale z metody CreateInstance(). Argumentami tej me-
tody są typ elementów tablicy i jej wymiary. I tak na przykład definicja jednowymiarowej
tablicy liczb całkowitych typu System::Int32 o 10 elementach będzie mieć postać:
Array^ tabl = Array::CreateInstance(System::Int32::typeid,10);
204 Microsoft Visual C++ 2012. Praktyczne przykłady

Typ tablicy musi być podany jako parametr klasy System::Type, dlatego należy skorzy-
stać z operatora typeid zwracającego wartość System::Type dla typu, na którym go
wywołujemy. Deklaracja tablicy dwuwymiarowej 10 na 10 elementów ma postać:
Array^ tabl = Array::CreateInstance(System::Int32::typeid,10,10);

Dodając czwarty parametr, można utworzyć tablicę trójwymiarową; jeżeli potrzebnych


jest więcej wymiarów, trzeba je podać za pomocą pomocniczej tablicy. Oto deklaracja
tablicy czterowymiarowej 5 na 5 na 10 na 10:
array <int>^wymiar = {5,5,10,10};
Array^ tabl = Array::CreateInstance(System::Int32::typeid, wymiar);

Zauważ, że mamy tu dwie klasy: array i Array — jedna klasa pochodzi z przestrzeni
nazw cli, a druga z System. Tak naprawdę jednak funkcjonalność obiektów utworzo-
nych za pomocą obu klas jest taka sama. Na obu obiektach można wywoływać metody
klasy Array, udostępniające wiele operacji na tablicach, oraz odczytywać właściwości.
Zasadniczą różnicą jest to, że wartości tablicy zadeklarowanej przy użyciu pierwszego
sposobu można zapisywać i odczytywać bezpośrednio, natomiast składniki tablicy
zadeklarowanej z użyciem metody CreateInstance() należy odczytywać i zapisywać za
pomocą metod GetValue() i SetValue(). Zestawienie metod i właściwości tablic Array
przedstawiają tabele 11.1 i 11.2.

Tabela 11.1. Niektóre metody działające na tablicach Array


Metoda Działanie
AsReadOnly(Array tablica) Zwraca kopię aktualnej tablicy jako pomocniczą tablicę tylko do
odczytu.
Clear(Array tablica, Czyści (ustawia na zero) wartości w tablicy tablica, zaczynając od
int start, int długość) indeksu start, poprzez długość elementów.
Clone() Tworzy kopię tablicy. Jeżeli tablica zawiera referencje do
elementów, nowa tablica będzie zawierała referencje do tych
samych elementów (metoda nie tworzy ich kopii).
Copy (Array źródło, Array Kopiuje dane z tablicy źródło do tablicy cel, rozpoczynając od
cel, int długość) pierwszego indeksu, poprzez długość elementów. Druga postać tej
Copy(Array źródło, int metody przepisuje długość wartości danych z tablicy źródło,
indeks_źródło,Array cel, rozpoczynając od indeksu indeks_źródło, do tablicy cel i zapisuje
int indeks_cel, int długość) je tam, rozpoczynając od indeksu indeks_cel.
Find(Array tablica, Przeszukuje tablicę tablica zgodnie z warunkiem określonym w
Predicate znajdź) znajdź; znajdź jest metodą (predykatem), która zawiera algorytm
przeszukania elementów. Metoda Find() zwraca pierwszy element,
który spełnia kryteria.
FindAll(Array tablica, Działa tak samo jak Find(), ale zwraca tablicę zawierającą
Predicate znajdź) wszystkie elementy, które spełniają kryteria.
FindIndex(Array tablica, Działa tak samo jak Find(), z tym że nie zwraca elementu tablicy
Predicate znajdź) spełniającego warunki, a jego indeks.
GetEnumerator() Zwraca enumerator do tablicy, czyli typ o funkcjonalności
wskaźnika, który pozwala na iterowanie po elementach tablicy.
GetLength(int wymiar) Zwraca długość tablicy w podanym wymiarze wymiar.
Rozdział 11. ♦ Tablice, uchwyty i dynamiczne tworzenie obiektów 205

Tabela 11.1. Niektóre metody działające na tablicach Array (ciąg dalszy)


Metoda Działanie
Resize(Array tablica, Zmienia rozmiar tablicy tablica na nowy_rozmiar.
int nowy_rozmiar)
Reverse(Array tablica) Odwraca kolejność elementów w całej tablicy lub zaczynając
Reverse(Array tablica, od początek, przez długość elementów.
int początek, int długość)
SetValue(Object wartość, Wpisuje wartość wartość do elementu indeks.
int indeks)
GetValue(int indeks) Zwraca element o indeksie indeks.

Tabela 11.2. Niektóre właściwości klasy Array


Właściwość Opis
IsReadOnly Określa, czy tablica jest tylko do odczytu.
Lenght Rozmiar tablicy, dla tablic wielowymiarowych jest to całkowita liczba elementów.
Rank Liczba wymiarów.

Niektóre metody z tablicy 11.1 pobierają argument w postaci predykatu. Jest to funk-
cja, która pobiera parametry i zwraca wartość prawda lub fałsz, w zależności od stanu
tych parametrów. Funkcja taka może na przykład sprawdzać, czy podana liczba jest
liczbą pierwszą, i wtedy zwracać wartość true typu System::Boolean. Jeśli predykat
pobiera jeden argument, nazywamy go unarnym, jeśli dwa — binarnym.

Przykład 11.1

Zadeklaruj dwie tablice jednowymiarowe po 10 elementów: jedną z inicjalizacją, a drugą


za pomocą metody CreateInstance(), następnie wypełnij je liczbami od 1 do 10 i wypisz
te liczby. Odwróć kolejność elementów pierwszej tablicy, przepisz jej zawartość do
drugiej i znowu wypisz zawartość obu tablic.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w jej oknie przycisk
Button oraz pole tekstowe TextBox.

Do wpisywania wartości do tablicy deklarowanej za pomocą CreateInstance() wykorzy-


stamy metodę SetValue(), zaś do odwrócenia kolejności elementów metodę Reverse().
Kod realizujący te operacje wywoływany po naciśnięciu przycisku będzie miał postać:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
array <System::Int32>^ tablica1 = {0,0,0,0,0,0,0,0,0,0};
Array^ tablica2 = Array::CreateInstance(System::Int32::typeid,10);
for (System::Int32 i=0;i<10;i++) {
tablica1[i]=i;
tablica2->SetValue(i,i);
}
textBox1->AppendText("Tablice oryginalne"+System::Environment::NewLine);
206 Microsoft Visual C++ 2012. Praktyczne przykłady

wyswietl(tablica1,tablica2);
System::Array::Reverse(tablica1);
System::Array::Copy(tablica1,tablica2,tablica1->Length);
textBox1->AppendText("Tablice po operacjach"+System::Environment::NewLine);
wyswietl(tablica1,tablica2); }

Do wyświetlania zawartości tablic napisz metodę wyswietl() i umieść ją ponad metodą


button1_Click().
private: System::Void wyswietl(array<System::Int32>^ tab1,Array^ tab2) {
System::Int32 w=0;
do {
textBox1->AppendText(tab1[w].ToString()+" " + tab2->GetValue(w) +
System::Environment::NewLine);
w++;
} while (w<tab1->Length);
textBox1->AppendText(System::Environment::NewLine);
}

Teraz możesz już skompilować program. Po naciśnięciu przycisku powinien on działać


zgodnie z oczekiwaniami.

Dostęp do elementów tablicy


za pomocą enumeratora
Do odczytu zawartości tablic można wykorzystywać indeksy, ale można też wskazywać
komórki tablicy za pomocą enumeratora. Jest to wygodniejsze, jeśli nie znamy liczby
elementów tablicy. Enumeratory są to obiekty, którymi można przesuwać się po tabli-
cy i wskazywać jej elementy. Tworzymy je za pomocą metody GetEnumerator() wy-
woływanej na konkretnym obiekcie tablicy. Nie jest istotne, jakie elementy zawiera
tablica, zawsze zostanie utworzony właściwy enumerator. Możesz o tym pomyśleć jako
o utworzeniu wskaźnika do typu takiego, jakiego są elementy tablicy. Do przesuwania
enumeratorów służy metoda MoveNext(), która przesuwa enumerator na następny ele-
ment tablicy. Metoda zwraca wartość true w przypadku udanego przesunięcia i false,
jeśli wskazanie następnego elementu nie udało się, na przykład z powodu osiągnięcia
końca tablicy. Można ją więc wykorzystać do kontroli prawidłowości operacji. Element,
na który wskazuje aktualnie enumerator, odczytujemy za pomocą metody Current().

Przykład 11.2

Napisz program wyszukujący w tablicy wypełnionej liczbami od 1 do 10 liczby parzy-


ste, a następnie wypisujący te liczby oraz całą zawartość tablicy za pomocą enumeratora.
Rozdział 11. ♦ Tablice, uchwyty i dynamiczne tworzenie obiektów 207

Rozwiązanie
Będziemy potrzebowali aplikacji z przyciskiem i polem tekstowym, jak w poprzed-
nim przykładzie. Definicję tablicy i wypełnienie liczbami również zrealizujemy po-
dobnie jak wcześniej.

Do wyszukania liczb parzystych użyjemy metody FindAll(); wymaga ona napisania


predykatu, który zawiera algorytm przeszukiwania elementów tablicy. Musi on akcepto-
wać pojedynczy element tablicy i zwracać wartość typu System::Boolean oznaczającą,
czy dany element spełnia kryterium wyszukiwania. Nasza tablica ma wartości typu
System::Int32, przy czym poszukujemy elementów parzystych. Metoda będzie miała
następującą postać:
System::Boolean czy_parzysta(System::Int32 liczba) {
if ((liczba%2)||(liczba==0)) return false;
else return true;
}

Znak % oznacza dzielenie modulo. Metoda ta nie może być metodą klasy Form1, musisz
ją zatem wpisać poza definicją tej klasy, najlepiej bezpośrednio po dyrektywach using
namespace.

Najpierw tworzymy dwie tabele: tabela1 i tabela2. Do pierwszej wpisujemy kolejne


liczby całkowite od 1 do 10. Następnie przygotowujemy parametry do metody FindAll().
Przekazanie predykatu czy_parzysta() do metody FindAll() odbywa się za pomocą
obiektu typu Predicate. Obiekt ten musi być tworzony dynamicznie, stąd użycie ope-
ratora gcnew, które wyjaśnię późnej. Metoda FindAll() zapełni tablicę tablica2 zna-
lezionymi liczbami parzystymi. Następnie utworzymy enumeratory dla obu tablic. Do
przesuwania enumeratorów służy metoda MoveNext(), która przesuwa enumerator na
następny element tablicy. Pętla while będzie wykonywana, aż MoveNext() zwróci false,
a więc do końca tablicy. Zauważ, że w pętli nie jest potrzebna żadna informacja doty-
cząca liczby elementów tabeli. Element, na który wskazuje aktualnie enumerator, od-
czytujemy za pomocą metody Current() i wyświetlamy go w polu tekstowym.

Cała metoda realizująca powyższe zadania będzie wywoływana naciśnięciem przycisku.


Oto jej postać:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
array <System::Int32>^ tablica1 = {0,0,0,0,0,0,0,0,0,0};
Array^ tablica2 = Array::CreateInstance(System::Int32::typeid,10);

for (System::Int32 i=0;i<10;i++) {


tablica1[i]=i;
}

Predicate<System::Int32>^ maska =
gcnew Predicate<System::Int32>(czy_parzysta);
tablica2=System::Array::FindAll(tablica1,maska);

System::Collections::IEnumerator^ enum1 = tablica1->GetEnumerator();


System::Collections::IEnumerator^ enum2 = tablica2->GetEnumerator();
208 Microsoft Visual C++ 2012. Praktyczne przykłady

textBox1->AppendText("Zawartość tablicy:"+System::Environment::NewLine);
while (enum1->MoveNext())
textBox1->AppendText(enum1->Current+System::Environment::NewLine);

textBox1->AppendText("Liczby parzyste:"+System::Environment::NewLine);
while (enum2->MoveNext())
textBox1->AppendText(enum2->Current+System::Environment::NewLine);
}

Po kompilacji i uruchomieniu programu efekt powinien wyglądać tak, jak na rysunku 11.1.

Rysunek 11.1.
Działanie aplikacji
wyszukiwania liczb
parzystych

Uchwyty
Uchwyt jest nowym typem danych, niewystępującym w C++. Jest to twór podobny
do wskaźnika, ale dotyczy zarządzanych typów danych i ma większą funkcjonalność.
W CLI/C++ mamy odśmiecacz (ang. garbage collector), który — optymalizując pamięć
— zmienia adresy obiektów w czasie działania programu. Uchwyt jest powiadamiany
o działaniu odśmiecacza, tak że nie gubi obiektu, na który wskazuje po zmianie adresu.

Wskaźnik dla danego typu był definiowany poprzez dodanie do nazwy typu gwiazdki (*),
natomiast definiując uchwyt, dodajemy znak ^. Na przykład uchwyt do zmiennej typu
System::Single zdefiniujemy następująco:
System::Single^ liczba;

Istnieje również referencja do zarządzanych typów danych tworzona za pomocą opera-


tora %. Jest to odpowiednik zwykłej referencji znanej z C++ (operator &). Za pomocą ta-
kiej referencji również można na przykład przekazać parametr do funkcji przez adres.

Podobnie jak wskaźnik, uchwyt jest tylko obiektem pośredniczącym w wymianie danych.
Aby działał, musi wskazywać na jakiś obszar pamięci. Ten obszar musi być zarezerwo-
wany dla określonych danych, dlatego pojęcie uchwytu łączy się bardzo mocno z dy-
namicznym tworzeniem obiektów.
Rozdział 11. ♦ Tablice, uchwyty i dynamiczne tworzenie obiektów 209

Dynamiczne tworzenie obiektów


— operator gcnew
W standardowym C++ do dynamicznej deklaracji obiektów używa się operatora new.
W C++/CLI zarządzane obiekty deklarujemy nie bezpośrednio na stercie, ale w obszarze
pamięci zarządzanym przez odśmiecacz. W dalszym ciągu istnieje możliwość deklaracji
obiektów niezarządzanych. W celu odróżnienia obiektów pozostających pod „władzą”
odśmiecacza i tych deklarowanych na stercie dla tych pierwszych wprowadzono ope-
rator gcnew (od ang. garbage collector new).

Zasadnicza różnica między obiektem zarządzanym i niezarządzanym jest taka, że obiekty


zarządzane nie wymagają zwolnienia pamięci po ich użyciu, czyli nie wymagają użycia
operatora delete. Zamiast programisty czyszczeniem i optymalizacją pamięci zajmuje
się odśmiecacz.

Składnia użycia operatora gcnew jest podobna do składni operatora new. I tak na przykład
utworzenie dynamicznie zmiennej typu System::Single wraz z definicją uchwytu będzie
miało postać:
System::Single^ liczba = gcnew System::Single;

Dostęp do wartości wskazywanej przez uchwyt uzyskujemy przez operator *, tak jak
w przypadku wskaźników. Komponenty wizualne umieszczane w formularzach również
można tworzyć dynamicznie; poświęcony został temu jeden z dalszych rozdziałów.

Przykład 11.3
Zadeklaruj dynamicznie dwie zmienne, a następnie wyświetl ich sumę w polu tekstowym
TextBox.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, w jej oknie głównym umieść przy-
cisk Button i kontrolkę TextBox.

Oto odpowiednia metoda wywoływania po naciśnięciu przycisku:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Single^ zmienna1 = gcnew System::Single(3.8);
System::Single^ zmienna2 = gcnew System::Single(4.6);
*zmienna2=*zmienna2+*zmienna1;
textBox1->AppendText(zmienna2->ToString());
delete zmienna1;
}

Przy deklarowaniu zmiennych wykorzystaliśmy konstruktor z inicjalizacją tak, że zmien-


nym przypisywane są od razu wartości.

Po skompilowaniu program wyświetli sumę liczb wpisanych przy inicjalizacji. Usunięcie


zmiennej operatorem delete jest jak najbardziej możliwe, choć niewymagane.
210 Microsoft Visual C++ 2012. Praktyczne przykłady

Dynamiczna deklaracja tablic


Tablice są obiektami klasy array i mogą być deklarowane dynamicznie, tak jak wszystkie
inne obiekty. Składnia jest następująca:

Dla tablicy jednowymiarowej typu System::Single:


array<System::Single>^ tablica = gcnew array<System::Single>(n);

przy czym n jest rozmiarem tablicy (liczbą elementów).

Dla tablicy dwuwymiarowej typu System::Single mamy:


array<System::Single,2>^ tablica = gcnew array<System::Single,2>(n,k);

przy czym n i k są wymiarami tablicy, odpowiednio w jednym i drugim kierunku. Oczy-


wiście, zamieniając typ System::Single na inny, możemy deklarować tablice różnych
typów.

Przykład 11.4

Utwórz za pomocą operatora gcnew dwie tablice: jednowymiarową i dwuwymiarową,


następnie wypełnij je liczbami i wyświetl ich zawartość wraz z podstawowymi informa-
cjami o nich.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, w jej oknie głównym umieść przy-
cisk Button i kontrolkę TextBox.

Metoda realizująca zadania z przykładu będzie miała postać:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
array<System::Single>^ tabli1 = gcnew array<System::Single>(5);
array<System::Single,2>^ tabli2 = gcnew array<System::Single,2>(5,6);
for (int n=0;n<5;n++) {
tabli1[n]=n;tabli2[n,n]=n;
}

textBox1->AppendText("Tablica 1 jest " + tabli1->Rank +


" wymiarowa" + System::Environment::NewLine);
textBox1->AppendText("i zawiera " + tabli1->Length +
" elementów" + System::Environment::NewLine);
textBox1->AppendText("Elementy tablicy 1 " +
´System::Environment::NewLine);
for (int n=0;n<5;n++)
textBox1->AppendText(tabli1[n].ToString()+
´System::Environment::NewLine);
textBox1->AppendText("Tablica 2 jest " + tabli2->Rank +
" wymiarowa" + System::Environment::NewLine);
textBox1->AppendText("i zawiera " + tabli2->Length +
" elementów" + System::Environment::NewLine);
Rozdział 11. ♦ Tablice, uchwyty i dynamiczne tworzenie obiektów 211

textBox1->AppendText("Elementy tablicy 2 " +


´System::Environment::NewLine);
for (int n=0;n<5;n++) {
for (int k=0;k<6;k++)
textBox1->AppendText(tabli2[n,k].ToString());
textBox1->AppendText(System::Environment::NewLine);
}
}

Wykorzystuje ona dynamiczne deklaracje tablic i metody operujące na tablicach, któ-


re znamy już z tablic statycznych.
212 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 12.
Komunikacja aplikacji
z plikami

Pliki jako źródło danych


Często aplikacja do działania potrzebuje danych lub wyniki jej działania muszą być
gdzieś zapisane. Przykładem mogą być pliki konfiguracyjne lub pliki wynikowe z pro-
gramów obliczeniowych. Funkcjonalność niektórych aplikacji (np. edytorów tekstu)
w ogóle byłaby bardzo mała, gdyby nie możliwość zapisu wyników pracy.

Format plików może być tekstowy lub binarny. W plikach tekstowych zawartość ma po-
stać znaków ASCII (cyfry, litery), tak że jest ona zrozumiała dla człowieka. Pliki binarne są
zapisane jako ciągi bajtów, tak że nie jest możliwe odczytanie tych danych bezpośrednio.

Pliki tekstowe mają tę przewagę, że można je modyfikować za pomocą dowolnego


edytora ASCII, jednak aby zapisać tę samą ilość danych, potrzeba większego pliku niż
w zapisie binarnym.

Klasy i metody wykorzystane w tym rozdziale umieszczone są w przestrzeni nazw


System::IO. Przestrzeń ta nie jest standardowo dołączana do programu, dlatego będziemy
musieli poinformować kompilator o jej użyciu za pomocą dyrektywy:
using namespace System::IO;

Tę dyrektywę należy umieścić pod dyrektywami załączającymi inne przestrzenie nazw,


na początku kodu w pliku Form1.h dla wszystkich przykładów z tego rozdziału.

Środowisko .NET Framework posiada cztery klasy, które umożliwiają operacje na plikach
i folderach:
 Directory — klasa zawierająca statyczne metody do operacji na folderach.
Statyczne metody nie wymagają utworzenia obiektu tej klasy.
214 Microsoft Visual C++ 2012. Praktyczne przykłady

 DirectoryInfo — również służy do operacji na folderach, ale aby używać


związanych z nią metod, trzeba utworzyć obiekt reprezentujący konkretny
katalog.
 File — zbiór statycznych metod do operacji na plikach.
 FileInfo — podobnie jak DirectoryInfo, tylko w odniesieniu do plików.

Wyszukiwanie plików
Zwykle dana aplikacja może korzystać tylko z jednego lub kilku rodzajów plików,
czasami też zachodzi potrzeba wyszukania pliku o konkretnej nazwie.

Do wyszukania plików o zadanych parametrach można wykorzystać metodę GetFiles()


z klasy Directory.
GetFiles(katalog, szukamy, opcje)

Metoda pobiera jeden, dwa lub trzy parametry. Parametr katalog to katalog do przeszu-
kania, szukamy to nazwa pliku lub plików do odnalezienia. Parametr opcje mówi, czy
przeszukane mają być także podfoldery. W nazwie pliku można stosować znaki * (do-
wolny ciąg znaków) i ? (dowolny pojedynczy znak). Metoda zwraca tabelę łańcuchów
znakowych typu System::String zawierających nazwy odnalezionych plików.

Przykład 12.1

Napisz program wyszukujący wszystkie pliki o rozszerzeniu .exe w katalogu


c:\Windows\system32.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, w okno aplikacji wstaw przycisk
Button i pole tekstowe TextBox. Pod właściwość Text przycisku podstaw Znajdz. Kliknij
wstawione pole tekstowe i otwórz zakładkę Properties. Ustaw wartość właściwości
Multiline na true. Następnie rozciągnij pole tak, aby mogło pomieścić więcej niż jedną
linię tekstu. Pamiętaj o dołączeniu przestrzeni nazw System::IO.

Metoda obsługująca naciśnięcie przycisku będzie odnajdowała pliki, a następnie wypi-


sywała zawartość tablicy łańcuchów znakowych do pola tekstowego.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e)
{
array<String^>^pliki=Directory::GetFiles("c:\\Windows\\system32", "*.exe",
System::IO::SearchOption::TopDirectoryOnly);
for (System::Int16 i=0;i<pliki->Length;i++)
textBox1->AppendText(pliki[i]+System::Environment::NewLine);
}
Rozdział 12. ♦ Komunikacja aplikacji z plikami 215

Po uruchomieniu programu naciśnij przycisk, a lista aplikacji .exe pojawi się w oknie
tekstowym, jak na rysunku 12.1.

Rysunek 12.1.
Aplikacja wyszukująca
pliki

Metodę GetFiles() wywołaliśmy z dwoma parametrami, określającymi folder i ro-


dzaj plików do znalezienia. Gdybyśmy chcieli przeszukać także podfoldery, należałoby
podać trzeci parametr.
array<String^>^pliki = Directory::GetFiles("c:\\Windows\\system32","*.exe",
System::IO::SearchOption::AllDirectories);

Odczyt własności plików i folderów


Po odnalezieniu pliku można odczytać jego właściwości, takie jak wielkość, data utwo-
rzenia i inne. Pomogą nam w tym klasy DirectoryInfo i FileInfo. Tworząc obiekty
tych klas, kojarzymy je z konkretnymi folderami lub plikami. Własności plików i folde-
rów podstawiane są pod właściwości tych obiektów. Tabela 12.1 przedstawia właściwości
FileInfo. Właściwości DirectoryInfo są takie same, z tym że odnoszą się do folderu
i nie występuje tu właściwość Length, ponieważ wielkość folderu samego w sobie (nie
plików w nim zawartych) nie jest istotna.

Tabela 12.1. Właściwości klasy FileInfo


Nazwa Wyjaśnienie
Atrributes Atrybuty pliku. Zwraca wartość typu wyliczeniowego FileAttributes.
Najważniejsze elementy składowe to:
 System::IO::FileAttributes::System — plik systemowy,
 System::IO::FileAttributes::Hidden — plik ukryty,
 System::IO::FileAttributes::ReadOnly — tylko do odczytu,
 System::IO::FileAttributes::Directory — oznacza folder.
216 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 12.1. Właściwości klasy FileInfo (ciąg dalszy)


Nazwa Wyjaśnienie
CreationTime Zawiera datę utworzenia pliku.
Exists Właściwość służąca do sprawdzania tego, czy plik istnieje.
FullName Nazwa pliku wraz ze ścieżką.
Length Długość pliku w bajtach.
Name Nazwa pliku.

Przykład 12.2

Napisz program wyświetlający nazwy i wielkości plików w bajtach dla wszystkich plików
.exe z katalogu c:\Windows\system32.

Rozwiązanie

Utwórz nowy projekt według przykładu 1.4, w okno aplikacji wstaw przycisk Button
i pole tekstowe TextBox.

Najpierw utworzymy obiekt typu DirectoryInfo reprezentujący katalog c:\Windows\


system32, a następnie za pomocą metody GetFiles() wypełnimy tablicę obiektów
FileInfo danymi o plikach *.exe z tego katalogu. Później wystarczy już tylko odczytać
właściwość Length dla każdego z plików.

Metoda obsługi zdarzenia Click dla przycisku realizująca powyższy algorytm będzie
wyglądała jak niżej:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
DirectoryInfo^ katalog = gcnew DirectoryInfo( "c:\\Windows\\system32" );
array<FileInfo^>^pliki=katalog->GetFiles("*.exe");
for (System::Int16 i=0;i<pliki->Length;i++)
textBox1->AppendText(pliki[i]->Name + " " + pliki[i]->Length +
System::Environment::NewLine);
}

Odczyt danych z plików tekstowych


Do odczytu plików tekstowych użyjemy obiektu klasy StreamReader; klasa ta ma bar-
dziej ogólne zastosowanie do odczytu danych strumieniowych, my jednak pozostaniemy
przy plikach dyskowych. W skrócie odczyt pliku polega na stowarzyszeniu konkret-
nego pliku z obiektem klasy StreamReader, a następnie czytaniu z tego obiektu.

Metody klasy StreamReader do odczytu pliku przedstawia tabela 12.2.


Rozdział 12. ♦ Komunikacja aplikacji z plikami 217

Tabela 12.2. Metody odczytu pliku dla klasy StreamReader


Metoda Działanie
int Read() Metoda bez parametrów czyta kolejny pojedynczy znak z pliku.
int Read(bufor, indeks, Znak jest zwracany jako wynik metody.
liczba_znakow)
Metoda z parametrami czyta określoną liczbę znaków podaną
w parametrze liczba_znakow, rozpoczynając od znaku numer
indeks, licząc od aktualnej pozycji w pliku. Przeczytane znaki
wpisywane są do kolejnych pól tablicy bufor, które muszą być
typu System::Char.
int ReadBlock(bufor, indeks, Funkcjonalność taka jak metody Read() z parametrami.
liczba_znakow)
System::String ReadToEnd() Czyta cały tekst aż do końca pliku. Tekst jest zwracany jako
wynik metody w postaci łańcucha znaków.
System::String ReadLine() Czyta pojedynczą linię z pliku. Tekst jest zwracany jako wynik
metody w postaci łańcucha znaków. W przypadku natrafienia
na koniec pliku zwraca pusty ciąg ("").

Przykład 12.3

Napisz program wypisujący zawartość pliku tekstowego w polu tekstowym.

Rozwiązanie

Otwórz nowy projekt aplikacji według przykładu 1.4 i umieść w oknie komponent
TextBox i przycisk Button.

Ustaw właściwość Multiline pola TextBox na true i rozciągnij je myszą tak, by miało
wysokość kilku linii.

Zakładamy, że tekst do odczytania znajduje się w pliku Plik.txt. Utwórz plik o takiej
nazwie, zawierający dowolny tekst, i zapisz go w katalogu projektu.

Po naciśnięciu przycisku najpierw połączymy ten plik z obiektem klasy StreamReader,


a następnie odczytamy od razu całą zawartość pliku. Aby polskie litery — i w ogóle
znaki Unicode — były czytane poprawnie, trzeba ustawić kodowanie w strumieniu zgod-
ne z aktualnym kodowaniem znaków w Windows (wartość System::Text::Encoding::
Default). Skorzystamy z metody ReadToEnd(). Odczytaną zawartość wpiszemy do pola
tekstowego. Do zdarzenia Click przycisku przyporządkuj następującą metodę wyko-
nującą te czynności:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ tekst;
StreamReader^ plik =
gcnew StreamReader("Plik.txt",System::Text::Encoding::Default);
tekst=plik->ReadToEnd();
textBox1->Text=tekst;
plik->Close();
}
218 Microsoft Visual C++ 2012. Praktyczne przykłady

Uruchom aplikację. Po naciśnięciu przycisku tekst z pliku pojawi się w oknie.

Jeżeli plik tekstowy jest zapisany w innym kodowaniu, na przykład ISO 8859-2, to stronę
kodową można ustawić za pomocą metody GetEncoding():
StreamReader^ plik = gcnew
StreamReader("Plik.txt",System::Text::Encoding::GetEncoding("ISO-8859-2"));

Zapisz projekt, w następnym przykładzie nieco go zmienimy.

Zamiast czytać od razu cały plik, można go odczytywać linia po linii za pomocą metody
ReadLine().

Przykład 12.4

Zmodyfikuj program z poprzedniego przykładu tak, aby czytał i wyświetlał zawartość


pliku linia po linii.

Rozwiązanie
Otwórz projekt z poprzedniego przykładu.

Czytanie kolejnych linii zrealizujemy za pomocą metody ReadLine(). Metoda ta będzie


wykonywana w pętli while, dopóki nie zwróci wartości zero, co oznacza koniec pliku.
Do wypisywania kolejnych linii wykorzystamy metodę pola tekstowego AppendText().
Ponieważ nie są wczytywane znaki końca linii, musimy je dodawać.

Metodę button1_Click() zmodyfikuj następująco:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ tekst;
StreamReader^ plik =
gcnew StreamReader("Plik.txt",System::Text::Encoding::Default);
while (tekst=plik->ReadLine()) {
textBox1->AppendText(tekst+System::Environment::NewLine);
}
plik->Close();
}

Teraz możesz uruchomić zmodyfikowaną aplikację; jej działanie będzie identyczne


jak poprzednio.

Jeżeli chcemy odczytywać liczby z pliku tekstowego, na początku są one czytane jako
tekst, a następnie trzeba je zamienić na postać liczbową, korzystając z metody Parse().
Problemem jest odczytywanie kolejnych liczb w jednym wierszu, oddzielonych spa-
cjami. W zwykłym C++ można było to zrobić bardzo prosto za pomocą operatora >>.
W C++/CLI trzeba składać liczby z poszczególnych cyfr jako łańcuch znakowy, a później
zamieniać na postać liczbową.
Rozdział 12. ♦ Komunikacja aplikacji z plikami 219

Przykład 12.5

Napisz program dodający dwie liczby zapisane w pliku tekstowym oddzielone spacją.

Rozwiązanie
Utwórz nowy projekt według przykładu 1.4. Na formatce umieść przycisk Button i pole
TextBox. Liczby będą czytane po naciśnięciu przycisku, czyli cały kod umieścimy w me-
todzie button1_Click (). Najpierw zadeklarujemy potrzebne zmienne:
System::Char znak;
System::String^ liczbaString;
System::Single liczba;
System::Single wynik;
StreamReader^ plik = gcnew
StreamReader("Plik.txt",System::Text::Encoding::Default);

Zmienna znak to pojedynczy znak czytany z pliku, liczbaString przechowuje składaną ze


znaków liczbę w postaci łańcucha znakowego. Łańcuch znakowy zamieniany będzie na
liczbę typu System::Single i zapisywany w zmiennej liczba, kolejne liczby będą sumo-
wane i zapisywane w wyniku. Potrzebujemy jeszcze obiektu StreamReader do czytania
z pliku. Cały odczyt będzie przebiegał w dwóch pętlach while. Zewnętrzna pętla prze-
biega po całym pliku. Metodą Peek() „podglądamy” kolejny znak w pliku; jeśli metoda
zwróci wartość -1, to oznacza koniec pliku i koniec pętli. W tej pętli znajduje się pętla
odczytująca kolejne cyfry danej liczby w postaci tekstowej. Znaki z pliku pobierane są
metodą Read() i dodawane do łańcucha liczbaString. Wewnętrzna pętla działa do napo-
tkania spacji lub końca pliku. Jeśli napotkano spację, to oznacza to koniec liczby; wy-
chodzimy z pętli wewnętrznej, w dalszym ciągu znajdujemy się w pętli zewnętrznej. Wy-
świetlamy odczytaną liczbę w postaci tekstowej, następnie zamieniamy ją na liczbę typu
Single. O konwersji będziemy jeszcze mówić w następnym rozdziale. Zamienioną liczbę
dodajemy do sumy. Następnie zerujemy łańcuch znakowy z liczbą metodą Read() i od-
czytujemy spację z pliku, aby ustawić miejsce odczytu z pliku na początek następnej
liczby. Pętla wewnętrzna może teraz składać następną liczbę z cyfr.
while (plik->Peek()!=-1) {
while ((plik->Peek()!=32)&&(plik->Peek()!=-1))
{ znak=(Char)plik->Read();
liczbaString=liczbaString+znak;
}
textBox1->AppendText(liczbaString+System::Environment::NewLine);
liczba=Convert::ToSingle(liczbaString);
wynik=wynik+liczba;
liczbaString="";
plik->Read();
}

Po wyjściu z zewnętrznej pętli wyświetlamy sumę i zamykamy plik.


textBox1->AppendText("Suma: "+wynik.ToString()+System::Environment::NewLine);
plik->Close();

Teraz w dowolnym edytorze ASCII przygotuj plik Plik.txt zawierający dwie lub więcej
liczb oddzielonych spacją. Plik może mieć na przykład taką postać:
23,6 34,3
220 Microsoft Visual C++ 2012. Praktyczne przykłady

Ważne jest, aby separator dziesiętny był zgodny z ustawieniami regionalnymi dla
Polski — będzie to przecinek (nie kropka). W przeciwnym razie metoda ToSingle()
zwróci błąd.

Po uruchomieniu i naciśnięciu przycisku w polu tekstowym wyświetlą się liczby z pliku


oraz ich suma.

Zapisywanie tekstu do pliku


Do zapisu tekstu można użyć klasy StreamWriter. Są w niej dostępne metody Write()
i WriteLine(). Pierwsza z nich zapisuje tekst do strumienia (w tym przypadku pliku),
a druga zapisuje tekst, dodając na końcu znak końca linii. Tekst lub liczbę do wpisania
do pliku podajemy jako parametr metody Write() lub WriteLine().

Obie metody mogą akceptować jako parametry także typy liczbowe (Single, Double)
i wypisywać je do pliku w postaci tekstowej. Aby zapisać wartości zmiennej liczbowej
za pomocą metody Write(), nie wystarczy podać jej jako parametru metody. Trzeba
ją albo jawnie zamienić na typ System::String() metodą ToString(), albo skorzystać
z łańcucha znakowego z szablonami. Ten drugi sposób jest szczególnie wygodny, jeśli
mamy do zapisania kombinacje tekstu i liczb. Metodę ToString() w najprostszym przy-
padku wywołujemy po prostu na obiekcie zmiennej. Szablony natomiast są znane z C++
jeszcze z funkcji printf(). Tutaj jednak ich użycie jest prostsze, nie ma podziału na
typy wypisywanych zmiennych. Miejsca wypisania liczb zaznaczamy kolejnymi licz-
bami w nawiasach klamrowych, następnie podajemy te liczby jako argumenty metody
Write(). Oba sposoby zaprezentuję na przykładzie. O metodzie ToString() będziemy
jeszcze mówić w następnym rozdziale.

Przykład 12.6

Napisz program wypisujący zadany tekst i liczby do pliku w postaci tekstowej.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do jej okna głównego
jeden przycisk Button.

Kliknij podwójnie przycisk i wpisz zawartość metody button1_Click(), jak niżej:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ tekst;
StreamWriter^ plik =
gcnew StreamWriter("Plik.txt",0,System::Text::Encoding::Default);
plik->WriteLine("Linia");
plik-> Write("Napis");
System::Single liczba_single=47.4;
System::Int32 liczba_int=1234;
Rozdział 12. ♦ Komunikacja aplikacji z plikami 221

plik->WriteLine(" "+liczba_single+" "+liczba_int);


plik->Write(liczba_single.ToString());
plik->Write(" Pierwsza liczba: {0} druga liczba: {1}", 3.23,4);
plik->Close();
}

Tutaj również musimy zadbać o ustawienie kodowania znaków.

Teraz uruchom program. Po naciśnięciu przycisku nic się nie stanie. Zamknij okno apli-
kacji i poszukaj w katalogu projektu pliku Plik.txt. Powinien on zawierać następujący
tekst:
Linia
Napis 47,4 1234
47,4 Pierwsza liczba: 3,23 druga liczba: 4

Zauważ, że liczbę 3,23 do wpisania za pomocą szablonu podałeś z kropką, a została wpi-
sana do pliku z przecinkiem. Metoda Write() wpisuje liczby zgodnie z ustawieniami lo-
kalnymi rodzaju punktu dziesiętnego. Zachowaj projekt do wykorzystania w następnym
przykładzie.

Jak łatwo się przekonać, każde naciśnięcie przycisku w przykładzie 12.6 spowoduje
wymazanie starych danych i wpisywanie od nowa do pliku Plik.txt. Przekształcenie
programu tak, aby dodawać dane do istniejącego pliku, jest jednak bardzo proste.

Przykład 12.7

Napisz program dopisujący treść do już istniejącego pliku.

Rozwiązanie
Otwórz projekt z przykładu 12.6.

Drugi parametr konstruktora tworzącego obiekt klasy StreamWriter określa, czy plik
jest tworzony od nowa, czy dane są dopisywane. Zmień linię:
StreamWriter^ plik = gcnew StreamWriter("Plik.txt", 0,
System::Text::Encoding::Default);

na:
StreamWriter^ plik = gcnew StreamWriter("Plik.txt", 1,
System::Text::Encoding::Default);

Parametr 1 jako drugi na liście parametrów konstruktora oznacza, że w przypadku gdy


plik istnieje, chcemy dopisywać zawartość na końcu.

Uruchom program. Teraz za każdym naciśnięciem przycisku tekst będzie dopisywany


do pliku.
222 Microsoft Visual C++ 2012. Praktyczne przykłady

Zapis danych do plików binarnych


Kiedy mamy do zapisania dużo danych, przeważnie liczbowych, dobrym rozwiązaniem
jest zapisanie ich do plików w postaci binarnej. W Visual C++, w aplikacjach .NET jest
to właściwie jedyne rozwiązanie, gdyż sprawne zapisywanie i odczyt większych ilości
danych liczbowych w postaci tekstowej są trudne. Zapis nie nastręczał dużych trudności,
jednak odczyt liczb z pliku tekstowego wymagał składania ich z kolejnych cyfr.

Zaczniemy od zapisu danych, ponieważ plików binarnych nie można utworzyć za


pomocą zewnętrznego edytora tekstowego. Do zapisu danych binarnych skorzystamy
z klasy BinaryWriter.

Klasa BinaryWriter zawiera metodę Write(), która akceptuje parametry różnych typów
i zapisuje je do pliku w postaci binarnej. Konstruktor obiektu BinaryWriter wymaga
parametru typu FileStream, musimy więc najpierw utworzyć obiekt typu FileStream,
wiążąc go z plikiem, a następnie skonstruować obiekt BinaryWriter.

Przykład 12.8

Napisz program wpisujący trzy liczby typu System::Single do pliku binarnego.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w oknie aplikacji przycisk.
W jego właściwość Text wpisz tekst Zapis do pliku. Prawdopodobnie będzie potrzeb-
ne rozciągnięcie przycisku, aby pomieścił cały tekst.

Metodę obsługującą zdarzenie Click przycisku napisz jak poniżej:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Single liczba_single;
FileStream^ plik_zap = File::Create("Plik.txt");
BinaryWriter^ plik_dane_zap = gcnew BinaryWriter(plik_zap,
´System::Text::Encoding::Default);
liczba_single=123.45;
plik_dane_zap->Write(liczba_single);
liczba_single=456.78;
plik_dane_zap->Write(liczba_single);
liczba_single=987.34;
plik_dane_zap->Write(liczba_single);
plik_zap->Close(); }

Teraz uruchom aplikację. Po naciśnięciu przycisku w folderze aplikacji utworzy się


plik Plik.txt i zostaną do niego wpisane liczby. Ponieważ liczby mają postać binarną,
plik będzie nieczytelny w edytorze ASCII, na przykład w Notatniku. Zachowaj pro-
jekt; w następnym przykładzie napiszemy procedurę odczytującą zapisane liczby.
Rozdział 12. ♦ Komunikacja aplikacji z plikami 223

Odczyt z plików binarnych


Odczyt z plików binarnych jest możliwy za pośrednictwem klasy BinaryReader. Ofe-
ruje ona zestaw metod do odczytu danych różnych typów. Odczytane dane są zwracane
jako wartość metody. Zestawienie metod do odczytu najczęściej spotykanych typów
danych przedstawia tabela 12.3.

Tabela 12.3. Metody odczytujące najczęściej spotykane typy danych


Nazwa metody Działanie
ReadBoolean() Czyta wartość typu System::Boolean.
ReadSingle() Czyta wartość typu System::Single.
ReadDouble() Czyta wartość typu System::Double.
ReadInt32(), ReadInt16(), Czyta wartość typu System::Int32, System::Int16 lub System::Int64.
ReadInt64()
ReadString() Czyta łańcuch znakowy typu System::String. Przy zapisywaniu
łańcucha metodą Write() z klasy BinaryWriter jest zapisywana także
długość łańcucha. Metoda ReadString() korzysta z tych informacji,
dzięki czemu nie trzeba podawać długości.

Przykład 12.9

Napisz program odczytujący i wyświetlający dane zapisane w poprzednim przykładzie.

Rozwiązanie
Otwórz projekt z poprzedniego przykładu.

Przesuń przycisk Zapis do pliku niżej w oknie i dodaj obok drugi przycisk Button. We
właściwości Text tego przycisku napisz Odczyt z pliku.

Na górze okna umieść pole tekstowe TextBox, a jego właściwość Multiline ustaw na
true. Całość może wyglądać na przykład tak, jak na rysunku 12.2.

Rysunek 12.2.
Aplikacja odczytująca
dane z pliku binarnego
i zapisująca je do niego
224 Microsoft Visual C++ 2012. Praktyczne przykłady

Metoda obsługująca zdarzenie Click przycisku Odczyt z pliku będzie taka, jak poni-
żej. Ważne jest, aby kodowanie przy odczycie było takie samo jak przy zapisie. Jeśli
jawnie nie podstawimy kodowania, na przykład System::Text::Encoding::Default, to
odczyt będzie się odbywał przy kodowaniu UTF-7, a zapis przy kodowaniu ustawionym
w Windows. Wygeneruje to błąd.
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
System::Single liczba_single;
FileStream^ plik_odcz = File::OpenRead("Plik.txt");
BinaryReader^ plik_dane_odcz =gcnew BinaryReader(plik_odcz,
´System::Text::Encoding::Default);
while (plik_dane_odcz->PeekChar()!=-1) {
liczba_single=plik_dane_odcz->ReadSingle();
textBox1->AppendText(liczba_single.ToString() +
System::Environment::NewLine);
}
plik_odcz->Close();
}
Rozdział 13.
Okna dialogowe

Okno typu MessageBox


Często przy pisaniu aplikacji zachodzi potrzeba wyświetlania komunikatów lub pytań
wymagających decyzji użytkownika. Najprościej zrealizować to z użyciem klasy Message-
Box. Okno dialogowe wyświetla się za pomocą różnych wersji statycznej metody Show().

Okno może zawierać ikonę, a także zdefiniowane w systemie przyciski: OK, Anuluj,
Przerwij, Ponów próbę, Ignoruj oraz Tak i Nie. Do tworzenia okna tego typu służy meto-
da Show(). Ma ona wiele wariantów listy parametrów, ale najczęściej stosowana jest w po-
staci akceptującej cztery lub pięć parametrów.

Postać 4-parametrowa:
Show(string tekst_w_oknie, string tekst_na_pasku, MessageBoxButtons przyciski,
MessageBoxIcon ikona);

Postać 5-parametrowa:
Show(string tekst_w_oknie, string tekst_na_pasku, MessageBoxButtons przyciski,
MessageBoxIcon ikona, MessageBoxDefaultButton przycisk_domyślny);

Znaczenie poszczególnych parametrów przedstawiono w tabeli 13.1.

Tabela 13.1. Parametry metody Show() klasy MessageBox


Parametr Znaczenie
tekst_w_oknie Tekst do wypisania w oknie wiadomości.
tekst_na_pasku Tekst na pasku tytułowym okna wiadomości.
przyciski Definiuje standardowe przyciski w oknie:
 MessageBoxButtons::OK — przycisk OK.
 MessageBoxButtons::AbortRetryIgnore — przyciski Przerwij, Ponów
próbę i Ignoruj.
226 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 13.1. Parametry metody Show() klasy MessageBox (ciąg dalszy)


Parametr Znaczenie
 MessageBoxButtons::OKCancel — przyciski OK i Anuluj.
 MessageBoxButtons::RetryCancel — przyciski Ponów próbę i Anuluj.
 MessageBoxButtons::YesNo — przyciski Tak i Nie.
 MessageBoxButtons::YesNoCancel — przyciski Tak, Nie oraz Anuluj.
ikona Ikona wyświetlana z lewej strony wiadomości, wskazuje ona na charakter
okna. Ikon jest kilka i nie mają wpływu na zachowanie okna, a jedynie na
znaczenie funkcjonalne. Pełen spis można znaleźć na stronach MSDN.
Najczęściej spotykane to:
 MessageBoxIcon::Exclamation — wykrzyknik, pasuje do okna ostrzeżenia,
 MessageBoxIcon::Information — okno informacyjne,
 MessegeBoxIcon::Question — okno z pytaniem.
przycisk_domyślny Oznacza przycisk aktywny w momencie pojawienia się okna. W celu jego
wyboru wystarczy wcisnąć klawisz Enter. Ten parametr może przybierać
wartości MessageBoxDefaultButton::ButtonX, gdzie X jest liczbą od 1 do 3
i oznacza numer aktywnego przycisku, licząc od lewej strony.

Metoda Show() zwraca parametr typu DialogResult oznaczający, który przycisk nacisnął
użytkownik. Możliwe wartości parametrów zestawia tabela 13.2.

Tabela 13.2. Parametry zwracane przez metodę Show()


Parametr Znaczenie
System::Windows::Forms::DialogResult::Yes Wybrano przycisk Tak.
System::Windows::Forms::DialogResult::OK Wybrano przycisk OK.
System::Windows::Forms::DialogResult::No Wybrano przycisk Nie.
System::Windows::Forms::DialogResult::Abort Wybrano przycisk Przerwij.
System::Windows::Forms::DialogResult::Retry Wybrano przycisk Ponów próbę.
System::Windows::Forms::DialogResult::Cancel Wybrano przycisk Anuluj.
System::Windows::Forms::DialogResult::Ignore Wybrano przycisk Ignoruj.

Przykład 13.1

Napisz program wyświetlający okno dialogowe z przyciskami Tak, Nie oraz Anuluj.
Kiedy użytkownik naciśnie przycisk, wybrana opcja powinna pojawiać się w polu
tekstowym.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do jej okna przycisk
Button i pole tekstowe TextBox.
Rozdział 13. ♦ Okna dialogowe 227

Oto odpowiednia metoda obsługująca naciśnięcie klawisza:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Windows::Forms::DialogResult odp =
MessageBox::Show("Wybierz opcję:",
"Pytanie", MessageBoxButtons::YesNoCancel,
´MessageBoxIcon::Question);

switch (odp) {
case System::Windows::Forms::DialogResult::Yes :
textBox1->AppendText("Wybrano przycisk Tak" +
System::Environment::NewLine);
break;
case System::Windows::Forms::DialogResult::No :
textBox1->AppendText("Wybrano przycisk Nie" +
System::Environment::NewLine);
break;
case System::Windows::Forms::DialogResult::Cancel :
textBox1->AppendText("Wybrano przycisk Anuluj" +
System::Environment::NewLine);break;
}
}

Wynik działania metody Show() jest podstawiany pod zmienną odp, a następnie instruk-
cja wielokrotnego wyboru switch wybiera odpowiedni komunikat do wpisania w pole
tekstowe.

Klasa MessageBox oferuje proste okna. Nic jednak nie stoi na przeszkodzie, aby okno
dialogowe było obiektem klasy Form, czyli klasy głównego okna aplikacji. Wtedy może
mieć praktycznie dowolny wygląd i liczbę komponentów. Będziemy to omawiać w roz-
dziale 21., „Dynamiczne tworzenie okien i komponentów”.

Okno dialogowe otwarcia pliku


Jest to systemowe okno otwarcia pliku, które pokazuje aktualny katalog i umożliwia
wybranie nazwy pliku. W VC++ do obsługi tego okna mamy obiekt typu OpenFile-
Dialog, dostępny w oknie narzędziowym w dziale Dialogs. Tabela 13.3 przedstawia
najważniejsze właściwości klasy OpenFileDialog, umożliwiające sterowanie wyglą-
dem i zachowaniem okna.

Generalnie użycie okna polega na przygotowaniu jego wyglądu poprzez wpisanie od-
powiednich wartości do jego właściwości, a następnie wyświetleniu go za pomocą
metody ShowDialog() i oczekiwaniu na zwrócenie przez tę metodę wartości DialogRe-
sult::OK oznaczającej, że użytkownik kliknął przycisk OK. Typowy kod użycia okna
OpenFileDialog wygląda następująco:
if ( saveFileDialog1->ShowDialog()==System::Windows::Forms::DialogResult::OK) {
// Tutaj instrukcje otwarcia pliku. Nazwę wybranego pliku zawiera właściwość
saveFileDialog1->FileName;
}
228 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 13.3. Niektóre właściwości okna OpenFileDialog


Właściwość Znaczenie
InitialDirectory Folder pokazywany w oknie po jego wyświetleniu.
Filter Steruje rodzajami plików, jakie wyświetlane są w oknie. Rodzaj plików
wybieramy na liście Pliki typu: na dole okna. Właściwość Filter pozwala
na zdefiniowanie tej listy. Definiujemy ją jako ciąg znaków w postaci
„komentarz1|rodzaj plików1|komentarz2|rodzaj plików2”. Wpisów tych może
być dowolnie dużo. Na przykład, jeżeli chcemy mieć możliwość wyboru
między obrazami .jpg lub wszystkimi plikami, zawartość łańcucha Filter
będzie następująca:
„Obrazy JPG|*.jpg|Wszystkie pliki|*.*”
Multiselect Wartość true oznacza, że dozwolone jest wybranie kilku plików. W takim
przypadku nazwy wraz ze ścieżkami plików znajdują się nie we właściwości
FileName, ale FileNames, która jest tablicą łańcuchów znakowych.
Title Tytuł okna wyświetlany w pasku.

Komponent OpenFileDialog jest niewidoczny i umieszcza się go na pasku niewidocz-


nych komponentów pod oknem aplikacji.

Przykład 13.2

Napisz program wczytujący wybrany plik tekstowy (z rozszerzeniem .txt) do pola


tekstowego.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w nim przycisk Button,
pole tekstowe TextBox oraz komponent OpenFileDialog. We właściwość Text przycisku
wpisz Otwórz. Właściwość Multiline pola tekstowego ustaw na true i powiększ wymiary
pola myszą, tak aby obejmowało kilka linii, jak na rysunku 13.1.

Okno aplikacji na rysunku 13.1 zawiera jeszcze przycisk Zapisz, który dodamy w przy-
kładzie 13.4. Po naciśnięciu przycisku we właściwość Filter wpiszemy możliwość
wyświetlania wszystkich plików lub tylko plików z rozszerzeniem .txt, a następnie
wyświetlimy okno dialogowe OpenFileDialog.

Po wybraniu pliku przez użytkownika i kliknięciu OK program utworzy obiekt Stre-


amReader do odczytu pliku (zobacz rozdział 12., podrozdział „Odczyt danych z plików
tekstowych”), a na końcu wpisze zawartość pliku do okna TextBox. Oto metoda, którą
należy powiązać ze zdarzeniem Click przycisku:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
openFileDialog1->Filter =
"Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*";
if (openFileDialog1->ShowDialog() ==
System::Windows::Forms::DialogResult::OK) {
Rozdział 13. ♦ Okna dialogowe 229

Rysunek 13.1.
Najprostszy edytor
ASCII

StreamReader^ plik = gcnew StreamReader(openFileDialog1->FileName,


System::Text::Encoding::Default);
textBox1->Text=plik->ReadToEnd();
plik->Close();
}
}

Na początku pliku Form1.h konieczne jest podłączenie przestrzeni nazw System::IO.


using namespace System::IO;

Aplikacja jest już gotowa; po wypróbowaniu zapisz ją na dysku, ponieważ będzie po-
trzebna do następnego przykładu.

Podany sposób korzystania z okna OpenFileDialog nie jest jedynym możliwym. Zamiast
niego można się oprzeć wyłącznie na zdarzeniach komponentu. W momencie kiedy
użytkownik kliknie w oknie przycisk Otwórz, zachodzi zdarzenie FileOK. Zmodyfi-
kujemy przykład tak, aby korzystał z tego zdarzenia.

Przykład 13.3

Zmodyfikuj program wczytujący wybrany plik tekstowy (z rozszerzeniem .txt) do


pola tekstowego, tak aby korzystał ze zdarzenia FileOK komponentu OpenFileDialog.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu. W metodzie button1_Click() pozosta-
wimy tylko linie przygotowujące i wyświetlające okno dialogowe. Po kliknięciu Otwórz
nastąpi zdarzenie FileOk, które wyzwoli nową metodę z kodem wczytania pliku. Zatem
button1_Click() będzie wyglądało jak niżej:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
230 Microsoft Visual C++ 2012. Praktyczne przykłady

openFileDialog1->Filter = "Pliki tekstowe (*.txt)|*.txt|


´Wszystkie pliki (*.*)|*.*";
openFileDialog1->ShowDialog();
}

Na pasku niewidocznych komponentów kliknij OpenFileDialog, następnie rozwiń za-


kładkę Properties i przełącz się za pomocą ikony błyskawicy na widok zdarzeń. Masz
tu tylko dwa zdarzenia; jednym z nich jest FileOK. Kliknij podwójnie na nazwę zda-
rzenia, a utworzy się metoda openFileDialog1_FileOk() i zostanie do niego podłączona.
Do tej metody przeniesiemy kod wczytania pliku.
private: System::Void openFileDialog1_FileOk(System::Object^ sender,
System::ComponentModel::CancelEventArgs^ e) {
StreamReader^ plik = gcnew StreamReader(openFileDialog1->FileName,
System::Text::Encoding::Default);
textBox1->Text=plik->ReadToEnd();
plik->Close();
}

Uruchom aplikację. Będzie działać identycznie jak poprzednio, ale została napisana
inaczej. Zapisz ją, ponieważ przyda się w następnym przykładzie.

Okno zapisu pliku


Podobnie jak okno otwarcia, istnieje też obiekt obsługujący systemowe okno wyboru pli-
ku do zapisu. Jest to obiekt SaveFileDialog, również dostępny w oknie narzędziowym
w dziale Dialogs. Zasada obsługi tego okna jest bardzo podobna do okna OpenFileDialog.

Okna OpenFileDialog i SaveFileDialog nie dokonują zapisu czy odczytu pliku,


służą jedynie do wyboru pliku do zapisu lub odczytu, dlatego ich funkcjonalność
jest bardzo podobna.

Okno SaveFileDialog ma te same właściwości, jakie przedstawia tabela 13.3, oprócz


właściwości Multiselect. Dodatkowo posiada użyteczne właściwości do weryfikacji
istnienia pliku i uniknięcia przypadkowego nadpisania istniejącego pliku. Te właści-
wości przedstawia tabela 13.4.

Tabela 13.4. Specyficzne właściwości okna SaveFileDialog


Właściwość Opis
CheckFileExists Wartość true powoduje, że niemożliwe jest tworzenie nowych plików; można
jedynie wybrać pliki z istniejących, ale nie można wpisać nowej nazwy.
CreatePrompt Jeżeli ta właściwość zawiera wartość true, to w przypadku wpisania nowej
nazwy pliku wyświetlane jest zapytanie, czy utworzyć plik. W przypadku
wartości false plik jest tworzony bez pytania.
OverwritePrompt W przypadku wartości true i wybrania istniejącego pliku aplikacja pyta
o pozwolenie na nadpisanie pliku.
Rozdział 13. ♦ Okna dialogowe 231

Przykład 13.4

Do aplikacji z poprzedniego przykładu dołącz możliwość zapisu pliku tekstowego,


tworząc najprostszy edytor ASCII.

Rozwiązanie
Otwórz projekt aplikacji z poprzedniego przykładu i dodaj do niego przycisk Button oraz
komponent SaveFileDialog. We właściwość Text tego przycisku wpisz słowo Zapisz.
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
saveFileDialog1->Filter =
"Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*";
if ( saveFileDialog1->ShowDialog() ==
System::Windows::Forms::DialogResult::OK) {
StreamWriter^ plik = gcnew StreamWriter(saveFileDialog1->FileName);
plik->Write(textBox1->Text);
plik->Close();
}
}

Do zapisu do pliku tekstowego użyliśmy klasy StreamWriter (zobacz rozdział 12.).


Zapisywana jest od razu cała zawartość pola TextBox. Wygląd programu po uruchomieniu
przedstawia rysunek 13.1.

Okno zapisu pliku również może korzystać z identycznego zdarzenia FileOK. Zadanie
przepisania aplikacji z poprzedniego przykładu jest na tyle proste, że pozostawiam je
Tobie. W razie problemów skorzystaj z przykładu 13.3.

Okno przeglądania folderów


Jeśli w aplikacji potrzebne jest wskazanie folderu, na przykład docelowego do zain-
stalowania aplikacji, można skorzystać z komponentu FolderBrowserDialog. Subiek-
tywny zbiór najbardziej przydatnych właściwości tego okna przedstawia tabela 13.5.

Tabela 13.5. Niektóre właściwości komponentu FolderBrowserDialog


Właściwość Opis
Description Określa napis w oknie ponad wyświetlanymi folderami. Właściwość ma typ
System::String.
RootFolder Folder początkowy wyświetlany w oknie po jego uruchomieniu. Nie może
być to jednak dowolny folder, ale jeden ze zbioru możliwych wartości wyliczenia
Environment::SpecialFolder. Są to specjalne foldery typu Moje dokumenty,
Pulpit, a także foldery systemowe. W wersji .NET 4.0 jest ich 47 i trudno wybrać,
które są ważniejsze, więc nie będę tu ich przytaczał. Pełną listę można
znaleźć w MSDN.
SelectedPath Ścieżka wybrana przez użytkownika.
ShowNewFolderbutton Określa, czy w oknie znajdzie się przycisk do tworzenia nowego folderu.
232 Microsoft Visual C++ 2012. Praktyczne przykłady

Sama procedura użycia okna jest podobna do okna wyboru pliku. Okno wyświetlamy
metodą ShowDialog(). Kiedy użytkownik kliknie OK, wybrany folder pobieramy z wła-
sności SelectedPath. Oto przykład:

Przykład 13.5
Napisz aplikację, która wyświetli pełną ścieżkę wybranego przez użytkownika folderu
w polu TextBox oraz poda łączną wielkość zawartych w nim plików.

Rozwiązanie
Do wykonania tego zadania będą potrzebne wiadomości z poprzedniego rozdziału. Za-
cznij od utworzenia okienkowej aplikacji C++/CLI według przykładu 1.4. Do okna wstaw
przycisk Button, pole TextBox i komponent FolderBrowserDialog. Kliknij TextBox,
rozwiń panel właściwości i ustaw jego właściwość Multiline na true, a następnie zwiększ
rozmiary pola. Teraz kliknij podwójnie na przycisk. Powstanie funkcja obsługi klik-
nięcia button1_Click(). W tej funkcji umieścimy wywołanie okna wyboru folderu.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e)
{
folderBrowserDialog1->Description="Wybierz folder:";
folderBrowserDialog1->RootFolder=Environment::SpecialFolder::Desktop;
if ( folderBrowserDialog1->ShowDialog() ==
System::Windows::Forms::DialogResult::OK) {
//tu będzie obsługa kliknięcia OK
}
}

Zażyczymy sobie, aby w oknie ponad folderami był napis Wybierz folder:, a w oknie
była wyświetlana zawartość pulpitu. Te dwa życzenia realizujemy przez ustawienie wła-
ściwości Description i RootFolder okna folderBrowserDialog1. Potem następuje wy-
wołanie okna, które jest identyczne jak przy oknie zapisu lub odczytu plików.

Teraz trzeba zaprogramować, co się stanie, kiedy użytkownik wybierze folder i kliknie
OK. Zajrzyj do rozdziału o komunikacji z plikami i znajdź tam przykład, w którym poda-
waliśmy wielkości plików w danym folderze. Korzystaliśmy tam z obiektu DirectoryInfo,
przyporządkowując mu folder. Tu folderem będzie ten wybrany w oknie dialogowym,
a podamy nie długości poszczególnych plików, ale ich sumę, którą obliczymy w pętli for.
Oto kod funkcji button1_Click() uzupełniony o zliczanie plików:
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
folderBrowserDialog1->Description="Wybierz folder";
folderBrowserDialog1->RootFolder=Environment::SpecialFolder::Desktop;
if ( folderBrowserDialog1->ShowDialog() ==
System::Windows::Forms::DialogResult::OK) {
System::Int64 suma=0;
DirectoryInfo^ katalog = gcnew DirectoryInfo(folderBrowserDialog1->
SelectedPath);
array<FileInfo^>^ pliki = katalog->GetFiles("*.*");
for (System::Int16 i=0; i<pliki->Length; i++)
suma=suma+pliki[i]->Length;
textBox1->AppendText("Folder "+folderBrowserDialog1->SelectedPath+" zawiera "
+(suma/1048576).ToString()+" MB plików"+System::Environment::NewLine);
}
}
Rozdział 13. ♦ Okna dialogowe 233

Możesz już uruchomić aplikację. Naciśnij przycisk w oknie głównym. Pojawi się
okno wyboru folderu jak na rysunku 13.2.

Rysunek 13.2.
Okno
wyboru folderu

Wybierz jakiś folder i naciśnij OK. W polu TextBox otrzymasz jego pełną ścieżkę i wiel-
kość w MB. Program nie zlicza plików w podfolderach, a jedynie w wybranym folderze.
Możesz ustawić sumowanie również podfolderów, dodając opcję System::IO::-
SearchOption::AllDirectories w funkcji GetFile(), identycznie jak to było w roz-
dziale o plikach.

Okno wyboru koloru


Okno wyboru koloru ColorDialog pozwala na wyświetlenie i obsługę systemowego
okna pozwalającego na wybór koloru z palety. Okno to ma zdefiniowany zestaw kolorów
podstawowych, a dodatkowo możliwy jest wybór dowolnego koloru z całej dostępnej
przestrzeni kolorów RGB. Komponent ColorDialog dołączamy do aplikacji tak jak
komponenty OpenFileDialog i SaveFileDialog. Najczęściej wykorzystywane są właści-
wości sterujące wyświetlaniem całej dostępnej palety kolorów. Przedstawia je tabela 13.6.

Tabela 13.6. Niektóre właściwości okna ColorDialog


Właściwość Opis
AllowFullOpen Określa, czy użytkownik może otworzyć część okna, w której można wybrać
kolory z przestrzeni RGB. W przypadku wartości false okno jest ograniczone
tylko do kolorów podstawowych.
FullOpen W przypadku wartości true okno z pełną paletą RGB jest otwarte już
w momencie wyświetlenia okna dialogowego.
234 Microsoft Visual C++ 2012. Praktyczne przykłady

Użycie tego okna jest bardzo podobne do poprzednich okien, za jego wyświetlenie odpo-
wiada metoda ShowDialog(), a po zamknięciu wybrany kolor znajduje się we właściwości
Color. Ta właściwość jest typu struktury Color i jest akceptowana przez właściwości
określające kolor komponentów.

Przykład 13.6

Napisz aplikację zmieniającą kolor tła w etykiecie Label zgodnie z wyborem użytkowni-
ka. Dodatkowo w polu tekstowym niech wyświetlają się informacje o wybranym kolorze.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w oknie przycisk Button,
etykietę Label oraz pole tekstowe TextBox. Pod oknem na pasku niewidocznych kom-
ponentów umieść okno ColorDialog.

Ustaw właściwość Multiline pola TextBox na true i powiększ jego wymiary.

Po naciśnięciu przycisku Button będzie wyświetlane okno wyboru koloru, a następnie


wybrany kolor będzie przekazywany do właściwości BackColor etykiety. Informacje
o kolorze będą wyświetlane w polu tekstowym po zamianie struktury Color na jej repre-
zentację w postaci łańcucha znakowego za pomocą metody ToString(). Jak widać, meto-
da ToString() jest metodą większości klas obiektów także struktury Color i nie ogra-
nicza się tylko do konwersji liczb na łańcuchy znakowe.

Oto kod, jaki należy przypisać zdarzeniu naciśnięcia przycisku:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
if (colorDialog1->ShowDialog() ==
System::Windows::Forms::DialogResult::OK) {
label1->BackColor=colorDialog1->Color;
textBox1->AppendText(colorDialog1->Color.ToString() +
System::Environment::NewLine);
}
}

Wybór czcionki
Komponent FontDialog reprezentuje systemowe okno wyboru kroju czcionki. Użycie
okna jest analogiczne do poprzedniego. Wygląd okna przedstawia rysunek 13.3, a je-
go najważniejsze właściwości zestawia tabela 13.7.
Rozdział 13. ♦ Okna dialogowe 235

Rysunek 13.3.
Okno FontDialog

Tabela 13.7. Niektóre właściwości okna FontDialog


Właściwość Opis
MaxSize Ogranicza maksymalną wielkość czcionki, jaką można wybrać.
MinSize Ogranicza minimalną wielkość czcionki, jaką można wybrać.
ShowColor Przy wartości true w oknie wyświetlana jest lista wyboru kolorów czcionki.
ShowEffects Wartość true powoduje wyświetlenie opcji czcionki podkreślonej bądź
pochyłej.

Wybrana czcionka jest zwracana we właściwości Font okna FontDialog, dodatkowo


kolor czcionki znajduje się we właściwości Color. Właściwość Font jest obiektem klasy
Font akceptowanym przez właściwości odpowiadające za krój czcionki w kompo-
nentach, natomiast właściwość Color jest to struktura typu Color, tak jak w przypadku
okna wyboru kolorów.

Przykład 13.7

Użyj okna wyboru czcionki do zmiany kroju pisma w etykiecie. Niech będzie możli-
wa zmiana wielkości, rodzaju i koloru czcionki. Dodatkowo wprowadź ograniczenia,
tak aby użytkownik nie mógł wybrać czcionki mniejszej niż 10 i większej niż 16
punktów.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w oknie przycisk Button
i etykietę Label. Pod oknem na pasku niewidocznych komponentów umieść komponent
FontDialog.
236 Microsoft Visual C++ 2012. Praktyczne przykłady

W metodzie obsługującej naciśnięcie przycisku najpierw ustawiamy wymagane parametry


okna, czyli MaxSize, MinSize i ShowColor, następnie wyświetlamy okno i wybraną przez
użytkownika czcionkę podstawiamy jako czcionkę etykiety. Kolor czcionki w etykiecie
zmieniamy za pośrednictwem właściwości ForeColor.

Oto metoda wykonywana po naciśnięciu przycisku:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
fontDialog1->MinSize=10;
fontDialog1->MaxSize=16;
fontDialog1->ShowColor=true;
if (fontDialog1->ShowDialog() ==
System::Windows::Forms::DialogResult::OK) {
label1->Font=fontDialog1->Font;
label1->ForeColor=fontDialog1->Color;
}
}
Rozdział 14.
Możliwości edycji tekstu
w komponencie TextBox

Właściwości pola TextBox


Pole tekstowe TextBox posiada wiele metod i właściwości, które sterują wyświetlaniem
tekstu, i może posłużyć jako prosty edytor ASCII. Główne ograniczenie tak zbudo-
wanego edytora to możliwość zastosowania tylko jednego kroju czcionki dla całego
tekstu. Pomimo tego ograniczenia można używać okna do edycji tekstów ASCII i ich
prezentacji, a nawet istnieje możliwość wzbogacenia tekstu o grafikę.

Najbardziej interesujące właściwości i metody pola TextBox przedstawiają tabele 14.1 i 14.2.

Tabela 14.1. Niektóre właściwości komponentu TextBox


Właściwość Znaczenie
Lines Tabela łańcuchów znakowych typu System::String, z których każdy
reprezentuje jedną linię tekstu. Numeracja wierszy zaczyna się od zera.
Właściwość ta służy tylko do odczytu tekstu.
ReadOnly W przypadku wartości true tekst z okna nie może być edytowany.
Przydatne do prezentacji tekstu.
TextAlign Wyrównanie tekstu, możliwe wartości to:
 HorizontalAlignment::Center — wyrównanie do środka,
 HorizontalAlignment::Left — wyrównanie do lewej,
 HorizontalAlignment::Right — wyrównanie do prawej.
CharacterCasing Zamiana na małe i wielkie litery.
 CharacterCasing::Upper — możliwe pisanie wyłącznie wielkimi literami,
 CharacterCasing::Lower — pisanie wyłącznie małymi literami,
 CharacterCasing::Normal — wielkie lub małe litery, w zależności
od wciśnięcia klawisza Shift.
238 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 14.1. Niektóre właściwości komponentu TextBox (ciąg dalszy)


Właściwość Znaczenie
WordWarp Wartość true oznacza, że tekst, który nie mieści się w wierszu, będzie
przenoszony do wiersza następnego.
W przypadku wartości false tekst wychodzący poza pole nie będzie widoczny.
SelectedText Zawiera tekst aktualnie zaznaczony w polu.

Tabela 14.2. Najczęściej używane metody klasy TextBox


Metoda Działanie
Clear() Usuwa całą zawartość okna.
AppendText Dopisuje łańcuch znakowy tekst na końcu aktualnego tekstu w oknie.
(System::String tekst)()
Copy() Kopiuje zaznaczony tekst do schowka.
Cut() Kopiuje zaznaczony tekst do schowka, wycinając go z okna.
Paste() Wkleja tekst znajdujący się aktualnie w schowku.

Przykład 14.1

Napisz aplikację, która będzie zamieniała wszystkie litery we wpisanym tekście na


wielkie, a następnie zapisywała go do pliku.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw w jej okno pole TextBox,
przycisk Button oraz komponent SaveFileDialog. Właściwość Multiline pola ustaw
na true i zwiększ wymiary pola tak, aby zmieściło kilka linii.

Działanie aplikacji po naciśnięciu przycisku będzie polegało na ustawieniu właściwości


CharacterCasing na wartość CharacterCasing::Upper, a następnie zapisie zawartości
pola do pliku w sposób podany w rozdziale 12.

Metody komunikacji z plikami są częścią przestrzeni nazw System::IO, więc najpierw


trzeba wskazać, że będziemy używali tej przestrzeni (zobacz rozdział 12.).
using namespace System::IO;

Metoda obsługująca naciśnięcie przycisku jest raczej prosta.


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
textBox1->CharacterCasing = CharacterCasing::Upper;

if ( saveFileDialog1->ShowDialog() ==
System::Windows::Forms::DialogResult::OK) {
StreamWriter^ plik = gcnew StreamWriter(saveFileDialog1->FileName);
plik->Write(textBox1->Text);
plik->Close();
}
}
Rozdział 14. ♦ Możliwości edycji tekstu w komponencie TextBox 239

Po uruchomieniu aplikacji, jeśli napiszesz dowolny tekst i naciśniesz przycisk, tekst


zostanie zamieniony na wielkie litery i zapisany do wskazanego pliku.

Kopiowanie i wklejanie tekstu


ze schowka
Kopiowanie i wklejanie tekstu do schowka jest bardzo proste. Realizują je metody
Cut(), Copy() i Paste().

Przykład 14.2

Napisz aplikację z trzema przyciskami: pierwszy kopiuje tekst do schowka, drugi kopiuje
i wycina, a trzeci wkleja tekst ze schowka.

Rozwiązanie
Stwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna pole TextBox oraz
trzy przyciski Button. Do właściwości Text pierwszego przycisku podstaw Kopiuj, do
drugiego Wytnij, a do trzeciego Wklej.

Właściwość Multiline pola ustaw na true i zwiększ wymiary pola tak, aby zmieściło
kilka linii.

Oto metoda obsługująca zdarzenie Click pierwszego przycisku:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
textBox1->Copy();
}

Dla przycisku drugiego, Wytnij, mamy metodę:


private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
textBox1->Cut();
}

A dla trzeciego, Wklej, taką:


private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
textBox1->Paste();
}

Po uruchomieniu aplikacji napisz w polu dowolny tekst, następnie zaznacz część tekstu
myszką — jeżeli teraz naciśniesz przycisk Kopiuj, tekst zostanie skopiowany do schowka,
przycisk Wytnij skopiuje tekst, równocześnie wycinając go, a przycisk Wklej wstawi
tekst ze schowka.
240 Microsoft Visual C++ 2012. Praktyczne przykłady

Wyszukiwanie znaków w tekście


Częstym problemem jest wyszukiwanie jakiegoś ciągu znaków w polu tekstowym.
Można do tego wykorzystać właściwość Lines, która zwraca tablicę łańcuchów zna-
kowych System::String, na których można łatwo przeprowadzać operacje wyszuki-
wania.

Przykład 14.3

Mamy pole tekstowe z wprowadzonym tekstem. Niech po naciśnięciu przycisku z tek-


stu zostaną automatycznie usunięte napisy po dwóch ukośnikach (//), czyli w C++
oznaczające komentarze. Potrzebne będą trzy metody klasy System::String. Zestawiłem
je w tabeli 14.3.

Tabela 14.3. Potrzebne metody klasy System::String


Metoda Opis
Contains(System::String tekst) Sprawdza, czy dany łańcuch znakowy zawiera napis tekst.
Zwraca wartość true lub false.
Remove(Int pozycja) Usuwa część łańcucha znakowego od znaku o numerze
pozycja do końca.
IndeksOf(System::String tekst) Zwraca pozycję początku podłańcucha tekst w tekście.

Rozwiązanie
Znowu będzie nam potrzebna aplikacja C++/CLI. Wykonaj przykład 1.4, do okna wstaw
pole TextBox i przycisk Button. Właściwość Multiline pola ustaw na true i zwiększ
wymiary pola tak, aby zmieściło kilka linii.

Ponieważ właściwość Lines nadaje się tylko do odczytu, nie można bezpośrednio wy-
konywać innych operacji na liniach pola tekstowego. Musimy stworzyć dynamicznie
tabelę łańcuchów System::String linie o długości takiej, aby pomieściła wszystkie
linie pola, a następnie przepisać je do tej tablicy.

Teraz usuniemy całą zawartość pola i tabeli linie, a następnie wpiszemy z powrotem,
usuwając znaki po //. Każdy wiersz tabeli linie jest łańcuchem znakowym System::
String. Najpierw za pomocą metody Contains() sprawdzimy, czy dany wiersz w ogóle
zawiera ukośniki. Jeśli nie zawiera, to przepisujemy go w całości do pola tekstowego.
Jeśli zawiera, to metoda IndexOf() sprawdzi, na jakiej pozycji są ukośniki, a Remove()
usunie wszystko od tej pozycji. Oto cały kod wykonywany po naciśnięciu przycisku:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
array<System::String^>^ linie =
gcnew array<System::String^>(textBox1->Lines->Length);
linie=textBox1->Lines;
textBox1->Clear();
for (System::Int32 i=0;i<linie->Length;i++)
if (linie[i]->Contains("//"))
Rozdział 14. ♦ Możliwości edycji tekstu w komponencie TextBox 241

textBox1->AppendText(linie[i]->Remove(linie[i]->
IndexOf("//"))+System::Environment::NewLine);
else
textBox1->AppendText(linie[i]+System::Environment::NewLine); }

Wstawianie tekstu
między istniejące linie
Pole TextBox nie posiada możliwości wstawiania nowych linii w środek istniejącego
tekstu. W prosty sposób można jednak zaprogramować taką funkcję. Można tę funkcję
wykonać jako metodę nowej klasy komponentu dziedziczącego (pochodnego) po klasie
TextBox, na tym etapie jednak dla ułatwienia zaprogramujemy ją po prostu jako metodę
klasy Form1. Później w rozdziale o programowaniu obiektowym zaprogramujemy wła-
sną kontrolkę MyTextBox z metodą dopisującą.

Przykład 14.4

Zaprogramuj metodę, która wstawi tekst po określonej linii pola TextBox, przesuwając
następne linie w dół.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, wstaw pole TextBox i przycisk
Button. Właściwość Multiline pola ustaw na true i zwiększ wymiary pola tak, aby
zmieściło kilka linii.

Argumentami naszej metody będą kolejno: uchwyt pola TextBox, w które chcemy
wstawić linię, numer linii, po której ma być wstawiony tekst, i sam tekst do wstawienia.

Nasza metoda (nazwałem ją dopisz()) będzie najpierw przepisywała linię tekstu do utwo-
rzonej tablicy, jak w przykładzie 14.3, a następnie czyściła całe pole tekstowe. Teraz
do pola zostaną na powrót wpisane linie od 0 do ostatniej linii przed nowym tekstem,
następnie nowy tekst i reszta linii. Oto cała metoda:
private: System::Void dopisz(System::Windows::Forms::TextBox^ pole_tekst,
System::Int16 nr_linii,System::String^ tekst) {
if (pole_tekst->Lines->Length==0)
return;
if (pole_tekst->Lines->Length<nr_linii)
return;
array<System::String^>^ linie =
gcnew array<System::String^>(pole_tekst->Lines->Length);
linie=pole_tekst->Lines;
pole_tekst->Clear();
for (System::Int32 i=0;i<nr_linii;i++)
pole_tekst->AppendText(linie[i]+System::Environment::NewLine);
pole_tekst->AppendText(tekst+System::Environment::NewLine);
242 Microsoft Visual C++ 2012. Praktyczne przykłady

for (System::Int32 i=nr_linii;i<linie->Length;i++)


pole_tekst->AppendText(linie[i]+System::Environment::NewLine);
}

Dwie początkowe linie zapobiegają błędom. Pierwsza uniemożliwia działanie metody,


gdy pole jest puste, a druga zapobiega próbie wstawienia linii poza ostatnim wierszem
istniejącego tekstu. Całość metody należy napisać ponad metodą button1_Click()
prezentowaną niżej.

Teraz pozostało wywołać tę metodę w metodzie obsługi przyciśnięcia przycisku;


jeżeli chcemy na przykład wstawić tekst po drugiej linii w polu, wystarczy napisać:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
dopisz(textBox1,2,"Linia wstawiona");
}

Po uruchomieniu programu wpisz więcej niż dwie linie tekstu do okna i naciśnij przy-
cisk. Po drugiej linii zostanie wstawiony tekst Linia wstawiona.

Wprowadzanie danych do aplikacji


Źródłem danych dla aplikacji zwykle jest plik dyskowy lub komponent umożliwiający
wpisywanie znaków, właśnie taki jak omawiane pole tekstowe. Współpracę z plikami
dyskowymi opisuje rozdział 12. W przypadku plików tekstowych lub pobierania danych
z pól tekstowych dane te mają postać łańcuchów znakowych. Jeżeli potrzebna jest
liczbowa postać danych, trzeba je poddać konwersji.

Konwersję można przeprowadzić za pomocą opisywanej już metody Parse() znajdują-


cej się w klasach reprezentujących standardowe typy danych. Metoda Parse() umoż-
liwia konwersje w różnych formatach, co zostanie pokazane w dalszej części tego roz-
działu. Oprócz tej metody istnieje jeszcze klasa Convert, zawierająca metody do prostej
konwersji między podstawowymi typami danych.

Prosta konwersja typów


— klasa Convert
Metody klasy Convert mają nazwy To+typ_docelowy i akceptują parametry typu do kon-
wersji. I tak na przykład metoda konwertująca z typu System::String na typ System::
Single nazywa się ToSingle() i akceptuje parametry typu System::String.

Przykład 14.5

Napisz aplikację, która przyjmuje w polu tekstowym parametry typu całkowitego Int32
i zmiennoprzecinkowego Single i wypisuje je w etykietach.
Rozdział 14. ♦ Możliwości edycji tekstu w komponencie TextBox 243

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, umieść w nim dwa pola tekstowe
TextBox, dwie etykiety Label i przycisk Button.

Metoda obsługująca naciśnięcie przycisku będzie miała postać:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Int32 liczba;
System::Single liczba_single;
liczba=Convert::ToInt32(textBox1->Text);
liczba_single=Convert::ToSingle(textBox2->Text);
label1->Text=liczba.ToString();
label2->Text=liczba_single.ToString();
}

Metody konwersji wywołujemy jako statyczne metody klasy Convert. Ponieważ etykieta
Label akceptuje tekst do wypisania jako łańcuch znakowy, należało zamienić liczbę
z powrotem na tę postać za pomocą metody ToString().

Po uruchomieniu aplikacji wpisz liczbę całkowitą w pierwsze okno i liczbę z przecinkiem


w drugie. Po naciśnięciu przycisku liczby te pojawią się w etykietach. Zwróć uwagę,
że w polskich ustawieniach regionalnych separatorem dziesiętnym jest przecinek, nie
kropka. Jeżeli wpiszesz kropkę, program nie zadziała poprawnie.

Zachowaj aplikację do dalszych przykładów.

Konwersja ze zmianą formatu danych


Metoda Parse() oprócz prostej konwersji akceptuje dodatkowe argumenty ustalające
format danych i (lub) informacje regionalne.

Informacje regionalne są zawarte w klasie CultureInfo. Metoda Parse() w jednej ze


swych postaci przyjmuje jako argument obiekty klas implementujących interfejs
IFormatProvider, a klasa CultureInfo dziedziczy po interfejsie IFormatProvider, zatem
można do metody Parse() przekazać obiekt klasy CultureInfo.

Aby konwertować łańcuch znakowy zapisany w innych ustawieniach regionalnych, nale-


ży utworzyć obiekt CultureInfo dla tego regionu i przekazać go do metody Parse().
W ten sposób informujemy ją o sposobie, w jaki jest zapisany tekst, który ma być
konwertowany. Domyślnie przyjmowane są bieżące ustawienia regionalne systemu
Windows. Region definiowany jest za pomocą pięcioznakowych skrótów (na przykład dla
Polski pl-PL); pełny spis tych skrótów można znaleźć w MSDN na stronie opisującej
klasę CultureInfo.

Przykład 14.6
Zmodyfikuj aplikację z poprzedniego przykładu tak, aby akceptowała liczby zmien-
noprzecinkowe z kropką jako separatorem dziesiętnym.
244 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Utworzymy obiekt klasy CultureInfo reprezentujący ustawienia amerykańskie (en-
US) i przekażemy go do metody Parse(), informując ją, że podany łańcuch znakowy
jest pisany w notacji amerykańskiej.

Klasa CultureInfo znajduje się w przestrzeni nazw System::Globalization, przed


skorzystaniem z tej klasy należy załączyć tę przestrzeń za pomocą dyrektywy:
using namespace System::Globalization;

Dyrektywę wpisujemy na początku programu (plik Form1.h), po istniejących dyrek-


tywach using.

Obiekt klasy CultureInfo tworzymy za pomocą metody CreateSpecificCulture(),


której argumentem są skróty oznaczające regiony, w tym przypadku en-US.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Int32 liczba;
System::Single liczba_single;
CultureInfo^ kultura=CultureInfo::CreateSpecificCulture("en-US");
liczba=Int32::Parse(textBox1->Text,kultura);
liczba_single=Single::Parse(textBox2->Text,kultura);
label1->Text=liczba.ToString();
label2->Text=liczba_single.ToString();
}

Teraz aplikacja będzie przyjmowała liczby z kropką.

Oprócz ustawień regionalnych można poinformować metodę konwertującą o sposobie


zapisu łańcucha znakowego za pomocą typu wyliczeniowego NumberStyles. Wartości
typu NumberStyles przedstawia tabela 14.4.

Tabela 14.4. Style łańcucha znakowego zawarte w NumberStyles


Styl Możliwa zawartość łańcucha do konwersji
HexNumber Liczba szesnastkowa.
AllowCurrencySymbol Wyłącznie liczba całkowita ze znakiem waluty. Dla polskich ustawień jest to „zł”.
AllowDecimalPoint Wyłącznie liczba z przecinkiem lub całkowita.
AllowExponent Wyłącznie liczba w notacji naukowej lub całkowita.
AllowHexSpecifier Wyłącznie liczba szesnastkowa.
AllowLeadingSign Wyłącznie liczba całkowita; może zawierać znaki +/– (dodatnia lub ujemna).
AllowLeadingWhite Dozwolone „białe znaki” na początku łańcucha znaków, wyłącznie liczby
całkowite. Białe znaki to znaki o kodach Unicode U+0009, U+000A,
U+000B, U+000C, U+000D i U+0020.
AllowParentheses Liczba całkowita, dozwolony nawias.
AllowThousands Liczba całkowita z dozwolonym separatorem oddzielającym setki od
tysięcy. Dla polskich ustawień regionalnych separatorem jest spacja.
AllowTrailingWhite Dozwolone białe znaki na końcu liczby.
Rozdział 14. ♦ Możliwości edycji tekstu w komponencie TextBox 245

Tabela 14.4. Style łańcucha znakowego zawarte w NumberStyles (ciąg dalszy)


Styl Możliwa zawartość łańcucha do konwersji
AllowTrailingSign Wyłącznie liczba całkowita oraz znaki określające, czy liczba jest dodatnia,
czy ujemna.
Any Kombinacja wszystkich opcji bez AllowHexSpecifier.
Currency Kombinacja wszystkich opcji oprócz AllowHexSpecifier i AllowExponent.
Służy do wprowadzania danych walutowych.
Float AllowLeadingWhite + AllowTrailingWhite + AllowLeadingSign +
AllowDecimalPoint + AllowExponent. Do liczb zmiennoprzecinkowych.
HexNumber AllowLeadingWhite + AllowTrailingWhite + AllowHexSpecifier. Umożliwia
wprowadzanie liczb w układzie szesnastkowym.
Integer AllowLeadingWhite + AllowTrailingWhite + AllowLeadingSign. Liczby
całkowite.
None Dozwolona tylko liczba całkowita niezawierająca nic oprócz cyfr.

Można stosować dowolną kombinację powyższych opcji. Dla wygody wprowadzono


kilka predefiniowanych kombinacji, odpowiadających najczęściej stosowanym przy-
padkom. Jeśli spróbujemy konwertować łańcuch znakowy, który nie odpowiada wyj-
ściowemu formatowi, to działanie programu zostanie przerwane. Na przykład taki za-
pis jest błędny:
Int32::Parse("12,34");

Aby zabezpieczyć się przed nieoczekiwanym wyjściem z aplikacji, trzeba zastosować


mechanizm wyjątków. Pisałem już o wyjątkach w rozdziale 4. W .NET Framework
występuje bardzo rozbudowana hierarchia klas wyjątków, którą oczywiście możemy
wykorzystywać, pisząc programy w VC++ 2012.

Przykład 14.7

Aplikacje z poprzedniego przykładu wyposaż w obsługę wyjątku powstałego przy


podaniu danych do metody Parse()w złym formacie.

Rozwiązanie
Otwórz projekt aplikacji z poprzedniego przykładu. Najpierw powrócimy do polskich
ustawień regionalnych przez skasowanie linii:
CultureInfo^ kultura=CultureInfo::CreateSpecificCulture("en-US");

Metoda Parse() wysyła w przypadku podania liczby w złym formacie wyjątek typu
FormatException. Umieszczamy tę metodę w bloku try. W przechwytującej instrukcji
catch mamy zdefiniowany uchwyt do obiektu, który reprezentuje ten wyjątek. Poprzez
obiekt możemy odczytać informacje o wyjątku. W tym przypadku ograniczymy się do
wyświetlenia komunikatu wyjątku w oknie dialogowym, a następnie wyjścia z funkcji
za pomocą instrukcji return. Oto cała funkcja button1_Click () w nowym kształcie:
246 Microsoft Visual C++ 2012. Praktyczne przykłady

private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {


System::Int32 liczba;
System::Single liczba_single;

try {
liczba=Int32::Parse(textBox1->Text);
}
catch(FormatException^ ex) {
MessageBox::Show(ex->Message,"Bład",
MessageBoxButtons::OK,MessageBoxIcon::Exclamation);
return;
}
liczba_single=Single::Parse(textBox2->Text);
label1->Text=liczba.ToString();
label2->Text=liczba_single.ToString();

Uruchom aplikację, a następnie wpisz liczbę z przecinkiem w górne okno tekstowe i na-
ciśnij przycisk. Wystąpi błąd (i wyjątek), ponieważ spodziewana jest liczba całkowita.
Opis wyjątku z właściwości Message zostanie wyświetlony jak na rysunku 14.1, a program
będzie się wykonywał dalej. Bez obsługi wyjątku program zostałby przerwany.

Rysunek 14.1.
Wyjątek
FormatException

Podobnie mógłbyś umieścić w drugim bloku try również instrukcję konwersji zmiennej
liczba_single. Formatowanie można stosować do wykrywania błędnie wprowadzanych
danych. Problemem jest jednak konieczność zastosowania wyjątków do obsługi ewentu-
alnego błędu. Przy braku przechwytywania wyjątków program po prostu się zakończy.

Konwersja liczby na łańcuch znakowy


Często zachodzi potrzeba odwrotnej konwersji — z liczby na łańcuch znakowy. Służy do
tego metoda ToString(), którą stosowaliśmy w tym rozdziale już wielokrotnie. Po-
dobnie jak Parse(), metoda ta oferuje możliwość formatowania tworzonego łańcucha
znakowego.
Rozdział 14. ♦ Możliwości edycji tekstu w komponencie TextBox 247

W metodzie ToString() mamy możliwość definiowania formatu za pomocą klasy


CultureInfo dla poszczególnych ustawień regionalnych lub poprzez podanie formatują-
cego łańcucha znaków.

Przykład 14.8

Spowoduj, aby liczby wpisane w polskich ustawieniach regionalnych (z przecinkiem jako


separatorem dziesiętnym) były wyświetlane w ustawieniach anglosaskich (z kropką).

Rozwiązanie
Utwórz nową aplikację według przykładu 1.4 . Wstaw do okna dwa pola TextBox, dwie
etykiety Label i przycisk Button. Liczby zmiennoprzecinkowe wpisane w pola tekstowe
wypiszemy w etykietach. Użyjemy tu obiektu CultureInfo z ustawieniami angielskimi
i przekażemy go do metody ToString().

Metoda button1_Click() będzie miała postać:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Single liczba1;
System::Single liczba2;
CultureInfo^ kultura=CultureInfo::CreateSpecificCulture("en-US");
liczba1=Single::Parse(textBox1->Text);
liczba2=Single::Parse(textBox2->Text);
label1->Text=liczba1.ToString(kultura);
label2->Text=liczba2.ToString(kultura);
}

Drugim sposobem formatowania jest przekazanie do metody ToString() łańcucha for-


matującego zawierającego specjalne znaki. Znaki formatujące przedstawia tabela 14.5.

Tabela 14.5. Najważniejsze łańcuchy formatujące metody ToString()


Zawartość Znaczenie (przykłady podane dla polskich ustawień regionalnych)
łańcucha
formatującego
C Do liczby dodawany jest znak waluty zgodnie z ustawieniami regionalnymi.
Dn Liczba całkowita, gdzie n po znaku oznacza liczbę cyfr (np. D4 konwertuje 456
na 0456).
En Notacja naukowa, gdzie n oznacza liczbę cyfr po przecinku (np. E4 konwertuje
34,5 na 3,4500E+001).
Fn Stały punkt dziesiętny, n oznacza liczbę cyfr po przecinku (np. F3 konwertuje
23,7 na 23,700).
Nn Liczba z separatorami rzędów wielkości, n oznacza liczbę cyfr po przecinku
(np. N1 konwertuje 565787,3 na 565 787,3).
P Interpretuje liczby jako procent (np. 0,23 przedstawia jako 23%).
Xn Zamienia podaną liczbę na system szesnastkowy, n oznacza liczbę cyfr (np. X4
konwertuje 123 na 007B).
248 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 14.9

Zmień aplikację z poprzedniego przykładu tak, aby podawała dane z pierwszego pola
tekstowego jako złote, a z drugiego jako liczbę z separatorami rzędów wielkości i dwoma
miejscami po przecinku.

Rozwiązanie
Użyjemy metody ToString() z łańcuchem formatującym, odpowiednio, „C” dla pierw-
szego okna i „N2” dla drugiego.

Oto postać metody obsługującej naciśnięcie przycisku:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Single liczba1;
System::Single liczba2;
liczba1=Single::Parse(textBox1->Text);
liczba2=Single::Parse(textBox2->Text);
label1->Text=liczba1.ToString("C");
label2->Text=liczba2.ToString("N2");
}

Uruchom aplikację. Wynik dla przykładowych danych przedstawia rysunek 14.2.

Rysunek 14.2.
Działanie aplikacji
do konwersji liczb
Rozdział 15.
Komponent tabeli
DataGridView

Podstawowe właściwości komponentu


DataGridView
Komponent DataGridView jest uniwersalnym obiektem do przedstawiania danych tek-
stowych i graficznych w tabeli. Komórki tabeli mogą zawierać nie tylko dane, ale także
komponenty typu Button, listy rozwijane ComboBox, pola wyboru CheckBox i odnośniki
do adresów internetowych. Daje to ogromne możliwości; właściwie w komponencie
DataGridView można wykonać całą kompletną aplikację, podobną do arkuszy kalkulacyj-
nych z szablonami i makrami tworzonych w programach MS Excel czy OpenOffice Calc.

Możliwości wykorzystania komponentu DataGridView są ogromne i ten rozdział na pewno


ich nie wyczerpie. Opiszę tu jedynie podstawy, tak by zrozumieć ideę komponentu i otwo-
rzyć możliwości samodzielnych poszukiwań w bogatej literaturze i opisach tego kom-
ponentu w Internecie.

Na początek zajmiemy się tabelami zawierającymi wyłącznie tekst, ponieważ są naj-


łatwiejsze. Właściwości odnoszące się do całej tabeli zawiera tabela 15.1.

Tabela 15.1. Podstawowe właściwości kontrolki DataGridView


Właściwość Opis
ColumnCount Liczba kolumn tabeli.
RowCount Liczba wierszy tabeli.
AutoSize Przy wartości true rozmiar kontrolki zostanie dopasowany do rozmiaru
tablicy danych.
RowHeadersWidth Szerokość komórki nagłówka wiersza.
250 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 15.1. Podstawowe właściwości kontrolki DataGridView (ciąg dalszy)


Właściwość Opis
ColumnHeadersHeight Wysokość komórki nagłówka kolumny.
Columns[] Zbiór kolumn tabeli (obiekty typu DataGridViewColumn).
Rows[] Zbiór wierszy tabeli (obiekty typu DataGridViewRow).
ReadOnly Wartość true oznacza, że cała tabela jest wyłącznie do odczytu. Nie można
wprowadzać danych z klawiatury.
DefaultCellStyle Właściwość pozwalająca ustalić wygląd całej tabeli (kolor tła, tekstu,
czcionka, format wyświetlania danych itd.).
AllowUserToAddRows Określa, czy użytkownik może dodawać nowe wiersze tabeli przez
wpisanie wartości w ostatnim istniejącym wierszu. Jeśli jest ona ustawiona
na false, nowe wiersze można dodawać tylko przez zmianę wartości
właściwości RowCount w kodzie aplikacji.

W tabelach DataGridView często spotykamy właściwości, które są złożonych typów i same


z kolei posiadają właściwości. Mamy więc do czynienia z pewną hierarchią właściwości.

Komponent DataGridView składa się z kolumn i wierszy, te zaś składają się z komórek.
Zarówno kolumny, wiersze, jak i pojedyncze komórki mają specyficzne właściwości.
Niektóre właściwości wierszy przedstawia tabela 15.2.

Tabela 15.2. Niektóre właściwości obiektu DataGridViewRow, czyli wiersza tabeli DataGridView
Właściwość Opis
Cells[] Zbiór komórek znajdujących się w danym wierszu.
Height Wysokość wiersza w punktach.
Index Numer wiersza.
Resizable Określa, czy można zmieniać wysokość wiersza za pomocą myszy. Możliwe
wartości to:
 System::Windows::Forms::DataGridViewTriState::False — niemożliwa
zmiana wysokości,
 System::Windows::Forms::DataGridViewTriState::True — możliwa zmiana
wysokości,
 System::Windows::Forms::DataGridViewTriState::NotSet — położenie
„neutralne”, zmiana wysokości zależna od innych parametrów.
DefaultCellStyle Właściwość opisująca wygląd wiersza (kolor tła, tekstu, czcionka, format
wyświetlania danych itd.).
HeaderCell Opisuje wygląd i zachowanie pola nagłówka wiersza.
ReadOnly Wartość true powoduje, że wiersz może być wyłącznie do odczytu.

Specjalną rolę pełni ostatni wiersz tabeli DataGridView z gwiazdką. W przypadku wpi-
sania czegokolwiek w tym wierszu zostanie utworzony nowy wiersz tabeli zawierający
wpisaną zawartość, a wiersz z gwiazdką przesunie się niżej.
Rozdział 15. ♦ Komponent tabeli DataGridView 251

Pojedyncza kolumna tabeli, czyli obiekt DataGridViewColumn, ma podobne właściwo-


ści jak DataGridViewRow, nie posiada jednak właściwości Cells, a zamiast właściwości
Height ma właściwość Width, która steruje szerokością kolumny.

Ponieważ obiekt DataGridViewColumn nie zawiera właściwości Cells, wartości w po-


szczególnych komórkach tabeli można odczytywać tylko wierszami.

Właściwości pojedynczych komórek przedstawia tabela 15.3.

Tabela 15.3. Wybrane właściwości obiektu DataGridViewCell, czyli pojedynczej komórki tabeli
DataGridView
Właściwość Opis
Value Wartość umieszczona aktualnie w komórce. Wartość ta jest typu Object, będącego
typem pierwotnym, po którym dziedziczą wszystkie inne typy .Net. Dzięki temu
tabela może zawierać dane różnych typów. Konwersje do pożądanego typu
przeprowadza się zgodnie z zasadami opisanymi w rozdziale 7.
ColumnIndex Numer kolumny, w której znajduje się dana komórka.
RowIndex Numer wiersza, w którym znajduje się dana komórka.
Style Właściwość opisująca wygląd komórki (kolor tła, tekstu, czcionka, format
wyświetlania danych itd.).
Resizable Właściwość umożliwiająca odczyt parametru określającego, czy wielkość komórki
można zmieniać.

Kolumny tabeli można tworzyć na etapie projektowania aplikacji w panelu Properties


kontrolki, korzystając z właściwości Columns, lub podczas działania programu. Wiersze
natomiast można tworzyć tylko za pomocą instrukcji w programie.

Kontrolka DataGridView jest silnie związana z bazami danych. W celu uzyskania dostępu
do danych mamy do dyspozycji jeszcze dwa związane z nią komponenty z działu Data,
mianowicie BindingSource i BindingNavigator. Pierwszy udostępnia dane z bazy i można
go podłączyć bezpośrednio do siatki DataGridView, a drugi służy do nawigacji po tych
danych. W tym rozdziale zajmiemy się samą kontrolką DataGridView, a sposób łącze-
nia z bazą prześledzimy na przykładach w następnym rozdziale.

Przykład 15.1

Wyświetl tabelę składającą się z pięciu wierszy i pięciu kolumn, w każde pole wpisz
sumę indeksu wiersza i kolumny pola.

Rozwiązanie
Utwórz projekt aplikacji okienkowej C++/CLI i wstaw do niego z okna narzędziowego
komponent DataGridView — znajdziesz go w dziale Data.
252 Microsoft Visual C++ 2012. Praktyczne przykłady

W tym przykładzie zarówno kolumny, jak i wiersze utworzymy podczas działania apli-
kacji. Moglibyśmy w tym celu posłużyć się przyciskiem, jednak wtedy tabela byłaby
utworzona dopiero po naciśnięciu przycisku. Jeżeli chcemy mieć tabelę zaraz po urucho-
mieniu programu, należy wpisać kod jej tworzenia do metody stowarzyszonej z jakimś
zdarzeniem wywoływanym przy rozpoczynaniu programu. Takim zdarzeniem jest Load,
które zachodzi przy wyświetlaniu okna aplikacji.

Aby utworzyć metodę i powiązać ją ze zdarzeniem Load, kliknij dwukrotnie okno apli-
kacji (nie kontrolkę DataGridView, ale powierzchnię okna głównego). Zostaniesz prze-
niesiony do kodu aplikacji, gdzie już została utworzona metoda Form1_Load(). Wpi-
sujemy w nią najpierw instrukcję utworzenia w siatce pięciu wierszy i pięciu kolumn.
dataGridView1->ColumnCount=5;
dataGridView1->RowCount=5;

Teraz w dalszym ciągu metody wypełnimy komórki za pomocą dwóch pętli for, ko-
rzystając z właściwości Rows i Cells.
for (System::Int32 i=0;i<5;i++) {
for (System::Int32 j=0;j<5;j++) {
dataGridView1->Rows[i]->Cells[j]->Value=i+j;
}
}

Po kompilacji programu powinna pojawić się tabela wypełniona jak na rysunku 15.1.
Jeżeli tabela nie mieści się w kontrolce, należy zwiększyć wymiary okna aplikacji
i zwiększyć wymiary kontrolki lub ustawić właściwość AutoSize na true.

Rysunek 15.1. Tabela DataGridView wypełniona danymi liczbowymi

Zbudowanej aplikacji użyjemy w następnym przykładzie.


Rozdział 15. ♦ Komponent tabeli DataGridView 253

Zmiana wyglądu tabeli


Zajmiemy się teraz formatowaniem komórek tabeli. Łatwo można zmieniać kolor ko-
lumn i wierszy, czcionkę, jaką są wypisywane dane w tabeli, a także wyrównanie i for-
mat danych.

Za wygląd komórki odpowiada właściwość DefaultCellStyle. Jest ona dostępna zarówno


dla całej tabeli, jak i dla poszczególnych wierszy (poprzez właściwość Rows) i kolumn
(poprzez właściwość Columns). Oznacza to, że można zmieniać wygląd całej tabeli lub
tylko wybranych wierszy i kolumn. Poszczególne komórki mają właściwość Style,
która ma praktycznie te same właściwości co DefaultCellStyle. Właściwość Default-
CellStyle jest obiektem klasy DataGridViewCellStyle, a jej najważniejsze właści-
wości zestawia tabela 15.4. Dodatkowo dla wierszy parzystych mamy właściwość
AlternatingRowsDefaultCellStyle, która przesłania DefaultCellStyle. Jeśli zmieni-
my styl w DefaultCellStyle, to AlternatingRowsDefaultCellStyle za nią nadąży i cała
tabela będzie w jednym stylu. Jeśli przyporządkujemy właściwości AlternatingRows-
DefaultCellStyle inny styl, to parzyste wiersze będą wyświetlone w tym stylu.

Tabela 15.4. Wybrane właściwości DataGridViewCellStyle


Właściwość Opis
Alignment Wyrównanie tekstu w komórkach tabeli. Mamy do wyboru dziesięć rodzajów
ustawienia tekstu, tu przedstawię trzy:
 DataGridViewContentAlignment::MiddleCenter — wyrównanie do środka
komórki,
 DataGridViewContentAlignment::BottomLeft — wyrównanie do lewego
dolnego rogu komórki,
 DataGridViewContentAlignment::TopRight — wyrównanie do prawego
górnego rogu komórki.
BackColor Kolor tła w komórce.
ForeColor Kolor tekstu w komórce.
Font Czcionka używana w komórkach.
Format Określa format wyświetlania danych w komórce tabeli. Format podajemy przez
podstawienie do tej właściwości formatującego łańcucha znaków. Znaki, jakie
może zawierać ten łańcuch, podane są w tabeli 14.5.

Przykład 15.2

Wypełnij wiersze i kolumny nagłówkowe tabeli, jak w przykładzie poprzednim, od-


powiednio numerami wierszy i kolumn. Dodatkowo niech parzyste wiersze mają zielony
kolor.
254 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu.

Aby wyświetlać tytuły kolumn i wierszy, użyjemy właściwości HeaderCell, czyli ko-
mórki nagłówka, odpowiednio dla wierszy i kolumn. Kolumna nagłówka, podobnie
jak kolumny „robocze”, ma właściwość Value, do której podstawiamy opis. Komórka
ta wymaga, aby opis był w formacie łańcucha znakowego.

Kolor nieparzystych wierszy ustawimy za pomocą właściwości DefaultCellStyle,


natomiast parzystych za pomocą AlternatingRowsDefaultCellStyle. Oto kompletny
uzupełniony kod metody Form1_Load():
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=5;
dataGridView1->RowCount=5;
dataGridView1->DefaultCellStyle->BackColor=Color::White;
dataGridView1->AlternatingRowsDefaultCellStyle->BackColor=Color::LightGreen;
for (System::Int32 i=0;i<5;i++) {
for (System::Int32 j=0;j<5;j++) {
dataGridView1->Rows[i]->Cells[j]->Value=i+j;
dataGridView1->Columns[j]->HeaderCell->Value=j.ToString();
}
dataGridView1->Rows[i]->HeaderCell->Value=i.ToString();
}

Równie łatwo można zmieniać kolor i czcionkę tekstu w komórkach tabeli, a także format
wyświetlania danych. Wszystko to jest częścią stylu, a więc może być zmienione
przez właściwość DefaultCellStyle lub AlternatingRowsDefaultCellStyle.

Przykład 15.3

Niech tabela z przykładu 15.1 wyświetla parzyste wiersze czcionką 14 punktów w kolorze
niebieskim. Dodatkowo dane z kolumny pierwszej będą wyświetlane w zapisie wa-
lutowym.

Rozwiązanie
Otwórz aplikację z przykładu 15.1.

Aby zmienić czcionkę i kolor parzystych wierszy, znowu posłużymy się właściwością
AlternatingRowsDefaultCellStyle. Jak już pisałem, jest to właściwość typu obiektu
i ma własne pola. Tym razem, zamiast ustawiać wartości tych pól bezpośrednio, utwo-
rzymy oddzielny obiekt typu DataGridViewCellStyle, który nazwałem styl. Ustawimy
odpowiednio pola w tym obiekcie, a następnie cały obiekt podstawimy do właściwości
AlternatingRowsDefaultCellStyle.

Rodzaj czcionki zmieniamy, podstawiając pod właściwość Font obiektu styl obiekt
typu System::Drawing::Font, który wcześniej utworzymy. Przy tworzeniu tego obiektu
Rozdział 15. ♦ Komponent tabeli DataGridView 255

musimy podać rodzaj czcionki. Posłużymy się tu standardową czcionką SansSerif,


następnie podajemy jej nową wielkość i styl (normalny, pogrubiony, pochylony itp.).
Właściwość ForeColor odpowiada za kolor czcionki. Tak spreparowany obiekt styl
podstawiamy do AlternatingRowsDefaultCellStyle, formatując parzyste wiersze.
Niezależnie można zmieniać format wyświetlania w poszczególnych kolumnach za po-
mocą właściwości Format. Sformatowanie komórek jako walutowych wymaga podania
symbolu c, tak jak to jest zapisane w tabeli 14.5, w rozdziale o formatowaniu wyświe-
tlania zmiennych tekstowych. Aby ustawienie tej właściwości dało efekt, trzeba zde-
finiować rodzaj komórki za pomocą ValueType. W przypadku waluty powinna być to
komórka typu zmiennoprzecinkowego. Makro typeid zwraca identyfikator typu w po-
staci akceptowanej przez ValueType.

Oto cały poprawiony kod metody Form1_Load():


private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=5;
dataGridView1->RowCount=5;
DataGridViewCellStyle^ styl = gcnew DataGridViewCellStyle();
System::Drawing::Font^ czcionka = gcnew
System::Drawing::Font(System::Drawing::FontFamily::GenericSansSerif, 14,
FontStyle::Regular);
styl->Font=czcionka;
styl->ForeColor = System::Drawing::Color::LightBlue;
dataGridView1->AlternatingRowsDefaultCellStyle=styl;
dataGridView1->Columns[1]->ValueType = System::Double::typeid;
dataGridView1->Columns[1]->DefaultCellStyle->Format = "c";
for (System::Int32 i=0;i<5;i++) {
for (System::Int32 j=0;j<5;j++) {
dataGridView1->Rows[i]->Cells[j]->Value=i+j;
}
}
}

Dopasowanie wymiarów komórek


tabeli do wyświetlanego tekstu
Możliwa jest zarówno bezpośrednia zmiana szerokości i wysokości wierszy w tabeli
do zadanych wartości, jak i automatyczne dopasowanie wymiarów do zawartości.
Najpierw zajmijmy się bezpośrednim podawaniem wymiarów dla kolumn i wierszy.

Szerokość kolumn ustawiamy za pomocą właściwości Width, a wysokość wiersza za


pomocą właściwości Height, odpowiednio dla każdej kolumny i wiersza.

Przykład 15.4

Utwórz tabelę 5×5 z kolumnami o szerokości 50 punktów i wierszami o wysokości 20


punktów.
256 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Utwórz projekt aplikacji okienkowej według przykładu 1.4 i wstaw do okna komponent
DataGridView.

W metodzie Form1_Load() umieść następujący kod:


private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=5;
dataGridView1->RowCount=5;
for (System::Int32 i=0;i<dataGridView1->ColumnCount;i++)
dataGridView1->Columns[i]->Width=50;
for (System::Int32 i=0;i<dataGridView1->RowCount;i++)
dataGridView1->Rows[i]->Height=20;
}

Szerokość kolumn można także zmieniać za pomocą okna EditColumns na etapie


tworzenia aplikacji; jest to opisane w dalszej części rozdziału.

Kontrolka DataGridView posiada także metody, które automatycznie dopasowują wymiary


komórek tabeli do tekstu, jaki ta komórka aktualnie zawiera. Są to metody AutoResize-
Columns() i AutoResizeRows().

Przykład 15.5

Utwórz tabelę, w której szerokość i wysokość komórek będą dopasowywane przy


zmianie zawartości.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w nim komponent Data-
GridView.

Kliknij podwójnie okno aplikacji i w metodzie Form1_Load() umieść następujący kod:


dataGridView1->RowCount=5;
dataGridView1->ColumnCount=5;

Kiedy użytkownik kończy wprowadzanie danych z klawiatury do okna tabeli, genero-


wane jest zdarzenie CurrentCellChanged. Wystarczy podpiąć do tego zdarzenia meto-
dę wywołującą obie metody dopasowujące wymiary kolumn i wierszy. Aby utworzyć
metodę i powiązać ją ze zdarzeniem, w widoku projektowania aplikacji (zakładka
Form1.h [Design]) wybierz myszą kontrolkę DataGridView, a następnie w prawym panelu
kliknij ikonę błyskawicy, przechodząc do widoku zdarzeń.

Teraz na liście zdarzeń w dziale Action odszukaj CurrentCellChanged i kliknij podwój-


nie nazwę zdarzenia. W utworzoną metodę dataGridView1_CurrentCellChanged()
wpisz następujący kod:
dataGridView1->AutoResizeColumns();
dataGridView1->AutoResizeRows();
Rozdział 15. ♦ Komponent tabeli DataGridView 257

Uruchom aplikację. Kliknij myszą dowolną komórkę tabeli, aby przejść do trybu edycji.
Teraz wpisz w komórkę dowolny tekst i wciśnij Enter lub kliknij myszą inną komórkę.
Wymiary komórki zostaną dopasowane do tekstu.

Odczytywanie danych z komórek tabeli


Tabela DataGridView posiada właściwości ułatwiające odczyt danych z zaznaczonych
komórek tabeli. Właściwości te przedstawia tabela 15.5.

Tabela 15.5. Właściwości komponentu DataGridView służące do odczytu zaznaczonych komórek


Właściwość Opis
CurrentCell Aktualnie zaznaczona komórka. Właściwość typu DataGridViewCell.
SelectionMode Opisuje, w jaki sposób mogą być zaznaczane wiersze w tabeli:
 DataGridViewSelectionMode::FullRowSelect — wybieranie całych wierszy,
 DataGridViewSelectionMode::FullColumnSelect — wybieranie całych
kolumn,
 DataGridViewSelectionMode::CellSelect — wybieranie pojedynczych
komórek,
 DataGridViewSelectionMode::RowHeaderSelect — wybieranie wierszy
przez kliknięcie nagłówka,
 DataGridViewSelectionMode::ColumnHeaderSelect — wybieranie kolumn
przez kliknięcie nagłówka.
SelectedCells[] W przypadku gdy wybranych jest kilka komórek, właściwość ta zawiera ich
listę ułożoną według kolumn. Lista składa się z obiektów typu
DataGridViewCell.
SelectedRows[] Właściwość zawiera listę zaznaczonych wierszy. Liczą się tylko wiersze
zaznaczone w całości. Najlepiej używać tej właściwości w połączeniu
z właściwością SelectionMode ustawioną na FullRowSelect. Lista składa się
z obiektów typu DataGridViewRow.
SelectedColumns[] Podobnie jak SelectedRows, tylko zawiera listę wybranych kolumn w postaci
obiektów typu DataGridViewColumn.

Najprostszym przypadkiem jest odczyt danych znajdujących się w pojedynczej komórce


tabeli. Można to zrobić na dwa sposoby: wykorzystując właściwość CurrentCell lub
SelectedCells.

Przykład 15.6

Zakładamy, że tabela 5 na 5 elementów zawiera liczby typu System::Single. Niech


kliknięcie przycisku powoduje wypisanie zawartości aktualnie zaznaczonej komórki
tabeli w etykiecie. Wykorzystaj właściwość CurrentCell.
258 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie

Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna tabelę DataGrid-
View, etykietę Label i przycisk Button.

Wymiary tabeli określimy w metodzie Form1_Load() stowarzyszonej ze zdarzeniem


rozpoczęcia działania aplikacji.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=5;
dataGridView1->RowCount=5;
}

Oto gotowa metoda obsługująca naciśnięcie przycisku:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
label1->Text =
(Convert::ToSingle(dataGridView1->CurrentCell->Value).ToString());
}

Uruchom aplikację, a następnie wpisz w komórki dowolne liczby z klawiatury. Teraz


zaznacz dowolną liczbę i naciśnij przycisk. Zawartość komórki pojawi się w etykiecie.

Przykład 15.7

Wykonaj aplikację podającą liczbę zaznaczonych komórek tabeli i sumę liczb z tych
komórek; zastosuj właściwość SelectedCells.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna tabelę DataGri-
dView, dwie etykiety Label i przycisk Button.

Utwórz metodę Form1_Load() o postaci takiej, jak w poprzednim przykładzie.

Liczbę zaznaczonych komórek odczytamy poprzez właściwość Count listy Selected-


Cells. Następnie dodajemy wszystkie elementy tej listy w pętli for. Wartości odczytu-
jemy własnością Value.

Oto odpowiedni kod:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Int32 ile_liczb;
System::Single suma;
ile_liczb=dataGridView1->SelectedCells->Count;
label1->Text=L"Zaznaczyłeś "+ile_liczb.ToString()+L" komórek";
for (System::Int32 i=0;i<ile_liczb;i++)
suma=suma+Convert::ToSingle(dataGridView1->SelectedCells[i]->Value);
label2->Text="Suma liczb z zaznaczonych komórek "+suma.ToString();
}
Rozdział 15. ♦ Komponent tabeli DataGridView 259

Po uruchomieniu aplikacji wpisz liczby w kilka komórek, a następnie zaznacz je


myszką. Po naciśnięciu przycisku pojawi się liczba zaznaczonych komórek oraz suma
wartości z tych komórek.

Przykład 15.8

Użytkownik ma możliwość zaznaczania całych wierszy tabeli. Niech po naciśnięciu


przycisku aplikacja wyświetla w polu tekstowym sumy wartości z poszczególnych
zaznaczonych wierszy. Wykorzystaj właściwość SelectedRows.

Rozwiązanie

Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna tabelę DataGrid-
View, pole tekstowe TextBox i przycisk Button.

Utwórz metodę Form1_Load() o postaci takiej, jak w przykładzie 15.6.

Teraz w widoku projektowania okna aplikacji zaznacz kontrolkę DataGridView, w pa-


nelu właściwości znajdź właściwość SelectionMode i przestaw jej wartość na FullRow-
Select. Kliknij myszką na pole tekstowe i przestaw jego właściwość Multiline na true.
Następnie rozciągnij pole tak, aby obejmowało kilka linii.

Po naciśnięciu przycisku będzie deklarowana tablica o wielkości równej liczbie wybra-


nych wierszy, a następnie sumowane będą kolejne komórki z poszczególnych wierszy
umieszczonych w SelectedRows. Na końcu w pętli wyświetlane będą wyniki. Oto meto-
da obsługująca zdarzenie Click przycisku:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
array<System::Single>^ suma =
gcnew array<System::Single>(dataGridView1->SelectedRows->Count);
for (System::Int32 i=0;i<dataGridView1->SelectedRows->Count;i++) {
for (System::Int32 j=0;j<dataGridView1->ColumnCount;j++)
suma[i]+=
Convert::ToSingle(dataGridView1->SelectedRows[i]->
´Cells[j]->Value);
}
for (System::Int32 z=0;z<dataGridView1->SelectedRows->Count;z++)
textBox1->AppendText(suma[z].ToString()+System::
´Environment::NewLine);
}

Po uruchomieniu programu wpisz w wiersze dowolne liczby. Następnie zaznacz jeden


lub więcej wierszy i naciśnij przycisk. W oknie tekstowym pojawią się sumy liczb z po-
szczególnych wierszy.
260 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 15.9

Wykonaj aplikację jak poprzednio, ale podającą sumy z kolumn.

Rozwiązanie

Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna tabelę DataGrid-
View, pole tekstowe TextBox i przycisk Button.

Aby wprowadzona tabela DataGridView działała poprawnie w trybie wybierania ca-


łych kolumn (właściwość SelectionMode równa FullColumnSelect), należy najpierw
wyłączyć możliwość sortowania wartości w kolumnach. Funkcję sortowania kontrolu-
jemy za pomocą właściwości SortMode dla poszczególnych kolumn. Dopiero po wyłą-
czeniu sortowania można zmienić wartość właściwości SelectionMode na FullColumn-
Select. Opisane wyżej operacje wykonamy w metodzie Form1_Load(). Będzie ona
miała postać:
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=5;
dataGridView1->RowCount=5;
for (System::Int32 i=0;i<dataGridView1->ColumnCount;i++)
dataGridView1->Columns[i]->SortMode=
´DataGridViewColumnSortMode::NotSortable;
dataGridView1->SelectionMode=DataGridViewSelectionMode::
´FullColumnSelect;
}

Mimo że będziemy zaznaczać całe kolumny, nie możemy posłużyć się właściwością
SelectedColumns do odczytu wartości, gdyż nie posiada ona właściwości Cells i nie
daje możliwości dostępu do danych w kolumnie. Zamiast tego wykorzystamy wła-
ściwości SelectedCells, w której zaznaczone komórki ułożone są kolumnami. Wła-
ściwość SelectedColumns wykorzystamy jedynie do określenia liczby zaznaczonych
kolumn.

Oto odpowiednia metoda zdarzenia Click dla przycisku:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
array<System::Single>^ suma =
gcnew array<System::Single>(dataGridView1->SelectedColumns->Count);
for (System::Int32 i=0;i<dataGridView1->SelectedColumns->Count;i++) {
for (System::Int32 j=0;j<dataGridView1->RowCount;j++)
suma[i]+= Convert::ToSingle(dataGridView1->SelectedCells[i *
´dataGridView1->RowCount+j]->Value);
}
for (System::Int32 z=0;z<dataGridView1->SelectedColumns->Count;z++)
textBox1->AppendText(suma[z].ToString()+System::
´Environment::NewLine);
}

Po wpisaniu wartości do komórek i zaznaczeniu jednej lub kilku z nich otrzymamy


sumy wartości.
Rozdział 15. ♦ Komponent tabeli DataGridView 261

Zmiana liczby komórek


podczas działania aplikacji
Dodawanie i odejmowanie wierszy oraz kolumn jest możliwe za pomocą metod ope-
rujących na właściwościach Rows i Columns tabeli, czyli na obiektach klas, odpowied-
nio, DataGridViewRowCollection i DataGridViewColumnCollection. Możliwości doda-
wania i wycinania wierszy są większe niż kolumn, ponieważ poprzez obiekt DataGrid-
ViewColumn nie ma bezpośredniego dostępu do danych w tabeli. Zestawienie metod dla
wierszy przedstawia tabela 15.6, a dla kolumn tabela 15.7.

Tabela 15.6. Metody operacji na wierszach tabeli


Metoda Działanie
Add() Dodaje nowy wiersz do tabeli.
Add(System::Int32 n) Dodaje n nowych wierszy do tabeli.
Add(array<System::Object>^ tabela) Dodaje wiersz i wypełnia go danymi z tablicy tabela.
Add(DataGridViewRow wiersz) Dodaje wiersz i wypełnia go danymi z podanego
wiersza. Wiersz podany jako argument nie może
należeć do żadnej kontrolki DataGridView.
AddCopies(System::Int32 Dodaje do tabeli kopie liczba wcześniejszych wierszy,
start,System::Int32 liczba) zaczynając od wiersza start.
AddCopy(System::Int32 nr_wiersza) Dodaje kopię pojedynczego wiersza o numerze
nr_wiersza.
Insert(System::Int32 numer, Wstawia wiersze do tabeli poniżej wiersza numer
array<System::Object>^tabela) i wypełnia je danymi z tabeli tabela.
InsertCopies(System::Int32 Wstawia liczba istniejących wierszy, poczynając od
źródło_pozycja,System::Int32 wiersza źródło_pozycja, do tabeli poniżej wiersza
cel_pozycja,System::Int32 liczba) cel_pozycja.
InsertCopy(System::Int32 Kopiuje istniejący wiersz o numerze źródło_pozycja
źródło_pozycja, System::Int32 do tabeli poniżej wiersza cel_pozycja.
cel_pozycja)
InsertRange(System::Int32 pozycja, Wstawia tablicę wierszy DataGridViewRow poniżej
array<DataGridViewRow^>^ wiersze) wiersza pozycja. Wiersze podane jako argument nie
mogą należeć do żadnej kontrolki DataGridView.
Remove(DataGridViewRow wiersz) Usuwa z tabeli wiersz wiersz.
RemoveAt(System:Int32 numer) Usuwa wiersz o numerze numer.
262 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 15.7. Metody operacji na kolumnach tabeli


Metoda Działanie
Add(System::String^ Dodaje do tabeli kolumnę o podanej nazwie i tekście
nazwa_kolumny,System::String^ tekst wiersza nagłówka.
nagłówka)
Add(DataGridViewColumn kolumna) Dodaje do tabeli kolumnę typu DataGridViewColumn
kolumna; kolumna podana jako argument nie może
należeć do żadnej kontrolki DataGridView.
AddRange(array<DataGridViewColumn^>^ Dodaje do tabeli tablicę kolumn typu DataGridView;
kolumny kolumny podane jako argument nie mogą należeć do
żadnej kontrolki DataGridView.
Insert(System::String pozycja, Wstawia do tabeli kolumnę kolumna na pozycji
DataGridViewColumn kolumna) pozycja; kolumna podana jako argument nie może
należeć do żadnej kontrolki DataGridView.
Remove(DataGridViewColumn^ kolumna) Usuwa kolumnę kolumna z tabeli.
Remove(System::String nazwa) Usuwa kolumnę o podanej nazwie.
RemoveAt(System::Int32 numer) Usuwa kolumnę o podanym numerze.

Przykład 15.10
Niech aplikacja zawiera dwa przyciski: jeden dodaje podaną liczbę wierszy do tabeli,
a drugi usuwa ostatni wiersz.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, wstaw siatkę DataGridView, pole
TextBox oraz dwa przyciski Button.

Niech nowa tabela na początku ma tylko jeden wiersz i trzy kolumny. Utwórz je tak jak
poprzednio, w metodzie Form1_Load() wykonywanej przy zdarzeniu Load, czyli przy wy-
świetleniu okna głównego.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=3;
dataGridView1->RowCount=1;
}

We właściwość Text pierwszego przycisku wstaw słowo Dodaj, a drugiego Usuń. Oto
kod wykonywany przy naciśnięciu przycisku Dodaj:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->Rows->Add(Convert::ToInt16(textBox1->Text));
}

A to metoda przycisku Usuń:


private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->Rows->RemoveAt(dataGridView1->RowCount-1);
}
Rozdział 15. ♦ Komponent tabeli DataGridView 263

Jak widać, metoda button2_Click() usuwa ostatni wiersz siatki. Ten wiersz rozpoczyna
się gwiazdką i ma specjalny status, wpisanie w nim dowolnej wartości spowoduje do-
danie nowego wiersza. Ponieważ jest specjalny, nie można go usunąć. Można jednak
zażyczyć sobie, aby tego wiersza nie było, ustawiając właściwość AllowUserToAddRows
siatki DataGridView na false. Wtedy dopiero program zadziała. Kliknij więc kontrolkę
DataGridView w oknie aplikacji, rozwiń panel Properties i ustaw AllowUserToAddRows na
false. Po uruchomieniu aplikacji w polu tekstowym podajemy liczbę wierszy do doda-
nia. Wygląd aplikacji przedstawia rysunek 15.2.

Rysunek 15.2.
Dodawanie wierszy
do tabeli

Ostatni wiersz tabeli, z gwiazdką po lewej stronie, nie może być usunięty.

Przykład 15.11

Do aplikacji z poprzedniego przykładu dodaj trzeci przycisk, za pomocą którego będzie


można dodawać kolumnę typu DataGridViewColumn.

Rozwiązanie
Do aplikacji z poprzedniego przykładu dodaj trzeci przycisk Button.

We właściwości Text trzeciego przycisku wpisz Dodaj kolumnę. W celu uzyskania lep-
szego efektu możesz tak zmienić wymiary przycisku, aby tekst pojawił się w dwóch
wierszach (przyciski mają włączoną opcję zawijania wierszy).

W metodzie obsługującej zdarzenie Click przycisku najpierw utworzymy obiekt ko-


mórki tekstowej, czyli takiej, jakimi się do tej pory posługiwaliśmy. Następnie wykorzy-
stamy ten obiekt do zadeklarowania kolumny tabeli z komórkami tekstowymi i w końcu
dodamy utworzoną kolumnę do tabeli dataGridView1. Dodatkowo w nagłówku ko-
lumny wpiszemy słowo Nowa. Oto odpowiednia metoda:
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
DataGridViewTextBoxCell^ komorka = gcnew DataGridViewTextBoxCell();
DataGridViewColumn^ kol = gcnew DataGridViewColumn(komorka);
kol->HeaderCell->Value="Nowa";
dataGridView1->Columns->Add(kol);
}
264 Microsoft Visual C++ 2012. Praktyczne przykłady

Po uruchomieniu programu każde naciśnięcie przycisku Dodaj kolumnę spowoduje


dodanie nowej kolumny do tabeli.

Przykład 15.12

Mamy tabelę DataGridView. Niech po naciśnięciu przycisku wybrany wiersz zostanie


skopiowany na końcu tabeli.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, wstaw do okna siatkę DataGri-
dView, pole TextBox oraz przycisk Button.

Utwórz tabelę o trzech kolumnach i pięciu wierszach, w celu odróżnienia wierszy


zrób tło każdego w innym kolorze. Tworzenie tabeli i kolorowanie wierszy będzie się
odbywać w metodzie obsługującej zdarzenie Load dla głównego okna.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=3;
dataGridView1->RowCount=5;
dataGridView1->Rows[0]->DefaultCellStyle->BackColor=
´System::Drawing::Color::AliceBlue;
dataGridView1->Rows[1]->DefaultCellStyle->BackColor=
´System::Drawing::Color::Beige;
dataGridView1->Rows[2]->DefaultCellStyle->BackColor=
´System::Drawing::Color::Bisque;
dataGridView1->Rows[3]->DefaultCellStyle->BackColor=
´System::Drawing::Color::Blue;
dataGridView1->Rows[4]->DefaultCellStyle->BackColor=
´System::Drawing::Color::ForestGreen;
}

W polu TextBox będziemy podawać numer wiersza do skopiowania. Wykorzystamy


metodę InsertCopy(). Kopiowanie zrealizujemy za pomocą metody uruchamianej po
naciśnięciu przycisku.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->Rows->
´InsertCopy(Convert::ToInt32(textBox1->Text),dataGridView1->RowCount-1);
}

Po uruchomieniu aplikacji wpisz numer wiersza w pole tekstowe i naciśnij przycisk


— wiersz zostanie skopiowany na końcu tabeli.

Skopiowanie wiersza za pomocą metody InsertCopy() lub InsertCopies() nie ozna-


cza skopiowania wartości w tym wierszu, a jedynie samego obiektu wiersza z jego
właściwościami.
Rozdział 15. ♦ Komponent tabeli DataGridView 265

Tabela DataGridView
z komórkami różnych typów
Do tej pory posługiwaliśmy się tylko tabelami z komórkami umożliwiającymi wpro-
wadzenie tekstu lub liczb. Tabela może zawierać także grafikę i (lub) niektóre kom-
ponenty wizualne. Zestawienie typów komórek zawiera tabela 15.8.

Tabela 15.8. Typy komórek w tabeli DataGridView


Nazwa typu Rodzaj komórki
DataGridViewTextBoxCell Komórka tekstowa.
DataGridViewButtonCell Komórka z przyciskiem Button.
DataGridViewCheckBoxCell Komórka z polem wyboru CheckBox.
DataGridViewComboBoxCell Komórka zawierająca listę rozwijaną ComboBox.
DataGridViewImageCell Komórka z grafiką.
DataGridViewLinkCell Komórka z odnośnikiem internetowym.

Tablicę można zaprojektować albo na etapie tworzenia aplikacji, używając okna


EditColumns, albo dynamicznie już w czasie działania programu.

Przy tworzeniu tabeli z komórkami różnych typów za pomocą edytora obowiązuje za-
sada, że w danej kolumnie mogą się znajdować komórki tylko jednego typu. W przy-
padku tworzenia tabeli dynamicznie istnieje możliwość umieszczenia komórek róż-
nych typów w jednej kolumnie.

Rysunek 15.3 przedstawia okno Edit Columns; uruchamiamy je poprzez wybranie


w widoku projektowania aplikacji kontrolki DataGridView, a następnie otwarcie zakład-
ki Properties, odszukanie w panelu właściwości Columns i kliknięcie przycisku wielo-
kropka (…) po prawej stronie tej właściwości.

Rysunek 15.3. Okno Edit Columns — edytor kolumn w tabeli


266 Microsoft Visual C++ 2012. Praktyczne przykłady

Za pomocą okna Edit Columns projektujemy pierwszy wiersz tabeli, następnie wyma-
ganą liczbę wierszy dodajemy już podczas działania aplikacji za pomocą właściwości
RowCount, jak poprzednio.

Przykład 15.13

Zaprojektuj tabelę zawierającą po jednej kolumnie komórek z każdego możliwego typu.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna siatkę DataGridView.

Teraz kliknij komponent siatki w oknie, w panelu Properties znajdź właściwość Columns
i kliknij przycisk wielokropka (…) po jej prawej stronie.

W edytorze kolumn kliknij przycisk Add, następnie z listy Type wybierz kolumnę typu
DataGridTextBoxColumn i kliknij Add.

W ten sam sposób dodawaj inne dostępne na liście rodzaje kolumn. Lista kolumn będzie
się tworzyła w oknie Selected Columns.

Po dodaniu wszystkich typów kliknij Close, aby zamknąć okno Add Column.

W oknie edytora kolumn z prawej strony listy Selected Columns znajduje się okno po-
kazujące właściwości zaznaczonej kolumny. Można tu na przykład zmieniać szerokość
kolumny (właściwość Width).

Kliknij przycisk OK w oknie Edit Columns.

Utwórz metodę Form1_Load() i ustaw właściwość RowCount na 3, aby uzyskać trzy wiersze.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->RowCount=3;
}

Uruchom aplikację. Otrzymasz tabelę z trzema wierszami, z których każdy będzie


zawierał komórki wszystkich możliwych typów ułożone w kolumnach.

Przykład 15.14

Zaprojektuj dynamicznie tabelę 4×2 zawierającą w jednej kolumnie komórki różnych


typów.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna siatkę DataGrid-
View.

Aby program działał poprawnie, potrzebny jest dowolny rysunek w formacie .jpg o na-
zwie rysunek.jpg. Należy go zapisać w folderze z plikami źródłowymi aplikacji.
Rozdział 15. ♦ Komponent tabeli DataGridView 267

Dynamiczne dodawanie wierszy jest podobne do dynamicznego dodawania kolumn tabe-


li, które już ćwiczyłeś w przykładzie 15.10. Najpierw tworzymy obiekty komórek, a póź-
niej wypełniamy nimi wiersze. Niektóre komórki, takie jak DataGridViewImageCell
i DataGridViewCheckBoxCell, wymagają ustawienia właściwości Value w komórce na
dozwoloną wartość przed dodaniem do tabeli.

Po wypełnieniu wiersza komórkami dodajemy go do tabeli. Liczba komórek w wierszu


nie może być większa niż liczba kolumn w tabeli określona wartością ColumnCount.
Całość tworzenia tabeli zrealizujemy w metodzie Form1_Load(). Podany sposób nie jest
najkrótszy, ale dobrze ilustruje proces dynamicznego tworzenia tabeli.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=4;
DataGridViewTextBoxCell^ komorka = gcnew DataGridViewTextBoxCell();
DataGridViewImageCell^ komorka1 = gcnew DataGridViewImageCell();
komorka1->Value=System::Drawing::Bitmap::FromFile("rysunek.jpg");
DataGridViewTextBoxCell^ komorka2 = gcnew DataGridViewTextBoxCell();
DataGridViewButtonCell^ komorka3 = gcnew DataGridViewButtonCell();
DataGridViewRow^ wiersz = gcnew DataGridViewRow();
wiersz->Cells->Add(komorka);
wiersz->Cells->Add(komorka1);
wiersz->Cells->Add(komorka2);
wiersz->Cells->Add(komorka3);
dataGridView1->Rows->Add(wiersz);
DataGridViewTextBoxCell^ komorka10 = gcnew DataGridViewTextBoxCell();
DataGridViewCheckBoxCell^ komorka11 = gcnew DataGridViewCheckBoxCell();
komorka11->ValueType=System::Boolean::typeid;
komorka11->Value=false;
DataGridViewComboBoxCell^ komorka12 = gcnew DataGridViewComboBoxCell();
DataGridViewTextBoxCell^ komorka13 = gcnew DataGridViewTextBoxCell();
DataGridViewRow^ wiersz1 = gcnew DataGridViewRow();
wiersz1->Cells->Add(komorka10);
wiersz1->Cells->Add(komorka11);
wiersz1->Cells->Add(komorka12);
wiersz1->Cells->Add(komorka13);
dataGridView1->Rows->Add(wiersz1);
dataGridView1->AutoResizeRows();
}

Wygląd tabeli po uruchomieniu przedstawia rysunek 15.4.

Rysunek 15.4.
Tabela DataGridView
z różnymi typami
komórek

Zajmiemy się teraz bliżej poszczególnymi typami komórek.


268 Microsoft Visual C++ 2012. Praktyczne przykłady

Przyciski w komórkach
— DataGridViewButtonCell
Komórki tego typu pełnią funkcję przycisku. Ponieważ te przyciski są tak naprawdę
komórkami tabeli, nie mają oddzielnego zdarzenia Click przy naciśnięciu. Odebranie zda-
rzenia naciśnięcia przycisku odbywa się przez zdarzenie Click dla całej tabeli. W meto-
dzie związanej z tym zdarzeniem należy rozpoznać, że została naciśnięta komórka
przycisku, i wykonać operacje, które dany przycisk ma obsługiwać.

Przykład 15.15

W tabeli składającej się z dwóch kolumn umieść na dole pierwszej kolumny przycisk,
który będzie wyświetlał średnią z liczb w drugiej kolumnie. Wygląd aplikacji przed-
stawia rysunek 15.5.

Rysunek 15.5.
Tabela z komórką
DataGridViewButtonCell

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna tabelę DataGrid-
View.

Całą tablicę zadeklarujemy w metodzie Form1_Load(). Dla wierszy tekstowych wystarczy


ustawić odpowiednie wartości RowCount i ColumnCount, natomiast ostatni wiersz z przy-
ciskiem utworzymy dynamicznie, jak w przykładzie 15.14.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=2;
dataGridView1->RowCount=5;
DataGridViewButtonCell^ komorka0 = gcnew DataGridViewButtonCell();
DataGridViewTextBoxCell^ komorka1 = gcnew DataGridViewTextBoxCell();
komorka0->Style->BackColor=System::Drawing::Color::LightGreen;
komorka0->Value="Średnia";
DataGridViewRow^ wiersz = gcnew DataGridViewRow();
wiersz->Cells->Add(komorka0);
wiersz->Cells->Add(komorka1);
dataGridView1->Rows->Add(wiersz);
}
Rozdział 15. ♦ Komponent tabeli DataGridView 269

Teraz pozostało tylko oprogramowanie zdarzenia Click dla tabeli DataGridView. Klik-
nięcie komórki tabeli powoduje jej zaznaczenie oraz wygenerowanie zdarzenia Click.
Po zaznaczeniu komórki można uzyskać do niej dostęp przez właściwość CurrentCell
tabeli. Tak więc metoda wykonywana po zdarzeniu Click dla tabeli odczytuje współ-
rzędne klikniętej komórki poprzez właściwość CurrentCell. Kliknij pojedynczo na
kontrolkę tabeli, rozwiń panel Properties. Przełącz się na widok zdarzeń i znajdź zdarze-
nie Click. Kliknij po prawej stronie nazwy zdarzenia. Zostanie wygenerowana metoda
dataGridView1_Click(). Jeżeli kliknięto przycisk, metoda ta wylicza średnią i umiesz-
cza ją w polu na prawo od przycisku.
private: System::Void dataGridView1_Click(System::Object^ sender,
System::EventArgs^ e) {
System::Single suma=0;
if ((dataGridView1->CurrentCell->ColumnIndex==0) &&
(dataGridView1->CurrentCell->RowIndex==4)) {
for (System::Int32 i=0;i<4;i++)
suma+=Convert::ToSingle(dataGridView1->Rows[i]->Cells[1]->Value);
dataGridView1->Rows[4]->Cells[1]->Value=suma/4;
}
}

Komórki z polami wyboru


— DataGridViewCheckBoxCell
Zawierają pole wyboru, które może mieć dwa stany: true lub false. Stany odczytujemy
za pomocą właściwości Value, która w tym przypadku jest typu System::Boolean.

Przykład 15.16

Napisz program składający się z tabeli DataGridView o dwóch kolumnach i pięciu wier-
szach. Pierwsza kolumna będzie zawierała komórki DataGridViewCheckBoxCell, zaś
druga dowolne liczby. Niech po naciśnięciu przycisku w ostatnim wierszu w drugiej
kolumnie pojawia się suma liczb, przy których komórki DataGridViewCheckBoxCell są
zaznaczone.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna siatkę DataGrid-
View oraz przycisk Button.

Pierwszy wiersz tabeli stworzymy za pomocą edytora kolumn. Aby go uruchomić,


wybierz komponent DataGridView w budowanym oknie aplikacji, następnie w panelu
właściwości wybierz właściwość Columns i kliknij przycisk po prawej stronie.

Posługując się edytorem, jak w przykładzie 15.13, wstaw kolumnę DataGridViewCheck-


BoxColumn, a następnie DataGridViewTextBoxColumn.
270 Microsoft Visual C++ 2012. Praktyczne przykłady

Wyjdź z edytora. Teraz należy ustawić liczbę wierszy w tabeli i początkowe wartości
komponentów CheckBox w komórkach tabeli. Zrobimy to za pomocą metody Form1_
Load(), którą powiążemy ze zdarzeniem Load okna. Aby ją utworzyć, wystarczy kliknąć
dwukrotnie obszar okna aplikacji, a następnie wpisać zawartość.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->RowCount=5;
for (System::Int32 i=0;i<dataGridView1->RowCount;i++)
dataGridView1->Rows[i]->Cells[0]->Value=false;
}

Teraz zajmiemy się obsługą kontrolek w tabeli. W metodzie obsługi naciśnięcia przycisku
w pętli for będziemy sumować te komórki drugiej kolumny, przy których w pierwszej
kolumnie mamy zaznaczenie (wartość true).

W widoku projektowania aplikacji naciśnij podwójnie przycisk. Utworzy się metoda


obsługi, w której umieścimy sumowanie zgodnie z opisem w poprzednim zdaniu.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Single suma=0;
for (System::Int32 i=0;i<dataGridView1->RowCount;i++)
if ((System::Boolean)(dataGridView1->Rows[i]->Cells[0]->Value)==true)
suma+=Convert::ToSingle(dataGridView1->Rows[i]->Cells[1]->Value);
dataGridView1->Rows[dataGridView1->RowCount-1]->Cells[1]->Value=suma;
}

Zauważ, że wartość odczytywana z komórki wymaga jawnej konwersji do typu System::


Boolean.

Grafika w tabeli
— komórka DataGridViewImageCell
Komórka tego typu ma wartość Value typu Bitmap, czyli może wyświetlać rysunki
z map bitowych. Posiada ona dwie ważne właściwości sterujące wyświetlaniem grafiki.
Przedstawia je tabela 15.9.

Tabela 15.9. Specyficzne właściwości komórki DataGridViewImageCell


Właściwość Opis
ImageLayout Określa sposób wyświetlania grafiki w komórce tabeli:
 DataGridViewImageCellLayout::Normal — grafika jest wyświetlana
w oryginalnych rozmiarach, wyśrodkowana w komórce.
 DataGridViewImageCellLayout::NotSet — wartość nieustawiona. W praktyce
efekt jest taki, jak w opcji Normal.
 DataGridViewImageCellLayout::Stretch — rozciągnięcie rysunku tak,
aby zajmował całą powierzchnię komórki.
 DataGridViewImageCellLayout::Zoom — powiększenie rysunku tak, aby
zajmował jak największą powierzchnię komórki, ale bez zmiany jego proporcji.
ValueIsIcon Wartość true oznacza, że grafika w komórce jest typu Icon. Służy do wyświetlania ikon.
Rozdział 15. ♦ Komponent tabeli DataGridView 271

Przykład 15.17

Wygeneruj siatkę DataGridView z jedną kolumną i dwoma wierszami. Wyświetl w pierw-


szym wierszu dowolny rysunek w formacie .jpg tak, aby był dostosowany do wielkości
komórki, ale niezdeformowany. W drugim wierszu wyświetl jedną ze standardowych
ikon systemu Windows.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna siatkę Data-
GridView.

Pierwszy wiersz tabeli znowu stworzymy za pomocą edytora kolumn. Posługując się
edytorem, jak w poprzednich przykładach, wstaw kolumnę DataGridViewImageColumn.
Właściwość Cells, za pomocą której odczytywaliśmy i zapisywaliśmy wartości w ko-
mórkach tabeli, zwraca obiekty typu DataGridViewCell. Jest tak nawet wtedy, gdy kolum-
ny tabeli są stworzone edytorem jako kolumny zawierające komórki z grafiką. Klasa
DataGridViewImageCell dziedziczy po tej klasie, ale poprzez właściwość Cells nie mamy
dostępu do pól właściwych tylko dla DataGridViewImageCell. Nie możemy więc zmie-
niać wartości właściwości, na przykład tych z tabeli 15.9. Pokonać tę trudność można
na dwa sposoby. Można rzutować właściwość Cells w dół, co nie jest — jak wiesz —
najlepszym pomysłem. Drugim sposobem jest utworzenie komórki typu DataGridView-
ImageCell niezależnie od tabeli DataGridView, następnie ustawienie właściwości w tej
komórce i przyporządkowanie jej do tabeli w wybranym miejscu.

Tworzenie tabeli i wyświetlanie rysunków zrealizujemy w metodzie Form1_Load().


Kolejno ustawimy w niej liczbę kolumn i wierszy w tabeli, a następnie powiększymy
wysokość wierszy w celu uzyskania lepszej widoczności rysunków. Teraz utworzymy
dwie komórki typu DataGridViewImageCell niezależnie od tabeli. Pod właściwość Value
pierwszej komórki podstawiamy rysunek z pliku rysunek.jpg i ustawiamy właściwość
ImageLayout komórki na Zoom, aby uzyskać powiększenie bez deformacji. Gotową
komórkę podstawiamy do tabeli w pierwszym wierszu i pierwszej (jedynej) kolumnie.

Teraz tworzymy obiekt typu Icon z jednej z ikon systemowych, a dalej postępujemy
według identycznej zasady jak przy pierwszej komórce. Deklarujemy komórkę Data-
GridViewImageCell, ustawiamy jej właściwość ValueIsIcon na true, a następnie pod-
stawiamy obiekt ikony pod właściwość Value komórki. Wreszcie podstawiamy komórkę
w odpowiednie miejsce tabeli. Oto cała metoda:
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=1;
dataGridView1->RowCount=2;
dataGridView1->Rows[0]->Height=70;
dataGridView1->Rows[1]->Height=70;
DataGridViewImageCell^ KomorkaImage = gcnew DataGridViewImageCell;
DataGridViewImageCell^ KomorkaImage1 = gcnew DataGridViewImageCell;
KomorkaImage->Value=System::Drawing::Bitmap::FromFile("rysunek.jpg");
KomorkaImage->ImageLayout=DataGridViewImageCellLayout::Zoom;
dataGridView1->Rows[0]->Cells[0]=KomorkaImage;
System::Drawing::Icon^ ikona =
´gcnew System::Drawing::Icon(SystemIcons::Asterisk,10,10);
272 Microsoft Visual C++ 2012. Praktyczne przykłady

KomorkaImage1->ValueIsIcon = true;
KomorkaImage1->Value=ikona;
dataGridView1->Rows[1]->Cells[0]=KomorkaImage1;
}

Efekt działania programu przedstawia rysunek 15.6.

Rysunek 15.6.
Wyświetlanie grafiki
w komórkach
DataGridViewImageCell

Komórka z listą rozwijaną


— DataGridViewComboBoxCell
Komórka daje możliwość umieszczenia w tabeli listy rozwijanej, czyli listy opcji, z któ-
rych użytkownik może wybrać jedną. Jej najczęściej wykorzystywane właściwości
zawiera tabela 15.10.

Tabela 15.10. Niektóre właściwości komórki DataGridViewComboBoxCell


Właściwość Opis
Items[] Zawiera opcje na liście do wyboru.
Sorted Właściwość określająca, czy elementy na liście mają być posortowane
alfabetycznie.
FlatStyle Sposób wyświetlania listy:
 System::Windows::Forms::FlatStyle::Flat — lista płaska, bez efektu wklęsłości.
 System::Windows::Forms::FlatStyle::Popup — lista płaska, kiedy znajdzie się
nad nią kursor myszy, staje się wklęsła.
 System::Windows::Forms::FlatStyle::Standard — lista z wrażeniem wklęsłości.
 System::Windows::Forms::FlatStyle::System — wygląd listy jest określony
przez ustawienia systemu operacyjnego.
Rozdział 15. ♦ Komponent tabeli DataGridView 273

Przykład 15.18

Napisz aplikację zawierającą tabelę z jedną kolumną i trzema wierszami. Wszystkie


komórki niech będą typu DataGridViewComboBoxCell. Na listach wyboru umieść na-
zwy trzech kolorów. Obok w oknie umieść trzy etykiety Label; kolor tła w tych etykie-
tach będzie można ustawić na listach wyboru.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, zawierający tabelę DataGridView
i trzy etykiety Label.

W edytorze kolumn wstaw kolumnę DataGridViewComboBoxColumn.

Wpisy do listy można dodawać przez edytor kolumn za pomocą właściwości Items
lub w trakcie działania programu za pomocą metody Add() właściwości Items. Tak
jak poprzednio, jest jednak problem z dostępem do tej właściwości, ponieważ jest ona
zdefiniowana w klasie DataGridViewComboBoxCell, a właściwości tabeli są typu Data-
GridViewCell. Rozwiążemy ten problem tak jak w poprzednim przykładzie: tworząc
oddzielne komórki DataGridViewComboBoxCell. Do tych komórek dodamy wpisy i podłą-
czymy je do tabeli. Oto metoda Form1_Load() generująca i wypełniająca tabelę:
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=1;
dataGridView1->RowCount=3;
DataGridViewComboBoxCell^ komorkaCombo1 = gcnew DataGridViewComboBoxCell;
komorkaCombo1->Items->Add("Czerwone");
komorkaCombo1->Items->Add("Zielone");
komorkaCombo1->Items->Add("Niebieskie");
DataGridViewComboBoxCell^ komorkaCombo2 = gcnew DataGridViewComboBoxCell;
komorkaCombo2->Items->Add("Czerwone");
komorkaCombo2->Items->Add("Zielone");
komorkaCombo2->Items->Add("Niebieskie");
DataGridViewComboBoxCell^ komorkaCombo3 = gcnew DataGridViewComboBoxCell;
komorkaCombo3->Items->Add("Czerwone");
komorkaCombo3->Items->Add("Zielone");
komorkaCombo3->Items->Add("Niebieskie");

dataGridView1->Rows[0]->Cells[0]=komorkaCombo1;
dataGridView1->Rows[1]->Cells[0]=komorkaCombo2;
dataGridView1->Rows[2]->Cells[0]=komorkaCombo3;
}

Zmiana kolorów odpowiedniej etykiety powinna następować po wybraniu nowej


wartości na odpowiadającej jej liście. Metodę zmieniającą kolor dowiążemy więc do zda-
rzenia CellEndEdit występującego po zakończeniu edycji komórki. Metodę tę najpro-
ściej utworzysz, klikając w widoku budowanego okna kontrolkę siatki DataGridView,
a następnie rozwijając panel Properties, przełączając się na widok zdarzeń i klikając
podwójnie zdarzenie CellEndEdit. To, która komórka została kliknięta, określimy,
pobierając właściwość Index klikniętego wiersza. Ponieważ mamy w naszej aplikacji
tylko jedną kontrolkę siatki, to wiadomo, że ona właśnie wygenerowała zdarzenie
274 Microsoft Visual C++ 2012. Praktyczne przykłady

CellEndEdit, nie trzeba więc określać tego z parametru sender metody dataGridView1_
CellEndEdit(). W zwykłej kontrolce ComboBox aktualnie wybraną pozycję na liście
można odczytać z właściwości SelectedIndex. Tu niestety nie ma takiej możliwości, trze-
ba odczytywać treść wybranej pozycji poprzez właściwość Value komórki z listą.
private: System::Void dataGridView1_CellEndEdit(System::Object^ sender,
System::Windows::Forms::DataGridViewCellEventArgs^ e) {
System::Int16 wiersz;
wiersz=dataGridView1->CurrentRow->Index;
if (wiersz==0) {

if (dataGridView1->Rows[0]->Cells[0]->Value->ToString() == ("Czerwone"))
label1->BackColor=System::Drawing::Color::Red;
if (dataGridView1->Rows[0]->Cells[0]->Value->ToString() == ("Zielone"))
label1->BackColor=System::Drawing::Color::Green;
if (dataGridView1->Rows[0]->Cells[0]->Value->ToString() == ("Niebieskie"))
label1->BackColor=System::Drawing::Color::Blue;
}
if (wiersz==1) {
if (dataGridView1->Rows[1]->Cells[0]->Value->ToString() == ("Czerwone"))
label2->BackColor=System::Drawing::Color::Red;
if (dataGridView1->Rows[1]->Cells[0]->Value->ToString() == ("Zielone"))
label2->BackColor=System::Drawing::Color::Green;
if (dataGridView1->Rows[1]->Cells[0]->Value->ToString() == ("Niebieskie"))
label2->BackColor=System::Drawing::Color::Blue;
}
if (wiersz==2) {
if (dataGridView1->Rows[2]->Cells[0]->Value->ToString() == ("Czerwone"))
label3->BackColor=System::Drawing::Color::Red;
if (dataGridView1->Rows[2]->Cells[0]->Value->ToString() == ("Zielone"))
label3->BackColor=System::Drawing::Color::Green;
if (dataGridView1->Rows[2]->Cells[0]->Value->ToString() == ("Niebieskie"))
label3->BackColor=System::Drawing::Color::Blue;
}
}

Po uruchomieniu programu można zmieniać kolory etykiet za pomocą list.

Odnośniki internetowe w komórkach


— DataGridViewLinkCell
Jest to specjalny rodzaj komórki tekstowej, w której zawartość wyświetlana jest w stylu
odnośnika do adresu internetowego. Wartość komórki, czyli adres, jest przechowy-
wana w zmiennej Value. Domyślnie komórka nie może być edytowana przez użyt-
kownika podczas działania programu; odnośnik jest umieszczany w niej za pomocą
edytora kolumn lub w programie poprzez podstawianie pola Value. Ma ona właściwo-
ści pozwalające na zmianę kolorów odnośników już odwiedzonych i nieodwiedzonych.
Odnośnik uzyskuje status odwiedzonego po pierwszym kliknięciu komórki. Podstawowe
właściwości komórki tego typu przedstawia tabela 15.11.
Rozdział 15. ♦ Komponent tabeli DataGridView 275

Tabela 15.11. Niektóre właściwości komórki DataGridViewLinkCell


Właściwość Opis
LinkColor Właściwość typu Color, określająca kolor nieodwiedzonego odnośnika.
LinkVisited Określa, czy odnośnik z komórki został odwiedzony. W chwili utworzenia
komórki właściwość ma wartość false. Po pierwszym kliknięciu wartość
zmienia się na true. Działa tylko, jeżeli właściwość TrackVisitedState ma
wartość true.
TrackVisitedState Wartość true oznacza, że odwiedzony odnośnik zmienia kolor na określony
we właściwości VisitedLinkColor. W przypadku wartości false po kliknięciu
kolor odnośnika nadal jest taki, jak określony w LinkColor. Jeśli ustawimy tę
właściwość na false, to można określać, czy odnośnik został odwiedzony,
przez ustawienie wartości właściwości LinkVisited z poziomu programu.
VisitedLinkColor Kolor odwiedzonego odnośnika, czyli takiego, dla którego właściwość
LinkVisited ma wartość true.

Przykład 15.19

Niech tabela o jednej kolumnie i trzech wierszach zawiera komórki typu DataGrid-
ViewLinkCell z popularnymi odnośnikami internetowymi. Po kliknięciu komórki powinna
się otwierać przeglądarka z wybraną stroną.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, zawierający tabelę DataGridView.

W edytorze kolumn wstaw kolumnę DataGridViewLinkColumn. Utworzenie wierszy tabeli


i wpisanie odnośników zrealizujemy w metodzie Form1_Load().
private: System::Vo d Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->ColumnCount=1;
dataGridView1->RowCount=3;
dataGridView1->Rows[0]->Cells[0]->Value="www.helion.pl";
dataGridView1->Rows[1]->Cells[0]->Value="www.google.pl";
dataGridView1->Rows[2]->Cells[0]->Value="new.meteo.pl";
}

W momencie kliknięcia tabeli generowane jest dla niej zdarzenie Click. Do obsługi tego
zdarzenia podłącz metodę otwierającą przeglądarkę z adresem pobranym z klikniętej
komórki.
private: System::Void dataGridView1_Click(System::Object^ sender,
System::EventArgs^ e) {
System::String^ adresURL="http://";
adresURL=safe_cast<DataGridView^>(sender)->CurrentCell->Value->ToString();
System::Diagnostics::Process::Start(adresURL);
}

Wygląd aplikacji po uruchomieniu przedstawia rysunek 15.7.


276 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 15.7.
Tabela odnośników
internetowych

Zauważ, że dostęp do klikniętego obiektu tabeli DataGridView uzyskujemy poprzez para-


metr sender. Ponieważ w aplikacji mamy tylko jedną tabelę, dataGridView1, można napi-
sać funkcję obsługi kliknięcia przycisku, powołując się bezpośrednio na nią. Unikniemy
w ten sposób rzutowania w dół.
private: System::Void dataGridView1_Click(System::Object^ sender,
System::EventArgs^ e) {
System::String^ adresURL="http://";
adresURL=dataGridView1->CurrentCell->Value->ToString();
System::Diagnostics::Process::Start(adresURL);
}

Po kompilacji program będzie działał identycznie.


Rozdział 16.
Aplikacja bazy danych

Baza danych i aplikacja


W praktyce programowania wcześniej czy później spotkasz się z problemem napisa-
nia aplikacji wykorzystującej bazy danych. Ten rozdział opisuje podstawy obsługi serwera
bazy PostgreSQL z poziomu aplikacji C++/CLI. Najpierw pobierzemy i zainstalujemy
bazę, a później napiszemy aplikację do jej obsługi; przykładem będzie baza danych
o płytach CD. Baza danych jest w tym przypadku serwerem, a aplikacja klientem. Wysyła
ona zapytania do bazy, które manipulują danymi. W odpowiedzi na zapytanie baza
może przesłać jakieś dane, ale ten termin odnosi się też do komendy wpisania danych
do bazy lub ich skasowania.

Bardzo pomocnym obiektem w aplikacjach baz danych jest DataGridView, znany z po-
przedniego rozdziału. Za pomocą operacji zwanej bindowaniem można go podłączyć
do bazy, ułatwiając wymianę danych. Zaczniemy od zainstalowania darmowej bazy
Open Source PostgreSQL. Przez cały ten rozdział będziemy tworzyć jedną aplikację
bazy danych. Najpierw pobierzemy i zainstalujemy serwer bazy, następnie utworzymy
przykładową bazę i napiszemy aplikację klienta bazy w VC++ 2012.

Instalacja PostgreSQL
Pierwszy przykład to pobranie i instalacja bazy.

Przykład 16.1

Pobierz z Internetu i zainstaluj wybraną wersję PostgreSQL.


278 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Bazę pobierzemy ze strony projektu www.postgresql.org. Na górze strony jest szary
pasek, kliknij w nim opcję Download. Na stronie, która się otworzy, znajdź nagłówek
Binary packages i kliknij pod spodem na Windows. Na kolejnej stronie masz akapit
One click installer, a pod nim odnośnik Download. Kliknij go, a zostaniesz przenie-
siony na stronę www.enterprisedb.com do działu Downolad PostgreSQL. Jest tu kilka
wersji, nie tylko dla Windows; zaczynając od wersji 9.0, mamy wersje dla 32-bitowego
i 64-bitowego Windows. Nie zawsze pobranie najnowszej wersji jest najlepszym
rozwiązaniem. Jeśli uruchamiasz bazę, która będzie zawierała ważne dane, można po-
brać wersję nieco straszą, ale sprawdzoną. W momencie pisania książki były dostępne
wersje 8.3.19, 8.4.12, 9.0.8 i 9.1.4. Ja zdecydowałem się na 64-bitową wersję 9.0.8.
Znajdź punkt Installer version Version 9.0.8 i kliknij na Win x86-64 pod spodem.
Rozpocznie się pobieranie instalatora w postaci pliku wykonywalnego. W przypadku
wersji 9.0.8 będzie to plik postgresql-9.0.8-1-windows-x64.exe. Do momentu wydania
tej książki wersja zapewne się zmieni, więc numerki mogą być nieco inne. Po pobraniu
wejdź do folderu z plikiem, na przykład za pomocą narzędzia Mój komputer, i uruchom
pobrany plik, klikając na nim dwukrotnie.

Po uruchomieniu instalatora widać okno powitalne. Kliknij Next >. Następne okno
pokazuje rysunek 16.1. Jest to wybór folderu docelowego instalacji. Najprościej zo-
stawić folder domyślny C:\Program Files\PostgreSQL\9.0, ja jednak musiałem zmie-
nić dysk na D:, ponieważ tak wynika z mojej konfiguracji folderów.

Rysunek 16.1.
Wybór folderu
docelowego instalacji

Kliknij Next >. W kolejnym ekranie, pokazanym na rysunku 16.2, masz wybór folderu
docelowego bazy. Znaczenie tego folderu jest drugorzędne — i tak właściwą bazę zain-
stalujemy w innym folderze.
Rozdział 16. ♦ Aplikacja bazy danych 279

Rysunek 16.2.
Folder instalacji bazy

Klikamy Next >. Kolejny krok to dwukrotne podanie hasła do konta administratora
(login: postgres). Znowu Next > i mamy wybór portu dla serwera bazy; ja zostawiłem
standardowy 5432. Kliknij Next >. Czas na ustawienia lokalne: zostaw [Default locale],
a ustawienia będą takie jak w systemie. Po kliknięciu Next > wreszcie rozpocznie się
instalacja.

Po instalacji pojawi się okno kończące pracę instalatora. Będzie tam przycisk opcji z napi-
sem Launch Stack Builder at Exit. Jeżeli jest on zaznaczony (domyślnie jest), po za-
kończeniu pracy instalatora uruchomi się konfigurator bazy danych Stack Builder. Zostaw
zaznaczone pole i kliknij Finish.

I oto masz przed sobą pierwsze okno Stack Buildera, konfigurującego bazę PostgreSQL.
W tym pierwszym oknie wybierasz instalację bazy do skonfigurowania. Jeśli wszystkie
opcje przy instalacji podawałeś, jak proponowałem, to po rozwinięciu listy znajdziesz
na niej pozycję jak na rysunku 16.3. Wybierz ją i kliknij Next >.

Teraz konfigurator pobierze z Internetu listę aplikacji, które mogą być zainstalowane
jako dodatki do bazy. Po pobraniu zobaczysz tę listę w formie rozwijalnego drzewa, jak
na rysunku 16.4.

Na razie potrzebne są nam głównie sterowniki do .NET Framework, bo inaczej nasza


aplikacja nie będzie mogła połączyć się z bazą. Rozwiń gałąź Database Drivers i zaznacz
Npgsql, jak na rysunku 16.4. Kliknij Next >. Pojawi się lista wybranych dodatków
— zatwierdź ją kolejnym kliknięciem Next >. Sterowniki zostaną pobrane z Inter-
netu. Po pobraniu znowu kliknij Next >, zostaniesz z kolei przeniesiony do konfi-
guratora sterowników. Już w instalatorze sterowników kolejny raz kliknij Next >. Masz
teraz przed sobą okno z wyborem folderu instalacji sterowników. Proponuję zostawić
domyślny C:\Program Files\PostgreSQL\Npgsql. Kliknij Next >, następnie zatwierdź
280 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 16.3.
Pierwszy ekran
konfiguratora
Stack Builder

Rysunek 16.4.
Lista rozszerzeń
bazy, które
mogą zostać
zainstalowane

instalację kolejnym Next >. Rozpocznie się instalacja sterowników. Po instalacji kliknij
Finish, znajdziesz się znowu w ostatnim ekranie Stack Buidera. Kliknij Finish i to już
koniec, baza wraz ze sterownikami została zainstalowana.

Standardowo baza jest instalowana jako usługa systemowa uruchamiana przy każdym
starcie systemu. Jest to dobre rozwiązanie, jeśli chcesz intensywnie z niej korzystać.
Możesz pozostać przy tej opcji, ale jeśli zainstalowałeś bazę tylko do nauki i testów,
możesz wyłączyć autouruchamianie usługi. Przyspieszy to start komputera i zmniejszy
zużycie zasobów. Minusem jest to, że przed każdym uruchomieniem programu bę-
dziesz musiał uruchomić serwer bazy ręcznie, jest to jednak bardzo proste.
Rozdział 16. ♦ Aplikacja bazy danych 281

Wyłączenie usługi bazy


Następny przykład wykonaj tylko, jeśli nie chcesz uruchamiana serwera bazy przy każ-
dym starcie systemu.

Przykład 16.2

Wyłącz autostart usługi serwera bazy PostgreSQL.

Rozwiązanie
Jeśli chcesz wyłączyć automatyczne uruchamianie usługi bazy, kliknij Start/Panel ste-
rowania, wybierz Narzędzia administracyjne, a następnie Usługi. Pojawi się lista usług,
jak na rysunku 16.5.

Rysunek 16.5.
Usługi systemowe
wraz z usługą bazy

Znajdź na liście usługę PostgreSQL Server (zaznaczoną strzałką na rysunku 16.5) i kliknij
ją dwukrotnie. Na karcie usługi, która się pojawi, ustaw Tryb uruchomienia na Wyłączony.
Kliknij OK. Teraz najlepiej wykonać restart komputera.

Inicjalizacja bazy
Zanim będzie możliwe korzystanie z bazy, trzeba stworzyć strukturę do przechowy-
wania danych. PostgreSQL wymaga dosyć skomplikowanego drzewa plików i kata-
logów. Na szczęście, nie musimy nic tworzyć sami, wystarczy tylko wskazać folder,
w którym chcemy mieć dane, a wszystko utworzy się automatycznie za pomocą programu
initdb dołączonego do dystrybucji bazy. Program initdb znajduje się w folderze z pli-
kami wykonywalnymi bazy. Pod Windows przy wersji PostgreSQL 9.0 standardowo
jest to katalog C:\Program Files\PostgreSQL\9.0\bin.
282 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 16.3

Utwórz strukturę folderów z bazą danych.

Rozwiązanie
Uruchom wiersz polecenia przez Start/Wszystkie programy/Akcesoria/Uruchom, a następ-
nie wpisanie w okno komendy cmd. Przejdź do folderu z programem initdb, wpisując
kolejno dwie komendy:
cd \

Naciśnij Enter.
cd “c:\Program files\PostgreSQL\9.0\bin”

Naciśnij Enter.

Teraz uruchom program initdb, jako argument podając opcję -D oraz nazwę folderu,
w którym mają być przechowywane dane. Może to być zupełnie dowolny folder, ja
wybrałem d:\Database.
initdb -D d:\Database

Teraz możesz już uruchomić serwer bazy. Jeśli zainstalowałeś bazę jako usługę, to uru-
chomienie następuje automatycznie po starcie systemu. Jeżel ma być zainstalowana
jako aplikacja, musisz ją uruchomić sam za pomocą programu pg_ctl znajdującego się
w folderze z plikami wykonywalnymi bazy. Będąc w wierszu poleceń w tym samym
folderze, z którego uruchamiałeś initdb, uruchamiasz bazę, wpisując:
pg_ctl start -D d:\Database

Jeśli uruchomienie bazy przebiega prawidłowo, powinieneś zobaczyć następujące


komunikaty:
DZIENNIK: system bazodanowy został zamknięty o 2012-06-07 23:07:00 CEST
DZIENNIK: system bazy danych jest gotowy do przyjmowania połączeń
DZIENNIK: uruchomiono program wywołujący autoodkurzanie

Tu mała niespodzianka: w wersji 8.4, na której pracowałem poprzednio, komunikaty


były po angielsku, a tu mamy spolszczenie. Zatrzymujesz bazę jak niżej:
pg_ctl stop –D d:\Database

oczywiście zakładając, że folderem mieszczącym dane bazy jest d:\database.


Rozdział 16. ♦ Aplikacja bazy danych 283

Organizacja i typy danych


w bazach PostgreSQL
Dane fizycznie przechowywane są w strukturze folderów opisanej wyżej. Nie będę tu
się zagłębiał w zawartość poszczególnych zbiorów, opiszę jedynie, jak to wygląda z per-
spektywy użytkownika, który chce mieć po prostu działającą bazę danych.

Zacznijmy od tego, że folder z danymi może zawierać nie jedną, ale kilka baz danych.
Każda z baz posiada swoją nazwę i użytkowników. Użytkownicy dzielą się podobnie
jak w systemach operacyjnych: na zwykłych użytkowników i administratorów (superu-
żytkowników). Administrator danej bazy ma, oczywiście, większe prawa. Obecnie Po-
stgreSQL nie używa terminu „użytkownik”, ale „rola” (ang. role). „Rola” to pojęcie
wprowadzone zamiast użytkowników i grup użytkowników.

Nowo zainstalowany serwer posiada jedną bazę danych, nazwaną postgres. Baza ta ma
przypisaną jedną rolę administratora, dla której nazwą/loginem jest nazwa użytkownika,
który uruchomił program initdb.

Jeżeli chcesz utworzyć inne bazy, to najprościej zrobić to programem createdb, do-
stępnym w folderze z binariami bazy. Składnia jest bardzo prosta — wystarczy podać
nazwę nowej bazy (np. mybase).
createdb mybase

Ta nowa baza będzie miała jedną rolę o loginie takim jak login systemowy użytkownika,
który wywołał initdb.

Baza danych ogólnie zawiera tabele, w tych tabelach mamy rekordy, czyli wiersze
zawierające już konkretne dane. Dane te umieszczone są w polach; pole to po prostu
komórka tabeli. Tabele w bazie mogą być (i zwykle są) wzajemnie powiązane, to zna-
czy jedna tabela zawiera w sobie odnośniki do innej. Rysunek 16.6 przedstawia przy-
kładową bazę danych dla serwisu samochodowego.

Rysunek 16.6.
Przykładowa baza
danych serwisu
samochodów
284 Microsoft Visual C++ 2012. Praktyczne przykłady

Baza przedstawiona na rysunku 16.6 składa się z trzech tabel. W tabeli „Właściciele”
są zapisane dane właścicieli samochodów znajdujących się aktualnie w serwisie. Każ-
dy właściciel ma nadany indywidualny kod, po którym można go jednoznacznie rozpo-
znać. Tabela „Magazyn części” zawiera listę części zamiennych w magazynie. Każda
część również oznaczona jest numerem. W tabeli „Wymiany części” zapisywane jest,
jaką część i w czyim samochodzie wymieniono. Te dwie informacje zapisywane są po-
przez zapisanie numeru części i numeru właściciela samochodu wziętego odpowiednio
z tabel „Magazyn części” i „Właściciele”. Wszystkie tabele są więc ze sobą powiązane
(są ze sobą w relacji). Taki typ bazy jest obecnie najczęściej stosowany i nazywa się rela-
cyjną bazą danych.

Pola w tabelach mają określone typy, podobnie jak typy zmiennych w językach progra-
mowania. Typy pól tabeli trzeba określić na etapie tworzenia tabeli. Nazwy typów są
nieco różne dla różnych baz danych, choć dąży się do standaryzacji. Tabela 16.1 przed-
stawia podstawowe typy rozpoznawane przez PostgreSQL.

Tabela 16.1. Podstawowe typy danych w PostgreSQL


Nazwa typu Opis
FLOAT Liczba zmiennoprzecinkowa.
VARCHAR(N) Łańcuch znakowy długości maksymalnie N znaków.
DATE Data.
TIME Czas.
TIMESTAMP Data i czas.
INTEGER Typ całkowity
BOOL Typ boolowski.

Język SQL
Język SQL (Structured Query Language) jest szeroko przyjętym językiem komunikacji
z bazą danych. Za jego pomocą można zarówno dodawać nowe tabele w bazie, jak i do-
pisywać czy kasować dane oraz — co może najważniejsze — odczytywać dane według
podanego klucza. Komunikacja z bazą następuje za pomocą zapytań formułowanych
w języku SQL. W odpowiedzi na zapytanie baza może wykonać zleconą komendę lub
też przesłać dane, o które pytamy. Składnia języka jest rozbudowana; w tej książce
skupię się na sposobie łączenia z bazą danych i przesyłaniu zapytań do bazy w aplika-
cjach C++/CLI. Treść zapytań opisują książki poświęcone wyłącznie SQL, tutaj ograni-
czę się do podstawowych komend. Wydawanie tych komend ręcznie i odczytywanie
wyników byłoby niewygodne. Aplikacja, którą napiszemy, będzie właśnie komunikowała
się z bazą, formując komendy SQL na podstawie tego, co wprowadzimy w okienkach,
oraz przedstawiała wyniki. W ten sposób obsługa bazy będzie łatwa i intuicyjna.
Rozdział 16. ♦ Aplikacja bazy danych 285

Utworzenie bazy danych


Nasza aplikacja będzie tylko graficznym interfejsem do bazy danych; bazę trzeba naj-
pierw utworzyć.

Przykład 16.4

W strukturze folderów utwórz bazę danych do przechowywania tytułów płyt CD.

Rozwiązanie
Wszystko przebiegnie tak, jak opisałem wcześniej, a więc teraz już tylko przedstawię
komendy z krótkim opisem. Zaczynamy od wywołania okna linii komend i przejścia
do folderu z PostgreSQL.

Uruchom wiersz polecenia przez Start/Wszystkie programy/Akcesoria/Uruchom, a na-


stępnie wpisanie w okno komendy cmd. Przejdź do folderu z programem initdb, wpi-
sując kolejno dwie komendy:
cd\

Naciśnij Enter.
cd c:\Program files\PostgreSQL\9.0\bin

Naciśnij Enter.

Utworzenie bazy wymaga, aby serwer był uruchomiony, a zatem wpisujemy:


pg_ctl start –D d:\Database

Nie przejmuj się tym, że po komunikatach uruchamiania serwera nie pojawił się tzw.
znak zachęty, czyli nazwa aktualnego folderu i kursor.
c:\Program files\PostgreSQL\9.0\bin>

Możesz normalnie wpisywać dalsze komendy. Teraz w folderze d:\database utwo-


rzymy drugą bazę danych, pod nazwą utwory, zatem w linii komend wpisz:
createdb utwory

W bazie będzie jedna tablica plyty, utworzymy ją za pomocą komendy SQL CREATE
TABLE. Format tej komendy jest następujący:
CREATE TABLE NazwaTabeli(NazwaKolumny1 TypPól1 OpcjePola1, NazwaKolumny2 TypPól2
OpcjePola2, …);

Utworzona tabela ma kolumny o nazwach NazwaKolumny. Typ pól w kolumnie jest


jednym z typów określonych w tabeli 16.1. Parametr opcje ustawia dodatkowe wła-
ściwości pola. Tutaj ograniczę się tylko do podania dwóch z możliwych wartości tego
parametru, to jest PRIMARY KEY. Kolumna z tą opcją stanowi tak zwany klucz główny
286 Microsoft Visual C++ 2012. Praktyczne przykłady

tabeli. Oznacza to, że takie pole jednoznacznie identyfikuje rekord. Innymi słowy, po
ustawieniu tej opcji baza nie pozwoli na wpisanie w tę kolumnę dwóch takich samych
wartości. Znacznie ułatwia to wyszukiwanie rekordu. Inną możliwą wartością parametru
opcje jest SERIAL. W pole z taką wartością będzie automatycznie wpisywany kolejny
numer rekordu, poczynając od jedności. Musi być to pole o typie całkowitym. Kiedyś
w bazach PostgreSQL pole typu SERIAL było automatycznie kluczem głównym, od
wersji 7.3 już tak nie jest — trzeba dodatkowo zadeklarować jako PRIMARY KEY.

Aby wydawać instrukcje bazie w trybie konsoli, trzeba skorzystać z komendy psql. Aby
połączyć się z bazą utwory, napisz:
psql –d utwory

Teraz możemy już przesyłać zapytania SQL do bazy bezpośrednio z linii komend. Tabela
plyty będzie miała cztery pola:
 Numer — liczba całkowita, klucz główny, indeksowanie SERIAL.

 Artysta — nazwa artysty, pole znakowe.


 Tytuł — tytuł płyty, pole znakowe.
 Rok — rok wydania, liczba całkowita.

Oto odpowiednia postać zapytania CREATE TABLE. Wpisz ją w konsolę po znaku zachęty
programu psql. Pamiętaj o średniku na końcu.
CREATE TABLE plyty(numer SERIAL PRIMARY KEY, artysta varchar(50), tytul
varchar(50), rok INTEGER);

Aby wyjść z programu psql, wpisz polecenie \q.

Za pomocą psql możesz też odczytać domyślny login administratora nadawany po


utworzeniu nowej bazy. Wystarczy, że wywołasz:
psql --help

Wśród prezentowanych opcji zobaczysz taką linię:


-U, --username=NZAWAUZYTKOWNIKA
nazwa użytkownika bazy danych (domyślnie: "Mariusz")

W nawiasie mamy domyślną nazwę konta, która u Ciebie będzie prawdopodobnie in-
na. Baza utwory jest gotowa, zajmiemy się teraz aplikacją okienkową do jej obsługi.

Interfejs użytkownika
Zostawmy na chwilę bazę danych i zajmijmy się wreszcie budową naszej aplikacji.
Ponieważ jest spora, zbudujemy ją w kilku przykładach.
Rozdział 16. ♦ Aplikacja bazy danych 287

Przykład 16.5

Utwórz projekt aplikacji klienta bazy danych i okno z kontrolkami.

Rozwiązanie
Utwórz projekt aplikacji okienkowej C++/CLI zgodnie z przykładem 1.4. Zaczniemy jak
zwykle od interfejsu użytkownika. Głównym jego składnikiem będzie tabela DataGrid-
View, taka sama jak przy automacie komórkowym, z tym że teraz zostanie ona użyta
do prezentacji danych. Rozwiń panel Toolbox z zakładki po prawej stronie środowiska,
znajdź dział Data i wstaw do okna budowanej aplikacji komponent DataGridView. Na-
sza siatka będzie służyć tylko do odczytu danych. Kliknij na nią w widoku projektowa-
nia aplikacji i w panelu właściwości znajdź właściwość ReadOnly. Ustaw jej wartość
na true. Potrzebne będą jeszcze dwa komponenty z działu Data, mianowicie Binding-
Source i BindingNavigator. W wielkim skrócie: BindingSource to komponent do podłą-
czenia danych z bazy, a BindingNavigator służy do nawigacji po tych danych. Pobierz
oba komponenty z zakładki Toolbox i wstaw do okna aplikacji. Oba zostaną umiesz-
czone na pasku niewidocznych komponentów pod oknem, jak pokazuje rysunek 16.7.

Rysunek 16.7. Obszar projektowania aplikacji Baza danych

Komponent BindingNavigator manifestuje swoją obecność w oknie aplikacji, dodając pa-


sek nawigacyjny. Nie będziemy go wykorzystywać, więc go ukryjemy. Kliknij w pa-
sku niewidocznych komponentów bindingNavigator1, następnie w panelu Properties
po prawej stronie znajdź właściwość Visible i ustaw na false.
288 Microsoft Visual C++ 2012. Praktyczne przykłady

W ten sposób ukryliśmy pasek bindingNavigator, lecz niestety i tak potrzebuje on


swoich ikonek pobieranych z zasobów. W rozdziale 10. były już z tym problemy przy
paskach narzędziowych. Trzeba tu zastosować identyczne rozwiązanie, czyli zmianę
nazwy przestrzeni nazw na odpowiadającą aktualnemu projektowi. Pełen opis czynno-
ści znajdziesz w przykładzie 10.6. Tu tylko przypomnę, że jeżeli aktualny projekt na-
zywa się PR_16_5, trzeba zmienić w pliku WindowsFormApplication1.cpp
// WindowsFormApplication1.cpp : main project file.
#include "stdafx.h"
#include "Form1.h"
using namespace WindowsFormApplication1;

na:
using namespace PR_16_5;

A w pliku Form1.h zaś zamiast nazwy przestrzeni:


#pragma once

namespace WindowsFormApplication1 {

wpisz nazwę projektu, tę samą co w dyrektywie using.


#pragma once
namespace PR_16_5 {

Teraz dodaj trzy pola tekstowe TextBox i trzy komponenty Label do ich opisu. We wła-
ściwość Text komponentów Label wpisz odpowiednie opisy, zgodnie z rysunkiem 16.7.
Pola tekstowe ustaw tak, aby textBox1 był pod etykietą Artysta, textBox2 pod Tytuł,
a textBox3 pod Rok. Całości dopełnią cztery przyciski Button, które wstaw na dole okna.
Nasza aplikacja nie będzie miała menu, całość obsłużymy za pomocą przycisków.
Właściwości Text kolejnych przycisków ustaw zgodnie z rysunkiem 16.7 na Dodaj,
Zmień, Skasuj i Wyjdź.

Włączenie sterowników bazy


PostgreSQL do projektu
Ponieważ standardowo środowisko nie ma sterowników do PostgreSQL, trzeba je
połączyć z projektem, czyli dodać referencję do biblioteki.

Przykład 16.6

Dodaj sterowniki PostgreSQL do projektu aplikacji.

Rozwiązanie
Sterowniki zainstalowałeś na etapie instalacji bazy w folderze C:\Program Files\
PostgreSQL\Npgsql. Teraz trzeba spowodować, aby projekt je „widział”. W panelu
Rozdział 16. ♦ Aplikacja bazy danych 289

Solution Explorer z lewej strony środowiska kliknij prawym przyciskiem na nazwę


Twojej aplikacji, jak na rysunku 16.8. Z menu, które się rozwinie, wybierz References.

Rysunek 16.8.
Podłączenie referencji
do biblioteki

Otworzy się okno właściwości aplikacji. Kliknij na przycisk Add New reference…,
kliknij zakładkę Browse w oknie wyboru pliku, przejdź do folderu C:\Program Files\
PostgreSQL\Npgsql lub tam, gdzie zainstalowałeś sterowniki Npgsql. W tym folderze
masz sterowniki do kilku wersji .NET Framework. Ponieważ masz co najmniej Win-
dows 7, prawdopodobnie Twoja wersja .NET to 4.0. Wejdź więc w folder ms.net4.0
i wybierz plik biblioteki Npgsql.dll. Kliknij Add. Następnie kliknij OK w oknie właści-
wości. Teraz pozostaje włączyć przestrzeń nazw Npgsql oraz SqlClient do projektu.
W pliku Form1.h bezpośrednio pod istniejącymi dyrektywami using namespace napisz
dwie kolejne.
using namespace System::Drawing; // ta linia istnieje
using namespace System::Data::SqlClient;
using namespace Npgsql;
290 Microsoft Visual C++ 2012. Praktyczne przykłady

Łączenie z bazą i odczyt danych


Przy uruchomieniu aplikacji połączymy się z bazą, odczytamy dane i wyświetlimy je
w tabeli. Zakładam, że nasza baza jest niewielka, więc można wyświetlić w tabeli
wszystkie rekordy bez nadmiernego obciążania systemu. Gdyby była duża, należałoby
wyświetlać tylko dane spełniające określone kryteria. Wyszukiwanie danych realizuje-
my zapytaniem SELECT.
SELECT NazwaKolumny1, NazwaKolumny2, … FROM NazwaTabeli WHERE warunek

Ta komenda, w przeciwieństwie na przykład do CREATE, nie powoduje żadnych zmian


w bazie. Zwraca natomiast odpowiedź w postaci tabeli. Tabela ta jest podzbiorem tabeli
NazwaTabeli i zawiera te wiersze z kolumn NazwaKolumny1, NazwaKolumny2 itd., których
pola spełniają warunek. Na przykład jeśli chcemy znaleźć wszystkie płyty z naszej ta-
beli wydane w roku 2000, to zapytanie SELECT będzie miało postać:
SELECT artysta, tytul, rok FROM plyty WHERE rok=2000;

Dowolny ciąg znaków można zastąpić *. Najkrótsze zapytanie o wszystkie dane z tabeli:
SELECT * FROM plyty

Gwiazdka oznacza, że pytamy o wszystkie kolumny tabeli, natomiast warunek WHERE


w ogóle nie istnieje, czyli interesują nas bezwarunkowo wszystkie wiersze tabeli. Masz
już gotową bazę danych z tabelą utwory. W następnym przykładzie czeka nas najważniej-
sze zadanie: połączenie aplikacji z bazą danych i wyświetlenie tytułów pustych na ra-
zie kolumn.

Przykład 16.7

Połącz tabelę w aplikacji z bazą danych i wyświetl tytuły kolumn tabeli płyty bazy
utwory.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu. Łączenie się z bazą danych będzie nastę-
powało po uruchomieniu aplikacji. Odpowiedni kod umieścimy w metodzie wykony-
wanej przy zdarzeniu wyświetlania okna aplikacji. Kliknij podwójnie na okno aplikacji
w widoku projektowania. Utworzy się metoda Form1_Load() i zostanie od razu połączo-
na ze zdarzeniem wyświetlania okna aplikacji.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
}

Metoda będzie operowała na dwóch obiektach, które zadeklarujemy jako pola klasy
Form1. Przed metodą Form1_Load() po dyrektywie #pragma endregion zadeklarujemy
obiekt połączenia z bazą connection i obiekt komendy SELECT selectcomm.
#pragma endregion //ta linia istnieje
private: NpgsqlConnection^ connection;
private: NpgsqlCommand^ selectcomm;
Rozdział 16. ♦ Aplikacja bazy danych 291

Typy zadeklarowanych obiektów są określone w przestrzeni nazw Npgsql. Jest to prze-


strzeń nazw sterowników PostgreSQL dla .NET Framework. Typy tych obiektów są
więc specyficzne dla PostgreSQL. Przy wykorzystaniu innej bazy nazwy typów będą
trochę inne, ale zasada pozostaje ta sama.

Następne dwie linie programu wpisuj we wnętrzu metody Form1_Load(). Zaczniemy od


połączenia się z bazą. Połączenie to reprezentuje obiekt connection. Zadeklarowałeś
go już, a teraz utworzymy go za pomocą gcnew. Parametrem konstruktora jest tak zwany
łańcuch połączenia (ang. connection string). Zawiera on parametry połączenia. Wpisz:
connection = gcnew NpgsqlConnection("server=127.0.0.1;Port=5432;User
Id=NazwaUzytkownika;Database=utwory");

W łańcuchu mamy adres serwera bazy (tu komputer lokalny), port, nazwę użytkownika
oraz nazwę bazy danych. Słowo NazwaUzytkownika trzeba zastąpić rzeczywistą nazwą
użytkownika w Twojej bazie. Jeśli wykonywałeś wszystko zgodnie z tym rozdziałem,
to w bazie utwory masz tylko konto administratora o loginie takim jak nazwa konta
Windows. Możesz odczytać tę nazwę za pomocą psql, jak podałem w przykładzie o two-
rzeniu tabeli.

Ponieważ odczytywanie zawartości bazy przyda się wielokrotnie, napiszemy je w od-


dzielnej funkcji odswiez(). W ostatniej jak na razie linii Form1_Load() wywołujemy tę
nieistniejącą jeszcze funkcję.
odswiez();

Opuszczamy metodę Form1_Load(). Po klamrze zamykającej zadeklaruj w klasie Form1


nową metodę odswiez().
private: System::Void odswiez() {}

Kolejne linie wpisuj teraz we wnętrzu funkcji odswiez(). Przygotujemy teraz obiekt
zapytania SELECT. W przypadku korzystania z Npgsql zapytania są obiektami klasy
NpgsqlCommand. W celu zachowania przejrzystości każda komenda będzie oddzielnym
obiektem; dla komendy SELECT mamy obiekt selectcomm. Utworzymy go operatorem gcnew,
jako parametr konstruktora podajemy komendę. Drugim parametrem jest połączenie
z bazą, do której komenda będzie wysłana.
selectcomm = gcnew NpgsqlCommand("select * from plyty",connection);

Zgodnie z tym, co napisałem wyżej, komenda w takiej postaci pobierze całą tabelę. Wy-
konanie komendy powierzymy tak zwanemu adapterowi. Jest to obiekt klasy Npgsql-
DataAdapter. Adapter jest obiektem umożliwiającym wydawanie komend i odczyty-
wanie ich wyników w dosyć prosty sposób. Komendę (zapytanie) podajemy jako
parametr konstruktora klasy NpgsqlDataAdapter, a wyniki odbieramy poprzez metodę
Fill() tej klasy. Metoda Fill() wypełnia wynikami obiekt kolejnej potrzebnej nam
klasy, a mianowicie DataSet. W przypadku zapytań zwracających wynik użycie adapte-
ra jest chyba najprostszym sposobem. Utworzymy teraz obiekty NpgsqlDataAdapter
i DataSet w pamięci.
DataSet^ ds = gcnew DataSet("Dane o plytach");
NpgsqlDataAdapter^ adapter = gcnew NpgsqlDataAdapter(selectcomm);
292 Microsoft Visual C++ 2012. Praktyczne przykłady

Teraz metoda Fill() wypełni obiekt ds danymi odebranymi przez adapter z bazy.
adapter->Fill(ds);

Obiekty klasy DataSet przechowują tabele danych w pamięci.

Po wypełnieniu obiekt ds podstawiamy do obiektu BindingSource, a dokładnie do jego


właściwości DataSource.

Ponieważ obiekt DataSet może zawierać wiele tabel, trzeba wskazać, że chodzi nam
o pierwszą (i jedyną w tym przypadku) tabelę (która ma indeks zero).
bindingSource1->DataSource=ds->Tables[0];

Komponent źródła danych bindingSource1 ma już dostęp do danych.

Wreszcie podłączymy bindingSource1 do tabeli dataGridView1, wyświetlając dane.


dataGridView1->DataSource = bindingSource1;

I to koniec metody odswiez(). W celu zachowania jasności opisu podam ją jeszcze raz
w całości.
private: System::Void odswiez() {
selectcomm = gcnew NpgsqlCommand("SELECT * from plyty",connection);
DataSet^ ds = gcnew DataSet("Dane o plytach");
NpgsqlDataAdapter^ adapter = gcnew NpgsqlDataAdapter(selectcomm);
adapter->Fill(ds);
bindingSource1->DataSource=ds->Tables[0];
dataGridView1->DataSource =bindingSource1;
}

Uruchom serwer bazy w sposób opisany w podrozdziale „Inicjalizacja bazy”, a następnie


skompiluj i uruchom program. W siatce zobaczysz pustą tabelę z nazwami kolumn.

Dodawanie danych do bazy


Dodawanie danych zrealizujemy za pomocą metody ExecuteNonQuery(). Do dodawa-
nia danych służy komenda SQL INSERT.
INSERT INTO NazwaTabeli(NazwaKolumny1,NazwaKolumny2,…) VALUES
(wartość1,wartość2,….);

Parametr NazwaTabeli określa tabelę w bazie, do której chcemy dodać wiersz. Nazwy
NazwaKolumny1, NazwaKolumny2 itd. określają kolumny, w jakie mają być wpisane wartości.
Wartości podane w nawiasie po VALUES trzeba podać w kolejności określonej przez
parametry NazwaKolumny1, NazwaKolumny2 itd. Muszą być takiego typu jak poszczególnie
kolumny tabeli.

Tworzymy obiekt zapytania SQL typu NpgsqlCommand. Najpierw zadeklarujemy w klasie


Form1 obiekt NpgsqlCommand o nazwie inscomm, tam gdzie deklarowaliśmy już obiekt
Rozdział 16. ♦ Aplikacja bazy danych 293

selectcomm. Dostęp do bazy zaprogramujemy z obsługą tak zwanych transakcji. Trans-


akcje powodują, że zapytanie jest wykonywane ostatecznie dopiero po wywołaniu spe-
cjalnej metody Commit(). Zanim zostanie wywołana ta metoda, można odwołać ostatnią
transakcję z bazą za pomocą metody Rollback(). Drugą korzyścią jest to, że w przy-
padku wielu klientów łączących się z jednym serwerem bazy nie stracimy spójności
danych, ponieważ transakcja wykonywana jest jako całość. Nie ma zatem niebezpie-
czeństwa, że dwa zapytania zmodyfikują bazę częściowo, tworząc luki i błędy.

Przykład 16.8

Zaprogramuj dodawanie danych wpisanych w pola tekstowe po naciśnięciu przycisku


Dodaj.

Rozwiązanie

Otwórz nasz projekt klienta bazy danych. Po dyrektywie #pragma endregion zadeklaru-
jemy dwa obiekty, a właściwie uchwyty do nich. Oprócz obiektu transakcji, który nazwa-
łem inscomm, do jej przeprowadzenia potrzebny będzie obiekt typu NpgsqlTransaction.
Zadeklarujemy go zaraz po obiekcie inscomm.
private: NpgsqlCommand^ selectcomm; //ta linia istnieje
private: NpgsqlCommand^ inscomm;
private: NpgsqlTransaction^ transaction;

Dodanie rekordu do bazy nastąpi po naciśnięciu Dodaj. Kliknij dwukrotnie ten przy-
cisk w oknie projektowania aplikacji, aby utworzyć metodę obsługi kliknięcia button1_
Click().
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
}

W metodzie button1_Click() tworzymy obiekt inscomm, identycznie jak przy komendzie


SELECT.
inscomm = gcnew NpgsqlCommand("insert into plyty(artysta,tytul,rok)
values(@artysta,@tytul,@rok) ",connection);

Tym razem komenda zawiera parametry @artysta, @tytul i @rok. Niedługo zobaczysz,
jak za te parametry będziemy podstawiać konkretne wartości, które mają być dodane
do tabeli.

Zasadnicza różnica między poprzednią komendą SELECT a komendą INSERT jest taka, że
INSERT potrzebuje parametrów. Parametrami są dane, które mają być dodane do bazy.
Miejsca na wpisanie parametrów zaznaczyliśmy przez nazwy ze znakiem @ w treści
komendy. Teraz możemy uzupełnić łańcuch znakowy o konkretne wartości tych para-
metrów. Zostaną one wpisane w miejsca wyrazów ze znakiem @. W dalszym ciągu je-
steśmy w metodzie button1_Click(). Najpierw czyścimy zawartość parametrów metodą
Clear(), a następnie podstawiamy wartości metodą Add(). Obie metody są częścią
klasy obiektu zapytania SQL.
inscomm->Parameters->Clear();
inscomm->Parameters->Add("@artysta",textBox1->Text);
294 Microsoft Visual C++ 2012. Praktyczne przykłady

inscomm->Parameters->Add("@tytul",textBox2->Text);
inscomm->Parameters->Add("@rok",textBox3->Text);

Po wstawieniu parametrów prześlemy komendę do bazy i tu mamy kilka nowych rzeczy.


Napiszę teraz resztę kodu metody button1_Click(), a później będę objaśniał.
try {
connection->Open();
transaction=connection->BeginTransaction();
inscomm->Transaction = transaction;
inscomm->ExecuteNonQuery();
transaction->Commit();
connection->Close();
} catch (Exception^ e) {
transaction->Rollback();
MessageBox::Show(e->Message,"Błąd",MessageBoxButtons::OK,MessageBoxIcon::Error);
}
odswiez();

Pierwsza nowość to obsługa wyjątków. Wyjątki są narzędziem do reakcji na błędy; opi-


sałem je w podrozdziale „Wyjątki” w rozdziale 4. W bloku try mamy kolejno otwarcie
utworzonego wcześniej połączenia z bazą connection. Następnie otwieramy transakcję
dla tego połączenia, podstawiając obiekt transakcji do transaction. Dane do bazy po-
bieramy z odpowiednich pól tekstowych. Teraz transaction podstawiamy do właści-
wości Transaction obiektu komendy INSERT, specyfikujemy w ten sposób, do jakiej
komendy odnosi się transakcja. Wreszcie wykonujemy INSERT, wywołując metodę Exe-
cuteNonQuery(). Zauważ, że jest to metoda klasy obiektu zapytania do bazy inscomm.
Transakcja nie zostanie wykonana ostatecznie, dopóki nie wywołamy metody Commit()
na obiekcie transakcji. Jeśli wystąpi błąd, to w bloku catch mamy odwołanie transak-
cji metodą RollBack() i wyświetlenie rodzaju błędu pobranego z obiektu wyjątki. Na
końcu wyświetlamy aktualny stan bazy za pomocą naszej metody odswiez().

Skompiluj i uruchom program. Pamiętaj, że serwer bazy musi być uruchomiony. Wpisz
dowolne dane o albumie w pola tekstowe zgodnie z opisem, na przykład tak jak na
rysunku 16.9, i naciśnij Dodaj. Dane zostaną wpisane do bazy.

Rysunek 16.9.
Wpisywanie danych
do bazy
Rozdział 16. ♦ Aplikacja bazy danych 295

Zmiana danych w bazie


Kolejną operacją, jaką zaprogramujemy, będzie zmiana danych w bazie. Po kliknięciu na
wiersz dane z tego wiersza pojssawią się w polach tekstowych. Tu możesz je zmienić,
a następnie po kliknięciu przycisku Zmień wiersz zostanie odpowiednio zmieniony.

Modyfikacja danych w istniejącym wierszu (rekordzie) tabeli jest możliwa za pomocą


instrukcji UPDATE.
UPDATE NazwaTabeli SET NazwaKolumny1=NowaWartość1,
NazwaKolumny2=NowaWartość2,…WHERE NazwaKolumnyIdentyfikacji=warunek;

Po słowie SET wpisujemy nowe wartości kolumn. Parametry po WHERE określają, że mo-
dyfikujemy wiersz, w którym NazwaKolumnyIdentyfikacji równa jest wartości warunek.

Przykład 16.9
Dodaj do aplikacji możliwość edycji wpisów w wybranym wierszu bazy.

Rozwiązanie
Znowu będziemy potrzebować naszej aplikacji. Ustawimy parametry tabeli dataGrid-
View1 tak, aby można było zaznaczać całe wiersze. Kliknij na kontrolkę tabeli w widoku
projektowania okna aplikacji, następnie rozwiń panel Properties zakładką na prawej
krawędzi okna VC++ 2012 i znajdź właściwość SelectionMode. Ustaw jej wartość na
FullRowSelect.

Zmiana wartości wyświetlanych w polach tekstowych będzie następowała zawsze,


kiedy użytkownik zmieni zaznaczoną kolumnę tabeli. Metoda zmieniająca zawartość
pól będzie podłączona pod zdarzenie SelectionChanged tabeli dataGridView1. Jeśli tabela
jest wciąż zaznaczona w widoku projektowania aplikacji, przejdź w prawym panelu
za pomocą ikony błyskawicy do widoku zdarzeń. Następnie znajdź zdarzenie Selec-
tionChanged i najprościej kliknij dwukrotnie myszką po prawej stronie nazwy zdarze-
nia. W kodzie programu zostanie dodana metoda dataGridView2_SelectionChanged()
i zostaniesz do niej przeniesiony.
private: System::Void dataGridView2_SelectionChanged(System::Object^ sender,
System::EventArgs^ e) {
}

Danych z aktualnie zaznaczonego wiersza nie pobierzemy z tabeli, ale z obiektu binding-
Source1. Obiekt ten jest tak silnie związany z tabelą, że ma informację, który wiersz
tabeli jest zaznaczony. Zaznaczony wiersz można pobrać za pomocą właściwości Current.
Właściwość Current reprezentuje aktualny element bazy bardziej ogólnie, dlatego jej
typ to System::Object^. W naszym przypadku jest to wiersz tabeli, więc rzutujemy war-
tość zwróconą przez Current na typ DataRowView^. Obiekt DataRowView^ zawiera war-
tości wierszy w kolejnych polach tabeli. Wpiszemy je w pola tekstowe. Do identyfikacji
wiersza przy komendzie UPDATE potrzebna będzie kolumna Numer, ale dla niej nie ma
pola tekstowego. Zadeklarujemy więc zmienną CDNumer do przechowania tej wartości.
Oto cała metoda:
296 Microsoft Visual C++ 2012. Praktyczne przykłady

private: System::Void dataGridView2_SelectionChanged(System::Object^ sender,


System::EventArgs^ e) {
DataRowView^ SelectedRow;
SelectedRow=(DataRowView^)(bindingSource1->Current);
CDNumber=SelectedRow[0]->ToString();
textBox1->Text=SelectedRow[1]->ToString();
textBox2->Text=SelectedRow[2]->ToString();
textBox3->Text=SelectedRow[3]->ToString();
}

Zmienną CDNumber zadeklarujemy jako pole klasy na zewnątrz metody dataGridView2_


SelectionChanged(), tam gdzie deklaracja obiektów komend i transakcji.
private: NpgsqlTransaction^ transaction; //ta linia istnieje
private: String^ CDNumber;

Możesz skompilować program. Jeśli klikniesz jakiś wiersz, to dane z niego pojawią
się w polach.

Teraz kolej na procedurę zmiany danych. Jeśli uruchomiłeś aplikację, to zakończ jej
pracę. W widoku projektowania okna kliknij podwójnie przycisk Zmień. Zostanie utwo-
rzona metoda obsługi kliknięcia.
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {}

Zanim zaczniemy wypełniać tę metodę, trzeba utworzyć uchwyt do obiektu komendy


typu NpgsqlCommand, tam gdzie pozostałe obiekty, jako pole klasy Form1.
private: NpgsqlCommand^ inscomm; //ta linia istnieje
private: NpgsqlCommand^ updacomm;

Możemy już przejść do pisania samej metody button2_Click(), wysyłającej do bazy


komendę UPDATE. Zasada działania jest bardzo podobna do metody z komendą INSERT.
Na początku mamy wstawianie parametrów, a następnie utworzenie transakcji i uru-
chomienie jej. Jeśli wystąpi błąd, to jest on przechwytywany w wyjątku i transakcja
jest odwoływana.

Oto cała metoda:


private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
updacomm = gcnew NpgsqlCommand("update plyty set
artysta=@artysta,tytul=@tytul,rok=@rok where numer=@numer",connection);
updacomm->Parameters->Clear();
updacomm->Parameters->Add("@artysta",textBox1->Text);
updacomm->Parameters->Add("@tytul",textBox2->Text);
updacomm->Parameters->Add("@rok",textBox3->Text);
updacomm->Parameters->Add("@numer",CDNumber);

try {
connection->Open();
transaction=connection->BeginTransaction();
updacomm->Transaction = transaction;
updacomm->ExecuteNonQuery();
transaction->Commit();
connection->Close();
} catch (Exception^ e) {
Rozdział 16. ♦ Aplikacja bazy danych 297

transaction->Rollback();
MessageBox::Show(e->Message,"Błąd",MessageBoxButtons::OK,MessageBoxIcon::Error);
}
odswiez();
}

Skompiluj projekt i podświetl myszką dowolny wiersz z wpisanych do bazy. Zawartość


wiersza pojawi się w odpowiednich polach. Zmień dowolnie napisy w polach i naciśnij
Zmień; wiersz w bazie zostanie zmieniony.

Kasowanie danych
Do kasowania danych służy komenda SQL DELETE.
DELETE FROM NazwaTabeli WHERE NazwaKolumnyIdentyfikacji=warunek;

Dane kasujemy z tabeli NazwaTabeli. Określenie, która kolumna ma zostać skasowana,


jest identyczne jak przy instrukcji UPDATE. Implementacja tego zapytania jest prosta,
bo zawiera tylko jeden parametr. Kolumną identyfikującą rekord (wiersz) do skasowa-
nia będzie Numer, tak jak w instrukcji UPDATE.

Przykład 16.10

Zaprogramuj kasowanie wiersza danych z bazy.

Rozwiązanie
Rozpoczynamy od deklaracji obiektu komendy; będzie się nazywał delcomm. Deklaru-
jemy go jako pole klasy Form1, tam gdzie pozostałe komendy.
private: NpgsqlCommand^ updacomm; //ta linia istnieje
private: NpgsqlCommand^ delcomm;

Przejdź do zakładki widoku projektowania okna aplikacji i kliknij podwójnie na przy-


cisk Skasuj. Tradycyjnie doprowadzi to do utworzenia metody obsługi kliknięcia
przycisku.
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {}

We wnętrzu metody button3_Click() tworzymy obiekt zapytania kasującego operato-


rem gcnew.
delcom = gcnew NpgsqlCommand("delete from plyty where numer=@numer",connection);

Jak już pisałem, komenda ma tylko jeden parametr, wskazujący, który wiersz należy
skasować.

Działanie tej metody jest bardzo podobne do poprzednich, a więc mamy najpierw
podstawienie wartości parametru @numer ze zmiennej CDNumber przechowującej wartość
298 Microsoft Visual C++ 2012. Praktyczne przykłady

kolumny numer aktualnie zaznaczonego wiersza. Następnie przygotowanie transakcji


i jej wykonanie z obsługą wyjątku.
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
delcomm = gcnew NpgsqlCommand("delete from plyty where numer=@numer",connection);
delcomm->Parameters->Clear();
delcomm->Parameters->Add("@numer",CDNumber);
try {
connection->Open();
transaction=connection->BeginTransaction();
delcomm->Transaction = transaction;
delcomm->ExecuteNonQuery();
transaction->Commit();
connection->Close();
} catch (Exception^ e) {
transaction->Rollback();
MessageBox::Show(e->Message,"Błąd",MessageBoxButtons::OK,
´MessageBoxIcon::Error);
}
odswiez();
}

Pozostało jeszcze oprogramowanie przycisku Wyjdź. Jest to banalnie proste. Dwukrotnie


kliknij na przycisk Wyjdź w widoku projektowania okna aplikacji, następnie w metodę,
która się utworzy, wpisz wywołanie metody Close().
private: System::Void button4_Click(System::Object^ sender, System::EventArgs^ e) {
Close();
}

I to już całość aplikacji. Możesz ją skompilować i uruchomić, jeśli działa serwer bazy.

Obsługa bazy
Po uruchomieniu nasza baza danych wyświetla zawartość tabeli płyty w siatce. Dane
do bazy wpisuje się wierszami. Aby dodać wiersz do bazy, wypełnij pola tekstowe
i naciśnij przycisk Dodaj. W celu modyfikacji wiersza zaznacz go, zawartość pojawi się
w odpowiednich polach tekstowych. Możesz je zmodyfikować i nacisnąć Zmień,
zmiany zostaną wprowadzone do bazy. Aby skasować wiersz, zaznacz go i naciśnij
Skasuj. Opuszczasz program przyciskiem Wyjdź.
Rozdział 17.
Metody
związane z czasem
— komponent Timer

Czas systemowy
Obiektem reprezentującym czas i datę oraz umożliwiającym operacje na nich jest
DateTime. Czas może być przechowywany we właściwościach tego obiektu, natomiast
metody klasy DateTime służą do działań na czasie. Obiekt ten zawiera datę i czas
w ogólności, to znaczy nie zawsze musi zawierać aktualną datę systemową. Każde jego
wystąpienie może być użyte do przechowania dowolnej daty i czasu.

Wybrane właściwości obiektu DateTime przedstawia tabela 17.1, a metody tabela 17.2.

Tabela 17.1. Właściwości obiektu DateTime


Właściwość Znaczenie
Date Zawiera datę (dzień, miesiąc, rok) w formacie zależnym od ustawień lokalnych.
Day Dzień miesiąca.
DayOfWeek Nazwa dnia tygodnia.
DayOfYear Numer dnia w roku (np. 1 lutego jest 32. dniem roku).
Hour Godzina.
Millisecond Milisekunda.
Minute Minuty w godzinie.
Month Numer miesiąca.
Now Właściwość zawierająca zawsze aktualny pełny czas i datę systemową.
Second Sekunda.
300 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 17.1. Właściwości obiektu DateTime (ciąg dalszy)


Właściwość Znaczenie
TimeOfDay Zawiera sam czas (godziny, minuty, sekundy, setne sekundy), bez daty.
Today Zawsze aktualna data systemowa (bez czasu).
UtcNow Aktualny czas UTC (uniwersalny) dla czasu systemowego.
Year Rok.

Tabela 17.2. Wybrane metody obiektu DateTime


Metoda Działanie
AddDays(double wartość), Dodaje podaną liczbę dni, miesięcy, lat do czasu w obiekcie
AddHours(double wartość), DateTime, na którym wywołujemy metodę.
AddYears(double wartość)
Compare(DateTime czas1, Porównuje dwa obiekty DateTime i zwraca wartości:
DateTime czas2)
Wartość większa od 0 oznacza, że moment czasu czas1 jest
późniejszy niż czas2.
Wartość równa 0 oznacza, że oba momenty są równe.
Wartość mniejsza od 0 oznacza, że moment czasu czas1 jest
wcześniejszy niż czas2.
IsDaylightSavingTime() Zwracana wartość true oznacza, że czas przechowywany przez
obiekt, na którym wywołano tę metodę, jest czasem letnim dla danej
strefy czasowej.
IsLeapYear() Zwracana wartość true oznacza, że obiekt DateTime, na którym
wywołano tę metodę, wskazuje na rok przestępny.

Na obiektach DateTime można także wykonywać działania dodawania i odejmowania.


Otrzymujemy wtedy obiekt TimeSpan, który jest przedziałem czasowym.

Przykład 17.1

Napisz aplikację, która przy zamknięciu będzie wyświetlała okno dialogowe z podanym
czasem uruchomienia tej aplikacji.

Rozwiązanie
Do nowego projektu aplikacji okienkowej C++/CLI wstaw przycisk Button. We wła-
ściwości Text tego przycisku wpisz Zamknij, bo będzie on zamykał aplikację.

Aby określić czas działania aplikacji, musimy znać moment jej uruchomienia. Zapro-
gramuj metodę uruchamianą zdarzeniem Load występującym w momencie uruchomienia,
która zapisze ten czas. Najpierw w klasie Form1 po dyrektywie #pragma endregion za-
deklaruj obiekt typu DateTime do zapisu tego czasu.
private: DateTime czas_start;
Rozdział 17. ♦ Metody związane z czasem — komponent Timer 301

Następnie w trybie budowy okna Form1.h [Design] wybierz myszką okno aplikacji,
rozwiń panel Properties, przełącz go na widok zdarzeń, znajdź zdarzenie Load w dziale
Behavior i kliknij je dwukrotnie. Powstałą metodę zmodyfikuj następująco:
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
czas_start=DateTime::Now;
}

Teraz w taki sam sposób znajdź zdarzenie FormClosing. W metodzie wywoływanej tym
zdarzeniem będziemy obliczać czas działania i wyświetlać okno dialogowe.
private: System::Void Form1_FormClosing(System::Object^ sender,
System::Windows::Forms::FormClosingEventArgs^ e) {
TimeSpan^ czas_dzial = gcnew TimeSpan();
czas_dzial=DateTime::Now-czas_start;
MessageBox::Show("Od uruchomienia programu minęło " +
czas_dzial->Minutes.ToString() + " minut " +
czas_dzial->Seconds.ToString() + " sekund",
"Czas pracy", MessageBoxButtons::OK, MessageBoxIcon::Information);
}

W metodę obsługi zdarzenia Click przycisku wystarczy wstawić wywołanie metody


kończącej aplikację.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Close();
}

Po uruchomieniu aplikacji zaczekaj chwilę, a następnie zamknij ją przyciskiem. Pojawi


się okno z informacją o czasie działania. Taki sam efekt da zamknięcie okna za pomocą
standardowego przycisku w prawym górnym rogu.

Komponent Timer
Czasomierz Timer jest komponentem generującym określone zdarzenie w podanych od-
stępach czasu. Zdarzenie generowane przez Timer ma nazwę Tick. Dzięki możliwości
podłączenia pod nie dowolnej metody uzyskujemy możliwość jej okresowego uru-
chamiania. Komponent Timer jest niewidoczny w oknie aplikacji i pojawia się jedynie
na pasku niewidocznych komponentów. Dwie najważniejsze właściwości komponentu
przedstawia tabela 17.3.

Tabela 17.3. Właściwości obiektu Timer


Właściwość Znaczenie
Interval Czas między generacją zdarzenia Tick w milisekundach.
Enabled Właściwość określająca, czy timer jest włączony: true — włączony, false — wyłączony.

Uruchomienie odliczania czasu następuje poprzez ustawienie właściwości Enabled na


true lub wywołanie metody Start() na obiekcie czasomierza. Zatrzymanie, odpowiednio,
poprzez ustawienie właściwości na false lub wywołanie metody Stop().
302 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 17.2

Wyświetl w oknie aplikacji pełne dane o aktualnym czasie i dacie. Niech dane te będą
na bieżąco uaktualniane.

Rozwiązanie
Znowu będziemy potrzebowali projektu aplikacji okienkowej C++/CLI według przy-
kładu 1.4. Do okna aplikacji wstaw dwa komponenty Label oraz komponent Timer.
W celu uzyskania lepszej widoczności ustaw właściwość Font/Size dla etykiet na 14.

Aktualny czas będziemy sprawdzać co sekundę za pomocą komponentu Timer. W widoku


projektowania aplikacji zaznacz w pasku na dole komponent Timer. Ponieważ wystarczy
sprawdzać czas co sekundę, ustaw właściwość Interval na 1000. Przełącz panel na
widok zdarzeń, odnajdź zdarzenie Tick i kliknij je podwójnie. Metoda obsługująca to
zdarzenie będzie miała postać jak niżej.
private: System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e) {
DateTime czas = DateTime::Now;
label1->Text=czas.Hour.ToString("D2") + ":" + czas.Minute.ToString("D2") +
´":" + czas.Second.ToString("D2");
label2->Text=czas.Day.ToString() + "-" + czas.Month.ToString() + "-" +
´czas.Year.ToString()+ " " + czas.DayOfWeek.ToString();
}

Uruchomienie czasomierza będzie następowało przy starcie aplikacji w obsłudze zda-


rzenia Load.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
timer1->Start();
}

Natomiast zatrzymanie nastąpi przy zamykaniu (zdarzenie FormClosing).


private: System::Void Form1_FormClosing(System::Object^ sender,
´System::Windows::Forms::FormClosingEventArgs^ e) {
timer1->Stop();
}

Skompiluj i uruchom aplikację. Nie wymaga ona żadnej obsługi, na formatce od razu
pojawia się zegar z aktualnym czasem, jak na rysunku 17.1

Rysunek 17.1.
Aplikacja
wyświetlająca
aktualny czas
Rozdział 18.
Grafika w aplikacjach
.NET Framework

Obiekt Graphics
— kartka do rysowania
Większość komponentów wizualnych VC++ zawiera właściwość Graphics, dzięki której
można rysować, wypisywać teksty i umieszczać na nich grafiki w postaci bitmapy. Mo-
żesz pomyśleć o właściwości Graphics jako o kartce czy płótnie, na którym można
tworzyć grafikę.

Na początku po utworzeniu komponentu jego właściwość Graphics jest „pusta”. Przed


rozpoczęciem rysowania trzeba stworzyć obiekt typu Graphics i „podpiąć” do tej wła-
ściwości. Dopiero na tym obiekcie można rysować. Do tworzenia obiektu Graphics służy
metoda CreateGraphics() wywoływana na komponencie, na którym chcemy rysować.

Oprócz podłoża do rysowania niezbędne są także obiekty klasy Pen i Brush, a do wy-
świetlania tekstu także obiekt opisujący czcionkę typu Font.
 Pen — pióro, jakim rysujemy.
 Brush — pędzel; rodzaj wypełnienia rysowanych obiektów (kolor, deseń).
Mamy dwa rodzaje pędzli: SolidBrush to pędzel jednokolorowy,
TextureBrush zaś wypełnia obiekty deseniem z podanej bitmapy.
 Font — określa czcionkę do rysowania napisów.

Zasadę rysowania najlepiej wyjaśni prosty przykład.


304 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 18.1

Po naciśnięciu przycisku narysuj ukośną niebieską linię na oknie aplikacji.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do formatki przycisk
Button.

Po naciśnięciu przycisku należy utworzyć obiekt typu Graphics dla głównego okna
aplikacji, a następnie obiekt pióra Pen. Ponieważ metoda button1_Click() jest metodą
klasy reprezentującej główne okno, odwołujemy się do tego okna za pomocą wskaźnika
this. Teraz można już rysować po oknie, korzystając z metody obiektu Graphics rysu-
jącej linie. Oto kod metody, którą należy przypisać do zdarzenia Click:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Graphics^ g1=this->CreateGraphics();
Pen^ pioro = gcnew Pen(System::Drawing::Color::Blue);
g1->DrawLine(pioro,10,10,100,100);
}

Po naciśnięciu przycisku na oknie pojawi się linia jak na rysunku 18.1.

Rysunek 18.1.
Rysowanie linii w oknie
aplikacji

Zauważ, że pióro Pen i pędzel Brush nie są właściwościami obiektu Graphics, ale
oddzielnymi obiektami.

Obiekt Graphics posiada wiele metod służących do rysowania punktów, linii, figur, a na-
wet wyświetlania całych bitmap z plików. Zestawienie metod klasy Graphics podaje
tabela 18.1.

Ponieważ metod jest dużo i każda ma kilka postaci, tabela 18.1 podaje tylko po jednej
postaci każdej metody, aby możliwe było zestawienie wszystkich.
Rozdział 18. ♦ Grafika w aplikacjach .NET Framework 305

Tabela 18.1. Wybrane metody rysujące obiektu Graphics


Metoda Działanie
DrawLine(Pen^ pi,int x1,int y1, Rysuje linie o początku w x1, y1 i końcu w x2, y2,
int x2,int y2) używając pióra pi.
DrawArc(Pen^ pi,float x,float y, Wycinek okręgu lub elipsy rysowany piórem pi,
float szer,float wys,float środek w (x, y), łuk mieści się w prostokącie
kąt_start,float kąt) o wymiarach szer i wys, zaczyna się od kąta
kąt_start i biegnie przez kąt stopni.
DrawBezier(Pen^ pi,float x1, Rysuje krzywą sklejaną Béziera przez cztery podane
float y1,float x2,float y2, punkty.
float x3,float y3,float x4,float y4)
DrawBeziers(Pen^ pi, Rysuje serię krzywych Béziera zgodnie z podaną
array<System::Drawing::PointF>^ tablicą punktów.
punkty)
DrawClosedCurve(Pen^ pi, Rysuje zamkniętą krzywą sklejaną zgodnie z podaną
array<System::Drawing::PointF>^ tablicą punktów.
punkty)
DrawCurve(Pen^ pi, Rysuje krzywą sklejaną (spline) między punktami
array<System::Drawing::PointF>^ podanymi w tabeli.
punkty)
DrawEllipse(Pen^ pi,float x, float y, Rysuje elipsę mieszczącą się w prostokącie określonym
float szer, float wys) współrzędnymi oraz wysokością i szerokością.
DrawIcon(Icon^ ikona, int x, int y) Rysuje ikonę w podanych współrzędnych.
DrawImage(Image^ obraz,int x,int y) Rysuje bitmapę obraz we współrzędnych (x, y).
DrawPie(Pen^ pi,float x,float y, Rysuje wycinek koła piórem pi, środek w (x, y),
float szer,float wys,float wycinek mieści się w prostokącie o wymiarach szer
kąt_start,float kąt)
i wys, zaczyna się od kąta kąt_start i biegnie przez
kąt stopni.
DrawPolygon(Pen^ pi, Tworzy zamknięty wielobok o wierzchołkach
array<System::Drawing::PointF>^ określonych w tabeli punkty.
punkty)
DrawRectangle(Pen^ pi,float x, Rysuje prostokąt o lewym górnym rogu w punkcie (x, y)
float y,float szer,float wys) i bokach o długości szer i wys.
DrawString(String^ tekst,Font^ Wypisuje tekst tekst z lewym górnym rogiem
czcionka,Brush^ pędzel, float x,float y) w punkcie (x, y), używając podanej czcionki i pędzla.
FillClosedCurve(Brush^ pędzel, Tworzy wypełnioną pędzlem pędzel zamkniętą
array<System::Drawing::PointF>^ krzywą sklejaną (spline) określoną punktami punkty.
punkty)
FillEllipse(Brush^ pędzel, float x, Rysuje elipsę w prostokącie określonym
float y, float szer, float wys) współrzędnymi (x, y) oraz wysokością i szerokością
wypełnioną pędzlem pędzel.
FillPie(Brush^ pędzel, float x, Maluje wypełniony pędzlem pędzel wycinek koła,
float y,float szer,float wys, środek w (x, y), wycinek mieści się w prostokącie
float kąt_start,float kąt) o wymiarach szer i wys, zaczyna się od kąta
kąt_start i biegnie przez kąt stopni.
FillPolygon(Brush^ pędzel, Tworzy wypełniony pędzlem pędzel wielokąt
array<System::Drawing::PointF>^ punkty) o wierzchołkach w tabeli punkty.
306 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 18.1. Wybrane metody rysujące obiektu Graphics (ciąg dalszy)


Metoda Działanie
FillRectangle(Brush^ pędzel, Rysuje wypełniony pędzlem pędzel prostokąt o lewym
float x,float y,float szer,float wys) górnym rogu w punkcie (x, y) oraz podanej szerokości
i wysokości.
Clear(Color kolor) Czyści całą powierzchnię rysunku i wypełnia ją
kolorem kolor.

Przykład 18.2

Wyświetl w oknie programu tekst podany w polu tekstowym. Nie używaj komponentu
Label.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna pole tekstowe
TextBox oraz przycisk Button.

Po naciśnięciu przycisku zostanie utworzony pędzel typu SolidBrush (jednokolorowy)


i wypisany tekst z pola.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Graphics^ g1=this->CreateGraphics();
g1->Clear(System::Drawing::Color::FromName("Control"));
SolidBrush^ pedzel = gcnew SolidBrush(System::Drawing::Color::DarkGreen);
System::Drawing::Font^ czcionka =
´gcnew System::Drawing::Font(System::Drawing::FontFamily::GenericSansSerif,
´14, FontStyle::Regular);
g1->DrawString(textBox1->Text,czcionka,pedzel,10,50);
}
Wynik działania aplikacji przedstawia rysunek 18.2.

Rysunek 18.2.
Rysowanie tekstu
w oknie aplikacji

Przykład 18.3

Po naciśnięciu przycisku wyświetl na formularzu wykres ze współrzędnych podanych


w siatce DataGridView.
Rozdział 18. ♦ Grafika w aplikacjach .NET Framework 307

Rozwiązanie
Utwórz projekt aplikacji według przykładu 1.4. Wstaw do okna siatkę DataGridView
i przycisk Button.

Teraz kliknij komponent siatki w oknie, rozwiń panel Properties, znajdź właściwość
Columns i kliknij przycisk po jej prawej stronie. W edytorze kolumn kliknij Add…. Na li-
ście rozwijalnej w pozycji Type edytora kolumn wybierz DataGridViewTextBoxColumn
i dwukrotnie kliknij Add, wstawiając dwie kolumny z polem tekstowym. Następnie kliknij
Close, zamykając okno dodawania kolumn.

W edytorze kolumn, przy zaznaczonej w lewym oknie pierwszej kolumnie, znajdź w pra-
wej tabeli właściwości właściwość HeaderText i wpisz X. Następnie zaznacz drugą
kolumnę i wpisz w HeaderText Y. Naciśnij OK. Nasz wykres będzie się składał z pięciu
punktów. Kliknij podwójnie gdziekolwiek na obszarze okna aplikacji, tworząc metodę
Form1_Load() wykonywaną przy zdarzeniu Load okna. Tu wpisz kod tworzący pięć wierszy.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
dataGridView1->RowCount=5;
}

Powiększ wymiary okna i kontrolki siatki, tak aby po uruchomieniu całość wyglądała jak
na rysunku 18.3 (na razie bez wykresu i wpisanych współrzędnych).

Rysunek 18.3.
Aplikacja do rysowania
wykresów

Po naciśnięciu przycisku Rysuj współrzędne będą pobierane z pól siatki i przekazywane


do tablicy obiektów System::Drawing::PointF, które reprezentują punkty na płaszczyźnie.
Dane z pól siatki pobierane są wierszami w pętli for. Następnie narysujemy wykres za
pomocą metody DrawCurve(). Pod zdarzenie Click przycisku podepnij podaną metodę:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Graphics^ g1=this->CreateGraphics();
g1->Clear(System::Drawing::Color::FromName("Control"));
array<System::Drawing::PointF>^ punkty = gcnew array
´<System::Drawing::PointF>(5);

for (System::Int32 i=0;i<5;i++){


punkty[i].X=Convert::ToSingle(dataGridView1->Rows[i]->Cells[0]->Value);
308 Microsoft Visual C++ 2012. Praktyczne przykłady

punkty[i].Y=(this->Height-30)-Convert::ToSingle(dataGridView1->Rows[i]->
´Cells[1]->Value);
}

Pen^ pioro1 = gcnew Pen(System::Drawing::Color::DarkGreen);


g1->DrawCurve(pioro1,punkty);
}

Przykład 18.4

Wyświetl w oknie plik rysunek.jpg z lewym górnym rogiem w punkcie (50, 50).

Rozwiązanie
Do nowego projektu aplikacji okienkowej utworzonej według przykładu 1.4 wstaw
przycisk Button.

Aby wyświetlić obrazek, najpierw utworzymy obiekt Image zawierający plik rysunek.jpg
(plik o takiej nazwie trzeba wcześniej umieścić w folderze programu), a następnie
wyświetlimy go w oknie, używając metody DrawImage().
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Graphics^ g1=this->CreateGraphics();
Image^ obrazek = Image::FromFile("rysunek.jpg");
g1->DrawImage(obrazek,50,50);
}

Pióro Pen
Używany już przez nas obiekt Pen, czyli pióro, daje wiele możliwości. Za pośrednictwem
właściwości tego obiektu można zarówno sterować grubością linii, jak i rysować linie
przerywane, wybierać styl rozpoczęcia i zakończenia linii itp. Najważniejsze właści-
wości przedstawia tabela 18.2.

Tabela 18.2. Niektóre właściwości obiektu Pen


Właściwość Znaczenie
Width Szerokość pióra w punktach.
Brush Pędzel, jakim rysowana jest linia.
Color Kolor linii.
StartCap Rodzaj znaczka dodawanego na rozpoczęcie linii. Wartości tej właściwości są typu
System::Drawing::Drawing2D::LineCap, oto kilka z nich:
 System::Drawing::Drawing2D::LineCap::Square — linia z kwadratowym
zakończeniem,
 System::Drawing::Drawing2D::LineCap::ArrowAnchor — linia ze strzałką,
 System::Drawing::Drawing2D::LineCap::Triangle — linia z trójkątem.
Rozdział 18. ♦ Grafika w aplikacjach .NET Framework 309

Tabela 18.2. Niektóre właściwości obiektu Pen (ciąg dalszy)


Właściwość Znaczenie
EndCap Rodzaj zakończenia linii, wartości takie jak w StartCap.
DashStyle Ustawia linię przerywaną. Wartości to:
 System::Drawing::Drawing2D::DashStyle::Dash — linia z kresek,
 System::Drawing::Drawing2D::DashStyle::DashDot — linia z kresek i kropek,
 System::Drawing::Drawing2D::DashStyle::DashDotDot — linia z kreski i dwóch kropek,
 System::Drawing::Drawing2D::DashStyle::Dot — linia z kropek,
 System::Drawing::Drawing2D::DashStyle::Solid — linia ciągła.
DashCap Ustawia rodzaj zakończenia elementów linii przerywanej. Wartości to:
 System::Drawing::Drawing2D::DashCap::Flat — zakończenie prostokątne,
 System::Drawing::Drawing2D::DashCap::Triangle — zakończenie trójkątne,
 System::Drawing::Drawing2D::DashCap::Round — zakończenie zaokrąglone.
DashOffset Przesunięcie wzoru kreskowanej linii względem początku linii.

Przykład 18.5

Wyświetl w oknie kilka linii rysowanych różnymi stylami.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do niego przycisk Button.

Po naciśnięciu przycisku będziemy zmieniać parametry pióra i rysować nim kolejne linie.
Oto metoda dla zdarzenia Click przycisku:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Graphics^ g1=this->CreateGraphics();
Pen^ pioro1 = gcnew Pen(System::Drawing::Color::DarkGreen);
pioro1->StartCap = System::Drawing::Drawing2D::LineCap::RoundAnchor;
pioro1->EndCap = System::Drawing::Drawing2D::LineCap::ArrowAnchor;
pioro1->Width=4;
g1->DrawLine(pioro1,10,10,250,10);

pioro1->StartCap = System::Drawing::Drawing2D::LineCap::Square;
pioro1->EndCap = System::Drawing::Drawing2D::LineCap::Square;
pioro1->DashStyle = System::Drawing::Drawing2D::DashStyle::DashDotDot;
pioro1->Color=System::Drawing::Color::DarkRed;
pioro1->Width=1;
g1->DrawLine(pioro1,10,30,250,30);

pioro1->DashStyle = System::Drawing::Drawing2D::DashStyle::Dash;
pioro1->Width=6;
pioro1->DashCap = System::Drawing::Drawing2D::DashCap::Triangle;
g1->DrawLine(pioro1,10,50,250,50);

pioro1->DashStyle = System::Drawing::Drawing2D::DashStyle::DashDot;
pioro1->Width=10;
pioro1->Color=System::Drawing::Color::Blue;
pioro1->DashCap = System::Drawing::Drawing2D::DashCap::Triangle;
g1->DrawLine(pioro1,10,70,250,70);
310 Microsoft Visual C++ 2012. Praktyczne przykłady

pioro1->DashOffset = 1;
g1->DrawLine(pioro1,10,90,250,90);

pioro1->DashOffset = 2;
g1->DrawLine(pioro1,10,110,250,110);
}

Efekt po naciśnięciu przycisku przedstawia rysunek 18.4.

Rysunek 18.4.
Działanie różnych
rodzajów piór

Pędzle zwykłe i teksturowane


Pędzle są obiektami dwóch klas potomnych klasy Brush, mianowicie SolidBrush
i TextureBrush. Pierwszy z nich to pędzel jednokolorowy, natomiast drugi maluje za
pomocą podanych bitmap (tekstur) i daje znacznie większe możliwości.

Kolor pędzla SolidBrush ustawiamy, używając właściwości Color tego obiektu.

Właściwości pędzla TextureBrush przedstawia tabela 18.3.

Tabela 18.3. Właściwości pędzla TextureBrush


Właściwość Znaczenie
Image Określa bitmapę (teksturę) pędzla.
WrapMode Sposób wyświetlania tekstury:
 System::Drawing::Drawing2D::WrapMode::Tile — tekstura wyświetlana jest
sąsiadująco (jak kafelki),
 System::Drawing::Drawing2D::WrapMode::Clamp — tekstura wyświetlana jest
jako pojedyncza,
 System::Drawing::Drawing2D::WrapMode::TileFlipX — wyświetlanie sąsiadująco
tekstury obróconej względem osi X,
 System::Drawing::Drawing2D::WrapMode::TileFlipY — jak wyżej, ale względem
osi Y,
 System::Drawing::Drawing2D::WrapMode::TileFlipXY — jak wyżej, ale obrócenie
względem obu osi.
Transform Określa transformacje, jakim może zostać poddana tekstura przed wyświetleniem.
Może to być skalowanie, obracanie lub pochylanie obrazu.
Rozdział 18. ♦ Grafika w aplikacjach .NET Framework 311

Przykład 18.6

Narysuj na oknie aplikacji dwa wycinki kół: jeden wypełniony jednym kolorem i drugi
wypełniony teksturą pobraną z pliku rysunek.jpg w ułożeniu kafelkowym.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do niego przycisk Button.

Po naciśnięciu tego przycisku utworzymy dwa pędzle: jednokolorowy i teksturowany,


a następnie wyświetlimy wycinki kół za pomocą metody FillPie(). Aby program
działał, potrzebny jest rysunek rysunek.jpg w folderze programu.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Graphics^ g1=this->CreateGraphics();
Image^ obrazek = Image::FromFile("rysunek.jpg");
SolidBrush^ pedz = gcnew SolidBrush(System::Drawing::Color::DarkGreen);
TextureBrush^ pedz_text = gcnew TextureBrush(obrazek);
pedz_text->WrapMode=System::Drawing::Drawing2D::WrapMode::Tile;
g1->FillPie(pedz,50,20,100,100,0,250);
g1->FillPie(pedz_text,180,20,100,100,0,250);
}

Po uruchomieniu i naciśnięciu przycisku aplikacja może wyglądać tak, jak na rysunku 18.5
(efekt jest zależny od zawartości rysunku użytego do teksturowania).

Rysunek 18.5.
Malowanie pędzlem
zwykłym
i teksturowanym

Kafelkowe układanie tekstury jest opcją domyślną, dlatego linię


pedz_text->WrapMode=System::Drawing::Drawing2D::WrapMode::Tile;

można pominąć i nie zmieni to efektu działania programu.

Zajmiemy się teraz właściwością Transform, umożliwiającą transformacje tekstury przed


jej wyświetleniem. Właściwość ta może przybierać wartości typu System::Drawing::
Drawing2D::Matrix, które są macierzami transformacji. Obiekt Matrix jest macierzą 3×3,
a każda liczba w macierzy oznacza rodzaj transformacji obiektu. Strukturą macierzy
nie będziemy się tu zajmować, ponieważ do podstawowych operacji na teksturach jej
znajomość nie jest potrzebna. Operacje na macierzy można wykonywać za pomocą
metod klasy Matrix. Tabela 18.4 przedstawia niektóre z nich.
312 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 18.4. Metody transformujące obiektu Matrix (macierzy transformacji)


Metoda Znaczenie
Rotate(Single kąt) Obraca teksturę o kąt stopni w prawo.
RotateAt(Single kąt, Obraca teksturę o kąt stopni, przy czym środek obrotu znajduje się
PointF punkt) w punkcie punkt.
Scale(Single skalaX, Skaluje teksturę; skalaX i skalaY to skale w kierunkach osi X i Y.
Single skalaY) Wartość 1 oznacza oryginalny rozmiar.
Shear(Single wspX, Transformacja polegająca na obrocie płaszczyzny rysunku wokół osi X
Single wspY) lub Y.
Translate(Single Przesuwa teksturę o odlX i odlY punktów w kierunku odpowiednich osi.
odlX, Single odlY)

Przykład 18.7

Narysuj kwadrat wypełniony teksturą z pliku rysunek.jpg w ułożeniu kafelkowym.


Tekstura ma być obrócona o 20 stopni i przesunięta o 10 punktów w kierunku osi Y.

Rozwiązanie
Do nowego projektu aplikacji utworzonego według przykładu 1.4 wstaw przycisk
Button.

Do zdarzenia Click przycisku przypiszemy metodę, w której utworzymy macierz trans-


formacji, a w tej macierzy zapiszemy odpowiednie transformacje za pomocą metod
Rotate() i Translate(). Następnie podstawimy tę macierz do właściwości Transform
pędzla, którym pomalujemy kwadrat.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Graphics^ g1=this->CreateGraphics();
Image^ obrazek = Image::FromFile("rysunek.jpg");
System::Drawing::Drawing2D::Matrix^ macierz =
gcnew System::Drawing::Drawing2D::Matrix();
macierz->Rotate(20);
macierz->Translate(0,10);
SolidBrush^ pedz =
gcnew SolidBrush(System::Drawing::Color::DarkGreen);
TextureBrush^ pedz_text = gcnew TextureBrush(obrazek);
pedz_text->WrapMode=System::Drawing::Drawing2D::WrapMode::Tile;
pedz_text->Transform=macierz;
g1->FillRectangle(pedz_text,80,20,100,100);
}

Uruchom aplikację. Po naciśnięciu przycisku wyświetli się kwadrat z teksturą jak na


rysunku 18.6.
Rozdział 18. ♦ Grafika w aplikacjach .NET Framework 313

Rysunek 18.6.
Wyświetlanie
tekstury z obrotem

Rysowanie pojedynczych punktów


— obiekt Bitmap
Jak może zauważyłeś, na obiekcie Graphics nie da się wykonywać operacji na pojedyn-
czych punktach obrazu. Aby rysować pojedyncze piksele, należy stworzyć obiekt typu
Bitmap, na nim rysować piksele za pomocą dostępnej w nim metody SetPixel(), a następ-
nie wyświetlić cały obiekt Bitmap na obiekcie Graphics za pomocą metody DrawImage().

Przykład 18.8

Narysuj na oknie aplikacji dwieście punktów o losowych współrzędnych i w losowych


kolorach.

Rozwiązanie
Utwórz projekt aplikacji według przykładu 1.4. W oknie umieść przycisk Button.

Po kliknięciu przycisku utworzymy obiekt Bitmap, na którym w pętli for zostanie umiesz-
czonych dwieście punktów. Następnie ten obiekt zostanie wyświetlony na obiekcie
Graphics okna aplikacji, czyli punkty pojawią się na tym oknie. Metoda DrawImage()
wymaga argumentu typu Image, a ponieważ obiekt bitmapa jest typu Bitmap, trzeba
zastosować konwersję typów. Oto odpowiedni kod:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Int32 wspx;
System::Int32 wspy;
System::Drawing::Color kolor;
Graphics^ g1=this->CreateGraphics();
Random^ liczba_los = gcnew Random();
Bitmap^ bitmapa = gcnew Bitmap(this->Width,this->Height);
for (System::Int32 i=0;i<200;i++) {
wspx=liczba_los->Next(this->Width);
wspy=liczba_los->Next(this->Height);
kolor=System::Drawing::Color::FromArgb(liczba_los->Next(255),
liczba_los->Next(255), liczba_los->Next(255));
314 Microsoft Visual C++ 2012. Praktyczne przykłady

bitmapa->SetPixel(wspx,wspy,kolor);
}
g1->DrawImage(dynamic_cast<Image^>(bitmapa),10,10);
}

Uruchom aplikację. Po kliknięciu przycisku zostanie narysowanych na formatce 200


punktów w losowych współrzędnych.

Rysowanie trwałe
— odświeżanie rysunku
Jeśli zwiniesz do paska zadań, a następnie rozwiniesz aplikację z dowolnego przykładu
w tym rozdziale, to rysunek zniknie z okna. Dzieje się tak, ponieważ kiedy okno jest po-
wtórnie wyświetlane, wszystkie kontrolki są malowane jeszcze raz. Generowane jest wte-
dy zdarzenie Paint. Aby rysunek był w dalszym ciągu widoczny w oknie, należy go
również odświeżyć. W praktyce oznacza to, że metoda obsługująca zdarzenie Paint
powinna rysować rysunek jeszcze raz.

Przykład 18.9
Stwórz aplikację z dwoma polami wyboru CheckBox. Niech pierwsze pole włącza i wyłą-
cza rysunek kwadratu w oknie, a drugie — rysunek koła. Rysunek ma być zachowywany
po przykryciu okna przez inne okno.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 .Wstaw w okno dwa pola wyboru.
We właściwość Text pierwszego pola wyboru wpisz Kwadrat, a drugiego — Koło.

Najpierw napisz metodę rysującą koło i kwadrat podanego koloru. Metodę umieść w kla-
sie Form1, po dyrektywie #pragma endregion, podobnie jak metody obsługujące zdarzenia.
private: System::Void rysuj_kwadrat(System::Drawing::Color kolor) {
Graphics^ g1=this->CreateGraphics();
Pen^ pioro = gcnew Pen(kolor);
g1->DrawRectangle(pioro,10,10,150,150);
delete g1;
delete pioro;
}
private: System::Void rysuj_kolo(System::Drawing::Color kolor) {
Graphics^ g1=this->CreateGraphics();
Pen^ pioro = gcnew Pen(kolor);
g1->DrawEllipse(pioro,20,20,130,130);
delete g1;
delete pioro;
}

Zaprogramuj reakcję na zaznaczenie lub wyczyszczenie pól wyboru. Kliknij dwu-


krotnie pole Kwadrat i uzupełnij powstałą metodę, która będzie obsługiwała zmianę
stanu zaznaczenia pola pierwszego:
Rozdział 18. ♦ Grafika w aplikacjach .NET Framework 315

private: System::Void checkBox1_CheckedChanged(System::Object^ sender,


System::EventArgs^ e) {
if (checkBox1->Checked)
rysuj_kwadrat(System::Drawing::Color::DarkBlue);
else
rysuj_kwadrat(System::Drawing::Color::FromName("Control"));
}

Tak samo dla pola drugiego — Koło:


private: System::Void checkBox2_CheckedChanged(System::Object^ sender,
System::EventArgs^ e) {
if (checkBox2->Checked)
rysuj_kolo(System::Drawing::Color::DarkBlue);
else
rysuj_kolo(System::Drawing::Color::FromName("Control"));
}

Możesz teraz uruchomić program. Zwiń okno do paska zadań za pomocą lewego przyci-
sku w pasku tytułowym okna. Kliknij ikonę aplikacji w pasku zadań, wyświetlając
okno z powrotem. Figury znikną.

Aby narysować je trwale, muszą być odrysowywane w zdarzeniu Paint. W widoku bu-
dowy okna aplikacji (zakładka Form1.h [Design]) kliknij budowane okno aplikacji, a na-
stępnie rozwiń panel Properties i przełącz się na widok zdarzeń (ikona błyskawicy).
Teraz znajdź zdarzenie Paint i kliknij je dwukrotnie. Zostaniesz przeniesiony do kodu
aplikacji, gdzie utworzy się metoda Form1_Paint(). Metoda ta będzie rysowała figury,
gdy będą zaznaczone odpowiednie pola. Uzupełnij ją jak niżej:
private: System::Void Form1_Paint(System::Object^ sender,
System::Windows::Forms::PaintEventArgs^ e) {
if (checkBox1->Checked)
rysuj_kwadrat(System::Drawing::Color::DarkBlue);
if (checkBox2->Checked)
rysuj_kolo(System::Drawing::Color::DarkBlue);
}

Uruchom i wypróbuj działanie programu. Zaznacz pola, wyświetlając figury. Zwiń i roz-
wiń okno aplikacji, figury będą nadal wyświetlone. Wygląd aplikacji przedstawia ry-
sunek 18.7.

Rysunek 18.7.
Aplikacja z trwałym
rysowaniem
316 Microsoft Visual C++ 2012. Praktyczne przykłady

Animacje
Proste animacje można zrealizować za pomocą cyklicznego wyświetlania figur w zmie-
niających się współrzędnych. Figury wyświetlamy w wybranym kolorze, a następnie
kasujemy przez rysowanie ich w kolorze tła. Kolejnymi przerysowaniami kształtów
można sterować za pomocą timera.

Przykład 18.10

Napisz program wyświetlający animowany prostokąt z obracającą się teksturą. Pro-


stokąt powinien przemieszczać się w górę i w dół przez całą wysokość okna.

Rozwiązanie
Utwórz nowy projekt aplikacji C++/CLI i wstaw do niego komponent Timer oraz dwa
przyciski, które będą służyły do zatrzymania i rozpoczęcia ruchu prostokąta. We właści-
wość Text pierwszego przycisku wpisz Start, a drugiego Stop. Potrzebny będzie jesz-
cze rysunek o nazwie rysunek.jpg umieszczony w folderze projektu. Po dyrektywie
#pragma endregion umieść deklaracje trzech pól klasy Form1, które będą pełniły rolę
zmiennych globalnych. Pierwsze z nich to szybkość przesuwania prostokąta, drugie to
aktualny kąt obrotu, trzecie to współrzędne lewego górnego rogu prostokąta, a czwarte
to uchwyt do obiektu Graphics.
private: System::Int16 krok;
private: System::Int16 obrot;
private: System::Int16 y;
private: Graphics^ g1;

Początkowe wartości tych pól będą ustawiane zaraz po pierwszym wyświetleniu okna.
Nadaje się więc do tego celu metoda wywoływana po zdarzeniu Load. Kliknij podwójnie
okno aplikacji, a środowisko napisze odpowiednią metodę Form1_Load(), na razie pu-
stą. Do tej metody wpisz inicjalizację zmiennych krok i obrót, a także przypisanie do
uchwytu konkretnego obiektu Graphics.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
krok=2;
obrot=0;
g1=this->CreateGraphics();
}

Kliknij obiekt timer1 w pasku niewidocznych komponentów, następnie rozwiń panel


Properties za pomocą pionowej zakładki z prawej strony okna środowiska VC++ 2012.
Przełącz się na widok zdarzeń i znajdź zdarzenie Tick timera. W sumie słowo „znajdź”
jest tu nieodpowiednie — nie ma czego szukać, ponieważ timer ma tylko jedno zdarzenie.
Kliknij podwójnie nazwę zdarzenia, a utworzy się metoda jego obsługi timer1_Tick().
W tej metodzie znajdzie się clue naszego programu. Kolejne linie wpisuj w jej wnętrze aż
do odwołania. Pierwsza linia to zamknięcie rysunku rysunek.jpg w obiekcie Image.
Będzie to nasza tekstura.
Image^ obrazek = Image::FromFile("rysunek.jpg");
//Następnie potrzebna będzie macierz transformacji do obracania tekstury
Rozdział 18. ♦ Grafika w aplikacjach .NET Framework 317

System::Drawing::Drawing2D::Matrix^ macierz =
gcnew System::Drawing::Drawing2D::Matrix();

Obracaliśmy już teksturę wcześniej, tu mamy to samo: za pomocą metody rotate()


obiektu macierzy obracamy ją o aktualny kąt ukryty w zmiennej obrot. Następnie
zwiększamy wartość tej zmiennej, przygotowując ją do następnego obrotu.
macierz->Rotate(obrot);
obrot=obrot+1;

Zauważ, że przy kolejnych krokach zawsze obracamy teksturę od kąta zero stopni, nie ma
tu inkrementacji obrotu. Teraz potrzebujemy dwóch pędzli: zwykłego o kolorze tła do
kasowania figury i teksturowanego do jej rysowania.
SolidBrush^ pedzel_kas=gcnew SolidBrush(System::Drawing::Color::FromName("Control"));
TextureBrush^ pedzel_text = gcnew TextureBrush(obrazek);

Teksturę w pędzlu obracamy.


pedzel_text->Transform=macierz;

I wreszcie rysowanie. Najpierw wycieramy stary kwadrat w poprzednich współrzędnych,


a następnie zmieniamy współrzędne i rysujemy nowy.
g1->FillRectangle(pedzel_kas,10,y,50,50);
y=y+krok;
g1->FillRectangle(pedzel_text,10,y,50,50);

Jeśli kwadrat osiągnął dolną krawędź okna, to odwracamy kierunek ruchu.


if ((y>this->Height-100)||(y<1))
krok*=-1;

Ponieważ y to współrzędna lewego górnego rogu kwadratu, trzeba uwzględnić jego


wymiary, tak aby zmiana kierunku ruchu następowała odpowiednio wcześniej. I to
koniec metody timer1_Tick().

Pozostało zaprogramować włączanie i wyłączanie animacji. Kliknij podwójnie przycisk


Start i do metody obsługi wpisz linię uruchamiającą timer.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
timer1->Start();
}

Identycznie przycisk Stop kończy pracę timera.


private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
timer1->Stop();
}

I to już wszystko. Uruchom program i naciskając przycisk Start, rozpocznij animację.


Prędkość animacji możesz kontrolować poprzez zmianę wartości właściwości Interval
timera. Im mniejsza wartość, tym szybszy ruch.
318 Microsoft Visual C++ 2012. Praktyczne przykłady
Rozdział 19.
Podstawy aplikacji
wielowątkowych

Wątki
W systemach wielowątkowych wiele programów może być wykonywanych jednocze-
śnie. W systemach jednoprocesorowych wrażenie jednoczesności wykonania powstaje
dzięki przydzielaniu czasu procesora dla kolejnych programów na przemian. Każdy
z takich wykonywanych równolegle algorytmów nosi nazwę wątku. Znaczenie aplikacji
wielowątkowych wzrosło po pojawieniu się procesorów z kilkoma rdzeniami. Dzięki
takiej architekturze możliwa jest rzeczywista jednoczesność wykonywania wątków.

Standardowo aplikacja składa się tylko z jednego wątku, związanego z oknem głównym.
Taki model nie zawsze jest wystarczający. Ponieważ podczas wykonywania kodu
metody nie są przetwarzane zdarzenia dla danego wątku, w przypadku dłuższych metod
okno aplikacji nie jest odświeżane i nie jest możliwa obsługa kontrolek. Z tego powo-
du okno aplikacji wydaje się „zablokowane” i nie jest możliwe wyświetlanie żadnych
danych na przykład w kontrolce etykiety Label.

Aby poprawić działanie takiej aplikacji, należy wykonywać część kodu jako oddzielny
wątek. Wątek jest reprezentowany w aplikacji przez obiekt klasy Thread. Przy tworzeniu
tego obiektu parametrem konstruktora jest metoda, która będzie wykonywana w tym
wątku. Następnie do rozpoczęcia wątku służy metoda Start() klasy Thread, którą wy-
konujemy na utworzonym obiekcie. Prześledzimy zastosowanie oddzielnych wątków
na przykładzie programu wymagającego długich obliczeń.

Przykład 19.1

Napisz program odnajdujący liczby pierwsze w przedziale od 2 do 100 000. Liczba


pierwsza to taka, która dzieli się tylko przez 1 i przez samą siebie.
320 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Najpierw napiszemy tę aplikację jako klasyczną aplikację z pojedynczym wątkiem.

Utwórz nowy projekt według przykładu 1.4 i wstaw do formatki przycisk Button oraz
pole tekstowe TextBox. Właściwość Multiline pola ustaw na true i zwiększ wymiary pola
tak, aby zmieściło kilka linii. Zdarzenie Click przycisku będzie uruchamiało metodę po-
szukującą liczb pierwszych. W tym przypadku zastosujemy prosty algorytm, który
dzieli modulo kolejne liczby i przez liczby od 2 do i, sprawdzając w ten sposób, czy
liczba i ma podzielnik inny niż ona sama. W momencie znalezienia dzielnika kończy się
pętla while. Jeżeli dzielnikiem dla danej liczby jest ona sama, to liczba jest wyświe-
tlana. Nie jest to najbardziej efektywny algorytm, ale nie o to tu chodzi.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::Int32 n=2;
for (System::Int32 i=2;i<100000;i++) {
n=2;
while ((i%n))
n++;
if (i==n)
textBox1->AppendText(i.ToString()+System::Environment::NewLine);
}
}

Po uruchomieniu obliczeń przyciskiem program wydaje się działać normalnie, jednak


próba przesunięcia okna programu nie powiedzie się, a w przypadku zasłonięcia okna
przez inną aplikację i powtórnego odsłonięcia okno będzie puste, dopóki nie skończą
się obliczenia.

Przykład 19.2

Napisz program identyczny jak w przykładzie 19.1, ale z uruchamianiem obliczeń


w oddzielnym wątku.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna przycisk Button
i pole tekstowe TextBox. Również tu ustaw właściwość Multiline pola TextBox na true.

Na początku kodu w pliku Form1.h obok dyrektyw załączania przestrzeni nazw using
namespace dołącz do programu przestrzeń nazw z metodami wielowątkowości.
using namespace System::Threading;

Teraz utwórz metodę, która będzie się uruchamiała w wątku jako metoda klasy Form1.
Metoda będzie poszukiwała liczb pierwszych tak samo jak poprzednio. Zadeklaruj ją
po dyrektywie #pragma endregion.
private: System::Void watek() {
System::Int32 n=2;
for (System::Int32 i=2;i<100000;i++) {
n=2;
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 321

while ((i%n))
n++;
}
}

Naciśnięcie przycisku utworzy i uruchomi wątek.


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Thread^ watek_liczenia = gcnew Thread(gcnew ThreadStart(this,&Form1::watek));
watek_liczenia->Start();
}

Argumentem konstruktora obiektu Thread jest metoda wątku. Nie podstawiamy jej
bezpośrednio, ale używamy delegata ThreadStart. O delegatach pisałem w rozdziale
o funkcjach. Tak samo jak tam konstrukcję tego obiektu umieściłem bezpośrednio na
liście parametrów konstruktora obiektu Thread. Parametrami obiektu ThreadStart są:
obiekt klasy, do której należy metoda wątku (w tym przypadku jest to klasa głównego
okna aplikacji pobierana za pomocą wskaźnika this), oraz referencja do metody wątku.
Referencja musi zawierać nazwę klasy, do której należy metoda wątku, i nazwę samej
metody wątku.

Po uruchomieniu programu i naciśnięciu przycisku program rozpoczyna obliczenia,


ale nic nie wyświetla. Można sprawdzić, że program liczy, wywołując Menedżera zadań.
Po naciśnięciu przycisku rośnie stopień wykorzystania procesora przez naszą aplikację.
Mimo że aplikacja liczy, można ją swobodnie przesuwać, a po zakryciu i odkryciu
okno ma normalny wygląd.

Zapisz aplikację, dokończymy ją w następnym przykładzie.

To, że program z poprzedniego przykładu nic nie wyświetla, nie wynika z zapomnie-
nia, ale wymaga oddzielnego omówienia. W aplikacji wielowątkowej można korzystać
z komponentów wizualnych należących do tego samego wątku. Ponieważ pole tekstowe
TextBox jest w innym wątku niż obliczenia, nie jest możliwe korzystanie z niego bezpo-
średnio. Dzieje się tak dlatego, że mógłby wtedy wystąpić konflikt między komuni-
katami przesyłanymi do kontrolek z różnych wątków. Następny przykład pokazuje,
jak poradzić sobie z tym ograniczeniem.

Komunikacja z komponentami z innych


wątków — przekazywanie parametrów
Ponieważ niemożliwa jest komunikacja z kontrolkami pochodzącymi z innego wątku,
każde odwołanie do komponentu musi być realizowane z wątku, do którego należy
komponent. Wiele komponentów posiada metodę Invoke(), która wywołuje dowolną
inną metodę (za pomocą delegata) tak, jakby była ona wywoływana w wątku, do które-
go należy kontrolka. Komunikację z komponentami z innych wątków można zapisać
w punktach:
322 Microsoft Visual C++ 2012. Praktyczne przykłady

a) W klasie okna, do której należy kontrolka okna, definiujemy metodę


komunikującą się z tą kontrolką (na przykład piszącą do pola tekstowego).
b) Również w klasie okna, do którego należy kontrolka, deklarujemy delegat ze
słowem kluczowym delegate (podobnie do deklaracji zmiennej lub pola klasy).
c) W metodzie wątku (która też jest metodą klasy tego okna) definiujemy
delegat metody, przy czym jako argument konstruktora podajemy metodę
zdefiniowaną w punkcie a).
d) Kiedy potrzebne jest odwołanie do kontrolki w wątku, używamy metody
Invoke() ze zdefiniowanym w punkcie c) delegatem jako parametrem.

Myślę, że wiele wyjaśni następny przykład.

Przykład 19.3

Popraw aplikację tak, aby wyświetlała znalezione liczby pierwsze.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu.

Cała trudność tego przykładu polega na tym, że metoda poszukiwania liczb pierwszych
uruchamiana w oddzielnym wątku będzie musiała pisać liczby do okna tekstowego
znajdującego się w wątku okna głównego.

Najpierw w klasie Form1 napisz metodę, która będzie wpisywała podaną jako argument
liczbę do pola tekstowego.
private: System::Void wyswietl(System::Int32 i) {
textBox1->AppendText(i.ToString()+System::Environment::NewLine);
}

Teraz również w klasie Form1 utwórz deklarację delegata — tak jak zwykłej metody klasy,
ale ze słowem kluczowym delegate. Delegat musi mieć listę parametrów taką jak
metoda, która będzie posługiwała się komponentem z innego wątku (czyli u nas metoda
wyswietl()).
private:delegate void wyswDelegat(System::Int32 i);

W metodzie wątku watek() trzeba zdefiniować wystąpienie delegata. Konstruktor


delegata ma dwa parametry: pierwszy to klasa, z której pochodzi metoda odwołująca się
do kontrolki, a drugi to wskaźnik do samej metody. Po stworzeniu obiektu delegata
w celu pisania do pola tekstowego wywołujemy metodę Invoke(). Pierwszy parametr tej
metody to obiekt delegata, a drugi to tabela obiektów typu obiekt zawierająca wartości
parametrów metody. W naszym przypadku metoda wyswietl() ma tylko jeden para-
metr. Oto poprawiony kod metody watek():
private: System::Void watek() {
wyswDelegat^ wyswietlDelegat =
gcnew wyswDelegat(this,&Form1::wyswietl);
System::Int32 n=2;
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 323

for (System::Int32 i=2;i<100000;i++) {


n=2;
while ((i%n))
n++;
if (i==n)
this->Invoke(wyswietlDelegat, gcnew array <System::Object^>(1){i});
}
}

Można już uruchomić program. Po naciśnięciu przycisku liczby pojawiają się w polu,
a okno daje się swobodnie przesuwać i zakrywać.

Jeśli zakończysz działanie aplikacji przyciskiem w prawym górnym rogu, zobaczysz


komunikat o błędzie:
Additional information: Cannot access a disposed object.

Pojawia się on dlatego, że zamykamy główne okno aplikacji, a wątek nadal istnieje i pró-
buje napisać coś w kontrolce TextBox, która już nie istnieje. Jak temu zaradzić, napiszę
w dalszej części rozdziału.

Zapisz program, ponieważ będzie przydatny w następnym przykładzie.

Przekazywanie parametrów
do metody wątku
Do tej pory metoda wątku była bezparametrowa, czasem jednak konieczne jest przeka-
zanie parametrów. Wtedy przy tworzeniu obiektu klasy Thread posługujemy się delegatem
ParameterizedThreadStart zamiast ThreadStart. Wartość parametru przekazujemy
w metodzie Start() przy uruchamianiu wątku.

Przykład 19.4

W aplikacji poszukującej liczb pierwszych przekazuj górną granicę poszukiwania liczb


do metody wątku za pomocą parametru.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu.

Dodaj pole tekstowe, w które będziemy wprowadzać górną granicę poszukiwań.

Metodę wątku zmodyfikuj jak niżej. Parametr przekazywany do metody musi być typu
Object, dlatego przy podstawianiu do pętli for wykonujemy konwersję.
private: System::Void watek(Object^ i_max) {
wyswDelegat^ wyswietlDelegat =
gcnew wyswDelegat(this,&Form1::wyswietl);
324 Microsoft Visual C++ 2012. Praktyczne przykłady

System::Int32 n=2;
for (System::Int32 i=2;i<Convert::ToInt32(i_max);i++) {
n=2;
while ((i%n))
n++;
if (i==n)
this->Invoke(wyswietlDelegat,gcnew array <System::Object^>(1){i});
}
}

Metoda obsługująca zdarzenie Click będzie teraz korzystała z delegata Parameterized-


ThreadStart i metody Start() z argumentem pobranym z drugiego pola tekstowego.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Thread^ watek_liczenia = gcnew Thread(gcnew
ParameterizedThreadStart(this,&Form1::watek));
watek_liczenia->Start(textBox2->Text);
}

Po uruchomieniu programu wpisz liczbę całkowitą w drugie pole tekstowe i naciśnij


przycisk. Zostaną wygenerowane tylko liczby mniejsze do zadanej. Wygląd aplikacji
przedstawia rysunek 19.1.

Rysunek 19.1.
Aplikacja wyszukująca
liczby pierwsze

Niestety, ten sposób przekazywania parametrów ma wady. Po pierwsze, można przeka-


zać tylko jeden parametr (można przekazać tablicę, ale nie zawsze jest to wygodne), a na
dodatek musi być on typu Object. Jest to bardzo pierwotny typ i akceptuje większość
typów standardowych, co powoduje, że nie ma kontroli nad przekazywanymi danymi
i łatwo popełnić błąd, który nie zostanie zauważony.

Klasa wątku — przekazywanie


parametrów z kontrolą typu
Aby pozbyć się powyższych problemów z typami danych, można zapisać metodę
wątku jako metodę oddzielnej klasy, a parametry jako zmienne tej klasy.
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 325

Przykład 19.5
Zapisz metodę wątku jako metodę klasy.

Rozwiązanie
Utwórz nowy projekt aplikacji okienkowej C++/CLI i wstaw do niego dwa pola teksto-
we oraz przycisk.

Pole tekstowe textBox1 będzie służyło do wyświetlania liczb, a textBox2 do wprowa-


dzania górnej granicy poszukiwań. Właściwość Multiline pola textBox1 ustaw na
true i powiększ je, aby mieściło kilka linii tekstu.

Nie zapomnij o załączeniu przestrzeni nazw dla wielowątkowości.


using namespace System::Threading;

Zacznij od napisania klasy wątku, którą nazwiemy SzukLiczbPierw. Oprócz metody


liczącej wątku klasa będzie zawierać konstruktor inicjalizujący zmienne klasy poda-
nymi wartościami. Wartości te nie są już typu Object, ale mają konkretne typy. Z tych
zmiennych będzie korzystać metoda wątku. Jedną ze zmiennych klasy jest uchwyt do
pola tekstowego, do którego będą wpisywane liczby. Metoda wpisująca liczby do pola
również jest częścią klasy wątku. Wywołanie tej metody poprzez metodę Invoke() po-
woduje, że jest ona wywoływana jako metoda wątku, w którym znajduje się pole tek-
stowe. Zauważ, że teraz metoda Invoke() nie jest wywoływana na oknie aplikacji, ale
na obiekcie pola tekstowego. Klasę umieść przed klasą Form1, zaraz po dyrektywach
using namespace.
public ref class SzukLiczbPierw {
private: System::Int32 i_max;
private: TextBox^ pole;
private:delegate void wyswDelegat1(System::Int32 i);
private: System::Void wyswietl1(System::Int32 i) {
pole->AppendText(i.ToString()+System::Environment::NewLine);
}
// konstruktor
public: SzukLiczbPierw (System::Int32 gora,TextBox^ ramka)
{
i_max=gora;
pole=ramka;
}
// metoda obliczeń
public: System::Void watek1() {
wyswDelegat1^ wyswietlDelegat =
gcnew wyswDelegat1(this,&SzukLiczbPierw::wyswietl1);
System::Int32 n=2;
for (System::Int32 i=2;i<Convert::ToInt32(i_max);i++) {
n=2;
while ((i%n))
n++;
if (i==n)
pole->Invoke(wyswietlDelegat,gcnew array
´<System::Object^>(1){i});
}
}
};
326 Microsoft Visual C++ 2012. Praktyczne przykłady

Teraz trzeba zaprogramować metodę dla zdarzenia Click przycisku. W tej metodzie
będzie tworzony obiekt klasy wątku, a następnie sam wątek. Parametry do metody wątku
przekazujemy poprzez konstruktor klasy wątku. Ponieważ metoda licząca jest teraz
metodą klasy SzukLiczbPierw, a nie klasy Form1, parametry delegata tej metody to nie
wskaźnik this, ale obiekt klasy SzukLiczbPierw. Drugim parametrem jest referencja do
metody obliczającej.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
SzukLiczbPierw^ obliczenia =
gcnew SzukLiczbPierw(Convert::ToInt32(textBox2->Text),textBox1);
Thread^ watek_liczenia =
gcnew Thread(gcnew ThreadStart(obliczenia,
&SzukLiczbPierw::watek1));
watek_liczenia->Start();
}

Program po uruchomieniu będzie działał identycznie jak program z poprzedniego


przykładu.

Tryb graficznego projektowania okna aplikacji wymaga, aby klasa Form1 była pierw-
szą w pliku Form1.h. Jeżeli umieścisz na początku inną klasę, stracisz możliwość
wizualnego projektowania aplikacji. Dlatego wszystkie kontrolki należy umieścić
przed napisaniem klasy wątku. Także metodę obsługującą zdarzenia trzeba utworzyć
przed umieszczeniem klasy wątku.

Kończenie pracy wątku


Przed zakończeniem działania aplikacji powinna ona zamknąć wszystkie wątki, które
do niej należą. Inaczej występuje błąd, z którym się już zetknąłeś. Może on prowadzić
do tzw. wycieków pamięci i na pewno nie zwiększa bezpieczeństwa aplikacji.

Wątek zakończy pracę w normalnym trybie, jeśli powrócimy z jego funkcji za pomo-
cą instrukcji return. Eleganckie zakończenie wątku może więc wyglądać tak:
1. Deklarujemy zmienną globalną typu bool.
2. W metodzie wątku okresowo sprawdzamy, czy ta zmienna ma wartość
np. true. Jeśli tak, to wychodzimy z wątku za pomocą return.
3. W wątku okna głównego wystarczy zmienić w dowolnym momencie wartość
tej zmiennej na true i wątek się zakończy.

Zakończenie wątku nie będzie natychmiastowe, ale upłynie pewien czas, zanim wątek
sprawdzi wartość zmiennej. Jeśli kończymy wątek przyciskiem, to nie ma problemu,
czas ten będzie niezauważalny. Gorzej, jeśli kończymy wątek z powodu tego, że zamy-
kamy okno aplikacji. Wtedy od ustawienia zmiennej kontrolnej może upłynąć za mało
czasu i wątek się nie zakończy. Jest kilka sposobów na rozwiązanie tego problemu.
Najprościej wstrzymać w jakiś sposób zamykanie aplikacji, aż wątek się zakończy.
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 327

Ja zastosuję sposób chyba najprostszy i może mało efektowny, ale skuteczny. Przy za-
kończeniu aplikacji odwrócimy uwagę użytkownika, wyświetlając okno dialogowe
i prosząc o zatwierdzenie przyciskiem OK. Zanim użytkownik zatwierdzi okno, wątek
już się zakończy.

Przykład 19.6

Biorąc za punkt wyjścia aplikację z przykładu 19.3, zadbaj o zakończenie wątku.

Rozwiązanie
Wykonaj jeszcze raz przykład 19.3, jeśli nie masz tego programu. Dodaj aplikacji drugi
przycisk Button. We właściwości Text tego przycisku wpisz Stop, będzie on zatrzymy-
wał wątek. Po dyrektywie #pragma endregion zadeklaruj zmienną sterującą.
private: bool koniec_watku;

Do metody watek w głównej jej pętli dodamy okresowe sprawdzenie wartości zmiennej.
Znajdź w metodzie poniższą linię i dopisz następną:
for (System::Int32 i=2;i<100000;i++) { // ta linia istnieje
if (koniec_watku==true) return;
}

Teraz kliknij podwójnie przycisk Stop i uzupełnij powstałą funkcję jak niżej:
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
koniec_watku=true;
}

W powyższej funkcji zmieniamy wartość zmiennej sterującej, powodując, że warunek


zakończenia wątku będzie spełniony.

Teraz to samo trzeba zrobić, kiedy okno będzie zamykane. Wykorzystamy do tego
zdarzenie FormClosing. Kliknij okno aplikacji w widoku projektowania. Rozwiń panel
Properties, przełącz się na widok zdarzeń, znajdź FormClosing i kliknij dwukrotnie.
Utworzy się funkcja obsługująca to zdarzenie. W tej funkcji umieścimy ustawienie
zmiennej koniec_watku na true, ale to nie daje pewności, że wątek zakończy się przed
zakończeniem programu. Wobec tego, aby zyskać na czasie, wyświetlimy okno Message-
Box, które już znasz. Oto cała funkcja:
private: System::Void Form1_FormClosing(System::Object^ sender, System::Windows::
´Forms::FormClosingEventArgs^ e) {
koniec_watku=true;
MessageBox::Show(L"Zatrzymuje wątki liczenia","Zaczekaj",
´MessageBoxButtons::OK);
}

Uruchom aplikację, kliknij przycisk uruchamiający wątek, a następnie, nie czekając na


koniec obliczeń, spróbuj zamknąć okno przyciskiem X w pasku.
328 Microsoft Visual C++ 2012. Praktyczne przykłady

Pojawi się okno dialogowe, a w oknie Output na dole środowiska zobaczysz taki ko-
munikat:
The thread '<No Name>' (0xa98) has exited with code 0 (0x0).

Oznacza to, że wątek zakończył się bezpiecznie. Kliknięcie OK zakończy działanie


całej aplikacji.

Semafory
Pomimo tytułu dalszy ciąg książki to nie podręcznik kolejnictwa — zostajemy przy
programowaniu. Nazwa jest jak najbardziej trafiona. Semafory to obiekty, które prze-
puszczają określoną liczbę wątków. Stan semafora opisywany jest przez liczbę. Jest to
liczba wątków, jaka może być wpuszczona. Wątek poprzez wywołanie określonej
metody zapytuje semafor, czy jest dla niego miejsce. Jeśli tak, to wartość jest obniżana
o jeden, a wątek może kontynuować działanie. Jeśli wartość wynosi zero, to wątek jest
zawieszany. Proces, który został wpuszczony, wychodząc z obszaru działania semafora,
wywołuje inną metodę, która podnosi wartość semafora. Wtedy inny czekający wątek
może zostać odblokowany, a wartość jest obniżana. W praktyce mamy klasę Semaphore
i dwie metody: WaitOne() i Relase(). Tworzymy obiekt Semaphore, następnie w metodzie
wątku wywołujemy na nim WaitOne(). Od tego momentu albo wątek jest „wpuszczany”
przez semafor i wykonuje się dalej, albo jego wykonanie jest zawieszane, aż semafor
będzie akceptował nowe wątki. Jeśli wątek chce zwolnić semafor, wywoła na nim me-
todę Relase(). Powoduje to podwyższenie wartości semafora, który może wpuścić
następny wątek. Zwolnienie semafora nie musi oznaczać końca wątku, który go
opuszcza. Może on nadal dziać, ale nie ma już związku z semaforem.

Konstruktorów samego obiektu Semaphore jest kilka. Ja użyję formy, która akceptuje
dwie liczby. Pierwsza to początkowa wartość semafora, czyli liczba wątków, które
może przepuścić zaraz po utworzeniu. Druga liczba to jego maksymalna dopuszczalna
wartość.

Przykład 19.7

Napisz program, w którym kolejne wątki uruchamia się za pomocą przycisku. Po rozpo-
częciu działania wątki będą pytać o wpuszczenie przez semafor. Niech semafor
w aplikacji przepuszcza maksymalnie trzy wątki.

Rozwiązanie
Utwórz nowy projekt aplikacji okienkowej C++/CLI według przykładu 1.4. Do okna
wstaw dwa okna TextBox i przycisk Button. Jedno pole tekstowe będzie pokazywało
komunikaty wątków poza semaforem, a drugie wątków przepuszczonych. W bloku dy-
rektyw using namespace dołącz przestrzeń nazw System::Threading.
using namespace System::Threading;
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 329

Aby wątki mogły pisać w polach tekstowych, trzeba skorzystać z delegata metody piszą-
cej do pola tekstowego, tak jak w przykładzie 19.3. Tym razem mamy dwa pola i napi-
szemy dwie metody. Wpisz je po dyrektywie #pragma endregion.
private: System::Void wyswietl1(System::String^ st)
{
textBox1->AppendText(st);
}
private: System::Void wyswietl2(System::String^ st)
{
textBox2->AppendText(st);
}

Zaraz niżej zadeklaruj delegata metody o liście parametrów takiej jak wyswietl1()
i wyswietl2().
private: delegate void wyswDelegat(System::String^ st);

Wreszcie czas na samą metodę wątku. Tak jak w przykładzie 19.3 mamy deklarację
obiektów delegata. Następnie wątek melduje się w pierwszym oknie i zapytuje
o wpuszczenie przez semafor. Jeśli zostanie wpuszczony, to symuluje swoją pracę przez
3-sekundowy „sen”, a następnie zwalnia semafor metodą Relase(). Od razu wyświetla
wartość zwróconą z metody — jest to stan semafora przed jej wywołaniem. Aktualny stan
będzie o jeden większy, bo stan to liczba wątków, które mogą być przepuszczone.
Oto cała metoda wątku:
private: System::Void watek(System::Object^ num)
{
wyswDelegat^ wyswietlDelegat1 = gcnew wyswDelegat(this,&Form1::wyswietl1);
wyswDelegat^ wyswietlDelegat2 = gcnew wyswDelegat(this,&Form1::wyswietl2);
this->Invoke(wyswietlDelegat1, safe_cast<System::Object^> (L" Wątek "+num->
ToString()+" czeka pod semaforem."+System::Environment::NewLine) );
//zapytanie o stan semafora
semafor->WaitOne();

this->Invoke(wyswietlDelegat2, safe_cast<System::Object^> (L"Wątek "+num->


ToString()+" przekroczył semafor."+System::Environment::NewLine));
Thread::Sleep(3000);
this->Invoke(wyswietlDelegat2, safe_cast<System::Object^> (L"Wątek "+num->
ToString()+" zwolnił semafor."+System::Environment::NewLine));
//zwolnienie semafora
this->Invoke(wyswietlDelegat2, safe_cast<System::Object^> (L"Aktulany stan
´semafora: "+(semafor->Release()+1).ToString()+System::
´Environment::NewLine));
}

Trzeba jeszcze zadeklarować uchwyt do obiektu semafora. Możesz to zrobić zaraz po


metodach wyswietl1() i wyswietl2().
private: Semaphore^ semafor;

Zaraz pod nim zadeklaruj zmienną, która przyda się przy numeracji wątków.
private: System::Int32 i;
330 Microsoft Visual C++ 2012. Praktyczne przykłady

Na początku wykonywania aplikacji utworzymy obiekt semafora i ustawimy wartość


zmiennej pomocniczej. Dobrze do tego celu nadaje się zdarzenie Load okna aplikacji.
Kliknij na to okno w widoku projektowania. Ważne, aby kliknąć na samo okno, a nie
na którąś z kontrolek. Utworzy się metoda obsługi zdarzenia Load. W tej metodzie wy-
wołamy konstruktor semafora. Parametry oznaczają, że na początku semafor ma war-
tość 3 i jest to zarazem maksymalna jego wartość. Zmienna i ma wartość jeden i jest
to numer pierwszego wątku.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
semafor = gcnew Semaphore(3, 3);
i=1;
}

Pozostała metoda kliknięcia przycisku. Kliknij go dwukrotnie w widoku projektowania


aplikacji. Wnętrze będzie raczej proste: tworzymy nowy wątek, uruchamiamy go, prze-
kazując jako parametr jego numer, i zwiększamy numer o jeden dla następnego wątku.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e)
{
Thread^ t = gcnew Thread(gcnew ParameterizedThreadStart(this,&Form1::watek));
t->Start(i);
i++;
}

Uruchom aplikację. Kliknij 4 razy w miarę szybko na przycisk. Chodzi o to, żeby
uruchomić cztery wątki w ciągu mniej niż trzech sekund. Wynik działania mamy na
rysunku 9.2.

Rysunek 9.2.
Działanie
aplikacji z semaforem

Wątki 1, 2 i 3 zostały wpuszczone prawie bez czekania, a wątek 4 czeka. Stan semafora
wynosi w tej chwili zero. Kiedy wątek 1 zwalnia semafor, jego stan zmienia się na 1 i zo-
staje wpuszczony wątek 4. Po pracy wszystkie wątki zwiększają stan semafora.
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 331

Sekcje krytyczne — klasa Monitor


Niektóre części kodu aplikacji nie powinny być dostępne jednocześnie dla więcej niż
jednego wątku. Można to zrealizować za pomocą semafora o początkowej wartości
równej jeden. Innym sposobem jest użycie specjalnej klasy .NET Framework o nazwie
Monitor. W prostym przypadku ważne będą dla nas dwie metody statyczne tej klasy:
Enter() i Exit(). Metoda Enter() oznacza początek sekcji krytycznej, a Exit() koniec.
Parametrem tych metod jest obiekt, który chcemy udostępnić tylko danemu wątkowi.
Załóżmy, że mamy obiekt o nazwie okno, na którym będzie operował jakiś wątek;
może to być dowolna operacja, na przykład zmiana koloru okna. W metodzie wątku
piszemy:
Monitor:: Enter(okno);

Od teraz tylko ten wątek ma dostęp do okna. Każdy inny wątek, który spróbuje się po-
wołać na obiekt okno, zostanie zawieszony. Teraz zmieniamy kolor okna:
Okno->Frenolog=Blue;

i sygnalizujemy, że już nie chcemy mieć wyłączności na dostęp:


Monitor::Exit(okno);

To cała filozofia sekcji krytycznych z klasą Monitor. Oczywiście, jak to często bywa,
problemem są szczegóły. Jeśli wątek zapętli się w sekcji krytycznej, to może nigdy
nie zawołać metody Exit() i zablokować dostęp do okna na stałe. Dlatego korzystamy
z obsługi wyjątków. Instrukcje sekcji krytycznej umieszczamy w bloku try, a metodę
Exit() umieszczamy w części finally tego bloku. Kod z bloku finally zostanie wyko-
nany zawsze, niezależnie od tego, czy blok try wyrzuci jakiś wyjątek, czy nie. Przy-
kład sekcji krytycznej będzie ostatecznie wyglądał tak:
Monitor::Enter(okno);
try { Okno->ForeColor=Blue;}
finally { Monitor::Exit(okno);}

Jako przykład pokażę różnicę w dostępie do okna bez sekcji krytycznej i z zabezpie-
czeniem sekcją.

Przykład 19.8

Niech trzy wątki starają się równocześnie pisać do kontrolki TextBox w oknie aplikacji.
Dostęp do okna w jednym wariancie będzie nielimitowany, a w drugim zabezpieczony
sekcją krytyczną.

Rozwiązanie
Utwórz nowy projekt aplikacji okienkowej według przykładu 1.4. Wstaw do okna pole
tekstowe TextBox i przycisk Button. Właściwość Multiline kontrolki TextBox ustaw na
true i zwiększ jej wymiary tak, aby mogła pomieścić kilka linii. Tak samo jak w po-
przednim przykładzie będziemy potrzebować przestrzeni nazw System::Threading.
Dołącz ją w bloku using namespace.
332 Microsoft Visual C++ 2012. Praktyczne przykłady

using namespace System::Drawing; //ta linia istnieje


using namespace System::Threading;

Aby metoda wątku mogła pisać do pola tekstowego, potrzebna jest metoda pisząca
i delegat, tak jak to robiliśmy już wielokrotnie. Zasada sekcji krytycznej będzie lepiej
widoczna, jeśli wszystkie napisy z wątków będą w jednej linii. Wprowadzimy uchwyt
do zmiennej System::String tekst. Wątki będą dopisywać wyniki swojego działania
do tej zmiennej i będzie ona wyświetlana w polu tekstowym. Poniższy kod wpisz po
#pragma endregion:
#pragma endregion //ta linia istnieje private: static System::String^ tekst;

private: System::Void wyswietl1(System::String^ st)


{
tekst=tekst+" "+st;
textBox1->AppendText(tekst+System::Environment::NewLine);
}

private: delegate void wyswDelegat(System::String^ st);

Metoda wątku będzie miała dwa warianty, które będziemy uruchamiać kolejno. W każ-
dym wariancie działanie wątku będzie polegało na wypisaniu liczb od 1 do 5, tyle że raz
będzie to realizowane bez żadnych ograniczeń, a raz z zamknięciem dostępu do okna
dla innych wątków. Kod wariantu, którego nie chcemy uruchamiać, oznaczymy jako
komentarz. Oto cała metoda wątku:
private: System::Void watek(Object^ nr)
{
wyswDelegat^ wyswietlDelegat1 = gcnew wyswDelegat(this,&Form1::wyswietl1);
//wariant 1 bez kontroli
for (System::Int32 j=1;j<5;j++)
this->Invoke(wyswietlDelegat1, safe_cast<System::Object^>
´(nr+"_"+j.ToString()+","));

//wariant 2 sekcja krytyczna


/*
Monitor::Enter(this); //blokada dostępu do całej klasy Form1
try {
for (System::Int32 j=1;j<5;j++)
this->Invoke(wyswietlDelegat1, safe_cast<System::Object^>
´(nr+"_"+j.ToString()+","));
}
finally
{Monitor::Exit(this);}
*/
}

Podstawowym działaniem jest wypisanie pięciu liczb do pola tekstowego w pętli for
zgodnie z zasadami pisania do kontrolek przez wątki, czyli z użyciem delegatów i me-
tody Invoke(). Dodatkowo wypisywany jest też numer wątku. Schemat wpisu w jednym
kroku pętli wygląda tak:
Numer Wątku_kolejna liczba
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 333

Na przykład liczba dwa wypisana przez wątek pierwszy będzie w postaci 1_2. W podanej
formie uruchomi się wariant pierwszy, bo wariant drugi jest w bloku komentarza. Użycie
klasy Monitor jest takie jak w wyjaśnieniach teoretycznych. Obiektem zamykanym do
wyłącznego dostępu w sekcji krytycznej jest główne okno aplikacji, czyli obiekt klasy
Form1, który jest dostępny we wskaźniku this, ponieważ metody wątku są metodami
tej klasy. Pozostało zaprogramowanie naciśnięcia przycisku button1. Będzie on tworzył
trzy wątki i uruchamiał je. Naciśnij dwukrotnie ten przycisk w widoku projektowania
i uzupełnij metodę button1_Click() jak niżej:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Thread^ t1 = gcnew Thread(gcnew ParameterizedThreadStart(this,&Form1::watek));
t1->Start(1);

Thread^ t2 = gcnew Thread(gcnew ParameterizedThreadStart(this,&Form1::watek));


t2->Start(2);

Thread^ t3 = gcnew Thread(gcnew ParameterizedThreadStart(this,&Form1::watek));


t3->Start(3);
}

Uruchom aplikację i naciśnij przycisk w oknie. W polu tekstowym zobaczysz napisy


jak na rysunku 19.3.

Rysunek 19.3.
Wynik
działania aplikacji
bez sekcji krytycznej

W ostatniej linii masz wszystkie napisy wygenerowane przez wątki. Prawdopodobnie


(nie jest to w 100% pewne, bo zależy od szybkości wykonywania niezależnych wątków)
będziesz miał tam liczbę jeden wpisaną przez wątek pierwszy, drugi i trzeci, następ-
nie liczbę dwa wpisaną przez kolejne wątki i tak dalej. Na rysunku 19.3 właśnie tak
jest. Teraz uruchomimy wariant drugi. Zdejmij znaki komentarzy /* i */ z tego warian-
tu, a wariant pierwszy zasłoń znakiem komentarza.
private: System::Void watek(Object^ nr)
{
wyswDelegat^ wyswietlDelegat1 = gcnew wyswDelegat(this,&Form1::wyswietl1);
//wariant 1 bez kontroli
//for (System::Int32 j=1;j<5;j++)
//this->Invoke(wyswietlDelegat1, safe_cast<System::Object^>
//(nr+"_"+j.ToString()+","));

//wariant 2 sekcja krytyczna


Monitor::Enter(this); //blokada dostepu do calej klasy Form1
334 Microsoft Visual C++ 2012. Praktyczne przykłady

try {
for (System::Int32 j=1;j<5;j++)
this->Invoke(wyswietlDelegat1, safe_cast<System::Object^>
´(nr+"_"+j.ToString()+","));
}
finally
{Monitor::Exit(this);}
}

Uruchom aplikację. Teraz napisy są jak na rysunku 19.4.

Rysunek 19.4.
Aplikacja z wątkami
w sekcji krytycznej

W ostatniej linii widać (teraz mamy pewność, że tak jest), że najpierw swoje liczby
wpisał wątek pierwszy, następnie drugi i trzeci. Podczas kiedy jeden wątek pisał, inne
były zawieszone. Możliwa jest zmiana kolejności wykonywania wątków. Zależy to od
wielu czynników, ale kiedy wątek zostanie dopuszczony do sekcji krytycznej, będzie
mógł spokojnie działać, mając wyłączny dostęp do okna.

Komponent BackgroundWorker
Komponent BackgroundWorker jest kolejną możliwością implementacji wielowątko-
wości. Za jego pomocą można uruchamiać metody w wątkach i kontrolować ich wy-
konanie. Jego funkcjonowanie opiera się na zdarzeniach. Tabela 19.1 przedstawia trzy
ważne zdarzenia związane z tym komponentem.

Tabela 19.1. Zdarzenia komponentu BackgroundWorker


Zdarzenie Opis
DoWork Zdarzenie generowane przy rozpoczęciu działania wątku. Metoda obsługi
tego zdarzenia uruchamia metodę wątku.
ProgressChanged Zdarzenie występujące w trakcie działania wątku, po wywołaniu metody
ReportProgress(). Może być użyte do pokazywania postępu wykonania
wątku lub do innych celów wymagających komunikacji z komponentami
okna głównego.
RunWorkerCompleted Zdarzenie to występuje po zakończeniu pracy przez metodę wątku.
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 335

Najczęściej używane metody tego komponentu przedstawia tabela 19.2.

Tabela 19.2. Niektóre metody komponentu BackgroundWorker


Metoda Działanie
RunWorkerAsync() Generuje zdarzenie DoWork, które uruchamia proces w tle. Wersja
RunWorkerAsync z parametrem przekazuje parametr do metody wątku.
(Object^ parametr)
ReportProgress(int postęp) Generuje zdarzenie ProgressChanged. Przekazuje do niego liczbę
typu int, która może wyrażać stopień zaawansowania wątku.
CancelAsync() Metoda do przerywania działania wątku. Nie przerywa ona jego
działania natychmiast, a jedynie ustawia właściwość
CancellationPending na true. Metoda wątku powinna okresowo
sprawdzać tę właściwość i przerwać wykonywanie wątku.

Obiekt BackgroundWorker posiada także właściwości kontrolujące jego zachowanie.


Prezentuje je tabela 19.3.

Tabela 19.3.Właściwości komponentu BackgroundWorker


Właściwość Opis
WorkerReportsProgress Wartość true powoduje, że można korzystać z metody
ReportProgress().
WorkerSupportsCanceletion Umożliwia działanie mechanizmu przerywania wątku.
CancellationPending Wartość true oznacza, że wywołano metodę CancelAsync().

Napiszemy teraz program obliczający w wątku średnią z liczb podanych w tabeli.

Przykład 19.9

Napisz program obliczający średnią z liczb podanych w tabeli. Użyj komponentu


BackgroundWorker. Tabela powinna być przekazywana do metody wątku jako parametr.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do niego przycisk Button,
etykietę Label i komponent BackgroundWorker. Ten ostatni znajdziesz w dziale Compo-
nents okna narzędziowego.

Zacznij od oprogramowania zdarzenia Click przycisku Button. W metodzie zdefiniuj


tablicę liczb do określenia średniej i wywołaj metodę RunWorkerAsync(), przekazując
tę tablicę jako parametr.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
array<System::Single>^ tablica =
gcnew array<System::Single> (5) {2,2,3,4,5};
backgroundWorker1->RunWorkerAsync(tablica);
}
336 Microsoft Visual C++ 2012. Praktyczne przykłady

Metoda RunWorkerAsync() wywoła zdarzenie DoWork i przekaże parametr do metody


obsługującej to zdarzenie. Aby utworzyć metodę obsługi zdarzenia, zaznacz myszką
komponent BackgroundWorker na pasku niewidocznych komponentów, a następnie przejdź
w prawym panelu do widoku zdarzeń i kliknij dwukrotnie zdarzenie DoWork. Parametr
sender metody obsługi zawiera uchwyt do obiektu BackgroundWorker, który wywołał
zdarzenie DoWork. W metodzie obsługi należy rzutować w górę parametr sender do obiektu
BackgroundWorker, aby zapisać ten uchwyt. Następnie wywołujemy metodę wątku. Para-
metrem metody wątku jest między innymi uchwyt do obiektu BackgroundWorker po-
brany z parametru sender. Wynik działania metody podstawiamy pod pole Result
drugiego z parametrów metody obsługi zdarzenia DoWork.
private: System::Void backgroundWorker1_DoWork(System::Object^ sender,
System::ComponentModel::DoWorkEventArgs^ e) {
BackgroundWorker^ back_worker = dynamic_cast<BackgroundWorker^>(sender);
e->Result=watek( safe_cast<array <System::Single>^>(e->Argument),
back_worker, e );
}

Wreszcie sama metoda wątku:


private: System::Single watek(array<System::Single>^ n, BackgroundWorker^ worker,
DoWorkEventArgs ^ e) {
System::Single suma;
for (System::Int16 i=0;i<n->Length;i++)
suma+=n[i];
return suma/n->Length;
}

Po zakończeniu jej wykonywania wystąpi zdarzenie RunWorkerCompleted. Do jego ob-


sługi będziesz potrzebować metody, która wyświetli wartość zwróconą przez metodę
wątku. Wartość ta została przekazana przez metodę obsługi zdarzenia DoWork do zmien-
nej e, z niej będziemy ją odczytywać. Kliknij pojedynczo na komponent Background-
Worker w pasku niewidocznych komponentów, a następnie przełącz na widok zdarzeń
i znajdź zdarzenie RunWorkerCompleted. Kliknij po prawej stronie tego zdarzenia, two-
rząc metodę obsługi, w którą wpisz kod jak niżej:
private: System::Void backgroundWorker1_RunWorkerCompleted(System::Object^ sender,
´System::ComponentModel::RunWorkerCompletedEventArgs^ e) {
label1->Text=e->Result->ToString();
}

Po uruchomieniu programu otrzymamy średnią z liczb z tablicy wypisaną w etykie-


cie. Obliczanie średniej zajmuje mało czasu, dlatego nie widać tu zalet programowa-
nia wielowątkowego, ale przy długich procesach jest ono koniecznością.

W celu uzyskania większej kontroli nad procesem wątku niezbędna jest możliwość
przerwania tego procesu, a także kontroli postępów jego wykonania. Komponent Back-
groundWorker posiada mechanizmy, które to umożliwiają.
Rozdział 19. ♦ Podstawy aplikacji wielowątkowych 337

Przykład 19.10

Za pomocą komponentu BackgroundWorker napisz aplikację wyszukującą liczbę pierwszą


najbardziej zbliżoną do zadanej. Program ma mieć możliwość przerwania obliczeń
w dowolnym momencie, a także powinien informować o zaawansowaniu procesu.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4. Wstaw do okna dwa przyciski
Button, etykietę Label, komponent BackgroundWorker, wskaźnik postępu ProgressBar
(dział Common Controls na pasku narzędziowym) oraz pole tekstowe TextBox.

We właściwość Text przycisku button1 wpisz Licz, a button2 — Anuluj.

Ponieważ będziemy używać mechanizmów raportowania zaawansowania procesu i jego


przerwania, za pomocą panelu Properties ustaw właściwości WorkerReportsProgress
i WorkerSupportsCancellation obiektu backgroundWorker1 na true.

Sam program będzie wyglądał podobnie jak poprzednio, z tym że znajdą się tu nowe
elementy. Zacznij od metody zdarzenia Click pierwszego przycisku button1.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
backgroundWorker1->RunWorkerAsync(Convert::ToInt32(textBox1->Text));
}

Jako parametr do metody wątku przekazujemy liczbę pobraną z pola tekstowego. Jest
to liczba, w okolicy której poszukujemy liczby pierwszej.

Metoda RunWorkerAsync() wywołuje zdarzenie DoWork, które będziemy obsługiwać za


pomocą poniższej metody:
private: System::Void backgroundWorker1_DoWork(System::Object^ sender,
´System::ComponentModel::DoWorkEventArgs^ e) {
BackgroundWorker^ back_worker = dynamic_cast<BackgroundWorker^>(sender);
e->Result = watek( safe_cast<System::Int32>(e->Argument), back_worker, e );
}

Metoda różni się od poprzedniego przykładu jedynie typem argumentu (teraz jest to
zmienna typu System::Int32, a nie tablica typu System::Single, jak poprzednio). Teraz
napisz samą metodę wątku:
private: System::Single watek(System::Int32 i_max, BackgroundWorker^ worker,
´DoWorkEventArgs ^ e) {
System::Single liczba;
System::Int32 n=2;
System::Int32 procent;
for (System::Int32 i=2;i<i_max;i++) {
if (worker->CancellationPending==true) {
e->Cancel=true;return liczba;
}
else {
n=2;
while ((i%n))
n++;
338 Microsoft Visual C++ 2012. Praktyczne przykłady

if (i==n)
liczba=i; // ostatnia znaleziona
}
procent=(int)((float)i/(float)i_max*100);
worker->ReportProgress(procent);
}
return liczba;
}

Tu mamy największe zmiany w stosunku do poprzedniego przykładu. W każdym


kroku pętli jest sprawdzana właściwość CancellationPending. Zmienia ona wartość na
true w przypadku wywołania metody CancelAsync(); jest to znak, że użytkownik chce
przerwać działanie wątku. W takim przypadku poprzez parametr e informacja ta zostaje
przekazana dalej, a instrukcja return powoduje opuszczenie metody wątku. Również
w każdym kroku jest obliczane zaawansowanie procesu na podstawie aktualnej wartości
zmiennej sterującej pętli. Wartość zaawansowania jest przekazywana poprzez wywoła-
nie metody ReportProgress(), która wywołuje zdarzenie ProgressChanged. Zauważ,
że cała komunikacja między metodami odbywa się poprzez parametry metod.

Utwórz metodę obsługującą zdarzenie ProgressChanged, tak jak to robiłeś dla zdarzenia
DoWork, a następnie doprowadź ją do postaci jak niżej:
private: System::Void backgroundWorker1_ProgressChanged(System::Object^ sender,
´System::ComponentModel::ProgressChangedEventArgs^ e) {
progressBar1->Value=e->ProgressPercentage;
}

Metoda CancelAsync() jest wywoływana przez naciśnięcie drugiego przycisku.


private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
backgroundWorker1->CancelAsync();
}

Po zakończeniu działania wątku nastąpi zdarzenie RunWorkerCompleted; w metodzie jego


obsługi wypiszemy wynik lub informację, że proces został przerwany.
private: System::Void backgroundWorker1_RunWorkerCompleted(System::Object^ sender,
´System::ComponentModel::RunWorkerCompletedEventArgs^ e) {
if (e->Cancelled==true)
label1->Text="Anulowano";
else
label1->Text=e->Result->ToString();
}

Po uruchomieniu programu i wpisaniu liczby w pole tekstowe otrzymamy liczbę pierwszą


poprzedzającą wpisaną liczbę. O przebiegu poszukiwań informuje pasek, a przycisk
Anuluj pozwala anulować obliczenia.
Rozdział 20.
Połączenie aplikacji
z siecią Internet

Komponent WebBrowser
Czasami istnieje potrzeba wyświetlania w oknie aplikacji danych pobranych bezpośred-
nio ze stron WWW. W .NET Framework mamy komponent, który jest właściwie kom-
pletną przeglądarką stron opartą na Internet Explorerze.

Za pomocą tego komponentu w prosty sposób można wyświetlać zawartość całych stron
WWW w oknie aplikacji. Może być on użyty nie tylko do przeglądania stron w sieci,
ale także do wyświetlania dokumentów HTML z komputera lokalnego (na przykład
plików pomocy aplikacji). Podstawowe właściwości komponentu WebBrowser przedsta-
wia tabela 20.1.

Tabela 20.1. Wybrane właściwości kontrolki WebBrowser


Właściwość Znaczenie
AllowNavigation Właściwość umożliwiająca zablokowanie przeglądarki tak, że nie można przejść
na inne strony niż aktualna. Wartość false oznacza zablokowanie.
Url Adres strony do wyświetlenia w przeglądarce. Ta właściwość jest typu Uri^.
CanGoBack Wartość true oznacza, że odwiedzana strona nie jest pierwszą (istnieje historia).
CanGoForward Wartość true oznacza, że użytkownik cofał się w historii odwiedzanych stron
i wyświetlana strona nie jest ostatnią odwiedzoną.
Document Właściwość typu HtmlDocument zawierająca aktualnie wyświetlaną w kontrolce
stronę. Może być użyta do pobrania danych ze strony.
DocumentText Zawiera źródło HTML strony aktualnie wyświetlonej w przeglądarce.
DocumentTitle Tytuł aktualnie wyświetlanej strony.

Najprostszy sposób wyświetlenia strony WWW pokazuje przykład.


340 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 20.1

Po naciśnięciu przycisku wyświetl w oknie aplikacji stronę helion.pl.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do okna kontrolkę Web-
Browser (zakładka okna narzędziowego, ostatnia kontrolka w dziale Common Controls)
oraz przycisk Button.

Powiększ rozmiary okna aplikacji i kontrolki WebBrowser tak, aby zwiększyć komfort
oglądania stron.

Do zdarzenia Click przycisku przypisz następującą metodę:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Uri^ adres= gcnew Uri("http://helion.pl");
webBrowser1->Url=adres;
}

Uruchom aplikację. Po naciśnięciu przycisku w oknie kontrolki pojawi się odpowied-


nia strona WWW (rysunek 20.1).

Rysunek 20.1.
Wyświetlanie stron
WWW w komponencie
WebBrowser

Adres przekazywany do właściwości Url należy zawsze poprzedzać prefiksem. W przy-


padku stron WWW jest to http://.

W podobny sposób można wyświetlić plik lokalny.

Przykład 20.2

Utwórz w katalogu na dysku C: folder Aplikacja, a następnie w tym folderze plik


pomoc.html o dowolnej treści.

Wyświetl w oknie kontrolki WebBrowser plik pomoc.html znajdujący się w folderze


C:\Aplikacja lub inny plik HTML.
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 341

Rozwiązanie
Zbuduj program identyczny jak w poprzednim przykładzie, zmień jedynie adres do-
kumentu.
Uri^ adres= gcnew Uri("c:\\aplikacja\\pomoc.html");

Program będzie teraz wyświetlał plik lokalny.

Klasa kontrolki WebBrowser posiada też wiele metod, które umożliwiają nawigację po
stronach WWW. Przedstawia je tabela 20.2.

Tabela 20.2. Wybrane metody obiektu WebBrowser


Metoda Działanie
GoBack() Przenosi użytkownika do poprzedniej strony w historii. Działa tylko,
jeżeli właściwość CanGoBack==true.
GoForward() Przenosi do następnej strony w historii, jeżeli użytkownik cofał się
wcześniej.
GoHome() Przenosi do strony domowej. Strona domowa jest tą samą, jaka została
określona w Internet Explorerze.
GoSearch() Przenosi do strony domyślnej wyszukiwarki. Również lokalizacja tej
strony jest pobierana z Internet Explorera.
Navigate Wyświetla w kontrolce stronę o adresie adres.
(System::String adres)
Navigate(Uri adres)
Stop() Zatrzymuje wczytywanie aktualnej strony.

Bez problemu można dodać możliwość przechodzenia do stron wcześniej odwiedzo-


nych, tak jak w przeglądarce. Chociaż pisanie kolejnej przeglądarki internetowej mija
się właściwie z celem, to nawigację można wykorzystać do opracowania na przykład
plików pomocy czy prezentacji, którą będzie można oglądać wewnątrz aplikacji.

Przykład 20.3

Napisz przeglądarkę stron WWW z możliwością poruszania się po historii odwiedza-


nych stron.

Rozwiązanie
Utwórz aplikację według przykładu 1.4 i dodaj do jej okna komponent WebBrowser,
dwa przyciski i pole tekstowe. We właściwość Text pierwszego przycisku wpisz Wstecz,
a drugiego — Naprzód.

W pole tekstowe będziemy wpisywać stronę do odwiedzenia; jej wczytanie powinno


nastąpić, kiedy użytkownik naciśnie klawisz Enter. Aby tak się stało, trzeba obsłużyć
zdarzenie KeyDown dla pola tekstowego. Zaznacz pole tekstowe myszą w widoku pro-
jektowania aplikacji, odnajdź to zdarzenie w panelu zdarzeń, a następnie kliknij je
dwukrotnie. Utworzy się metoda obsługi tego zdarzenia. Jednym z parametrów tej
342 Microsoft Visual C++ 2012. Praktyczne przykłady

metody będzie rodzaj naciśniętego klawisza. Oto kod tej metody, w którym przejście
do strony następuje przy wykryciu naciśnięcia klawisza Enter:
private: System::Void textBox1_KeyDown(System::Object^ sender,
System::Windows::Forms::KeyEventArgs^ e) {
if (e->KeyData==System::Windows::Forms::Keys::Enter)
webBrowser1->Navigate("http://"+textBox1->Text);
}

Teraz wystarczy już tylko zaprogramować metody przycisków, odpowiednio, cofają-


ce lub przenoszące do przodu w historii.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
webBrowser1->GoBack();
}
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
webBrowser1->GoForward();
}

Po wypróbowaniu zapisz projekt na dysku, ponieważ będziemy jeszcze z niego korzystać.

Przetwarzanie stron Web


— obiekt HtmlDocument
Do lepszego zrozumienia tej części rozdziału konieczna jest znajomość podstaw
składni języka HTML. Celem użycia kontrolki WebBrowser nie będzie raczej napisanie
kolejnej przeglądarki Web, bo takich jest już dużo. Zamiast tego za pomocą tej kontrolki
można wykonywać operacje na dokumentach pisanych w języku HTML. Środowisko
.NET Framework zawiera klasę HtmlDocument, która reprezentuje dokument tego typu. Za
pomocą tego obiektu uzyskujemy dostęp do poszczególnych części składowych pliku
HTML. Te części składowe są zawarte w obiektach HtmlElement. Obiekt typu HtmlDocu-
ment grupuje więc kilka obiektów HtmlElement.

Właściwości obiektu HtmlDocument przedstawia tabela 20.3.


Tabela 20.3. Niektóre właściwości obiektu HtmlDocument
Właściwość Znaczenie
All Tabela obiektów HtmlElement zawierająca wszystkie części składowe dokumentu.
Body Element zawierający część dokumentu po znaczniku BODY.
Cookie Zawiera wszystkie znaczniki kontekstu (ang. cookies) powiązane z danym dokumentem.
Encoding Kodowanie używane przez aktualny dokument.
Forms Tabela obiektów HtmlElement zawierająca wszystkie części po znacznikach FORM.
Images Obiekty HtmlElement reprezentujące części dokumentu po znacznikach IMG (obrazy).
Links Zbiór odnośników do innych stron zawartych w aktualnym dokumencie.
Title Tytuł dokumentu.
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 343

Obiekt HtmlElement posiada właściwości ogólne odnoszące się do wszystkich rodza-


jów sekcji kodu HTML. Najbardziej interesujące są zwykle właściwości szczególne,
odnoszące się do konkretnych części kodu, na przykład znacznik SRC w kodzie wsta-
wiania obrazka oznacza ścieżkę do pliku graficznego. Dostęp do tych szczególnych
właściwości uzyskujemy za pomocą metod GetAttribute() lub SetAttribute(). Ar-
gumentami tych metod są znaczniki w kodzie, do którego chcemy uzyskać dostęp (na
przykład SRC dla odczytania ścieżki dostępu do obrazka).

Przykład 20.4

Wypisz w polu tekstowym ścieżki do wszystkich plików graficznych na stronie


WWW wyświetlonej w kontrolce WebBrowser.

Rozwiązanie
Otwórz projekt z przykładu 20.3.

Zmniejsz trochę obszar kontrolki WebBrowser i dodaj do okna aplikacji kolejny przy-
cisk Button oraz pole tekstowe TextBox; całość niech wygląda jak na rysunku 20.2.

Rysunek 20.2.
Aplikacja pokazująca
obiekty graficzne
na stronie

Po naciśnięciu trzeciego przycisku w polu tekstowym powinny się pojawić odnośniki


do wszystkich obrazków zawartych na wyświetlanej stronie Web. Aby to zrobić, skorzy-
stamy z właściwości Image obiektu HtmlDocument. Właściwość Image jest typu tablico-
wego, do odczytu jej elementów użyjemy enumeratora. Do zdarzenia Click przycisku
przypisz metodę:
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
System::Collections::IEnumerator^ element=webBrowser1->Document->
´Images->GetEnumerator();
textBox2->Clear();
while (element->MoveNext()) {
textBox2->AppendText((safe_cast<HtmlElement^>(element->Current))->
´GetAttribute("SRC")->ToString());
344 Microsoft Visual C++ 2012. Praktyczne przykłady

textBox2->AppendText(System::Environment::NewLine);
}
}

Działanie programu dla strony helion.pl pokazuje rysunek 20.2.

W łatwy sposób można też napisać program, który będzie sprawdzał, czy dana strona
WWW posługuje się jakimiś znacznikami kontekstu. Wykorzystamy do tego odpowiednią
właściwość obiektu HtmlDocument.

Przykład 20.5

Wyposaż przeglądarkę w możliwość podglądu znaczników kontekstu na danej stronie.

Rozwiązanie
Utwórz aplikację identyczną jak w przykładzie 20.4.

Tym razem metoda wywoływana przy naciśnięciu trzeciego przycisku jest następująca:
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ cookie;
cookie=webBrowser1->Document->Cookie;
textBox2->Clear();
if (cookie!=nullptr)
textBox2->AppendText(cookie);
else
textBox2->AppendText("Nie znaleziono znaczników kontekstu!");
}

Przykład 20.6

Po naciśnięciu przycisku wyświetl w polu tekstowym wszystkie odnośniki znajdujące się


na danej stronie.

Rozwiązanie
Utwórz aplikację jak w przykładzie 20.4.

Po naciśnięciu trzeciego przycisku odczytamy zawartość właściwości Links dla danej strony.
Podobnie jak to było we właściwości Image, jest to tablica obiektów HtmlElement, którą
będziemy czytać za pomocą enumeratora. Właściwość InnerText obiektu HtmlElement
pozwala na odczytanie tekstu związanego z odnośnikiem. Oto odpowiednia metoda
trzeciego przycisku:
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
System::Collections::IEnumerator^ odnosnik=webBrowser1->Document->
´Links->GetEnumerator();
textBox2->Clear();
while (odnosnik->MoveNext()) {
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 345

if (safe_cast<HtmlElement^>(odnosnik->Current)->InnerText)
´textBox2->AppendText((safe_cast<HtmlElement^>(odnosnik->
´Current))->InnerText->ToString());
else textBox2->AppendText(L"<Brak tekstu opisu odnośnika>");
textBox2->AppendText(System::Environment::NewLine);
textBox2->AppendText((safe_cast<HtmlElement^>(odnosnik->Current))->
´GetAttribute("href")->ToString());
textBox2->AppendText(System::Environment::NewLine);
}
}

Rysunek 20.3 pokazuje aplikację w działaniu.

Rysunek 20.3.
Wyświetlanie
odnośników
ze strony Web

Uruchamianie skryptów JavaScript


z poziomu aplikacji
Obiekt HtmlDocument umożliwia uruchamianie skryptów JavaScript znajdujących się
na stronie internetowej otwartej w komponencie WebBrowser. Właściwie nie całych
skryptów, a konkretnych funkcji z nich. Najciekawszą sprawą jest to, że wyniki działania
takiego skryptu mogą być bezpośrednio przechwycone do aplikacji. Uruchamiamy go,
wywołując na obiekcie HtmlDocument metodę InvokeScript(). Parametrem tej metody
jest nazwa funkcji w skrypcie, którą chcemy uruchomić. InvokeScript() zwraca to, co
zwraca funkcja skryptu. Skrypty mogą zwracać zmienne czy obiekty różnych typów.
Dlatego parametr zwracany przez InvokeScript() jest ogólnego typu Object. Zadaniem
tej metody jest tylko przeniesienie wartości ze skryptu. Programista powinien wiedzieć,
jaki typ ma ta wartość i jak ją wykorzystać w aplikacji. Pokażę to na przykładzie pro-
stego skryptu zwracającego nazwę przeglądarki, w której go otwarto.
346 Microsoft Visual C++ 2012. Praktyczne przykłady

Przykład 20.7

Uruchom w aplikacji C++/CLI skrypt ze strony inernetowej.

Rozwiązanie
Zanim zaczniemy pisać aplikację, potrzebujemy strony internetowej ze skryptem Java-
Script zawierającym przynajmniej jedną funkcję. Przygotowałem krótki skrypt wy-
świetlający nazwę przeglądarki:
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pl" lang="pl">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-2" />
<title>Tu nagłówek strony</title>
<script type="text/javascript">
function rozpoznaj() {
document.write("Stronę otwarto w przeglądarce: ")
document.write(navigator.appName+" ")
document.write(navigator.appVersion+"<br\>")

return navigator.appName
}
</script>
</head>

<body>
<script type="text/javascript">
rozpoznaj()
</script>

</body>
</html>

Nazwa przeglądarki jest zwracana przez funkcję rozpoznaj(). Zapisz ten plik pod na-
zwą skrypt1.htm w jakimś folderze. Ja przyjmę, że jest to folder d:\przykład. Przecho-
dzimy do VC++. Utwórz nowy projekt aplikacji okienkowej C++/CLI. Wstaw do niego
komponent WebBrowser, etykietę Label i przycisk Button.

Naszą „stronę WWW” z pliku skrypt1.htm będziemy wczytywać do kontrolki webBrow-


ser1 zaraz po utworzeniu okna. Kliknij podwójnie gdziekolwiek na obszar okna poza
kontrolkami, aby utworzyć metodę uruchamianą zdarzeniem Load, czyli przy tworzeniu
okna. W tej metodzie wpiszemy kod wczytujący plik skrypt1.htm. To już ćwiczyliśmy,
więc nie wymaga komentarza.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
Uri^ adres= gcnew Uri("D:\\aplikacja\\skrypt1.htm");
webBrowser1->Url=adres;
}
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 347

Uruchomienie funkcji rozpoznaj() ze skryptu nastąpi po naciśnięciu przycisku. Kliknij


go podwójnie w widoku projektowania okna aplikacji, aby utworzyć metodę obsługi
kliknięcia. Wiemy, że funkcja skryptu zwróci łańcuch znakowy z nazwą przeglądarki.
Zadeklarujemy więc uchwyt do typu System::String o nazwie jakaprzeg. Następnie
uruchomimy metodę InvokeScript() na obiekcie HtmlDocument ukrytym we właściwości
Document kontrolki. Wartość zwracaną przez tę metodę podstawimy do uchwytu jaka-
przeg. Wreszcie wyświetlimy zawartość uchwytu w etykiecie label1. Oto cała meto-
da wyzwalana przy naciśnięciu przycisku:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ jakaprzeg;
jakaprzeg=(webBrowser1->Document->InvokeScript("rozpoznaj"))->ToString();
label1->Text=jakaprzeg;
}

Uruchom aplikację. Po naciśnięciu przycisku w kontrolce label1 ukaże się napis Micro-
soft Internet Explorer — na tej przeglądarce zbudowany jest komponent WebBrowser.

Protokół FTP
Protokół FTP służy do przesyłania plików przez Internet. Można go użyć we wnętrzu
aplikacji, na przykład do automatycznego pobrania uaktualnienia lub potrzebnych plików
z danymi.

Implementacja FTP w .NET Framework jest na poziomie, który nazwałbym „półniskim”,


co oznacza, że nie trzeba mieć wiedzy o FTP, aby się nim posługiwać, ale nie jest to
też kwestia użycia jednej metody pobierającej lub wysyłającej pliki. Połączenia FTP
umożliwiają obiekty dwóch klas: FtpWebRequest i FtpWebResponse. Pierwszy z nich re-
prezentuje zapytanie do serwera FTP, a drugi odpowiedź serwera. Do poprawnej pracy
będą potrzebne dwie właściwości obiektu FtpWebRequest, które przedstawia tabela 20.4.

Tabela 20.4. Wybrane właściwości obiektu FtpWebRequest


Właściwość Znaczenie
Credentials Zawiera login i hasło stosowane przy logowaniu do serwera FTP.
Method Określa rodzaj operacji do wykonania na serwerze. Możliwe wartości to:
 WebRequestMethods::Ftp::DownloadFile — pobranie pliku,
 WebRequestMethods::Ftp::UploadFile — wysłanie pliku,
 WebRequestMethods::Ftp::ListDirectoryDetails — pobranie szczegółowych
informacji o plikach znajdujących się na serwerze.

Oprócz tych właściwości będziemy używać dwóch metod opisanych w tabeli 20.5.
348 Microsoft Visual C++ 2012. Praktyczne przykłady

Tabela 20.5. Metody obiektu FtpWebRequest do pobierania lub wysyłania danych


Metoda Działanie
GetResponse() Zwraca obiekt typu FtpWebResponse, z którego możemy czytać dane
wysyłane przez serwer.
GetRequestStream() Zwraca strumień, do którego można pisać dane, jakie mają być wysłane na
serwer FTP.

Ogólnie zaprogramowanie pobierania danych z FTP wymaga następujących czynności:


a) Utworzenie obiektu FtpWebRequest. Parametrem dla konstruktora obiektu jest
adres serwera; w przypadku pobrania lub wysłania pliku parametrem jest
pełna ścieżka wraz z nazwą pliku.
b) Zapisanie we właściwości Credentials loginu i hasła do serwera.
c) Wybranie czynności (wysłanie bądź pobranie pliku, pobranie zawartości
katalogu) i odpowiednie ustawienie właściwości Method.
d) Wysłanie zapytania do serwera i pobranie odpowiedzi (czyli obiektu
FtpWebResponse) za pomocą metody GetResponse().
e) Pobranie strumienia odpowiedzi z obiektu FtpWebResponse i pobieranie
z niego danych (zawartości pliku lub katalogu).

W przypadku wysłania pliku na serwer czynności od a) do c) są takie same, a dalej


mamy:
f) Otwarcie strumienia do pisania na serwer metodą GetRequestStream() obiektu
FtpWebRequest.
g) Zapisanie danych (zawartości pliku lokalnego) do tego strumienia.

Pobieranie zawartości katalogu


z serwera FTP
Teraz zobaczmy, jak praca z FTP wygląda w praktyce.

Przykład 20.8
Pobierz zawartość podanego katalogu z podanego serwera FTP.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, do okna wstaw dwa pola tekstowe
TextBox oraz przycisk Button.

Aby program działał, dodaj do niego możliwość korzystania z przestrzeni nazw System::
´Net i System::IO, które zawierają potrzebne klasy.
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 349

using namespace System::Net;


using namespace System::IO;

Ustaw właściwość Multiline pola tekstowego textBox2 na true i powiększ je tak, aby
mogło wyświetlić kilka linii tekstu.

Metodę obsługującą zdarzenie Click napisz jak niżej:


Uri^ adres = gcnew Uri("ftp://"+textBox1->Text);
FtpWebRequest^ req = dynamic_cast<FtpWebRequest^>(WebRequest::Create(adres));
req->Credentials=gcnew NetworkCredential("anonymous","moja_nazwa@moj_adres.pl");
req->Method=WebRequestMethods::Ftp::ListDirectoryDetails;
FtpWebResponse^ resp;
try {
resp=dynamic_cast<FtpWebResponse^>(req->GetResponse());
}
catch (WebException^ ex)
{
MessageBox::Show(ex->Message);
return;
}
Stream^ resp_stream = resp->GetResponseStream();
StreamReader^ reader = gcnew StreamReader(resp_stream);
String^ linia;
textBox2->Clear();
while (!reader->EndOfStream) {
linia=reader->ReadLine();
textBox2->AppendText(linia+System::Environment::NewLine);
}

Zauważ, że funkcja GetResponse(), która łączy się z serwerem, jest w bloku try i jeste-
śmy przygotowani na przechwycenie wyjątku WebException w razie błędu połączenia.
Po uruchomieniu programu wpisz adres dowolnego publicznego serwera FTP i naciśnij
przycisk. W polu tekstowym otrzymasz zawartość głównego katalogu. Działanie aplikacji
przedstawia rysunek 20.4. Zapisz aplikację na dysku.

Rysunek 20.4.
Aplikacja pobierająca
zawartość folderu
z serwera FTP
350 Microsoft Visual C++ 2012. Praktyczne przykłady

Pobieranie plików przez FTP


Po znalezieniu potrzebnego pliku można go pobrać na dysk. Rozszerzymy aplikację o taką
możliwość.

Przykład 20.9

Dodaj do aplikacji z przykładu 20.8 możliwość pobrania pliku.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu i umieść trzeci przycisk Button oraz jeszcze
jedno pole tekstowe do wpisywania nazwy pliku do pobrania.

Metoda zdarzenia Click dla drugiego przycisku będzie miała postać:


private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
Uri^ adres = gcnew Uri("ftp://"+textBox1->Text+"/"+textBox3->Text);
FtpWebRequest^ req = dynamic_cast<FtpWebRequest^>(WebRequest::Create(adres));
req->Credentials=gcnew NetworkCredential
´("anonymous","moja_nazwa@moj_adres.pl");
req->Method=WebRequestMethods::Ftp::DownloadFile;
FtpWebResponse^ resp=dynamic_cast<FtpWebResponse^>(req->GetResponse());
Stream^ resp_stream = resp->GetResponseStream();
FileStream^ stru_plik = gcnew FileStream("./"+textBox3->Text,
´FileMode::Create);
// czytaj plik z serwera i zapisuj do strumienia
int ile_bajtow;
array<Byte> ^ bufor = gcnew array<Byte>(1024);
do {
ile_bajtow=resp_stream->Read(bufor,0,bufor->Length);
stru_plik->Write(bufor,0,ile_bajtow);
} while(ile_bajtow!=0);
stru_plik->Flush();
stru_plik->Close();
resp_stream->Close();
resp->Close();
}

Powyższa metoda działa przy założeniu, że pole tekstowe textBox1 służy do wpisywania
adresu FTP, textBox2 do wyświetlania zawartości katalogu, a textBox3 do wpisywania
nazwy pliku do pobrania.

Po uruchomieniu aplikacji najpierw należy wpisać adres serwera wraz z folderem, w któ-
rym znajduje się plik, a następnie nacisnąć przycisk Katalog. Po wyświetleniu listy pli-
ków sprawdź, czy plik znajduje się na tej liście, a następnie wpisz jego pełną nazwę
w pole tekstowe i naciśnij przycisk Pobierz plik. Po pobraniu plik będzie się znajdował
w folderze roboczym aplikacji. Wygląd aplikacji przedstawia rysunek 20.5.
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 351

Rysunek 20.5.
Aplikacja do pobierania
plików

W przypadku pobierania dłuższych plików metody pobierania i wysyłania powinny uru-


chamiać się w osobnym wątku. Zajmiemy się tym w dalszej części tego rozdziału.

Wysyłanie pliku na serwer FTP


Czas na zaprogramowanie możliwości przesyłania pliku na serwer.

Przykład 20.10

Dołącz do aplikacji z poprzedniego przykładu możliwość przesyłania plików z dysku


lokalnego na serwer.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu i umieść trzeci przycisk Button oraz kom-
ponent systemowego okna otwarcia pliku OpenFileDialog. We właściwości Text trzeciego
przycisku wpisz Wyślij plik.

Po naciśnięciu przycisku powinno się pojawić standardowe okno wyboru pliku, w którym
będzie można wskazać plik do wysłania.

Wysyłanie pliku zrealizujemy w metodzie obsługującej zdarzenie Click przycisku.


private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
if (openFileDialog1->ShowDialog()==
System::Windows::Forms::DialogResult::OK) {
if (openFileDialog1->FileName=="")
return;
Uri^ adres = gcnew Uri("ftp://"+textBox1->Text+"/"+
Path::GetFileName(openFileDialog1->FileName));
352 Microsoft Visual C++ 2012. Praktyczne przykłady

FtpWebRequest^ req = dynamic_cast<FtpWebRequest^>


´(WebRequest::Create(adres));
req->Credentials=gcnew NetworkCredential
´("anonymous","moja_nazwa@moj_adres.pl");
req->Method=WebRequestMethods::Ftp::UploadFile;
FileStream^ stru_plik = gcnew FileStream
´(openFileDialog1->FileName,FileMode::Open);
Stream^ req_stream = req->GetRequestStream();
int ile_bajtow;
array<Byte> ^ bufor = gcnew array<Byte>(1024);
do {
ile_bajtow=stru_plik->Read(bufor,0,1024);
req_stream->Write(bufor,0,ile_bajtow);
} while(ile_bajtow!=0);
req_stream->Close();
MessageBox::Show( "Plik wysłany",
"Potwierdzenie",MessageBoxButtons::OK,
MessageBoxIcon::Information);
}
}

Po uruchomieniu programu należy wpisać adres serwera wraz z katalogiem, w którym


chcemy umieścić plik (na przykład ftp.mojftp.net/upload), a następnie najlepiej w celu
sprawdzenia pobrać spis plików za pomocą przycisku Katalog. Teraz naciśnij przycisk
Wyślij plik, wybierz plik w oknie dialogowym i kliknij OK — plik zostanie wysłany
na serwer. Po wysłaniu możesz znowu pobrać zawartość katalogu, aby sprawdzić, czy plik
został wysłany.

Aby program działał, serwer musi zezwalać na przyjmowanie plików. Jeżeli chcesz się
zalogować na prywatny serwer, musisz podać swój login i hasło we właściwości
Credentials.
Wpisywanie swojego loginu i hasła na stałe do programu jest niebezpieczne; ra-
czej należy dodać możliwość wpisywania tych danych do pól tekstowych, skąd bę-
dą za każdym razem pobierane.

Klasa do obsługi FTP


Napiszemy klasę z dwoma metodami. Jedna będzie pobierała plik z serwera, a druga wy-
syłała. Metody tej klasy będą wykonywane w wątkach. Co do zasady pobieranie i wy-
syłanie będzie działało jak w przykładach wyżej. Można by poprawić aplikację z powyż-
szych przykładów tak, aby korzystała z wątków, ale dla jasności napiszmy całkiem
nową.

Przykład 20.11

Napisz klasę z metodami do pobrania plików z serwera FTP i wysłania ich na serwer.
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 353

Rozwiązanie
Utwórz aplikację okienkową C++/CLI jak w przykładzie 1.4. Samą klasę umieścimy
w oddzielnym pliku nagłówkowym dołączonym do projektu. Kliknij na nazwie projektu
w drzewie na lewym panelu Solution Explorer tak, aby rozwinąć drzewo projektu.
Teraz kliknij prawym przyciskiem na gałęzi Header Files. Z menu wybierz Add, a na-
stępnie Class. Pojawi się okno Add Class, jak na rysunku 20.6.

Rysunek 20.6. Okno dodawana klas do projektu

W środkowym panelu wybierz C++ Class i kliknij przycisk Add. Otworzy się kreator klas.
W polu Class Name wpisz pobierz_wyslij. Pola .h file i .cpp file powinny wypełnić
się automatycznie. Kliknij Finish. Pojawi się plik pobierz_wyslij.h — w nim umieścimy
klasę z metodami poboru i wysyłania pliku. Będzie to klasa referencyjna; nazwałem ją
pobierz_wyslij. VC++ wygenerował deklarację klasy i bezparametrowy konstruktor, ale
nie zwracaj na to uwagi — napiszemy wszystko sami. Środowisko utworzyło dwa
pliki: pobierz_wyslij.h i pobierz_wyslij.cpp. Zasady pobierania i wysyłania plików są
takie same jak w poprzednich przykładach. Pierwszy plik powinien zawierać deklara-
cję, a drugi definicję metod. Podam od razu całą zawartość plików.

W pobierz_wyslij.h wpisz:
#pragma once
using namespace System;
using namespace System::Net;
using namespace System::IO;
ref class pobierz_wyslij {
private: String^ sciezka; // nazwa pliku do pobrania i ścieżka
private: String^ plik;
private: String^ login;
private: String^ haslo;
354 Microsoft Visual C++ 2012. Praktyczne przykłady

// konstruktor
public: pobierz_wyslij (String^,String^,String^,String^);

// metoda pobierająca plik


public: System::Void pobierz_plik();

public: System::Void wyslij_plik();


};

W pobierz_wyslij.cpp wpisz:
#include "stdafx.h"
#include "pobierz_wyslij.h"
pobierz_wyslij::pobierz_wyslij(String^ scie,String^ zbior,String^ logi,String^ has)
{
sciezka=scie;
plik=zbior;
login=logi;
haslo=has;
}
System::Void pobierz_wyslij::pobierz_plik() {
Uri^ adres = gcnew Uri("ftp://"+sciezka+"/"+plik);
FtpWebRequest^ req = dynamic_cast<FtpWebRequest^>
´(WebRequest::Create(adres));
req->Credentials=gcnew NetworkCredential(login,haslo);
req->Method=WebRequestMethods::Ftp::DownloadFile;
FtpWebResponse^ resp=dynamic_cast<FtpWebResponse^>(req->GetResponse());
Stream^ resp_stream = resp->GetResponseStream();
FileStream^ stru_plik = gcnew FileStream("./"+plik,FileMode::Create);
// czytaj plik z serwera i zapisuj do strumienia
int ile_bajtow;
array<Byte> ^ bufor = gcnew array<Byte>(1024);
do {
ile_bajtow=resp_stream->Read(bufor,0,bufor->Length);
stru_plik->Write(bufor,0,ile_bajtow);
} while(ile_bajtow!=0);
stru_plik->Flush();
stru_plik->Close();
resp_stream->Close();
resp->Close();
}
System::Void pobierz_wyslij::wyslij_plik() {
Uri^ adres = gcnew Uri("ftp://"+sciezka+"/"+plik);
FtpWebRequest^ req = dynamic_cast<FtpWebRequest^>
´(WebRequest::Create(adres));
req->Credentials=gcnew NetworkCredential(login,haslo);
req->Method=WebRequestMethods::Ftp::UploadFile;
FileStream^ stru_plik = gcnew FileStream("./"+plik,FileMode::Open);
Stream^ req_stream = req->GetRequestStream();
int ile_bajtow;
array<Byte> ^ bufor = gcnew array<Byte>(1024);
do {
ile_bajtow=stru_plik->Read(bufor,0,1024);
req_stream->Write(bufor,0,ile_bajtow);
} while(ile_bajtow!=0);
req_stream->Close();
FtpWebResponse^ resp=dynamic_cast<FtpWebResponse^>(req->GetResponse());
}
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 355

Metody z klasy pobierz_wyslij wykorzystamy w klasie okna głównego. Możesz skom-


pilować program, jednak na tym etapie nic się nie stanie oprócz wyświetlenia pustego
okna. Zapisz aplikację, ponieważ będziemy ją rozbudowywać.

Zanim zaczniemy pobierać pliki, trzeba się zalogować na serwer i wyświetlić zawartość
katalogu. Zrobimy to identycznie jak w przykładzie 20.8. Lista plików nie będzie jed-
nak wyświetlana w polu tekstowym, ale w kontrolce listy ListBox. Umożliwi to po-
bieranie pliku poprzez wskazanie go myszką, a nie wpisywanie całej nazwy.

Przykład 20.12

Zaprogramuj wyświetlanie katalogu serwera FTP.

Rozwiązanie
Otwórz projekt z poprzedniego przykładu. Do okna wstaw z panelu Toolbox przycisk
Button, w jego właściwość Tekst wpisz napis Katalog. Będziemy potrzebować jeszcze
jednego pola tekstowego TextBox i kontrolki ListBox. Kontrolki ustaw tak, aby okno
wyglądało jak na rysunku 20.7.

Rysunek 20.7.
Okno klienta FTP
z kontrolką ListBox

Aby program działał, dodaj do niego możliwość korzystania z przestrzeni nazw System::Net
i System::IO.
using namespace System::Net;
using namespace System::IO;

Kliknij dwukrotnie przycisk Katalog, tworząc metodę obsługi zdarzenia kliknięcia.


W części pobierającej listę plików jej zawartość będzie identyczna jak w przykładzie
20.8, różnice są tylko w części wyświetlającej pobraną zawartość katalogu.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Uri^ adres = gcnew Uri("ftp://"+textBox1->Text);
356 Microsoft Visual C++ 2012. Praktyczne przykłady

FtpWebRequest^ req = dynamic_cast<FtpWebRequest^>


´(WebRequest::Create(adres));
req->Credentials=gcnew NetworkCredential
´("anonymous","moja_nazwa@moj_adres.pl");
req->Method=WebRequestMethods::Ftp::ListDirectoryDetails;
FtpWebResponse^ resp;
try {
resp=dynamic_cast<FtpWebResponse^>(req->GetResponse());
}
catch (WebException^ ex)
{
MessageBox::Show(ex->Message);
return;
}
Stream^ resp_stream = resp->GetResponseStream();
StreamReader^ reader = gcnew StreamReader(resp_stream);
String^ linia;
listBox1->Items->Clear();
while (!reader->EndOfStream) {
linia=reader->ReadLine();
listBox1->Items->Add(linia);
}
}

Zamiast kontrolki TextBox mamy tu kontrolkę Listbox. Jej zawartością zarządzamy po-
przez właściwość Items oznaczającą kolejne linie. Najpierw kasujemy jej zawartość,
a następnie dodajemy linie listy plików metodą Add(). Skompiluj i uruchom program,
wpisz adres dowolnego serwera ftp obsługującego logowanie anonimowe. Naciśnij
przycisk Katalog. W oknie tekstowym pojawi się zawartość głównego katalogu. Zapisz
projekt aplikacji.

Pobieranie plików w oddzielnym wątku


Zaprogramujemy pobieranie plików za pomocą metody wcześniej napisanej klasy.

Przykład 20.13

Zaprogramuj pobranie pliku, korzystając z klasy z poprzedniego przykładu.

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu. Na początku pliku Form1.h załącz dyrek-
tywą include plik z naszą klasą FTP.
#include "pobierz_wyslij.h"

Niżej, pod dyrektywami using namespace, dołącz przestrzeń nazw konieczną do wie-
lowątkowości.
using namespace System::Threading;
Rozdział 20. ♦ Połączenie aplikacji z siecią Internet 357

Obok przycisku Katalog wstaw z panelu Toolbox drugi przycisk i zmień wartość jego
właściwości Text na Pobierz. Przycisk będzie pobierał zaznaczony plik z listy. Format
listy plików zwracany przez różne serwery nieco się różni. Dosyć trudno jest więc wy-
odrębnić z katalogu wszystkie składowe, czyli nazwy plików, ich rozmiary, daty itp.
W miarę małym nakładem pracy można uzyskać nazwę i atrybuty. Użytkownik zazna-
cza myszką na liście wpis z plikiem, który chce pobrać, a aplikacja musi wypreparo-
wać z tego wpisu nazwę pliku potrzebną do metody pobrania. Oto przykładowy wpis
z katalogu:
-rw-r--r-- 1 500 549 Nov 22 2006 README

Opisuje on plik README o atrybutach tylko do odczytu i rozmiarze 549 bajtów. Atry-
buty pliku opisane są w formacie systemu UNIX. Na razie zajmiemy się pobraniem
samej nazwy pliku z wpisu. Jak widać, nazwa rozpoczyna się po ostatniej spacji. A więc
znajdziemy ostatnią spację metodą LastIndexOf() i utworzymy łańcuch ze znaków po tej
spacji. Ta metoda działa, jeżeli sama nazwa pliku nie zawiera spacji. Takie przypadki
są jednak raczej rzadkie, choć jak najbardziej możliwe.

Kliknij podwójnie przycisk button2. W utworzonej metodzie obsługi zdarzenia Click()


najpierw pobieramy zaznaczoną pozycję na liście za pomocą właściwości SelectedItem.
Następnie wycinamy wszystko przed ostatnią spacją z pobranego wpisu za pomocą
metody Substring(). Utworzenie wątku z metody klasy opisane jest w rozdziale 19.
Oto cały kod metody:
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e)
{ (tu pisac)
System::String^ aktPozycja=listBox1->SelectedItem->ToString();
pobierz_wyslij^ pobierz = gcnew pobierz_wyslij(textBox1->Text,
aktPozycja->Substring(aktPozycja->LastIndexOf(" ")+1), login,haslo);
Thread^ watek_pobrania = gcnew Thread(gcnew
ThreadStart(pobierz,&pobierz_wyslij::pobierz_plik));
watek_pobrania->Start();
}

Uruchom aplikację, wpisz adres dowolnego serwera ftp, który umożliwia publiczne logo-
wanie. W katalogu tego serwera wybierz dowolny plik i naciśnij przycisk Pobierz. Plik
zostanie pobrany do folderu z projektem aplikacji. Jeśli chcesz przejść do podkatalogu na
serwerze FTP, musisz wpisać nazwę serwera z katalogiem, np. ftp.przyklad.pl/pub, i na-
cisnąć przycisk Katalog. Zapisz projekt aplikacji.

Wysyłanie plików w wątku


Uruchomimy teraz metodę klasy odpowiedzialną za wysyłanie plików na serwer FTP.

Przykład 20.14

Zaprogramuj wysyłanie pliku, korzystając z klasy z poprzedniego przykładu.


358 Microsoft Visual C++ 2012. Praktyczne przykłady

Rozwiązanie
Otwórz aplikację z poprzedniego przykładu. Dodaj do niej trzeci przycisk i wpisz w jego
właściwość Text słowo Wyślj; będzie on służył do wysyłania pliku na serwer. Dodaj
do aplikacji komponent OpenFileDialog. Tu sytuacja jest prostsza: plik do wysłania
wybieramy za pomocą okna dialogowego OpenFileDialog. Po zamknięciu okna wy-
słanie następuje za pomocą metody wyslij() z klasy pobierz_wyslij.
private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {
if (openFileDialog1->ShowDialog()==
System::Windows::Forms::DialogResult::OK) {
if (openFileDialog1->FileName=="")
return;
pobierz_wyslij^ wyslij = gcnew pobierz_wyslij(textBox1->Text, Path::GetFileName
´(openFileDialog1->FileName), "anonymous","moja_nazwa@moj_adres.pl");
Thread^ watek_wyslania = gcnew Thread(gcnew ThreadStart(wyslij, &pobierz_wyslij::
´wyslij_plik));
watek_wyslania->Start();
}
}

Teraz w przypadku pobierania dłuższych plików nie dochodzi do blokowania aplikacji.


Uruchom aplikację, wpisz adres dowolnego serwera ftp, który umożliwia wysyłanie
plików. Serwer publiczny trudno Ci będzie znaleźć, raczej będziesz musiał postarać
się o konto z loginem i hasłem. A może już takie posiadasz, skoro jesteś zainteresowany
przesyłaniem plików? W takim przypadku zamiast anonymous wpisujesz login, a zamiast
moja_nazwa@moj_adres.pl — hasło. Jednak uważaj: po zakończeniu pracy usuń hasło,
aby nikt go nie przechwycił. Klikając na przycisk Wyślij, uruchamiasz okno wyboru
pliku, wybierasz plik i naciskasz OK. Plik zostanie przesłany na serwer.
Rozdział 21.
Dynamiczne tworzenie
okien i komponentów

Wyświetlanie okien — klasa Form


Większość aplikacji nie składa się z jednego okna, ale zawiera ich wiele; oprócz okna
głównego mamy okna do ustawiania opcji lub wprowadzania danych. Takie okna można
bardzo łatwo tworzyć dynamicznie podczas działania programu. Jak już wspomniałem
w rozdziale 9., okno jest obiektem klasy Form. Tak jak inne obiekty, można je tworzyć
podczas wykonywania programu przy użyciu operatora gcnew.

Przykład 21.1

Po naciśnięciu przycisku wyświetlaj nowe, puste okno aplikacji.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do niego przycisk Button.

Napisz metodę obsługującą zdarzenie Click, jak niżej.


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Form^ okno = gcnew Form;
okno->Show();
}

Po naciśnięciu przycisku ukaże się nowe okno.

Mamy dwie możliwości wyświetlenia okna klasy Form: jako okna „zwykłego” (takiego
jak okno główne aplikacji) lub okna dialogowego. Okno zwykłe po utworzeniu wy-
świetlamy za pomocą metody Show(), natomiast okno dialogowe za pomocą metody
360 Microsoft Visual C++ 2012. Praktyczne przykłady

ShowDialog(). Różnice w wyglądzie obu okien przedstawia rysunek 21.1. Są one prawie
niezauważalne, chodzi o kilka kropek w prawym dolnym rogu okna.

Rysunek 21.1.
Okno wyświetlone
za pomocą metod
a) Show(),
b) ShowDialog()

Zasadnicza różnica nie tkwi jednak w wyglądzie, ale w działaniu tych okien. Po wyświe-
tleniu okna za pomocą metody Show() metoda, która wyświetliła okno, wykonuje się
dalej, zaś przy wyświetleniu za pomocą metody ShowDialog() wykonywanie programu
jest zatrzymane aż do zamknięcia okna. Właściwości okna głównego zestawione w ta-
beli 9.1 w rozdziale 9. odnoszą się do każdego obiektu klasy Form. Korzystając z nich,
wyświetlimy teraz okno o żądanych parametrach.

Przykład 21.2

Wyświetl okno o wymiarach 200×200 punktów z napisem „Okno 2” na pasku tytuło-


wym, umieszczone w środku ekranu.

Rozwiązanie
Utwórz taką samą aplikację jak w poprzednim przykładzie.

Metoda obsługująca zdarzenie Click będzie miała teraz postać:


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Form^ okno = gcnew Form;
okno->Width=200;
okno->Height=200;
okno->Text="Okno 2";
Rectangle ekran=System::Windows::Forms::Screen::GetBounds(okno);
okno->Top=(ekran.Height/2)-(okno->Height/2);
okno->Left=(ekran.Width/2)-(okno->Width/2);
okno->Show();
}

Rozdzielczość ekranu, na którym będzie wyświetlane okno, pobieramy za pomocą me-


tody GetBounds() klasy Screen.
Rozdział 21. ♦ Dynamiczne tworzenie okien i komponentów 361

Komponenty
w oknie tworzonym dynamicznie
Liczba i rodzaj komponentów w oknie przechowywane są we właściwości Controls klasy
okna Form. Właściwość ta jest typu tablicowego. Aby dodać nowy komponent do okna,
należy najpierw zdefiniować obiekt komponentu, a następnie dodać go do tablicy
Controls za pomocą metody Add().

Przykład 21.3

Wyświetl okno z kontrolkami TextBox, Button i CheckBox.

Rozwiązanie
Do wykonania zadania potrzebna będzie aplikacja z pojedynczym przyciskiem But-
ton. Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do formatki przy-
cisk Button.

Komponenty są obiektami odpowiednich klas. W metodzie obsługującej zdarzenie Click


przycisku utwórz kolejno kontrolki wymaganych typów za pomocą operatora gcnew,
ustaw ich właściwości tak, aby uzyskać zamierzony wygląd okna, a następnie dodaj je
do okna i wyświetl.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Form^ okno = gcnew Form;
// pole tekstowe
TextBox^ pole_tekst = gcnew TextBox;
pole_tekst->Width=190;
// przycisk
Button^ przycisk = gcnew Button;
przycisk->Text="OK";
przycisk->Location=Point(5,80);
// pole wyboru
CheckBox^ wybor = gcnew CheckBox;
wybor->Location=Point(5,50);
// dodanie komponentów do okna
okno->Controls->Add(pole_tekst);
okno->Controls->Add(przycisk);
okno->Controls->Add(wybor);
okno->Show();
}

Po wypróbowaniu programu zachowaj go do wykorzystania w następnym przykładzie.


362 Microsoft Visual C++ 2012. Praktyczne przykłady

Przesyłanie danych
z okien dialogowych
Pobieranie danych z komponentów tworzonych dynamicznie jest takie samo, jak z kom-
ponentów utworzonych na etapie projektowania aplikacji. Zazwyczaj dodatkowe okna
aplikacji służą do określania jej parametrów lub wprowadzania danych i wygodniej
jest wyświetlać je jako okna dialogowe. Wtedy program czeka, aż użytkownik przepro-
wadzi operacje w oknie i je zamknie.

Przykład 21.4

Zmodyfikuj program z poprzedniego przykładu tak, aby okno było wyświetlane w trybie
dialogowym. Po zamknięciu okna niech program wyświetla w głównym oknie tekst
wpisany w pole tekstowe i stan pola wyboru.

Rozwiązanie
Po otwarciu projektu z poprzedniego przykładu umieść w oknie głównym dwie etykiety
Label.

Po naciśnięciu przycisku w oknie głównym będzie wyświetlane nowe okno w trybie


dialogowym. Zamknięcie tego okna nastąpi po naciśnięciu przycisku OK. Aby tak się
stało, przycisk w momencie naciśnięcia musi przekazać do okna dialogowego jeden
z parametrów typu DialogResult. Zestawienie tych parametrów znajduje się w tabeli 13.2
w rozdziale o oknach dialogowych. Wartość parametru, jaki zwróci przycisk, ustawia-
my we właściwości DialogResult przycisku. Po zamknięciu okna odczytujemy parametry
kontrolek i wyświetlamy w etykietach. Oto metoda, jaką należy podpiąć pod zdarzenie
Click przycisku w oknie głównym:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Form^ okno = gcnew Form;
// pole tekstowe
TextBox^ pole_tekst = gcnew TextBox;
pole_tekst->Width=190;
// przycisk
Button^ przycisk = gcnew Button;
przycisk->Text="OK";
przycisk->Location=Point(5,80);
// pole wyboru
CheckBox^ wybor = gcnew CheckBox;
wybor->Location=Point(5,50);
// dodanie komponentów do okna
okno->Controls->Add(pole_tekst);
okno->Controls->Add(przycisk);
okno->Controls->Add(wybor);
przycisk->DialogResult = System::Windows::Forms::DialogResult::OK;
okno->ShowDialog();
label1->Text=pole_tekst->Text;
label2->Text=wybor->Checked.ToString();
}
Rozdział 21. ♦ Dynamiczne tworzenie okien i komponentów 363

Okna tworzone dynamicznie można też wykorzystać jako okna do wyświetlania tytułu
aplikacji lub do zabezpieczania programu hasłem.

Okno tytułowe aplikacji


Przykład 21.5

Utwórz aplikację z oknem tytułowym. Okno powinno pojawić się podczas uruchamiania
i zniknąć po 3 sekundach.

Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4. Umieść w nim komponent Timer.

Okno tytułowe będzie składową klasy Form1. Wpisz deklarację uchwytu do okna, tam
gdzie inne metody i zmienne klasy.
private: Form^ tytul;

Ponieważ tytuł będzie się wyświetlał przy starcie aplikacji, metodę, która go wyświetla,
podepniemy do zdarzenia Load głównego okna. Taką metodę najłatwiej utworzysz,
klikając podwójnie obszar okna w widoku projektowania aplikacji. W metodzie stwo-
rzymy obiekt okna za pomocą operatora gcnew, a następnie dołączymy etykietę z ty-
tułem i włączymy czasomierz.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
tytul = gcnew Form;
tytul->Height=100;
Label^ napis = gcnew Label;
System::Drawing::Font^ czcionka = gcnew System::Drawing::Font
´(System::Drawing::FontFamily::GenericSansSerif,
14, FontStyle::Regular);
napis->Text="Tytuł programu";
napis->Font=czcionka;
napis->AutoSize=true;
napis->Location=Point(80,20);
tytul->Controls->Add(napis);
tytul->TopMost=true;
tytul->Show();
timer1->Start();
}

Teraz w zakładce projektowania okna aplikacji Form1.h [Design] zaznacz komponent


Timer na pasku na dole, a następnie w panelu zdarzeń zmień wartość jego właściwości
Interval na 3000 (3 sekundy).

Po upłynięciu tego czasu zajdzie zdarzenie Tick. W metodzie obsługi tego zdarzenia
zamkniemy okno i wyłączymy czasomierz. Metodę obsługi zdarzenia utworzysz i pode-
pniesz do zdarzenia, klikając dwukrotnie komponent Timer na pasku w widoku pro-
jektowania aplikacji. Zmodyfikuj ją następująco:
364 Microsoft Visual C++ 2012. Praktyczne przykłady

private: System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e) {


timer1->Stop();
tytul->Close();
}

Po uruchomieniu program powinien działać zgodnie z oczekiwaniami.

Obsługa zdarzeń dla komponentów


tworzonych dynamicznie
Przy tworzeniu komponentów na etapie projektowania aplikacji przyporządkowywanie
metod zdarzeniom jest ułatwione. Środowisko wyświetla listę zdarzeń dla każdego kom-
ponentu i automatycznie tworzy metodę obsługi po kliknięciu nazwy zdarzenia. W przy-
padku komponentów tworzonych dynamicznie trzeba samemu napisać metodę zdarzenia
i przyporządkować ją do odpowiedniego zdarzenia komponentu.

Nie można przyporządkować dowolnej metody do obsługi zdarzenia bez specjalnych


zabiegów, obowiązują tu pewne zasady:
a) Metoda musi posiadać określoną listę parametrów. Ogólnie, pierwszy
parametr to komponent generujący zdarzenie, a drugi to argumenty tego
zdarzenia. Pierwszy parametr jest zwykle typu System::Object^, a drugi
System::EventArgs^. Jednak dla niektórych zdarzeń może to być inna lista
parametrów. Łatwo to sprawdzić, wstawiając identyczny komponent w edytorze
i tworząc metodę obsługi odpowiedniego zdarzenia poprzez panel zdarzeń.
Ta automatycznie utworzona metoda ma właściwą listę parametrów.
b) Przyporządkowanie metody do zdarzenia następuje za pośrednictwem delegata
EventHandler lub podobnego. Rodzaj delegata dla konkretnego zdarzenia
można sprawdzić, tworząc metodę obsługi takiego zdarzenia w środowisku
i zaglądając do wnętrza metody InitializeComponent() w pliku Form1.h.
Znajdują się tam przyporządkowane metody obsługi zdarzeń dla poszczególnych
komponentów. Parametrami konstruktora delegata EventHandler są obiekty
klasy, z której pochodzi metoda obsługi zdarzenia i referencja do samej metody.
Delegat podstawiamy do zdarzenia komponentu podobnie jak wartość
właściwości. Nie używamy jednak operatora podstawienia =, ale dodania
o wielkość +=.

Najlepiej wyjaśni to przykład.

Przykład 21.6

Po naciśnięciu przycisku wyświetl okno z kolejnym przyciskiem; niech naciśnięcie


tego drugiego przycisku powoduje wyświetlenie w oknie napisu.
Rozdział 21. ♦ Dynamiczne tworzenie okien i komponentów 365

Rozwiązanie
Do okna nowego projektu aplikacji wstaw przycisk Button.

W metodzie obsługi tego przycisku będą tworzone i wyświetlane nowe okno oraz
nowy przycisk. Następnie do obsługi zdarzenia Click tego nowego przycisku podstawimy
metodę przycisk_Click() i wyświetlimy okno. Oto kod:
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
Form^ okno = gcnew Form;
Button^ przycisk = gcnew Button;
przycisk->Text="Napis";
okno->Controls->Add(przycisk);
przycisk->Click += gcnew EventHandler(this,&Form1::przycisk_Click);
okno->Show();
}

Teraz napisz metodę przycisk_Click(), która wyświetli napis w nowym oknie.


private: System::Void przycisk_Click(System::Object^ sender, System::EventArgs^ e) {
Label^ etykieta = gcnew Label;
etykieta->Text="Napis w oknie";
etykieta->Location=Point(100,100);
safe_cast<Button^>(sender)->Parent->Controls->
Add(etykieta); }

Metoda obsługi przycisk_Click() ma parametr sender, pod który jest podstawiany


komponent wywołujący, w tym przypadku przycisk. Aby wyświetlić napis na oknie, mu-
simy uzyskać do niego dostęp. Okno jest „rodzicem”, czyli kontrolką nadrzędną dla
przycisku, więc dostęp do niego uzyskamy przez właściwość Parent przycisku. Na
tym oknie wywołamy metodę Add() dodającą etykietę z napisem.

Aplikacja zabezpieczona hasłem


Za pomocą dynamicznie tworzonych okien można zabezpieczyć aplikację hasłem.

Przykład 21.7

Niech po uruchomieniu aplikacji pojawia się okno z pytaniem o hasło. Niemożliwe będzie
zamknięcie okna za pomocą przycisku ani kombinacji klawiszy Alt+F4 bez podania
hasła.

Rozwiązanie
Utwórz nowy projekt aplikacji.

Wyświetlenie okna z pytaniem o hasło będzie się odbywało bezpośrednio po urucho-


mieniu aplikacji. Odpowiednią metodę podepniemy więc pod zdarzenie Load:
366 Microsoft Visual C++ 2012. Praktyczne przykłady

private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {


Form^ okno = gcnew Form;
okno->Height=120;
okno->Width=200;
okno->FormClosing+= gcnew FormClosingEventHandler
´(this,&Form1::okno_FormClosing);
TextBox^ haslo = gcnew TextBox;
haslo->Location=Point(20,30);
haslo->PasswordChar='*';
Button^ przycisk = gcnew Button;
przycisk->Click += gcnew EventHandler(this,&Form1::przycisk_Click);
przycisk->Location=Point(20,60);
przycisk->Text="OK";
Label^ etykieta = gcnew Label;
etykieta->Text="Podaj hasło:";
etykieta->Location=Point(20,10);
okno->Controls->Add(haslo);
okno->Controls->Add(przycisk);
okno->Controls->Add(etykieta);
okno->ShowDialog();
}

Sprawdzanie poprawności hasła powinno być przeprowadzane zawsze przy zamykaniu


okna z pytaniem o hasło. Przy zamykaniu występuje zdarzenie FormClosing i pod nie pod-
pinamy metodę sprawdzania. Zauważ, że używamy delegata FormClosingEventHandler,
a nie EventHandler, jak w przypadku zdarzenia Click.

Metoda obsługująca zdarzenie FormClosing dla okna tworzonego dynamicznie będzie


miała postać:
private: System::Void okno_FormClosing(System::Object^ sender,
´System::Windows::Forms::FormClosingEventArgs^ e) {
if ((((Form^)sender)->Controls[0]->Text)!="Haslo")
e->Cancel=true;
else
e->Cancel=false;
}

To, czy okno można zamknąć, zależy od właściwości Cancel parametru e przekazywane-
go do metody przez obsługę zdarzeń. W przypadku gdy pole tekstowe nie zawiera prawi-
dłowego hasła, ustawiamy wartość true parametru Cancel, co uniemożliwia zamknięcie
okna.

Teraz pozostało już tylko zaprogramować przycisk w oknie pod hasłem, który będzie po
prostu próbował zamknąć okno, wyzwalając procedurę sprawdzania hasła.
private: System::Void przycisk_Click(System::Object^ sender, System::EventArgs^ e) {
((Form^)((Button^)sender)->Parent)->Close();
}
Rozdział 22.
Prosty manager plików

Interfejs managera
Na zakończenie napiszmy trochę większą aplikację do zarządzania plikami na dysku.
Aplikacja będzie wyświetlała zawartość dwóch folderów. Za pomocą myszy będzie
możliwe poruszanie się w górę i w dół drzewa folderów. Okno aplikacji będzie miało
kilka przycisków do kopiowania plików między folderami oraz ich kasowania. Uży-
jemy tu głównie metod i klas, które poznałeś w rozdziale o komunikacji z plikami.

Zacznij od utworzenia nowego projektu aplikacji okienkowej C++/CLI według przy-


kładu 1.4. Do pustego na razie okna wstaw dwie listy listBox, cztery przyciski Button
i dwie etykiety Label. Przyciski podpisz następująco za pomocą właściwości Text:
button1 - Kopiuj ->
button2 - <-Kopiuj
button3 - Kasuj ->
button4 - <-Kasuj

Rozmiary komponentów listBox zmień na 370; 290. W przypadku pierwszej kon-


trolki listBox1 zrobisz to, klikając na nią w oknie aplikacji i rozwijając panel Pro-
perties. Następnie znajdź właściwość Size i wpisz 370;290. Powiększ też odpowied-
nio okno aplikacji, aby wszystko się mieściło.

Rozplanowanie reszty kontrolek pokazałem na rysunku 22.1.

Wyświetlanie zawartości folderów


W bloku dyrektyw using na początku pliku Form1.h podłącz przestrzeń nazw Sys-
tem::IO, aby umożliwić operacje na plikach.
using namespace System::Drawing; //ta linia istnieje
using namespace System::IO;
368 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 22.1. Widok projektowanego okna aplikacji

Pierwszą rzeczą, jaką zrobi nasza aplikacja po uruchomieniu, będzie wyświetlenie


zawartości dwóch folderów. Mogą być to dowolne foldery; ja wybrałem najprostsze
rozwiązanie — folder projektu aplikacji. Kliknij podwójnie okno aplikacji poza obszarem
kontrolek. Utworzy się metoda Form1_Load() wykonywana po wyświetleniu okna.

Foldery aktualnie wyświetlane w okienkach listBox będą przechowywane w strukturach


DirectoryInfo, które będą polami klasy Form1. Ponieważ będą dwa foldery, potrzebu-
jemy dwóch struktur. Nazwałem je AktFold1 i AktFold1.
DirectoryInfo^ AktFold1;
DirectoryInfo^ AktFold2;

Zdefiniuj dwa pola zaraz po dyrektywie #pragma endregion w pliku Form1.h.

Wróćmy do metody Form1_Load(). Zdefiniowane uchwyty do struktur folderów trzeba


wypełnić zawartością; zrobimy to już na etapie tworzenia obiektu DirectoryInfo, na
który będą wskazywały uchwyty AktFold1 i AktFold2. Konstruktor z nazwą folderu jako
parametrem był już używany w rozdziale o plikach. Kropka oznacza aktualny folder,
w którym uruchamiana jest aplikacja. Następnie wyświetlimy zawartości folderów
w listach za pomocą funkcji WyswietlFold(), którą zaraz zdefiniujemy. Wreszcie pełne
ścieżki folderów wyświetlimy w etykietach, żebyśmy wiedzieli, gdzie jesteśmy. Oto
cała metoda Form1_Load():
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
AktFold1 = gcnew DirectoryInfo("."); // punkt startowy to folder programu
AktFold2 = gcnew DirectoryInfo(".");
WyswietlFold(listBox1,AktFold1);
WyswietlFold(listBox2,AktFold2);
label1->Text=AktFold1->FullName;
label2->Text=AktFold2->FullName;
}

Do działania Form1_Load() brakuje metody WyswietlFold(). Powyżej Form1_Load()


napisz pustą metodę składową klasy Form1 WyswietlFold().
private: System::Void WyswietlFold(ListBox^ lista, DirectoryInfo^ katalog)
{}
Rozdział 22. ♦ Prosty manager plików 369

Od razu zauważ listę parametrów. Pierwszym jest lista do wyświetlenia, a drugim fol-
der do prezentacji w tej liście. Metoda będzie wyświetlała zawartość folderu, podobnie
jak w rozdziale o plikach. Tam jednak wyświetlaliśmy zawartość w oknie textBox, a tu
listBox. Zysk jest taki, że poszczególne linie z listy można zaznaczyć myszką.

Nasza aplikacja nie powinna prezentować folderu byle jak, ale porządnie, w kolumnach.
Jedna kolumna będzie zawierała nazwy plików, a druga ich rozmiary. Aby kolumny były
stałej szerokości, zmienimy czcionkę w liście na stałą szerokość znaków. Taką czcionką
jest Courier. Kolejne linie wpisuj w nawiasy klamrowe metody WyswietlFold().
System::Drawing::Font^ czcionka =
gcnew System::Drawing::Font("Courier",10,FontStyle::Regular);
lista->Font = czcionka;

Następnie wyczyścimy listę.


lista->Items->Clear();

Jako pierwszy wstawimy znacznik, na który klikniemy, jeśli będziemy chcieli przejść
o jeden folder w górę w drzewie. Wstawiamy go oczywiście tylko wtedy, jeśli jest gdzie
przechodzić, czyli dany folder ma folder „rodzica”. Sprawdzamy to właściwością Pa-
rent struktury DirectoryInfo.
if (katalog->Parent) lista->Items->Add("(..)"); // znacznik do folderu nadrzędnego

Następnie wyświetlimy podfoldery aktualnego folderu. Robimy to tak samo jak wyświe-
tlanie plików, które już znasz, tylko zamiast metody GetFiles() pobierającej nazwy pli-
ków używamy GetDirectories().
array<DirectoryInfo^>^ foldery = katalog->GetDirectories();
for (System::Int32 i=0;i<foldery->Length; i++) {
lista->Items->Add(FormatWpisFolder(foldery[i]));
}

Nazwy folderów wyświetlamy nie bezpośrednio, ale za pomocą metody formatującej


każdą linię FormatWpisFolder(), którą za chwilę napiszemy. Po folderach wyświetlamy
pliki identycznie, jak to robiliśmy w przykładzie w rozdziale o plikach.
array<FileInfo^>^ pliki = katalog->GetFiles("*.*");
for (System::Int32 i=0;i<pliki->Length; i++) {
lista->Items->Add(FormatWpisPlik(pliki[i]));
}

Tu znowu mamy metodę formatującą FormatWpisPlik(), inną niż w przypadku folderów.


I to już koniec WyswietlFold().

Formatowanie prezentacji folderu


Pojawiły się dwie metody, które trzeba zrealizować. Każdy wpis pliku lub folderu na
liście składa się z 30 znaków na nazwę i 10 znaków na rozmiar w przypadku pliku lub
napis SUB-DIR w przypadku folderu. Zaczniemy od FormatWpisFolder(). Zadaniem tej
metody jest sformatowanie wpisów dla folderów. Jeśli nazwa jest krótsza niż 30 znaków,
370 Microsoft Visual C++ 2012. Praktyczne przykłady

to dopełniana jest spacjami. Dłuższa nazwa jest z kolei obcinana. Zasadę formatowania
linii pokazuje rysunek 22.2.

Rysunek 22.2.
Formatowanie linii
dla plików i folderów

Napiszmy więc metodę FormatWpisFolder(). Metoda będzie zwracała obiekt String


z gotową linią. Wprowadzimy pole klasy Form1, do którego będziemy wpisywać wynik
działania FormatWpisFolder(). Zwracany będzie uchwyt do tego obiektu. Przy okazji
zadeklarujemy podobny obiekt dla metody formatującej wpisy plików. Pod deklaracją
AktFold2 wpisz:
private: DirectoryInfo^ AktFold2; //ta linia istnieje
private: System::String^ OpisPlikuFormat;
private: System::String^ OpisFolderuFormat;

Piszemy FormatWpisFolder(); najpierw pusta metoda z listą parametrów — wpisz ją


ponad metodą WyswietlFold().
private: System::String^ FormatWpisFolder(DirectoryInfo^ folder) {}

Kolejne linie wpisuj w nawiasach klamrowych. Najpierw zerowanie obiektu OpisFol-


deruFormat.
OpisFolderuFormat="";

Następnie wpiszemy nazwę folderu.


OpisFolderuFormat+=folder->Name;

Dopełnienie do 30 znaków.
//spacje po nazwie folderu
System::Int32 spacje;
spacje=30-folder->Name->Length;
for (int sp=0;sp<spacje;sp++)
OpisFolderuFormat+=" ";

Spacje przed tekstem SUB-DIR i sam tekst. Ponieważ tekst ma 7 znaków, należy dosta-
wić trzy spacje.
spacje=3;
for (int sp=0;sp<spacje;sp++)
OpisFolderuFormat+=" ";
//SUB-DIR
OpisFolderuFormat+="SUB-DIR";

Zwracamy uchwyt do pola OpisFolderuFormat.


return OpisFolderuFormat;
Rozdział 22. ♦ Prosty manager plików 371

I to wszystko. Teraz zajmiemy się metodą FormatWpisPlik(), formatującą wpisy plików.


Zasada działania jest podobna. Najpierw pusta metoda, którą wpisz przed WyswietlFold().
private: System::String^ FormatWpisPlik(FileInfo^ plik) {}

Akceptuje ona jako parametr uchwyt do obiektu FileInfo z opisem pliku. Dalsze linie
wpisuj w nawiasach klamrowych. Będzie to wykasowanie zawartości obiektu zwraca-
nego, następnie wpisanie nazwy z ewentualnym przycięciem do 30 znaków:
OpisPlikuFormat="";
//nazwa pliku; jeśli większa od 30 znaków, to przytnij
if (plik->Name->Length>30)
OpisPlikuFormat+=plik->Name->Remove(30);
if (plik->Name->Length<30)
OpisPlikuFormat+=plik->Name;

Dopełnienie spacjami do 30 znaków.


System::Int32 spacje;
spacje=30-plik->Name->Length;
for (int sp=0;sp<spacje;sp++)
OpisPlikuFormat+=" ";

Teraz wypiszemy rozmiar. Jeśli jest on mniejszy niż jeden kilobajt (1024 bajty), to
wypiszemy go w bajtach. Jeśli zawiera się między jednym kilobajtem a jednym me-
gabajtem (1024*1024 bajty), to wypiszemy w kilobajtach. Jeśli jest większy niż jeden
megabajt, to w megabajtach.
//rozmiar
System::Int64 rozmiar=plik->Length;
System::String^ Rozmiar_String;
//konwersja na rozmiar tekstowo wraz z jednostką
if (rozmiar<1024)
Rozmiar_String=((System::Double)plik->Length).ToString()+"B";
if ((rozmiar>=1024)&&(rozmiar<=1048576))
Rozmiar_String=((System::Double)plik->Length/1024).ToString("F2")+"KB";
if (rozmiar>1048576)
Rozmiar_String=((System::Double)plik->Length/1048576).ToString("F2")+"MB";

Po ustaleniu wielkości łańcucha znakowego z rozmiarem najpierw dopełniamy do 10


spacji i wypisujemy rozmiar.
//spacje przed rozmiarem pliku — dopiero teraz wiadomo, ile zajmie rozmiar
spacje=10-Rozmiar_String->Length;
for (int sp=0;sp<spacje;sp++)
OpisPlikuFormat+=" ";
OpisPlikuFormat+=Rozmiar_String;

Pozostało zwrócić gotową linię do wypisania w liście.


return Opis likuFormat;

Koniec FormatWpisPlik(). Jeśli teraz skompilujesz program, powinieneś zobaczyć za-


wartość folderów, jak na rysunku 22.3.
372 Microsoft Visual C++ 2012. Praktyczne przykłady

Rysunek 22.3. Wyświetlanie zawartości folderu

Przechodzenie w dół i w górę


drzewa plików
Na razie aplikacja umie tylko wpisywać do list zawartość jednego folderu. Aby pro-
gram miał zastosowanie praktyczne, trzeba zaprogramować nawigację po folderach.

Idziemy w górę
Zaprogramowanie przejścia do folderu nadrzędnego jest łatwe, bo jest on dostępny we
właściwości Parent obiektu DirectoryInfo. Kiedy klikniemy na liście znacznik (..),
przejdziemy do folderu wyżej. Kliknięcie listy wyzwala zdarzenie Click. Kliknij listę
listBox1, następnie w panelu Properties przełącz się na widok zdarzeń i znajdź zda-
rzenie Click. Kliknij je dwukrotnie, a utworzy się metoda jego obsługi.
private: System::Void listBox1_Click(System::Object^ sender, System::EventArgs^ e) {}

Aktualnie zaznaczoną linię na liście można pobrać z właściwości SelectedItem tej listy.

Jeśli zawiera ona znak (..), to ustawimy uchwyt do aktualnego folderu na folder nad-
rzędny, następnie wypiszemy w etykiecie ścieżkę i wyświetlimy nowy folder w oknie.

Do metody listBox1_Click() wpisz kod realizujący te kroki:


private: System::Void listBox1_Click(System::Object^ sender, System::EventArgs^ e) {
if (listBox1->SelectedItem->ToString()=="(..)") {
AktFold1=AktFold1->Parent; //aktualny folder wyżej
label1->Text=AktFold1->FullName;
WyswietlFold(listBox1,AktFold1);
return;
}
}
Rozdział 22. ♦ Prosty manager plików 373

Instrukcją return wychodzimy z metody po zmianie folderu. Zapobiega to niestabilnym


zachowaniom aplikacji w nowym folderze. Będzie potrzebne, kiedy dodamy nawigację
w dół.

Teraz kliknij na listę listBox2 i znowu utwórz metodę obsługi Click.


private: System::Void listBox2_Click(System::Object^ sender, System::EventArgs^ e) {}

Będzie ona działać identycznie, z tym że obsługuje listBox2, etykietę label2 i folder
AktFold2.
private: System::Void listBox2_Click(System::Object^ sender, System::EventArgs^ e) {
if (listBox2->SelectedItem->ToString()=="(..)") {
AktFold2=AktFold2->Parent; //aktualny folder wyżej
label2->Text=AktFold2->FullName;
WyswietlFold(listBox2,AktFold2);
return; }
}

Idziemy w dół
Ze schodzeniem w dół drzewa folderów jest trudniej, bo podfolderów może być kilka
i trzeba otrzymać nazwę klikniętego. Wiemy, że kliknięta linia to opis podfolderu, jeśli
zawiera napis SUB-DIR. A zatem w metodzie obsługującej zdarzenie Click listy będzie-
my to sprawdzać. Jeśli test wypadnie pomyślnie, to z linii w liście trzeba wyłuskać
nazwę folderu. Posłużą do tego metody działające na obiektach System::String, bo
linia jest właśnie takim obiektem.

Ponieważ wiemy, że nazwa ta mieści się w pierwszych 30 znakach, to wyciągniemy


z linii tę liczbę znaków metodą Remove(n). Wyciąga ona pierwsze n znaków z łańcucha.
Następnie pozbędziemy się ewentualnych spacji metodą Trim(). Gotową nazwę pod-
folderu przekażemy do metody GetDirectories(). Poszuka ona folderu o takiej nazwie
w aktualnym folderze AktFold dla danego okna. Używaliśmy już tej metody i wiemy,
że zwraca ona całą tablicę folderów. Ponieważ nie może być dwóch folderów o takich
samych nazwach, to tablica ta będzie zawierała tylko jeden wpis o indeksie 0 i to bę-
dzie nasz kliknięty podfolder. Wpis ten jest obiektem typu DirectoryInfo. Podstawimy
go do uchwytu AktFold. Dalsze działania są takie jak przy poruszaniu się w górę:
wyświetlimy nowy folder i zmienimy napis w etykiecie. Wszystkie powyższe kroki
będziemy realizować w dalszym ciągu metody listBox1_Click() dla pierwszej listy
i listBox2_Click() dla drugiej.

Dla porządku podam całą metodę listBox1_Click(), choć część kodu już mamy:
private: System::Void listBox1_Click(System::Object^ sender, System::EventArgs^ e) {
if (listBox1->SelectedItem->ToString()=="(..)") {
AktFold1=AktFold1->Parent; //aktualny folder wyżej
label1->Text=AktFold1->FullName;
WyswietlFold(listBox1,AktFold1);
return;}
if (listBox1->SelectedItem->ToString()->Contains("SUB-DIR")) {
array<DirectoryInfo^>^ SubDir = AktFold1->GetDirectories(listBox1->
´SelectedItem->ToString()->Remove(30));
374 Microsoft Visual C++ 2012. Praktyczne przykłady

//niby tablica, ale i tak znajdzie tylko jeden folder o zaznaczonej nazwie
AktFold1=SubDir[0]; //aktualny folder niżej
WyswietlFold(listBox1,AktFold1);
label1->Text=AktFold1->FullName;
}
}

I cała metoda listBox2_Click(), która działa na tej samej zasadzie:


private: System::Void listBox2_Click(System::Object^ sender, System::EventArgs^ e) {
if (listBox2->SelectedItem->ToString()=="(..)") {
AktFold2=AktFold2->Parent; //aktualny folder wyżej
label2->Text=AktFold2->FullName;
WyswietlFold(listBox2,AktFold2);
return; }
if (listBox2->SelectedItem->ToString()->Contains("SUB-DIR")) {
array<DirectoryInfo^>^ SubDir = AktFold2->GetDirectories(listBox2->
´SelectedItem->ToString()->Remove(30));
//niby tablica, ale i tak znajdzie tylko jeden folder o zaznaczonej nazwie
AktFold2=SubDir[0]; //aktualny folder niżej
WyswietlFold(listBox2,AktFold2);
label2->Text=AktFold2->FullName;
}
}

Możemy już zmieniać foldery w górę i w dół poprzez klikanie ich myszą. Jeśli chcesz,
możesz to sprawdzić, kompilując i uruchamiając program. Pozostało zaprogramowanie
kopiowania i kasowania plików.

Kopiowanie plików między panelami


Kiedy naciśniesz przycisk Kopiuj->, aktualnie zaznaczony plik z lewego panelu powinien
zostać skopiowany do prawego. Odwrotny kierunek kopiowania będzie po naciśnięciu
przycisku <-Kopiuj. Samo kopiowanie zrealizujemy za pomocą statycznej metody Copy()
klasy File, o której wspominałem w rozdziale o komunikacji z plikami. Kliknij dwukrot-
nie przycisk Kopiuj ->, tworząc metodę obsługi.
private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {}

Nazwę pliku do skopiowania wyciągniemy z aktualnie zaznaczonej linii, podobnie jak ro-
biliśmy to z nazwą katalogu. Najpierw bierzemy pierwsze 30 znaków, a następnie odrzu-
camy spacje. W tym przypadku sama nazwa nas nie zadowala, bo do metody Copy()
potrzebujemy pełnej ścieżki. Pełna ścieżka do pliku to przecież aktualnie wyświetlony
folder, bo plik właśnie w nim się znajduje. W przypadku listBox1 znajdziemy ją we
właściwości FullName obiektu AktFold1. To samo dotyczy folderu docelowego, dla
listBox2 pełna ścieżka to AktFold2->FullName. Po całej operacji wyświetlamy folder
docelowy z nowym plikiem.
Rozdział 22. ♦ Prosty manager plików 375

Oto cała metoda button1_Click():


private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ PlikZrodlo = listBox1->SelectedItem->ToString()->
´Remove(30); //nazwa pliku ze spacjami
KopiujPlik(AktFold1->FullName+"\\"+PlikZrodlo->Trim(),AktFold2->FullName+"\\"
´+PlikZrodlo->Trim()); //po skopiowaniu wyświetl folder
WyswietlFold(listBox2,AktFold2);
}

Kliknij teraz podwójnie przycisk <- Kopiuj. Metodę button2_Click() napisz bardzo
podobnie; działa ona „w odwrotną stronę”.
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ PlikZrodlo = listBox2->SelectedItem->ToString()->
´Remove(30); //nazwa pliku ze spacjami
KopiujPlik(AktFold2->FullName+"\\"+PlikZrodlo->Trim(),AktFold1->FullName+"\\"
´+PlikZrodlo->Trim()); //po skopiowaniu wyświetl folder
WyswietlFold(listBox1,AktFold1);
}

Jak widzisz, nie użyliśmy tu jeszcze funkcji Copy(), ale została ona ukryta w Kopiuj-
Plik(), którą musimy napisać. Zrobiłem tak w celu skrócenia, aby możliwie używać
wiele razy tego samego kodu.

Napisz pustą na razie deklarację metody KopiujPlik(), będzie to kolejna metoda klasy
Form1. Ważne, abyś zadeklarował ją przed button1_Click().
private: System::Void KopiujPlik(System::String^ zrodlo, System::String^ cel) {}

Jeśli plik o takiej samej nazwie istnieje już w folderze docelowym, metoda będzie wy-
świetlać okno dialogowe z zapytaniem, czy na pewno nadpisać plik. Jeśli nie, kopio-
wanie nastąpi od razu.
private: System::Void KopiujPlik(System::String^ zrodlo, System::String^ cel) {
System::Windows::Forms::DialogResult odp;
if (File::Exists(cel)) {
odp = MessageBox::Show(L"Plik docelowy istneje, czy napisać?",L"Plik istnieje",
´MessageBoxButtons::YesNo,MessageBoxIcon::Question);
}
if (odp==System::Windows::Forms::DialogResult::Yes) File::Copy(zrodlo,cel,1);
else
File::Copy(zrodlo,cel);
}

Kasowanie plików
Kasowanie jest prostsze choćby dlatego, że wymaga tylko jednej nazwy pliku. Kliknij
podwójnie przycisk Kasuj->; będzie on kasował plik w liście listBox2. Od razu pokażę,
jak uzupełnić metodę kliknięcia. Myślę, że po zrozumieniu metod kopiowania nie wy-
maga ona komentarza.
376 Microsoft Visual C++ 2012. Praktyczne przykłady

private: System::Void button3_Click(System::Object^ sender, System::EventArgs^ e) {


System::String^ PlikZrodlo = listBox2->SelectedItem->
´ToString()->Remove(30); //nazwa pliku ze spacjami
SkasujPlik(AktFold2->FullName+"\\"+PlikZrodlo->Trim());
WyswietlFold(listBox2,AktFold2);
}

Podobnie zaprogramuj metodę obsługi kliknięcia przycisku <-Kasuj.


private: System::Void button4_Click(System::Object^ sender, System::EventArgs^ e) {
System::String^ PlikZrodlo = listBox1->SelectedItem->
´ToString()->Remove(30); //nazwa pliku ze spacjami
SkasujPlik(AktFold1->FullName+"\\"+PlikZrodlo->Trim());
WyswietlFold(listBox1,AktFold1);
}

Pozostała metoda SkasujPlik(). Tu również korzystamy ze statycznej metody klasy


File o nazwie Delete().
private: System::Void SkasujPlik(System::String^ plik) {
System::Windows::Forms::DialogResult odp = MessageBox::Show
´(L"Czy naprawde skasować pilk " +System::Environment::NewLine+
´plik,L"Potwierdzenie", MessageBoxButtons::YesNo,
´MessageBoxIcon::Question);;
if (odp==System::Windows::Forms::DialogResult::Yes) File::Delete(plik);
}

Napisz tę metodę przed button3_Click().

Prosty manager jest gotowy. Skompiluj i uruchom aplikację, a wszystko powinno działać.
Oczywiście, można go rozbudować, na przykład biorąc pod uwagę, że umożliwia on
działania tylko w obrębie jednej partycji i ma kłopoty z przetwarzaniem nazw więk-
szych niż 30 znaków. Myślę, że to dobry początek własnych poszukiwań.
Skorowidz
A C sterowników PostgreSQL,
288
adapter, 291 CIL, Common Intermediate wierszy, 263
adres serwera bazy, 291 Language, 12 dostęp do
animacja, 316 czas systemowy, 299 elementów tablicy, 206
animacja figury, 161 czasomierz, 301, 363 składowych, 73
ANSI, 140 czcionka, 234 drzewo plików, 372
aplikacja DS, dialog style, 132
z semaforem, 330 dynamiczna
zabezpieczona hasłem, 365
D deklaracja obiektów, 209
aplikacje debugowanie, 24 deklaracja tablic, 210
.NET Framework, 11 definicja tabela łańcuchów, 240
Metro, 11 destruktora, 97 dynamiczne tworzenie
Windows, 16, 18 dziedziczenia, 80 komponentów, 359
argument funkcji, 14 konstruktora, 97 dyrektywa
automatyczne obiektu, 111 #define, 33, 123
dopasowanie komórek, 255 przeciążania, 88 #ifdef, 34
rozpoznawanie typu, 45 struktury, 73, 75 #include, 30
szablonu, 89 #pragma endregion, 290,
300, 314, 329
B zasobu okna, 131
using, 31
definicje metod klasy, 76
baza danych, 277 deklaracja using namespace, 289
postgres, 283 delegata, 64 using namespace std, 94
serwisu samochodów, 283 funkcji, 59 dziedziczenie, 80
PostgreSQL, 278 szablonu, 70
biblioteka tablic, 54, 203, 210 E
.NET Framework, 165 uchwytu do okna, 119
Npgsql.dll, 289 wskaźnika do funkcji, 63 edytor
blok deklaracje dynamiczne, 209 ASCII, 229
catch, 92 delegat ikon, 126
try, 246 FormClosingEventHandler, 366 kolumn, 265
błąd odczytu, 110 delegaty, 64 menu, 191
błędne dane, 246 destruktor, 97, 108 zasobów, 125
błędny przydział pamięci, 85 dodawanie elementy zarządzane, managed,
danych do bazy, 292 12
klas do projektu, 353 enumerator, 206
kolumn, 263 etykiety, 173
378 Microsoft Visual C++ 2012. Praktyczne przykłady

F H kasowanie
danych, 297
folder Npgsql, 288 hasło, 365 plików, 375
formatowanie folderu, 369 hermetyzacja danych, 77, 110 klasa, 15, 75
funkcja, 14, 59 hierarchia array, 210
BeginPaint(), 157 klas, 85 BinaryWriter, 222
Button_GetCheck(), 154 wywołań, 28 Bitmap, 201
button1_Click (), 232, 245 Button, 171
CreateWindow(), 118, 141
DialogBox(), 135 I ContextMenuStrip, 187
Convert, 242
DispatchMessage(), 122 IDE, Integrated Development CultureInfo, 244
DodajTekst(), 151 Environment, 29 DataGridViewComboBox
FromFile(), 201 identyfikator, 117 ´Cell, 273
getch(), 49 identyfikatory ikon, 126 DateTime, 299
GetClientRect(), 162 ikona, 123, 126 Directory, 213
GetCommandLineArgs(), 69 implementacja FTP, 347 DirectoryInfo, 214
GetMessage(), 122 informacje o procesie, 337 enum, 43
GetResponse(), 349 inicjalizacja Environment, 69
InitInstance(), 26, 119 bazy, 281 File, 214
InvalidateRect(), 145, 159 obiektu struktury, 111 FileInfo, 214
LoadCursor(), 117 pól, 75 Form, 172, 359
LoadIcon(), 117 instalacja PostgreSQL, 277
main(), 30, 67 Form1, 196, 296, 322
instalator Visual Studio, 13 HtmlDocument, 342
MyRegisterClass(), 115, 125 instancja, instance, 114
RegisterClassEx(), 118 MenuStrip, 187
instrukcja MessageBox, 225
rozpoznaj(), 346 break, 58
SendMessage(), 143, 150 Monitor, 331
delete, 98
SetTimer(), 160 NpgsqlDataAdapter, 291
do...while, 57
ShowWindow(), 120 okna głównego, 115
for, 57
t_main(), 32 OpenFileDialog, 227
if...else, 56
TranslateAccelerator(), 123 switch, 56 pobierz_wyslij, 355
TranslateMessage(), 122 system(„pause”), 49 StreamReader, 217
tWinMain(), 120 throw, 92 StreamWriter, 221
WndProc(), 135, 154, 163 try, 92 SzukLiczbPierw, 325
WyswietlFold(), 368 while, 57 TextBox, 175
funkcje instrukcje Thread, 323, 325
GDI, 158 iteracji, 57 WNDCLASSEX, 115
główne, 115 warunkowe, 56 klasy
operatorowe, 89 interface bazowe, 81, 85
wirtualne, 83 win32, 113 dziedziczone, 104
zwrotne, 114 managera plików, 367 pochodne, 81, 85
funkcji użytkownika, 286 klawisze skrótów, 128
deklaracja, 59 klucz główny, 285
przeciążanie, 60 kod zarządzany, 12
przekazywanie argumentów, J kodowanie, 140
61 jawna konwersja, 270 ASCII, 213
szablony, 70 język Unicode, 69, 140
wskaźnik na adres, 63 C++/CLI, 12 UTF-7, 224
wywołanie, 60 CIL, 12 kolor
wywołanie przez wskaźnik, 63 SQL, 284 czcionki, 236
etykiety, 274
G K pędzla, 310
tła, 234
GDI, Graphical Device kalkulator, 184 wierszy, 254
Interface, 157 kapsułkowanie, 110
Skorowidz 379

komenda kontrola błędu, 108 mechanizm


cmd, 282 kontrolka garbage collector, 12
CREATE TABLE, 285 ComboBox, 155 precompiled headers, 30, 32
DELETE, 297 DataGridView, 249, 256 przeciążania funkcji, 60
INSERT, 292, 296 GroupBox, 183 wyjątków, 245
psql, 286 Listbox, 356 Menedżer zadań, 321
SELECT, 290–293 Panel, 183 menu, 187, 191
UPDATE, 295–297 Picture Control, 137 menu podręczne, 193
komórka tekstowa, 133 metoda, 15
DataGridViewButtonCell, WebBrowser, 339, 343 Add(), 361
268 konwersja AppendText(), 175
DataGridViewCheckBoxCell, liczby, 147, 246 AutoResizeColumns(), 256
269 typów, 242 AutoResizeRows(), 256
DataGridViewComboBox ze zmianą formatu, 243 button1_Click(), 171, 176,
´Cell, 272 kopiowanie 218, 304, 333, 375
DataGridViewImageCell, obiektu, 100 button2_Click(), 263
270 plików, 374 button3_Click(), 297
DataGridViewLinkCell, 275 wierszy, 264 CancelAsync(), 338
kompilacja warunkowa, 34 Clear(), 293
komponent Close(), 172, 298
BackgroundWorker, 334 L Commit(), 293
MenuStrip, 187 liczba Copy(), 374
OpenFileDialog, 358 parametrów, 115 CreateGraphics(), 303
Timer, 301, 363 zaznaczonych komórek, 259 CreateInstance(), 205
WebBrowser, 339 liczby makr, 124 CreateSpecificCulture(), 244
komponenty tworzone liczby pierwsze, 319, 337 Current(), 206
dynamicznie, 364 link do strony, 174 dataGridView1_
komunikat, message, 16, 114, 139 lista CellEndEdit(), 274
BM_GETCHECK, 154 ComboBox, 157, 191 dataGridView1_Click(), 269
EM_GETSEL, 151 rozszerzeń bazy, 280 dataGridView2_SelectionC
EM_REPLACESEL, 152 rozwijana, 272 hanged(), 295
EM_SETSEL, 151 l-wartość, 46 DrawCurve(), 307
WM_COMMAND, 122, DrawImage(), 313
142, 151, 162 ExecuteNonQuery(), 292, 294
WM_CREATE, 149, 158 Ł Exit(), 331
WM_DESTROY, 122 Fill(), 291
WM_MOUSEMOVE, 146 łańcuch FillPie(), 311
WM_PAINT, 122, 145, 161 formatujący, 247 Form1_Load(), 252, 270,
WM_SETTEXT, 150 połączenia, connection 290, 368
komunikaty string, 291 Form1_Paint(), 315
kontrolki ComboBox, 156 znakowy, 140, 176 FormatWpisFolder(), 370
tekstowe, 151 łączenie się z bazą, 279, 290 FormatWpisPlik(), 371
konfigurator Stack Builder, 280 GetAttribute(), 343
konstruktor, 97 M GetDirectories(), 369, 373
bezparametrowy, 100 GetEncoding(), 218
domyślny, 97 macierz transformacji, 312 GetEnumerator(), 206
klasy, 48 makra standardowe, 37 GetFiles(), 214–216, 369
klasy dziedziczonej, 104 makro GetType(), 193
klasy pochodnej, 104 _MSC_VER, 35 IndexOf(), 240
kopiujący, 100, 106 dla ikony, 124 InitializeComponent(), 364
przenoszący, 102 MAKEINTRESOURCE, 117 InsertCopy(), 264
w szablonie klasy, 107 malowanie pędzlem, 311 Invoke(), 321
kontekst, 157 manager plików, 367 InvokeScript(), 345
kontener GroupBox, 184 maska, 180, 182 KopiujPlik(), 375
380 Microsoft Visual C++ 2012. Praktyczne przykłady

metoda operacji na kolumnach, 262 obsługa


LastIndexOf(), 357 operacji na wierszach, 261 bazy, 298
listBox1_Click(), 373 reakcji, 23 błędów, 92
MoveNext(), 206, 207 rysujące, 305 FTP, 352
odswiez(), 292 statyczne, 31, 78, 179 komunikatów, 139
opcja1ToolStripMenuItem_ szablonu, 91 przycisków Button, 154
Click(), 190, 193 transformujące, 312 wyjątku, 93
Parse(), 180, 218, 243 wirtualne, 83 zdarzenia Click, 192, 216
Peek(), 219 zmieniające zawartość pól, obszar projektowania, 287
przycisk_Click(), 365 295 odczyt
Read(), 219 modyfikator const, 51 z komórki, 257
ReadLine(), 218 modyfikatory typów, 38 z pliku, 49, 223
ReadToEnd(), 217 odnośniki internetowe, 274
Relase(), 328 odśmiecacz, garbage collector,
Relase()., 329 N 208
Remove(n), 373 nagłówek odświeżanie
ReportProgress(), 338 conio h, 49 okna, 145
Reverse(), 205 fstream, 49 rysunku, 314
Rollback(), 293 iostream, 49 okno, 114, 166
rotate(), 317 nawiasy Add Class, 353
Rotate(), 312 klamrowe, 53 Add Resource, 136
RunWorkerAsync(), 336, kwadratowe, 67 DIALOGEX, 134
337 nawigacja po folderach, 372 dialogowe, 131, 133, 135
SetAttribute(), 343 nazwa EditColumns, 265
SetPixel(), 313 bazy danych, 291 edycji, 142
SetValue(), 205 koloru, 117 FontDialog, 235
Show(), 225, 359 projektu, 288 główne, 113, 166, 196
ShowDialog(), 227, 360 przestrzeni, 288 hierarchii wywołań, 28
Start(), 174, 324 użytkownika, 291 IDE, 17
Substring(), 357 klienta FTP, 355
timer1_Tick(), 317 komunikatów, 23
toolStripButton1_Click(), O MessageBox, 225
199 nowego projektu, 20, 21
ToSingle(), 220 obiekt, 16 OpenFileDialog, 227, 228
ToString(), 180, 220, 247 BindingSource, 292 przeglądania folderów, 231
Translate(), 312 Bitmap, 313 SaveFileDialog, 230
WaitOne(), 328 connection, 290 Stack Buildera, 279
watek(), 322 DataSet, 292 tytułowe aplikacji, 363
wątku, 332, 336 Graphics, 303, 305 VC++, 23
Write(), 220 HtmlDocument, 342 wyboru czcionki, 184, 235
WriteLine(), 31, 220 HtmlElement, 344 wyboru folderu, 233
wyslij(), 358 Image, 308 wyboru koloru, 233
wyswietl(), 206 inscomm, 292 z komunikatami, 144
WyswietlFold(), 368 Matrix, 312 zapisu pliku, 230
metody NpgsqlCommand, 292 operator
działające na tablicach, 204 okno, 331 %, 208
klasy System selectcomm, 293 &, 84
String, 240 Semaphore, 328 <<, 32
komponentu this, 196 =, 101
BackgroundWorker, 335 Thread, 321 >>, 218
obiektu FtpWebRequest, 348 TimeSpan, 300 delete, 55, 209
obiektu WebBrowser, 341 obiekty graficzne na stronie, dostępu ., 73
odczytu pliku, 217 343 dostępu ->, 73, 98, 173
odczytujące, 223 obliczanie średniej, 335 gcnew, 55, 209, 359
Skorowidz 381

new, 55, 98, 209 .suo, 24 przełączanie kolorów, 198


przekierowania, 48 .vcxproj, 25 przepuszczanie wątków, 329
uzyskania wielkości .vcxproj filters, 25 przestrzeń nazw, 16, 31, 94
obiektu, 47 binarne, 213 System::Threading, 328
zasięgu ::, 78, 94 tekstowe, 213 System::Net, 348
operatory pobieranie System::IO, 348
arytmetyczne, 47 danych z komponentów, przesyłanie danych, 362
dostępu, 47 362 przetwarzanie
jednoargumentowe, 47 plików, 350, 356 komunikatów, 120
logiczne, 47 z serwera, 349 stron, 342
porównania, 47 podgląd znaczników kontekstu, przycisk, 171
przypisania, 47 344 <-Kasuj, 376
optymalizacja pamięci, 208 podłączenie referencji do <-Kopiuj, 374
biblioteki, 289 Button, 153
podświetlanie, 25 Kopiuj->, 374
P pola toolStripButton, 200
panel maskowane, 181 przyciski
Properties, 138 statyczne, 78 domyślne, 133
Toolbox, 137 pole, 15 graficzne, 201
z kontrolkami, 137 tekstowe TextBox, 175, opcji, 184
parametry 180, 184, 237 w komórkach, 268
linii komend, 70 typu SERIAL, 286 przyporządkowanie metody do
metody Show(), 225 wyboru, 154, 269 zdarzenia, 364
pasek predykat, 205
binarny, 205
niewidocznych
unarny, 205
R
komponentów, 287
ToolStrip, 197 prekompilowane nagłówki, 31 RC, Release candidate, 9
paski narzędzi, 197 PRIMARY KEY, 285 referencje, 50
pędzle, 310 procedura referencje do r-wartości, 51
pętla komunikatów, 114, 122 okna, 120 rejestracja klasy, 114
pióro Pen, 308 WndProc(), 143 rodzaje menu, 187
plik program rodzaje piór, 310
Form1.h, 31, 213 createdb, 283 rola, role, 283
okienko rc, 124 GIMP, 124 rola administratora, 283
okno.cpp, 114 initdb, 282, 283 rozmiar
pobierz_wyslij.cpp, 354 Paint, 168 komórek, 255
pobierz_wyslij h, 353 pg_ctl, 282 tabeli, 258
pomoc html, 340 psql, 286 r-wartość, 46
README, 357 projekt okienkowy rysowanie, 303
resource h, 124 C++/CLI, 21, 29 figur, 160
skrypt1.htm, 346 win32, 21, 29, 114 linii, 304
stdafx h, 31 protokół FTP, 347 punktów, 313
windows h, 115 przeciążanie tekstu, 306
WindowsFormApplication1. funkcji, 60 trwałe, 314
cpp, 169, 288 konstruktorów, 99 wykresów, 307
winuser h, 117 operatorów, 88 rysunek na przycisku, 200
pliki przedstawianie danych, 249 rzutowanie
.cpp, 25, 30 przekazywanie argumentów const_cast, 40, 51
.exe, 216 przez wartość, 61 dynamic_cast, 41
h, 30 za pomocą adresu, 61 dynamiczne, 85
.ico, 24, 124 przekazywanie parametrów, 321 jawne, 44
.rc, 24 do funkcji main(), 68 niejawne, 40
.sdf, 24 do metody wątku, 323 safe_cast, 41
.sln, 24 z kontrolą typu, 324 static_cast, 39
382 Microsoft Visual C++ 2012. Praktyczne przykłady

rzutowanie DS_MODALFRAME, 132 obiektu Graphics, 303


statyczne, 85 DS_SETFONT, 132 obiektu klasy potomnej, 105
typów, 39 WS_OVERLAPPED okna aplikacji, 168
w dół, 85 ´WINDOW, 119 okna edycji, 142
w górę, 85 Styl Metro, 11 okna dialogowego, 133, 135
style projektu, 19
łańcucha znakowego, 244 przycisku, 133
S okna, 118 struktury folderów, 282
sekcje krytyczne, 331, 334 sygnalizacja wystąpienia tabeli, 271
semafory, 328 komunikatu, 145 tabeli dynamicznej, 267
semantyka przenoszenia, 102 szablon timera, 160
serwer FTP, 348 klasy, 89-91 tymczasowego obiektu, 104
pobranie pliku, 350 projektu okienkowego, 19 wątku, 321, 357
wysyłanie pliku, 351 funkcji, 70 typ NumberStyles, 244
siatka DataGridView, 262, 271 typy
skróty klawiaturowe, 195 Ś danych, 178
skrypt zasobów, 117, 124 danych w PostgreSQL, 284
skrypty JavaScript, 345 środowisko komórek w tabeli, 265
słowo .NET Framework, 12 wskaźnikowe, 51
delegate, 64 IDE, 22, 29 wyliczeniowe, 41
DIALOGEX, 131 zmiennych, 37
Icon, 126 T
instancja, 114 U
MENUITEM, 128 tabela
mutable, 67 DataGridView, 252, 287 uchwyt, 208
operator, 88 dynamiczna, 266 uchwyt do obiektu semafora,
słowo kluczowe odnośników internetowych, 329
auto, 45 276 uruchamianie bazy, 281
const, 41 znaków TCHAR, 147 ustawienie Debug, 24
POPUP, 128 tablica, 52, 203 usuwanie
static, 78 dwuwymiarowa, 54 obiektów, 105
struct, 110 jednowymiarowa, 53 wiersza, 263
template, 89 tekst, 239
typeid, 45 kopiowanie, 239
virtual, 83 wklejanie, 239 V
sortowanie, 260 wstawianie, 241 Visual Studio, 11
specyfikator public:, 110 wycinanie, 239
sprawdzanie poprawności timer, 160, 317
hasła, 366 tryb W
SQL, Structured Query dialogowy, 362
wartość
Language, 284 dziedziczenia, 80
NULL, 87, 103
sterowniki PostgreSQL, 288 kompilacji, 24
void, 63
sterta, 55 Release, 24
zwracana, 14
struktura, 73 wizualny, 165
wątek, 319
DirectoryInfo, 369 zdarzeń, 23
pobieranie plików, 356
do przechowywania tworzenie
bazy danych, 285 wysyłanie plików, 357
danych, 281 wczytywanie pliku do pola, 228
procedury okna, 121 dynamiczne okien, 359
ikony, 125 wersja kompilatora, 35
projektu, 24 wersja RC VC++ 2012, 9
strumień cout, 32 kalkulatora, 184
menu, 130, 191 wiązanie późne, 83
styl widok
BS_AUTOCHECKBOX, menu głównego, 189
menu podręcznego, 194 projektowania aplikacji, 256
154 RESOURCE VIEW, 125
DS_FIXEDSYS, 132 obiektu, 115
Skorowidz 383

WinAPI, 113 wskaźniki zapytania z linii komend, 286


właściwości, properties, 16 do funkcji, 62 zarządzanie plikami, 367
klasy FileInfo, 215 do obiektu, 86 zasoby
komórki do stałej, 51 ikon, 123
DataGridViewComboBox na klasy, 85 menu, 128
´Cell, 272 na zmienne, 63 zaznaczanie
DataGridViewImageCell, współrzędne wskaźnika, 148 komórek, 258
270 wstawianie siatki, 266, 271, 307 komponentu, 138
DataGridViewLinkCell, wyjątek, 92, 296 wierszy, 259
275 wyliczenia, 42 zdarzenia, 16, 165, 171
komponentu, 170 wyłączanie usługi bazy, 281 zdarzenia komponentu
BackgroundWorker, 335 wyrażenia lambda, 65 BackgroundWorker, 334
FolderBrowserDialog, 231 wysyłanie zdarzenie
MaskedTextBox, 181 komunikatów, 140 CellEndEdit, 273
MenuStrip, 188 plików, 351, 357 Click, 171, 259, 349, 360
ToolStripMenuItem, 195 wyszukiwanie, 26 CurrentCellChanged, 256
kontrolki liczb pierwszych, 324 DoWork, 336
CheckBox, 183 plików, 214 FormClosing, 301, 366
DataGridView, 249, 257 w tablicy, 206 KeyDown, 341
RadioButton, 183 znaków, 240 Load, 252, 264, 330, 365
TextBox, 175 wyświetlanie Paint, 314
WebBrowser, 339 argumentów linii komend, 69 RunWorkerCompleted, 336
obiektu czasu, 302 Tick, 316
DataGridViewCell, 251 figur, 158 WM_TIMER, 160
DataGridViewRow, 250 grafiki w komórkach, 272 zmiana
DateTime, 299 katalogu serwera FTP, 355 danych w bazie, 295
FtpWebRequest, 347 liczb pierwszych, 322 liczby komórek, 261
HtmlDocument, 342 nazwy przeglądarki, 346 zmienianie
Pen, 308 obrazka, 308 wielkości okna, 195
Timer, 301 odnośników, 344 wyglądu tabeli, 253
okna, 137 okien, 359 zmienna, 14
aplikacji, 166 okna dialogowego, 226 CDNumber, 296
ColorDialog, 233 stron, 340 char, 38
FontDialog, 235 ścieżki, 232 double, 38
OpenFileDialog, 228 tekstu, 175, 306 float, 38
SaveFileDialog, 230 tekstury, 313 message, 121
paska ToolStrip, 197 w etykiecie, 190 TCHAR, 140
pędzla TextureBrush, 310 wartości zmiennych, 179 WCHAR, 140
pola TextBox, 237 zawartości folderu, 367, 372 zmienne sterujące, 327
właściwość zawartości pliku, 218 znacznik (..), 372
DataGridViewCellStyle, 253 wywołanie funkcji przez znak
ImageScalingSize, 200 wskaźnik, 63 *, 208
WM, Windows Message, 122 ^, 208
wprowadzanie danych, 176, &, 66
242, 294
Z @, 293
WS, window style, 118 zakończenie wątku, 326 =, 66
wskaźnik, 50 zamiana liter, 238 komentarza, 333
myszy, 146 zapis do pliku, 48 zachęty, 285
postępu, 337 binarnego, 222
this, 79, 304 tekstowego, 220

You might also like