Professional Documents
Culture Documents
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
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.
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.
Rysunek 1.1.
Ekran powitalny
instalatora Visual
Studio 2012 RC
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.
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:
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
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.
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.
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.
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
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
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
Rozwiązanie
Z menu wybierz opcję File/New/Project. Pojawi się okno jak na rysunku 1.4.
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
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.
Przykład 1.3
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.
Przykład 1.4
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.
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
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.
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
Oprócz tego folder zawiera kilka plików nagłówkowych, tekstowy plik z opisem, a także
pewne pliki zależne od rodzaju projektu.
Przykład 1.5
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.
Definicja wygląda bardzo podobnie, tylko po pierwszej linii następuje kod funkcji (ciało)
ujęty w nawiasy klamrowe.
Przykład 1.6
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()
Przykład 1.7
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
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
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"
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.
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.
Przykład 2.1
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.
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");
return 0;
}
Rozdział 2. ♦ Struktura programów C++ i C++/CLI 33
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.
Przykład 2.2
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>
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;
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.
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
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"
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.
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.
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;
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
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
//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.
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;
Przykład 2.5
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};
Przykład 2.6
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};
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;
}
Zauważ, że polecenie wypisania elementu wyliczenia nie wypisało liczby, ale nazwę
elementu wyliczenia.
Przykład 2.7
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};
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.
Przykład 2.8
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
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.
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
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
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>
return 0;
}
Przykład 2.10
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>
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;
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;
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;
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.
Przykład 2.11
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>
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
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 . . .
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];
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]
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];
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
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.
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 {}.
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()?
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;
}
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.
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.
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
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()
}
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
Przykład 3.1
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>
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 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ź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
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>
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().
Przykład 3.3
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);
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
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));
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 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.
Przykład 3.4
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;
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
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.
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
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
Przykład 3.6
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"
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
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
Rozwiązanie
Potrzebny będzie projekt aplikacji konsolowej win32 z przykładu 1.1. W bloku nagłów-
ków wpisz dyrektywę:
#include <iostream>
Niestety, nie zawsze będzie to miało sens, a szablon nie ma żadnej kontroli błędów, jest to
tylko przykład.
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;
};
Przykład 4.1
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;
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;
}
Przykład 4.2
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
zbior1 dane1={4,5.6};
std::cout<<"dane1.a1="<<dane1.a1<<std::endl;
system("pause");
return 0;
}
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 }
Przykład 4.3
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>
class prosta {
public:
int x,y;
};
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
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>
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).
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
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>
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
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;
}
Przykład 4.6
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>
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;
};
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
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>
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.
class aparat {
protected:
int masa;
float ognisko;
bool flash;
};
82 Microsoft Visual C++ 2012. Praktyczne przykłady
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::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;
}
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>
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;}
};
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>
class 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;
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;
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();
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
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>
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;
};
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
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;}
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
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()
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
Po klamrze ze średnikiem zamykającej ciało klasy piszemy definicje metod jako szablony
funkcji:
template <class T> T szablon<T>::podajDana()
{
return _dana;
}
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
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>
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
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
}
Przykład 4.14
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>
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;
}
Widać, że mimo identycznych nazw nie ma konfliktu i możliwe jest wywołanie od-
powiednich konstruktorów.
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) {}
Przykład 5.1
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
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
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ę.
Przykład 5.2
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;
_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
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";
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;
}
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
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
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;
_getch();
return 0;
}
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;
};
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
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;
}
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
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
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];
}
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
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);
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;
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.
Przykład 5.8
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>
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.
Przykład 6.1
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.
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
Przykład 6.2
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
return RegisterClassEx(&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;
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);
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
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);
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));
Style mogą być stosowane łącznie. Suma wszystkich powyższych stylów jest zawarta
w stylu WS_OVERLAPPEDWINDOW.
Przykład 6.3
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().
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.
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
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);
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)
Przykład 6.4
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
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);
}
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"
Przykład 6.5
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
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
//
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));
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
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.
Rysunek 6.6.
Struktura projektu
VC++ 2012
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.
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
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
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
Jeśli przy działającym programie naciśniesz Alt+P, to otworzy się ten panel menu.
Przykład 6.8
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
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.
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
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.
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.
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ść
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.
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
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
//
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
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
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
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
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
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.
Skompiluj i uruchom program. Wybierz opcje Help/About. Okno powinno być takie
samo jak w poprzednim przykładzie.
Rozdział 7.
Obsługa komunikatów
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
a dla Unicode:
WCHAR znaki[] = L"Łańcuch znakowy";
Przykład 7.1
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);
Rysunek 7.1.
Okno aplikacji do
testowania komunikatów
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
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);
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)
Na razie nic nie wpisuj, ponieważ tylko analizujemy kod. Wiadomość WM_COMMAND jest
przetwarzana od razu w pierwszym paragrafie instrukcji switch.
case WM_COMMAND:
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:
dopisz linię:
DodajTekst(hEdit, _T("Komunikat WM_COMMAND About - okno główne\r\n"));
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
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;
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.
Przykład 7.4
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;
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ść.
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
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
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
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.
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
Przykład 8.1
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);
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);
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;
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
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
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
Rysunek 8.2.
Lista wyboru ComboBox
z opcjami
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);
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
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ć.
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
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.
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;
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
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++.
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
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.
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.
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
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.
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.
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.
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().
#include "stdafx.h"
#include "Form1.h"
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;
Po kompilacji ukaże się główne okno w postaci elipsy. Niestety, zawiera ono ramkę,
która psuje efekt.
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.
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
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) {
}
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();
}
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.
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.
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
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
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.
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.
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
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.
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
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++.
Przykład 9.7
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.
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
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.
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.
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
Rozwiązanie
Otwórz aplikację z przykładu 9.7 za pomocą opcji File/Open/Project Solution.
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.
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++.
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.
Przykład 9.10
Rozwiązanie
Utwórz projekt aplikacji według przykładu 1.4. Wstaw w okno etykietę Label i przycisk
Button.
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
Przykład 9.11
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
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.
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.
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
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
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
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
Przykład 10.1
Rozwiązanie
Utwórz nowy projekt aplikacji.
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.
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.
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
Rysunek 10.3.
Menu z różnymi
komponentami
Rozwiązanie
Do wykonania tego zadania będzie potrzebna aplikacja z poprzedniego przykładu.
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.
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
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.
Rysunek 10.4.
Projektowanie
menu podręcznego
Dodaj do menu opcje Opcja1, Opcja2 i Opcja3 oraz Koniec, jak na rysunku 10.4.
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
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.
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.
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.
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.
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--;
}
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.
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"
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;
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).
Możesz już uruchomić aplikację — pierwsza etykieta będzie zmieniała kolory zgodnie
z przyciskami.
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.
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;
}
}
Przykład 10.7
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).
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
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}};
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);
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.
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
Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w jej oknie przycisk
Button oraz pole tekstowe TextBox.
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); }
Przykład 11.2
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.
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.
Predicate<System::Int32>^ maska =
gcnew Predicate<System::Int32>(czy_parzysta);
tablica2=System::Array::FindAll(tablica1,maska);
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;
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
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.
Przykład 11.4
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.
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.
Ś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
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.
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
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.
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
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.
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);
}
Przykład 12.3
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.
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"));
Zamiast czytać od razu cały plik, można go odczytywać linia po linii za pomocą metody
ReadLine().
Przykład 12.4
Rozwiązanie
Otwórz projekt z poprzedniego przykładu.
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);
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.
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
Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do jej okna głównego
jeden przycisk Button.
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
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);
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
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.
Przykład 12.9
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 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);
Metoda Show() zwraca parametr typu DialogResult oznaczający, który przycisk nacisnął
użytkownik. Możliwe wartości parametrów zestawia tabela 13.2.
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
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”.
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
Przykład 13.2
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.
Rysunek 13.1.
Najprostszy edytor
ASCII
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
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
Uruchom aplikację. Będzie działać identycznie jak poprzednio, ale została napisana
inaczej. Zapisz ją, ponieważ przyda się w następnym przykładzie.
Przykład 13.4
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();
}
}
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.
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.
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.
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
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
Najbardziej interesujące właściwości i metody pola TextBox przedstawiają tabele 14.1 i 14.2.
Przykład 14.1
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.
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
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.
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
Przykład 14.3
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
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.
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.
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().
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.
Przykład 14.7
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
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.
Przykład 14.8
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().
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.
Rysunek 14.2.
Działanie aplikacji
do konwersji liczb
Rozdział 15.
Komponent tabeli
DataGridView
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
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ć.
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.
Przykład 15.2
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.
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
Przykład 15.4
Rozwiązanie
Utwórz projekt aplikacji okienkowej według przykładu 1.4 i wstaw do okna komponent
DataGridView.
Przykład 15.5
Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i umieść w nim komponent Data-
GridView.
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.
Przykład 15.6
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.
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.
Przykład 15.8
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.
Przykład 15.9
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.
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.
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));
}
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
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).
Przykład 15.12
Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, wstaw do okna siatkę DataGri-
dView, pole TextBox oraz przycisk Button.
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.
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.
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
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).
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;
}
Przykład 15.14
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
Rysunek 15.4.
Tabela DataGridView
z różnymi typami
komórek
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.
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;
}
}
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.
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).
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.
Przykład 15.17
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.
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;
}
Rysunek 15.6.
Wyświetlanie grafiki
w komórkach
DataGridViewImageCell
Przykład 15.18
Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4, zawierający tabelę DataGridView
i trzy etykiety Label.
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;
}
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;
}
}
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 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);
}
Rysunek 15.7.
Tabela odnośników
internetowych
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
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.
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
Przykład 16.2
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
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
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.
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
Przykład 16.4
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.
Naciśnij Enter.
cd c:\Program files\PostgreSQL\9.0\bin
Naciśnij Enter.
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>
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, …);
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.
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);
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
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.
na:
using namespace PR_16_5;
namespace WindowsFormApplication1 {
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ź.
Przykład 16.6
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
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
Dowolny ciąg znaków można zastąpić *. Najkrótsze zapytanie o wszystkie dane z tabeli:
SELECT * FROM plyty
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
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.
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);
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];
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;
}
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.
Przykład 16.8
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) {
}
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);
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
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.
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
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) {}
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();
}
Kasowanie danych
Do kasowania danych służy komenda SQL DELETE.
DELETE FROM NazwaTabeli WHERE NazwaKolumnyIdentyfikacji=warunek;
Przykład 16.10
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;
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
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.
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);
}
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.
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.
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ę.
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.
Przykład 18.1
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);
}
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
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.
Rysunek 18.2.
Rysowanie tekstu
w oknie aplikacji
Przykład 18.3
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
punkty[i].Y=(this->Height-30)-Convert::ToSingle(dataGridView1->Rows[i]->
´Cells[1]->Value);
}
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.
Przykład 18.5
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);
}
Rysunek 18.4.
Działanie różnych
rodzajów piór
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 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
Przykład 18.7
Rozwiązanie
Do nowego projektu aplikacji utworzonego według przykładu 1.4 wstaw przycisk
Button.
Rysunek 18.6.
Wyświetlanie
tekstury z obrotem
Przykład 18.8
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);
}
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;
}
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
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();
}
System::Drawing::Drawing2D::Matrix^ macierz =
gcnew System::Drawing::Drawing2D::Matrix();
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);
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
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);
}
}
Przykład 19.2
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++;
}
}
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.
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.
Przykład 19.3
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);
Można już uruchomić program. Po naciśnięciu przycisku liczby pojawiają się w polu,
a okno daje się swobodnie przesuwać i zakrywać.
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.
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
Rozwiązanie
Otwórz aplikację z poprzedniego przykładu.
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});
}
}
Rysunek 19.1.
Aplikacja wyszukująca
liczby pierwsze
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.
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();
}
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.
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
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;
}
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);
}
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).
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();
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
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
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;
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
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;
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()+","));
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);
Rysunek 19.3.
Wynik
działania aplikacji
bez sekcji krytycznej
try {
for (System::Int32 j=1;j<5;j++)
this->Invoke(wyswietlDelegat1, safe_cast<System::Object^>
´(nr+"_"+j.ToString()+","));
}
finally
{Monitor::Exit(this);}
}
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.
Przykład 19.9
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.
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
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.
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 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;
}
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;
}
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.
Przykład 20.1
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.
Rysunek 20.1.
Wyświetlanie stron
WWW w komponencie
WebBrowser
Przykład 20.2
Rozwiązanie
Zbuduj program identyczny jak w poprzednim przykładzie, zmień jedynie adres do-
kumentu.
Uri^ adres= gcnew Uri("c:\\aplikacja\\pomoc.html");
Klasa kontrolki WebBrowser posiada też wiele metod, które umożliwiają nawigację po
stronach WWW. Przedstawia je tabela 20.2.
Przykład 20.3
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.
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);
}
Przykład 20.4
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
textBox2->AppendText(System::Environment::NewLine);
}
}
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
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
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.
Wyświetlanie
odnośników
ze strony Web
Przykład 20.7
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.
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.
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
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
Ustaw właściwość Multiline pola tekstowego textBox2 na true i powiększ je tak, aby
mogło wyświetlić kilka linii tekstu.
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
Przykład 20.9
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.
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
Przykład 20.10
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.
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.
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.
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^);
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
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
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;
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.
Przykład 20.13
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.
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.
Przykład 20.14
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();
}
}
Przykład 21.1
Rozwiązanie
Utwórz nowy projekt aplikacji według przykładu 1.4 i wstaw do niego przycisk Button.
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
Rozwiązanie
Utwórz taką samą aplikację jak w poprzednim przykładzie.
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
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.
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.
Okna tworzone dynamicznie można też wykorzystać jako okna do wyświetlania tytułu
aplikacji lub do zabezpieczania programu hasłem.
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();
}
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
Przykład 21.6
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();
}
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.
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.
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;
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]));
}
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
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";
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;
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";
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.
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.
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;
}
}
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.
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
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
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