This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Ali rights reserved. No part ofthis book may be reproduced or transmitted in any form or by any
means, electronic or mechanical, including photocopying, recording or by any information storage
retrieval system, without permission from the Publisher.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej
publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną,
fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje
naruszenie praw autorskich niniejszej publikacji .
Wszystkie znaki ich właścicieli.
występujące
w
tekście są zastrzeżonymi
znakami firmowymi
bądź
towarowymi
The Wrox Brand trade dres s is a trademark ofWiley Publishing, Inc. in the United States and/or other
countries. Used by permission.
Visual C++ is registered trademark of Microsoft Corporation in the United States and/or other
countries. Ali other trademarks are the property of their respective owners.
The Wrox Brand jest zastrzeżonym znakiem towarowym Wiley Publishing, Inc . na terenie Stanów
Zjednoczonych i innych krajów. Wykorzystano za zgodą właściciela .
Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje
były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialnościani za ich wykorzystanie,
ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz
Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody
wynikłe z wykorzystania informacji zawartych w książce .
Wydawnictwo HELION
ul. Kościuszki lc , 44-100 GLIWICE
tel. 032 231 22 19, 032 230 98 63
e-mail : he/ion@he/ion.p/
WWW : http://he/ion.p/(księgarnia internetowa, kata
Drogi Czytelniku!
chcesz ocenić tę książkę, zajrzyj pod adres
http://he/ion.p//user/opinie?vcpppo Możesz tam wpisać swoje uwagi, spostrzeżenia , rec Jeżeli
Printed in Poland .
Ks iążkę dedykuję A/exandrowi Gi/bey owi.
Z n iecierpli woś cią oczekuję na jego komentarze,
a/e pewnie będę musial j eszcze troch ę p oczekać.
Podziękowania Za włożony wysiłek i wsparcie chciałbym podziękować wydawnictwu John Wiley & Sons oraz zespołowi edytorskiemu i produkcyjnemu wydawnictwa Wrox . W szczególności chciał bym tutaj wyróżnić redaktora naczelnego ds. rozwoju - Kevina Kenta, który wspierał mnie od samego początku do końca pisania tej książki. Chciałbym również podziękować edytorowi technicznemu - Johnowi Muellerowi za dokładne przeczytanie mojego tekstu i poprawienie, mam nadzieję, większości popełnionych przeze mnie błędów, wypróbowanie wszystkich fragmentów kodu oraz konstruktywne komentarze, które uczyniły tę książkę o wiele lepszą. Na koniec chciałbym podziękować mojej żonie , Eve, za jej cierpliwość , pogodę ducha oraz wspieranie mnie podczas długiego okresu przelewania moich myśli na karty tej książki. Jak już wielokrotnie wspominałem wcześniej - bez niej książka ta nie mogłaby powstać.
Spis treści oautorze
19
Wstęp
21
Rozdział
1. Programowanie przy użyciu Visual C++ 2005
Środow is ko programistyczne .NET
Common Łanguage Runtime (CLR) Pisanie programów w C++ Nauka programowania dla systemu Windows Nauka C++ Standardy C++ Aplikacje działające w trybie konsoli Koncepcje programowania w systemie Windows Czym jest zintegrowane środow isko programistyczne Składn iki systemu Używanie IDE Opcje paska narzędz i Dokowalne paski narzędzi Dokumentacja Projekty i rozwiązania Ustawianie opcji w Visual C++ 2005 Tworzenie i uruchamianie programów dla Windowsa Tworzenie aplikacji Windows Forms Podsumowan ie Rozdział 2.Dane. zmienne i dZiałania
arytmetyczne
Struktura programu w C++ Funkcja mainO Instrukcje programu Białe znaki Bloki instrukcj i Programy konsolowe generowane automatyczn ie Definiowanie zmiennych Zasady nadawania nazw zmiennym Deklarowanie zmiennych Wartość początkowa zmiennej Podstawowe typy danych Zmienne całkowite Znakowe typy danych Modyfikatory typu integer Typ logiczny
27
27
28
29
30
31
32
32
33
35
35
37
3 8
39
39
40
54
55
58
61
63
64
71
72
74
75
75
76
77
78
79
80
80
81
82
83
6
Visual C++ 2005. Od podstaw Typy zmienno pozycyjne Literały
Definiowanie synonimów typów danych Zmienne o okre ś lonych zbiorach wartości Określanie typu st-ałych wyliczeniowych Podstawowe operacj e wejś c ia -wyj śc ia Wprowadzanie danych z klawiatury Wysyłanie danych do wiersza poleceń Formatowan ie wysyłanych danych Kodowanie znaków specjalnych Wykonywanie obliczeń w C++ Instrukcja przypisania Działania arytmetyczne Obliczanie reszty Modyfikowanie zmiennej Operatory inkrem entacji i dekrementacji Kolejność wykonywania obliczeń Typy zmiennych i rzutowanie Zasady rzutowania operandów Rzutowanie w instrukcjach przypisania Rzutowanie jawne Rzutowanie w starym stylu Operatory bitowe Czas życi a i zas ięg zmiennych Zmienne automatyczne Pozycjonowanie deklaracj i zmiennych Zmienne globalne Zmienne statyczne Przestrzenie nazw Deklarowanie przest rzeni nazw Wielokrotne deklar acje przestrzeni nazw Programowanie w C++jCLI Fundamentalne typy danych w C++jCLI Wysyłan ie danych do wiersza poleceń w C++jCLI C++jCLI - formatowanie danych wyjściowych C++jCLI - wprowadzanie danych z klawiatury Bezpieczne rzutowanie Wyliczenia w C++jCLI Podsumowanie Ćwiczenia Rozdział 3.Decyzje i pętle
Porównywanie wartości Instrukcja warunkowa if Zagnieżdżanie inst rukcji warunkowych if Rozszerzona instrukcja warunkowa if Zagnieżdżanie instru kcji warunkowych lf-else Operatory logiczne i wyrażen ia Operator warunkowy Instrukcja switch Przejśc ie bezwarunkowe
:
84
85
86
87
88
89
89
90
91
92
94
9 4
95
100
101
102
104
106
106
107
108
109
109
116
116
119
119
123
123
125
126
128
128
133
133
136
137
138
141
142
145
145
147
148
150
152
154
158
159
162
Spis Ireści Powtarzanie bloków instrukcji Czym jest pętla Różne sposoby użycia pętli for Pętla while Pętla do-while Zagnieżdżanie pętli
Programowanie w C++jCLI Pętla for each Podsumowan ie Ćwiczenia
Rozdział 4. Tablice, łańcuchy
znaków i wskaźniki
Obsługa
wielu wartości danych tego samego typu Tablice Deklarowanie tablic Inicjalizacja tablic Tablice znakowe oraz obsługa łańcuchów Tablice wielowymiarowe Pośredni dostęp do danych Czym jest wskaźnik Deklarowanie wskaźników Używanie wskaźników , Inicjalizowanie wskaźników Operator sizeof Stałe wskaźniki oraz wskaźniki do stałych Wskaźniki i tablice Dynamiczne przydzielanie pamięci Pam ięć wolna, czyli sterta : Operatory new i delete Dynamiczne przydzielanie pamięc i tablicom Dynamiczne przydzielanie pamięci tablicom wielowymiarowym Używanie referencji Czym jest referencja Deklarowanie i inicjalizowanie referencji Programowanie w C++JCLI Uchwyty śledzące Tablice CLR Łańcuchy
Referencje
śledzące
Wskaźniki wewnętrzne
Podsumowan ie Ćwiczenia
Rozdział5. Wprowadzanie struktury do programu Zrozumieć
funkcje Do czego potrzebne są funkcje Struktura funkcji Używanie funkcji Przekazywanie argumentów do funkcji Mechanizm przekazywania przez wartość Wskaźniki jako argumenty funkcji
7 163
163
165
174
176
177
180
184
187
187
189
190
190
191
194
196
200
203
203
204
205
207
213
215
217
224
224
224
225
228
229
229
229
230
231
233
248
258
258
261
263
265
266
267
267
269
273
274
275
8
Visual C++ 2005. Od podstaw Przekazywa nie tabl ic do funkcji Referencje jako argumenty funkcj i Zastosowanie modyfikatora const Argumenty funkcji malm) Akceptowanie zmiennej liczby argumentów funkcji Zwracanie wartości przez funkcję Zwracanie wskaźnika Zwracanie referencji Zmienna statyczna w funkcji Wywołania funkcj i rekurencyjnej Stosowanie rekurencji Programowanie w C++/CLI Funkcje przyjmujące zmienną liczbę argumentów Argumenty funkcji maint) Podsumowan ie
277
281
283
285
287
289
289
292
295
297
300
300
301
302
303
304
Ćwiczenia
Rozdział 6. Ostrukturze programu -
ciąg
dalszJ
305
Wskaźniki
do funkcji Deklarowan ie wskaźników do funkcji Wskaźnik do funkcji jako argument Tablice wskaźników do funkcj i Inicjalizowan ie parametrów funkcji
Wyjątki Wywoływanie wyjątków
Przechwytywanie wyjątków MFC Obsługa błędów przydzielania pamięci Przeładowywaniefunkcji Czym jest przeładowywaniefunkcji Kiedy stosować przeładowywanie funkcji Szablony funkcji Stosowanie szablonu funkcji Przykład używania funkcji Implementacja kalkulatora Usuwanie spacji z łańcucha Obliczanie wartości wyrażen ia Obliczanie wartości składn ika Analizowanie liczby Składanie całego programu Rozszerzanie programu Wydobywanie podłańcucha Uruchamianie zmodyfikowanego programu Programowanie w C++/CLI Funkcje generyczne Kalkulator CLR Podsumowanie Obsługa wyjątków w
Ćwiczenia
:
306
306
309
311
312
314
316
316
318
318
320
321
323
323
324
326
326
330
330
333
334
337
339
340
343
343
345
351
357
358
Spis treści Rozdział 7. Deliniowanie własnych
Iypów danych
Struktury w języku C++ Czym jest struktura Definiowanie struktury Inicjali zowanie struktury Uzyskiwanie dostępu do pól struktury Pomoc mechanizmu Intellisense w pracy ze strukturami Struktura RECT Używanie wskaźników ze strukturam i Typy danych , obiekty, klasy i egzemplarze Zrozumieć klasy Definiowanie klasy Deklarowanie obiektów klasy Uzyskiwanie dostępu do zmiennych składowych klasy Funkcje składowe klasy Umiejscowienie definicji funkcji składowej Funkcje inline Konstruktory klas Czym jest konstruktor Konstruktor domyślny Przypisywanie domyślnych wartośc i parametrom umieszczonym w klasach Używanie listy inicjalizacyjnej w konstruktorze Prywatne składowe klasy Uzyskiwanie dostępu do prywatnych zmiennych składowych klasy Przyjaciele klasy Domyślny konstruktor kopiujący Wskaźnik th is Stałe obiekty klasy Stałe funkcje składowe klasy Definiowan ie funkcji składowej poza klasą Tablice obiektów klasy Składowe statyczne klasy Statyczne zmienne składowe klasy Statyczne funkcje składowe klasy Wskaźniki i referencje do obiektów klasy Wskaźn ik i do obiektów Referencje do obiektów Programowanie w C++/CLI Definiowanie typów klas wartości Definiowanie typów referencyjnych Właściwości klasy , Pola initonly Konstruktor statyczny Podsumowanie Ćwiczenia
9 359
360
360
360
361
361
365
366
367
369
372
373
373
374
376
378
379
380
380
382
385
387
387
390
391
394
395
398
399
.400
401
402
403
405
406
406
409
411
:
412
417
420
433
434
435
436
10
Visual C++ 2005. Od podslaw Rozdziala. Więcej na lemat klas Destruk tory klas Czym jest destruktor Destruktor domyślny Destruktory i dynamiczne przydzielani e pam ięci Implementacja konstruktora kopiującego Dzielenie pamięci pomiędzy zmiennymi Definiowanie unii Unie anonimowe Unie w klasach i strukturach Przeładowywanie operatorów Implementacja przeładowanego operatora Implementacja pełnej obsługi operatora Przeładowywanie operatora przypisania Przeładowywanie operatora dodawania Przeładowywanie operatorów inkrement acj i i dekrementacji Szablony klas Definiowanie szablonu klasy Tworzenie obiektów klasy szablonu Szablony klas z wieloma parametrami Używanie klas Interfejs klasy Definiowan ie problemu Implementacja klasy Definiowanie klasy CBox Zastosowanie klasy CBox Organizowanie kodu programu Nazewnictwo plików programu Programowanie w C++jCLI Przeładowywanie operatorów w klasach wartości Przeładowywanie operatorów inkrement acj i i dekrementacji Przeładowywanie operatorów w klasach referencyjnych Podsumowanie Ćwiczenia
Rozdzial9. Dziedziczenie i funkcje wirlualne Podstawy programowan ia zorientowanego obiektowo (OOP) Dziedziczenie w klasach Czym jest klasa bazowa Tworzenie klas pochodnych Kontrola dostępu do dziedziczonych składowych Działanie konstruktora w klasie pochodnej Deklarowan ie chronionych składowych klasy Poziom dostępu do dziedziczonych składowych klasy Konstruktor kopiujący w klasie pochodnej Składowe klasy jako przyjaciele Klasy zaprzyjaźnione Ograniczenia klas zaprzyjaźnionych Funkcje wirtualne Czym jest funkcja wirtualna Używanie wskaźników do obiektów klas Używanie referencji z funkcjami wirtualnymi
439
439
440
440
442
445
448
448
450
450
450
451
454
458
464
468
468
469
472
475
477
477
477
478
486
497
500
500
502
503
508
509
511
512
515
515
517
517
518
521
524
528
531
532
537
538
538
539
541
544
545
Spis treści Funkcje czysto wirtualne Klasy abstrakcyjne Pośrednie klasy bazowe Wirtualne destruktory Rzutowanie pomiędzy typam i klasowym i Klasy zagnieżdżone Programowanie w C++ /CLI Dziedziczenie w C++/CLI Klasy interfejsowe Definiowanie klas interfejsowych Klasy i asemblacje Definiowanie nowych funkcji Delegaty i zdarzenia Finalizatory i destruktory w klasach referencyjnych Klasy generyczne Podsumowanie Ćwiczenia
_
547
548
551
553
559
559
563
563
569
570
574
579
579
592
594
605
607
,
:
Rozdzial10. Debugowanie
611
Co znaczy debugowanie Błędyoprogramowania
Najczęściej spotykane błędy Podstawowe operacje debugowania Ustawianie punktów wstrzymania Ustawianie punktów śledzenia Rozpoczynanie debugowania Zmien ianie wartości zmiennej Dodawanie kodu debugującego Asercje Dodawanie własnego kodu debugowanła Debugowanie programu Stos wywołań Szukanie błędu krok po kroku Testowanie rozszerzonej klasy Odnajdywanie następnego błędu Debugowanie pamięci dynamicznej Funkcje sprawdzające obszar wolnej pamięci Sterowanie operacjami debugowanla obszaru wolnej Dane wyjściowe debuggera obszaru wolnej pamięci Debugowanie programów w C++/CLI Używanie klas Debug i Trace Podsumowanie
_
pam i ęci
Rozdzial11. Założenia programowania dla systemu Windows Podstawy programowania dla systemu Windows Elementy okna Programy dla Windowsa i system operacyjny Programy st erowane zdarzeniami Komunikaty Windowsa Windows API Typy danych w systemie Windows Notacj a w programach dla systemu Windows
11
611
613
614
615
617
619
620
625
625
626
627
633
633
635
638
641
641
642
643
644
650
650
659
661
:
662
663
664
665
665
666
666
667
12
Visual C++ 2005. Od podslaw Struktura programu dla systemu Windows Funkcja WinMalnt) Funkcje przetwarzania komunikatów Prosty program dla systemu Windows Organizacja programu dla systemu Windows Microsoft Foundation Classes Notacja MFC Jak jest ustrukturyzowany program MFC Korzystani e z formularzy systemu Windows Podsumowanie
668
669
681
686
686
689
689
690
693
695
Rozdzial12. PrOgramowanie dla systemu Windows
zwykorzystaniem Microsoft Foundation C1asses
699
Architektura dokument-widok w MFC Czym jest dokument Interfejsy dokumentu Czym jest wido k lączenie dokumentu i jego widoków Aplikacja a MFC Tworzenie aplikacji MFC Tworzenie apl ikacji SDI Wynik działania MFC Application Wizard Tworzenie aplikacji MDI Podsumowanie Ćwiczenia
700
700
700
70'1
702
702
705
707
710
721
724
724
Rozdzial13. Praca zmenu i paskami narzędzi Komun ikacja z systemem Windows Zrozumieć mapy komunikatów Kategorie komunikatów Obsługa komun ikatów w programie Rozwijanie programu Sketcher Elementy menu Tworzenie i edycja zasobów menu Dodawanie procedur obsługi dla komunikatów menu Wybieranie klasy obsługującej komunikaty menu Tworzenie funkcj i komunikatu menu Tworzenie kodu dla funkcji komunikatów menu Dodawanie procedur obsługi komunikatów uaktualniających interfejs Dodawanie przycisków paska narzędzi Edycja właściwośc i przycisku paska narzędzi Testowanie przycisków narzędzi Dodawanie wskazówek Podsumowanie Ćwiczenia
Rozdzial14. Rysowanie woknie Podstawy rysowania w oknie Obszar klienta okna Graphical Device Interface
727
użytkownika
727
728
731
732
733
734
734
739
740
741
743
747
752
753
754
755
756
757
759
759
760
761
SpiS treści Mechanizm rysowania w Visual C++ Klasa widoku w aplikacji Klasa CDC Rysowanie grafiki w praktyce Programowanie myszy Komunikaty z myszy Procedury obsługi komunikatów myszy Rysowanie za pomocą myszy Testowanie szkicownika Uruchamianie przykładu Przechwytywanie komunikatów myszy Podsumowanie Ćwiczenia Rozdział 15. Tworzenie dokumentu ipoprawianie
:
widoku
Czym są klasy kolekcji Typy kolekcji Klasy kolekcji z kontrolą typów Kolekcje obiektów Kolekcje wska źników z kontrolą typów Korzystanie z szablonu klasy CList Rysowanie krzywej Definiowanie klasy CCurve Implementacja klasy CCurve Sprawdzanie klasy CCurve Tworzenie dokumentu Używanie wzorca CTypedPtrList Poprawianie widoku Uaktualnianie wielokrotnych widoków Przewijanie widoków Korzystanie z trybu mapowania MM_LOENGLlSH Usuwanie i przesuwanie kształtów Implementacja menu kontekstowego Łączenie menu z klasą Wybieranie menu kontekstowego Podświetlanie elementów Obsługa komunikatów menu Rozwiązywanie problemu nakładających się elementów Podsumowanie Ćwiczenia Rozdział 16. Praca z oknami
dialogowymi ikontrolkami
Poznaj okna dialogowe Poznaj kontrolki Wspólne kontrolki Tworzenie zasobu okna dialogowego Dodawanie kontrolek do okna dialogowego Programowanie okna dialogowego Dodawanie klasy dialogu Modalne i niemodalne okna dialogowe Wyświetlanie okna dialogowego
13 763
763
765
774
776
777
779
781
805
806
807
808
809
811
811
812
813
813
823
825
826
827
829
830
831
831
837
837
840
844
846
847
848
850
855
860
867
869
870
871
871
872
874
874
875
877
877
878
878
14
Visual C++ 2005. Od podstaw Obs ł u ga
kont rolek okna dialogowego Inicjalizowanie kontrolek O b s ł u ga komunikatów przycisku opcj i Ko ń czenie operacj i okna dialogowego Dodawanie szerokośc i pióra do dokument u Dodawanie szerok ości pióra do elementów Tworzenie elementów w widoku Testowanie okna dialogowego U żywani e p okrętł a
Dodawanie element u menu Scale oraz przycisku paska Tworzenie po k rętła Generowanie klasy okna dialogowego Scale Wyśw i e tl an i e po k rętła
Korzystanie ze w spółc zynni ka skali Skalowalne tryby mapowania Ustawianie rozmiaru dokumentu Ust awianie t rybu mapowania Impleme ntowanie przewijania ze skalowaniem Praca z paskami stanu Dodawanie paska stanu do ramki U żyw ani e pól list Usuwanie okna dialogowego Scale Tworzenie kontro lki pola list Korzystan ie z kont rolki pola tekstowego Tworzenie zasobu pola tekst owego Tworzenie klasy okna dialogowego Dodawanie element u menu Text Definiowanie elementu Text Impleme ntacja klasy CText Tworzenie element u Text Podsumowanie Ćwiczeni a
Rozdział 17. Przechowywanie i drukowanie dokumenlów Poznaj seri al i z acj ę Serializowanie dokumentu Serializacja w definicji klasy dokumentu Serializacj a w implement acji klasy dokumentu Zestaw fun kcj i klas opartych na CObject Jak d zi ał a serializacj a Jak za im p l em entowa ć se r ia l izację klasy Stosowanie seri alizacji Rejest rowanie zmian dokumentu Serializowanie dokumentu Serializowanie klas elementów Testowani e seriali zacji Przenoszenie tekst u Drukowanie dokument u Proces drukowania Implement acja wielostron icowych wydruków Uzyskiwanie c ał kowitego rozmiaru dokum entu Przechowywanie danych drukowania
Spis treści Przygotowania do wydruku Porządkowanie po drukowaniu Przygotowywanie kont ekstu urządzenia Drukowanie dokumentu Drukowanie dokum entu Podsumowanie
945 947 947 948 952 95 3 954
Ćwiczenia
Rozdzial18. Tworzenie własnych plików DLL
955
Poznaj DLL Jak działają DLL Zawartość DLL Odmiany DLL Co umieścić w DLL Pisanie DLL Pisanie i używanie rozszerzającej DLL Eksportowan ie zmiennych i funkcj i z DLL Importowanie symbol i do programu Implementowanie eksportowania symboli z DLL Podsumowani e
955 957 960 961 962 963 963 970 971 972 974 975
Ćwiczenia
Rozdział 19. lączenie się
ze źródłami danych
Podstawy baz danych Nieco o języku SQL Pobieranie danych z użyciem języka SQL Łączenie tab el w języku SQL Sortowanie rekordów : Obsługa baz danych w MFC Klasy MFC obsługujące ODBC Tworzenie aplikacji bazodanowej Rejestrowanie bazy danych ODBC Generowan ie programu MFC ODBC Poznaj strukturę programu Testowanie przykładu Sortowanie zestawu rekordów Zmienianie podpisu okna Używanie drugiego obiektu zestawu rekordów Dodawanie klasy zestawu rekordów Dodawanie klasy widoku dla zestawu rekordów Dostosowywan ie zestawu rekordów Dostęp do wielu widoków tablic Przeglądanie zamówień na produkt Przeglądanie informacji o kliencie Dodawanie zestawu rekordów dla informacji o kliencie Tworzenie zasobu okna dialogowego z informacjami o kliencie Tworzenie klasy widoku dla informacji o kliencie Dodawanie filtra Implementacja parametru filtra Łączenie okna dialogowego Order z oknem dialogowym Customer Testowanie przeglądarki bazy danych Podsumowanie Ćwiczenia
VislJal C++ 2005. Od podslaw Rozdział20.lklualizacja źródeł danych
1033
Operacje aktual izacji Operacje aktualizacji CRecordSet Transakcje Prosty przykład uaktualnienia Dostosowywanie aplikacji Zarządzanie procesem aktualizacji Implementacja trybu uaktualniania Dodawanie wierszy do tabeli Proces wpisywania zamówienia Tworzenie zasobów Tworzenie zest awów rekordów Tworzenie widoków zestawu rekordów Dodawanie kontrolek do zasobów dialogu Implementacja przełączania okien dialogowych Tworzenie identyf ikatora zamówienia Przechowywanie danych zamówienia Wybieranie produktów dla zamówienia Dodawanie nowego zamówienia Podsumowanie Ćwiczenia
1033
1034
1036
1038
1040
10 42
1044
1052
1053
1054
1055
1055
1060
1064
1068
1073
1075
1077
1082
1082
Rozdział21.lplikacjewYkorzystująceWindows Forms
1083
Poznaj formularze systemu Windows Poznaj aplikacje Windows Forms Zmienianie właściwości formularza Jak startuje aplikacja Dostosowywanie GUl aplikacji Dodawanie kontrolek do formularza Dodawanie zakładek Korzystanie z kontrolki GroupBox Używanie kontrole k Button Korzystanie z kontrolki WebBrowser Sposób działania aplikacji Winning Application Dodawanie menu kontekstowego Tworzenie procedur obsługi zdarzeń , Obsługa zdarzeń dla menu l.lrnits Tworzenie okna dialogowego Używanie okna dialogowego Dodawanie drugiego okna dialogowego Implementacja elementu menu Help/About Obsługa kliknięcia przycisku Reagowanie na menu kontekstowe Podsumowanie
1083
1084
1086
1087
1088
1089
1092
1094
1097
1099
1100
1102
1102
1108
1109
1115
1120
1128
1128
1131
1138
1139
Ćwiczenia
Rozdział 22. Dostęp
do źródeł danych waplikacjach Windows Forms
Praca ze źródłam i danych Dostęp do danych i ich wyświetlanie Używanie kontrolki DataGridView Używanie kontrol ki DataGridView w trybie
niezwiązanym
1141
1142
1143
1143
1145
Spis Ireści Dostosowywan ie kontrolki DataGridView Dostosowywan ie komórek nagłówkowych Dostosowywanie pozostałych komórek Dynamiczne ustawianie stylów komórki Używanie trybu związanego Komponent BindingSource Korzystanie z kontro lki BindingNavigator W ią zan ie z pojedynczymi kontrolkami Praca z wielom a tabelami Podsumowani e Ćwiczenia
17 1151
1152
1153
1160
1165
1166
1171
1174
1178
1179
1180
Dodalek A Slowa kluczowe w Języku C++
1181
Dodatek B Kody ASCii
1183
Skorowidz
1189
18
Visual C++ 2005. Od podstaw
oautorze Ivor Horton z wyk ształcenia jest matematykiem, a do informatyki zwabiła go obietnica zarobków przy niewielkim nakładzie pracy. Mimo że rzeczywistość okazała się całkiem inna - dużo pracy i raczej średnie zarobki - komputerami Ivor zajmuje się do dziś. Pracował już jako programista, projektant systemów, konsultant oraz kierownik wdrażania projektów o dużym stopniu złożoności. dużych
Horton ma wieloletnie doświadczenie w tworzeniu i wdrażaniu systemów komputerowych zastosowanie w projektowaniu inżynierskim oraz w produkcji w różnych sekto rach gospodarki. Posiad a także duże doświadczenie w tworzeniu czasami przydatnych pro gramów w różnych językach programowania oraz nauczaniu, przede wszystkim naukow ców i inżynierów, robienia tego samego. Książki na temat programowania pisze już od ponad dziesięciu lat. Pośród jego naj nowszych publikacji znajdują się pozycje dotyczące języków C, C++ oraz Java. Obecnie, kiedy Ivor nie pisze książek o programowaniu i nie zajmuje się doradzaniem , jego głównymi zajęciami są łowienie ryb, podróżowanie oraz szlifowanie francuskiego . mających
20
Visual C++ 2005. Od podstaw
Wstęp
Witaj drogi Czytelniku. Dzięki temu egzemplarzowi możesz stać się efektywnym progra mistą C++. Najnowszy system firmy Microsoft Visual Studio 2005 pozwala na tworzenie programów w dwóch różnych, ale blisko spokrewnionych wersjach języka C++. Obsługuje on zarówno oryginalną, standardową wersję C++ ISO/ANSI, jak również jego nowszą wersję znaną pod nazwą C++/CLI, która została stworzona przez Microsoft i jest dzisiaj standardem ECMA. Obie te wersje uzupełniają się i pełnią różne role. CH ISO/ANSI służy do tworzenia wysoko wydajnych programów, które można uruchamiać natywnie na komputerze. Natomiast CH/CLI został przygotowany specjalnie z myślą o platformie .NET. Z książki tej nauczysz się programować w obu wersjach C++. Przy pisaniu programu w ISO/ANSI CH znaczna część kodu generowana jest automatycznie. Mimo tego ułatwienia istnieje także konieczność samodzielnego wpisywania jego dużych partii. Do tego celu trzeba dobrze rozumieć ideę programowania zorientowanego obiektowo, a także dysponować znaczną wiedzą na temat specyfiki programowania dla systemu Windows . Mimo że C++/CLI stworzony został dla platformy .NET , jest on także narzędziem wspoma gającym tworzenie aplikacji za pomocą biblioteki Windows Forms, przy użyciu której można tworzyć programy, pisząc niewielkie ilości kodu, a czasami nawet bez takiej potrzeby. Oczywiście, gdy zachodzi potrzeba dodania kodu do aplikacji Windows Forms, nawet nie wielkiej jego ilości, trzeba posiadać dogłębną wiedzę na temat języka CH/CLI. Język
C++ ISO/ANSI jest nadal wybierany przez wielu profesjonalistów, ale szybkość two rzenia programów, jaką oferuje język C++/CLI w połączeniu z biblioteką Windows Forms, powoduje, że ma on także duże znaczenie. Z tego też powodu zdecydowałem się przedstawić w tej książce oba rodzaje języka C++ .
Dla kogo iest ta książka Celem tej książki jest nauka pisania w języku C++ programów przeznaczonych dla systemu operacyjnego Microsoft Windows przy użyciu programu Visual C++ 2005 lub jednej z edycji środowiska Visual Studio 2005. Książka ta nie wymaga żadnego wcześniejszego doświad czenia w programowaniu w jakimkolwiek innym języku programowania. Książka ta jest dla Ciebie, jeśli: • Posiadasz niewielkie doświadczenie w programowaniu w innych językach, takich jak BASIC czy Pascal , i chcesz nauczyć się C++ oraz rozwinąć praktyczne zdolności programowania dla systemu Microsoft Windows.
22
Visual C++ 2005. Od podstaw • Masz pewne
doświadczenie w
środowisku niż
w
środowisku
programowaniu w C lub C++, ale w innym Windows, i chcesz poszerzyć swoje umiejętności o programowanie Windows przy użyciu naj nowszych narzędzi i technologii.
• Dopiero zaczynasz programować i masz wystarczająco dużo chęci, aby zgłębiać tajniki programowania w języku C++. Aby nauka była owocna, musisz przynajmniej znać podstawy działania komputera - jak wygląda organizacja pamięci oraz w jaki sposób przechowywane są dane i instrukcje.
oCZym jest ta książka Pisałem tę książkę z myślą nauczenia Czytelnika podstaw programowania w języku C++ przy użyciu obu technologii obsługiwanych przez Visual C++ 2005. Niniejszy egzemplarz stanowi szczegółowy przewodnik po obu typach języka C++. Zatem opisane tutaj zostało tworzenie programów dla systemu Windows w natywnym języku C++ ISO/ANSI przy użyciu biblioteki Microsoft Foundation Classes (MFC) oraz w języku C++/CLI przy użyciu biblioteki Windows Forms. Ze względu na wszechobecność technik bazodanowych w dzisiej szych czasach, książka ta zawiera również wprowadzenie do technik, za pomocą których można uzyskać dostęp do zasobów danych zarówno z poziomu programów utworzonych w technologii MFC, jak i Windows Forms. Programy MFC wymagają pisania większych ilości kodu w porównaniu z aplikacjami Windows Forms. Spowodowane jest to tym, że Win dows Forms pozwala na korzystanie z wysoko rozwiniętych narzędzi Visual C++, umoż liwiających utworzenie całości graficznego interfejsu użytkownika (ang. Graphical User Interface - GUl) w trybie graficznym przy automatycznym wygenerowaniu kodu. Z tego też względu większa część tej książki poświęcona została programowaniu w technologii MFC, a nie Windows Forms.
Organizacja książki •
Rozdział
1. stanowi wprowadzenie do podstawowych zagadnień, które należy móc tworzyć zarówno natywne, jak i korzystające z platformy .NET programy w języku C++. Dodatkowo wprowadzam także podstawy posługiwania się środowiskiem programistycznym Visual C++ 2005. Podstawy te pozwolą na wykorzystanie możliwości Visual C++ 2005 do tworzenia różnego typu programów w języku C++, o których będzie mowa w dalszych rozdziałach książki. zrozumieć, aby
•
Rozdziały
2. - 10. poświęcone zostały nauce obu wersj i języka C++ oraz podstawowych zagadnień i technik znajdowania błędów. Każdy z tych rozdziałów został napisany według tego samego wzoru. Pierwsza połowa poświęcona jest tematom związanym z C++ ISO/ANSI, a druga traktuje o C++/CLI.
•
Rozdział
11. stanowi opis struktury aplikacji systemu Microsoft Windows i opisuje oraz pokazuje najważniejsze komponenty, które posiada każdy taki program. W rozdziale tym znajdują się wyjaśnienia prostych przykładowych programów
Wstęp
23
korzystających z C++ ISO/ANSI oraz API Windows, jak również przykład prostego programu utworzonego w technologii C++ /CLI przy użyciu biblioteki Windows Forms.
•
Rozdziały 12. - 17. szczegółowo opisują możliwości , jakie daje MFC przy budowie GUl. Nauczysz się tworzyć i używać najczęściej spotykane kontolki do budowy graficznego interfejsu użytkownika swojego programu, a także obsługiwać zdarzenia będące rezultatem interakcji użytkownika z programem. W ten sposób utworzymy w pełni działającą aplikację. Dodatkowo na przykładzie tego programu nauczysz się drukować i zapisywać na dysku dokumenty przy użyciu biblioteki MFC.
•
Rozdział
18. zawiera niezbędne informacje potrzebne do tworzenia własnych bibliotek przy użyciu MFC. Dowiesz się, jakiego rodzaju biblioteki możesz tworzyć, oraz utworzysz przykładowe biblioteki, które będę współpracowały z programem napisanym w poprzednich sześciu rozdziałach.
•
Rozdziały 19. i 20. poświęcone zostały uzyskiwaniu dostępu do zasobów danych z poziomu aplikacji MFC. Nauczysz się uzyskiwać dostęp do bazy danych w trybie tylko do odczytu, a następnie poznasz podstawowe techniki programistyczne pozwalające uaktualniać zawartość bazy danych przy użyciu biblioteki MFC. W przykładach wykorzystano bazę danych Nortwind, którą można pobrać z sieci . Opisane techniki można również zastosować do własnego źródła danych .
• W rozdziale 21. za pomocą Windows Forms oraz C++/CLI piszemy przykładowy program, dzięki któremu nauczymy się tworzyć kontrolki Windows Forms, dopasowywać je do własnych potrzeb i ich używać. Program ten jest rozbudowywany w trakcie rozdziału, pozwalając Czytelnikowi na praktyczne zdobywanie doświadczenia. •
Rozdział 22. bazuje na wiedzy zdobytej w poprzednim rozdziale. Poświęcony został on kontrolkom służącym do uzyskiwania dostępu do źródeł danych oraz sposobom dopasowywania ich do własnych potrzeb. Nauczysz się także tworzyć programy z dostępem do bazy danych bez wpisywania kodu źródłowego.
Wszystkie techniki programistyczne opisane w poszczególnych rozdziałach zostały zilustro wane konkretnymi przykładami. Na końcu każdego rozdziału znajduje się podsumowanie najważniejszych zagadnień w nim poruszonych. Większość rozdziałów została dodatkowo uzupełniona o ćwiczenia pozwalające zastosować zdobytą wiedzę w praktyce. Rozwiązania do tych ćwiczeń można pobrać ze strony wydawcy. Część książki poświęcona
samemu językowi C++ bazuje na przykładowych programach trybie konsoli, z którymi komunikacja odbywa się z poziomu wiersza poleceń. Dz ięki temu można skupi ć s i ę na samych możliwościach oferowanych przez język C++, bez wprowadzania niepotrzebnego zamętu związanego z zawiłościami programowania GUl Windows. Programowanie dla systemu Windows jest możliwe dopiero wtedy, gdy zdobędzie się gruntowną wiedzę na temat języka programowania C++. działających w
sobie prostotę mogą rozpocząć naukę od samego C++ ISO/ANSI. Każdy z roz 2. - 10. najpierw opisuje określone możliwości języka C++ ISO/ANSI, a następnie wprowadza nowe właściwości dostępne w C++ /CLI dla tego samego kontekstu . Powodem takiej organizacji książki jest fakt , że C++/CLI został określony jako rozszerzenie C++
Osoby
ceniące
działów
24
Visual C++ 2005. Od podstaw ISO/ANSI. W związku z tym, aby zrozumieć C++/CLI , należy wpierw nauczyć się C++ ISO/ ANSI. Można zatem skupić s i ę jedynie na tematach opisujących s ta n dardow ą wersję języka C++ w rozdziałach od 2. do 20. i pominąć części dotyczące C++/CLI. Następnie można przejść do tworzenia programów dla systemu Windows w języku C++ ISO/ANS] bez konieczności pamiętania o drugiej wersji tego języka . Do C++/CLI można powrócić, gdy poczujemy się pewnie w standardowej wersji. Można oczywiście rozpocząć naukę obu wersji języka od samego początku , stopniowo zwiększając swoją wiedzę na ich temat.
Czego będZiesz
potrzebować
do tei książki
Aby móc w pełni korzystać z tej książki, potrzebujesz jednego z trzech programów: Visual Studio 2005 Standard Edition, Visual Studio 2005 Professional Edition lub Visual Studio 2005 Team System . Visual C++ Express 2005 nie przyda s i ę nam, ponieważ nie zawiera biblioteki MFC. Aby móc zainstalować oprogramowanie Visual Studio 2005, należy posiadać system Windows XP z zainstalowanym zestawem poprawek Service Pack 2 lub Windows 2000 z zainstalowanym Service Pack 4. Wymagania sprzętowe Visual Studio to co najmniej procesor z zegarem l GHz, 256 MB pamięci RAM oraz nie mniej niż 1 GB wolnej prze strzeni na dysku systemowym oraz 2 GB na dysku, na którym zostanie zainstalowany pro gram. Do zainstalowania pełnej dokumentacj i MSDN dostępnej razem z oprogramowaniem potrzebne będzie dodatkowe 1,8 GB na dysku instalacyjnym. Przykłady z bazą danych korzystają z bazy Northwind Traders. Można ją znaleźć, s zu k aj ąc frazy Northwind Traders na stronie http://msdn.microsoft.com, z której można ją pobrać . Można oczywiście skorzystać ze wszystkich opisywanych przykładów w pracy z dowolną inną bazą danych.
Aby odnie ść największe korzyści z czytania tej książki, należy mieć zapał do nauki oraz z uporem dążyć do opanowania naj potężniejszego dostępnego obecnie narzędzia programi stycznego dla systemu Windows. Musisz się poświęcić i wpisać wszystkie przykłady kodu oraz je przeanalizować, a także spróbować rozwiązać zadane ćwiczenia. Wszystko to brzmi o wiele gorzej, niż prezentuje się w rzeczywistości . Nie oprzesz się uczuciu zaskoczenia, jak wiele udało Ci się osiągnąć w krótkim czasie . Należy także pamiętać, że każdy, kto uczy się programować, od czasu do czasu przechodzi trudne chwile. Ale jeśli się nie poddasz, z czasem wszystko stanie się jasne. Książka ta pomoże Ci rozpocząć eksperymentowanie na własną rękę, a co za tym idzie - odnieść sukce s jako programista C++ .
Konwencie Aby umożliwi ć Czytelnikowi odniesienie jak największych korzyści z czytania tej i ułatwić zorientowanie się w temacie, w książce tej przyjęto kilka konwencji.
książki
Wstęp
Jako Spróbuj sam oznaczamy
ćwiczenia,
które powinny
zostać
25
wykonane na podstawie tekstu
książki.
Jak to działa Po kilku Spróbuj sam
następuje
Jak to działa, czyli
W ramkach takich jak ta przechowywane średnio do otaczającego je tekstu.
Wskazówki, porady, sztuczki i uwagi Style w
są
szczegółowe objaśnienie
są ważne
informacje
wpisanego kodu.
odnoszące się
lekko przesunięte i napisane
bezpo
taką czcionką.
tekście:
• Nowe terminy i ważne słowa pisane ich pierwszego pojawienia się. • Kombinacje klawiszy prezentowane
są pismem
pochylonym w momencie
są następująco:
• Nazwy plików, adresy URL oraz kod w
obrębie
Ctrl+A.
tekstu oznaczone są następująco:
persistence.properties. • Kod prezentowany jest na dwa
różne
sposoby:
Nowe i ważne fragmenty kodu w przykładach znajdują się w ramkach Ramka używana jest do oznaczania kodu mniej ważnego w określonym kontekście lub kodu. który był już wcześniej prezentowany. Pracując
z
przykładami
w
książce,
kod
można wpisywać ręcznie
lub
skorzystać
z zasobów
dostępnych dla tej książki. Wszystkie przykłady kodu z tej książki można pobrać ze strony
http://helion.pl/ksiazki/vcppo.htm.
•
26
Visual C++ 2005. Od podstaw
1
Programowanie
przy użyciu lisual C++ 2005
Programowanie dla systemu Window s nie jest trudne , a Microsoft Yisu al C++ 2005 sprawi a, staje się ono wręcz banalne, o czym przekonamy s i ę w trakcie czytania tej książk i . Jest tylko jeden problem: zanim zagłęb imy się w zawiło ści programowania dla Windowsa, najpierw musimy dokładnie zapozna ć si ę z możliwo ściam i oferowanymi przez język C++, a w szcze gólności z technikami programowania zorientowanego obiektowo w tym języku. Techniki te s ta now i ą podstawę efektywn ości wszy stkich narzędzi dostarczanych przez Yisual C++ 2005 dla programowania w systemie Windows. W zwi ązku z tym bardzo ważne jest ich dobr e zro zumienie. W rozdziale tym dowiemy się: że
są najw ażniejsze składniki środowiska
•
Jakie
•
Z czego
•
Czym
są rozwiązania
•
Czym
są
•
Jak
utworzyć
•
Jak
sk omp i l ow ać , sk o nso l i d ować
skład a się
programy
Yisual C++ 2005.
platforma .NET oraz jakie oferuje i proj ekty oraz j ak
działające
s ię je
korzyści .
tworzy.
w trybie konsoli.
i ed ytować program. i uruchomi ć program konsolowy w C++.
A zatem nadszedł czas, by włączyć komputer, wisko Yisu al C++ oraz rozpocząć przygodę.
uruchomić
system Windows i potężne
ś ro d o
Środowisko programistyczne .NET Środowisko programistyczne .NET stanowi cen t ra l n ą koncepcję Yisual C++ 2005, jak również wszystkich innych produktów firmy Microsoft wykorzystujących tę platformę. Środowisko .NET składa się z dwóch komponentów : Common Language Runtime (CLR) ,
28
Visual C++ 2005. Od podstaw w którym wykonywane są programy, oraz zbioru bibliotek, zwanych bibliotekami klas środo wiska .NET. Biblioteki klas platformy .NET do starczają funkcji potrzebnych do wykonania kodu w CLR bez względu na użyty język programowania. Oznacza to, że programy .NET napisane w C++, C# lub jakimkolwiek innym języku obsługującym platformę .NET korzy stają z tych samych bibliotek .NET. Za pomocą pakietu Visual C++ 2005 można tworzyć dwa podstawowe typy programów w C++. Istnieje możliwość napisania programu , który jest wykonywany natywnie, na tym samym komputerze - tego typu programy nazywamy programami natywnymi C++ i two rzymy je w C++ ISO/ANSI. Drugi typ to programy działające pod kontrolą CLR , które zostały napisane w rozszerzonej wersji C++, czyli C++/CLI. Programy te nazywamy programami CLR lub programami C++/CLI. Platforma .NET nie jest częścią Visual C++ 2005, ale raczej składnikiem systemu operacyj nego Windows, który ułatwia tworzenie oprogramowania oraz usług sieciowych. Platforma ta zapewnia większą niezawodność kodu i jego bezpieczeństwo,a także pozwala na integrację kodu C++ z kodem napisanym w ponad 20 innych językach programowania, które z nią współpracują. Jedną z wad programowania dla platformy .NET jest niewielka strata wydaj ności , ale w większości przypadków jest ona całkowicie niezauważalna.
Common Language Runtime (CLRJ CLR jest standardowym środowiskiem do wykonywania programów napisanych w wielu różnych językach wysokiego poziomu, takich jak Visual Basic, C# czy właśnie C++. Spe cyfikacja CLR w chwili obecnej zawiera się w standardzie CLI (ang. Common Language Infrastructurei, europejskiego stowarzyszenia producentów komputerów ECMA (ang . Euro pean Computer Manufacturersi - ECMA-335 , a także w równorzędnym standardzie ISO ISO/lEC 23271 , a więc CLR jest implementacją tego standardu. Można łatwo odgadnąć , dlaczego język C++ dla CLR nazywany jest C++/CLI - jest to C++ dla Common Language Infrastructure (wspólna infrastruktura dla języków). Dzięki temu kompilatory C++/CLI można spotkać także w innych systemach operacyjnych, które posiadają implementację CLI.
Wszelkie informacje o standardach ECMA dostępne są pod adresem: http ://www. ecma -international.org. Z tej strony można nieodpłatniepobrać standard ECMA-335. CLI jest w rzeczywistości specyfikacją maszyny wirtualnej, która umożliwia uruchamianie programów napisanych w różnych językach programowania wysokiego poziomu w różnych systemach operacyjnych bez zmiany lub ponownej kompilacji kodu źródłowego. CLI defi niuje standardowy język pośredni dla maszyny wirtualnej , do którego kompilowany jest kod napisany w jednym z języków programowania wysokiego poziomu. Na platformie .NET język ten nazywany jest Microsoft Intermediate Language (MSIL). Kod pośredni jest ostatecznie mapowany na kod maszynowy "w locie" przez kompilator typu JIT podczas wykonywania programu. Kod pośredni CLI można oczywiście uruchomić w dowolnym śro dowisku posiadającym implementację CLI .
Rozdzial1. • Programowanie przy użyciu Visual C++ 2005
29
CLI definiuje także wspólny zbiór typów danych, zwany Common Type System (CTS), którego należy używać przy pisaniu programów w językach mających na celu implementację CLI. CTS określa sposób używania typów danych w CLR i zawiera zestaw predefiniowanych typów. Można także definiować własne typy danych, ale należy trzymać się określonych reguł , aby zachować zgodność z CLR (o tym za chwilę). Standardowy system reprezentacji typów danych pozwala na jednolitą obsługę danych z poziomu komponentów napisanych w różnych językach programowania, a także na ich integrację w obrębie jednej aplikacji . CLR znacznie zwiększa bezpieczeństwo danych i niezawodność kodu, częściowo ze względu na fakt, że dynamiczne przydzielanie i zwalnianie pamięci odbywa się w pełni automatycznie, a częściowo ponieważ kod MSIL jest dokładnie sprawdzany i poddawany walidacji przed wykonaniem programu. CLR jest tylko jedną implementacją specyfikacji CLI , która jest wykonywana w systemie Microsoft Windows na komputerach osobistych. Bez wątpienia implementacje CLI dla innych systemów operacyjnych i platform sprzętowych także będą się pojawiać. Terminy CLI i CLR mogą być czasami stosowane zamiennie, ale należy pamiętać , że nie oznaczają one dokładnie tego samego. CLI jest specyfikacją standardu, CLR zaś stwo rzonąprzez firmę Microsoft implementacją CLI.
Pisanie programów wC++ Visual C++ 2005 umożliwia tworzenie wszelkiego rodzaju programów i ich składników . Jak już wspominałem wcześniej, w systemie Windows mamy do wyboru dwa typy aplika cji: programy wykonywane za pomocą CLR oraz programy kompilowane bezpośrednio do kodu maszynowego i wykonywane natywnie na komputerze . Tworząc aplikacje oparte na oknach dla CLR, jako podstawę GUl wykorzystuje się Windows Forms, które dostarczane są w bibliotekach platformy .NET. Korzystanie z Windows Forms znacznie przyspiesza tworze nie graficznego interfejsu użytkownika, gdyż tworzy się go w trybie graficznym ze standar dowych komponentów, a kod generowany jest automatycznie. Programiście pozostaje już tylko dopasowanie tak powstałego kodu do własnych potrzeb w celu uzyskania wymaganej funkcjonalności. Tworząc
kod wykonywany natywnie, do wyboru mamy kilka opcji . Jedną z nich jest użycie biblioteki Microsoft Foundation Classes (MFC) służącej do zaprogramowania interfejsu użytkownika aplikacji Windows. Biblioteka MFC zawiera w sobie API systemu operacyjnego Windows do tworzenia i kontrolowania GUl, a także znacznie ułatwia proces rozwoju pro gramu. API Windows powstało dużo wcześniej niż język C++, a więc nie zawiera żadnych cech właściwych technice programowania zorientowanego obiektowo, a byłoby tak, gdyby zostało napisane dzisiaj. Oczywiście, nie ma obowiązku używania MFC. Jeśli chcemy zyskać na wydajności , możemy napisać kod C++ z bezpośrednim dostępem do API Windows. Kod C++ wykonywany w CLR opisywany jest jako CH zarządzany (ang . managed C++), dane i kod są zarządzane przez CLR. W programach CLR zwalnianie pamięci przydzielonej dynamicznie odbywa się automatycznie. W ten sposób eliminuje się ryzyko wystąpienia błędów typowych dla natywnych aplikacji CH. Kod CH, który wykonywany jest poza CLR, zwany jest czasami przez Microsoft CH niezarządzanym (ang. unmanaged C++), ponieważ CLR nie bierze udziału w jego wykonywaniu. Korzystając z niezarządzanego ponieważ
30
Visual C++ 2005. Od podslaw C++, trzeba samodzielnie przydzielać i zwalniać pamięć podczas wykonywania programu . Należy się także liczyć z obniżeniem poziomu bezpieczeństwa, które daje CLR . Niezarządzany C++ może być czasami nazywany natywnym C++, gdyż kompilowany jest on wprost do kodu maszynowego . Na rysunku 1.1 pokazano podstawowe
możliwości
tworzenia programów w C++.
Rysunek 1.1
System operacyjny
Sprzęt
Rysunek 1.1 nie przedstawia jednak pełnego obrazu. Program może składać się jednocześnie z kodu napisanego w zarządzanym i natywnym C++ , a więc nie musimy trzymać się sztywno jednego stylu programowania. Oczywiście, mieszając dwa różne typy kodu , tracimy pewne rzeczy, a więc podejście to powinno być stosowane wyłącznie wtedy, gdy jest to konieczne, na przykład gdy chcemy przekonwertować istniejący już program napisany w natywnym C++ na program działający pod kontrolą CLR. Korzyści wynikające z używania zarządzanego C++ nie są oczywiście dostępne z poziomu C++ natywnego, a komunikacja pomiędzy składnikami programu napisanymi w tych dwóch typach języka może być znacznie wydłu żona. Możliwość łączenia zarządzanego i niezarządzanego kodu w jednej aplikacji może oka zać się jednak nie do przecenienia, gdy zajdzie potrzeba rozwinięcia lub rozszerzenia istnieją cego niezarządzanego kodu przy jednoczesnym korzystaniu z zalet używania CLR. Oczywiś cie, tworząc program od początku, przed rozpoczęciem jego pisania należy zdecydować się, jakiego typu aplikacją ma on być .
Nauka programowania dla slslemu Windows Tworzenie programów wykonywanych w systemie Windows oparte jest zawsze na dwóch podstawowych aspektach działania: utworzeniu graficznego interfejsu użytkownika (GUl), z którym użytkownik wchodzi w interakcje, oraz wykonaniu kodu przetwarzającego te inte
Rozdzial1. • Programowanie przy użyciU Visual C++ 2005
31
rakcje w celu zapewnienia aplikacji funkcjonalności. Visual C++ 2005 znacznie ułatwia pracę nad tymi aspektami tworzenia aplikacji Windows . W dalszej części tego rozdziału przekonamy się, że można stworzyć działający program dla Windowsa z GUl bez napisania nawet jednego wiersza kodu . Cały podstawowy kod może zostać wygenerowany automatycznie przez Visual C++ 2005 . Zrozumienie sposobu działania tego kodu jest jednak niezbędne, gdyż później będziemy chcieli go zmodyfikować i rozszerzyć, aby wykonywał zamierzone przez nas czyn ności. Aby tego dokonać , musimy bardzo dobrze rozumieć C++ . Z tego powodu na początku skupimy się na nauce samego języka C++ (zarówno natywnego, jak i w wersji C++/CLI), bez wdawania się w zawiłości programowania dla systemu Win dows. Gdy opanujemy już sam język C++, przejdziemy do tworzenia prawdziwych aplikacji Windows przy użyciu obu wersji języka C++. Oznacza to, że podczas nauki C++ będziemy tworzyć programy działające z poziomu wiersza poleceń. Dzięki takiemu podejściu będzie można skupić się na specyfice działania języka C++ i uniknąć w przyszłości komplikacji związanych z tworzeniem i kontrolą GUL Po opanowaniu C++ stwierdzisz, że przejście od praktycznego wykorzystania zdobytej wiedzy do tworzenia programów dla Windowsa jest naturalnym krokiem.
Nauka C++ Visual C++ 2005 standardach:
obsługuje
w
pełni
dwie wersje języka C++, zdefiniowane w dwóch
różnych
• Standard C++ ISO/ANSI służący do implementacji natywnych programów - C++ niezarządzany . Ta wersja języka obsługiwana jest przez większość platform komputerowych. • Standard C++/CLI, który został zaprojektowany specjalnie do tworzenia aplikacji działających pod kontrolą CLR i stanowi rozszerzenie C++ ISO/ANSI. Rozdziały od 2. do 10. poświęcone są nauce języka C++. Ze względu na fakt, że C++/CLI jest rozszerzeniem C++ ISO/ANSI , pierwsza część każdego rozdziału wprowadza elementy języka C++ ISO/ANSI, a druga objaśnia dodatkowe możliwości, których dostarcza C++/CLI. Pisząc programy w C++/CLI , możemy w pełni wykorzystać możliwości platformy .NET, co nie jest możliwe w programach pisanych w C++ ISO/ANSI. Mimo że C++/CLI jest rozszerze niem C++ ISO/ANSI , aby program mógł zostać wykonany całkowicie pod kontrolą CLR, musi on zostać napisany zgodnie z wymaganiami tej technologii. Oznacza to, że C++ ISO/ANSI posiada pewne właściwo ści , których nie można wykorzystywać w CLR. Jednym z przykładów, jak można się domyślić, jest brak kompatybilności mechanizmów przydzielania i zwalniania pamięci oferowanych przez C++ ISO/ANSI z CLR . Do zarządzania pamięcią musimy korzy stać z mechanizmu CLR, a to z kolei oznacza, że musimy używać klas C++/CLI, a nie na tywnych klas C++ .
32
Visual C++ 2005. Od podstaw
Standardy C++ Standard ISO/ANSI zdefiniowany jest w dokumencie ISO/lEC 14882 opublikowanym przez American National Standards Institute (ANSI). C++ ISO/ANSI istnieje od roku 1998 i ma już ugruntowaną pozycję . Obsługiwany jest przez kompilatory większości komputerowych platform sprzętowych i systemów operacyjnych. Programy napisane w C++ ISO /ANSI można dość łatwo przenosić pomiędzy różnymi systemami, chociaż prawdziwym wyznaczni kiem tego , czy dany program można łatwo przenieść, czy nie , są funkcje klas przez niego używanych w szczególności klas związanych z budową GUl. Standard C++ ISO/ANSI jest wybierany przez wielu profesjonalnych programistów ze względu na jego powszechną implementację oraz dlatego, że jest to jeden z naj potężniej szych dostępnych obecnie języków programowania.
Dokument iso.org.
opisujący
standard C++ ISO/ANSI
można zamówić
na stronie: http://www.
C++/CLI jest natomiast wersją języka C++, która rozszerza jego standardowe możliwości, czemu lepiej obsługuje specyfikację CLI zdefiniowaną w standardzie ECMA-355. Pierwsza wersja robocza tego standardu pojawiła się w 2003 roku i była rozwijana ze wstępnej specyfikacji techn icznej stworzonej przez Microsoft w celu umożliw ienia uruchamiania programów C++ na platformie .NET. Tak więc zarówno CLI, jak i C++ /CLI wywodzą się z firmy z Redmond , a ich przeznaczeniem jest współpraca z platformą .NET. Oczywiście ustandaryzowanie CLI oraz C++/CLI znacznie podwyższa prawdopodobieństwo implemen tacji w środowiskach innych niż Windows . Należy jednak pamiętać, że mimo iż C++/CLI jest rozszerzeniem C++ ISO/ANSI, to niektórych właściwości tego języka nie możemy wykorzy stywać, jeżeli chcemy, aby nasze programy działały w pełni pod kontrolą CLR . O właściwo ściach tych piszę w następnych rozdziałach.
dzięki
CLR ma pewne właściwości, które dają mu znaczną przewagę nad środowiskiem natywnym. Programy pisane dla CLR są bezpieczniejsze i mniej podatne na potencjalne błędy, które łatwo popełnić podczas wykorzystywania wszystkich możliwości C++ ISO/ANSI. CLR usu wa również wszelkie niekompatybilności związane z zastosowaniem różnych języków wyso kiego poziomu poprzez ustandaryzowanie środowiska, dla którego tworzone są programy. Dzięki temu można łączyć moduły napisane w C++ z modułami napisanym i w innych języ kach, takich jak C# lub Yisual Basic .
Aplikacje działające
wtrybie konsoli
Poza typowymi programami dla Windowsa, Yisual CH 2005 pozwala także na pisanie, kom pilowanie i testowanie programów C++ pozbawionych całego bagażu wymaganego od apli kacji okienkowych. Aplikacje te działają w oparciu o tryb tekstowy i wiersz poleceń . W Yisual CH 2005 nazywają się one aplikacjami konsolowymi (ang. eonsole applicationsy, ponieważ komunikacja z nimi odbywa się za pomocą klawiatury i ekranu w trybie tekstowym . Pisząc tego typu programy, można odnieść wrażenie, że odchodzimy nieco od tematu książki (jest to konieczne przed rozpoczęciem programowania specjalnie dla systemu Windows), ale jest to najlepszy sposób nauki C++. Nawet prosty program w Windowsie zbudowany jest
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
33
liczby wierszy kodu i ważne jest, aby zawiłości związane z systemem Windows nie naszej uwagi skupionej na mechanizmach działania języka C++. W związku z tym w początkowych rozdziałach, w których uczymy się samego języka C++, będziemy tworzyć proste aplikacje konsolowe, a dopiero później przejdziemy do bardziej skompliko wanych, złożonych z dużej liczby wierszy kodu, programów dla Windowsa. z
dużej
rozpraszały
Podczas nauki C++ będziemy mogli skoncentrować się na właściwościach języka, nie mar twiąc się o środowisko, w którym operujemy. Aplikacje konsolowe, które będziemy tworzyć, posiadają interfejs tekstowy, ale dla zrozumienia C++ to całkowicie wystarczy. Sam język z definicji nie posiada żadnych możliwości graficznych. Oczywiście programowaniu graficz nego interfejsu użytkownika poświęciłem dużo miejsca, ale w tej części książki, która została poświęcona programowaniu dla Windowsa przy użyciu biblioteki MFC w natywnym C++ oraz Windows Forrns z CLR . Istnieją dwa rodzaje aplikacji konsolowych i będziemy używać obu. Aplikacje konsolowe Win32 kompilowane są do kodu natywnego i za ich pomocą będziemy wypróbowywać moż liwości C++ ISO/ANSI. Aplikacje konsolowe CLR tworzone są dla CLR, a więc będziemy ich używać, pracując z językiem CH/CLI.
Koncepcie programowania wsystemie Windows do programowania dla systemu Windows zakłada wykorzystanie wszystkich w Visual C++ 2005. Narzędzia służące do tworzenia nowego projektu potrafią automatycznie wygenerować kod szkieletu różnego rodzaju aplikacji, włączając w to podstawowe programy dla Windowsa. Proces pisania każdego programu lub komponentu w Visual C++ 2005 rozpoczyna się od utworzenia nowego projektu. Aby sprawdzić , jak to działa, w dalszej części rozdziału utworzymy kilka przykładów włącznie ze szkieletem pro gramu dla Windowsa.
Nasze
podejście
narzędzi dostępnych
Programy w Windowsie mają inną budowę niż typowe aplikacje konsolowe wykonywane za pomocą wiersza poleceń - są bardziej skomplikowane. W aplikacji konsolowej dane można przyjmować wprost z klawiatury i wyniki działań wysyłać z powrotem do wiersza poleceń. Programy dla Windowsa natomiast pobierają i wysyłają dane wyłącznie za pomocą funkcji systemu Windows . Nie pozwalają one na dostęp do zasobów sprzętowych . Ze względu na fakt, że system Windows pozwala na uruchamianie kilku aplikacji naraz, musi on umieć określić, dla której z nich przeznaczone zostało dane zdarzenie, np. kliknięcie przyciskiem myszki lub naciśnięcie klawisza na klawiaturze, a następnie wysłać sygnał do właściwego programu. Dzięki temu system Windows sprawuje podstawową " ko n tro l ę nad całym procesem komuni kacji z użytkownikiem. A zatem natura interfejsu pomiędzy użytkownikiem a systemem Windows jest taka, że po zwala na wiele różnych operacji wejścia i wyjścia w tym samym czasie. Użytkownik może wybrać jedną z wielu opcji dostępnych w menu, kliknąć przycisk na pasku narzędzi czy przy ciskiem myszki w dowolnym miejscu okna aplikacji. Dobrze zaprojektowany program dla Windowsa musi być przygotowany na wszelkiego rodzaju dane wejściowe w dowolnym czasie, gdyż nie ma sposobu dowiedzenia się z góry, jakiego rodzaju dane wejściowe zostaną przekazane . Czynności tego typu wykonywane przez użytkownika są przechwytywane przez system jako pierwsze i nazywają się zdarzeniami. Zdarzenie wywołane przez użytkownika
34
Visual C++ 2005. Od podstaw w obrębie interfejsu powoduje zazwyczaj wykonanie określonego fragmentu kodu. A zatem sposób wykonywania całego programu zależy od zachowania użytkownika. Programy ope rujące w ten sposób nazywane są programami zdarzeniowymi i różnią się od zwykłych programów proceduralnych, które odznaczają się pojedynczą kolejnością wykonywania . Wprowadzanie danych do programu proceduralnego kontrolowane jest przez jego kod i może mieć miejsce tylko wtedy, gdy program na to zezwoli. Tak więc program Windows składa się przede wszystkim z fragmentów kodu , które reagują na zdarzenia spowodowane przez użyt kownika lub sam system. Struktura tego typu programu została przedstawiona na rysunku 1.2.
Rysunek 1.2 Każdy prostokąt na rysunku 1.2 reprezentuje fragment kodu napisany specjalnie do obsługi jednego określonego zdarzenia. Z rysunku można wywnioskować, że przedstawiony pro gram jest nieco rozbity ze względu na brak połączeń pomiędzy niektórymi blokami, ale tak nie jest, gdyż głównym spoiwem jest tutaj system operacyjny Windows. Pisząc program, można go traktować jako swego rodzaju naukę wykonywania określonych czynności w sys temie Windows.
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
35
Oczywiście wszystkie moduły obsługujące różne zdarzenia zewnętrzne, takie jak wybór menu
lub kliknięcie przyciskiem myszki , mają dostęp do wspólnej puli danych właściwych dla danej aplikacji. Dane te zawierają informacje o tym, czym jest program - np. bloki tekstu w edytorze lub rekordy przechowujące punkty gracza w programie mającym za zadanie śle dzenie wyników drużyny piłkarskiej - jak również informacje o niektórych zdarzeniach ma jących miejsce podczas wykonywania programu . Te współdzielone dane pozwalają różnym częściom programu, które wydają się niezależne, komunikować się oraz operować w skoor dynowany i zintegrowany sposób. Więcej na ten temat piszę w dalszej części książki. Nawet najbardziej podstawowy program dla Windowsa składa się z kilku wierszy kodu, a w programach wygenerowanych za pomocą kreatora takiego jak Visual C++ 2005 kilka przeistacza się w kilkaset. Aby uprościć proces nauki C++, potrzebujemy jak najprostszego kontekstu. Na szczęście Visual C++ 2005 posiada ś ro d o w i s k o w sam raz nadające się do tego celu.
Czym jest zintegrowane środowisko programistyczne Zintegrowane środowisko programistyczne (ang. Integrated Developm ent Environment IDE) , które dostarczane jest z Visual C++ 2005, jest kompletną platformą do tworzenia, kom pilowania, konsolidowania i testowania programów napisanych w C++. Tak się składa, że doskonale nadaje się również do nauki języka C++ (w szczególności w połączeniu z dobrą książką).
Visual C++ 2005 zawiera wiele w pełni zintegrowanych narzędzi zaprojektowanych w celu uproszczenia całego procesu pisania programów w C++. Część tych narzędzi poznamy już w tym rozdziale, ale zamiast przedzierać się przez nudną listę abstrakcyjnych opcji i właściwo ści, najpierw opanujmy podstawy, aby zobaczyć, jak działa IDE. Reszta przyjdzie stopniowo, w miarę postępu nauki.
Składniki
systemu
Podstawowymi składnikami Visual C++ 2005, dostarczanymi jako część IDE, są edytor, kompilator, program łączący (konsolidator) oraz biblioteki. Są to narzędzia niezbędne do napisania i wykonania programu w C++ . Ich funkcje zostały opisane poniżej .
Edylor Edytor jest interaktywnym środowiskiem do tworzenia i edycji kodu źródłowego w języku C++. Poza typowymi, znanymi każdemu funkcjami typu kopiuj i wklej, edytor posiada także funkcję kolorowania kodu . Edytor automatycznie rozpoznaje podstawowe słowa kluczowe
36
VisIlai C++ 2005. Od podstaw w języku C++ i nadaje im odpowiedni kolor, zgodnie z ich przeznaczeniem. Funkcja ta nie tylko sprawia, że kod jest o wiele bardziej czytelny, ale także pozwala natychmiast zoriento wać się, że został popełniony błąd przy wpisywaniu tych słów.
Kompilator Kompilator konwertuje kod źródłowy na kod obiektowy oraz wykrywa błędy występujące podczas procesu kompilacji i o nich raportuje. Kompilator potrafi wykryć wiele różnego rodzaju błędów związanych z nieprawidłowym lub nierozpoznanym kodem, jak również błę dów strukturalnych, np. kiedy fragment kodu nigdy nie zostanie wykonany. Kod obiektowy wygenerowany przez kompilator przechowywany jest w plikach zwanych plikami obiekto wymi. Istnieją dwa rodzaje kodu obiektowego, który może zostać wygenerowany przez kom pilator. Kody te zazwyczaj przechowywane są w plikach o rozszerzeniu .obj.
Program łącząCY Program łączący (konsolidator) dołącza różne moduły wygenerowane przez kompilator z pli ków z kodem źródłowym, dodaje wymagane moduły z kodem z bibliotek dostarczanych jako część C++ oraz łączy wszystko w jedną wykonywa1ną całość. Konsolidator może także wykrywać błędy i o nich raportować, np. gdy brakuje części programu lub gdy znajdzie odwołanie do nieistniejącego komponentu biblioteki.
Biblioteki Biblioteka to po prostu zbiór wcześniej napisanych procedur, które rozszerzają możliwości C++, dostarczając standardowych, profesjonalnie zaprojektowanych jednostek kodu, które można wykorzystać we własnych programach w celu wykonania niektórych częstych operacji. Operacje zaimplementowane przez procedury w rozmaitych bibliotekach dostar czanych przez Visual C++ 2005 znacznie zwiększają produktywność, ponieważ pozwalają zaoszczędzić czas potrzebny na napisanie i testowanie kodu dla tych operacji. Wspominałem już o bibliotece platformy .NET, ale jest ich o wiele więcej, zbyt wiele, by je wszystkie tutaj wymienić, ale najważniejsze z nich zostaną opisane.
języka
Standardowa biblioteka C++ definiuje podstawowy zestaw procedur wspólnych dJa wszyst kich kompilatorów C++ ISO/ANSI. Zawiera wiele procedur, na przykład funkcje operujące na liczbach (na przykład obliczające pierwiastek kwadratowy czy funkcje trygonometryczne), procedury przetwarzania znaków i ciągów, takie jak klasyfikacja znaków i porównywanie ciągów, a także wiele innych. W trakcie nauki języka C++ ISO/ANSI nauczymy się posługi wać wieloma z nich. Istnieją także biblioteki obsługujące rozszerzenia C++/CLI do C++ ISO/ANSI. Natywne aplikacje oparte na oknach obsługiwane są przez bibliotekę zwaną Microsoft Foun dation Classes (MFC). Biblioteka ta w znacznym stopniu ułatwia proces tworzenia graficz nego interfejsu aplikacji. Więcej na temat biblioteki MFC dowiemy się po zakończeniu nauki języka C++. Inna biblioteka zawiera zestaw narzędzi - zwanych Windows Forms - mniej
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
37
więcej o d po w i ad ający c h
bibliotece MFC dla programów opartych na oknach, które wyko nywane są za pomocą ś ro d o w is k a .NET . W dalszej częśc i ks i ążki dowi emy się, jak używać biblioteki Windows Forms do tworzenia programów.
Używanie IDE Wszystkie programy w tej książce s ą tworzone i wykonywane wewnątrz IDE. Po uruchomien iu aplikacj i Visual C++ 2005 powinni śm y zoba czyć okno podobne do tego na rysunku 1.3. 1'';
m [gJ
Start Page - Microsoft Visual Studio Edit View rocs
File
- =t
..
l'r1SON: Vlsual C+ +
Recent Proleci'
C+ + At Wark : IRegis:tr ar , Finding su bme nus . and Mor-e Fr l, 08 sep 2C06 21:49 ;4 1 GMT - Thls rnonlh : DLL problams, conta xt menus, rvu= C str j-qs to managedC++, and mors.
C+ + At Wark : Cr eat e dynamie dialogs, sat ellite Dt.Ls, and more fv'o1, 07 Aug 2006 2 1:36 :2 1 GMT - Th is mon lh Pau l rx.ssoa te ac hes
Open : Create:
Project.;
1\.';Jf"-!'"' s a..
Pro]BC t ..
[weo SIt..
rea ders the rkjl t wCtI to ( rea le dyna mk: dł a logs , explains setalute OLls and discusses lcI1guagereso.rce Oll s. C + + At Wark : Customizing Combobox and Listbox Fn, 07.1J ł 2OC620 :29 :28 GMT - This rnonlh Faul Djt esc ta codes same M ~rosoft OffK:e· styls dialog box featur es.
Net ti ng C + + : Resouc:e Cleanup
Ge lting Sta rted
No detin1t.1on e e i e e e e e
Ready
Rysunek 1.3 Okno znajduj ące się po lewej stronie na rysunku 1.3 to okno ekspl oratora rozwiązań (So/ution Exp/orer), okno znajdujące się w prawym górnym rogu, w którym obecnie wyświetlona zo st ała strona startowa, to okno edytora (Edit or window), a okno na samym dole to okno wyj ś ci a (Output window). Okno eksploratora rozwiązań umożl iwia nawigację pomiędzy plikami programu oraz wy świetlan i e ich zawarto ści w oknie edytora, a także dodawanie nowych plików do programu. Okn o Solution Explorer może m ieć do trzech dodatkowych z akładek (na rysunku 1.3 widoczne s ą tylko dwie) , które reprezentują C/ass View (widok klas), Resour ce View (widok zasobów) oraz Property Manager (menedżer właściwo ści) aplika cji . Wyboru wyśw i e t l a nyc h zakład e k można dokonać w menu View. Okno edytora służy do wprowadza nia i modyfikowania kodu źródłowego oraz innych komponentów aplikacji. Okno Output wyświetla komunikaty powstałe w wyniku kompilacji i konsolidacji programu .
38
Visual C++ 2005. Od podslaw
Opcje paska narzędzi Paski narzędzi , które mają być wyśw i etl an e , w Visual C++ można wybrać, przyci skiem myszy w polu paska n arzędzi. Pojawia się menu zawieraj ące pasków n arzędzi (rysunek lA). Aktywne paski są zaznaczone.
Rysunek 1.4
'" l:=: '"
IL......J
klikając
prawym
listę dostępnych
Bu ild Class Designer Crystal Reports - In ser t Crys ta I Reports - Ma in Data Design Database Diagr-am
El
Debug Debug Location Device Dialog Edito r Formatti ng Help HTML Source Edit ing Image Editor Layout Query Designer Report Border s Report For matti ng Sour ce Contro i
El Standar d Sty le Shee t Tab le Designer Text Editor
W menu tym można wybrać , które paski narzędzi maj ą być zawsze widoczne. Można sobie podobne ś ro d ow i s ko pracy jak na rysunku 1.3, wybi eraj ąc kolejno elementy menu: Sui/d, Class Designer, Debug , Standard oraz View Designer. Aby wybrać element z listy, na leży klikn ąć na szarym polu po jego lewej stronie . Aby sc h ować element, trzeba kliknąć znak zaznaczenia znajdujący si ę po jego lewej stronie.
stworzy ć
Nie ma potrzeby przeładowywać okna aplikacji paskami n arzędzi , które mogą s ię nam kiedyś Niektóre z nich pojawiają się automatycznie, gdy są potrzebne, a więc prawdopo dobnie w większości przyp adków najlepszym rozwiązaniem okażą się domyślne ustawienia. Podczas tworzenia programu mo żemy dojść do wniosku , że byłoby wygodniej, gdyby niektóre paski narzędzi były wyświetlone cały czas. Zestaw wyświetlanych pasków można modyfi kowa ć wedle potrzeb, klik ając praw ym przyciskiem myszy na szarym polu w obrębi e paska n arzędzi i wybieraj ąc żąd an e paski z menu kontekstowego. przydać .
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
39
Podobnie jak we wszystkich programa ch w Windowsie, w paskach narzędzi Visual C+ + 2005 dostępn e są chmurki z podpowiedziami . Aby dowiedzieć s ię. do czeg o służy dana opcja, należy umieścić nad nią kursor i odcz ekać sekundę lub dwie na wyświetlenie informacji w chmurce.
Dokowalne paski narzędzi Dokowalny pasek narzędzi to taki , który można dowolnie przemieszczać w obrębie okna za pomocą myszy . Kiedy zostanie umieszczony w pobli żu jednej z czterech krawędzi okna , jest dokowany i wyglądem przypomina paski widoczne na górze okna. Pasek znajdujący się w górnej linii, zawierający ikony dyskietek i ramkę tekstową po prawej stronie lornetki, nosi nazwę Standard. Można go przeciągnąć w inne miejsce okna , klikając go lewym przyciskiem myszy i - nie puszczając tego przycisku - umieszczając go w innym dowolnym miejscu. Po odciągnięciu od krawędzi pasek zamienia się w oddzielne okno, które można dowolnie przemieszczać.
Po odciągnięciu dokowalnego paska narzędzi wygląda on jak standardowy pasek widoczny na rysunku 1.5. Ma on postać niewielkiego okna opatrzonego odpowiednią etykietą. Pasek w takim stanie nazywa się paskiem pływającym. Wszystkie paski narzędzi widoczne na rysunku 1.3 mogą być dokowane i pływające. Spróbuj przeciągnąć niektóre z nich. Kiedy zostaną zadokowane, wracają do swojego dawnego wyglądu. Paski można dokować przy każdej z czterech krawędzi okna głównego.
Rysunek 1.5 Niektóre ikony paska zadań Visual C++ 2005 będą wyglądały podobnie do tych znanych z innych aplikacji systemu Windows, ale ich przeznaczenie w Visual C++ może być trochę inne. Z tego powodu będę wyjaśniał, do czego one służą, gdy nastąpi potrzeba ich użycia. Jako że do tworzenia nowego programu za każdym razem trzeba tworzyć nowy projekt, dobrym punktem startowym nauki obsługi Visual C++ 2005 będzie zaznajomienie się z mechanizmem definiowania projektów.
Dokumentacia Nadarzy się wiele sytuacji, w których będziemy mieli potrzebę zasięgnięcia dodatkowych informacji o Visual C++ 2005. Wyczerpującym źródłem wiedzy na ten temat jest dokumenta cja MSDN (ang. Microsoft Development Network Librarys. Zawiera ona opis wszystkich możliwości programu, a także wiele innych informacji. Podczas instalacji Visual C++ 2005 pojawia się opcja pozwalająca zainstalować pełną dokumentację MSDN. Jeżeli dysponujesz wystarczającą ilością miejsca na dysku, to zachęcam Cię do jej zainstalowania.
40
Visual C++ 2005. Od podstaw Aby uzyskać dostęp do zasobów MSDN, należy nacisnąć klawisz F l . Menu Help umożliwia przeszuk iwanie dokumentacji na różne sposoby. Poza źródłem wiedzy o możliwościach pro gramu, dokumentacja MSDN stanowi przydatne narzędzie, gdy ma się do czynienia z błędam i w kodzie , o czym przekonamy się w dalszej części rozdziału .
Projekt' i rozwiązania Projekt jest zbiorem wszystkich składników składających się na program - może to być program konsolowy, program oparty na oknach lub jeszcze inny typ programu. Zazwyczaj składa się on z jednego lub większej liczby plików z kodem źródłowym oraz prawdopodobnie innych plików zawierających dodatkowe dane . Wszystkie pliki projektu przechowywane są w folderze projektu , a szczegółowe informacje o nim przechowywane są w pliku XML o rozszerzeniu . vcproj, który również znajduje s i ę w folderze projektu. Folder projektu zawie ra równ ież inne foldery, w których zapisywane są pliki powstałe w procesach kompilacji i konsolidacji projektu. Czym jest rozwiązanie, mówi już sama jego nazwa. Jest to mechanizm łączący wszystkie pro gramy i inne zasoby składające się na rozwiązanie jednego problemu związanego z przetwa rzaniem danych . Na przykład rozproszony system zgłaszania zamówień dla operacji bizneso wych mógłby składać się z kilku różnych programów, z których każdy mógłby być rozwijany jako projekt w obrębie jednego rozwiązania. Tak więc rozwiązanie stanowi folder, w którym przechowywane są wszelkie informacje dotyczące jednego lub większej liczby projektów. Co za tym idzie, w folderze tym znajduje się co najmniej jeden podkatalog z projektem. Dane na temat projektów rozwiązania przechowywane są w dwóch plikach o rozszerzeniach .sln oraz .suo. Nowe rozwiązanie tworzone jest automatycznie, gdy tworzy się nowy projekt, chyba że dodamy go do już istniejącego rozwiązania. Kiedy podczas tworzenia projektu zostanie utworzone rozwiązanie , to istnieje możliwość póź niejszego dodawania do niego następnych projektów. Można dodawać projekty dowolnego rodzaju, ale zazwyczaj dodaje się takie, które są w jakiś sposób powiązane z już istniejącym lub istniejącymi projektami w rozwiązaniu. Z reguły, jeżeli nie istnieją żadne przeciwwska zania, każdy projekt powinien być przypisany do jakiegoś rozwiązania. Wszystkie przykłady w tej książce stanowią pojedyncze projekty z własnymi rozwiązaniami .
Definiowanie projektu Pierw szą czynnością, którą należy wykonać , aby rozpocząć pisanie programu w Yisual C++ 2005, jest stworzenie nowego projektu, kolejno wybierając opcje File/New/Project lub naci skając kombinację klawiszy Ctrl+Shift+N. Poza plikami zawierającymi cały kod i wszelkie inne dane, które składają się na program , w folderze projektu znajduje się plik XML. Są w nim zapisywane wszystkie opcje Yisual C++ 2005, których używaliśmy. Mimo że nie ma potrzeby własnoręcznego edytowania tego pliku (tym zajmuje się w całości IDE), to można go otworzyć i sprawdzić, co zawiera. Pamiętaj tylko, aby nie zmienić przez przypadek jego zawartości.
Na tym
skończymy, jeśli
chodzi o teorię. Czas
zabrać się
do pracy.
Rozdział1.
• Programowanie przy użyciu Visual C++ 2005
41
~ Tworzenie proiektu aplikacji konsolowej dla systemu Win32 Utworzymy teraz projekt aplikacj i konsolowej. Najpierw z menu Fil e n ależy w ybr a ć opcje New/Projec t. Pojawi się okno dialogowe tworzenia nowego projektu, podobne do pokazanego na rysunku 1.6. -
_-
..
-
-
-- -
-----
-
----
-
[1]rBJ
New Project Project types :
- ---,
lIisual Studio installed templates
1 - - - -.0. 0.00.. 0 -
ATL CLR
00
;~ W in32 Console Applicatron
General
t
m(@,
Templates :
;= Visual C++
Mf C Smar t Oevt e Win32
Other Languages
O\her ProJect Types
I
1'5IW k132 ProJect
I
~v Temp lat~
. Search Onlne Templ atesooo
L
IA prcject for creatrng a Wrn32 consoleapplication LQCation:
l ID :\Translations'ł>ellon\jvO'
sa"!lon Name :
,
Name :
Cwl _0l
I CW1_Ol
~'
Hortons Visual C++
.. -
I
-
L~l l
2005\Przyk łady
I
~Create di' ectDry for -soluton
I
ca ncel
I
--
I
I
BrOWS8 ...
OK
II
Rysunek 1.6 W lewym panelu okna dialogowego New Pr oject pokazane s ą dostępne typy tworzonych projektów . My wybieramy Win32. W ten sposób informujemy program , którego kreatora ma użyć do utworzen ia wstępn ych plików projektu . W prawym panelu widoczna jest lista szablo nów dostępnych dla wybranego typu projektu. Wybrany szablon zostanie wykorzystany przez kreator podczas tworzenia plików projektu. W następnym oknie, które poj awia się po klik n ięciu przycisku OK, możemy ustawić opcje dla tworzonych plików. W przypadku większo ści typów lub szablonów autom atycznie tworzony jest podstawowy zestaw modułów źródło wych programu . Możemy
teraz wpisać w polu Name wybraną nazwę dla naszego projektu, np. Cwl_Ol. Visual C++ 2005 pozwal a na stosowanie długich nazw plików, a więc mamy tu duże pole manewru. Nazwa folderu rozwiązania pojawia się w polu tekstowym na dole i domyślnie jest taka sama jak nazwa projektu. W razie potrzeb y można j ą jednak zmienić . W tym samym okn ie można także zmieni ć lok al izację folderu rozwiązani a na dysku za pomoc ą pola Location. Jeżeli wpi szemy tylko nazwę projektu, to zostanie on umieszczony w folderze o takiej samej nazwie w lokalizacji pok azanej w polu Location. Domyślnie, jeżeli fold er rozwiązania nie istnieje, zostanie on automatycznie utworzony. Jeśli chcemy, aby nasze pliki zostały zap isane w innym
42
Visual C++ 2005. Od podstaw katalogu , wystarczy zmienić ścieżkę w polu Location, wpisując ją ręcznie lub wyszukując za pomo cą opcji Browse. Kliknięcie przycisku OK spowoduje ukazanie s i ę okna dialogowego kreatora aplika cji Win32, który został zapre zentowan y na rysunku 1.7.
Rysunek 1.7 Welcom e to th e Win32 Application Wlzard
These ere the currentprO)ect settings:
Ovet"łiew AppkaŁkm
5ettirqs
• ccesce ~(~ion
ekkFinishfrem any wroow to eccept tbe current seł:Łf'J05 . Afteryou creete the project, see the pecject 'sreeone. txt f ~ e for inforrMtion ebout the projectfeaturesand flles that.ere ęenereted.
",. "
"' I
LI
Next>
II
F" j,h
II
C.ncel
l
Okno to zawi era informacj e o wybr anych opcjach. Kliknięcie przycisku Finish spowoduj e utworzenie na podstawie tych informacji wszystkich plików projektu. W oknie tym możem y także zmienić ustawienia aplikacji , klikając Application Settings po lewej stronie kreatora, jak pokazano na rysunku 1.8.
Rysunek loB Appllcation Set tings
Overvlew
AppIication SeUjng
-
A _c_ tive _ '_OlJ _tioo --,-p _latfur _ m_'
~r! ~32y .,---------.J
'V
--'---' Platform
WII'132
r1JlR! __ ;u ~
-----,
--"
Buikl
o
c_
Wybierz interesującą Cię konfigurację, a następnie naciśnij przycisk Close. Podczas two rzenia aplikacji będziesz pracować z konfiguracją testową. Po przeprowadzeniu testów przy użyciu konfiguracji debugowania, kiedy program nie sprawia żadnych problemów, zazwyczaj kompiluje się go jako wersję ostateczną. W ten sposób powstaje zoptymalizowany kod bez zbędnych możliwości debugowania i śledzenia, co sprawia, że działa on szybciej i zajmuje mniej pamięci.
Uruchamianie programu skompilowaniu rozwiązania możemy uruchomić nasz program, wciskając klawiszy Ctrl+F5. Powinno ukazać się okno, które zostało zaprezentowane na rysunku 1.12.
Po
pomyślnym
kombinację
48
Visual C++ 2005. Od podstaw
RysUnek 1.12
.. .
••
\H taj ś w Le c I e Aby kun ly n u uu",ć~
Ih' c i ćn i j
d O \.łOl ny
x
_ D
k lrt uis..! .
.
II _
_ _
Jak widać na rysunku, tekst, który znajdował się pomięd zy podwójnymi cudzysłowami, został wypisany w wierszu poleceń . Znaki \ n na końcu ciągu to specjalna sekwencja, zwana znakiem zastępczym, która oznacza znak nowego wiersza. Znaki zastępcze służą do reprezentowa nia w łańcuchach znaków, których nie można bezpośrednio wpisać z klawiatury.
Rm!rImI Tworzenie pustego projektu konsolowego Poprzedni przykład zawiera pewną ilość niepotrzebnego bagażu, który nie jest nam potrzebny podczas pracy z prostymi przykładami w języku C++. Domyślnie wybrana opcja prekom pilowanych nagłówków spowodowała stworzenie w projekcie pliku stdafx .h. Mechanizm ten usprawnia proces kompilacji w przypadku programu składającego się z dużej liczby plików, ale w wielu naszych przykładach jest on niepotrzebny. W takich przypadkach rozpoczynamy od utworzenia nowego projektu , do którego możemy dodać własne pliki źródłowe. Możesz sprawdzić, jak to dzi ała, tworząc nowy projekt w nowym rozwiązaniu dla programu konso lowego Win32 o nazwie Cwl_02. Po wprowadzeniu nazwy projektu i kliknięciu przycisku OK kliknij Applieation Settings po lewej stronie okna dialogowego, które się ukaże. N astęp nie po prawej stronie odszukujemy opcję Empty projeet i zaznaczamy ją, jak pokazano na rysunku 1.13.
Rysunek 1.13
Win32 ~P lic atio n Wizard - Cw1 ~02-
-
-- - m~
-
Application Settings
Overview
App~ cation type:
o l1ijr>Jows
. Add comrr.on heeder nes for:
. ~ic.t ion
o CQ'lsoie.pphcation
eh
O Q.ll
o
~tatic library
Aó1itional Opt IOOS:
o ~mpty proj ect o
t..,rLlft ..
er"'" c'
"lot
(j,-
I nent Class
Resource Web
Utility Proper ty Sheets
_ My T~p~~!~S
.j
Search
I
ontne Templates...
I
L.
l
Creates a file containl '19 CH socrcs eode
Name: Locatlon :
I.
..
I
ICW1.o2/ , I d :\T ransla tJOns\)1elion\) vor HortDns Vlsual CH 2005\,Pr zyklady\Cw l . 02\Cw l . 02
II
Browse., ,
II
Cancel
..
I
Add
I I
Rysunek 1.14 przycisku OK do projektu dodawany jest nowy plik, a jego zawartość zostaje oknie edytora. Plik jest oczywiście pusty, a więc widoczne jest tylko białe pole. W oknie edytora wprowadź do niego n astępującą treś ć :
Po
kliknięciu
wyświetlona w
II Cwl_02.cpp - prosty program konsolowy.
#incl ude
II Podstawowa biblioteka II wejś cia-wyjś cia.
int ma i n() (
st d: :cout st d: :cout st d: :cout ret urn O:
Zauważ, że
« « «
"To j est prosty program. który wy świetla te kst. " « st d .endl : "M oż n a wy św ietl i ć wię c ej lini j ek t ekstu . " « st d: :endl : "powt a r zaj ą c in st r uk cj ę wyj śc i a podob n ą do tej " « st d: :endl .
II Powrót do systemu operacyjnego.
w czasie pisania kodu program automatycznie robi wcięcia. Wcięcia w języku C++ stosowane w celu zwiększenia przejrzystości kodu . Edytor stosuje wcięcie dla każdego wiersza na podstawie zawartości poprzedniego. W czasie wpisywania można także zaobser wować kolorowanie składni. Niektóre elementy programu pokazane są w różnych kolorach, ponieważ edytor automatycznie koloruje elementy języka w zależności od ich przeznaczenia. są
50
Visual C++ 2005. Od podstaw Kod na p ow y ż s zym listingu jest kompletnym programem. Można zauw a ży ć kilka różnic w porównaniu z kodem automaty cznie wygenerowanym przez kreator aplikacji w poprzednim przykładzie . Brakuje dyrektywy #i ncl ude dla pliku stdafx.h. Nie dołączyli śmy tego pliku do naszego projektu , gd yż nie używamy w nim narzęd zia prekompilowanych n agłówków . Tutaj mamy funkcj ę o nazwie ma i n, a w poprzednim przykład z ie była to _ tmai n. W rzeczyw i s tośc i wszystkie programy w C++ ISO/ANSI ro zpoczynaj ą si ę od funkcji main ( ). Microsoft dodał także wersję tej funkcji o nazwie wma i n, która jest u żyw an a przy wykorzystaniu znaków Uni code . Funkcja _ tma i n z o s t ał a zdefin iowana jako ma i n lub wma i n w zależn o ś c i od tego, czy w programie będ ą u żywane znaki Unicode. W poprzednim p rzykładzie funkcja _t ma in została niejawnie zdefini owana jako ma in. Funkcj i o nazwie mai n używamy we wszystki ch przykła dach w CH ISO/ANSI. Instrukcje w yj ś c i a st d: :cout
«
są trochę
inne. Pierwsza instrukcja w main( ) to:
"To j est prosty program. który
wy św tet la
tekst . "
«
st d :endl :
W dwóch miej scach pojawia się o perator « i za każd ym razem powoduje on wysłanie wszystkiego, co po nim następuje, do st d: :cout , czyli stand ardowego strumienia w yj ścio wego. Ciąg spomiędzy podwójnych c u d zys ł o w ó w zostaje w y słany najpierw do strumienia, a następnie do st d: :endl, który zdefiniow any jest w bibliotece standardowej jako znak nowego wiersza. Wc ze śniej wewnątrz łań cu ch a jako znaku nowego wiersza użyl i śm y znaku za s tęp czego \ n umieszczonego w cudzysłowach . Poprzednie wyraże n i e moglibyśmy również zap i s ać w następując y sposób: st d cout
«
"To j est prosty program , który
wyświet l e
t ekst .\ n":
Powinienem wyjaśnić , dlaczego powy ższy wiersz jest na przy ciemnionym tle . Ot ó ż , gdy p is zę wiersz kodu, który już wcześniej wid zieli śmy , to um ieszczam go na białym tle. Po wyż szy wiersz jest nowy, a więc o z naczyłe m to poprzez umie szczenie go na ciemniejszym tle . Tak jak poprzednio, możemy teraz s kom p i l ow a ć nasz projekt. Z auważ, że wszystkie otwarte pliki źró d ł o we zo stan ą zapisane automatycznie, jeżeli nie zr obili śmy tego sami w cze śniej . Po pomyślnym skompilowaniu programu w ci śnij kombinację klawi szy Ctrl+F5 w ce lu jego uruchomienia. Pojawi si ę okno podobne do tego, które jest wido czne na rysunku 1.15.
Rysunek 1.15
"
C:\WINDOWS\system32\cmd.exe
~~ ż~:~~y~~1:~ri2~:t~~:j ~~~i~ekY~;~::~: tekst . ~b~tk~~~~~~o~:~~r~:~1 ~ n~Jj~~t: lr.~d~~:~i~~ l ej .
1I1!1E:l
a
D Błędy Jeżeli
wpiszemy kod programu niepoprawnie, to kompilator zgło s i błędy. Aby sprawdzi ć , jak celowo popełnimy jeden błąd w naszym programie. Je żel i jednak udało Ci s i ę już zro b ić j akiś błąd , to mo że sz go wykorzyst ać do wykonan ia tego ćwicze ni a. Przejdź do okna edytora i usuń średnik znaj d uj ący s i ę na ko ń cu przedostatniego wiersza pomiędzy nawiasami to
działa,
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
51
klamrowymi (ósmy wiers z). N astępnie skompiluj program ponownie. Panel wyników na dole zawiera teraz n astępujący komunikat o błędzi e:
C2143: synt ax error : mi ssi ng ' : ' before ' retur n' Każdy
komun ikat o błędzi e podc zas kompilacji ma swój numer, które go znaczenie m ożna w dokumentacji . W tym przypadku wiadomo, co s p ow o d owało wy s tąp i e ni e pro blemu . Jednak w wielu przypadkach dokumentacja może o kazać s i ę bardzo pomocna w odna lezieniu źródła problemu . Aby otwo rzyć tę część dokumentacji, która dotyczy naszego błędu , n ale ży klikn ąć wiersz zawie rający numer błędu i nacisnąć FI . Pojawi się nowe okno z dodat kowymi inform acjami na tem at błędu. Możn a to wypróbow ać na naszym błędzie. sp rawdzić
Po poprawieniu błędu możemy ponownie spróbować skompilować projekt. Operacja ta prze biega sprawnie, gd yż definicja projektu ś l edzi status plików s kład aj ącyc h s ię na niego. Pod czas normalnego procesu komp ilacji Visual C++ 2005 ponownie kompiluje tylko te pliki, które zmieniły się od momentu poprzedniej kompilacji. Ozn acza to, że j eśli mamy projekt skł adający się z kilku plików źródłowych i od momentu ostatniej kompilacji projektu zmiany wprowadziliśmy tylko do jednego z nich, to tylko ten jeden plik zostanie ponownie skompi lowany przed pro cesem kon sol idacji mając ym na celu utworzenie nowego pliku .exe. Będ ziemy także omawiać kł ad
pro gramy konsolowe CLR, a zatem projektu konsolowego CLR .
poniżej
przedstawiam przy
~ Tworzenie projektu konsolowego elB N a ci śnij
klawisze Ctrl+Shijt+N, aby pojawiło się okno dialogowe New Project. N astępnie jako typ projektu wybieramy CLR, a j ako szablon - CLR Console Application, j ak pokazano na rysunku 1.16. W polu Name wpisz nazwę Cwl _03. Po klikn ię c iu przycisku OK zostaną utworzone pliki projektu. Dla projektu konsolowego CLR nie ma żadnych opcji , a więc przy użyciu tego szablonu zawsze rozpoczynamy pracę z tym samym zestawem plików. Jeśli chcemy stwo rzy ć pusty projekt (w tej książce nie będziem y tego robić ) , to musimy skorzysta ć z innego specjalnie do tego przygotowanego szablonu. Na rysunku 1.17 w panelu Solu/żon Explorer mamy kilka plików gdy tworzyliśmy projekt konsolowy Win32.
więcej, niż widzieliśmy,
W wirtu alnym folderze Resouree Files zn ajdują się dwa pliki . Plik o rozszerzeniu . ico prze chowuje ikonę apl ikacji, która będzie widoczna po zminim alizowaniu okna programu. Plik o rozszerzeniu .re zapisuje zasoby aplikacj i - w tym przypadku zawiera tylko ikonę. W projekcie znajduje się także plik o nazwie Assemblylnfo.cpp . Każdy progr am CLR składa się z jednej lub większej liczby asemblacji, które stanowią zbiór kodu i zasobów tworzących funkcjonalną jednostkę. Asemblacj a zawiera równ ież dużą ilo ś ć danych dla CLR - specy fikacje używanych typów danych , informacje o wersj i kodu oraz czy do tej asembl acji mogą mie ć dostęp inne asemblacje. Mówiąc krótko, asembl acja jest podst awowym tworzywem do budowy wszystkich programów CLR .
52
Visual C++ 2005. Od podstaw -
-
-
-
-
m~ mI§]
New Project Templates :
Projecl types : - v.sual C++ ATL QR
I
--"isu a! studio installed t empl ates
~, ASP. NE T Web Servk:e
~' ''.j ;; l i mfłi1 m!tiM
General
H ,SQL Server ProjecI
MFC Smart Devlce W n32 .!J Other Languages ." Other Project Types
.ill WJndows Form s Controi Laary
r n Class Librar y 3lCLR Emply Projec l ',J1 Wndows Forms Apphcallon ifll W indows Serv ice
t e r r evcave c em . : Sc r i ng A> Aa r::: g S ) ś e t ec t e v j r
re turn O;
'-'- - -
..
.... .
2"; ) JI
Rysunek 1.18 Wszystkie narzędzia biblioteki .NET są zdefiniowane w obrębie jakiej ś przestrzeni nazw. Wszystkie standardowe biblioteki, których najprawdopodobniej będziemy używać , należą do przestrzeni nazw System. Ale czym właściwie je st przestrzeń nazw ? Przestrzeń
nazw jest bardzo prostym pojęciem. W kodzie programu oraz w kodzie tworzącym biblioteki .NET znajduje się bardzo dużo rzeczy , które muszą mieć swoje nazwy (typy danych, zmienne, bloki kodu zwane funkcjami). Z tym związany jest jeden problem - łatwo jest czemuś nadać nazwę, która została już użyta w innym miejscu, co prowadzi do nieporozumień . Przestrzenie nazw pomagają uniknąć tego problemu . Wszystkie nazwy w kodzie biblioteki zdefiniowanej w przestrzeni nazw System poprzedzone są przedrostkiem stanowiącym nazwę tej przestrzeni. Tak więc nazwa Str i ng w bibliotece to w rzeczywisto ści System: :String. Oznacza to, że jeżeli przez nieuwagę u żyjemy w nasz ym kodzie nazwy St ri ng, to w celu odwołania się do tej nazwy w bibliotece .NET możem y u ży ć zapisu System: :St ri ng. Dwa dwukropki ( : :) stanowią operator, który nazywany jest operatorem zasięgu. W naszym przypadku operator ten oddziela nazwę przestrzeni nazw Syst emod nazwy typu Stri ng. W przykładach dotyczących natywnego C++ używali śmy go we fragmentach std: :cout oraz std : :endl. W tym przypadku sytuacja wygląda podobnie - std jest nazwą przestrzeni nazw bibliotek natywnego C++, a cout i end l stanowią nazwy zdefiniowane w obrębie tej przestrzeni i reprezentują odpowiednio standardowy strumień wyj ściowy oraz znak nowego wiersza. Zastosowane w przykładzie wyra żenie us i ng namespace pozwala nam używać dowolnych nazw z prze strzeni nazw System, bez konie czności podawania jej nazwy jako przedrostka. Jeżeli spowodowal i śmy konflikt nazw, to problem ten możemy rozwiązać, usuwając wyraże nia usin g namespace i jawnie przyporządkowującnazwę z biblioteki do nazwy przestrzeni nazw. Wię cej na temat przestrzeni nazw dowiesz s i ę w rozdziale 2. Program możemy skompilować i widoczny jest na rysunku 1.19.
uru chomi ć , naci skając
klawisze Ctrl+F5. Rezultat tego
54
Visual C++ 2005. Od podstaw
Rysunek 1.19
Wynik d ział an i a tego kodu jest taki sam jak kodu z pierwszego tekstu odpowiada wiersz : Consol e: :Wr iteLi ne(L"Wit aj
przykładu .
Za
wyświet lenie
ś wi ecie " ) ;
W tym przyp adku w celu wydru kowania w wierszu po lec e ń za warto ś c i c u dzysłowów p o słu żyl iśmy s i ę funk cj ą na leżącą do biblioteki .NET, a więc jes t to odpowie dnik CLR wyraże nia w natyw nym C++ , którego u ży l iś my w przykła dzie Cwl_O l : st d: :cout « Wy raże n i e
"Witaj
ś w t ec ie
vn" :
CLR j est bardziej przejrzyste
niż wyraż eni e
w natywn ym C++.
Ustawianie opcji wVisual C++ 2005 I stn iej ą dwa rodzaj e opcj i, które m o ż e my ustawi ać. Może my ustawi ć opcje narzęd z i Visua l C++ 2005, które mają zastosowanie w każdy m projekcie, lub opcje dotyczące konkretnego proj ektu, o kreś lające sposó b przetwarzania kod u tego proj ektu podczas procesów komp ilacj i i konsolidacji . Opcje ustawia s ię w oknie dialogowy m Options (Tools/Op tions) . Okno to wi doczne j est na rysunku 1.20. Opti~~5
D Always show Err"," List if build f1nishes wl th errors
D Track Active !tem in so lutlon Explorer 0Show advanCedbuild configuratlons
o Always show solut ion 0 s ave new projects w hen created 0warn user when the project Iocation is no t trusted 0 Show OUtput w rnow when bulld star ts DPrompt for symbohc renaming when renamng files
t OK
Rysunek 1.20
II
cancel
Rozdział 1.
Klikając
jeden ze znaków +
• Programowanie przy użyciu Visual C++ 2005
55
znajdujących się
obok elementów w lewym panelu, spowodujemy podtematów. Na rysunku 1.20 widać opcje podtematu General elementu Projects and Solutions . W prawym panelu widoczne są opcje dostępne dla wybra nego podtematu. W tej chwili interesują nas tylko niektóre z nich , ale dobrze jest poświęcić później trochę czasu na zapoznanie się z pozostałymi . Aby wyświetlić pomoc dotyczącą bie żących opcji , należy kliknąć znak zapytania znajdujący się w prawym górnym rogu okna.
wyświetlenie listy dostępnych
Tworząc
nowy projekt, prawdopodobnie chcemy wybrać domyślny katalog, w którym mają przechowywane wszystkie pliki. Możemy tego dokonać za pomocą pierwszej opcji widocznej na rysunku 1.20. Wystarczy wybrać lokalizację, w której chcemy przechowywać wszystkie pliki naszych rozwiązań i projektów . być
Aby ustawić opcje odnoszące się do każdego projektu C++, należy kolejno wybrać Projects and Solutions/VC++ Project Settings w lewym panelu. Można także dostosować ustawienia tylko dla bieżącego projektu, wybierając pozycję Prop erties z głównego menu Project. Ten element menu zawsze zawiera na
początku nazwę bieżącego
projektu .
Tworzenie iuruchamianie programów dla Windowsa Abyśmy
mogli przekonać się, jak proste jest to zadanie, utworzymy teraz dwie działające aplikacje Windows . Najpierw stworzymy program w natywnym C++, wykorzystującym bibliotekę MFC, a następnie aplikację Windows Forms działającą pod kontrolą CLR. Kod tych programów przedyskutujemy trochę później , gdy będziemy już posiadali niezbędną do ich zrozumienia wiedzę. Jednak już teraz przekonasz się, że procesy ich tworzenia są na prawdę proste.
Tworzenie aplikacji MFC Na początek, jeżeli mamy otwarty jakiś projekt (je śli jest otwarty, to na pasku tytułu głównego okna widnieje jego nazwa), to możemy go zamknąć, wybierając z menu File CIose Solution . Można także utworzyć od razu nowy projekt, a wtedy automatycznie zostanie zamknięte bie żące rozwiązanie.
dla Windowsa, należy wybrać New/Project z menu File lub wcisnąć klawiszy Ctrl+Shijt+N, a następnie wybrać typ projektu MFC oraz szablon projektu MFC Application. Projekt nazwiemy Cwl_04, jak pokazano na rysunku 1.21. Aby
utworzyć program
kombinację
Po kliknięciu przycisku OK pojawi się okno dialogowe tworzenia programu Application Wizard. W oknie tym mamy możliwość wyboru spośród wielu właściwości, które chcemy umieścić w naszej aplikacji . Występują one jako elementy listy po prawej stronie okna, jak widać na rysunku 1.22 . Wielu z nich będziemy używać jeszcze wielokrotnie . Wszystkie te opcje możemy w tej chwili zignorować i zatwierdzić domyślne ustawienia. W tym celu klikamy przycisk Finish, aby utworzyć nowy projekt z domyślnymi ustawieniami. Panel Solutton Explorer w oknie IDE wygląda teraz tak , jak pokazano na rysunku 1.23 .
56
Visual C++ 2005. Od podstaw
Iemplates : Visu al St udio installed templa t es
f~ MFC Appl ication
Mi MFC ActiveX Contra I m\ MFC DLL
ił1
~!.-,:.,!,p lat es
S mart Oevlee Win32 Other Lar<juages
Other Pro ject Types
• ' Search Online Ternplates...
A prcject fOr' creabng an applicatoo that uses the Mk:rosoft Fwndation Class Library t:1ame : ~ocatlon :
SolutlOnNaille:
D:\T ranslabons\{lelion\Ivor Hortons Visual C++ 2005'f'rzyklady
I
Cw l _D4
0 Create 'g irectory fOr solution
OK
'l[
Cancel
Rysunek 1.21 Rysunek 1.22 W elco me t o the MFC Applic at ion Wiz ard
'l hese ere
Overvicw Application Type
Compound Oocument Support Dccument Template Str1nQS
Detebese Suppott User Interface
Peetu-es
AdvencedFeetures Genereted
the ct.r'rent prc ject seUings:
• MtJItiple document nerface • No detebese support
• Nocompound document support Cllck Finishfrorn any windowto eccect the rurrent
settoęs .
After you « ee te the crotect, see the prolect's reedrre .txt f~ e for information
ebout the project teetwes end files that ere qenereted .
dasses
Next
Za uważ, że sc howałem zakła d kę
>
II
Firlsh
II
(oneel
Property Manager, kli kając j ą pra wym przyciskiem myszy i wybie rając Hide z menu , które s ię pojawi ł o. Lista utworzonych plik ów jest dosyć dłu ga . Do pisania programów dla Windowsa trzeba mieć bardzo dużo miejsca na dysku twar dym ! Pliki z rozszerzeniem .cpp zawi erają wykonywalny kod w ję zyku C++ , a z rozszerze niem .h kod w C++ składający s ię z defin icji wykorzystywanyc h w kodzie wyko nywa lnym.
Pliki O rozszerzeniu .ico zawierają ikony. Wszystkie pliki zostały pogrupowane i umiesz czone w odpowiednich podkatalogach dla ułatwienia dostępu do nich . Nie są to jednak prawdziwe katalogi i nie zobaczymy ich w folderze projektu na dysku . Jeśli
zajrzymy do foldera rozwiązania Cwl_04 za pomocą eksploratora Windows lub jakie gokolwiek innego programu służącego do przeglądania plików na dysku , to zauważymy , że w sumie zostały wygenerowane 24 pliki. Trzy z nich znajdują się w folderze rozwiązania , 17 w folderze projektu, a pozostałe cztery w podkatalogu folderu projektu o nazwie res. Pliki w tym folderze zawierają zasoby wykorzystywane przez program - takie jak menu oraz iko ny. Wszystkie te pliki zostają utworzone po wpisaniu żąd anej nazwy projektu . Teraz już łatwo zrozumieć , dlaczego tak dobrym pomysłem j est stworzenie oddzielnego folderu dla każdego projektu. Wśród
plików w folderze projektu znajduje się plik o nazwie ReadMe.txt. W nim znajduje s ię przeznaczenia każdego pliku wygenerowanego przez kreator aplikacji MFC . Można go odczytać za pomocą programów Notepad, WordPad lub nawet edytora Visual C++ 2005. Aby obejrzeć go w oknie edytora, należy kliknąć go dwukrotnie w panelu Solutżon Explorer.
wyjaśnienie
Kompilacia iuruchamianie aplikacii MFC Przed uruchomieniem programu należy skompilować kod źródłowy , a następnie moduły pro gramu poddać procesowi konsolidacji. Robi s i ę to w identyczny sposób jak w przypadku programu konsolowego. W celu zaoszczędzen ia na czas ie można użyć kombinacji klawiszy Ctrl+F5, co spowoduje kompila cję oraz uruchomienie pro gramu .
58
Visual C++ 2005. Od podstaw Po kompilacji projektu w oknie Output widzimy, że nie wystąpiły żadne błędy, a program rozpoczyna działanie. Okno wygenerowanego przez nas programu widać na rysunku 1.24.
Rysunek 1.24
na rysunku , wygenerowane zostało w pełni funkcjonalne okno , z menu i paskiem Mimo że sam program nic określonego nie robi (dodanie funkcjonalności należy do programisty), to wszystkie menu działają. Możemy je wypróbować , a nawet utworzyć nowe okna, wybierając opcję New z menu File .
Jak
w idać
narzędzi .
Myślę, iż każdy się ze mną zgodzi , że utworzenie programu dla Windowsa za pomocą kreatora aplikacji MFC nie wymagało zaangażowania zbyt wielu szarych komórek. Więcej wysiłku będzie trzeba włożyć w zrobienie czegoś pożytecznego z wygenerowanym przed chwilą programem, ale też nie będzie to zadanie zbyt trudne. Niektórzy ludzie piszący powa żne-programy dla Windowsa za pomocą staroświeckich metod , bez użyc ia Visual C++ 2005 , przed rozpoczęciem prób musieli przynajmniej na kilka miesięcy przestawiać się na dietę rybną. Dlatego tak wielu programistów miało zwyczaj jadać sushi . Dzięki Visual C++ 2005 nie ma już takiej potrzeby. Chociaż tak naprawdę nigdy nie wiadomo, co czai się za rogiem w świecie technologii programowania. Jeżeli lubisz sushi, to lepiej nie przestawaj go je ść, tak na wszelki wypadek.
Tworzenie aplikacji Windows Forms W tym przypadku skorzystamy z innego kreatora. Tworzymy jeszcze jeden projekt, ale tym razem wybieramy opcję CLR w lewym panelu okna dialogowego New Project oraz szablon Windows Forms Application. Następnie wpisujemy nazwę projektu Cwl _05, jak widać na rysunku 1.25.
w tym przypadku nie mamy żadnych opcji do wyboru, a więc naciskamy OK w celu utworze nia nowego projektu.
Rozdział 1. -
-
--~
--
-
-
-
-
-
-
• Programowanie przy użyciu Visual C++ 2005
---
-
-
--------
New Project Pro ject types :
-
_ visual .~tudio Installed templates W' ASP.NET Web Service ,~C LR Console Applicatlon ij1SQL Server Project .i'iJ Windows Form s Controi Librar y
General MFC
smart Device Win32 iii Olher Languages
iłI Othar Projec t Types
A proJect for creathj
GlClass LIbrar y ::!J QR Empty Projsct .]:lWhc!ows Forms Application .!mWhc! ows Service
~y Temp la t es
. .: Search Online Templa tes...
an applicalion w llh a Wndows user interface
I v! I
: CWl _osl
Name :
Location: SO....tlon :
-ffi~
CJ9j
Telll'lates :
Ci Visual c++ ATL CLR
59
. iD:\ TranslatKJns\helion\Jva
Hor tons vi sus l c+ + 2005\Pr zy k ł ady
~
!creata new Soubon Solu tion Na me:
-
~
__ _:J -, -
Brows e...
I
Cancel
I
0 create drectorv filr solulion
-
---
I
OK
II
Rysunek 1.25 W panelu Solution Explorer, widocznym na rysunku 1.26, pokazano pliki, które zostały wyge nerowan e dla tego projektu.
app.rc Source F iles ej Asserrbly ln fil .cpp c.::'} c w l _OS.cpp cj stdafx.cpp ReadMe.txt
""ilsolutio...
w tym
projekcie mamy znacznie mniej plików - w sumie jest ich tylko 15 (włącznie z plika Jest to spowodowane między innymi tym, że początkowe GUl jest prostsze n iż w aplikacj i w natywnym C++ używającym biblioteki MFC . Aplikacja Windows Fonns nie
mi
rozwiązania) .
60
Visual C++ 2005. Od podslaw ma żadnych menu ani pasków narzędzi i składa s ię tylko z jednego okna . Oczywiście , łatwo te elementy dodać , ale kreator aplikacji Windows Forms nie zakłada z góry, że potrzebujemy ich od samego po czątku. Okno edytora wygląda trochę inaczej, jak
widać
na rysunku 1.27.
Rysunek 1.27 W okn ie edytora zam iast kodu widzimy okno aplikacj i. Jest to spowodowane tym, że pro jektowanie GUl dla Windows Forms zorientowane jest na tryb graficzny , a nie kodowy. Ele menty interfejsu umies zczamy w oknie, przeciągając je lub umi eszczając w odpowiednich miejscach w trybie graficznym, a Visual C++ 2005 automatycznie wstawia potrzebny kod. Naciśn ięcie klawiszy Ctrl+Alt+X lub wybranie z menu Viewopcji Toolbbx spowoduje poja wienie się dodatkowego okna zawierającego listę składników GUl , jak widać na rysunku 1.28. Okno Toolbox przedstawia listę standardowych komponentów, które można dodawać do apli kacji Windows Forms . Możemy na próbę dodać kilka przycisków do okna projektu Cwl_05. W tym celu klikamy Bulion na liście w oknie Toolbox, a następnie klikamy w obszarze klienc kim okna apl ikacji Cwl_05 wyświetlonego w oknie edytora w miejscu, w którym chcemy umieścić nasz przycisk . Można dowolnie dopasować rozmiar przycisku , przeciągając jego krawędzie, oraz przem ieszczać go w dowolne miejsce za pomocą przeciągania . Aby zm ieni ć jego etykietę, wystarczy ją wpisać - spróbuj napisać Sta rt , a następnie naciśnij Enter. Napis na przycisku zmienia się i jednocześnie pojawia się inne okno, pokazujące właściwości tego przycisku. Nie będziemy się teraz nimi zajmować . Wyjaśnię tylko, że są to opcje pozwalające na kontrolę wyglądu przycisku i że za ich pomocą możemy go dostosować do własnych po trzeb. Spróbujm y doda ć jeszcze jeden przycisk z napisem St op. Okno edytora będzie wyglą dało podobnie do tego na rysunku 1.29. W trybie grafi cznym można w dowolnym czasie dodać dowolny komponent GUl, a kod zostanie automatycznie dopasowany. Spróbuj dodać kilka innych komponentów w podobn y sposób jak pierwsze dwa, a następnie skompiluj i uruchom program, wciskając klawisze Ctrl+F5. Po wykonaniu tych czynności aplika cja prezentuje się w pełnej krasie. Mogłoby być jeszcze prościej?
Rozdzial1. • Programowanie pru u2rciu Visual C++ 2005 Rysunek 1.28
Toolbox
61
• -l=l X
',' AlI WIndows fonn.
; ! Common Cootrols II; Pointer
o Button o CheckBox l "O Checked-istBox ~ ComboBox
~ Da~Timepicker A Label
A LinI 1 -
f:I Form 1
Stall
bJ(gJ[8]
Stop
Podsumowanie W tym rozdziale poznaliśmy podstawy tworzenia różnego typu aplikacji za pomocą Visual CH 2005. Utworzyliśmy i uruchomiliśmy natywne programy konsolowe CLR, a za pomocą kreatorów aplikacji utworzyliśmy oparte na bibliotece MFC programy Windows oraz urucha miany pod kontrolą CLR - Windows Forms.
62
VisIlai C++ 2005. Od podstaw Z tego
rozdziału powinniśmy zapamiętać, że:
•
Common Language Runtime (CLR) to implementacja firmy Microsoft standardu CLI (ang. Common Language Infrastructurei .
•
Platforma .NET składa działające pod CLR.
•
Natywne aplikacje CH pisane
•
Programy napisane w CH/CLI uruchamia
•
Rozwiązanie jest zbiorem rozwiązanie
się
z CLR oraz bibliotek .NET, które są
wspomagają aplikacje
w języku C++ ISO/ANSI. się
za
pomocą
CLR.
jednego lub większej liczby projektów, które razem pewnego problemu związanego z przetwarzaniem danych. składają się
na
tworzą
funkcjonalnąjednostkę
•
Projekt zawiera kod i elementy, które w programie.
•
Asemblacjajest podstawowąjednostką w programach CLR . Wszystkie tego typu programy składają się z co najmniej jednej asemblacji.
Zaczynając
od drugiego rozdziału aż do połowy książki, będziemy tworzyć programy konsolowe. Wszystkie przykłady ilustrujące sposoby użycia elementów języka C++ są uruchamiane przy użyciu aplikacji konsolowych Win32 lub CLR. Do aplikacji opartych na MFC, tworzonych za pomocą kreatora, powrócimy, gdy opanujemy w wystarczającym storiU sekrety języka C++.
2 Dane, zmienne
i działania arytmetyczne
W tym rozdziale nauczysz się podstaw programowania w języku C++. Po jego lekturze na uczysz się pisać proste programy w C++ w tradycyjnej formie: wejście-przetwarzanie-wyjście. Jak już informowałem w poprzednim rozdziale, najpierw opiszę właściwości języka C++ ISO/ANSI, a następnie przejdę do omówienia dodatkowych cech lub różnic w języku C++/CLI. przy użyciu działających przykładów będziemy mieli okazję zdobyć w poruszaniu się po środowisku Visual C++. Dla każdego przykładu w książce, przed przejściem do kompilacji i uruchomienia, należy utworzyć oddzielny projekt. Pamiętaj, że projekty definiowane w tym rozdziale i we wszystkich następnych aż do dziesią tego są aplikacjami konsolowymi .
W trakcie nauki
języka
więcej doświadczenia
W rozdziale tym dowiesz
się :
• Jakajest struktura programu w C++. • Czym są przestrzenie nazw . • Czym są zmienne w C++ . • Jak
definiować
zmienne i stałe w C++.
• O podstawowych operacjach wprowadzania danych z klawiatury oraz danych na ekran. • Jak
wykonywać
obliczenia arytmetyczne.
• Co to jest rzutowanie typów. • Co to jest zasięg zmiennej .
wysyłania
64
Visual C++ 2005. Od podslaw
Struktura programu wC++
Programy działające jako aplikacje konsolowe w Yisual C++ 2005 pobierają dane z wiersza poleceń i do niego też wysyłają wyniki. Aby uniknąć niepotrzebnego grzebania w skompli kowanych mechani zmach tworzenia i zarządzania aplikacją Windows, bez posiadania wiedzy pozwalającej je zrozumieć, wszystkie przykłady mające ułatwić zrozumienie języka C++ będą aplikacjami konsolowymi Win32 lub .NET. Dzięki temu będziem y mogli całkowicie skupić się na języku C++. Po j ego opanowaniu będziemy w stanie poradzić sobie z aplika cjami Windows . Najpierw zobaczymy, jaka jest struktura programu konsolowego. Każdy
program napisany w języku C++ składa się z co najmniej jednej funkcji. W rozdziale l . widzieli śmy przykład programu konsolowego Win3 2, który zawierał tylko jedną funkcję ma t ru ), gdzie main to nazwa tej funkcji . Każdy program w języku C++ ISO/ANSI zawiera funkcję main() i wszystkie programy w C++, bez względu na rozmiar, składają się z kilku funkcji main() , oznaczającej początek programu, oraz pewnej liczby innych funkcji. Funk cja to po prostu blok kodu o unikalnej nazwie, który jest wywoływany w celu wykonania za pomocą jej nazwy. Jak widzieliśmy w rozdziale 1., aplikacja konsolowa Win32 wygenero wana przez kreator aplikacji ma podstawową funkcję o nazwie _tmai n. Nazwa ta może ozna czać zarówno main,jak i wmai n, w zależności od tego , czy program używa znaków Unicode, czy nie. Nazwy wmain i _tmain zostały stworzone przez firmę Microsoft. Nazwa tej funkcji w standardzie CH ISO/ANSI to mai n. Będę jej używał we wszystkich przykładach w języku CH ISO /ANSI. Typowy program komunikujący się za pomocą wiersza taką, jaka została zaprezentowana na rysunku 2.1.
poleceń może posiadać strukturę
Wykonywanie programu przedstawionego na rysunku 2.1 zaczyna się w miejscu rozpoczęcia funkcji main() . Następnie kontrolę przejmuje funkcja input_names (), która zwraca wynik w miejscu znajdującym się bezpośrednio za tym, w którym została wywołana w nat ru ). Następnie w funkcji main() wywoływana jest funkcja sort_names(), a po powrocie kontroli do main() wywoływana jest ostatnia funkcja output_names () . Po zakończeniu wysyłania wy ników na wyjście kontrola wraca z powrotem do funkcji mai n() i program kończy działanie . Oczywiście , różne
programy mogą mieć całkiem inną strukturę, ale ich wykonywanie zawsze rozpoczyna się od funkcji main(). Główną zaletą podziału programu na funkcje jest fakt, że poszczególne jego części można pisać i testować oddzielnie. Drugą zaletą jest możliwość wykorzystania raz napisanych funkcji w innych programach. W bibliotekach C++ znajduje się wiele standardowych funkcji, których można używać we własnych programach. Dzięki nim można zaoszczędzić bardzo dużo pracy. Więcej
na temat tworzenia i
~
używaniafunkcji
dowiesz
się w
rozdziale 5.
Prosly prOgram
Prosty przykład nowy projekt -
pomoże
nam lepiej zrozumieć poszczególne elementy programu. Utwórzmy to szybko zrobić za pomocą kombinacji klawiszy Ctrl+Shift+N.
możemy
Rozdział2.
• Dane, zmienne i działania arytmetyczne
65
Po wywołaniu funkcja zaczyna wykonywanie kodu od pierwszego wiersza void lnpurnamest) [
Wykonywanie programu rozpoczy na się od funkcji mainO
II ... return ;
int mainO )
(
inputjrarnesl):
sort _namesO; void sort jrarnest) output_names();
[ +-
'--
II ...
return O; return ; )
l
Wynik działania funkcji mainO zostaje zwrócony do systemu operacyjnego Wynik działania funkcji zostaje zwrócony za miejscem, w którym została wywołana
void outpurnarnest) [
II ... return ;
l
Rysunek 2.1 Kiedy pojawi się okno dialogowe New Project, widoczne na rysunku 2.2, wybierz typ pro jektu Win32 i szablon (ang. templatey Win32 Console Application . Projekt nazwij Cw2_01. Po kliknięciu przycisku OK ukaże się nowe okno dialogowe, podobne do tego, które widać na rysunku 2.3. Przedstawione w nim są podstawowe informacje na temat tego , co zostanie wygenerowane przez kreator. Kliknięcie Application Settings po lewej stronie okna spowoduje wyświetlenie dodatkowych opcji dla aplikacji Win32 , jak widać na rysunku 2.4. Domyślnie utworzona zostanie aplikacja konsolowa zawierająca plik z funkcją main () , ale my zaczniemy pracę od najbardziej podstawowej struktury projektu. W tym celu zaznaczamy opcję Empty project i klikamy przycisk Finish. Utworzony został nowy projekt, ale nie zawiera on żadnych plików. Jego zawartość można sprawdzić w panelu Solution Explorer, jak widać na rysunku 2.5.
66
Visual C++ 2005. Od podstaw --
-
-
-
-
--
-
(1]1&1
New Project
lm @]
Ta rrolates :
Project types:
r
8 vis ual C++
ATl
Visua! _~tudio
installed templates
~ Win32 Console Applicatlon
, CLR General
31 Wln32 Project
My Templates
MFC Smart Oevice
1 .'
Wln32
Search Onllne TemplalEs..,
I
Othsr Languages
- Oltl er Project ryp ss
I I I
II ..J!
A project for creating a Win32 console application
Name:
"
Location:
-- - - §T rans la t lor~ 'I1e lion \I ,a- HorlOns
SOlution Name:
I c w2_01 _ _
---- - - -
I
,
[ Cw2_01
Visual CH
2005l/'r~ła dy
dl
Browse..,
I
Cancel
I
I o CrealE directDry for solutbn
- - ---- --
l
OK
II
Rysunek 2.2 Rysunek 2.3 Welcome to th e Win32 Applicatlon Wlzard
o vervtew Application Settings
These ere the rurrent oroject s e trinęs : •
Consołe
applicalion
dek Flnish łrom any windOlł\l to ecceot the CLU"ren\':settinQs; , After yOU creete the project, see the prcjecrs readme.t xt filefor inforTl"latiOn aboutthe proe ct feetwes andfi1es thet M e eenereted .
" H.
Pracę
N_xl >
li
FI",sh
I[
Concel
rozpoczniemy od dodania nowego pliku źródłowego do projektu. W tym celu klikam y prawym przyciskiem myszy Source Fi/es w panelu Solution Explorer i wybieramy kolejno Add/New ltem. W tym momencie powinno pojawić się okno dialogowe Add New Item , podob ne do widocznego na rysunku 2.6.
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
67
Rysunek 2.4 Appllcation Settings
Applic~tłon
Addcommon beeder files for:
Apphcation tvpe:
Overview
Settings
o \'1.ind_ appkcalioo o C
""
.]1
Cw2 01
"MSm'.i@"Mj,\·Im'm Macrosand Constants
'-
' ,
,
"
• ..
_ .-
._
..
.~ mai1(void)
--
~ So lutJon E
1 ę] , który dodaliśmy na sam ym poc zątku do programu za pomo cą dyrek tywy #i nc l ude. cout w bibliotece standardowej jest nazwą, a wię c należy do przestrzeni nazw std. Gdyby nie deklaracja usi ng, to nazwa ta nie zostałaby rozpoznana , chyba że użyl ibyśmy pełnej nazwy z kwalifikatorem - std : :cout, jak już w s p o m i n ałe m wcześn iej . Ze względu na fakt , że cout jest nazwą zarezerwowanądo reprezentowania standardowego strumienia wyj ściowego, nie powinno się jej używać do innych celów, a więc na przykł ad nie można jej stosować jako nazwy zmiennej w programie. Oczywiście używanie tej samej nazwy w odnie sieniu do różn ych rzeczy zwiększa prawdopodobieństwo powstania nieporozumień.
74
Visual C++ 2005. Od podstaw Druga instrukcja
wyjścia
podzielona została na dwa wiersze:
cout « "Mamy nie t yl ko p om a rańcze .. . « "- w sumie mamy " « fruit «
"
« endl
owoców. ":
już mówiłem wcześniej, każdą instrukcję można podzielić
na dowolną liczbę wierszy, czemu kod stanie się bardziej przejrzysty. Koniec instrukcji zawsze oznaczony jest średnikiem, a nie końcem wiersza. Następujące po sobie wiersze są odczytywane i łączone przez kompilator w jedną instrukcję do momentu napotkania średnika oznaczającego koniec instrukcji. Oznacza to, że jeżeli zapomnimy na końcu instrukcji postawić średnik, to kompila tor potraktuje następny wiersz jeszcze jako część tej instrukcji. W rezultacie zazwyczaj po wstaje coś , czego kompilator nie potrafi zrozumieć, więc zgłasza błąd .
Jak
dzięki
Instrukcja ta wysyła łańcuch znaków: ,,Mamy nie tylko pomarańcze ... " do wiersza poleceń , po którym następuje znak nowego wiersza (endl), łańcuch" - w sumie mamy", wartość zmien nej fruit oraz łańcuch "owoców". Nie ma żadnych przeciwwskazań co do łączenia w taki sposób różnych wysyłanych elementów . Instrukcja wykonywana jest od lewej do prawej, a każdy element wysyłany jest do strumienia eout. Należy zauważyć, że każdy element, który ma zostać wysłany do eout , poprzedzony jest operatorem « . Trzecia i zarazem ostatnia instrukcja wyjścia wysyła już tylko znak nowego wiersza. Rezultat wszystkich tych trzech instrukcji oglądamy na ekranie. Ostatnia instrukcja w programie to:
retur n
o:
// Koniec programu.
ona wykonywanie funkcji maint ), która zatrzymuje program. Kontrola powraca do systemu operacyjnego . Bardziej szczegółowo na temat tych instrukcji będziemy jeszcze mówili.
Kończy
Instrukcje w programie wykonywane są w takiej kolejności, w jakiej zostały wpisane , chyba że któraś z nich celowo ten stan zmienia. Instrukcje zmieniające normalną kolejność wykony wania opisane zostały w rozdziale 3.
Riale znaki Terminem białe znaki w C++ określa się wszystkie znaki tabulacji, nowego wiersza , przesu nięcia strony oraz komentarze. Białe znaki rozdzielają poszczególne części instrukcji oraz pozwalają kompilatorowi zorientować się, gdzie kończy się jej jeden element (np. i nt ), a gdzie zaczyna następny. W innych przypadkach białe znaki są ignorowane i nie wywołują żadnego efektu. Spójrzmy na i nt
fruit:
Pomiędzy
poniższą przykładową instrukcję : // ...i jeszcze j edna zmienna typu ca łko wi t ego .
i nt a fr uit musi być co najmniej jeden biały znak, aby kompilator mógł rozróż te dwa elementy. Można oczywiście dodać więcej białych znaków, ale zostaną one zigno rowane . Wszystko, co znajduje się za średnikiem, to białe znaki, a więc są one ignorowane. nić
Rozdział 2.•
Spójrzmy teraz na inny
fr uit
~
apples
+
Dane. zmienne i działania arytmelyczne
75
przykład:
oranges :
II Obliczani e sumy wszystkich owoców.
Pomiędzy
frui t i = oraz = i app l es nie muszą występować żadne białe znaki, choć można je chcemy. Bierze się to stąd, że znak = nie jest znakiem należącym do alfabetu ani cyfrą, a więc kompilator potrafi oddzielić go od otaczających znaków . Podobnie wygląda sytuacja ze znakiem + - nie ma konieczności wstawiania białych znaków w jego sąsiedztwie, ale można to zrobić w celu zwiększenia czytelności kodu.
zastosować,jeżeli
Jak już powiedziałem , białe znaki używane są do rozdzielania elementów instrukcji, w in nych przypadkach są one ignorowane przez kompilator (z wyjątkiem oczywiście łańcuchów znaków w cudzysłowach) . Dzięki temu, aby zwiększyć przejrzystość kodu w swoich progra mach, możemy wstawiać dowolną liczbę białych znaków, tak jak zrobiliśmy wcześniej, roz bijając instrukcję wysyłającą dane na kilka wierszy . Pamiętaj, że w C++ koniec instrukcji oznaczany jest średnikiem.
Bloki instrukcji Kilka instrukcji
można umieścić pomiędzy
nawiasami klamrowymi . To, co powstaje, nazywa blokiem jest ciało funkcji. Taką złożoną instrukcję można traktować jako pojedyncze wyrażenie (o czym przekonamy się przy omawianiu podejmowania decyzji w C++ w rozdziale 3.) W rzeczywistości wszędzie tam , gdzie można wstawić pojedynczą instrukcję, można również wstawić cały blok instrukcji otoczony nawiasami klamrowymi. W konsekwencji wewnątrz bloków mogą znaleźć się inne bloki, które można zagnieżdżać w dowolnej liczbie. się
blokiem instrukcji lub
instrukcją złożoną . Przykładowym
Bloki instrukcji wywierają także poważny wpływ na zmienne, ale więcej na ten temat powiem w dalszej części rozdziału przy okazj omawiania zasięgu zmiennych.
Programy konsolowe generowane automatycznie W poprzednim przykładzie utworzyliśmy pusty projekt bez żadnych plików źródłowych, które później dodaliśmy samodzielnie. Jeśli pozwolimy kreatorowi na wygenerowanie projektu tak jak w rozdziale l., to utworzy w nim kilka plików, których zawartości powinniśmy dokładnie się przyjrzeć. Utwórzmy nowy projekt konsolowy Win32 o nazwie Cw2_01A i tym razem pozwólmy kreatorowi działać, nie zmieniając żadnych ustawień. Projekt zawiera trzy pliki z kodem: pliki źródłowe Cw2_0IA.cpp i stdafx.cpp oraz plik nagłówkowy stdafx.h, Pliki te dostarczają podstawowych narzędzi, których możemy potrzebować w programie konsolowym, i składają się na działający program, który w obecnym stanie nic nie robi . Jeżeli mamy otwarty jakiś projekt, to możemy go zamknąć, wybierając z menu File polecenie C/os e Solution. Możemy także utworzyć nowy projekt, a wtedy bieżący zostanie automatycznie zamknięty, chyba że zechcemy go dodać do tego samego rozwiązania. Najpierw przyjrzymy
się zawartości pliku
Cw2_0IA:
76
Visual C++ 2005. Od podstaw #include "stdaf x.h" i nt _tmai n(i nt argc . _TCHAR* argv[] )
{
retu rn O;
Zawartoś ć
tego pliku znacznie
#i ncl ude dla pliku
różni się
nagłówkowego
od poprzedniego przykładu. Mamy tutaj dyrektywę stdafx.h, której wcześniej nie było. Program zaczyna się od
funkcji _t ma i nt ), a nie mai nt ). Kreator aplikacji wygenerował plik nagłówkowy stdafx.h jako część projektu. Po otworzen iu tego pliku zobaczymy, że zawiera on dwie dyrektywy #i ncl ude dla plików nagłówkowych biblioteki standardowej stdio.h oraz tehar.h. Plik stdio.h jest starym nagłówkiem obsługują cym stand ardowe operacje wejścia-wyjścia, które były używane przed pojawieniem się obec nego standardu C++ ISO/ANSI. Jego funkcjonalność pokrywa się z nagłówkiem . tehar.h jest nagłówkiem stworzonym przez firmę Microsoft i definiuje funkcje tekstowe. W zasadzie plik stdafx.h służy jako zbiór standardowych, dołączanych do projektu plików systemowych, które dodalibyśmy do projektu za pomocą dyrektywy #i ncl ude. Podczas nauki CH ISO/ANSI nie będziemy potrzebowali żadnego z nagłówków pojawiających si ę w pliku nagłówkowym stdafx.h, co jest jednym z powodów n ieużywania domyślnych ustaw ień gene rowania aplikacji przez kreator. Jak już wspominałem , Visual C++ 2005 wykorzystuje funkcję wmai n( ) w zamian za ma i n() wtedy, gdy tworzony program używa znaków Uni code. Funk cja wmain ( ) została stworzona przez firmę Microsoft i nie należy do standardu C++ ISO/ANSI. W związku z tym n agłówek tchar.h definiuje nazwę _tmain, dzięki czemu jest ona zastępowana przez main, ale jeżeli sym bol_UNICODE został zdefiniowany, to zostanie zamieniona na wma in. Aby więc poinformować , że program korzysta ze znaków UNICODE, należy dodać następującą instrukcję na początku pliku nagłówkowego stdafx.h :
#define UNI COOE Po tych wszystkich
wyjaśnieniach będziemy
main O we wszystkich
przykładach w
konsekwentnie trzymali C++ ISO/ANSI.
się
stosowania funkcji
Deliniowanie zmiennych Podstawowym celem ws zystkich programów komputerowych jest przetwarzanie danych i zwracanie wyników . Podstawowym elementem tego procesu j est posiadanie fragmentu pamięci, któremu możemy nadać wybraną nazwę, do którego możemy się odwoływać przy u życiu tej nazwy oraz w którym możemy przechowywać dane . Każdy fragment pamięci o takich wła ś ciwościach nazywa się zmienną. Jak już wiemy, każda zmienna przechowuje dane określonego typu , który ustalamy podczas jej definiowania w programie. Zmienna może zostać zdefiniowana do przechowywania tylko liczb całkow itych (to znaczy danych typu i nt eger) i nie można jej używ ać do liczb ułam
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
77
kowych. Wartość przechowywana przez zmienną w danym momencie zależna jest od instruk cji w programie i zazwyczaj zmienia się wielokrotnie w czasie wykonywania przez program obliczeń. Poniżej opisuję
zasady nadawania nazw zmiennym wprowadzanym do programu.
Zasady nadawania nazw zmiennym Nazwa nadana zmiennej to jej identyfikator lub po prostu nazwa zmiennej . W nazwach zmiennych można używać wielkich i małych liter alfabetu, cyfr oraz znaku podkreślenia. Żadne inne znaki nie są dozwolone i w przypadku niezastosowania się do tych reguł podczas kompilacji zazwyczaj otrzymamy komunikat o błędzie. Nazwy zmiennych muszą dodatkowo zaczynać się od litery lub znaku podkreślenia. Najlepiej , aby nazwa odzwierciedlała rodzaj przechowywanych informacji. Ze względu na fakt , że w Visual C++ 2005 nazwy zmiennych mogą mieć długość do 2048 znaków, mamy dość duże pole manewru. Poza zmiennymi, w C++ nazwy można nadawać także innym elementom, które podlegają takim samym zasadom nazewniczym co zmienne. Używanie nazwo maksymalnej długości może znacznie utrudni ć odczytywanie kodu progra mu, a ponadto ich wpisywanie zabiera dużo czasu (chyba że wyjątkowo sprawnie posługuje my się klawiaturą) . Ważniejszym powodem do niestosowania tak długich nazw jest fakt, że nie wszystkie kompilatory potrafiąje obsłużyć. Jeśli przewidujemy, że nasz kod będzie kom pilowany także w innych środowiskach, to dobrze jest ograniczyć długość nazw do 31 znaków . Liczba ta, będąc wystarczającą do stworzenia znaczących nazw, jest także bezpieczna, jeśli chodzi o ograniczenia większości kompilatorów. nazwy zmiennych mogą rozpoczynać się od znaku podkreślenia , to lepiej tak nie w ten sposób unikamy ryzyka wystąpienia potencjalnych konfliktów ze standar dowymi zmiennymi systemowymi, które mają taką właśnie formę . Z tego samego powodu powinniśmy także unikać nazw zmiennych rozpoczynających się od dwóch znaków pod Mimo
że
robić , gdyż
kreślenia . Poniżej
zamieszczam
•
cena ,
•
rabat,
•
pKszta1t,
•
wartosc_,
•
LICZNIK.
przykłady dobrych
nazw zmiennych:
Nazwy takie jak 8_Ba11 , 7Up czy 6_pack są niedozwolone. Podobnie jak Cisza ! czy Anna-Maria , chociaż Ann a_Mar i a jest już w porządku. Oczywiście Anna Mari a jest nieprawidłowąnazwą, gdyż spacje w nazwach zmiennych są niedozwolone. Zauważ, że nazwy zmiennych repub l i can i Republ i can są różne , ponieważ wielk ie i małe litery są rozróżniane. Oczywiście w nazwach nie mogą się pojawiać białe znaki, a ich przypadkowe tam umieszczenie spowoduje powstanie dwóch lub więcej nazw zamiast jednej, co z kolei zazwyczaj prowadzi do zgłoszenia błędu przez kompilator.
78
Visual C++ 2005. Od podstaw Często stosowanąkonwencjąjest m ałej .
od
O klasach
piszę
rozpo czynanie nazw klas od wielkiej litery, a nazw zmiennych w rozdziale 8.
Slowa kluczowe wC++ W C++ istnieje pewna liczba zarezerwowanych słów, tzw . słów kluczowych, które mają spe cjalne znaczenie w języku. Edytor Visual C++ 2005 oznacza je specjalnym kolorem - u mnie domyślnym jest niebieski. Jeżeli słowo kluczowe nie zost ało wyróżnione kolorem, to zna czy, że wpisaliśmy je niepoprawnie. Należy pamięta ć , że
w przypadku słów kluczowych , tak jak i wszystkich innych elementów wielkie i małe lite ry. Na przykład program, który omawialiśmy wcześniej , zawierał słowa kluczowe int i ret urn. Gdybyśm y napisali Int lub Return, to nie byłyby to już słow a kluczowe i nie zostałyby one jako takie rozpoznane. W trakcie poznawa nia język a C++ poznamy jeszcze wiele słów klu czowych . Należy zawsze się upewnić , czy nazwy nadawane różnym elementom programu, takim jak np. zmienne, nie są identyczne ze słowami klu czowymi w C++. Pełna lista słów kluczowych używanych w Visual C++ 2005 znajduje s i ę w dodatku A.
języka
C+ +,
rozróżn i ane są
Deklarowanie zmiennych Jak już widzieli śmy , deklaracja zmiennej jest typu. Na przykład :
instrukcją określającą nazwę
zmiennej danego
int val ue: fragment kodu deklaruje zmienną o nazwie val ue, która może przechowywać liczby T yp dan ych , które może przechowywać zmienna va l ue, określony został przez słowo kluczowe i nt (który wła śnie oznacza liczby cał kow i te) . Ze względu na fakt, że i nt jest słowem klu czowym , nie mo żemy go użyć jako nazwy jednej ze swoich zmiennych. Powyższy
całkowite .
Zauważ, że
deklaracja zmiennej zawsze zakonczona j est ś redn ikiem.
jednej dekl aracji można nadać nazwy wielu zmiennym, ale jak już wcześniej jest je deklarować za pomocą pojedynczych instrukcji - każda w od dzielnym wierszu. Czasami będę od tej zasady odstępował, ale tylko w przypadkach, gdy kod zajmowałby zbyt wiele stron.
Za
pomocą
wspominałem, lepiej
dane (na przykład wartość liczby c a łko wi tej ), nie wystarczy tylko nazwy . Mus imy także skojarzyć z nimi fragment pamięc i komputera. Proces ten nazywa się definicją zmiennej . W C++ deklaracja zmiennej stanowi zarazem jej definicję (z wyjątkiem kilku przyp adków, o który ch będziemy jeszcze mówi ć) . W przypadku pojedyn czej instrukcji podajemy nazwę zmiennej i jednocze śnie przypisujemy ją do obszaru pamięci o odpowiednim rozmiarze.
Aby móc
przechowywać
zdefiniować
A zatem instrukcja :
int value:
Rozdział2.•
Dane. zmienne i działania arytmetyczne
79
jest zarówno deklaracją, jak i definicją. Za pomo cą nazwy zadekl arow anej zmiennej va l ue uzyskujemy dostęp do zdefiniowanego fragmentu pamięci komputera, w którym można prze chowywać pojedynczą wartość typu i nt.
Terminu deklaracja używa się, kiedy wprowadzanaj est nazwa do programu wraz z infor o j ej przeznaczeniu. Termin " defini cja " odnosi się natomiast do procesu przydzie lenia tej nazwie obszaru pamięci komputera. Zmienne można deklarowa ć i definiować w jednej instrukcji - jak w powyższym przykładzie. Powodem takiego dokladn ego roz dzielenia terminów " deklaracja " i " definicja" j est fakt, że można spo tkać instrukcje będąc e deklaracjami. ale nie definicjami. macją
Zmienne w programie mu szą być deklarowane w miejscu znajdującym się wcześniej niż ich pierwsze użycie. Dobrą praktyką w C++ jest deklarowanie zmienn ych w pobliżu miejsca ich pierwszego użycia.
Wartość początkowa zmiennei Deklarując zmienną, możn a pod ać
jej wartość początkową. Deklaracja zmiennej z przypisa niem do niej wartości po czątkowej nazyw a się inicjalizacją . Aby zainicjalizować zmienną podczas deklaracji, wystarczy po jej nazwie postawić znak równości, a po nim podać żądaną wartość. Każda z poniższych instrukcji nadaje zmiennym wartość początkową: int valu e = o:
int count ~ 10:
int number ~ 5 :
W tym przypadku wartość początkowa zmiennej va l ue wynosić 10, a zmiennej number - 5.
będzie
O, zmiennej count
W C++ istnieje jeszcze inny spos ób wpisywania początkowej wartości dla zmiennej, zwany notacją funkcjonalną. Zami ast podawać wartość po znaku równości, można ją umieścić w nawiasach okrągłych po nazwie zmiennej . Wszystkie powyższe przykłady można zapisać następująco :
i nt val ue (O) :
i nt count t l u) :
in t number ( 5) :
Jeśli
nie podamy wartoś ci początkowej zmiennej, to zazwyczaj będzie ona przechowywała to, co z os tało po poprzednim programie w przydzielonym jej obszarze pamięci (jest od tej reguły wyjątek, o którym będziemy mówi ć w dalszej części rozdziału). W miarę możliwości powin no się ini cj al izować wszystkie zmienne. Jeśli zmienne przy rozpoczęciu pracy programu mają znane wartości, to ł atwiej jest znaleź ć źródło problemu, gdy wystąpią błędy. Jednej rzeczy możemy by ć pewni - błęd y na pewno wystąpią.
80
Visual C++ 2005. Od podstaw
Podstawowe typy danych
Rodzaj informacji przechowywanych przez zmienną określony jest przez typ danych. Wszyst kie dane i zmienne w programie muszą być określonego typu. Standard C++ ISO/ANSI definiuje pewną liczbę fundamentalnych typów danych określanych za pomocą słów klu czowych. Nazwa "typy fundamentalne" bierze się stąd, że służą one do przechowywania pod stawowych danych komputera - przede wszystkim danych numery cznych , do których wlicza się także litery, ponieważ są one reprezentowane za pomocą kodu numerycznego. Do tej pory widzieliśmy już słowo kluczowe i nt używane do definiowania zmiennych przechowujących liczby całkowite. W C++/CLI dostępne są także podstawowe typy danych, które nie należą do standardu ISO/ANSI - o nich będzie mowa w dalszej części rozdziału. Jedną
z cech języka zorientowanego obiektowo jest możliwość definiowania własnych ty pów danych, o czym przekonamy się później, a ponadto w bibliotekach, które mamy do dys pozycji w Visual C++ 2005, zdefiniowanych jest wiele dodatkowych typów danych. W tej chwili poprzestaniemy na typach danych dostępnych w C++ ISO/ANSI. Typy fundamen talne dzielą się na trzy kategorie: typy przechowujące liczby całkowite , typy przechowujące liczby niecałkowite , zwane liczbami zmiennopozycyjnymi. oraz typ void, który określa pusty zbiór wartości lub brak typu .
Zmienne calkowite Jak już mówiłem, zmienne typu i nteger (całkowite) mogą przechowywać tylko liczby cał kowite . Liczba graczy w meczu jest liczbą całkowitą (przynajmniej na początku gry) . Wiemy już, że zmienne typu całkowitego deklarujemy za pomocą słowa kluczowego i nt. Zmienne tego typu zajmują cztery bajty pamięci komputera i mogą przechowywać zarówno dodatnie, jak i ujemne wartości. Dolny i górny limit wartości typu całkowitego odpowiada minimal nej i maksymalnej liczbie binarnej ze znakiem, która może być rerrezentowana za pomocą 32 bitów. Górna granica dla zmiennej typu całkowitego wynosi 2 3 -l, czyli 2 147483647, a dolna to --{2 31) , czyli -2 147 483 648. Poniżej znajduje się przykładowa definicja zmiennej całkowitej :
int t oeCount = 10; W Visual C++ 2005 istnieje też słowo kluczowe s hort , które również służy do definiowania zmiennych całkowitych, ale zajmujących tylko dwa bajty pamięci. Słowo kluczowe short jest równoznaczne z short in t . Zmienne typu short (całkowite krótkie) można deklarować następująco:
short feetPerPerson = 2; short int feetPerYard = 3; zmienne są tego samego typu, gdyż s hort i s hor t i nt oznaczają to samo. tutaj obu form , aby pokazać sposób ich użycia, ale w praktyce należy zdecydować się na jedną metodę i konsekwentnie ją stosować. Najczęściej wybierana jest krótsza - short.
Obie
powyższe
Użyłem
Rozdział 2.•
W C++
Dane. zmienne i działania aryimetyczne
81
dost ępny
r ównie ż zapisa ć
jest jeszcze jeden typ całk ow ity - long (całkowity długi), który można jako l ong i nt. Poniżej znajduj ą s i ę przykłady deklarowania zmiennych typu
l ong: long bigNumber = lOOOO OOL: long largeValue = OL; Powyższe
instrukcje dekl aruj ą zmienn e bigNumber i l argeValue o wa rtości ac h początko wych 1000000 i O. Litera L znajdująca się na końcu k ażdego literału informuje, że są to liczby cał kowite typu l ong. Można w tym celu również użyć małej litery l, ale jest ona często mylona z cyfrą l. Literały całkowite bez dołączonego L są typu i nt .
W kodzie nie m ożna s toso wać spacji w dużych liczbach. W tekście ale w programie musi to już być 12345.
m ożna napisać
12 345,
Zmienne całkowite zadeklarowane jako l ong w Visual C++ 2005 zajmuj ą cztery bajty pamięci i mogą mieć wartość od - 2 147483 648 do 2 147483 647, czyli mają taki sam zakres wartości jak zmienne typu i nt.
W innych kompilatorach zmienne typ u 70ng (który jest równoznac zny z 70ng i nt) m ogą nie być identyczne z i nt. Należy o tym pamiętać, planując kompilację swoich pro gramów w innych środowiskach. Aby kod był w pełni przenośny, nie m ożna nawet zakładać, że typ i nt zajmuje cztery bajty (np. w starszych 16-bitowych wersjach Visual C++ zmienna typu int z ajm owała tylko dwa bajty).
Znakowe Iypy danych Typ danych char służy do dwóch celów . Określa on jednobitow ą zmienną, której można użyć do przechowywania liczb całko witych z określonego zbioru lub pojedynczego znaku ASCII (ang . American Standard Code for 1nformation Exc hange). Kody znaków ze zbioru ASCII znajduj ą się w dodatku B. Zmienną typu char definiujemy w na stępujący sposób:
char lett er Powyższa
~
'A' ;
nazwie letter i inicjalizuje ją wartością początkową tylko z jednej litery podajemy w pojedynczych cudzysłowach, a łańcuchy znaków w podwójnych. Łańcuch znakó w to szereg wartości typu char zgrupowanych w jeden c i ąg , zwany tablicą. Do tablic i ł ańcuchów znaków wrócimy jeszcze w rozdziale 4. 'A' .
instrukcja deklaruje
zmienną o
Zauważmy, że wartości składające się
Ze wzgl ędu na fakt , że litera " A" w ASCII jest reprezentowan a przez dobrze mogliśmy zapisać n aszą instrukcję następująco :
char let t er = 65 ;
li czbę
65 , to równie
II Równoznaczne z A.
Instrukcja ta spowoduje taki sam rezultat jak poprzednia. Liczby w zmiennych znakowych mogą mieć rozmiar od - 128 do 127.
całkowite
przechowywane
82
Visual C++ 2005. Od podslaw Pamiętaj, ż e
standard C++ ISO/ANSI nie wymaga, aby typ char reprezentował j edno bajtowe liczby całkowite ze znakiem. Czy typ char ma reprezen to wać liczby całkowite ze znakiem, należące do zbioru od -128 do + 127, czy liczby calkowite bez znaku od Odo 255, zależy wyłącznie od twórcy kompilatora. Należy o tym pamiętać podczas przenoszenia kodu C+ + do innego ś ro dowis ka. Typ wchar_t oznacza typ wide character. Zmienne tego typu przechowują dwubajtowe kody znakowe o wartościach z przedziału 0- 65 535. Poniżej znajduje się przykład definicji zmien nej typu wchar_t:
wchar t letter = L' Z':
II Zmienna przechowują c a 16-bit owy kod znak owy.
Powyższa
instrukcja definiuje zmienną 1et t er, która jest zainicjalizowana 16-bitowym kodem litery "Z". Znajduj ąc a s i ę z przodu wielka litera L informuje kompilator, że jest to 16-bitowy kod znakowy.
Do inicjalizacji zmiennych znakowych Ci innyc h typów całkowitych) możem y także używać wartości szesnastkowych, które oczywiśc ie są prostsze w u życ iu, gdy kod y znaków mam y podane w systemie szesnastkowym (heksadecymalnym). Liczba szesnastkowa zapisywana jest przy użyc iu standardowego systemu szesnastkowego - za pomocą cyfr od O do 9 oraz liter A - F (lub a - f) reprezentujących liczby od 10 do 15. Z przodu zaw sze dodaje s i ę znaki Ox (lub OX) w celu odróżnienia od wartości dziesiętnych . A zatem aby u zyskać wynik identyczny z poprzednim, ostatnią instrukcję możemy zapisać następująco:
cha r let t er = Dx4 l :
II Równoznaczne z A.
Nie zapisuj liczb całkowitych z wiodącym zerem. Kompilator potraktuje je jako ósemkowe (base 8) - wartość 065 w systemie dz i esi ętnym b ędzie wynosiła 53.
wartości
Warto także pamiętać , że w systemie Windows XP dostępne j est narzędzie Tablica znaków, które pozwala zlokalizować znaki dowolnego fontu dostępnego na komputerze. Pokazuje kod szesnastkowy danego znaku i podpowiada, jak go wprowadzi ć z klawiatury. Narzędzie to dostępne jest w menu Start/Akcesoria/Narzędz ia systemowe.
Modyfikatory typU integer Zmienne jednego z typów i nt eger - char, i nt , short lub Ionq - domyślnie przechowują warto ś ci całkow ite ze znakiem , a więc można ich używać do przechowywania zarówno warto ści dodatnich, jak i ujemnych. Jest to możliwe , ponieważ zakłada się, że mają one domyślny modyfikator typu si gned. W związku z tym w każdym miejscu, gdzie napisaliśmy i nt lub l onq, równie dobrze mogliśmy napisać si gned i nt lub s i gned l ang. Możemy również określić
przypadku
będzie
signed va lue
~
to
-5:
typ zmiennej za pomocą samego s i gned i nt . Na przykład:
oznaczać
II Równ oznaczne z signed in/o
słowa
kluczowego s i gned. W takim
Rozdzial2. • Dane, zmienne i działania aryimetyczne Zapis taki nie jes t często spotykany i ja osob iście wo lę jest czytelniejszy.
używać
83
i nt, który sprawia , że program
Wartości, które można przechowywać w zmiennej typu cha r, zawierają się w zakre sie od - 128 do + 127, czyli takim samym jak dla zm iennej typu signed char. Poza tym typy char i signed char są różn ymi typami i nie należy zakłada ć , że to to samo. Jeśli masz pewność, że nie będzie konieczności przechowywania wartości ujemnych w danej zmiennej (je śli np. zapisujemy liczbę przejechanych w c i ągu tygodnia kilometrów), to jej typ m ożemy określi ć jako unsigned:
unsi gned lang mileage = OUL: W tym przypadku minimalną wartością zmiennej mi l eage jest zero, a m aksymalną 32_ liczba 4 294 967 295 (czyli 2 1). Porównaj tę li czbę z maksym alną warto ś cią typu signed long - - 2 147483648. Bit, który był użyty do określenia znaku liczby, w typie un si gned jest wykorzystany jako część wartości. W konsekwencji zmienna typu unsigned ma większy zasięg warto ś ci dodatnich, ale nie może przechowywać wartości ujemnych. Zauważ , że do stałych bez znaku dołączana je st litera U(lub u). W powyższym przykładzie dołączyłem także L, aby zaznaczyć, że stała jest typu l onq. Zarówno L, jak i U mogą być pisane wielką lub małą lite rą. Kolejno ś ć nie gra tutaj roli, choć dobrze jest konsekwentnie stosować jeden sposób określania taki ch wartości. Słowa kluczowego unsigned W takim przypadku zmienna Pamiętaj, ż e
można także uży ć będzie typu
samodzielnie do
okre ślenia
typu zm iennej .
unsi gned i nt.
si gned oraz unsi gned to sło wa kluczow e i nie
można używać
ich jako nazw
zmi ennych.
Typ logiczny Zmienne logiczne mogą mieć tylko jedną z dwóch wartości : true lub false. Typem logicznej zmiennej jest bool - nazwany od twórcy algebry boolowskiej, George'a Boole'a. Typ bool zaliczany jest do typów całkowitych. Zmienne logiczne nazywane są także czasami zmiennymi boolowskimi. Wykorzystywane są one do przechowywania wyników testów , które mogą być albo true albo fal se - np . czy jedna wartość jest równa drugiej, czy nie. Nazwę
zmiennej logicznej
można zadeklarować za pomocą następującej
instrukcji:
baal testResult : Oczyw i ście ,
podczas deklaracji
możemy także inicjalizować
zmienne logiczne:
baal calarIsRed = tr ue : Wartości TRUf i FAL5f są powszechnie używane ze zmiennymi o typa ch num erycznych. a w szczególn ośc i int. Jest to pozostałość po czasach, gdy nie było j eszcze implem entacji zmiennych logicznych w C++ i do reprezentacji wartości logicznych wykorzystywane były
84
Vilillal C++ 2005. Od podstaw zmienne typu in t . Wartos ć Ow takim przypadku traktowana jest jako f a lse, a każda inna jako true. Symbole TRUf i FA L5f są wciąż używane w bibliotece MFC. gdzie reprezentują odp owiednio wartos ć niezerową oraz O. Zauważ, że TRUf i FAL5f (pisane wielkimi literami) nie są słowami kluczowymi w C++ - są one tylko symbolami zdefiniowany mi w obrębie biblioleki MFC Należy równieżpamiętać, że TRUf i FAL5f nie są prawidłowymi warto ś ci am i logicznymi. Ni e należy zolem mylić t r ue z TRUf.
Typy zmiennopoZYCyjne Wartości, które nie są liczbami całkowitymi , przechowywane są jako liczby zmiennopozy cyjne. Mogą być one reprezentowane jako wartości dziesiętne (np . 112,5) lub z wykładnikiem (np. l, l25E2), gdzie 'część dziesiętna jest mnożona przez liczbę dziesięć podniesioną do p otęgi podanej po literze "E ". Zatem nasza przykładowa liczba 112,5 to: 1,125'10 2 = 112,5. Stała zmiennopozycyjna musi zawierać kropkę o d d z i e l aj ąc ą ułamek dziesiętny lub wykład nik (albo j edno i drugie). Jeżeli napiszemy li czbę bez żadn ego z tych znaków. otrzymamy wartość całkowitą .
Zmienną zmiennopozycyjną można określić
szym
za
pomocą słowa
kluczowego doub l e, jak w
poniż
przykładzi e:
double in to mm = 25 .4: Zmienna typu doubl e zajmuje osiem bajtów pamięci i przechowuje wartości z dokładno ś c ią do około 15 miejsc Fao przecinku. Zbiór przechowywanych w artoś ci jest znacznie w iększy i wynosi od l,7 xlO- os do 1,7 xl0 30s. Jeżeli
nie planujemy wykonywać obliczeń z dokładnością do 15 miejsc po przecinku i nie potrzebujemy używać tak olbrzymich liczb, to możemy użyć słowa kluczowego f l oat w celu zadeklarowania zmienn ych zmiennopozycyjnych zajmujących tylko cztery bajty. Na przykład :
fl oat pi
=
3.14159f:
Powyższa
instrukcja deklaruje zmienną pi o wartości po czątkowej 3 . 14159f. Litera f na koń cu oznacza, że j est to stała typu fl oat . Gdyby śmy nie wpisali tej litery, to nasza stała byłaby typu doub l e. Zmienne deklarowane jako f loat mają dokładnoś ć do około siedmiu miejsc po prze cinku i przechowują wartości ze zbioru od 3,4x 10-3s do 3,4 x 103s. W standardzie C++ ISO/ANSI został zdefiniowany jeszcze typ zmiennoprzecinkowy l ong double, który w Visual C++ 2005 ma zaimplementowany taki sam zbiór wartości i dokład noś ćjak typ doubl e. W niektórych kompilatorach typ l ong double odpowiada 16-bitowej wartości zm iennopozycyjnej o znacznie większej dokładności i szerszym zakres ie możliwych wartości w porównaniu z typem doubl e.
Rozdzial2. • Dane. zmienne i działania arytmetyczne
85
Podstawowe typy danych wC++ 180/AN81 Poniższ a
tabela zawiera pod sum owanie wszystk ich pod stawowych typów danych C++ ISO/ANSI oraz zakres obsługiwanych przez nie wartości w Visual C++ 2005:
Rozmiar wbajtach
Typ
Zakres wartości
bool
t r ue lub f al se
char
Dom yślnie możn a
taki sam jak dla si gned char : -128 do + 127. Opcjo nalnie char n ad a ć taki sam zakres j ak uns ign ed char.
s i gned char
-128do+127
uns i gned char
Odo 255
wc har t
2
Odo 65 535
short
2
-32 768 do 32 767
unsigned short
2
Odo 65535
i nt
4
-2 147483648 do 2 147483647
unsi gned i nt
4
Odo 4 294 967 295
l ong
4
-2 147 483 648 do 2 147 483 647
unsi gned l ong
4
Odo 4 294 967 295
fl oat
4
±3,4 ' 1O±38 z dokladno ści ą do okola 7 miej sc po przecinku
doubl e
8
± I,7' 1O±308 z
dokł adności ą do około
15 miej sc po prze cinku
l ong doubl e
8
± 1,7'1 O±30 8 z
dokładnoś c ią
15 miejsc po przecinku
do
około
Literał, Do tej pory wielokrotnie już i n icja li z ow ał e m zmienne konkretnym i warto ściami . W C++ ustalone wartoś ci jakiegokolwiek typu nazywają się Iiterałami. Literał to wartość określonego typu, a więc wartoś ci 23,3 .14159,9 .5f oraz true są przykładami odpowiednio literałów typów i nt , doubl e, flo at oraz boa l. Literał "Samuel Beckett " jest przykładem literału będącego łań cuchem znaków (łańcuchom bliżej przyjrzymy się dopiero w rozdziale 4.). W tabeli na następ nej stronie znajduje s ię podsumowanie literałów różny ch typów. Choć
dla
literału
nie
można określi ć
początkowe wartoś ci ,
typu sho rt lub unsi gned short , to kompilator zaakceptuje typu i nt, dla zmiennych tych typów , pod warunkiem się w zakresie typu zmiennej.
które
że wartość literału mieści
Literałów często używa się
są literałami
w program ach do wykonywania obliczeń. Mogą to być na przykład konwer sji, takie jak 12 przy przeliczaniu stóp na cale czy 25,4 przy przeliczaniu cali na milimetry, albo łańcuch stanowiący komunikat o błędzie . Powinno s ię jednak unikać uży wan ia literałów liczbowych w programach, kiedy ich znaczenie nie jest oczywiste. Nie każdy musi wiedzieć, że kiedy używamy liczby 2,54, to odpowiada ona liczbie centymetrów w calu .
wartoś ci
86
Visual C++ 2005. Od podstaw
Typ
Przykład
char, signed char lub unsigned cher
"A", "Z", "8",
wchar t
LilA", L"Z" , L Il8 ", L"*"
int
- 77, 65, 12345, Ox9FE
unsi gned int
IOU,64000U
long
- 77L, 65L, 12345L
unsigned long
5UL, 999999999UL
flo at
3.14f, 34.506f
double
1.414, 2.71828
long double
1.414L,2.71828L
bool
true, false
Iileralu "*"
Lepiej jest zadeklarować zmienną o odpowiedniej stałej wartości - można ją na przykład nazwać InchesToCentimeter s. Za każdym razem, gdy używamy tej zmiennej w kodzie , jej prze znaczenie jest oczywiste. Jak nadać zmiennej stałą wartość, dowiemy się w dalszej części rozdziału.
Deliniowanie synonimów typÓW danych Słowo kluczowe typedef pozwala na zdefiniowanie własnej nazwy dla istniejącego już typu danych. Za jego pomocą standardowy typ l ong i nt możemy nazwać na przykład Bi gOne s:
typedef long i nt Bi gOnes; II Definiowanie nazwy BigOn es jako nazwy typu. Powyższa
temu
instrukcja definiuje Bi gOnes jako dodatkowy określnik dla typu l ong i nt . Dzięk i jako long i nt za pomocą następującej instrukcj i:
zmienną mynum możemy zdefiniować
BigOnes mynum
=
OL : II Definiowanie zmiennej typu long int.
Pomiędzy powyższą deklaracją
Równie dobrze
a tą używającą wbudowanej nazwy typu nie ma
żadnej różnicy .
moglibyśmy napisać:
long i nt mynum = OL; II Defini owan ie zmiennej typu fong int,
Rezultat będzie identyczny. W ten sposób, przy użyciu dwóch okre ślników typu w obrębie jednego programu, można zadeklarować różne zmienne, które będą miały ten sam typ. na fakt, że słowo kluczowe typedef służy do tworzenia synonimów typów, może jest ono niepotrzebne - ale tak nie jest. Później dowiesz się, że odgrywa ono bardzo ważną rolę w upraszczaniu skomplikowanych deklaracji , gdyż umożliwia zdefiniowanie pojedynczej nazwy do reprezentowania zawiłych specyfikacji typu. Sprawia to, że kod staje się o wiele bardziej czytelny.
Ze
względu
się wydawać, że
Rozdział
2.• Dane. zmienne i działania arylmelyczne
87
Zmienne ookreślonych zbiorach wartości Czasami będziesz potrzebować zmiennych mogących przyjmować wartości z okre ślonego zbioru, do których można się odwoływać za pomocą etykiet - na przykład nazwy dni tygo dnia czy miesięcy. W C++ dostępne jest specjalne narzędzie do tego celu, zwane wylicze niem (lub enumeracją) . Weźmy jeden z wymienionych przykładów - zmienna, która potrafi przyjmować odpowiednie wartości w zależności od dnia tygodnia. Jej definicja może wyglą dać następująco:
enumTydzien{pon . wt. sr .
CZW.
pia. sa . nd} tenTydzien:
Powyższa
instrukcja deklaruje typ wyliczeniowy o nazwie Tydzien oraz zmienną t enTydzi en egzemplarzem typu wyliczeniowego Tydz i en, który może przybierać tylko stałe war tości ze zbioru podanego w nawiasach klamrowych . Próba przypisania do tenTydzi en czego kolwiek spoza podanego zbioru spowoduje błąd. Symboliczne nazwy pomiędzy nawiasami nazywają się argumentami typu wyliczeniowego. W rze czywistości każda nazwa dnia zosta nie automatycznie zdefiniowana jako reprezentacja stałej wartości w postaci liczby całkowitej . P ierwsza nazwa na liście , pan, ma wartość O, wt - l itd. będącą
Jedną
ze stałych wyliczeniowych sposób:
można przypisać jako wartość zmiennej
tenTydzi en w
nastę
pujący
t enTydzien = czw: Zauważ, że stałej
wyliczeniowej nie trzeba kwalifikować za pomocą nazwy enumeracji . War zmiennej ten Tydzi en będzie wynosiła 3, ponieważ stałym symbolicznym, zdefiniowanym przez wyliczenie, domyślnie przyp isywane są kolejne wartości typu i nt, rozpoczynając od zera.
tość
Standardowo każdy następny argument jest większy od poprzedniego o jeden . chcemy rozpocząć wyliczanie od wybranej wartości, to możemy napisać:
enum Tydz ien {pan = l . wt . sr .
CZW.
Jeśli
jednak
pia. sa. nd} tenTydzien:
St ałe
wyliczeniowe w powyższym przykładzie będą miały wartości od l do 7. Argumenty nie nawet mieć unikalnych wartości. Możemy na przykład pan i v/t przypisać wartoś ć l za pomocą instrukcji: muszą
enumTydzien {pan ~ l . wt
=
l . sr . czw. pia. sa . nd} t enTydzien:
Jako że zmienna t enTydzi en jest typu i nt , zajmuje cztery bajty kie inne zmienne typu wyliczeniowego. Kiedy mamy już
zdefiniowanąformę
wyliczenia, to
pamięci,
podobnie jak wszyst
możemy zdefiniować inną zmienną:
enum Tydzien nastepnyTydzien : Powyższa
instrukcja definiuje zmienną nastepnyTydzien jako wyl iczenie, które może przy z poprzedniego wyliczenia. Możemy także pominąć słowo kluczowe enum
bierać wartości
i
powyższe wyrażenie zapisać następująco :
Tydzien nastepny t ydzi en:
88
Visual C++ 2005. Od podstaw Jeśli
chcemy, możemy przypisać określone wartości do wszystkich argumentów wyliczenia. na przykł ad zdefiniować takie wyliczenie:
Możemy
enum Interpunk cja {przecinek = ',', wykrzyknik =
'I',
pytajnik = '?'} rzeczy:
W powyższym wyrażeniu dla zmiennej rzeczy zdefiniowaliśmy wartości jako liczbowe ekwi walenty odpowiednich symboli. Z tablicy znaków ASCII w dodatku B wynika, że podane symbole w zapisie dziesiętnym mają odpowiednio kody 44,33 i 63. Jak widać, wartości nie muszą występować w porządku rosnącym. Jeżeli któremuś elementowi nie nadamy żadnej wartości, to zostanie mu nadana o jeden większa od poprzedniej, tak jak w naszym drugim przykładzie z dniami tygodnia. Typ wyliczenia możemy pominąć, jeżeli nie będziemy zmiennych tego typu . Na przykład :
później
potrzebowali
definiować
innych
enum {pon, wt , sr , czw , pia. so. nd} t enTydzien. na st epnyTydzien. zeszlyTydzien; W powyższej instrukcji mamy trzy zmienne, które mogą przyjmować wartości od pan do nd. Jako że typ wyliczenia nie został podany, nie możemy się do niego odwoływać. Zauważ, że dla tego wyliczenia nie możemy już zdefiniować żadnych innych zmiennych, ponieważ nie mogli byśmy powtórzyć definicji. Próba zrobienia tego potraktowana by była jako próba ponownego zdefiniowania wartości od pan do nd, ajest to niedozwolone.
Określanie typU stałych
wyliczeniowych
Stałe wyliczeniowe domyślnie mają typ int, ale możemy określić go w sposób jawny, dodając dwukropek oraz nazwę typu po nazwie wyliczenia w deklaracji. Stałym wyliczeniowym można nadać dowolny typ całkowity ze znakiem lub bez: short, int, 10n9 lub char albo typ logiczny , Dzięki temu wyliczenie sposób:
reprezentujące
dni tygodnia
moglibyśmy zdefiniować
w
następujący
enum Tydzien : char{ poni edzialek, wtorek, sroda , czwa rtek . piate k, sobota, niedzie la}: W tym wyliczeniu stałe będą typu c ha r , a pierwsza z nich będzie miała wartość O. Jednak nadając stałym typ char , prawdopodobnie wolelibyśmy je inicjalizowa ć jawnie w następujący sposób:
enum Tydzi en
char{ poniedzialek=' p' , v~ o r e k ~ · w' . sroda='s ', czwa rtek='c' . pi at e k~ 'p' , sobota='s ' , niedziel a= ' n'} :
W obecnej postaci wartości stałych trochę lepiej odzwierciedlają to, co przechowują, chociaż nie rozróżniają one takich dni jak s roda i sobota czy poniedzialek i piatek. Takie same war tości dla różnych stałych nie stanowią problemu, ale wszystkie nazwy muszą być oczywiście unikalne, Poniżej
mamy
enum Sta te
przykład
wyliczenia ze
bool { On
~
stałymi
true, Off=fal se} :
typu bool :
Rozdział 2.•
Ze
Dane. zmienne i działania arytmetyczne
89
względu
byśmy
na fakt, że On ma wartość początkową true, Off przybierze wartość fa l se. Gdy podali więcej stałych wyliczeniowych, to ich wartości zmieniałyby się na przem ian.
Podstawowe operacje wejścia-wyjścia Na temat operacji wejścia-wyjścia powiemy sobie tylko w odniesieniu do natywnego C++ . Nie jest to takie trudne - wręcz przeciwnie - ale do programowan ia dla Windowsa nie będziemy tej wiedzy potrzebować. Operacje wejścia-wyjścia w C++ skupiają się wokół koncepcji strumienia danych , które możemy wprowadzać do strumienia wyjściowego i pobie ra ć ze strumienia wejściowego. Dowiedzieliśmy się już, że do standardowego strumienia wyj ściowego C++ ISO/ANSI wysyłającego dane do wiersza poleceń na ekranie odwołujemy się za pomocą słowa kluczowego cout . Odpowiadający mu strumień wejściowy przyjmujący dane z klawiatury to ci n.
Wprowadzanie danych zklawiatury Dane z klawiatury przyjmujemy za pomocą strumienia wejściowego ci n przy użyciu operatora pobierania » . Aby pobrać dwie wartości typu i nteqer - numl i num2 - możemy posłużyć się następującą instrukcją:
cin
»
numl
»
num2:
Operator pobierania » wskazuje kierunek, w którym płyną dane - w tym przypadku ze strumienia ci n do każdej z podanych dwóch zmiennych . Wszelkie wiodące białe znaki są pomijane, a pierwsza wartość typu i nt, którą wprowadzimy z klawiatury, zostanie przypisana do zmiennej numl. Dzieje się tak, gdyż instrukcja wejściowa wykonywana jest od lewej do prawej. Wszelkie białe znaki występujące po zmiennej numl zostaną zignorowane, a druga wartość typu całkowitego, którą wprowadzimy z klawiatury, zostanie przypisana do zmiennej num2. Należy jednak pamiętać, że pomiędzy poszczególnymi wartościami musi wystąpić jakaś przerwa, aby można je było odróżnić . Operacja wprowadzania strumienia danych kończy się w momencie naciśnięcia klawisza Enter - program kontynuuje wówczas działanie, przecho dząc do następnego wyrażenia. Oczywiście, jeśli wprowadzimy nieprawidłowe dane, spowo dujemy błąd, ale zakładam, że zawsze wprowadzasz je prawidłowo! Liczby zmiennopozycyjne są wczytywane z klawiatury w taki sam sposób jak liczby cał kowite i można je oczywiście mieszać . Dane ze strumienia wejściowego i operacje automa tycznie radzą sobie ze zmiennymi i danymi jednego z typów podstawowych. Na przykład w poniższych instrukcjach:
int numl = O. num2 ~ o:
double fact or = 0.0:
cin » numl » ulamek » num2 :
ostatni wiersz wczyta liczbę całkowitą do zmiennej numl, następnie liczbę zmiennopozycyjną do zmiennej ul amek oraz liczbę całkowitą do zmiennej num2.
90
Visual C++ 2005. Od podstaw
Wysyłanie
danych do wiersza poleceń
Widzieliśmy już, jak wysyła s ię dane do wiersza poleceń, ale chciałbym do tego zagadnienia jeszcze na chwilę powrócić. Wypisywanie danych na ekranie działa w sposób uzupełniający do wprowadzonych danych . Jak wiemy, strumień wyjściowy nazywa się cout i do przesyłania do niego danych używamy operatora wstawiania «. Operator ten wskazuje również kierunek przepływu danych. Użyliśmy go już do wysłania danych spomiędzy cudzysłowów. Proces wy syłania wartości zmiennej zademonstruję na przykładzie prostego programu.
~ Wysyłanie danych do wiersza poleceń Zakładam, że
potrafisz już utworzyć nowy pusty projekt, dodać do niego nowy plik źródłowy oraz skompilować go do wykonywalnej postaci . Poniżej znajduje się kod, który należy wpisać do pliku źródłowego po utworzeniu projektu o nazwie Cw2_02: IICw2_02.cpp
II Ćwicze n ie wysyłania dany ch na wyjście.
#i ncl ude using std: :cout: using std' :endl: int main() (
5678 : II Rozpocznij w nowym wierszu. II Wyślij dwie wartości. II Zakończ na nowym wierszu . II Wyjdź z programu.
Jak to działa Pierwsza instrukcja w ciele funkcji main () deklaruje i inicjalizuje dwie zmienne: numl i num2. mamy dwie instrukcje wyjścia, z których pierwsza przenosi kursor do nowego wiersza. Jako że instrukcje wyjścia są wykonywane od lewej do prawej, to druga z nich wyświetla wartość zmiennej numl przed wartością zmiennej num2. Następnie
Po skompilowaniu i uruchomieniu tego programu otrzymamy
następujący
wynik:
12345678 Wynik jest poprawny, ale niezbyt pomocny. Pomiędzy poszczególnymi wartościami potrzebna jest co najmniej jedna spacja . Domyślnie strumień wyjściowy tylko wysyła cyfry, nie dodając żadnych spacji oddzielających poszczególne wartości, co umożliwiłoby ich rozróżnienie. W tej postaci nie mamy możliwości określenia, gdzie kończy się jedna liczba, a gdzie zaczyna druga.
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
91
Formatowanie wysylanych danych Problem braku spacji w wynikach można bardzo łatwo rozwiązać, wstawiając dwiema wartościami. Aby tego dokona ć, należy poniższy wiersz:
cout
«
num1
«
num2;
II
Wyślij
dwie
spację pomiędzy
war/oś ci ,
zastąpić następującym:
cout
«
num1
«
' , «
nu m2 :
II
Wyślij
dwie
war/ości.
Oczywiście gdybyśmy
mieli kilka wierszy wyników, które chcemy wyrównać w kolumnach, nie wiemy, z ilu cyfr będzie składać się każda wartość. W takiej sytuacji możemy posłużyć się tak zwanym manipulato rem. Służy on do modyfikowania sposobu obsługi danych wysyłanych do (lub otrzymywa nych ze) strumienia.
to
potrzebowalibyśmy trochę większych możliwości, ponieważ
Manipulatory zdefiniowane są w pliku nagłówkowym , aby więc z niego skorzy stać , musimy dodać odpowiednią dyrektywę #i ncl ude. Teraz skorzystamy z manipulatora set wrn), który spowoduje wyrównanie wysłanych danych do prawej w polu o szerokości n spacji . set w(6) spowoduje zatem, że następna wysłana wartość zostanie zaprezentowana w polu o szerokości sześciu spacji. Sprawdźmy, jak to wygląda w praktyce.
~ Uzywanie manipulatorów Aby
uzyskać pożądany
wynik, możemy
zmienić
nasz program w
następujący
sposób:
II Cw2_03 .cpp
II Ćwiczenie wysyłania danych na wyjście.
#include #include using std: :cout: usi ng std : :endl : usi ng st d: :setw: i nt mainO (
int num1 ~ 1234 , num2 ; 5678: cout « endl : cout « setw(6) « num1 « setw(6J « num2 : cout « end l , return O:
II Wyślij dwie war/ości. II Rozpo cznij w nowym wierszu. II Wyjdź z programu.
Jak to działa Zmiany w stosunku do poprzedniej wersji programu to dodanie dyrektywy #i ncl ude dla pliku nagłówkowego , deklaracja usi ng dla nazwy set w z przestrzeni nazw st d oraz wstawienie manipulatora set w( ) do strumienia wyjściowego przed każdą wartością, dzięki
92
Visual C++ 2005. Od podstaw czemu są one prezentowane w polach o szerokości rezultat z oddzielonymi od siebie dwiema liczbami:
sześciu
spacji. W wyniku otrzymujemy
1234 5678 Zauważmy, że
manipulator setw() działa tylko na pojedyncze wartości wyjściowe występujące po nim w strumieniu. Manipulator musi bezpośrednio poprzedzać każdą wartość, którą chcemy zaprezentować w polu o określonej szerokości w strumieniu wyjściowym. Jeśli użyjemy go tylko raz, to będzie miał zastosowanie tylko do tej wartości, która występuje po nim. Wszystkie pozostałe wartości zostaną wysłane w tradycyjny sposób. Możemy to spraw dzić, usuwając z kodu drugi przykład zastosowania setw(6) oraz jego operator wstawienia. bezpośrednio
KOIlowanie znaków specjalnych Pisząc łańcuch
znaków umieszczony w podwójnych cudzysłowach, możemy do niego wstawić specjalne znaki, zwane symbolami zastępczymi. Ich nazwa wzięła się stąd, że pozwalają one na wstawianie do łańcuchów znaków, których normalnie wstawiać tam nie można, zastępując je odpowiednimi symbolami. Kod znaku rozpoczyna się od znaku lewego ukośnika \, który informuje kompilator, że następny znak należy potraktować w specjalny sposób. Na przykład znak tabulacji ma postać \ t - kompilator rozumie, że litera t reprezentuje tabulację w łańcu chu, a nie prawdziwą literę "t". Spójrzmy na dwie poniższe instrukcje wyjściowe:
cout cout
endl endl
« «
« «
"To są dane wyjściowe."; "\tTo są dane wyjściowe po znaku tabulacji.
Rezultat ich działania
To
są
To
będzie następujący:
dane wyjściowe.
są dane wyjściowe po znaku tabulacji.
Sekwencja \ t w drugim pierwszego tabulatora.
wyrażeniu spowodowała wcięcie wysyłanego
tekstu do pozycji
W rzeczywistości, zamiast używać słowa kluczowego endl, jako znaku nowego wiersza można kodu xn. Tak więc powyższe instrukcje moglibyśmy równie dobrze zapisać w następujący sposób:
użyć
cout cout
« «
"\nTo są dane wyjściowe."; "\n\tTo są dane wyjściowe po znaku tabulacji.
Tabela na następnej stronie zawiera spis najbardziej przydatnych kodów znaków specjalnych. Oczywiście, jeśli
chcemy umieścić w łańcuchu znak lewego ukośnika lub podwójny cudzy jako znak, który ma być jego częścią, to musimy użyć odpowiedniego kodu (symbolu zastępczego). W przeciwnym przypadku lewy ukośnik zostanie zinterpretowany jako początek innego kodu, a podwójny cudzysłów jako wyznacznik końca łańcucha. słów
Kodów znaków specjalnych
można także używać
do inicjalizacji zmiennych typu char. Na
przykład:
char Tab
~
'\t'; II Inicjalizacja za
pomocą
znaku tabulacji.
Rozdział 2.
Kod
Przeznaczenie
\a
odtwar zanie
\n
nowy wiersz
\'
pojedynczy c u d z ys łów
\\
lewy uk o śnik
\b
backspace
\t
znak tabulacji
\"
podwójny c u dzys łów
\7
znak zapytania
Ze
• Dane. zmienne i działania arytmetyczne
93
dźw ię k u
względu
rału
na fakt, że lit er ały są otaczane pojedynczymi c udzysłow am i, w celu podania lite znakowego będ ąc e go pojedynczym cud zy sł owem musimy u ży ć kodu - ' \ ' , .
~ Używanie kodów znaków specjalnych Poni żej
znajduj e się pro gram, w któ rym wyk orzystan o niektóre kody znaków specjalnych z powyższej tabeli: II Cw2_04.cpp
II Używan ie symboli zastępczyc h.
#inc l ude #include usi ng std: :cout ; int ma i n()
{ char newl i ne = " vn " : II Sy mbol zas tępczy znaku nowego wiersza.
cout « newl i ne: II Rozpocznij w nowym wiersz u.
cout « "\ "We\ ' 11 make aur escapes i n sequence\". he sai d . " :
cout « "\ n\ tThe pr ogr am\ ' s over . i t\'s t i me t o make a beep beep. \ a\a ";
cout « newl i ne: II Rozpocznij w nowym wierszu . II Wyjdź z programu. re turn O;
Po skompilowaniu i uruchomien iu tego programu otrzy mamy
następuj ący
rezult at:
-We' l l make aur escapes i n sequence- , he sai d . The program 's over. i t ' s t i me tak e ma ke a beep beep.
Jak lo działa Pierwszy wiers z w funkcji mai n( ) definiuje zm i enn ą newl i ne i inicjal izuje ją za p om ocą zako dowanego znaku nowego wiersza. Dzi ęki temu zamiast s łowa kluczoweg o endl możem y uży wać
newl ine.
94
Visual C++ 2005. Od podstaw Po wysłaniu newl i ne do strumienia cout wysyłamy łańcuch , w którym użyte zostały zako dowane znaki podwójnego i pojedynczego cudzysłowu . Dla pojedynczych cudzysłowów nie musimy tutaj używać kodowania, gdyż ł ańcu ch otoczony je st c ud zysłowa m i podwójnymi, dzięki czemu kompilator rozpozna je j ako zwykłe znaki pojedynczego cudzysłowu, a nie znacznik końca łańcucha. Natomiast dla podwójnych cudzysłowów w tym łańcuchu musimy zastosować kodow anie . Łańcu ch rozpoczyna się od zakodowanego znaku nowego wiersza, po którym następuje zakodowany znak tabulacji , dzięki czemu wy słany wiersz zostanie wcięty . Na końcu łańcucha znajdują się jeszcze dwa zakodowane znaki powodujące odtworzenie dźwięku, a więc po uruchomieniu programu powinniśmy usł yszeć podwójny odgłos z głośnika komputerowego.
Wykonywanie obliczeń wC++ W tej chwili zaczynamy rzeczywiście coś robić z wprowadzanymi danymi . Wiemy już , jak proste operacje wejścia-wyj ś cia. Teraz przejdziemy do częś ci związanej z przetwa rzaniem danych w programie w C++. Prawie ws zystkie zagadnienia związane z liczeniem w C++ są całkowicie intuicyjne, a więc będzie to dla nas bułk a z masłem .
wykonać
Instrukcia przypisania Do tej pory
widzieliśmy już
instrukcje przypisania kilka razy. Taka typowa instrukcja
wygląda
następuj ąco:
calosc = czescl
+
czesc2
+
czesc3:
Instrukcja przypisania pozwala na oblic zenie wartości wyrażenia znajdującego si ę po pra wej stronie znaku równości - w tym przypadku sumy wartości zmiennych czescl, czesc2 i czesc3- i zapisanie wyniku w zmiennej , której nazwę podajemy po lewej stronie - w tym przypadku jest to ca l osc. W tej instrukcji ca l osc je st sumą swoich części i niczym więcej. Zauważ , że
instrukcja jak zawsze zako ńczon a j est ś redn ikiem .
Możemy również tworzyć powtarzające się
a
~
b
=
przypi sania, jak na
przykład :
2:
Powyższa
a, tak
że
instrukcja oznacza przypisanie zmiennej b wartości 2, a następnie wartości b zmiennej na końcu obie zmienne mają tę samą warto ść 2.
Typy Ivalue i rvalue Lvaluc to coś, co odnosi się do adresu w pamięci i nazywa s i ę tak, ponieważ każde wyrażenie , którego wynikiem jest l val ue, może pojawić się po lewej stronie znaku równoś ci (ang. lefi) w instrukcji przypisania. Większość zmiennych j est typu l val ue, ponieważ okre ślają one
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
95
miejsce w pam i ęc i . Jak się jednak później przekonamy, istnieją też zmienne, które nie są tego typu i nie mogą p oj aw i ć się po lewej stronie instrukcji przypisania, ponieważ ich wartości zo stały zdefiniowane j ako stałe . Zm ienne a i b p oj awiaj ące s i ę w poprzedn im przykładz ie są typu l va l ue. Natomiast wynik wyra żen i aa - b nie, po n ieważ nie okre ś la on adres u w p am i ęc i , gdzie warto ść ta mogłaby być przechowywana. Wy nik wyraże nia, który nie jest typu l val ue, nazywany jest r valu e.
Wiedza na temat typu 7va 7ue będzie nam s ię czasami przydawała w różnych częśc iach - często najmniej spodziewanych - a więc warto ją sobie przyswoić.
książki
Działania
arytmetyczne
Podstawowe operatory arytmetyczne , którymi dysponujemy, to operatory dodawan ia, odejmo wania , mnożenia i dzielenia. Reprezentowane są one odpow iednio przez symbole +, - , * oraz l . Sposó b ich działa nia jest dokładn ie taki, jakiego moglibyśmy s ię spodziewać, z wyjątkiem operatora dzielenia, który ma pewne odchylenia od normy , kiedy ma do czynienia ze zm ien nym i całkowitymi lub s tałymi, o czym s ię niebawem przeko namy . Przy użyc iu tych operato rów możemy pisać instrukcje pod obne do po niższej:
placaNet t o
=
godziny * stawk a - potracenia :
Wyrażenie to najpierw p omnoży wartości zmiennych godzi ny i stawka , a n a s tęp n i e od ilo czynu odejmie wartość zmiennej potra ceni a. Operacje mnożenia i dzielenia, jak można się spodziewać, wykonywa ne są przed dodawa niem i odejmowaniem. Na temat kolejności wyko nywania poszczególnych operatorów w wyrażeni ach będziemy jeszcze mówić w dalszej części tego rozdziału . Wyn ik całego działania godzi ny * st awka - potraceni a zostanie zapisany w zmiennej pl acaNetto.
Znak minusa, u żyty w ostat niej instrukcji , ma dwa operandy - odejm uje on wartość swo jego prawego operan du od wartości lewego. Nazywa się to operacją dwój kową, gdyż w grę wchodzą dwie wartości . Znak min usa może być także używany tylko z jednym operandem w celu zmiany znak u wartości, do której został zastosowany. W takim przypadku nazywa s i ę on jednoargumentowym znakiem minus. Po niższe p rzykła dy i l ustrują u życ i e operatora -:
int a =
o:
i nt b = -5: a = - b; II Zmienia znak operandu.
W powyższym przykładzie zmiennej a zostanie przypisana mentowy znak minus zmienia znak wartości operandu b. Zauważ, że
wartość
+5,
p o ni eważ
jednoargu
przypisywanie nie jest tym samym co równania znane nam z lekcj i algebry w szkole Jego celem jest określenie czynności do wyko nania, a nie proste stwierdzenie faktu. Wyraże nie po prawej stron ie operatora przypisania j est obliczane i wynik zostaje zapisany w l va l ue - zazwyczaj jest to zmienna , która znajduje się po lewej stronie . śred n i ej .
Spójrzmy na następuj ąc e
wyrażenie :
96
Visual C++ 2005. Od podstaw liczba = liczba
+
1;
Oznacza ono "dodaj l do bieżącej wartości przechowywanej w zmiennej l i czba, a następnie zapisz rezultat w tej samej zmiennej l i ezba". Jako normalne wyrażenie algebraiczne pozba wione byłoby ono sensu.
~ Ćwiczenie podstawowych działań arytmetycznych Podstawowe operacje arytmetyczne prze ćwiczymy sobie , obliczając , ile standardowych rolek tapety potrzeba nam do wytapetowania pokoju. Program wykonujący takie obliczenia przed stawiony jest na poni ższym listingu: II Cw2_05.cpp
II Oblicza nie liczby rolek po trzebnyc h do wytapetowa nia p okoju.
#include using std; ;cout;
using std : .ci n:
using st d: :endl;
int main() {
double height = 0.0. width double peri met er = O.O;
~
0.0. length
~
0.0;
II Wym iary pokoju.
II Obwód po koju.
const double rol lwidth ~ 53.0 ; const do uble rolllength ~ 10 .0*100.0:
II Szerokość standardowej rolki. II Długoś ć standardow ej rolki.
int st rips_per_roll = O; i nt strips_reqd = O: i nt nro11 s = O:
II Liczba pasków w rołce. II Liczba po trzebnych pasków. II Całko wita liczba rolek.
cout « endl « " Wpro wa d ź cin » height ;
II Przejście do nowego wiersza. wys o kość
pokoju w centymet rach: ".
cout «endl « " W p r owa d ź dł u g o ś ć i ci n » length » width:
II Przejś cie do nowego wiersza . s z e ro k o ś ć
st ri ps_per_roll = rolllength / height : perimeter = 2.0*(lengt h + widt h) ; strips_reqd = perimeter / roll wi dth :
w centymet rach:
u •
II Sprawdzanie liczby pas ków w rolce. II Obliczanie obwodu po koju. II Obliczanie ca łkowit ej liczby II potrzebnych pas ków tap ety. II Oblicza nie liczby rolek.
cout « endl « "Do wyt apetowani a tego pokoju potrzebujesz " « nrol l s « " rolek tapety . " « endl : return O:
Rozdział2.
• Dane. zmienne i działania arytmetyczne
97
Jeśli
nie je ste ś mistrzem klawiatury, to prawdopodobnie podczas pierwszej kompilacji otrzy masz kilka komunikatów o błędach. Po zlikwidowaniu literówek wszystko będzie działało bez zarzutów. Kompilator zgłosi także kilka o strzeżeń , ale nie przejmuj się nimi - upewnia się tylko, że wiemy, co robimy. Powód pojawiania się komunikatu o błędzie za chw il ę wyjaśnię .
Jak to działa Jedną
z rzeczy wyj aśn ij my sobie już na wstępie - nie b iorę odpowiedzialności za ewentualne braki tapety po wyliczeniach za pomocą tego programu! Jak si ę przekonasz, wszystkie błędy w obliczaniu liczby rolek spowodowane są sposobem działania C++ oraz odpadami , których nie da się uniknąć podczas przyklejania tapety - zazwyczaj 50 procent! Instrukcje z powyższego listingu przejrzymy sobie wiersz po wiersz u, zatrzymując się na chwi lę przy bardziej inte resuj ącyc h , nowych lub nawet ekscytujących właściwościach . Instruk cje pojawiające się przed rozpoczęciem funkcji ma in( ) są nam już bardzo dobrze znane, a więc ich tłumaczenie pomijam. Na początek warto powiedzie ć kilka słów na temat układu programu. Po pierwsze, instrukcje zawarte w ciele funkcji mai n() są wcięte , dzięki czemu łatw iej jest ocenić jego rozmiar. Po drugie, różne grupy instrukcji są od siebie oddzie lane pustym wierszem w celu zaznaczenia, że stanow ią grupy spełniające jakieś okre ślone funkcje. Wcinanie instrukcji jest podstawową techniką tworzenia układu kodu programów w C++. Jak się przekonasz, technika ta jest stoso wana w celu wizualnego wyróżnien ia logicznych bloków programu.
Modyfikator const Na samym początku ciała funkcji mai n() znajduje się blok deklaracji zmiennych używanych w programie . Instrukcje te s ą nam j uż bardzo dobrze znane, z wyjątkiem dwóch, które zawie rają pewne nowe właściwo ści : cons t daubl e ro11 width = 53.O: II Szero koś ć s tandardowej rolki. const daubl e ro11 1engt h = 10.0*100. O: II Długoś ć standa rdowej rolki). Każ da z nich rozpoczyna się od nowego s łowa kluczowego canst . Jest to modyfikator typu , który informuje, że zmienne nie tylko s ą typu daubl e, ale również są stałymi. Ze względu na fakt, że poi nformowali śmy komp ilator, że są to stałe, będzie on spraw dzał, czy któraś z in strukcji nie próbuje zmienić ich wartości . Je ś li tak , zgło si kom unikat o błędz ie . Zmienna zadeklarowana jako canst nie je st typu l val ue, a więc nie może występować po lewej stronie operatora przypisania. Możn a
to sp rawdz i ć , dodając w dowolnym miejscu po deklaracj i zmiennej ral l widt h wyrażenie podobne do tego p o n i żej : ra11wi dth
=
O:
Po dodani u tego kodu prog ram u nie b ę d z i e ' e r r o r C3 8 92 : ' r o l l wi d t h '
można skompilować,
a kompi lator
: you ca n n ot a ssig n to a v ariable t ha t
zgłosi błąd
i s c on s t " .
98
ViSUiII C++ 2005. Od podstaw w programie za pomocą sło wa kluczowego const je st bardzo gdy mamy zamiar używ ać ich wielokrotnie. Jednym z powodów, dla których lepiej je st deklarować stał e , niż u żywać c ałej masy literałów , j est fakt, że ich przezna czenie m o ż e nie b yć wystarczająco oczywi ste. Na przykład w a rtość 42 może o d no s ić się do dłu go ści ży c ia, do wsze ch świat a lub do c ze g oś jeszc ze innego, ale użycie st ał ej zmi ennej o nazw ie moj Wi ek o warto ś ci 42 wszystk o wyj aśnia . Drugim pow odem jest fakt , że j eśli zajdzie potrz eba zmienienia wartośc i takiej zmi enn ej, to wystarczy z m ie nić j ej defin i cj ę w pliku źró dłowym , a dok onane zmi any będ ą automatycznie widoczne w całym programie . Techniki tej
Defini owanie
stały ch używanych
u żyte czn e , z właszcza
b ęd ziem y używ a ć doś ć c zęsto.
Wyrażenia
wdeliniciach stałych
S tała
zm ienna rol l ength jest inicjalizow ana za p omoc ą wyrażenia arytmetyczn ego (lO. 0* 100. O). D zi ęki m ożl iwości użyci a wyrażeń aryt metycznych do inicjalizowa nia sta ły ch zmien nych nie musim y samod zielnie o b l i czać ich warto ści . Ponadto mog ą one by ć o wiele łatw iej sze do zrozumienia, tak jak w tym przypadku - wyrażeni e 10m razy 1OOcm jest o wiele j aśni ej sze , niż gdy byś my napi sal i po prostu 1000. Kompilator dokładn i e obl iczy w art o ści stałych . Natom iast gdyby śmy zro bili to sami, to - w za leżnośc i od naszych możliwo ści matema tycznych - istni eje pewien s topień ryzyk a wy stąpien i a pom yłki . Do inicjalizacji zmiennej można użyć dowolnego wyrażenia, włącznie z obiektami const , które Tak wi ęc je żeli j est to przydatne w naszym programi e, powierzchni ę stan dardo wej ro lki tapety m o żem y z adeklarować na stępuj ąc o:
już definiowali śmy.
~s t dou ble rolla rea = rol lwldth*rolllength ;
---~-------------------
Instrukcj a ta musi zostać umieszczona po deklaracji dwóch użytych w niej zmi ennych, ponie wszystkie zmienne pojawi aj ąc e się w wyrażen i u ini cj alizuj ącym stałą zmi enn ą muszą być wcześ niej przedstawione kompilatorowi. w aż
Wprowadzanie danych IJO programu Po deklaracji kilku zmi ennych typu i nt eqer następne cztery instrukcje danych wprowadzan ych za pom ocą klawi atury :
cout « end l « "W p rowadź ci n » helght;
cin
»
pokoju w centymetrach : ". II Przejście do nowego wiersza.
" Wpr owadź d ł ugo ś ć
l engt h
przyjm owani e
II Przejśc ie do nowego wiersza . wysoko ś ć
cout « end l «
ob sługuj ą
»
i
s z ero k ość
w centymetrac h. ".
widt h;
Najpierw wysłali śmy tekst na wyjście, w którym poprosiliśmy o podanie wym aganych danych , a następn i e wczyt ali śm y te dane za p om o cą ci n, który j est standardowy m strum ien iem wej śc iowy m. Jako pierwsz ą pobrali śmy wys oko ś ć pok oju , a następnie jego długo ść i s zerokoś ć. W prog ramie skierowanym do użytku dodaliby śmy j eszcze kontrolę poprawn o ś ci danych oraz sprawdze nie, czy maj ą one se ns, ale do tego mam y j eszcze zbyt mało wiedzy!
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
99
Obliczanie wyniku Liczbę
rolek tapety potrzebną do wytapetowania pokoju o podanych wymiarach obliczamy za instrukcji:
pomocą czterech
str ips_pe r_roll ~ roll lengt h / height ; perimete r ~ 2.0* (lengt h + width ): stri ps_reqd = perimeter / rol lwidth: nrol l s = st rips reqd / stri ps per rol l :
II Sprawdzanie liczby p ask ów w rolce. II Obliczanie obwodu p okoju. II Obliczani e ca łkowi tej liczby II potrzebnych p ask ów tapety . II Obliczanie liczby rolek.
Pierwsza instrukcja oblicza liczbę pasków tapety o długości odpowiadaj ącej wysoko ści pokoju , ze standardowej rolki, dzieląc długość rolki przez wysokość pokoju. A zatem jeżeli pokój ma 2,44 m wysoko ści , to 1000 dzielimy przez 244 , co daje nam wynik w przybli żeniu 4,09 . Jest tutaj jednak pewna rzecz, o której należy pamiętać . Zmienna prze chowująca wynik - str i ps-per -roll - została zadeklarowana jako typ i nt , a wi ęc może przechowywać tylko liczby całkowite. W wyniku tego wszystkie liczby z ułamkiem zostaną zaokrąglone do najbliższej liczby całkowitej , w tym przypadku do 4, i to właśnie ta wartość jest przechowywana w zmiennej . Jest to tak naprawdę taki wynik , jakiego byśmy tutaj chcieli, gdyż mimo że kawałki tapety można powklejać pod oknem lub nad drzwiami, to jednak przy obli czaniu się je pomija.
jaką możemy otrzymać
Konwersj a jednego typu na inny nazywa się rzutowaniem. W naszym przypadku jest to rzutowanie niejawne, gdyż w kodzie nie jest wyraźnie zaznaczone, ż e operacja taka jest wymagana - kompilator musi taką decyzję podjąć samodzielnie. Dwa ostrzeżenia , które otrzymaliśmy podczas kompilacji, informują nas o tym , że podczas konwersji typów mogą zostać utracone pewne dane. Podczas u żywania mechanizmu niejawnego rzutowania trzeba zawsze uważać. Kompilator nie zawsze zgłasza ostrzeżenie w takich przypadkach, a przypisując wartość jednego typu do zmiennej , której typ ma mniejszy zbiór możliwych do przechowywania danych, zawsze tra cimy jakąś ich część . Jeśli z mechanizmu rzutowania niejawnego skorzystamy w programie przypadkowo , to może to być trudnym do odnalezienia błędem. Jeśli nie mo żna takiego przypisania uniknąć, to można poinformow ać o tym kompilator w sposób jawny w celu pokazania, że nie jest to przypadek. Dokonujemy tego za pomocą rzutowania jawnego wartości znajdującej się po prawej stronie przypisania na typ i nt . Dzięki temu nasze wyrażenie wyglądałoby następująco:
stri psper ron
=
st atic_cast(ro l llengt h / height) ; II Oh licz licz b ę paskó w II w rolce.
stat i C_ cast z
wyrażeniem
w nawiasie po prawej stronie jawnie informuje kompilator, chcemy przekonwertować na typ i nt. Mimo że nadal tracimy jej część ułamkową, to kompilator zakłada, że wiemy, co robimy, i nie zgłosi ostrzeżenia. Więcej na temat stati c_cast() i innych typów rzutowania jawnego dowiemy się z dalszej części że wartość wyrażenia
rozdziału.
Zwróć uwagę
na sposób obliczania obwodu pokoju w następnej instrukcji. W celu pomnożenia sumy długości i szerokości przez dwa operację dodawania umieściliśmy w nawiasie. Zapew niamy w ten sposób, że dodawanie zostanie wykonane na początku , a następnie wynik zostanie
100
Visual C++ 2005. Od podstaw pomnożony
przez 2.0, dz ięki czemu otrzymamy właściwą wartość obwodu. Nawiasy są potrzebne, gdyż wyrażenia w nich zawarte obliczane są zawsze na początku. Jeśli w nawiasach znajdują się zagnieżdżone nawiasy, to obliczane są one w kolejności od najbardziej wewnętrz nego do najbardziej zewnętrznego . Trzecia instrukcja, obliczająca w ymaganą do pokrycia pokoju liczbę pasków tapety, jest podobna w działaniu do pierwszej. Wynik je st zaokrąglony do najbliższej liczby całkowitej, ponieważ ma być przechowywany w zmiennej całkowitej - st ri psJ eqd. Nie jest to d okładnie to, czego byśmy chcieli . Najlepiej byłoby , aby wartoś ć tę z aokrąglić, ale tego nie potrafimy jeszcze robić . Możesz do tego wróci ć i poprawić to po przeczytaniu następnego rozdziału . Ostatnie wyrażenie arytmetyczne oblic za liczbę potrzebnych rolek, dzieląc liczbę potrzeb nych pasków (i nteger) przez liczbę pasków w rolce (także i nt eger). Jako że dzielimy przez siebie dwie liczby całkowite, wynik także musi być całkowity , a reszta z dzielenia odrzucona . Musielibyśmy tak zrobić, nawet gdyby zmienna nro11 s była typu zmiennopozycyjnego. War tość całkowita będąca rezultatem wyraż enia zostałaby przekonwertowana na typ zmienno pozycyjny przed zapisaniem jej do zmiennej nro11s. Otrzymany wynik jest dokładnie taki sam, jak gdybyśmy zaokrąglili liczbę zmiennopozycyjnądo najbliższej liczby całkowitej . A le to nie jest to, czego my chcemy, awięc jeśli chcemy tego użyć , to musimy to poprawić .
Wyświetlanie Wyniki
wyniku obliczeń są wyświetlane
za
pomocą następującej
instrukcji :
cout « endl « "Do wyt apet owani a tego pokoj u pot rzebujesz " «
«
nrol ls
«
"
rolek tapety . "
endl :
Jest to prosta instrukcja wyjściowa, którą podzieliliśmy na trzy wiersze. Najpierw wysyła znak nowego wiersza, następnie łańcuch znaków : "Do wytapetowania tego pokoju potrzebujes z", potem wstawiona zostaje wartość zmiennej nro11s, a na koniec łańcuch "rolek tapety". Jak wi dać , instrukcje wyjścia w C++ są bardzo proste. Program ret urn
kończy się
po wykonaniu
poniższej
instrukcji:
o:
Wartość zero jest wartością zwracaną i w tym przypadku zostaje ona zwrócona do systemu operacyjnego. Więcej na temat wartości zwracanych dowiesz się w rozdziale 5.
Obliczanie reszty W poprzednim przykładzie zauważyliśmy, że w wyniku dzielenia dwóch liczb całkowitych otrzymuje się wynik z usuniętą resztą z dzielenia. W ten sposób, dzieląc 11 przez 4, otrzy mamy wynik 2. Jako że reszta z dzielenia może być czasami bardzo ważna (kiedy na przykład dzielimy ciastka pomiędzy dzieci), w C++ dostępny jest specjalny operator %. Przy jego użyciu możemy napisać następujące instrukcje, które rozwiążą nasz problem z ciasteczkami:
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
101
int reszta = O. ci ast ka = 19. dZleci = 5: reszt a = ciast ka %dzieci ; Zmienna reszta będzie miała wartość 4 - tyle wynosi reszta z dzielenia 19 przez 5. Aby obli czyć , ile ciastek dostanie każde dziecko, należy posłużyć się zwy kł ym operatorem dzielenia , jak poni żej :
each = ciastka I dzieci:
Modyfikowanie zmiennei Często
zachodzi konieczność zmodyfikowania istniejącej już w artośc i zmiennej (np. trzeba ją lub podwoić) . Aby zwiększyć zmienną o nazwie cou nt, możemy użyć n astępuj ącej instrukcji :
zw i ększyć
count = count + 5; Dodaje ona po prostu 5 do bieżącej wartości przechowy wanej w zmiennej count i wynik zapi suje z powrotem w tej zmiennej . Jeśli więc zmienna count po c z ątkowo miała w arto ść 10, to teraz ma już 15. To samo
wyrażenie możemy także zapisać
w krótszy sposób:
count += 5: Instrukcja ta mówi: .P obierz warto ś ć zmiennej count, dodaj do niej 5 i wynik zapisz z powro tem do tej zmiennej ". W podobny sposób możemy u żywać również innych oper atorów. Na przykł ad :
count *= 5: Instrukcja ta pomnoży bieżącą wartość zmiennej count przez 5 i wynik zapisze z powrotem do tej zmiennej . Ogólnie rzecz bi orąc , instrukcje można pisać według następującego schematu:
1s op= ps; W schemacie tym op oznacza jeden z poniżs zy ch operatorów:
*
+
«
»
&
>
Pi ęć
│
pierwszych z powyższych operatorów już znamy , a pozo stałe (które są operatorami prze i logicznymi) poznamy jeszcze w tym rozdziale. l s oznacza wszystko to, co mo że pojawi ć s ię po lewej stronie instrukcji, i zazwyczaj (chociaż nie zawsze) jest nazwą zmiennej . ps oznacza wszystko , co mo że pojawi ć się po prawej stron ie instrukcji. s unięc ia
Ogólna forma instrukcji jest równoznaczna z l s = l s op (ps);
poniższą:
102
Visual C++ 2005. Od podstaw Dzięki będzie
wstawieniu ps do nawiasu prawym operandem op.
Oznacza to, ż e
wyrażenie
to zostanie obliczone jako pierwsze, a wynik
możemy napi sać taką in strukcję :
a/ = b +c : i jest ona równoznaczna z:
a = a / (b +c) :
A zatem
wartość
a zostanie podzielona przez
sumę
b i c, a wynik z powrotem zapisany do a.
Operatory inkrementacji idekrementacji Wprowadzimy teraz dwa niezwykłe operatory arytmetyczne, zwane operatorami inkremen tacji i dekrementacji. Kiedy zaczniemy na poważne posługiwać się językiem C++, stwier dzimy, że operatory te są niezwykle przydatne. Są to operatory jednoargumentowe, których używamy do zwiększania lub zmniejszania wartości zmiennych przechowujących liczby cał kowite. Zakładając na przykład, że zmienna count je st typu i nt, poni ższe trzy instrukcje są jednoznaczne: count = count + l : count
+~
l ; ++count ;
Każda z nich zwiększa wartość zmiennej caunt o jeden. Ostatnia instrukcja, w której wyko rzystany został operator inkrementacji, jest najbardziej zwięzła.
Operator inkrementacji nie tylko zmienia wartość zmiennej, do której został zastosowany, ale w wyniku jego działania także powstaje wartość. A zatem użycie operatora inkrementacji w celu zwiększenia warto ści zmiennej o jeden może wystąpić jako część bardziej skompli kowanego wyrażenia. Je żeli zwiększamy wartość zmiennej za pomocą operatora ++, jak w ++count , wewnątrz innego wyrażenia, to jej wartość zostanie najpierw zwiększona o jeden, a następnie tak zwięks zona wartość zostanie użyta w dalszych oblic zeniach. Przypuśćmy na przykład, że zmienna caunt ma wartość 5 i że zdefiniowaliśmy zmienną typu i nt o nazwie ta ta l . Napiszmy następującą instrukcję: t otal = ++count + 6; Wynikiem jej
zmiennej cou nt do 6, a więc wartość całego 12 i liczba ta zostanie zapisana jako wartość zmiennej ta t a l .
działania będzie zwiększenie wartości
wyrażenia będzie wynosiła
Do tej pory wstawialiśmy operator inkrementacji przed nazwą zmiennej . Jest to tak zwana forma przedrostkowa tego operatora. Może on również mieć formę przyrostkową, czyli znajdować s i ę po nazwie zmiennej , do której został zastosowany. W zależności od użytej formy efekt jest nieco inny . Wartość zmiennej , do której stosujemy operator inkrementacji, zostanie zwiększona tylko wtedy, gdy została ona użyta w jakimś kontekście . Cofnijmy na przykład wartość zmiennej count z powrotem do 5 i przepiszmy naszą instrukcję w następujący sposób: tot al = count ++ + 6;
Rozdział 2.•
Dane. zmieniłe i działania arytmetyczne
103
Zmiennej t ata l zosta nie przyp isana wartość 11, ponieważ wartoś ć poc zątkowa zmiennej caunt jest tutaj użyta do obliczenia warto ści wyra żenia prze d zwięks zeniem j ej o jeden . Instrukcja tajest równoznaczna z dwiema poniższymi: t ota l = count + 6; ++count : Takie nagromadzenie znaków + jak w naszym przykładzie może prowadzić do ni ep oroz u m i e ń . Og ólnie rzecz biorąc , taki spos ób stosowania operatora inkrementacji jak w poprzednim przykładz ie nie jest zbyt dobry. O wie le lep iej byłoby napi sać : total = 6 + count++: M ając inst rukcję taką jak
a++ + b lub a+++b, moż na się łatwo pogubi ć , co ma zos tać wyko nane lub co zrob i kompi lator. Obie te instrukcje oz naczają to samo, ale w tej drugiej możliwe, że mie liśmy na myśl i a + ++b, co da j uż inny wyn ik - o jeden większy n i ż poprzednie dwie . Dokładnie
takie same zasady jak do operatora inkrementacj i maj ą zastosowanie do operatora Jeżel i na przykład zmienna ca unt ma warto ść początkową 5, to po wyko dekreme ntacj i: naniu instru kcji: -- o
t otal = --count + 6: zmienna t ata l t otal
=
będzie m iał a w artość
10, nato miast instrukcja :
6 + count - -:
na 11. Oba operatory stoso wane są zazwyczaj do liczb całkow ityc h, w szcze w pętlach , o czym przekonamy się j uż w nas tępny m rozdziale. P óźni ej dowiemy s i ę, że opera tory te mogą być stosowane także do innych typów danyc h, a zwłaszcza do zmien nych przec howującyc h adresy.
ustawi
tę wartość
gó l ności
~
Operator przecinkowy
Operator przecinkowy pozwala na podanie kilku wyrażeń w miej scu , gdzie normal nie ty lko j edn o. Najł atwiej jest to zrozumieć na przy kładzie :
p oj aw ić się
II Cw2_06.cpp
II Ćw iczen ie zast osowania opera tora prz ecinka.
#incl ude usi ng st d: .cout : usi ng st d . rendl : int mai n( )
« "j est wa r t o ś ć osta t niego wy raż em a po prawej: "
« num4 :
może
104
Visual C++ 2005. Od podstaw cout
«
end l ;
ret urn O;
Jak to dziala Po skompilowaniu i uruchomieniu tego programu otrzymamy War t o ś c i ą
szeregu
wy r a ż e ń
jest
wa r t o ś ć
następujący rezultat:
osta t niego wyrazeni a po prawej ; 30
Kod ten jest bardzo prosty. Zmiennej num4 została przypisana wartość ostatniego z trzech przy Nawiasy w tym przypisaniu są konieczne. Gdybyśmy je pominęli , to pierwsze wyrażenie oddzielone przecinkiem od pozostałych miałoby postać :
pisań .
num4 = numl = l a w wyniku czego
wartość
Oczywi ś cie wyrażenia
zmiennej num4 wyniosłaby 10.
oddzielane przecinkami nie instrukcje:
mus zą b yć
przypisaniami. Równie dobrze
mogliby śmy napisać na stępujące
long num l = l , num2 = l a, num3 = 100 , num4 num4 = (++numl, ++num2, ++num3 ) ;
~
O:
Rezultatem tego przypisania będzie zwięks zen ie wartości zmiennych numl, num2 i num3 o jeden, a następnie ustawienie wartości zmiennej num4 na wartoś ć ostatniego wyrażenia, czyli l al. Przykład ten ma na celu zaprezentowanie sposobu działania operatora przecinka , ale nie jest wzorem dobrego stylu programowania.
Koleiność wykonywania obliczeń Do tej pory nie mówiłem nic na temat kolejności obliczeń wykonywanych podczas wyzna czania w artości wyrażenia. Ogólnie jest ona podobna do tej, której uczymy się w szkole na lekcjach matematyki, ale w C++ istniej e trochę więcej operatorów. Aby zrozumieć , co się z nimi dzieje, musimy przyjrzeć się dokładniej mechanizmowi języka C++ stosowanemu do określan ia tej kolejności . Mechanizm ten nazywa si ę priorytetem operatorów.
Priorytety operatorów Jest to mechanizm , który ustawia w zadanej kolejności priorytety operatorów . W wyrażeniach operatory o najwyższym priorytecie wykonywane są na początku , następnie wykonywane są operatory o najwyższym po nich priorytecie i tak dalej aż do operatorów o prioryteci e najniż szym . P oni żej znajduje się tabela priorytetów operatorów C++:
reinterp ret _cas t sizeof new delete . . .typeid .* (jednoargumentowy) ->*
lewa
* / %
lewa
+ -
lewa
«
»
lewa
< >= I ~
lewa lewa
&
lewa lewa
>
lewa
&&
lewa
»
lewa
?
(operator warunkowy)
prawa prawa lewa
Tabela zawiera wiele operatorów, których jeszcze nie znamy, ale do końca książki będziemy znać je wszystkie. Umieściłem je wszystkie w tabeli , dzięki czemu w razie potrzeby zawsze będzie można do niej wrócić i sprawdzić priorytet jednego operatora w stosunku do innego . Operatory o najwyższym priorytecie znajdują się na samej górze tabeli. Operatory znajdujące się w tej samej komórce mają taki sam priorytet. Jeżeli w wyrażeniu nie ma nawiasów, to operatory o takim samym priorytecie wykonywane są w kolejności określonej przez łączność . A zatem jeżeli łączność jest lewa, to najpierw wykonywany jest operator znajdujący się po lewej stronie, przechodząc coraz dalej w prawą. Oznacza to, że wyrażenie takie jak a + b + C + d zostanie obliczone, tak jakby było zapisane (( (a +b) + c) + d ), ponieważ binarny + ma łączność lewą. Zauważ, że w przypadku operatorów posiadających zarówno formę jednoargumentową (działa z jednym operandem), jak i dwuargumentową (działa z dwoma operandami), ta pierwsza ma zawsze wyższy priorytet i dzięki temu wykonywana jest zawsze wcześniej .
106
Visual C++ 2005. Od podstaw Kolejność
wykonywania operatorów można zawsze zm ienić za pomocą nawiasów. Ze na fakt, że w C++ dostępnych jest tak wiele operatorów, czasami trudno się połapać, co ma pierwszeństwo przed czym. Dobrym pomysłem jest w takim przypadku zastosowanie nawiasów. Dodatkowym plusem takiego podejścia jest fakt, że nawiasy zwiększają czytelność kodu.
względu
Typy zmiennych irzutowanie Obliczenia w C++ mo gą być wykonywane przy użyciu w artości tego samego typu. Kiedy napiszemy wyrażenie zaw i e raj ące zmienne lub stałe różnych typów, to kompil ator dla każdej z takich operacji musi dokonać konwersji jednego z operandów, aby pasował do drugiego. Proces ten nazywa się rzutowaniem. Je żeli na przykład chcemy dodać liczbę typu doubl e do liczby typu i nteger, to liczba i ntege r najpierw zostanie przekonwertowana na typ doubl e, a dopiero potem zostanie wykonane działanie dodawania. Oczywiście sama zmienna, która zawiera rzutowaną wartość, nie jest zmieniana. Kompilator przechowa przekonwertowaną war tość w pamięci tymcza sowej i zostanie ona usunięta po zako ńc ze n i u wykonywania obliczeń . Wybór operandu, który zostanie przekonwertowany, jest uzależniony od pewnych reguł. Każde rozbija się na kilka operacji pomiędzy dwoma operandami. Na przykład wyrażenie 2*3-4+5 zostanie podzielone na następujący szereg działań: 2*3 da w rezultacie 6, 6-4 wynosi 2 i na koniec 2+5 da nam wynik 7. A zatem zasady dotyczące rzutowania operandów, tam gdzie to konieczne, muszą być zdefiniowane tylko w kwestiach dotyczących par operandów. Dla każdej pary operandów różnego typu stosowane są poniższe zasady w podanej kolejno ści. Gdy kt óraś z zasad odnosi się do określonej sytuacji to jest ona stosowana. wyrażenie
Zasady rzutowania operandów 1.
Jeśli
2.
Jeśli
3.
Jeżeli
jeden z operandów jest typu long doubl e, to drugi przekonwertowany na ten typ. jeden z operandów jest typu doubl e, to drugi na ten typ.
też
też
zostanie
zostanie przekonwertowany
jeden z operandów jest typu fl oat , to dru gi zostanie prze konwertowany na ten typ .
4. Typy char, s ig ned cha r , unsi gned cha r, short i unsig ned s hort konwertowane są
na typ i nt.
5. Typ wyliczeniowy konwertowany jest na pierwszy z typów i nt, uns i gned i nt , l ong lub uns i gned l ong, który
6.
Jeśli
7.
Jeśli
elementów wyliczenia.
jeden z operandów jest typu uns i gned l ong, to drugi konwertowany jest na ten typ .
są
8.
mo że pomieści ć liczbę
jeden operand jest typu Io nq, a drugi typu uns i gned i nt , to oba operandy konwertowane na typ unsi gned lo ng.
Jeśli
jeden z operandów jest typu l onq, to drugi konwertowany jest na ten sam typ.
Rozdział 2.•
Dane. zmienne i działania arytmetyczne
107
Na pierwszy rzut oka zasady te wydają s ię bardzo skomplikowa ne, ale ogó lna zasada j est taka, że typ o mniejszym zakresie wartości konwert uje s ię na typ o większy m zakres ie. Zwiększa to prawdopodob ieństwo , że b ędzi emy w stanie prze ch ować wynik. Dz i ałan ie tych zasad m ożem y wypróbować na hipotetyczn ym wyrażeni u . P rzypuśćmy , że mamy kilka deklaracj i zmiennych:
dou ble val ue = 31 .0;
i nt count = 16 :
flo at ma ny = 2.0f:
char num = 4:
Przypuś ćmy t akże , że
dysponujemy
p on i ż s zym
val ue = (value - count) *(count - num)!many Możemy
z niego instrukcj i.
wywnioskować ,
jakich
+
przypadkowym
wyrażenie m
arytmetycznym:
num!many:
rzu to wań
dokona komp ilator podczas wykonywania
Pierwsza operacja polega na obliczeniu wartości wy raże n ia (va l ue - count ), Zast osowanie ma tutaj re guła 2., w myśl której wartość zmiennej count zosta nie przekonwertowana na typ doubl e, a zwrócony wynik b ęd z i e tego właś n ie typu, czy li 15. O. N as tępnie
przechodzimy do obliczania wartości wyraże nia (count - num). W tym przypadku pierwsza zasada, która ma zastosowanie , to zasa da num er 4. A zatem zmie nna numzostanie prze konw ertowana z typu char na typ i nt i w wyniku otrzymamy li c zb ę całkowi tą 12.
Nas tępne
obliczanie dotyczy p owyż s zych dwóch wyników - liczby typu doubl e 15 oraz liczby 12. Tutaj zastos owanie ma reguła numer 2 - liczba 12 zostanie przekonwert owana na 12 . O, a n a stępni e zwrócony zosta nie wynik typu doubl e 180. O.
całkowi tej
Wynik ten musimy teraz pod z i eli ć przez wartość zmiennej many. Tutaj ponownie zastosowa nie ma r e guł a num er 2. W art o ś ć zmiennej many zos ta nie przekon wert owana na typ doubl e przed wygenerowaniem wyniku tego samego typu o w a rto ś c i 90. N as tępni e
ob licza na jest w arto ś ć wyraż en i a num/ many, gdzie zastosowanie ma reguł a num er 3, która powoduje powstanie wartości 2 . Of typu fl oat, będącej wynikiem konwersji zmiennej numz typu char na typ fl oa t.
Na zakończe nie wartość typu doubl e 90. Ojes t dodawana do w a rto ś c i typu fl oat 2 . Of . W tym przypadku stosujemy reguł ę 2. Po przekonwertowan iu 2 . Of na 2 .Odo zmiennej val ue zostaje zapisany wyni k 92 .O. Po tych wszystkich
wyjaś nie niac h powinn iś my mie ć już
ogó lne rozeznanie .
Rzutowanie winstrukcjach przypisania Jak j uż przekona liśmy się w p rzykład zi e Cw2_05.cpp, rzutowanie niejawne można spowodo w ać poprzez napisanie po prawej stronie przy pisa nia wyrażenia o innym typ ie n iż zmienna po lewej . Może to spowo dować zmianę wartości i utrat ę danych. Je śl i przypiszemy na przy kład w artoś ć typu f l oat lub doubl e do zmie nnej ty pu i nt lub l ong, to utracimy część uł am kow ą tych wartości (można utraci ć nawet więcej , j eż el i zmien na typu zmien nopozycyj nego przekracza zasięg wa rtoś c i prze widzianych dla typu i nteger).
108
Visual C++ 2005. Od podstaw Na
przykład
po wykonaniu
poniższego
fragmentu kodu:
int number = o;
float decimal = 2.5f ;
number = decimal ;
wartość zmiennej number będzie wynosiła 2. Zauważ literę f na końcu stałej wartości 2. 5f. Informuje ona kompilator, że jest to liczba zmiennopo zycyjna typu si ngle. Bez litery f do myślnie byłaby to liczba typu doubl e. Każda s t a ł a zawierająca czę ść dziesiętną jest typu zmiennopozycyjnego. Jeśli nie chcesz, aby była ona typu doubl e, to musisz dodać literę f. Takie samo znaczenie m iałaby tutaj wielka litera F.
Rzutowanie jawne W przypadku wyrażeń zawierających wartości różnych typów podstawowych kompilator automatycznie dokona rzutowania tam, gdzie jest ono potrzebne . Ale jeśli chcemy, to możemy także taką operację wymusić za pomocą rzutowania jawnego. W celu rzutowania wartości wyrażenia na okre ślony typ posługujemy się następującą instrukcją: static_cas t ( wyra żen i e )
Słowo kluczowe st at i c_cast oznacza, że rzutowanie je st sprawdzane w sposób statyczny, a więc podczas kompilacji programu. Po uruchomieniu programu nie będzie już sprawdzane, czy zasto sowanie rzutowania jest bezpieczne. Później , kiedy będziemy zajmować się kla sami, spotkamy się ze słowem kluczowym dynami c_cast, które powoduje, że konwersja jest sprawdzana dynamicznie, czyli podczas wykonywania programu . Istniejąjeszcze dwa rodzaje rzutowania: const _cast do usuwania stanu stało ści wyrażenia oraz re i nt erpr et _cast , które jest rzutowaniem bezwarunkowym, ale na razie nie będziemy s i ę nimi zajmować.
Efektem powyższego rzutowania statycznego jest konwersja wartości wyrażenia exp ressi on do typu podanego w nawiasach ostrych. Jako express i on mo żna wstawić cokolwiek - od pojedynczej zmiennej po złożone wyrażenie zawierające wiele zag n i eżd żo n y c h nawiasów . Poniżej
znajduje
s ię
szczególny przypadek
użycia
double value1 = 10.5: dou ble value2 = 15.5: i nt whole number = stat ic cast (val uel )
+
sta t i c_cast( ):
sta tl c cast(va l ue2);
Wartością inicjalizującą zmiennej whole_number jest suma całkowitych czę ści warto ści zmien nych val ue l i val ue2, a więc są one przekonwertowane w sposób jawny na typ i nt . Dzięki temu zmienna ta przyjmie wartość 25. Rzutowanie nie wpływa na warto ści przechowywane w zmiennych val ue1 i va l ue2, które nadal będą wynosić odpowiednio 10.5 i 15.5. Wartości 10 i 15 są tylko tymczasowo przechowywane do użycia w obliczeniach , a po ich zakończeniu usuwane z pamięci . Mimo że każde z tych rzutowań powoduje utratę danych, kompilator zakła da, że wiemy, co robimy, skoro stosujemy rzutowanie jawne.
Poza tym, jak już pi sał em w przykładzie Cw2_05.cpp odno szącym się do przypisań z różnymi typami, zawsze można poinformować, że wiemy, iż rzutowanie jest konieczne, poprzez zrobie nie go jawnym:
Rozdział 2.•
Dane. zmienne i działania arytmetyczne
109
st r tpsperj-ol l = stat ic_castCro111ength I hei qht ) : IISprawdź liczbę paskó w II w rolce.
Rzutowanie jawne można stosować z dowolnych wartości numerycznych na dowolne inne wartości numeryczne, ale trzeba zawsze być świadomym możliwości utraty informacji . Jeśli na p rzykład dokonamy rzutowania wartości typu fl oat lub doub l e na typ l onq, to w wyniku konwersji utracimy część ułamkową liczby, a co za tym idzie, jeżeli liczba była mniejsza od l , to otrzymamy w wyniku O. Przy rzutowaniu z typu doubl e na typ f lo at strac imy na precyzji , ponieważ zmienna typu fl oat ma precyzję tylko siedrniocyfrową, a doubl e aż piętnastocyfrową. Nawet rzutowanie pomiędzy typami i nteger niesie ze sobą ryzyko utraty danych, w zależności od zastosowanych wartości. Na przykład wartość typu l ong może być zbyt duża, aby można ją było przechowywać w zmiennej typu short , a więc rzutowanie z typu l ong na short może prowadzić do utraty danych. Ogólnie rzecz biorąc, w miarę możliwości powinno się unikać rzutowania. Jeżeli okaże się , w swoim programie potrzebujesz wielu rzutowań, to prawdopodobnie został on źle zapro jektowany. Należy w takim przypadku przejrzeć jeszcze raz jego strukturę oraz sposoby wybierania typów danych i w miarę możliwości zlikwidować lub przynajmniej zredukować że
liczbę rzutowań.
Rzutowanie wstarym stylu Przed wprowadzeniem do C++ rzutowania stat i c_cast () (i pozostałych typów rzutowania: const_cast-c-r ), dynami c_cast ( ) oraz rei nterpret _cast( ), o których będziemy jeszcze mówić) rzutowanie
jawne wyniku
wyrażenia na
inny typ
było
zapisywane
następująco:
Ctyp_do_kt órego_ma_nastąplć_ko n wersJaJwyrażen ie
Wynik liczbę
wy raż e n i a jest rzutowany na typ podany w nawiasie. Na pasków w rolce moglibyśmy zapisać następująco :
str i ps per roll = Ci nt )( ro11 1ength I heiqht ) :
II Oblicz
przykład instrukcję obliczającą
li czb ę pasków w
rolce.
Istnieją cztery typy rzutowania i każdego z nich można dokonać za pomocą starej składni. Ze względu na to kod, w którym wykorzystane jest rzutowanie starego typu, jest bardziej podatny na błędy - nie zawsze jest jasne, co mieli śmy na myśli, i możemy otrzymać inny wynik, niż się spodziewaliśmy. Mimo że rzutowanie w starym stylu jest jeszcze dość często spotykane (nadal jest częściąjęzyka i z powodów historycznych można j e spotkać w bibliotece MFC), to gorąco zachęcam do stosowania rzutowania tylko w nowym stylu .
Operatory bitowe Operatory bitowe traktują operandy jako szer eg bitów, a nie wartości numeryczne. Można ich używać tylko z typami całkowitymi, a więc : sho rt, i nt, l ong, si gned char i char oraz wer sjami tych typów bez znaku. Operatory bitowe są użyteczne w programowaniu sprzętu, gdzie status urządzenia reprezentowany jest przez szereg indywidualnych znaczników (to znaczy, że każdy bit w bajcie może określać status innego aspektu urządzenia), lub w każdej innej
110
Visual C++ 2005. Od podstaw sytu acji, w której zachodzi potrzeba upakowania zestawu znaczników typu włączon y- wyłą czony w jednej zmiennej. Sposób ich d z iałania pozn amy przy okazji s zczegółow ego om awia nia operacji wejścia-wyjścia, gdzie pojedyncze bity używ ane są do kontrolowania ró żn ych opcj i ob sługi dan ych . Istni eje
s z eść
operatorów bitowych: bitow y Ok
& bitowy AND
>
- bitow y NOT
» prze sun i ęc ie w prawo
Poniżej w yj aśniam
spos ób
A
bitowy
wyłączny
aR
« przesuni ęc i e w lewo
działani a każdego
z nich.
Opera10r bilowy AND Bito wy operator AND (&) jest operatorem binarnym, który łączy odpowiadające sobie bity w jego ope randach w okre ślony sposó b. Gdy oba bity maj ą w artość I, to zwracana jest warto ś ć l, w przeciwnym przypadku zwracana w arto ś ć to O. Efekt działania operatora binarnego cz ęs to pokazywany jest za pomoc ą tak zw anej tabeli prawdy. Pokazuje ona, jaki byłby wynik przy użyciu różnych kombinacji operandów. Tab ela prawdy dla operatora &przedstawia s ię następująco: Bitowy AND
O
O
o
1
O
O
Wynikiem łączeni a każdej kombinacji wiersza i kolumny za p omocą operatora &je st pod ana w punkcie ich przecięcia . Prze śl ed źmy na przykładzie , jak to d ziała:
char let t erl ~ 'A' , lett er2 = ' Z' . resul t result ~ let t erl &let t er2: Aby
~
wartość
O:
zobaczyć ,
co si ę dzieje, musimy spoj rzeć na wzory bitowe. Literom "A" i ,;Z" odpowia szesnastkowe Ox4] i Ox5 A (Kody ASCII pod ane zo stały w dod atku B). Sposób operatora bitowego AND na te dwie wartoś c i poka zany zo stał na rysunku 2.8.
daj ą wartości działania Mo żem y
to potwierdzić , sprawdzając , jak odpowiadaj ące sobie bity łączą s ię za pom ocą & w tabel i prawdy. Po przypisaniu otr zymamy wyn ik Ox40, co odpowiada znakowi @. na fakt , że operator & daje w wyniku zero, gd y ob a bit y m aj ą wartość zerow ą, go używać do ustawiania w zmi ennej niechcianych bitów na zero. Dokonujemy tego, tw orząc tak zwaną maskę i łącząc ją z oryginalną zmi enn ą za pomo cą operatora &. M askę tworzym y poprzez o kreś l e n i e wartości jeden, tam gd zie chcemy iachować bit , i zero tam , gdzie chcemy bit u stawi ć na zero . W wyniku łączenia maski z inną warto ści ą całkowitą otrzy mamy bity zerow e w miejscach , gdzie u stawili śmy bity zerowe w masce, oraz takie same war to ś ci jak oryginalny bit w zmiennej , tam gdz ie w masce jest bit o wartości jeden. Przypuśćmy,
Ze
w zględu
możn a
~
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
111
Rysunek 2.8
letter1 : Ox41
letter2 : ox5A
result: ox40
o
1
o
o
o o o
1
1
1
i& l& i& i& &i i& &i &i !o ! !o ! !!o ! !o i i i i i ii l J l JT rr J o o o o o 1
01
o
1
r
że
mamy z m ie nn ą l etter typu char , z której chcemy u sun ąć cztery bity wysokie i zach ować cztery bity niskie. M ożn a tego łatwo d okona ć , tworz ąc m a s k ę OxOF i łącz ąc j ą z wart o śc i ą zmiennej l etter przy uży ciu operatora &:
letter
=
l et t er &OxDF :
Lub bardziej zw i ęź le :
letter &= OxOF : Je żeli w arto ść
zmiennej char wyno s iła Ox4l , to po wykonaniu p owyż szy ch instrukcji otrzy mamy wynik OxO l. Operacja ta zo st ał a przedstawi ona na rysunku 2.9. Bity zerowe w masce powoduj ą, ż e odp owi adające im bity w zmiennej l etter zostają usta wione na zero, a bity o wartośc i jeden w masce powoduj ą, że od p owiad aj ące im bity w zmien nej zos tają zac howane w swojej oryg ina lnej postaci.
W podobny sposób za pom ocą maski OxfD możemy za c hować cztery bity wysokie i wyzerować cztery bity niskie. A zatem w wyniku poni żs zej instrukcji w arto ś ć zmiennej l etter zmieni się z Ox4l na Ox40:
letter &= OxFO:
Bitowy operator sumy logicznej Bitowy operator sumy logicznej OR ( I) , czasami nazyw any a lternatyw ą bitow ą, wiąże odpo wi ad aj ąc e sobie bity w tak i spos ób, że w wyniku otrzymuj emy w arto ść j eden, j e ż eli jeden z operandów ma warto ś ć je den, i zero, je żel i oba operandy m aj ą w artość zero. Tabela prawdy dla tego operatora przedstawia się następująco: Bitowy OR
O
O
o
l
112
Visual C++ 2005. Od podstaw
Rysunek 2.9
letter: ox41
ma sk:OxOf
result: oxoi
o
1
o
o
i& &ff& l& '!o !o Jo !o ffff - - - -
Jo J'l r o o o
o
00
ff f
1
t
& & & &
! !! ! ff ff J! ! 1
1
1
1
.o
o
o
1
Przećwic zmy na pr zykładzie , jak można ustawić poszczególne znaczniki upakowane w zmien nej typu c ałk owitego . Przypuśćmy , że mamy zmienną o nazwie styl e typu sho rt , która za wiera 16 jednobitowych znaczników. Załóżmy, że chcemy u stawi ć poszczeg ólne znaczniki w tej zmienn ej . Jednym sposobem jest zdefiniowanie warto ś ci , które można następnie powią zać z operatorem OR w celu włączenia określonych bitów . Aby u stawi ć ostatni bit po prawej, m ożn a zd e fi n i ow ać:
sho rt vredraw Aby
u stawi ć
~
OxO l;
drugi bit od prawej , zmienną hredraw można zd e fin i o wać w
sho rt hred raw
=
następujący
sposób :
Ox02;
A zatem aby w zmiennej sty le u stawić dwa ostatnie bity po prawej na l,
można posłużyć się
następuj ąc ą i nst ru kcją:
style
~
hredraw I vredraw;
Wynik tej instruk cji pokazany jest na rysunku 2.10 . Jako
że
operacj a przy
u życiu
wartość jeden, łączenie
bity
operatora OR daje wynik jeden, jeżel i któryś z dwóch bitów ma dwóch zmiennych za pomocą tego operatora daje wynik , w którym oba
są wł ąc zone .
C zęs to
zostać
zachodzi potrzeba ustawienia znaczników w zmiennej bez zmiany innych, które mogły ustawione już gdzie ś indziej. Można tego łatwo dokonać za pomoc ą poniższej instrukcji;
style
I~
hred raw I vredraw ;
Instrukcja ta ustawi dwa ostatnie (po prawej stronie) bity zmiennej sty l e na l , a pozo stałe pozostawi bez zmian.
Rozdział 2.
hredraw:oxoz
• Dane. zmienne i działania arytmetyczne
o o o o
00 o o ·
o o o o
on on on on
on on onon
on on on on
rrlr
on on aR on
l l l l i i i i = : ;: = =
l l l l i i i i = = = ::;:
lo lo lo lo i i i
lo lo lo l1 i i i i = = ::;: =
IIi l l l l l
., o o o o
vredraw:OxOl
ł
style:Ox03
!ł !
o o o o
o o o o
!.! !! o o o o
r I'rII
o o o o
113
o o 1 o
II r l !o !o ł1 !1
Rvsunek 2.10
BitoWY operator różnicy symetrycznej Bitowy operator różni cy symetrycznej (ang. Exclusive DR; A) ma podobne dzi ałani e do opera tora alternatywy, ale daje wynik zero, gdy oba operandy m ają w artość jeden. W zwi ązku z tym j ego tabela prawdy przed stawia s i ę następuj ąco: Bitow yEOR
O
O
O O
Spój rzmy na wynik wykonania poni ższej instrukcji przy nych co w przypadku operat ora AND: result = letter1
A
użyci u
tych samych
warto ści
zmien
l et t er2;
Operacj ę tę możemy przedstawić następuj ąco :
l et t er1 01 00 0001 let t er2 0101 lala
Wynik
dział an i a
operatora EOR jest n a st ępuj ący :
resul t 0001 101 1 Wartość
zmiennej result zostaje ustawi ona na Oxlb lub -
w zapisie
d zie siętnym
-
na 27.
Operator ma do ść zaskakuj ąc ą właściwoś ć . Przypuśćmy , że mamy dwie zmienne znakowe: fi rst o warto śc i A i l ast o warto ś ci Z, którym odpowiadają w arto ści binarne 0100 0001 i 0101 lala. W wyniku poniż szych instruk cj i: A
114
Visual C++ 2005. Od podslaw f i rst l ast : I/Wynik fi rslto 00011011. l ast A ~ fi rst : II Wynik last to 0100 0001.
f i rst l ast : II Wynik first to 0101 1010.
A=
A=
zmienne te zamienią się wartościami , nie korzystając z żadnej Działa to ze wszystkimi wartościami typu całkowitego .
pośredniej
lokalizacji
pamięci .
Bitowa negacia Bitowy operator negacji - zamienia wartości pojedynczych operandów na przeciwne. Tak więc jeden staje się zerem, a zero staje się jedynką. Przy zał ożeniu, że wartość zmiennej l etterl wynosi 0100 0001, po wykonaniu poniższej instrukcji: result
=
-l etter l; wartość
zmienna re sult przyjmie sie dziesiętnym .
10ll l ll O, co odpowiada
wartości
OxBE lub 190 w zapi
Bitowe operalory przeSlInlęć Operatory te zwracają wartość zmiennej całkowitej, przesuniętą o określoną liczbę bitów w lewo lub prawo . Operator » służy do przesuwania w prawo, a operator « - w lewo. Bity, które wychodzą poza granice zmiennej, są tracone. Na rysunku 2.11 widać efekt przesunięcia dwubajtowej zmiennej w prawo i w lewo . Pokazano także wartość początkową. Zmienną o
nazwie number deklarujemy za
unsigned int number
~
całkowite
Możemy przesunąć zawartość
« =
2;
instrukcji :
16387U;
Jak już mówiłem, zmienne number
pomocą następującej
bez znaku zapisujemy, dodając literę u lub Una ich końcu . tej zmiennej w lewo za pomocą poniższej instrukcji :
II Prz esunię cie w lewo o dwa bity.
Po lewej stronie operatora przesunięcia znajduje się wartość, którą chcemy przesunąć, a po prawej podajemy liczbę bitów, o którą ma nastąpić przesunięcie . Na rysunku widać efekt takiej operacji . Jak w idać , przesunięc ie wartości 16387 o dwie pozycje w lewo dało w wyni ku liczbę 12. Ta dość drastyczna zmiana wartości spowodowana została utratą bitu wysokiego w wyniku przesunięcia. Wartość tę możemy również przesunąć w początkową -
number » = 2;
16387, a
następnie
prawo. napiszmy:
Przywróćmy
zmiennej number jej
wartość
II Przesunięcie o dwa bity w prawo.
Instrukcja ta przesuwa liczbę 16387 o dwa bity w prawo, w wyniku czego otrzymujemy war 4096 . Przesunięcie o dwa bity w prawo spowodowało podzielenie wartości przez cztery (bez reszty). To także zostało pokazane na rysunku.
tość
Rozllział 2.
RysUnek 2.11
• Dane, zmienne i działania arytmetyczne
115
Liczba dz iesiętna 16 387 w zapisie dwójkowym :
Przesun ięcie
wlewoo2:
.Pr zesunięcie
w prawo o 2:
=4096
Dopóki żadne bity nie są tracone, przesuwanie o n bitów w lewo jest równoznaczne z mnoże niem n razy danej wartośc i przez 2. Inaczej m ówiąc, jest to równoznaczne z mnoż eniem przez 2". Podobnie prze suni ęci e w prawo o n bitów jest równoznaczne z dzieleniem przez 2". Na leży jednak pamiętać , że j e śli w wyniku przesun i ęcia zostan ą utracone jakie ś w ażne bity, to wynik w niczym nie będz i e przypominał tego, czego s ię spod zi e w aliśm y , choć ta operacja nie różn i się od mnożenia. Gdyby śmy pomnożyli l iczb ę dwubajtową przez cztery, to otrzymal i byś my taki sam wyn ik, tak więc prze sun ięcie w lewo i mnożenie nadal są tym samym. Problem z do kładn oś ci ą pojawia si ę , ponieważ wartość wyniku mnożen i a jest poza za si ęgi em dwubajtowej liczby c ałko witej . M o że
nam się wyd aw a ć , że operatory przesuni ę cia m o g ą myli ć si ę z operatorami wej ścia Kompilator zawsze odgadnie z kontekstu, o który operator chodzi. J e śli jednak nie będ zi e to takie oczywi ste, to wygeneruj e komunikat, ale musimy być tutaj bardzo ostro żni. Jeśl i na przykład chcemy wy słać na wyj ś ci e wynik przesunięcia w lewo o dwa bity zmiennej number , to m ożemy u żyć n astępującej instruk cji: -wyj ści a .
cout
«
(number
«
2);
W tym przypadku n ajważniejszą rolę odgrywają nawia sy. Bez nich opera tor przesunięci a zostałby potraktowany przez kompilator jako operator strumieniowy i w związku z tym otrzymany wynik różn iłby s ię od spodziewanego - otrzymalibyśmy warto ść zmiennej nurnber z c yfr ą dwa. Na og ół operacja prze sun ięcia w prawo przypomina o p e rację prz e sunięci a w lewo. Przypu śćmy na przykład, że zmienna nurnber ma w arto ść 24 i wykonujemy następującą in strukcj ę :
number » = 2; W wyniku tego działani a zmienna number - dzięki podzi eleniu oryginalnej w arto ś c i przez cztery - będzie miała w arto ś ć 6. Nal eży jedn ak pami ętać , że operator prze sun i ę cia w prawo działa w szczególny sposób z ujemnymi typami całkowitymi (w których bit przechowujący
116
Visual C++ 2005. Od podstaw znak będący pierwszym bitem z lewej ma warto ś ć l). W takim przypadku bit znakowy jest przenoszony w prawo. Zdefiniujmy i zainicjalizujmy zmienną number typu cha r wartością dziesiętną -104: char number = -104:
II W zap isie binarnym liczba
l a lO
10011000.
Teraz przesuwamy ją o dwa bity w prawo: number » = 2: II Rezultat 11100110.
Wynik w zapisie dziesiętnym wynosi -26, ponieważ bit znakowy jest powtarzany . Oczywiście w przypadku operacji z typami całkowitymi bez znaku bit znakowy nie jest powtarzany i wsta wiane są zera.
Czas życia i zasięg zmiennych Wszystkie zmienne mają podczas działania programu ograniczony czas życia. Są one powoły wane do życia w momencie deklaracji, a następnie znikają w pewnym momencie - najpóźniej w chwili zakończenia programu. To, jak długo dana zmienna jest dostępna, jest określone przez właściwość zwaną czasem życia zmiennej . Istnieją trzy różne rodzaje czasu życia zmiennej : •
automatyczny,
•
statyczny,
•
dynamiczny.
Od którego z nich będzie zależeć czas życia zmiennej, wynika ze sposobu , w jaki została utwo rzona. Opis zmiennych z dynamicznym czasem życia odłożymy do rozdziału 4., a o pozosta łych dwóch rodzajach będziemy mówili w tym rozdziale. Inną właściwością zmiennych jest zasięg, oznaczający tę część programu, w której rozpozna wanajest dana nazwa zmiennej. W ramach zasięgu zmiennej można się do niej odwoływać w celu ustawienia jej wartości lub użycia w wyrażeniu . Poza jej zasięgiem nie można się do niej odwoływać - wszelkie próby takich odwołań zakończą się zgłoszeniem błędu przez kompi lator. Należy zauważyć , że zmienna może istnieć także poza swoim obszarem zasięgu, mimo że nie można się do niej odwoływać za pomocąjej nazwy. Przykłady takich sytuacji zobaczy my za chwilę.
Wszystkie zmienne, które deklarowaliśmy do tej pory, miały automatyczny czas co nazywane są zmiennymi automatycznymi. Przyjrzyjmy się im bliżej .
życia,
przez
Zmienne automatyczne Zmienne dekl arowane przez nas do tej pory zawsze znajdowały s i ę wewnątrz bloku - to znaczy pomiędzy dwoma nawiasami klamrowymi . Są one nazywane zmiennymi auto matycznymi i mają tak zwany zasięg lokalny lub blokowy. Zasięg zmiennej automatycznej rozpoczyna się w momencie jej deklaracji i kończy w miejscu, gdzie kończy się blok zawie
Rozllzial2. - Dane, zmienne i dZiałania arytmetyczne
117
rając y t ę de klarację . P r zest rzeń zajmowana prze z z m ien n ą automatyczną jest przyd zielana automatycznie w specjalnie do tego celu przeznaczonym obszarze pam ię ci, zwanym stosem. Domyśln i e stos ma rozmiar l MB, co w z upełnośc i wystarcza do większo ś ci z astosowań , ale jeżeli wartość ta okaże s ię za mała, to m ożna ją zwi ęks zyć do żąd anej wi elkoś ci za p omoc ą opcji proj ektu /STACK.
Zmienna automatyczna powstaje w momencie jej zdefiniowania. W tej samej chwili przy dzielane jest dla niej miejsce na stosie oraz automatycznie przestaje i stnieć w miejscu , w któ rym znajduje s i ę nawias klamrowy zamykający zawi e raj ący j ą blok. Za każdym razem, gdy wykonyw any jest blok instrukcji zawier aj ący deklar acje automatycznej zmiennej, zmienna ta tworzona je st od nowa. Jeżeli okre śliliśmy dla niej wartoś ć p oc zątkową, to za k ażdym razem , gdy je st tworzona, będz ie ponownie inicjalizowana. Kiedy zmienna automatyczna przestaje i stni eć , to pam ięć zaj mowana przez nią na stosie zostaje zwolniona do użytku przez inną zmi enną automaty c zn ą.
Istnieje s łowo kluczowe auto , za pomocą którego mo żna określa ć zmienne automatyczne, ale jest ono rzadko używane , gd yż zmienne s ą domyślnie automatyczne . P oni żej podaję przykład dotyczący tego, co przed ch wi l ą powied ziałem na temat za s ięgu zmiennych. II Cw2_0 7.cpp
II Prezentacj a zas ięgu zmiennych.
#include
usi ng st d: :cout :
using st d:: endl;
int main()
II Zasięg zmiennej rozpoczyna
{
się
tutaj.
l nt count l = 10;
i nt count3 = 50 ;
cout « endl
« «
" Wart o ś ć zewn ęt rz nej
zmiennej countl
"
~
«
countl
endl ;
II No wy zas ięg rozp oczy na s ię tutaj ...
i nt countl = 20 ; II To chowa zewnętrzną zmienną count /.
i nt count 2 ~ 30 ;
cout « "Warto ś ć wewnę tr z ne j zmiennej countl = .. « countl
« endl ;
countl +~ 3; II To działa na wewn ętrzną zm ienną count/
count 3 +~ count 2;
}
II ...i
cout « « « «
"Wa rto ś ć z e wnęt r z n ej
ko ń czy się
tutaj . «
zmiennej count l = "
endl
"W art oś ć zewnet rznej zmiennej count 3
~
count l
« count 3
endl :
II cout « count 2 « endl ;
II
Us unięcie
tego kom entarza spowoduj e
return O;
Wynik
dział ania
tego programu przed stawia
Wa rt o ś ć z ewnę t r z ne j
Wart o ś ć wewnętr zn ej Wa r t o ś ć z ew n ę t r z n ej
War t o ś ć zewn ę t r z n ej
zmiennej zmiennej zmi ennej zmiennej
count l ~ 10
count l = 20
countl = 10
count3 = 80
się następuj ąco :
b łąd.
118
Visual C++ 2005. Od podstaw
Jak lo działa Dwie pierwsze instrukcje deklaruj ą i d efin iuj ą dwie zmienne typu całkowi tego count l i count3 o warto ś ciach poc zątk owych odpowiednio 10 i 50. Cykl życ ia obu tych zmiennych rozpoczyna się w tym momen cie i końc zy s ię wraz z zam ykaj ącym nawiasem klamrowym na końcu pro gramu. Z asięg tych zmiennych rozci ąga s i ę również do nawiasu zamykającego funkcję ma i n( l. Należy pamiętać, że długość życia
zmiennej i j ej zas ięg to dwa różn e pojęcia. Ważne jest, aby ich nie mylić. Dlugość życia to okres podczas działania programu, rozpoczynający się w momencie utworzenia zmiennej, a koń czący w momencie jej zniszczenia i zwo lnienia zaj mowanej przez nią pamię ci dla innych celów. Zas ięg zmiennej to obszar w kodz ie programu, w którym zmienna j est dostępna . Zgodnie z definicjami zmiennych wartoś ć zmiennej count l je st wysyłana na wyj ście, tw orząc dwa pierwsze wiersze widoczne powyżej . Następnie znajduje s i ę otwarcie drug iego nawiasu klamrowego, który rozpoczyna nowy blok . Wewnątrz nieg o zdefiniow ane są dwie zmienne countl i cou ntż o warto ś ci ach odpowiednio 20 i 30. Zmienn a countl zadeklarowana tutaj ma inną wartość niż pierwsza zmienna count .l, która nadal istniej e, ale jest przesłon i ęta przez tę drugą. Odwołując s ię do zmiennej countl po jej deklaracji w wewn ętrznym bloku, otrzymamy w artość tej zmienn ej zadeklarowanej w tym bloku . Nazwę zmiennej zdupliko wałem tylko po to, aby pokazać, co s ię wydarzy. Mimo że kod ten j est prawidlowy, to nie należy się na nim wzorować. W prawdziwym programie takie coś byłoby bardzo mylące. Takie duplikowani e nazw zmiennych m oż e częs to pro wadz ić do przyp adkowego ukrycia zmiennych zdefiniowa nych w blokach zewnętrznych. Wartoś ć
pokazana w drugim wierszu na wyjści u pokazuje, że w bloku wewnętrznym używamy zmiennej countl w zasięgu lokalnym, to znaczy w obrębie najbardziej wewnętrznych nawiasów klamrowych :
cout «
"War tość wewn ę tr znej
Gdybyśmy
nadal
=
"
«
count l
używal i zewnętrznej
N astępni e wartość
countl
zmiennej countl
endl ;
«
+=
zmiennej count l , to zostałaby wyświetlona w artość la . zmi ennej countl zostaje zwiększona za pomoc ą na stępującej instruk cji:
3;
II To dz ia ła na
wewnętrzną zm ienną
countl.
Zwi ększen ie dotyczy zmiennej o zasięgu wewnętrznym , jako że zewnętrz na wciąż jest scho wana. Natomiast zmienn a count 3, która została zdefiniowana w z a s i ęgu zewnętrznym , została w n astępnej instrukcji bez probl emu zwięk s z on a :
count3
+~
count2:
Dowo dzi to tego, że zmienn e zadeklarowane na początku zas ięgu zewn ętrznego dostępne są równi e ż w zas i ęgu wewn ętrznym (należy zauważyć , że gdyby zmienna COUIlt3 została zade klarowana po klamrze zamykaj ącej blok wewn ętrzny , to i tak byłaby w ob ręb i e zasi ęgu bloku zewnętrzn ego, ale nie istniałaby jeszcze w momencie wykonywania p owy żs zej instrukcji).
Rozdział2.
W momencie
countl
• Dane. zmienne i działania arytmetyczne
119
klamry kończącej blok wewnętrzny, zmienne cou nt2 i wewnętrzna Zmienne count l i count3 nadal istnieją w bloku zewnętrznym, a wy pokazują, że zmienna count3 rzeczywiście została zwiększona w bloku
wystąpienia
przestają istnieć.
świetlone wartości wewnętrznym.
Usunięcie
l i cout
komentarza z wiersza : «
count2
«
endl;
II Us unięc ie tego komentarz a spowoduj e błąd
spowoduje, że program nie będzie mógł zostać poprawnie skompilowany, gdyż znajduje się w nim odwołanie do nie istniejącej zmiennej. Otrzymamy komunikat o błędzie podobny do poniższego :
c:\ microsoft visua l studio\myprojects\Cw2_07\Cw2_07,cpp(29) : error C2065 : 'count2 ' : undeclared identifier Dzieje
się
tak,
ponieważ
zmienna cou nt2 znajduje
się już
poza
zasięgiem.
Pozycjonowanie deklaracii zmiennych Jeśli
chodzi o wybór miejsca do wstawiania deklaracji zmiennych, to mamy dużą swobodę. czynnikiem wpływającym na naszą decyzję powinien być zasięg, jaki ma mieć dana zmienna. Poza tym zmienne powinniśmy zawsze deklarować w pobliżu pierwszego ich użycia. Pisząc program, zawsze trzeba pamiętać o robieniu tego w taki sposób, aby był on jak najbardziej zrozumiały także dla innych, a deklarowanie zmiennych w pobliżu ich pierw szego użycia bardzo w tym pomaga. Najważniejszym
Istnieje możliwość deklarowania zmiennych poza wszelkimi funkcjami w programie. omówimy, jakie są skutki takiego podejścia.
Poniżej
Zmienne globalne Zmienne, które nie są zadeklarowane wewnątrz żadnych bloków ani klas (o klasach będziemy mówić później) , nazywane są zmiennymi globalnymi i mają zasięg globalny (czasami także zwany zasięgiem globalnym przestrzeni nazw lub zasięgiem plikowym). Oznacza to, że są one dostępne w każdym miejscu programu od miejsca, w którym zostały zadeklarowane. Jeśli zadeklarujemy je na samej górze, to będą one dostępne wszędzie. Zmienne globalne mają także domyślnie statyczny czas życia . Zmienne globalne o statycz nym czasie życia istnieją od początku działania programu do jego końca . Jeśli nie określimy wartości początkowej, to zmienna globalna domyślnie przyjmie wartość zerową. Inicjalizacja zmiennych globalnych ma miejsce przed rozpoczęciem wykonywania funkcji ma i nO, dzięki czemu są one zawsze gotowe do użycia w kodzie, który znajduje się w ich zasięgu . Na rysunku 2.12 pokazano każdej ze zmiennych .
zawartość
pliku źródłowego Example.cpp.
Strzałki pokazują zasięg
120
VisIlai C++ 2005. Od podstaw
,
long wartośćl ; int mainO (
int wartość 2; ,
warto ść l
.. (
int warto ść 3;
I
...
wart ść3
1o
}
waTść2
}
int wartość 4; int funct ion(int n) {
i
warto ś ć4
long wa rto ś ć s;
I '
int warto ść 1;
I
".
w artość 5
wal1rś:j
}
i
Rvsunek 2.12 Zmi enna w ar to ś ć l, która znajduj e s ię na początku pliku , ma zasięg globalny, podobnie jak zmienna wa rto ś ć 4, której deklaracj a znajduje się po funkcji ma i n( ). Za s i ęg każdej zmiennej globalnej rozciąga s ię od miejsca jej deklaracji do koń ca pliku . Mimo że zmienna wa rto ś ć4 istnieje w momencie rozpoczęc i a wykonywania programu, to nie możn a się do niej o dwoły w a ć w funkcj i ma i n( ), gdyż nie znajduje s ię ona w zas i ęgu tej zmiennej. Aby mi eć dostęp do tej zmiennej z funkcji ma t nt ), musi el ibyśm y jej dekl ara cję przeni e ś ć na początek pliku. Za równo zmienna wa rt o ś ć l, jak i wa r to ść 4 z ostaną zainicjalizowane wartości ą domyśln ą, czyli zerem, co nie dotyczy zmiennych automatycznych. Zau ważmy, że lokalna zmienna o nazwie war to ś ć l w funkcji f unct ion( ) przesłani a zmienną globalną o tej samej nazwie. Jako że zmienne istni ej ą do samego końc a działania programu, nasuwa się pytani e: "Czy nie m ożn a w takim razie wszystkich zmiennych uczyni ć globalnymi i nie prz ejmować się znika niem zmiennych lokalnych?". Na pierwszy rzut oka wydaje się to bard zo ku szące , ale podobnie jak w przypadku mitologicznych syren - skutki uboczne znacznie przewy ższają ewentu alne korzyści . Prawdziwe programy zazwyczaj s kładają si ę z dużej liczby instrukcji, funkcj i i zmiennych. Zadeklarowanie wszystkich zmiennych ja ko globalny ch znacznie zw ię kszyłoby ryzyko przy padkowego, błędnego zmodyfikowania wartości zmiennej, a także zdecydowanie utrudniłoby odnalezienie odpowiednich nazw dla wszystkich zmiennych. Ponadto każda zmienna zajmuje
Rozdział 2.
• Dane, zmienne i działania ar»tmet»czne
121
pewn ą i lość pami ęci
przez cały czas trwania programu . Dzięki deklarowaniu zmiennych lokal nie, wewn ątrz funkcji lub bloków, zapewniamy im praw ie p ełn ą oc hro nę przed działaniem czynników ze w nętrznych - będą one is tniały i zajmowały pamięć tylko od miejsca ich dekla racji do koń ca zawi erając ego je bloku, d zi ęki czemu o wiele łatwi ej zarządzać całym procesem tworz enia pro gramu . J e śli spojrzymy na panel Class View po prawej stronie okna IDE, m ając otwarty który ś z utwo rzonych do tej pory przykładów , i rozwiniemy drzewo klas projektu , klikaj ąc znak +, to zoba czymy opcję Global Functions and Variables. Klikając ją, spowodujemy pokazanie wszystkie go, co w programie ma zasięg globalny. Znajdą s i ę tam funkcje globalne, jak również wszystkie zadeklarowane przez nas zmienne globalne.
aa:m Operator widoczności Jak już widzieli śmy , zmienna globalna może zostać przesłonięta przez zmien n ą lokalną o tej samej nazwie. Mimo to do zmienn ej globaln ej można nadal u zysk ać dostęp za pomoc ą ope ratora widoczności ( : .), któr y widz i eli śmy już w rozd ziale l. podc zas op isu przestrzeni nazw . Sposób jego działania zademon struj ę za pomocą zmodyfikowanej wersji poprzedniego przykładu :
II Cw2_08.cpp
II Prezentacja zas ięg u zmiennyc h.
#i ncl ude usi ng Std: :cout : using st d: .end l : i nt count l
=
100 :
II Wersja globalna zmiennej count l .
i nt mai n()
II Tutaj rozpo czyn a się zasię g fu nkcji .
{
i nt countl ~ 10: i nt count3 = 50: cout « endl cout
" W ar t o ś ć z ew nę t r z n eJ
« «
endl :
« «
endl :
"W ar to ść
zmiennej countl
global nej zmiennej countl II Nowy
=
zasięg
~
"
"
«
«
countl : :countl
II Z b lo ku z ewn ę t rz n ego .
rozpoczyna s ię tutaj. ..
II Ta instrukcja przesłania zewn ętrzn ą zmienn ą count l . int count l = 20: i nt count2 ~ 30: cout « " Wa r t o ść wewnęt r zne j zmi ennej countl = " « countl « endl: cout « " W arto ść global nej zmi ennej countl = " « : :count l II Z wewnętrzn ego II bloku. «
countl count3
endl : += +=
3; count 2;
II To dzia ła na II ...i
cout
«
"W ar t o ś ć z ewnę t rznej
wewnę trzną zmienną
koń czy s ię
zm iennej countl
=
tutaj. "
«
countl
count l
122
Visual C++ 2005. Od podstaw endl
« « «
/ / cout
"Wartość zewnęt rz ne j
zmiennej count3
= "
«
count 3
end l ; count2 « endl:
«
II Usun ięcie komentarza z tego wiersza sp owoduje bląd.
ret urn O: Po skompilowaniu i uruchomieniu tego programu otrzymamy
następujący wynik:
zmi enne j count l = 10
global nej zmiennej count l = 100
wewn ęt r zne j zmi ennej count l = 20
globa lnej zmiennej count l = 100
z ewnętrz nej zm iennej count l ~ 10
z ewn ę t rz n e j zmiennej count 3 ~ 80
W a rt o ś ć z ewn ę t r z nej
Wa rtość Wa r t o ś ć W ar t o ś ć W art o ś ć
War t o ść
Jak to działa Wiersze kodu na szarym tle oznaczają zmiany w kodzie w stosunku do pierwszej jego wersji. tylko tych właśnie fragmentów. Deklaracja zmiennej countl znajdująca się przed definicją funkcji mai n() jest globalna, a wię c dostępna w każdym miejscu funkcji mai nt ). Zmienna tajest inicjalizowana wartością 100: Zajmę się objaśnieniem
int count l
=
100;
II Wersj a globalna zmiennej countl .
Ale poza powyższą w programie mamy jeszcze dwie inne zmienne o nazwie count l , zdefi niowane wewnątrz funkcji ma i n( ). A zatem w programie globalna zmienna countl jest prze słonięta przez zmienne lokalne o tej samej nazwie . Pierwsza instrukcja wyjśc iowa to:
cout
" W a rto ść
«
«
global nej zmiennej countl = "
«
; :countl
II Z bloku ze wnę trznego.
end l :
Użyty w niej został operator zasięgu , aby kompilator wiedział, że chcemy odnieść się do zmien nej globalnej o nazwie count l, a nie do jej lokalnej wersji . Z wartości wysłanych na wyjście wynika , że to działa .
W bloku
zmienna countl jest przesłonięta przez dwie zmienne countl: we countl i zewnętrzną zmienną count l. Operator zasięgu wykonuje swoje
wewnętrznym
wnętrzną z m i e n ną
zadanie w bloku
wewnętrznym,
co
widać
w danych wygenerowanych przez
wstawioną
tam
instrukcję:
cout
« «
" War tość
global nej zmiennej countl
~
..
«
: .count l
II Z wewnę trznego bloku.
endl :
Powoduje to wysłanie na wyjście wartości 100; podobnie jak poprzednio operator zasięgu w ten sposób zawsze dotrze do zmiennej globalnej . Wcześniej mówiliśmy, że
użyty
do nazw należących do przestrzeni nazw std można odnosić się, nich kwalifikator tej przestrzeni, na przykład: st d : : cout lub st d: : end7. Kom pilator przeszukuje przestrzeń nazw o nazwie podan ej po lewej stronie operatora za s i ęgu w celu znalezienia nazwy podanej po jego prawej stronie. W poprzednim przy kładzie użyliśmy operatora zasięgu do przeszukania globalnej p rzestrzeni nazw w celu dodając do
Rozllział 2.
• Dane. zmienne i działania arytmetyczne
123
znalezienia zmiennej count l . Jeśli nie podamy nazwy przestrzeni nazw po lewej stro nie operatora zasięgu, to oznacza to, że kompilator ma przeszukać przestrzeń globalną w celu znalezienia nazwy, która znajduje się po jego prawej stronie . Operatora tego będziemy używać znacznie częściej, kiedy w rozdziale 9. dojdziemy do pro gramowania zorientowanego obiektowo, gdzie jest on bardzo często używany .
Zmienne statyczne Niewykluczone, że możemy potrzebować zmiennej dostępnej lokalnie, istniejącej jednak także po wyjściu z bloku, w którym została zadeklarowana. Inaczej mówiąc, możemy potrzebować zmiennej o zasięgu lokalnym, ale o statycznym czasie życia. Zmienną taką można uzyskać za pomocą określnika st at ic , a potrzeba jej użycia stanie się dla nas bardziej oczywista, gdy doj dziemy do omawiania funkcji w rozdziale 5. zmienna statyczna będzie istnieć aż do zakończenia programu, mimo że zadeklarowana wewnątrz bloku i dostępna jest tylko w jego obrębie (oraz w blokach w nim zagnieżdżonych). Nadal ma ona z as i ę g lokalny, ale ma także statyczny czas życia . W celu zadeklarowania statycznej zmiennej typu całkowitego o nazwie count możemy użyć następującej instrukcji :
W
rzeczywistości
została
st at ic int count: Je żeli
podczas deklaracji zmiennej statycznej nie podamy wartości początkowej, to zostanie ona zainicjalizowana automatycznie. Zmienna count zadeklarowana przez nas powyżej zosta nie zainicjalizowana wartością zero. Domyślna wartość początkowa zmiennych statycznych to zawsze zero przekonwertowane na odpowiedni typ. Należy pamiętać, że nie dotyczy to zmiennych automatycznych. Jeśli
nie zainicjalizujemy zmiennej automatycznej, to będzie ona przechowywała niepo trzebne wartości pozostałe po programi e, który jako ostatni korzystał z zajmowanego prz ez nią obszaru pamięci. I
Przestrzenie nazw Do tej pory już kilkakrotnie wspominałem o przestrzeniach nazw, a więc nadszedł czas, aby przyjrzeć się im dokładniej . Nie są one używane w bibliotekach obsługujących klasy MFC . Natomiast biblioteki obsługujące CLR i Windows Forms korzystają z nich bardzo często. Oczywiście biblioteka standardowa C++ ANSI także . Wiemy już, że wszystkie nazwy używane w bibliotece standardowej C++ ISO/ANSI zostały zdefiniowane w przestrzeni nazw st d. Oznacza to, że wszystkie nazwy używane w bibliotece standardowej mają dodatkowy kwalifikator st d. A zatem cout na przykład w rzeczywistości ma postać std : :cout. W poniższym prostym przykładzie możemy obejrzeć wykorzystanie pełnych nazw :
124
VisualC++ 2005. Od podstaw II Cw2_09 .cpp
II Preze ntacja prz estrzeni nazw.
#i nclude i nt val ue
=
O;
int mai n() {
st d: ;cout « " wp r owa d ź wa r toś ć ca ł kowi t ą ; ";
st d; ;Cl n » val ue ;
st d: :cout « "\ nWprowadzona wart ość to " « val ue
« st d . : endl :
retur n O;
Deklaracja zmiennej val ue znajduj e się poza d efinicją funkcji ma i n( ). Deklaracja ta znajduje w zasięgu globaln ej przestrzeni nazw, pon ieważ deklaracja ta nie znajduje s i ę w obrę bie żadnej przestrzeni nazw . Zmienna ta d o stępna j est z każdego miejsca w funkcj i ma in (), jak równi e ż z definicji innych funkcji, które m ogą znajdować s ię w tym samym pliku źró dłowym . Deklarację zmiennej va l ue umie ściłem poza funk cją mai n() tylko po to, aby zade monstrować , jak mogłoby to wyglądać w przyp adku przestrzeni nazw . s i ę więc
na brak dekl aracji usin g dla eout oraz endl . N ie jest ona tutaj potrzebna, w tym przypadku podajemy pełne nazwy z kwalifikatorem przestrzeni nazw st d. Mimo że nie byłoby to dobrym pomysłem , to mogliby śmy w tym miejscu użyć słowa eout jako na zwy zmiennej c ałkowitej i nie sp ow od ow ał o b y to żadneg o konfliktu , ponieważ samo s łowo eout to nie to samo co st d: :eout . A zatem przestrzenie nazw s łużą do oddziel ania nazw uży wanych w jednej c zęści programu od naz w u żyw an ych w inn ej czę ści. Są one nie oc en ioną pom ocą prz y pracy nad du żym i projektami, w które zaangaż ow anych jest kilk a ze sp oł ów programistów. Każdy zespół może mieć własną przestrzeń nazw, d zi ęki czemu nie trzeba się obawiać , że dwa ze sp oły użyją tej samej nazwy dla różnych funkcji . Zwróćmy uw agę gdyż
Spój rzmy na p on i ższy wiersz kodu : using namespace st d:
Instrukcja ta je st
dyrektywą
using,
Efektem jej działania jest import wszystkich nazw z przestrzeni nazw st d do pliku źródło wego, dzięki czemu będziemy mogli od w oływać się do wszystkiego, co jest w niej zdefiniowa ne, bez potrzeby używania kwalifikatora. W ten sposób, zamiast pisać st d: :eout lub st d: :endl , mo żemy zapisywa ć krótko eout i endl . Wadą tak iego wykorzystania dyrektywy usi ng jest to, że tracim y n ajważniejsze korzyści płynące ze stosowania przestrzeni nazw - m ożliwość uniknięcia konflikt ów nazw. Najbe zpieczniejszym sposobem uzyskania dostępu do nazw nale żących do przestrzeni nazw j est jawne dodanie kwalifikatora do każdej użytej nazwy - podej ście to niestety znacznie zwi ększa obj ętość kodu i zmniejs za jego czytelnoś ć. Inn ą możliwo śc i ą j e st wpro wadzenie tylko tych nazw , których będziemy używ ali w programie, za pomoc ą deklaracji us i ng. Na przykł ad : usi ng std.: cout: II Pozwala na używa nie słowa cout bez kwalifikatora.
usi ng std: endl : II Porwala na używan ie s łowa endl bez kwalifikatora .
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
125
Instrukcje takie nazywane są deklaracjami using. Każda instrukcja wprowadza jedną nazwę z określonej przestrzeni nazw i pozwala na używanie jej bez kwalifikatora w kodzi e programu, który znajduje s i ę po niej. Jest to o wiele lepszy sposób importowania nazw z prze strzeni nazw , ponieważ importujemy tylko te nazwy, których rzeczywiście używamy. Ze względu na fakt, ż e firma Microsoft wprowadziła zwyczaj importowania wszystkich nazw z przestrzeni nazw Syst emw kodzie w CH /CLI , będę się tego trzymał w przykładach w tej wersji języka . Ogólnie rzecz biorąc , zalecam stosowanie deklaracj i zamiast dyrektyw usi ng podczas pisania większych programów. Oczywiście możemy także definiować własne prze strzenie części będz iemy się właśnie
tym
nazw o wybranej nazwie. W dalszej
zajmować.
Deklarowanie przestrzeni nazw Przestrzeń
nazw deklaruje
się
za
pomocą słowa
kluczowego namespace:
namespace mySt uff { II Kod, który chcemy
zawrz eć
w przestrzeni nazw myStuff. ..
} Powyższy
kod definiuje przestrzeń nazw o nazwie myStuff. Wszystkie deklaracje nazw, które nawiasami klamrowymi , zostaną zdefiniowane w przestrzeni nazw my St uff. W związku z tym w celu odwołania się do której ś z nich poza tą przestrzenią należy użyć kwalifikatora przestrzeni nazw myStuff lub skorzystać z deklaracji usi ng.
znajdą się pomiędzy
Przestrzeni nazw nie można zadeklarow ać wewnątrz funkcji . Ich przeznaczenie jest odwrot ne - to w przestrzeniach mogą znajdować się funkcje, zmienne globalne i inne nazwane encje, takie jak klasy . Należy jednak p amiętać, że defini cja funkcji mai n( ) nie może znajdo wać się wewnątrz definicji przestrzeni nazw . Funkcja na i nt ), od której rozpoczyna s i ę program, musi być zawsze w zasięgu globalnym - inaczej nie zostanie rozpoznana przez kompilator. Zm i e n n ą
val ue z poprzedniego przykładu możemy umieścić w przestrzeni nazw :
II Cw2_10.cpp II Deklarowanie przestrzeni nazw.
#include namespace myStuff {
int value
=
O:
int mai nO {
std : :cout « "Podaj licz b ę cał kowi t ą : ". std: :ci n » mySt uff : :val ue : std : .cout « "\nWprowadzona li czba t o " « mySt uff : .value « std : : end l: ret urn O:
126
Visual C++ 2005. Od podstaw Prz e strzeń nazw myStuff definiuje zasięg i wszystko , co się w nim znajduje, jest zakwalifiko wane do tej przestrzeni. Aby odnieść się do nazwy zadeklarowanej wewnątrz tej przestrzeni nazw, nale ży do nazwy w odwołaniu dołączyć nazwę przestrzeni nazw . Wewnątrz przestrzeni nazw do wszystkich nazw w niej zadeklarowanych można odnosić się bez kwalifikatora wszystkie one należą do tej samej rod ziny. Teraz musimy zakwalifikować nazwę va l ue do przestrzeni nazw myStuff. Jeśli tego nie zrobimy, programu nie będzie można skompilować . Funkcja mainO odnosi się teraz do nazw w dwóch różnych przestrzeniach nazw i, ogólnie rzecz biorąc , można zdefiniować dowolną liczbę przestrzeni nazw w programie . Aby unik nąć konieczności kwalifikowania nazwy val ue przy każdym jej użyciu, możemy skorzystać z dyrektywy using:
II Cw2_ 11.cpp
II Używan ie dyrek tywy using.
#inelude namespaee myStuff
{
int val ue
~
O:
}
usi ng namespaee myStuf f :
II
Udos tępn ij
wszystkie nazwy z przes trzeni naz w myStujf.
int ma in() {
std : :eout « "Poda j l iczbe eałkowit ą :
st d: :ein »val ue :
st d: :eout « "\ nWprowadzona l iczba to " « st d: : endl :
ret urn O:
«
va l ue
Oczywiście, możemy również zastosować dyrektywę usi ng dla przestrzeni nazw st d, aby śmy nie musieli posługiwać się kwalifikatorem dla nazw w bibliotece standardowej, ale zaprze czyłoby to całej filozofii używania przestrzeni nazw. Ogólnie, kiedy używamy przestrzeni nazw, nie powinniśmy dodawać dyrektyw usi ng w całym programie , w przeciwnym przypadku moglibyśmy wcale sobie nie zawracać głowy przestrzeniami. Mimo to dodamy dyrektywę usi ng dla przestrzeni nazw st d w naszych przykładach w celu uproszczenia kodu. Kiedy roz poczyna się naukę nowego języka programowania, można sobie poradzić bez zbędn yc do datków, bez względu na to, jak bardzo są przydatne.
Wielokrotne deklaracje przestrzeni nazw W rozbudowanych programach często można spotkać wiele deklaracji jednej przestrzeni nazw . Można mieć wiele deklaracji prze strzeni nazw o danej nazwie, w których zawartość każdego z bloków należy do tej samej przestrzeni. Możemy na przykład mieć plik programu z dwiema przestrzeniami nazw:
namespaee sort Stuf f { II Wszystko tutaj
}
należy
do przestrzeni nazw sortStujf.
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
127
namespace calculat eStuff
{
II Wszystko tutaj na leży do prz estrzeni nazw calculateS tuf{.
II Aby odnieś ć s ię do nazwy z prz estrzeni sortStujJ. na leży użyć kwalifikatora.
namespace sortStuf f
{
II To jest kontynuacja p rzestrzeni nazw sorzStuff;
II a więc m ożna s tąd odnosić się do nazw z p ierwszej p rzestrzeni sor tStujf
II bez używan ia kwa lifikatora.
Druga deklaracja przestrzeni nazwo danej nazwie stanowi tylko jej k ontynu ację. Dzięki temu w jej o bręb ie możemy odnosić się do nazw z poprzedniego bloku bez potrzeby używani a kwalifikatora. Wszystkie one nale ż ą do tej samej przestrzeni nazw. Oczywi ś cie nikt celowo nie organizuje w ten sposób zawartości swoich plików źródłowych , ale sytuacja taka może po wsta ć w sposób naturalny, gdy dodamy do programu pliki nagłówkowe . Możemy na przykład w programie m i eć coś takiego: #i ncl ude II Zawar tość n ależy do przes trzeni nazw std.
II I tak dalej...
W powyższym kodzie i stri ng to pliki nagłówkowe należące do biblioteki stan dardowej C++ ISO/ANSI, a nagł ówek myheader .h zawiera nasz własny kod. Mamy tutaj do kładnie taką samą sytuację jak w poprzednim przykładzie . Mamy ju ż ogólne rozeznanie w sposobie d zi ałania przestrzen i nazw. M ożna by o nich opo j eszcze dużo więcej , ale je śli zrozumiemy wszystko , co jest zawarte tutaj, to w razie potrzeby będziemy potrafili znaleźć dodatkowe informacje.
w iedzi e ć
Zauważ, że dwie fo rmy dyrektywy #inc 7ude w poprzednim fragmencie kodu zm usiły kom pilator do szukania pliku na dwa różn e sp osoby. Jeśli plik, który ma zos tać dołą czony, zostanie podany w nawiasach trójkątnych, to komp ilator b ędz ie go szukał w ścieżce okre ś lonej w opcji kompilatora / l , ajeśli go nie znaj dzie, to w scizżce określonej przez zmienną ś rodo wiskową lNCLUDE. W tych lokalizacjach znajdują s ię pliki iblioteczne C++ i dlatego form a ta jest zareze rwowana dla n agłó wków biblioteczny ch. Zmi enna śro dowis ko wa lNCLUDE wskazujefolder, w którym znajduje s ię nagłówek biblioteczny, a opcj a / l pozwala na okreś lenie dodatkowej lokalizacj i zawie rającej nagłó wki biblioteczne. Jeżeli nazwa pliku znaj duje się w cudzys łowach, to kompilator przeszuka fo lder zawierający plik, w któ rym znaj duje się dyr ektywa #inc 7ude. Jeżeli plik nie zostan ie odnaleziony, to kompila tor będzie go szukał w innych fo lderach zawierających b ieżący plik. Jeśli nadal go nie znaj dzie, to poszuk iwania są kontynu owane w katalogach bibliotecznych.
128
VisualC++ 2005. Oli podstaw
Programowanie wC++/CLI
Język C++/CLI udostępnia szereg usprawnień i rozszerzeń omawianych do tej pory zagadnień . Zanim przejdziemy do szczegółów , zrobimy krótkie podsumowanie tych nowych właściwości :
• Wszystkie fundamentalne typy danych C++ ISO/ANSI mogą być używane w C++/CLI w taki sam sposób, jak to zostało przedstawione, ale w pewnych sytuacjach mają one pewne dodatkowe cechy , o których będziemy mówić. • C++/CLI posiada własny mechanizm przyjmowania danych z klawiatury i wysyłania ich do wiersza poleceń w programie konsolowym. • C++/CLI wprowadza operator safe_cast, który sprawia, kod wygenerowany w wyniku operacji rzutowania.
że można zweryfikować
• C++/CLI dostarc za alternatywne narzędzie wyliczania oparte na klasach, które oferuje większą elastyczno ść niż deklaracja enum w C++ ISO/ANSI. O klasowych typach referencyjnych CLR będziemy się uczyć dopiero w rozdziale 4., ale jako że wprowadziłem już zmienne globalne w natywnym C++, to wspomnę, że zmienne referen cyjnych typów klas CLR nie mogą być zmiennymi globalnymi . Rozpoczniemy od fundamentalnych typów danych w C++/CLI.
Fundamentalne typy danych wC++ICLI Możemy
i powinniśmy używać nazw fundamentalnych typów danych z C++ ISO/ANSI w programach w C++/CLI. W operacjach arytmetycznych zachowują się one identycznie jak typy w natywnym C++. Dodatkowo C++/CLI definiuje jeszcze dwa typy całkowite :
Typ
Bajly
Zakres wartości
l ong long
8
Od 9 223 372 036 854 775 808 do 9 223 vn 036 854775 807
unsi gned long long
8
Od Odo 18446744073709551615
Aby określić literał typu l ong l ong, do wartości całkowitej musimy dołączyć dwie małe litery 11 lub wielkie litery LL . Na przykład: long long big = 123456789LL ; Aby
otrzymać literał
typu long lo ng bez znaku, do wartości
całkowitej
dodajemy ULL lub u11:
~ unsi gned long long huge ~ 999999999999999ULL: Mimo że wszy stkie operacje na typach fundamentalnych, które widzieliśmy, działają w po dobny sposób także w C++/CLI, to nazwy typów fundamentalnych w programach C++/CLI mają inne znaczenie i w pewnych sytuacjach mają dodatkowe możliwości. Typ fundamen talny w programie w C++/CLI jest typem klasy wartości i może zachowywać się jak zwykła wartość lub jak obiekt, jeśli sytuacja tego wymaga.
Rozdział2.
- Dane, zmienne i działania arytmetyczne
129
W języku C++/CLI każdy typ fundamentalny C++ ISO/ANSI mapuje si ę na typ klasy war tości, który jest zdefiniowany w przestrzeni nazw Syste m. A zatem w programie w C++/CLI nazwy typów fundamentalnych ISO/AN SI s ą skrótami do skoj arzon ych z nimi typów klas wart oś ci . Dzięki temu wart ości typów fundament alnych mogą być traktowane po prostu jako wartośc i lub też , kiedy j est to konieczne, m ogą być automatyczni e konwert owane na obiekt o skojarzonym z nim typie klasy wartości. Typy fundamentalne, i lość zajm owan ej przez nie pamię ci oraz odpowiadaj ące im typy klas w artoś c i pokazano w p oniższej tabeli :
Typ lundamentalny
Rozmiar wbailach
Klasa wartości CLI
boo l
System: :Bool ean
char
Syst em : :Sbyt e
si gned char
Syste m: :Sbyte
unsigned char
System: :Byte
short
2
System: : Int1 6
unsig ned short
2
System: :UInt1 6
i nt
4
System: : Int 32
unslg ned int
4
System: :Ul nt 32
long
4
Syst em: : Int 32
uns igned long
4
Syste m: :U l nt 32
long long
8
Syste m: : Int 64
unsi gned long long
8
Syst em: :Ul nt 64
fl oat
4
System : :Si ngl e
double
8
Syst em: :Doubl e
long double
8
Syst em : :Double
wchar t
2
System: :Char
typ char jest równoznaczny z typem signed cha r, tak w i ęc skojarzony typ klasy to Syste m: :SByt e. Zauważ , że domyślny typ char można z m i en ić na unsig ned char, u stawiaj ąc opcję kompilatora / J, w którym to przypadku skojarzonym typem klasy warto ś ci będzie System : :Byte. Systemto główna przestrzeń nazw, w której zdefiniowane są wszystkie typy klas wa rtoś ci C++/CL I. W przestrzeni nazw Syst em zdefini owanych jest także wiele innych typów, takie jak na przykład typ St ri ng reprez entuj ący ł ań cuch y znaków, z którym zetkniemy się jeszcze w rozdziale 4. W C++/CLI zdefiniowany jest także typ klasy wartoś ci Syst em: :Deci mal w przestrzeni nazw Syst em. Zmienne tego typu przechowuj ą wartości dzie s iętne z dokładno ścią do 28 miejsc po prze cinku .
D omyślni e wartości
J akjuż powiedziałem ,
typ klasy wartości skoj arzony z każdą z nazw typów fundamentalnych znacznie zwięk s z a mo żli w o ści zmi ennych tego typu w C++/CLI. Je śli j est to koni eczne, kompilator dokona automatycznej konwersji z warto ści oryginalnej do skojarzonego typu kla sy i odwrotnie. Procesy te nazywa s i ę odpowiednio opakowywaniem (ang. boxing) i odpa kowywaniem (ang. unboxing). Dzięki temu zmienna jednego z tych typów może zachowy wa ć się jak prosta warto ś ć lub jak obiekt, w zależności od warunków. Więcej na ten temat dowiemy s i ę w rozdzial e 6.
130
Visual C++ 2005. Od podstaw P oniewa ż
nazwy typów fundamentalnych w C++ ISO/ANSI są aliasami nazw typów klas w programach C++/C LI, to w zasadz ie w programach C++/CLI m ożn a u żywać za równo jednych, jak i drugich. Wiem y j uż na przykład , ż e aby utw orzyć zmi enn e całkow i te i typu zmiennopozycyjnego, mo żemy p o służyć s ię n astępującymi instrukcj am i:
w artości
int count ~ 10; double val ue = 2.5; Mogli byś my użyć
mu
s ko mp i low ać
nazw klas program :
wartości o dpowi adającyc h
typom fundamentalnym i bez proble
System; ;I nt 32 count ~ 10; Syst em: :Doub le value = 2.5; Mimo że je st to całkowici e poprawne, to jednak powinniśmy sto sować nazwy typów funda mentalnych, takie jak i nt i doubl e, zamiast nazw klas w arto ści Syst em: : Int3 2 i Syst em: :Doubl e. Pow ód jest taki , że map ow anie pomi ęd zy nazwami typów fund am ent aln ych i typami klas wartoś ci , które op isywałe m, odnosi s ię tylko do komp ilatora Visual C++ 2005. Inne komp i latory nie mus zą po siad ać implementacji tego mapow ania . Na zwy fundamentalnych typów danych są ustalone przez standard języka C++/CLI, ale mapowani e na typy klas wartości jest dla większoś c i typów uzal eżnione od implementacj i. Typ l ong w Visual C++ 2005 jest mapo wany na typ Int 32, ale możliw e jest, że w innych implementacjach będzie mapowany na Int 64. Posiadanie danych typów fundamentalnych reprezentowanych przez obiekty typu klasy warto ści jest ważną cechąjęzyka CH/CLI. W CH ISO/ANSI typy fundamentalne i typy klasowe są c a łki e m inne, natomiast w C++/CLI wszelki e dane przechowywane s ąjako obiekty typu klasy w postaci typu klasy wartości albo jako referencyjny typ klasy. O referencyjny ch typach klas powi emy wi ęcej w rozdziale 7. Teraz wypróbujemy program konsolowy CLR.
Rm!!mI OWOCOWI prOgram konsolowI CLR Utwórz now y projekt i jako typ proj ektu wybierz CLR , zaś jako szablon - CLR Console App lication. N ast ępnie nazwij projekt Cw2_ 12, jak pok azan o na rysunku 2.13. Po
klikni ę c iu
przycisku OK kreator aplikacji wygeneruje projekt
II Cw2_ 12.cpp: main proj ect file.
#lncl ude "stdafx.h" usi ng namespace System; i nt ma i n(array<Syst em : :St ri ng
A >
A
args)
(
Console: :WriteLi neCL "Hell o ret urn O;
\~o r l d " ) :
zawi eraj ący następujący
kod:
Rozdział2.
New Project ~~
'
• Dane. zmienne i działania arytmetyczne
.
131
®r8J
..
N
Project types : Templates : 8 V lsua l -C+-+·--------, r-. r_ . ~. '--a ,-"u '-,---~·i-os.!.':'. .in~t-alIe-d-t-em-p-la-t-es--------------...=; . ·ATL
CLR o
MFC .. SmartD evce Win32
lj;
J!ł. AS P ,NET Web Se-vc e
fł.lc la ss LIbrary
l @i'@.i;;.IWPł@@ii
General
Other Languages
I
C'!il CLR EmptyProject
fJ:jSQLSerYer ProJect ilil'lWindows FOrtns Contra I Lbr ary
.j;l Windows Forms Application ~W indows Service
I _~y !~_~ pl.~ tes
itJ Other Project Types
i5]Search onlne Te mplate s...
l.~project~r creating,; .console appUcatlon Name :
Locaton : so lutionName :
, L~~~Trans la tons\h e lion\l.or .Hor tnns VIS U.;i CH 200?~~łady,, _~~
lB_12
~
0 Create
~.J
I Brawse .. I
d~ectory for solutlon OK
jI
C= 'A' ) && (letter n~ " ~~%l}, lł?n ~7fc t",eept",~~~ill,S'\~~\b~h wyrażeń logicznych łączonych za pomocą operatora &&. Aby poznać wynik połączenia warunku o wartości true z warunkiem o wartości fa l se, n al eży sprawdzić wartość znajdującą się na przeci ęciu wiersza oznaczonego jako true z ko lumną o nagłówku 'false. W rzeczywistości nie potrzebujemy do tego żadnej tabeli, gdyż wartość true można uzyskać tylko wtedy, gdy oba warunki są t rue.
Operator sumy logicznej DR Operator sumy logicznej OR (II) ma zastosowanie, gdy mamy dwa warunki i chcem y otrzymać warto ść true, jeżeli przynajmniej jeden z nich ma wartość true. Na przykład bank może uznać , że kwalifikujemy się do otrzymania pożyczki , je śli zarabi amy 100 000 złotych rocznie lub mamy l 000 000 złotych w gotówce. Takie sprawdzanie można wykonać za pomocą nast ępującej instrukcj i warunkowej i f : i f ( ( i hcome >~ 100000 .00) II (capi t al cout « "Il e wie lce szanowny pan
>~
1000000.00) ) od nas
c hci a łby
pożyczyć ? " ;
Pochlebstwa posypią się na nas tylko wtedy, gdy co najmniej jeden z warunków ma wartość tru e (lepsze byłoby pytanie : "Czemu pan chce pożyczać pieniądze?" - zadziwiające jest, że banki chcą pożyczać pieniądze tylko tym, którzy już je mają). Dla operatora
II również można stworzyć tabelę prawdy:
II
false
true
false
fa l se
t rue
true
t r ue
t rue
W tym przypadku możemy otrzymać
również można w bardzo prosty spo sób okre śl i wynik : warto ś ć f al se tylko wtedy, gdy oba oper andy mają wartość fa l se.
Negacja logiczna NOT Trzeci operator logi czny! przyjmuje tylko jeden operand typu logicznego i odwraca jego wartość . Tak w i ęc je żeli zmienna test ma wartość t rue, to ! test ma warto ś ć fa l se. l podobnie, jeżeli zmienna t est ma wartość fa l se, to ! te st ma wartość t rue. Spójrzmy na prosty przykład : jeżeli zmienna x ma wartość 10, to wyrażenie : I
(x > 5)
wartość
ma
fa l se, ponieważ
Operatora ! możemy I
(dochód
>
także użyć
w ulubionym
x > 5 jest true.
wyrażeniu
Karola Dickensa:
wydatk i)
Jeżeli wyrażenie
bank zacznie
wartością wyrażenia
to ma wartość true, to mamie z nami, przynajmniej od momentu, w którym nasze czeki.
zwracać
Rozdział 3.
• Decyzje i pętle
157
Operatora ! możemy także używać z innymi podstawowymi typami danych . Przypuśćmy , że mamy zm ienną typu zmie nnopozycyjnego o nazwie rate i wartości 3. 2. Może zdarzyć się sytuacja, w której chcemy sprawdzić, czy wartość zmiennej rat e nie jest zerowa. W takim przy padku możemy u żyć następującej instrukcji: I
(rate )
Wartość 3. 2 jest różna od zera, a więc zostanie przekonwertowana na co z kolei spowoduje, że wynik tego wyrażenia będzie fa l se.
wartość logiczną
t rue,
l!mImI ~ączenie operatorów logicznych Operatory i wyrażenia logiczne można dowolnie łączyć . Na przykład sprawdzanie, czy zmien na zawiera literę, można wykonać za po mocą pojedynczej instrukcji warunkowej i f. Przed stawiam to na poniższym listingu : II Cw3_03.cpp
II Sprawdzanie za pomo cą operatorów logiczny ch, czy zmi enna zaw iera
literę.
#i ncl ude using std: :ci n: usi ng std: :cout: using std: :endl: int mai n() (
char l et t er = O: cout « endl « "Wpisz ci n » l etter :
j a ki ś
II Zmi enna do p rzechowywania II wprowadzonych dany ch.
znak :
if«( letter >= 'A') && ( let t er = ' a' ) && (letter
b ? a : b:
II Przypisz zmiennej c II która jes t większa .
wartość
zmi ennej a lub b, w zależn ości od tego,
Pierwszy operand operatora warunkowego musi być wyrażeniem , którego wynikiem jest war logiczna - w tym przypadku jest to wyrażenie a > b. Jeżeli wyrażenie to zwróci wartość t rue, to jako rezultat tej operacji zwróco ny zosta nie drugi operand, czy li a. J eże li natomiast pierwszy argument zwróci wartość f al se, to trzeci operand, tutaj b, zostanie wybrany jako war tość całej operacji . A zatem wartością wyrażenia warunkowego a > b?a :b bę dz ie a, jeże l i a jest większe od b, lub b w przeciwnym przypadku. Użyc ie operatora warunkowego w tej in strukcji przypisania jest równoznaczne z następuj ącą i n stru kcj ą i f :
to ś ć
if(a
> b )
c = a:
else
c
~
b:
Operator warunkowy
warunek ?
m o żn a przedstaw i ć
wyrażenie]
:
za
po mocą następ uj ącego
ogó lnego wzoru :
wyrażenie2
Jeże li
wynikiem warunku jest t rue, to zwrócona zostanie wartość pierwszego wyrażenia, w prze ciwnym przypadku zwrócona zostanie wartość drugiego wyrażen ia .
~ Używanie operatora warunkowego zdanymi wyjściowymi Operatora warunkowego najczęściej używa s ię w celu kontrolowania danych wysyłanych na wyjście - zależnie od wyniku wyrażenia lub wartości zmiennej. Można spowodować wysłanie różnyc h komunikatów w za leż ności od okreś lonego warun ku. II Cw3_04.cpp
II Operator warunk owy sterujący danymi
#incl ude usi ng std : :cout:
wyjś c iowym i .
Rozdział3.
• Decyzje i pętle
159
using st d: :endl: int main ( ) { int nCakes
=
l:
II Licznik liczby ciastek.
cout « endl « "Mamy" « nCakes « « endl :
" cias t " «
((nCa kes > 1)
"ka." : "ko, ")
" ci ast" «
(( nCa kes > 1) ? "ka."
++nCakes: cout « endl « "Mamy " « «
nCakes «
"ko. " )
endl :
retu rn O:
W wyniku
działania
tego programu otrzymamy:
Mamy 1 cias tk o. Mamy 2 ci ast ka.
Instrukcja switch Instrukcja swi tc h pozwala na wybór spośród wielu opcji na podstawie zbioru określonych wartości danego wyrażenia. Przypomina ona prawdziwy przełącznik obrotowy, z tym że można wybrać jedną z kilku dostępnych opcji. W niektórych pralkach na przyklad w taki spo sób wybiera się odpowiedni program prania. Podanych jest kilka możliwych ustawień prze łącznika, takich jak bawełna, wełna, włókna syntetyczne itd., i za pomocą przełącznika wybie ramy interesującą nas opcję . W instrukcji swi t ch wybór opcji uzależniony jest od wartości określonego wyrażenia . Poszcze gólne opcje do wyboru definiuje się za pomocą słowa kluczowego case. Określona opcja zosta nie wybrana, jeżeli wartość wyrażenia swi t ch równa będzie jej wartości . Dla każdego moż liwego wyboru jest tylko jedna wartość case, ale wszystkie wartości case muszą być różne. Każda opcja definiowana jest oddzielnie za pomocą słowa kluczowego case.
swi t ch nie pasuje do żadnej ze zdefiniowan ych opcji, to automatycz nie wybierana jest opcja domyślna (default). Opcję domyślną można w razie potrzeby okre ś l i ć samodzielnie, co zrobimy w poniższym przykładowym kodzie. Jeżeli jej nie zdefiniujemy, to nie będzie ona robiła nic.
Jeżeli wartość wyrażenia
~ Instrukcja switch Na
poniższym przykładzie prześledzimy
II Cw3_05.cpp
II Używanie instruk cji switch.
#i ncl ude
sposób działania instrukcji switch.
160
Visual C++ 2005. Od podstaw
using st d: :ci n:
using st d: :cout :
using st d: :endl :
int mai n()
{
i
nt cho i ce
=
O:
II Zmienna do przechowywa nia
wa rtości
wybranej opcji.
cout « endl
« "Twoj a elektroniczna ks ią żka kucharska jest do t woj ej dyspozycj i ." « "Do wyboru ma sz jedno z po n i żs zyc h pysznych d a ń :
Jak to działa Po zdefiniowaniu opcji w wyrażeniu wyjściowym i zapisaniu numeru wybranej opcji w zmien nej choice wykonywana jest instrukcja switc h z warunkiem w postaci zmiennej choice umiesz czonej w nawiasach znajdujących się bezpośrednio po słow ie kluczowym swit ch. Możliwe opcje wyboru w instrukcji switc h znajduj ą s i ę pomiędzy nawiasami klamrowymi i każda z nich zdefiniowana jest za pomocą słowa kluczowego case, po którym następuje warto ść zmiennej cho i ce, odpowiadająca tej opcji . Na końcu znajduje s i ę ś redn i k . Jak widać , instrukcje, które mają zostać wykonane w przypadku wyboru każdej z opcji, napi sane są po dwukropku na końcu każdej etykiety case, a ich działan ie kończone jest i nstrukcj ą break, która przenosi wykonywanie kodu do pierw szej instrukcji po switch. Zastosowanie break nie jest obowiązkowe, ale bez niej wykonane zostałyby wszystkie pozostałe instrukcje, czego zazwyczaj nie chcemy . Aby sp raw dz ić , co si ę stanie, mo żn a usunąć instrukcje brea k z powyższego przykładu .
Rozdział 3.
• Decyzje i pętle
161
zmiennej cho i ce nie odpowiada żadnej z warto ści case, to wykon ywana jest instrukcja default. Nie jest ona n iezbędna . Jeżeli jej nie ma i warto ś ć wyrażenia sprawdzają cego nie pasuje do żadnej opcji case, to następuje wyjście z instrukcji swit ch i program jest kontynuowany od instrukcji, która znajduj e s ię bezpośrednio po switch. Jeżeli wartość
~ WsPóldzielenie jednego przypadku Każde
z wyrażeń , które określamy w celu ident yfik acji opcji , musi b yć s tał e, aby w arto ś ć w czasie kompilacji, oraz jego wynikiem musi być unikalna warto ść typu całkowitego . Powodem, dla którego dwie stałe wartoś ci case nie mogą by ć takie same, jest fakt, że kompilator nie wiedziałby, na którą z nich s ię zdecydować . Oczywi ście , różne opcje case nie muszą wykonywać unikalnych czynności . Kilka opcji case może dzielić jedną czyn ność , jak pokazano poni żej . mo żna było określić
II Cw3_06.cpp
II Używan ie wielu czyn ności case.
#include using std : .ct n:
usi ng std: :cout ;
using std : .endl :
int mainO {
char l et t er = O:
cout « endl
« "Podaj ma ł ą ci n » letter;
l it e r ę :
switch(letter*(letter >= 'a ' && l ette r ~
'a' &&let t er
A
args )
(
int vowels = O: int consonants = O; St ring proverb = L" Ś l e pemu nic po okularach .": for each( wchar t ch in proverb) A
-
(
if (Char ; :IsLetter (ch) (
ch
~
Char: :ToLower (ch) :
II Przekonwertuj na małe litery.
swttcht cn)
{
case 'a ': case 'e' : case l : case 'o '; case ' u' : case 'y' : ++vowels;
znajdujący się
Rozdział3.
• Decyzie i pętle
185
break; default ; ++consona nts ; break; }
Console; ;Writ eL ine(proverb); Console ; ;WriteLine(L "Powiedzenie zawie ra {O}
sa mo g ło s ek
i {l } s pó ł g ł o s e k. ", vowel s . consonants);
ret urn O: W wyniku
działania tego
kodu otrzymamy następujący rezultat:
Ś lepemu
nic po okula rach. Powi edzeni e zawiera 9 s a mogł ose k i 12
s pó ł gło se k.
Jak to działa Program zlicza liczbę samogłosek i spółgłosek w łańcuchu wskazywanym przez zmienną proverb. Dokonuje tego, sprawdzając każdy znak w łańcuchu za pomocą pętli for each. Najpierw zdefiniowane zostały dwie zmienne do przechowywania liczby samogłosek i spółgłosek.
i nt vowel s = O; int consonants = O: Obie te zmienne w C++/CLI N astęp n i e
są typu
I nt 32, który przechowuje 32-bitowe liczby całkowite.
definiujemy tekst do anali zy.
Stri ng proverb = A
L" Ś l epemu
nic po okularach
Zmienna proverb j est typu Stri ng który jest opisywany jako "uchwyt do łańcucha" . Uchwyt używany jest do przechowywania lokalizacji obiektu na stercie, którą zarządza CLR. Na temat uchwytów i typu St ri ng dowiemy się więcej , gdy dojdziemy do typów klasowych C++/CLI. Na razie wystarc zy nam wiedza, że typ ten w C++/CLI używany jest dla zmiennych przechowujących łańcuchy znaków. A
,
A
Pętla
ma
for each sprawdzająca wszystkie znaki w łańcuchu wskazywanym przez zm i enną proverb
następującą formę :
for each( wch ar_t ch in proverb) { II Przetwarzaj
b ieżący
znak w zmiennej ch.
}
Znaki w zmiennej proverb są znakami Unicode, a więc używamy zmiennej typu wcha r_t (równoznaczny z typem Cha r) do ich przechowywania. Pętla zapisuje jedenpo drugim znaki ze zmiennej proverb do zmiennej pętlowej ch, która jest typu C++/CLI Char. Zmienna ta ma w pętli zasięg lokalny (inaczej mówiąc, istnieje tylko wewnątrz bloku pętli). Po pierwszej iteracji zmienna ta zawiera tylko pierwszy znak z łańcucha, po drugiej dwa pierwsze znaki, po trzeciej trzy itd., aż skoń czą się znaki i zakończy się działanie pętli .
186
Visual C++ 2005. Od podstaw Wewnątrz pętli
za
pomocą
instrukcji warunkowej i f sprawdzamy, czy dany znak jest
literą:
i f( Ch ar :: IsL etter(c h))
Funkcja Char: : I sLett er ( ) zwraca true , jeśli argument (w tym przypadku ch) jest literą, i f al se w przeciwnym przypadku . Tak więc blok znajdujący się po instrukcji warunkowej i f zostanie wykonany tylko wtedy, gdy zmienna ch zawiera literę. Jest to konieczne, gdyż nie chcemy, aby znaki interpunkcyjne były traktowane jak litery. Po ustaleniu, że zmienna ch jest
literą,
konwertujemy ją na
małą literę
za pomocą instrukcji :
ch = Cha r : :ToLower(ch) ; II Przekonwertuj na male litery.
Do tego celu używamy funkcji Char : :ToLower ( ) z biblioteki platformy .N ET, która zwraca odpowiednik argumentu (w naszym przypadku ch) przekonwertowany do małej litery. Jeżeli argument jest już małą literą, to funkcja zwróci go w niezmienionej postaci . Dzięki konwersji na małe litery unikamy konieczności sprawdzania zarówno wielkich, jak i małych liter. Za
pomocą
instrukcji switch sprawdzamy, czy zmienna ch zawiera
spółgłoskę,
czy samo-
głoskę .
switch( ch)
( case ' a' : case 'e': case 'i ' : case ' o ' : case 'u' : case ' s ' : ++vowels; bre ak : default : ++consonant s ; break :
W każdym z pięciu przypadków, kiedy zmienna ch zawiera samogłoskę, wartość zmiennej vowe l S jest zwiększana o jeden. W przeciwnym przypadku zwiększamy wartość zmiennej consonants . Instrukcja swi t ch wykonywana jest dla każdego znaku w łańcuchu proverb, a więc kiedy pętla kończy działanie , zmienna vowel S zawiera liczbę samogłosek w łańcuchu , a zmienna consonants liczbę spółgłosek . Następnie wynik wysyłamy na wyjście za pomocą następujących instrukcji : Console: :Writ eLi ne( pro ver b) ; Console: :Wr iteLi ne( L" Powi edzeni e zawier a {O} sa mog łos e k i { l } spó ł g łos e k. ", vowe ls . consonants) ;
W ostatniej instrukcji wartość zmiennej vowels zostaje wstawiona w miejsce sekwencji {O} w łańcuchu , a wartość zmiennej consonants w miejsce sekwencji {l }. Jest to możliwe, ponieważ do argumentów występujących po pierwszym argumencie łańcucha formatującego odnosimy się za pomocą wartości indeksowanych, zaczynając od O.
Rozdział 3.
• Decyzje i pętle
187
Podsumowanie W rozdziale tym poznaliśmy wszystkie najważniejsze mechanizmy podejmowania decyzji w programach w C++. Przejrze liśmy także wszystk ie narzędzia służąc e do powtarzania bloków instrukcji. A oto lista najważniejszych poruszonych zagadnień : •
Podstawowa zdo lność podejmowania decyzji oparta jest na zbiorze operatorów relacji, które pozwa lają na sprawdzanie i porównywanie wyrażeń oraz zwracają wartość logiczną jako wynik (t rue lub f al se).
•
Decyzje
można także podejmować na
podstawie warunków, które nie zwrac aj ą logicznych. W takim przypadku podczas sprawdzania wartości warunku wszys tkie warto ści niezerowe s ą konwertowane na t rue, a zerowe na f al se. wartości
•
Najbardziej podstawową strukturą pozwal ając ą na podejmowanie dec yzji w C++ jest instrukcja warunkowa i f . Dodatkowej e lastyczności n a d aj ą instrukcja swi tc h i operator warunkowy.
•
W C++ ISO /ANSI dostępne s ą trzy podstawowe metody powtarzania bloków instrukcji - pęt le : fo r , wh i l e oraz do-whil e. Pętl a for umożliwia wykonanie określonej liczby powtórzeń . Pęt la whi l e powtarza się, dopóki warunek ma wartość true. P ętl a do-whi l e wykonuje się co najmniej jeden raz i kontynuuje powtórzenia tak długo , jak długo warunek ma wartość tr ue.
•
W C++/CLI
•
Każdą pętlę można zagnieździć
•
Słowo kluczowe cont i nue pozwala na przeskoczenie i przejście do następnej iteracji .
•
Słowo kluczowe break powoduje natychmiastowe wyjście z pętli . Służy ono do wyc hodzenia z instrukcji swi t ch umieszczonej po instrukcjach case.
dostępna jest także pętla
for eac h.
w dowo lnej innej
pętli .
pozostałej części
iteracji w pętl i także
Ćwiczenia Kod
źródłowy
pobrać
wszystkic h przykładów w tej książce oraz ze strony: http://helion.pl/ksiazki/vcppo .htm.
rozwiązania
do
ćwiczeń można
1. Napisz program przyjmujący liczby ze strumienia wejściowego ci n, który je następn i e sumuje. Program powinien zatrzymać się w momencie podania zera . Utwórz ten progra m w trzech wersjach: przy u życ iu pętli wh i le, do-whi l e oraz for .
2. Napisz program w C++ ISO/ANSI samogłoski .
Liczenie powinno
p rzyj muj ący
znaki z klawia tury i zliczający w momencie napotk ania litery "q" pobierz znaki, a zliczania dokonaj za pomo cą
się zatrzymać
lub "Q". Za pomocą ni eskończonej instrukcj i switc h.
pętli
3. Napisz program wyświetlający w kolumnach tab liczkę
mnoże nia
od 2 do 12.
188
Visual C++ 2005. Od podstaw 4. Masz program, w którym chcesz ustaw i ć
zmienną określającą tryb otwarcia pliku w oparciu o dwa atrybuty: typ pliku, który może być tekstowy lub binarny, oraz tryb otwarcia pliku - do odczytu , zapisu lub dołączenia danych. Za pomocą operatorów bitowych (& i I) oraz zestawu znaczników opracuj metodę pozwalającą na ustawienie pojedynczej liczby całkowitej na dowolną kombinację tych dwóch atrybutów. Napisz program, który ustawia taką zmienną, a następnie ją dekoduje , wyświetlając jej ustawienia dla wszy stkich możliwyc h kombinacj i atryb utów .
5. Przepisz przykładowy program Cw3j jako program w języku C++/CLI u żywać
(możesz
funkcji Consol e: :ReadKey() do pobierania znaków z klawiatury).
łań cu ch znaków (jako typ St r t nq"), a następn ie analizuje znaki tego łańcucha w poszukiwaniu liczby wielkich liter, małych liter, znaków n i enal eż ącyc h do alfabetu oraz ogó lnej liczby znaków .
&. Napisz program konsolowy CLR, który definiuje
4 Tablice, łańcuchy znaków
i wskaźniki
Poznaliśmy już
wszystkie fundamentalne typy danych oraz posiadamy wiedzę na temat wykonywania obliczeń i podejmowania decyzji w programie . Rozdział ten poświęcony został szerszemu omówieniu zastosowań podstawowych technik, które poznaliśmy do tej pory . Zamiast pracować na pojedynczych danych, będziemy operować całymi ich zbiorami . W roz dziale tym dowiesz się: • Czym są tablice i jak ich używać. • Jak
deklarować
i inicjalizować tablice
• Jak
deklarow ać
tablice wielowymiarowe i jak ich
• Czym • Jak
są w skaźniki
deklarować
i jak ich
różnego
typu. używać.
używać .
i inicjalizować
wskaźniki różnego
typu.
• Co łączy tablice i wskaźniki . • Czym są referencje, jak się je deklaruje, a także poznasz kilka podstawowych
informacji na temat ich użycia.
• Jak dynamicznie przydzielać pamięć zmiennym w natywnym C++. • Jak
działa
dynamiczne przydzielanie pamięci w programach CLR.
• Czym są uchwyty i odwołania w programach CLR.
• Jak
pracować
• Czym
śledzące
oraz dlaczego potrzebujemy ich
z łańcuchami znaków i tablicami w programach w C++/CLI.
są wskaźniki wewnętrzne
i jak
się je
tworzy oraz jak
się
ich
używa.
W rozdziale tym będziemy znacznie częściej korzystali z obiektów , mimo że nie wiemy jesz cze, jak się je tworzy, ale jeśli nie wszystko jest całkiem jasne, nie musimy się tym przejmo wać. Bardziej szczegółowo na temat klas i obiektów będziemy mówić od rozdziału 7.
190
Visual C++ 2005. Od podstaw
ObslUga wielu warlości danych lego samego Iypu Wiemy już, jak zadeklarować i zainicjalizować różnego typu zmienne, z których każda prze chowuje pojedynczy fragment informacji. Fragmenty takie będę nazywał elementami danych. Potrafimy stworzyć zmienną typu cha r, przechowującą pojedynczy znak, zmienną typu snort , i nt i long, przechowującą jedną liczbę całkowitą, oraz zmienną typu f loat lub double, która przechowuje pojedynczą liczbę zmiennopozycyjną. Oczywistym krokiem naprzód jest naucze nie się operowania kilkoma elementami danych określonego typu za pomocą jednej nazwy zmiennej . Umiejętność taka umożliwiłaby nam znaczne rozszerzenie naszych możliwości. Oto przykład prezentujący, w jakiej sytuacji moglibyśmy tego potrzebować. Przypuśćmy, że mamy napisać program obsługujący listę płac. Tworzenie zmiennej o nowej nazwie dla pensji czy informacji podatkowych każdego pracownika byłoby zadaniem co najmniej żmudnym. O wiele wygodniej byłoby móc odnosić się do tego pracownika za pomocąjednej nazwy gene rycznej, takiej jak np. nazwaPracownika, oraz mieć inne nazwy generyczne dla różnego rodzaju danych każdego z pracowników, jak pensja czy podatek itd. Oczywiście przydałaby się także możliwość odnalezienia określonego pracownika wśród wielu innych pracowników oraz wydo bycia skojarzonych z nim danych generycznych, które są przechowywane w zmiennych . Tego typu potrzeba pojawia się za każdym razem , gdy mamy do czynienia ze zbiorem podmiotów, którymi chcemy zarządzać w programie - mogą to być zarówno piłkarze, jak i okręty wojenne. W C++ oczywiście dostępne są takie mechanizmy.
Tablice Podstawą rozwiązania
wymienionych problemów w C++ ISO/ANSI są tablice. Tablica to po prostu zbiór miejsc w pamięci, zwanych elementami tablicy lub prościej elementami, z któ rych każdy może przechowywać dane tego samego typu i do których odwołujemy się za po mocą tej samej nazwy zmiennej . Nazwy pracowników z listy płac można by było przechowy wać w jednej tablicy, ich płace w drugiej , a należny podatek w trzeciej. Poszczególne elementy w tablicy są określane przez wartość indeksową, która jest po prostu liczbą całkowitą i reprezentuje wszystkie elementy tablicy za pomocą kolejnych liczb, zaczy nając od zera. Wartość indeksową elementu tablicy można sobie także wyobrazić jako war tość jego przesunięcia od pierwszego elementu tablicy. Pierwszy element ma wartość prze sunięcia równą zero , a zatem jego indeks to O. Wartość indeksowa 3 odnosi się zatem do czwartego elementu w tablicy. W przypadku listy płac można by było tak zaprojektować tablice, żeby wartości indeksowe odnoszące się do nazwy pracownika, jego płac oraz informacji podat kowych miały ten sam indeks w poszczególnych tablicach. Podstawowa struktura tablicy
została przedstawiona
na rysunku 4.1.
Rysunek 4.1 przedstawia tablicę . Tablica o nazwie height zawiera sześć elementów , z których każdy przechowuje inną wartość. Mogą się one odnosić na przykład do wzrostu członków rodziny w porządku malejącym lub rosnącym. Jako że elementów jest sześć, to indeksy mają wartości od Odo 5. W celu odniesienia się do określonego elementu piszemy nazwę tablicy,
Rozdział 4.•
Rysunek 4.1
Tablice. łańcuch» znaków i wskaźniki
Warto ś ć indeksu _ _---, drugiego elementu
Nazwa
Wartość piątego
indeksu _ elementu
_
191
---,
"bl;" l
heightlO] 73
I
height[l]
heightl2]
height[3]
height[4]
heightlS]
62
51
42
41
34
Tablica height zawiera 6 elementów
po której następuje wartość indeksowa wybranego elementu otoczona nawiasami kwadra towymi. Do trzeciego elementu tablicy odwołaliby śmy s i ę za pomocą notacji hei ght[ 2]. Jeżeli przyjmiemy, że indeks stanowi warto ść przesunięcia względem pierwszego elementu, to z łatwością zauważymy , że indeks na przykład czwartego elementu wynosi 3. Ilość pamięci potrzebnej do przechowywania każdego elementu uzależniona jest od jego typu. Wszystkie elementy tablicy przechowywane są w sąsiadujących blokach pamięci .
Deklarowanie tablic Zasadniczo tablice deklaruje się tak samo jak zmienne . Jedyna różnica polega na tym, że bez pośrednio po nazwie tablicy w nawiasach kwadratowych znajduje s ię liczba jej elementów. Można na przykład zadeklarować tablicę liczb całkowitych o nazwie hei ght , którą widzieliśmy na poprzednim rysunku, za pomocą następującej instrukcji: long height [ 6J;
Jako że wartości typu l ong wymagają czterech bajtów pamięci , cała tablica zajmie ich 24. Ta blice mogą być dowolnego rozmiaru, oczywiście w granicach określonych przez ilość pamięci dostępnej na danej platformie sprzętowej . Tablice mogą być dowolnego typu . Aby na przykład utworzyć tablice do przechowywania informacji o pojemności i mocy silników, można napi sać następujące instrukcje: doubl e cubi c_i nc hes [ 10]; II Pojemność silnika.
doubl e hor sepower [ 10] ; II Moc silnika.
Jeżeli
interesujesz się samochodami, to w tych tablicach możesz przechowywać informacje o pojemności i mocy do dziesięciu silników, do których można się odnosić za pomocą warto ści indeksowych od Odo 9. Podobnie jak w przypadku innych zmiennych, w jednej instrukcji można zadeklarować kilka tablic określonego typu, ale w praktyce prawie zawsze lepiej jest każdą deklarację umieszczać w oddzielnym wierszu.
192
Visual C++ 2005. Od podslaw
~ Używanie tablic Przed przejściem do analizy kodu wyobraź sobie, że przy ka żdym tankowaniu zapisujesz i l ość zakupionej benzyny do samochodu oraz l i czbę przejechanych kilometrów. Może sz teraz napisa ć program anal i zuj ący te dane w celu sprawdzeni a, jak wygląda zużycie paliwa za każ dym razem, gdy je kupujesz: II Cw4_0i .cpp
II Oblicza nie liczby kilometrów p rzejec hanyc h na jednym baku.
#include
#include
using using usi ng using
std: .ci n:
st d: :cout:
std : :endl :
st d: :setw:
int main() {
const int MAX = za : doub le gas[ MAX J: long mi les[ MAX J: int count = o: char i ndicat or = ' t ':
II Maksym alna liczba wartości . II Jloś ć benzyny w litrach. II Wskazania liczn ika przebieg u. II Licznik pętli . II Wskaźn ik wprowadzania dany ch.
wh ile ( (i nd icat or == 't ' II indicat or == 'T') && count < MAX ) (
cout
« «
cin » cout « cin »
end l "Podaj il oś ć pa l iwa : II Wczytaj ilosć p aliwa. gas[countJ: "Podaj dane z l icznl ka przebiegu' " mi les[count J : II Wczyt aj wartoś ć z licznika przebiegu.
++count:
cout « "Czy chcesz ci n » indicat or :
i f( count Lengt h);
II Ustaw wszys tkie elementy na zero.
Pierwszym argumentem funkcji C1ear( ) jest tablica, którą chcemy wyczyścić, drugi argument to indeks pierwszego elementu, który chcemy wyczyścić, a trzeci to liczba elementów do wyczyszczenia. A zatem powyższy przykład ustawia wszystkie elementy tablicy sampl es na O. O. Jeżeli funkcję C1ear () zastosujemy z tablicą uchwytów śledzących , jak np. St r tnq", to elementy zostaną ustawione na nu 11. Zastosowanie tej funkcji dla tablicy wartości logicznych spowoduje ustawienie wartości elementów na fa l se. Czas na wyczyszczenie jakiejś tablicy .
R!lml!mI Używanie tablic ClR Przykład
ten generuje
tablicę zawierającą
losowe
wartości,
a
następnie
z nich:
using namespace System ; i nt main(array<System : :St ring A> Aargs)
r array<double>Asamples II Generowanie losowych
=
gcnew array<double>(50):
wartości
dla elementów.
RandomA generat or = gcnew Random ; for(i nt i = O : i< samples->Lengt h ; i++) samples[i ] = l OO.O*generat or->Next Double( );
II WY.lylanie na
wyjście
próbek.
Console : 'WriteLi ne(L"Tabl ica zawi era n a st ę pują c e l iczby:"); for( int i = O : i< samp les->Lengt h : i++)
odnajduje
największą
236
Visual C++ 2005. Od podstaw
Conso l e: :WriteC L"{O ,10:F2}", samples[l]) : i f (Ci +l )%5 ~ ~ O) Console: :WriteLi neC) : II Szukanie największ ej wartośc i ,
double max ~ O: for eachC doub le samp le i n samo les) if Cmax < samp le) max ~ samo le ; Co nso le : :WriteLine( L "Na jw ię ksza wa rtość
w tabl icy t o (O :F2}", max) :
ret urn O; Przykładowy
wynik
działania
tego programu
widać poniżej:
Tab l lca zawiera następu jące ll czby: 63,03 66,07 83,73 12,11 76,99 89,57 83,78 80,28 25,02 86, 09 56,39 69,56 64,79 90,73 0,84 11 ,58 11,26 55,80 75, 29 75,01 88,99 26,72 57,32 95,52 77,49 43,02 28 ,21 6,97 24,58 72.85 50,23 40,12 15,13 80,63 86.40 79,60 66.04 41.69 59.03 5.86 Najw ięks za wa r tość w tab licy to 95.52
50 elementów typu doub 1e: gcnew array<do ub le>(50);
Zmienna tablicowa samp1es musi stercie oczyszczonej . Tablicę wypełniamy
losowymi
być
uchwytem
wartościami
śledzącym, gdyż
tablice CLR tworzone
są na
typu doub1e za pomocą następujących instrukcji:
Ra ndamA generator = gcnew Ra ndom: tor t i nt i = O ; i < samp les ->Length , i++) samp l es[i] = 100. 0*generato r- >Next DoubleC) :
Pierwsza instrukcja tworzy na stercie CLR obiekt typu Random. Obiekt Random zawiera funkcje, które generują liczby pseudolosowe. W tym przypadku użyliśmy funkcji Next Doub 1e( ) w pętli, która zwraca losową liczbę typu doub 1e należącą do zbioru 0,0 - 1,0. Dzięki pomnożeniu tego przez 100 otrzymujemy wartości od 0,0 do 100,0. Pętla fo r zapisuje do każdego elementu tablicy samp 1es losową liczbę .
Obiekt Random zawiera również funkcję Next( ), która zwraca losową liczbę nieujemną typu i nt. Jeżeli w wywołaniu funk cji Next ( ) podamy argument w postaci liczby całkowitej, to zwró ci ona losową, nieujemną wartość mniejszą niż podany argument. Można także
Rozdzial4.• Tablice. łańcuchy znaków i wskaźniki poda ć
dwa argumenty w p ostaci liczb całko witych, które b ędą i maksymalną zwróconych liczb losowych.
237
reprezentowały wartości
minimalną
N astępna pętla wysyła
na wyjście
zawartość
tablicy, po
pięć
elementów na wiersz :
Console: :Wr i tel i netl "Tabl ica zawi era n a st ępują c e l iczby: ") ; far (i nt i = O : i< samples->length : 1++) {
Console: ;Wnte(l "{0.10:F2}". senplesj t D : if «i +1 )%5 == O) Con sole ; 'Wrl t eli ne( ) ; Wewnątrz pętli określamy, że wartość każdego
elementu ma znajdować się w polu o szero10 i mieć dwa miejsca po przecinku . Dzięki określeniu szerokości pól warto ści zostaną
wyrównane w kolumnach . Za każdym razem, gdy wynikiem działania Ci + l) %5 jest zero, wysy' łamy znak nowego wiersza, co ma miejsce co pięć wartości , dzięki czemu w każdym wierszu mamy pięć warto ś ci . ko ści
Na
zakończenie
sprawdzamy
największą wartość:
double max ~ O; for each(doub le sample l n samples ) i f (max < samp le) max = sample;
for each, aby pokazać, że można jej tutaj użyć. max z wartością każdego elementu i za każdym razem, gdy znajdzie większą od niej, wartoś ć ma x jest ustawiana na tę właśnie wartość. W ten sposób otrzy-
W po wyższym Porównuje ona wartość
przykładzie użyłem pętli
warto ś ć
mujemy największą warto ść . Gdyby śmy to ść ,
to
chcieli jeszcze
zapisać po zycję
moglib yśm y posłuży ć się pętlą
indeksu elementu
zaw i eraj ącego n ajwiększą
war-
for. Na przykład :
doub le max = O: i nt i ndex ~ O; for (i nt i ~ O ; i < sample ->length ; i ++ ) i f( max < samples[iJ ) {
max ~ samples[ i J: i ndex = l ;
Sortowanie tablic jednowymiarowych Klasa Array w przestrzeni nazw System definiuje funkcj ę Sort() , która sortuje elementy tablicy jednowymiarowej w porządku rosnącym . Aby po sortować zawartość tablicy, wystarczy jako argument funkcji So rt O podać uchwyt do niej. Poni żej znajduje s ię przykład:
arrayA samples = { 27 . 3. 54. 11. IB. 2. 16}: Array; ;Sart(samples) ; for each(int value i n samp les) Conso le :Write(l "{O . B}" : va lue): Conso le: :Wr i t el i ne( ) ;
II Sortuj elementy tablicy . II Wyś wietl elementy tablicy .
238
VisIlai C++ 2005. Od podstaw Wywołanie kolejności.
2
3
funkcj i So rt ( ) spowodowało ustawienie elementów tablicy samp l es w Wynik wykonania powyższego fragmentu kodu jest następujący:
11
16
18
27
rosnącej
54
Podając dwa dodatkowe argumenty do funkcji So rt () , można ustawić w kolejnoś ci pewien zbiór wartości . Te argumenty to indeks pierwszego elementu ze zbioru do posortowania, a drugi to liczba kolejnych elementów. Na przykład:
arr ayA samp les = { 27. 3.54 . 11. 18. 2, 16}; Ar ray: :SortCsamp les . 2. 3): II Sortuj
elementy ze zb ioru
2 - 4.
instrukcja sortuje trzy elementy tablicy samp les, które zaczynają się od pozycji indeksowej 2. Po wykonaniu tych instrukcji elementy w tablicy będą miały następujące wartości :
Powyższa
27
3
11
18
54
2
16
Funkcja So rt () występuje w jeszcze kilku innych wersjach, o których możemy przeczytać w dokumentacji. Jedną z nich , szczególnie przydatną, wprowadzę teraz. Ta wersja funkcj i zakłada , że mamy dwie skojarzone tablice , tak ż e elementy w pierwszej z nich są kluczami odpowiadających im elementów w drugiej . Moglibyśmy na przykład przechowywać w jednej tablicy imiona osób , a w drugiej ich wagę. Funkcja So rt() sortuje tablicę names (zawierającą imiona) w kolejności rosnącej , a elementy tablicy wei ghts (zawierającej wagę) sortuje w taki sposób, aby nadal odpowiadały one kolejności pierwszej tablicy. Spójrzmy na przykład .
R!lmI!tmI Sortowanie dwóch skojarzonych ze sobą tablic Poniższy
kod tworzy tablicę imion oraz przechowuje wagę każdej osoby w elementach drugiej tablicy, odpowiadających danym osobom . Następn ie obie tablice zostają posortowane za pomocąjednej operacji:
#i nclude "st dafx.h" usi ng namespace Syst em; int mai nCarray<Syst em: :St ri ng A> Aargs) (
Ar ray; :Sort Cnames .wei ght s ) ; for eachCSt ri ngA name i n names ) Console: :Wr iteCL"{ O. 10)" . name) : Console: :WriteLinet ):
II Sortuj tablice. II Wyświetl imiona.
for eachC i nt wei ght i n we ights) Conso le: :Wr iteC L"{O. 10)". wei ghtl ; Console: :WriteL i neC) ; ret urn O;
II
Wyświetl
wagi.
Rozdział 4.•
Wynik
d ziałania powyższego
Al 176
Bill 180
Tablice, łańcuchy znaków i wskaźniki
programu przedstawia się Eve 115
J ill 103
Ma ry 128
239
następująco :
l ed 168
Jak to działa Wartości w tablicy weig hts odpowi adają wadze osoby zn aj dując ej s ię w elemencie o tym samym indeksie w tablicy names. Funkcja Sor t ( ), którą tutaj wywołuj em y, sortuje obie tablice za pomo c ą użytej jako argument pierwszej tablicy (w tym przyp adku names) w celu określenia kol ejności obu tablic. W wynikach działan ia programu widać, że każdej osobie nadal przypisana jest odpowiednia waga w drugi ej tabli cy.
PrzeszlIkiwanie lablicy jednowymiarowej W klasie Array do stępne s ą równi eż funkcje s łuż ąc e do wyszukiwania elementów w tablicach jednowymiarowych. Funkcja Bi narySearch() przeszukuje całą tablicę lub okre śloną jej czę ść w celu odnale zienia indeksu danego elementu za pom o c ą algorytmu binarnego. Alg orytm binarny wymaga, aby elementy, które mają zostać przeszukane, były posortowane, a w ięc przed u życ iem tej funkcji najpierw trzeba tablicę po sortować . Poniżs zy
kod przeszukuj e całą t ablicę :
a r ray< i nt >Ą
values = { 23. 45. 68. 94, 123, 127. 150, 203. 299}: int t oBeFound ~ 127: i nt pos i t i on = Ar ray: :Bi narySea rch(value s , to BeFound) : if ( pos it i on-D) Console : :WriteLine( L"Li czba {O} nie zo s ta ł a odnalez iona " , t oBeFound); el se Console : :Wr iteLine(L"L i czba {O} zost a ł a odnal ezio na w indeksi e ( l }. " . to BeFound, posi tion) :
Wartoś ć , którą chcemy zn a leźć, przechowywana jest w zmiennej t oBeFound. Pierwszym argumentem do funkcji BinarySearch( ) jest uchwyt do tablicy, którą chcemy prze szukać , a drugi argument okre śla, czego szukamy. Rezultat poszukiwania zwracany prze z funk cj ę BinarySearch( ) jest warto ś ci ą typu i nt . Je żeli w tablicy zostanie znaleziony drugi z podanych argument ów, to zwrócony zostanie jego indeks. W przeciwnym przypadku zwrócon a zostanie ujemna liczba całkowita. A zatem zwróconą wartoś ć trzeba sprawdzić w celu okre ś l e n i a, czy cel został odnaleziony. Jako że wartoś ci w tablicy valu es s ąjuż posortowane ro snąco , nie ma potrzeby sortować tablicy przed jej przeszuk aniem. P owyższy fragment kodu da następujący wynik :
Li czba 127
zo st a ł a
odnal ezi ona w indeksi e 5.
Aby przeszukać tylko ok reś lo ny zbiór elementów tablicy, n al eży użyć wersji funkcji Bi nary Searcht ), która przyjmuje cztery argumenty . Pierwszy argument to uchwyt tablicy do przeszukania, drugi to indeks, od którego ma s i ę rozpocząć przeszukiwanie, trzeci określa li czbę elementów, które chcemy przeszukać , a ostatni po szukiwaną wartość . Poni żej znajduje się przykład takiego przes zukiwania:
240
VisIlai C++ 2005. Od podslaw arr ay< int >A val ues = { 23, 45, 68 , 94 , 123, 127, 150, 20 3, 299}; int t oBeFound ~ 127; int posit ion = Array: :Bl narySearchCvalues , 3, 6, toBeFound): Powyż szy kod przesz ukuje wartośc i tablicy od czwartego elementu do osta tniego . Podobnie jak poprze dnia wersja, funkcja zwraca zna leziony indeks lub wartość uj em n ą, jeże li nic nie znajdzi e.
Omówmy przeszukiwan ie na przykładzie .
~ Przeszukiwanie tablic Poniżej
znajduje kiwaniem:
się
zmodyfikowa na wersja kodu z poprzedniego listingu z dodanym przeszu-
#include "stdaf x.h" using namespace Sys t em: int mai nC arr ay<Syst em : :St ri ng A> Aargs) array<St ringA/ names
{ "Jll l ", "Ted", "Ma ry", "Eve". "Bi ll ", "Al" , "Ned". "Zoe" : "Dan". "Jean"}: arrayAweight s = { 60, 112, 70 , 80, 120, 110. 125, 58 . 119. 74 }; array<StrlngA>AtoBeFound = {"Bi l l ". "Eve", "Al ". "F red" }; ~
Array. :Sort Cnames . weights);
II Sortuj tablice.
int result = O: for each CSt ringA name i n toBeFound)
II Zap isz wyni k szukania. II Szukaj wag .
{
result
=
Array: :BinarySearchCnames . name ):
II Przeszukaj
tab licę
nam es.
if Cresult A newNames = gcnew ar ray<St ri ngA>(names->Lengt h+l ): II Skop iuj elementy z tablicy names do newNam es
for(i nt i = O ; i <positi on : newNames[i ] = names[l]: newNames[positi on]
~
name:
i ++)
II Skopiuj nowy elem ent.
i f( posit ionLength ) II Jeż eli w tablicy nam es pozostały jakieś elementy. for(i nt i = pos i t ion : iLengt h ; i++) newNames[i +l] = names[i ] ; II skop iuj j e do tablicy new Names . Powyższy kod tworzy tablicę o jeden element większą niż stara tablica. Następnie kopiujemy wszystkie elementy ze starej tablicy do nowej do indeksu posit i on - 1. Następnie kopiujemy nowe imię, po którym umieszczone zostają pozostałe elementy starej tablicy . Aby usunąć starą tablicę, piszemy:
names = nul lpt r;
Tablice wielowymiarowe Możemy tworzyć
tablice wielowymiarowe. Maksymalnie tablica może być 32-wymiarowa , co powinno w zupełności wystarczyć do większości zasto sowań. Liczbę wymiarów tablicy podajemy w trójkątnych nawiasach bezpośrednio po typie elementu , oddzielając ją od niego przecinkiem . Domyślnie tablica ma jeden wymiar (dlatego do tej pory nie musieliśmy podawać liczby wymiarów). Poniżej znajduje się przykład utworzenia dwuwymiarowej tablicy elementów typu całkowitego:
arr ay Ą
val ues
~
gcnew array( 4. 5):
Powyższa instrukcja tworzy dwuwymiarową tablicę z czterema wierszami i pięcioma kolumnami, a więc w sumie złożoną z 20 elementów. Aby uzyskać dostęp do tablicy wielowymiarowej, należy podać odpowiednią liczbę indeksów - po jednym dla każdego wymiaru. Indeksy podaje s i ę w nawiasach kwadratowych , oddzielanych przecinkami i umieszczanych po nazwie tablicy. Poniższy przykładowy kod ustawia wartości elementów dwuwymiarowej tablicy liczb całkowitych :
i nt nrows ~ 4: i nt ncol s = 5; arrayA va lues = gcnew array(nrows . ncols) : for(i nt i ~ O : i A. Elementy przechowywane w tablicach są także obiektami typu St ri ng, a więc w ostatnim wyrażeniu musimy zamienić typ na St r i nq". Dzięki temu otrzymujemy tablicę typu array< ar ray < St r i ngA >A >A. Mając już tablicę przykładowy
tablic, możem y przejść do tworzenia tablic imion. kod do wykonania tego zadania:
Poniżej
przedstawiam
grades[O] = gcnew ar ray<Str ingA>{"Loui se " , "Jack"}: II Ocena 5. grades D ] = gcnew array<Str i ngA>{"Bi ll" , "Mary" , "Ben" , "Joan"}: II Ocena 4. grades[2] ~ gcnew arra Y<String A>{ "J i 11 " , oWi11 ", "Phi l "}: II Ocena 3. grades[3] = gcnew arraY<StringA>{" Ned". "Fred" . "Ted" , "Jed" : "Ed"} : II Ocena 2. grades[4 ] = gcnew ar ray<String A>{ "Dan " , "Ann"}: II Ocena l. Wyrażenie
gr ades [ n] uzyskuje do stęp do n-tego elementu tablicy gr ades i jest to oczywiza każdym razem uchwyt do tablicy uchwytów typu St r i nq". A zatem każdy z powyż szych pięciu wierszy tworzy tablicę uchwytów do obiektów typu St r i ng i zapisuje adres do jednego z elementów tablicy gr ades. Jak widać, tablice łańcuchów mają różne rozmiary , a więc w ten sposób możemy zarządzać zbiorem tablic o dowolnych rozmiarach. ście
Całą tablicę
tablic
można utworzyć
i za i n i cj a l i zo w ać za
pomocą pojedynczej
instrukcji:
array< array < String A >A >A grades = gcnew array< ar ray< Str i ng A >A > { gcnew ar ray<StringA>{ "Louise " , "Jack"}. II Ocena 5. gcnew array<StringA>{"Bill" , "Mary" , "Ben". "Joan"}. II Ocena 4. gcnew array<StringA>{"Jill ". "Wi ll " , "Phil" }, II Ocena 3. gcnew array<Str ingA>{" Ned". "Fred" . "Ted" , "Jed" : "Ed"}. II Ocena 2. gcnew array<Str i ngA>{" Dan ". "Ann" } II Ocena l . }: Wartości początkowe
Spójrzmy teraz na tablic.
elementów podane
przykładowy
są w
nawiasach klamrowych.
program, w którym
zaprezentuję
sposób przetwarzania tablic
~ Używanie tablic tablic Utwórz program konsolowy CLR i umieść w nim
następujący
kod
źró dłowy :
#i nclude "st dafx.h" usi ng namespace System: int mai n(array<System: :St ri ng A> Aargs ) ar ray< array< Stri ng A >A >A grades {
~
gcnew array < array< Stri ng A >A >
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
gcnew array<StringA>{" lo uise" . "Jack"}. gcnew array<St ringA>{"Bill ". "Mary" . "Ben" . "Joan"}. gcnew arraY<String A>{"Jill" . "Will" . "Ph il "}. gcnew array<Str i ngA>{"Ned" . "Fred" . "Ted " . "Jed" . "Ed "}. gcnew ar ray<Str i ngA>{"Dan". "Ann"}
247
II Ocena 5. II Ocena 4. II Ocena 3. II Ocena 2. II Ocena l .
}:
wchar_t gradel et t er = ' 5' : for each(array< St r i ngA >A grade in grades) {
Consol e : :Writeli ne( "Uczni owi e z
oce ną
{O} : " , gradeletter - - ):
for each( St rin gA st udent in grade) Conso l e : :Write( " {O.12} " . student ) :
II
Wyś lij b ieżące im ię.
Consol e : :Writel i net ) :
II
Wyś lij
nolry wiersz.
retur n O: Poni żej
znajduje się wynik
Uczniow ie z oceną 5 : l oui se Jack Uczniowie z oceną 4 : Bil l Mary Uczni owi e z oce ną 3: J i ll Will Uczniowie z o ce ną 2: Ned Fred Uczni owi e z oc en ą l : Dan Ann
dzi ałani a powyższego
Ben
programu:
Joan
Phil led
Ed
Jed
Jak lo działa Definicja tablicy jest identyczna, jak w idzieli śmy wcześniej . Nas t ęp n i e definiujemy zmienn ą typu wchar_t o nazwie gradeLetter o wartości początkowej 1. Po służy nam ona do prezentacji ocen na ekranie. Imiona uczniów oraz ich oceny wy świetlone zostały za p om oc ą for each iteruje przez elementy tablicy grades:
pętli zagn i eżdżonych .
Ze-
wnętrzn a pętla
for each(ar ray< St ri ngA >A grade in grades) { II Przetwarzaj uczniów z
b ieżącą oceną...
}
Zmienna pętlowa grade jest typu array< Stri ng >A, ponieważ taki jest typ elementów w tablicy grades. Zmienna grade wskazuje po kolei każdą tablicę uchwytów typu Str i nq", a więc w pierwszej iteracj i wskazuje tablicę uczniów z oceną 5, w drugiej tablicę uczniów z oceną 4 i tak dalej aż do tablicy przechowuj ącej imiona uczniów z oceną l . A
Przy każdej iteracji pętli
zewnętrznej
Console : :Wri te l i net "Uczni owi e z
wykonywany jest następujący kod :
o c eną
{O}: ". gradeletter- - ) :
248
Visual C++ 2005. Od podstaw A
for each( Stri ng student i n gr ade ) Consol e : :Write( "{O .12}".st udent) :
II
Wyślij bieżące imię.
Console : :Wr it eLt ner ):
II
Wyślij n 0 lry
wiersz.
Pierwsza instrukcja wysyła wiersz zawierający bieżącą wartość zmiennej gra del etter, która początkowo ma wartość 5. Wyrażenie to zmniejsza także wartość zmiennej gradelette r , dzięki czemu w kolejnych powtórzeniach przyjmuje ona po kolei wartośc i 4, 3, 2 i 1. Następn ie wewnętrzna pętla
fo r eac h iteruje po kolei przez wszystkie imiona bieżącej tablicy ocen. Instrukcja wyjściowa użyta w tym przypadku to Conso l e : :Write O , a więc wszystkie imiona pojawią się w tym samym wierszu. Imiona wyrównane są do prawej w polach o szerokości 12 znaków . Po pętli funkcja Wr i t el i ne () wysyła nowy wiersz w celu przenie sienia danych dotyczących następnej oceny do następnego wiersza.
Jako
pętli wewnętrznej mogli śmy również użyć pętli
for (i nt i = O : i < grade- >Length : i++) Console: :Write( "{O.12}".qrade l i l ) : Pętla
ograniczona jest przez gra de.
właściwość
II
le ngt h
fo r : Wyślij b ieżące im ię.
bieżąc ej
tablicy imion wska zywanej przez
zmienną
Jako pętli zewnętrznej również mogliśmy użyć pętli fo r . W tym przypadku potrzebne by dalsze zmiany w wewnętrznej pętli i wyglądałaby ona następująco:
były
for (int j = O : j < grades ->Lengt h : j ++) { Consol e : :Wri t eLi ne( "Uczni owe z oc e n ą (O} : ". gr adeLette r+j) : fo r (i nt i = O : i < grades[ j ] ->Lengt h : i ++) Consol e : :Wr i ter "{ O. 12}".grades [j] l i D: II Wyślij b ieżące imię. Consol e: :WriteLi ne ( ) :
Teraz gra des [ j ] wskazuje j w j tablicy imion .
tablicę
elementów, a więc
wyrażenie
grad es [ j] [ i ] wskazuje i
imię
~ańcuchY Jak już wiemy, typ klasowy St r i ng, który jest zdefiniowany w przestrzeni nazw System, w języku C++/CLI reprezentuje łańcuch znaków (w rzeczywisto ści łańcuch składa si ę ze znaków Unicode) . Mówi ąc dokładniej , reprezentuje łańcuch składający s i ę z sekwencji znaków typu System: :Char. Obiekty klasy St r i ng mają bardzo dużą funkcjonalno ść , dzięki czemu przetwarzanie ł ańcuchów jest niezwykle proste. Zacznijmy od tworzenia łańcucha. Obiekt klasy St r i ng można
utworzyć
w
System::S tri ng saying
L"Co dwi e
g łowy .
A
=
następujący
sposób:
to nie j edna. " :
Zmienna say i ng je st uchwytem śledzącym do obiektu klasy St r i ng, który zo stał zainicjalizowany łańcuchem po prawej stron ie znaku =. Wskaźn iki do obiektów klasy St r i ng zawsze przechowuje się w uchwytach śledzących. Podany w powyższym przykładzie l iterał łańcu-
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
249
chowy jest typu wi de characte r, ponieważ przed nim stoi litera L. Jeżel i nie podamy litery L, to otrzymamy literał łańcuchowy zaw ierający znaki ośm iobitowe , ale kompilator przekonwertujeje do łańcuchów typu wi de-character. Dostęp do poszczególnych znaków można uzyskać za pomocą indeksów, tak jak w tablicy . Pierws zy znak w łańcuchu ma indeks O. Poni ż sza instrukcja wysyła na wyjście trzeci znak
sayi ng:
łańcucha
Console: :WriteLi ne( "Trzecim znaki emw ła ń cu c hu jest {O)". sayi ng [2]); Pamiętajmy, że choć za pomocą wartości indeksowych można uzyskać dostęp do określonego znaku w łańcuchu , to nie mo żna jednak zmienić jego zaw arto ś ci . Obiekty klasy St ri ng są stałe i nie można ich modyfikować . Liczbę
znaków danego
gość łańcucha
sayi ng
za pomocąjego właściwośc i Length. Dłu za pomocą następującej instrukcji :
łańcucha można sprawdzić
możemy wyświetlić
Consol e : : W riteLi ne( " Ł a ńcuch ma {O} znaków . ". sayi ng->Length);
Jako że sayi ng jest uchwytem śledzącym (który - jak wiemy - jest rodzajem wskaźnika) , aby uzyskać dostęp do właściwości Lengt h (lub jakiejkolwiek innej składowej obiektu) , musimy użyć operatora - >. Więcej na temat właściwości dowiemy się przy okazji szczegółowego omawiania klas w C++/CLI.
~ączenie łańcuchów Do łączenia łańcuchów znaków i tworzenia nowych obiektów klasy Stri ng operatora +. Na przykład :
mo żemy używać
St ri ng namel = L"Beth": St ri ng name2 = L"Betty" ; St ri ng name3 = namel + L" i " + name2: A A A
Po wykonaniu tych instrukcji zmienna na me3 zawiera łańcuch Bet h i Betty. Zauważ, w jaki sposób można łączyć obiekty klasy Stri ng z literałami łańcuchowymi za pomocą operatora +. Łączyć można również obiekty klasy Stri ng z wartościami liczbowymi i logicznymi , które zostaną automatycznie przekonwertowane do łańcuchów przed operacją łąc zenia . Poniżej znajdują się instrukcje ilustrujące to zj awisko: Stri ng St ri ng St ri ng St ri ng
A A
A
A
st r = L"War t o ś ć : ": st rl = st r + 2.5 : II Nowy łańcu ch st r2 = st r + 25; II Nowy łańcu ch st r3 = st r + t rue: II Nowy łańcu ch
Można również połączyć łańcuch
"Wartość: "Wartość: "Wartość:
2.5". 25 ". True ".
typu Str ing ze znakiem, ale wynik
znaku: char ch = ' Z' : wchar t wch = ' Z' Stri ng st r4 = st r + ch: St ri ngA st r5 = st r + wch; A
II Nowy II Nowy
łańcuch "Wartość:
łańcuch "Wartość:
90 ". Z".
będzie zal eżny
od typu
250
Visual C++ 2005. Od podstaw W komentarzach podane zo stały wyniki ka żdej instrukcji. Znak typu char traktowany jest jako wartość liczbowa i dlatego do łańcucha zos tała dołączona liczba . Znak typu w_char jest tego samego typu co znaki w obiekcie klasy Stri ng (typ Char), a więc do łańcucha dołączony został znak . Pamiętajmy, że obiekty łańcuchowe są stałe . Nie można zmien iać ich zawartości po ich utworzeniu . Oznac za to, że w wyniku wszelkich operacji mających na celu z m i anę zawarto ści obiektów klasy St r i ng zawsze otrzymujemy nowy obiekt klasy Stri ng.
W klasie Str i ng zdefiniowana jest również funkcja jo i nt ), która służy do łączenia w jeden kilku łańcuchów przechowywanych w tablicy z uwzględnieniem znaków oddzielających poszczególne łańcuchy . Poniż sza instrukcja łączy w jeden łańcuch imiona, oddzielając je przecinkami: . ar ray- St r t nq" >" names = { "J i ll ". "Ted". "Ma ry". "Eve". "Bi ll "}: Stri ng A separat or = ". ": St ri ngA joined = St ri ng: :Joi nCseparator. names ) :
Po wykonaniu powyższych instrukcji zmienna joi ned zawiera łańcuch "J i 11 . Ted. Mary . Eve. Bi 11 ". Łańcuch separ ator został wstawiony pomiędzy każdą parą łańcuchów z tablicy names. Oczywiście łańcuch ten może być dowolny - może to być na przykład i i wtedy otrzymamy wynik "J ill i Ted i Mary i Eve i Bi ll ". Spójrzmy teraz na przykładowy program z zastosowaniem obiektów klasy St r i ng.
~ Praca złańcuchami Przypuśćmy , że mamy tablicę liczb całkowitych , której wartości chcemy zaprezentować wyrównane w kolumnach . Chcemy, aby wartości były wyrównane i aby kolumny były wystarczająco szerokie, ponieważ zależy nam na zmieszczeniu największych wartości tablicy włącz nie z przestrzenią pomiędzy kolumnami. Poniższy program odpowiada tym wymaganiom.
II Cw4 17.cpp: main projectfile. II Tworzenie własnego łańcucha formatu.
#i ncl ude "stdafx.h" using namespace Syst em: int mai nCa rray<System : :St ri ng A> Aargs) {
arrayA values = { 2. 456. 23. -46, 34211, 456, 5609, 112098. 234, -76504 . 341, 6788, -909121, 99, l O}; String A formatStrI = "{O,"; II Pierwsza poło wa łań cu cha fo rmatu. II Druga poło wa łańcucha fo rmatu. St ri ng A format Str2 = T ' : St ri ngA number : II Przechowywanie liczby jako łańcu cha. II Sprawdź długość
najdłuższego łańcucha.
i nt maxLengt h = O: for each Cint value i n va lues )
II Przechowuje największą znalezioną
{
number = "" + value: i f CmaxLengt hLengt h)
II Utwórz
łańcu ch
z
wa rtości.
liczbę.
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
251
maxLength = number->Length: II Utwórz
lańcuch formatu
do
użycia
przy
wysyłaniu
danych na
wyjście.
St ring format = formatSt rI + (maxLength+1) + forma tSt r2: A
II
Wyślij wartości.
int numberPerLine = 3: for(in t i = O : i< values ->Length : i++) (
Rezultat działania tego programu jest następujący : 2 456 -46 34211 5609 112098 -76504 341 -909121 99
23 456 234 6788 10
Jak lo działa Celem tego programu jest utworzenie łańcucha formatującego wyrównującego liczby całko wite z tablicy val ues w kolumnach o szerokości wystarczającej dla najdłuższej z nich. Łańcuch formatujący zaczynamy tworzyć w dwóch częściach: St ri ng formatSt rI = "(O. " : St ri ng formatSt r2 = "}" : A
A
II Pierwsza polowa lańcuchaformatującego. II Druga polowa lańcuchaformatującego.
Powyższe dwa łańcuchy stanowią początek
i koniec łańcucha formatującego, który chcemy na koniec. Aby go uzupełnić, musimy pomiędzy dwiema połowami formatStr I oraz format Str 2 umieścić długość najdłuższego łańcucha reprezentującego liczbę .
otrzymać
Tę wartość
odszukujemy za pomocą poniższego kodu:
int maxLength = O: for each( int value in values)
II Przechowuje najdluższą znalezioną
liczbę.
(
numbe r = "" + value: i f (maxLengt hLengt h) maxLength = numbe r->Length:
II Utwórz
lańcuch
z
wartości.
Wewnątrz pętli każda liczbakonwertowana jest do typu łańcuchowego poprzez dołączenie jej do pustego łańcucha. Właściwość Length każdego łańcucha porównujemy ze zmienną ma xLength i jeżeli dana wartość jest od niej większa, to zmienna maxLength przyjmuje tę właśnie wartość .
Tworzenie łańcucha St ri ng format A
~
formatującego
jest bardzo proste:
formatSt rI + (maxLengt h+1) + formatSt r2;
252
Visual C++ 2005. Od podstaw Do zmiennej maxLengt h musimy dodać l w celu utworzenia dodatkowego pola, kiedy wyświe tlany jest naj dłuższy łańcuch . Umieszczenie wyrażenia maxLength + l w nawiasach daje gwarancję, że zostanie ono obliczone jako wyrażenie arytmetyczne przed operacją łączenia łańcuchów.
Na
zakończenie wysyłamy
na
wyjście wartości
z tablicy za
pomocą łańcucha
format:
int numberPerLine ~ 3; for(int i = O ; i< va l ues->Lengt h : i++) {
Conso le : ;Write (format, val ues l t j):
i f « i+l )%numberPerLine == O) Consol e; ;WriteLine( ): Instrukcja wyjściowa w pętli używa łańcucha forma t jako łańcucha do wysyłania. Dzięki zmiennej maxLengt h w łańcuchu format dane są umieszczone w kolumnach o szerokości o jeden większej niż długość naj dłuższej z wysyłanych wartości . Zmienna numberPerl i ne określa, ile wartości pojawia się w jednym wierszu, dzięki czemu pętla jest dość elastyczna, gdyż pozwala na zmianę liczby kolumn poprzez zmianę wartości zmiennej numberPerli ne.
Modyfikowanie łańcuchów Najczęściej spotykaną operacją
iz
tyłu .
Do tego celu
służy
na łańcuchach jest obcinanie spacji funkcja Tr i m():
Str ing st r = {" Nie szat a zdobi Str ing newStr = st r->Trim(): A
cz łowi e k a. "
znajdujących się
z przodu
"l ;
A
Funkcja Tri m( ) w drugiej instrukcji usuwa wszystkie spacje z przodu i z tyłu łańcucha st r i zwraca wynik w postaci nowego obiektu klasy St ri ng przechowywanego w zmiennej newStr. Oczywiście, jeżeli nie chcemy zachowywać oryginalnego łańcucha, możemy wynik zapisać z powrotem do zmiennej st r. Istnieje także inna wersja funkcji Tri m(), która pozwala na określenie znaków do usunięcia z początku i końca łańcucha . Funkcja ta jest bardzo elastyczna, ponieważ umożliwia określe nie znaków do usunięcia na więcej niż jeden sposób. Znaki te można zapisać do tablicy i uchwyt do niej przekazać jako argument do funkcji :
Str ing toBeTrimmed = L " we ł n a we łna owca owca w eł n a we łna wełna" :
array<wchar_t / notWanted = {L 'w' ,L 'e ' .rr. L' n'. ia '. L' ' l : Console ; :Wr iteLine(to BeTrimmed->Trim(not Wanted» : A
mamy łańcuch o nazwie t oBeTri mmed, który zawiera owcę "przyTablica znaków do usunięcia z łańcucha została zdefiniowana pod nazwą notWante d, a więc przekazanie jej do funkcji Tri m( ) zastosowanej dla łańcucha spowoduje usunięcie z jego końca i początku wszystkich znaków w niej podanych. Pamiętaj, że obiekty klasy Stri ng są stałe , a więc oryginalny łańcuch nie zostanie w żaden sposób zmieniony - w wy. niku działania funkcji ' Tr i m( ) tworzony jest nowy łańcuch, który jest następnie zwracany. Wykonanie powyższego fragmentu kodu da następujący rezultat: W
powyższym przykładzie
krytą" wełną.
owca owca
Rozdzial4.• Tablice, łańcuchy znaków i wskaźniki
253
Jeżeli literały znakowe podalibyśmy bez towarzy szącego im przedrostka L, to byłyby one typu char (który odpowiada typowi klasy wartości SByte). Mimo to kompilator sam zadba o ich konwersję do typu wcha r_t .
Znaki do usunięcia przez funkcję Tri m() można także podać wprost jako argumenty tej funkcji. W związku z tym ostatni wiersz poprzedniego fragmentu kodu moglibyśmy zapisać następująco : Console: :W riteLineCtoBeTrimmed->TrimCL 'w' . i :«:
.L'ł'.
L'n '. L'a '. L' ' l ) ;
Kod ten da taki sam wynik jak poprzednia wersja tej instrukcji. Liczba argumentów typu wcha r_t jest dowolna, choć jeśli jest ich bardzo dużo , to lepiej zdefiniować je w tablicy . Jeżeli
chcemy usunąć znaki tylko z jednej strony łańcucha, to możemy użyć funkcji Tri mEnd() lub Tri mSta rtO. Funkcje te występują w takich samych wersjach jak funkcja Tr imt ), a więc jeżeli nie podamy żadnych argumentów, to usunięte zostaną spacje. Jeżeli jako argument podamy uchwyt do tablicy , to usunięte zostaną znaki w niej zdefiniowane. Znaki do usunięcia można również podać wprost jako argumenty typu wchar_t funkcji . Działaniem przeciwnym do usuwania znaków z łańcucha jest jego dopełnianie z obu stron spacjami lub innymi znakami. Dostępne są funkcje PadLeft O i PadRi ght O, które dopełniają łańcuch odpowiednio z lewej i prawej strony . Głównym zastosowaniem tych funkcji jest formatowanie wysyłanych na wyjście danych, gdy chcemy je wyrównać do prawej lub lewej strony w polach o ustalonej szerokości . Prostsze wersje funkcji PadLeft () oraz PadRi ght () akceptują pojedyncze argumenty określające długość łańcucha powstałego w wyniku operacji . Na przykład :
St ring value = L"3.142 " : St ri nq" l eftPadded ~ val ue->PadLeft 00) ; II Wynik lo" 3. 142 ". St ri ng r i ghtPadded ~ val ue->PadRi ght 00): II Wynik lo "3.142 n A
A
Jeżeli długość łańcucha,
oryginalnego
łańcucha,
podana jako argument funkcji, jest równa lub mniejsza niż długość to obie funkcje zwrócą nowy obiekt klasy Stri ng identyczny z ory-
ginałem .
Aby
dopełnić łańcuch
Poniżej
znajduje
St ring
A
value
się
znakiem innym niż spacja, należy go podać jako drugi argument funkcji. kilka przykładów takiego dopełniania :
L"3 .142": = val ue->PadLeftOO. L' * '); II Wynik lo "*****3.142". r i ghtPadded = value-> PadRightOO, L'# '): II Wynik lo "3.142##### ". ~
Strinq" leftPadded
St ring
A
Oczywiście w każdym z powyższych przykładów moglibyśmy zapisać powstały łańcuch z powrotem do uchwytu do oryginalnego łańcucha, co spowodowałoby usunięcie oryginalnego łańcucha.
W klasie Stri ng dostępne są także funkcje ToUpper() oraz ToLower() zamieniające wszystkie litery w łańcuchu na wielkie lub małe . Spójrzmy na przykładowy kod z ich wykorzystaniem : St r tnq" proverb ~ L "~.~ dwi e głowy. to nie je dna. ": St ring upper ~ prover b-c-Tolo oer j ): II Wynik: "CO DWIE GŁOWY, TO NIE JEDNA ". A
254
Visual C++ 2005. Od podstaw Funkcja t oUpper ( ) zwraca nowy literami wielkimi . Funkcji I nser t() łańcuchu. Poniżej
używamy do
znajduje
łańcuch,
który jest
kopią
oryginalnego, ale z wszystkimi
wstawiania łańcuchów w określone miejsce w zastosowania tej funkcji :
istniejącym j uż
się przykład
String proverb = L" Co dwie głowy. to nie jedna . '" Str ing newProverb = proverb ->Insert( B. L" mą d r e ") : A
A
Funkcja ta wstawia łańcuch podany jako drugi argument w miejscu, którego początek zostal w starym łańcuchu przez pierwszy argument. W wyniku działania tego kodu powstanie nowy łańcuch : określony
Co dwie
mą d r e g łowy.
to nie jedna.
Mo żna także
wszystkie wystąpienia jednego znaku zastąpić innym znakiem lub wszystkie jednego fragmentu łańcucha zastąpić innym fragmentem . W poniższym przykła dzie wykonywane są oba rodzaje operacji :
wystąpienia
Str ing proverb = L" Co dwie głowy . to nie jedna.": Console: :WriteLine(prove rb->Replace( L' '. L'*' » : Console : :WriteLi ne(proverb->Replace(L"Co dwie". L"Co t rzy"»: A
Wykonanie powyższego fragmentu kodu da
następujący
rezultat:
Co*dw ie*głowy *to * ni e* je dna.
Co t rzy gł owy. t o nie j edna. Pierwszy argument funkcji Repl ace() określa znak lub fragment łańcucha, który ma zastąpiony, a drugi argument to, co ma zostać wstawione w zamian .
zostać
Przeszukiwanie łańcuchów Jedną
z najprostszych operacji przeszukiwania łańcucha jest sprawdzenie, czy na jego końcu lub początku znajduje si ę określony fragment łańcucha . Służą do tego funkcje Start sWit h() oraz EndsW i th ( ). Do każdej z tych funkcji należy przekazać uchwyt do poszukiwanego łań cucha. Funkcje zwrócą wartość logiczną określającą, czy łańcuch został odnaleziony , czy nie. Poniżej znajduje się przykład użycia funkcji St ar t sW ith():
String sentence = L"Krowy to m ił e z w i erzę t a . ":
i f( sentence ->Sta rtsWi t h(L"Krowy"» Consol e: :WriteLine( "Zdanie rozpoczyna się s ło wem ' Krowy' ."): A
Wykonanie powyższego fragmentu kodu da następujący rezultat:
Zdanie rozpoczyna Oczywiście
si ę s łowem
do tego samego
' Krowy' .
łańcucha możemy zastosować funkcję
Console: :WriteLi ne( "Zdan i e {Oj
k o ń c zy si ę słowem
EndsWi t h( ) :
' zw i e r z ę t a '. ".
sentence->EndsWith(L"zwi e rz ę t a " ) ? L"" : L" nie"):
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
255
Do łańcucha wyjściowego wstawiony został wynik wyrażenia z u życiem operatora warunkowego. Jeżeli funkcja EndsWit h( ) zwróci warto ść true, to wstawiony zostanie pusty łańcuch , a jeżeli fa l se, to wstawiony zostanie łańcuch ni e. W tym przypadku funkcja zwróci wartość fa l se (ponieważ na końcu łań cucha znajduje s i ę jeszcze kropka) . Funkcja IndexOf( ) przeszukuje łańcuch w celu odnalezienia pierwszego wystąpienia określo nego znaku lub łańcucha oraz zwraca indeks, jeżeli go odnajdzie, lub - I w przeciwnym przypadku . Poszukiwany znak lub łańcuch określamy jako argument funkcji . Na przykład :
String sentence = L"Krowy to faj ne zw i er z ęt a . " : i nt ePos it ion = sent ence ->IndexOf< L'w' ): II Zwraca 3. int thePosition = sentence- >IndexOfIndexOf (word.index)) >= Q)
we ł n a" :
A
{
i ndex += word->Length ; ++count : } Cons o l e :; W riteLin e( L "S ł owo
' {Q}'
zosta ło
znalezione {l} razy w:\ n{2}". word. count.
words) ; Powyższy fragment kodu liczy, ile razy w łańcuchu words wystąpiło słowo weł na. Operacja poszukiwania znajduje się w warunku pętli whi l e, a wynik przechowywany jest w zmiennej i ndex. Pętla powtarza się , dopóki zmienna i ndex nie ma wartości ujemnej, czyli aż do momentu, kiedy funkcja IndexOf () zwróci -l . Wartość zmiennej i ndex zw iększana jest wewnątrz ciała pętli o długość słowa wo rd, co powoduje przesunięcie pozycji indeksu do znaku znajdującego się po znalezionym słowie i pętla gotowa jest do nowej iteracji. Zmienna count wewnątrz pętli jest zwiększana za każdym razem, kied y odnajdywane jest szukane słowo , dzięki czemu przechowuje ona liczbę odnalezionych słów word w łańcuchu words. Wykonanie powyższego fragmentu kodu da następujący rezultat: S łowo 'weł n a ' zosta ło weł n a wełna
owca owca
zna lezione 5 razy w: we łn a we łna wełn a
Funkcja LastlndexOf ( ) jest podobna do funkcji IndexOf ( ), z tym wyjątkiem , ż e rozpoczyna przeszukiwanie od tyłu lub wstecz od okre ślonego indeksu. Poniższy kod wykonuje tę samą czynność co poprzedni za pomocą funkcj i LastI ndexOf ( ):
int i ndex = words->Length - l : int count = Q; whil e(index >= Q && (i ndex ~ words->La st lndexOf (word.index)) >= Q)
256
Visual C++ 2005. Od podslaw
--in dex: ++count : Przy użyciu tych samych łańcuchów word i wo r ds co wcześniej , kod da identyczny rezultat. Jako że funkcja LastI ndexOf( l przeszukuje wstecz , to indeks , od którego ma zacząć przeszukiwanie, określa ostatni znak łańcucha - wo rds->Length-1. Kiedy zostaje znalezione słowo word, zmniejszamy i ndex o jeden, dzięki czemu następne poszukiwanie rozpocznie się od znaku poprzedzającego bieżące słowo wo rd. Jeżeli poszukiwane słowo znajduje się na samym początku łańcucha wo rds (w pozycji indeksowej O), zmniejszenie wartości zmiennej i ndex spowoduje ustawienie jej na wartość -1. Taka wartość nie jest prawidłowym argumentem funkcji LastI ndexOf, ponieważ przeszukiwanie zawsze musi rozpoczynać się od jakiegoś miejsca w łańcuchu . Dodatkowe sprawdzanie wartości ujemnych zmiennej i ndex w warunku pętli zapobiega wystąpieniu takiej sytuacji . Jeżeli prawy operand operatora && ma wartość f al se, to wartość lewego nie jest sprawdzana. Ostatnia funkcja przeszukiwania , o której chcę napisać, to funkcja IndexOfAny(). Przeszukuje ona łańcuch w celu odnalezienia pierwszego miejsca wystąpienia jakiegokolwiek znaku w tablicy typu array<wchar_t >, którą podajemy jako argument. Podobnie jak funkcja I ndexOf ( l, funkcja IndexOfAny( l może rozpocząć przeszukiwanie łańcucha od samego początku lub od określonego miejsca. Poniżej znajduje się przykładowy program z wykorzystaniem funkcji IndexOfAny ( l .
RImmJI Poszukiwanie jednego zkilku znaków . Poniższy program poszukuje znaków interpunkcyjnych w
ciągu :
#include "st dafx.h" using namespace System: int main(ar ray<System: :String A> Aargs) {
array<wchar_t / punctuation = {L "". L'v'. L'. ' , L' , ' , L' :'. L': ' , L' ! ', L'? ' j; StringA sentence ~ L"\ "Zimno tu\ ", chłodno powied ziała matka do synka.": II Utwórz
tablicę
spa cji o takiej samej
długości jak zdani e.
array<wcha r_t>A indicators = gcnew ar ray<wcha r_t>(sentence->Length){L' 'j; i nt i ndex ~ O: II Liczba znalezionych znaków . i nt count ~ O: II Liczba znaków interpunkcyjnych. whi le((index = sentence->IndexOfAny(punctuation, index)) >= O) {
indicators[index] = L' A' ; ++index; ++count:
II Ustaw znacznik. II Zwiększ do następnego znaku. II Zwiększ licznik.
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
257
Console : :WriteL i ne(L"W ła ń c u c hu s ą {O} znaki inte rpunkcyj ne:" , count): Consol e: :WriteL i ner L" \n{O}\n{l }", sentence. gcnew Stri ng (i ndicators )) ; ret urn O; Wyn ik działania powyższego programu powinien
być następujący:
Wł a ńc uch u są 4 znaki i nterpunkcyjne : "Zimno tu", chło d no powie dzi ał a matka do synka, A
Jak to działa Najpierw tworzymy
tablicę
znaków do odnalezienia oraz łańcuch, który chcemy
przeszukać :
array<wchar_t / punctuatio n ~ {L"" , L'\", L' , ', L' ,' , L' ; ' , L';' , L' ! ' , L'? '}; Stri ng A sentence = L"\ "Zimno tu\ ", ch ło d no powiedzi a ł a matka do synk a,"; Zauważmy , że
znak pojedynczego cudzysłowu musieliśmy zapisać za pomocą kodu znaku specjalnego, gdyż pełni on rolę ogranicznika literałów znakowych. W literałach znakowych możemy stosować podwójne cudzysłowy bez kodowania , ponieważ nie ma ryzyka, że w tym kontekście zostaną one zinterpretowane jako znaki oddzielające . Następnie
definiujemy tablicę znaków, której elementy zostały zainicjalizowane znakiem spacji:
array<wchar_t>A i ndicators = gcnew arr ay<wcha r_t >(sent ence->Lengt h){L , ' }; Tablica ta ma tyle elementów, ile znaków jest w łańcuchu sent ence. Tablicy tej w danych wyjściowych będziemy używać do zaznaczania, gdzie w łańcuchu sent ence znajdują się znaki interpunkcyjne. Za każdym razem, gdy zostanie odnaleziony znak interpunkcyjny, odpowiedni element tablicy przyjmuje wartość Zauważ, że pojedynczy inicjalizator umieszczony w nawiasach klamrowych po specyfikacji tablicy może być używany do inicjalizowania wszystkich elementów tablicy . A .
Poszukiwanie odbywa
się
w pętli whi l e:
while((i ndex = sente nce ->IndexOfAny(punctuation , i ndex)) >= O) {
indicators[i ndex] = L' A'; ++i ndex; ++count ;
II Ustaw znacznik. II Zwiększ do następn ego znaku. II Zwiększ licznik.
Warunek pętli jest w zasadzie taki sam jak w poprzednich fragmentach kodu. Wewnątrz pętli elementu znajdującego się w miejscu i ndex w tablicy i ndi cat ors jest zmieniana na przed zw i ększen i em indeksu przed następną iteracją. Po zakończeniu pętli zmienna count zawiera liczbę znalezionych znaków interpunkcyjnych, a tablica i ndi cat ors zawiera znaki w tych miejscach, gdzie zostały one znalezione.
wartość
A
A
Dane na wyjśc ie
wysyłane są za pomocą następujących
instrukcji :
Console : :Wri t eLine(L" Wł a ńcuc hu są {O} zna ki i nte rpunkcyj ne:", count) ; Console; ;Wri t eLinerL"\n{O} \ n{ l }" sente nce , gcnew St ri ng(i nd i cators )) ;
258
Visual C++ 2005. Od podstaw Druga z powyższych instrukcji tworzy nowy obiekt klasy St ri ng na stercie z tablicy i ndicatars poprzez przekazanie tablicy do konstruktora klasy St ri nq. Konstruktor klasy to funkcja tworząca nowy obiekt klasy w momencie jego wywołania. Więcej na temat konstruktorów dowiesz się , kiedy dojdziemy do definiowania własnych klas .
Relerencie Śledzące Referencje śledzące są podobne do referencji w natywnym c ++ pod tym względem , że stanowią alias czego ś znajdującego się na stercie. Można je tworzyć do typów wartości na stosie i do uchwytów na oczyszczonej stercie. Same referencje śledzące zawsze tworzone są na stosie, a ich zawartość jest automatycznie uaktualniana, gdy obiekt przez nie wskazywany zostanie przesunięty przez mechanizm usuwający nieużytki. Referencję śledzącą definiujemy
śledząc ą do
typu
za
pomocą
operatora %.
Poniższy przykład
tworzy
referencj ę
wartości :
int value = lO: int% trac kValue = value: Druga z powyższych instrukcji definiuje referencję ś led zącą o nazwie t rackVa l ue do zmiennej va l ue, która zo stała utworzona na stosie. Za pomocą referencj i trackVal ue możemy teraz modyfikować zmienną va l ue:
trac kValue *= 5: Consol e: :WriteL i ne( va l ue): Jako że referencja śledząca trackVa l ue je st aliasem zmiennej va l ue, druga z strukcji zwróci warto ś ć 50.
powyższych
in-
Wskaźniki wewnętrzne Mimo że adresów przechowywanych przez uchwyty śledzące nie można używać w działaniach arytmetycznych, w języku C++/CLl istnieje rodz aj wskaźnika , który do tego celu może być używany . Nazywa się on wskaźnikiem wewnętrznym i definiuje się go za pomocą słowa kluczowego i nteri ar_pt r. Adres przez niego przechowywany może być w razie potrzebyautomatycznie aktualizowany przez system zbierający nieużytki. Wskaźnikiem wewnętrznym jest zawsze zmienna automatyczna o zasięgu lokalnym w funkcji . Poniżej znajduje się przykładowa definicja pierwszego elementu tablicy:
wskaźnika wewnętrznego zawierającego
adres
array<double>Adata = {l.5 . 3.5. 6.7. 4.2. 2.l }: interior ptr<doub le> pstart = &data [OJ: Obiekt wskazywany przez wskaźnik wewnętrzny podajemy w nawiasach trójkątnych po słowie kluczowym i nteri ar_pt r. W drugiej z powyższych instrukcji wskaźnik wewnętrzny inicjalizujemy adresem pierwszego elementu tablicy za pomocą operatora &, podobnie jak w przypadku wskaźników w natywnym C++. Jeżeli nie podamy wartości początkowej wskaźnika,
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
259
to domyślnie zostanie on zainicjalizowany wartością nu ll ptr. Pamięć dla tablicy jest zawsze przydzielana na stercie , a więc jest to sytuacja, w której system usuwania nieużytków może dostosować adres zawarty we wskaźniku wewnętrznym . Istnieją
typu wskaźnika wewnętrznego . Wskaźnik weadres obiektu klasy wartości na stosie lub adres uchwytu do obiektu na stercie CLR. Nie może zawierać adresu całego obiektu na stercie CLR. Wskaźnik wewnętrz ny może także wskazywać natywny obiekt klasy lub wskaźnik natywny. ograniczenia
dotyczące okreś lania
wnętrzny może zawierać
Wskaźnika wewnętrznego można również użyć
do przechowywania adres u obiektu klasy na stercie, takiego j ak np. elementu tab licy CLR . W ten sposób możemy utworzyć wskaźnik wewnętrzny przec h owuj ący adres uchwytu ś l edzące go do obiektu System: :Stri ng, ale nie możemy utworzyć wskaźnika wewnętrznego do przechowywania adresu samego obiektu klasy Stri ng. Na przykład : wartości ,
który jest
częścią obiektu
inte ri ar_pt r<St ri nq" > pst r-l : interiar ptr<Stri nq> pst r2;
II OK - wskaźn ik do uchwytu . II Nie skomp iluje s ię - wskaźn ik obiektu String.
Z zasto sowaniem wskaźnika wewnętrznego można wykonywać takie same działania arytmetyczne co ze wskaźnikami natywnymi. Można także użyć operatora inkrementacji lub dekrementacji w celu zmiany zawartego w nim adres u na poprzedni lub następny element. Moż na również dodawać i odejmować liczby całkowite oraz porównywać w skaźniki wewnętrzne . Spójrzmy na przykład z zasto sowaniem tego , co zosta ło opisane.
liIIlmmUI Tworzenie i używanie wskaźników wewnętrznych Poniższy program jest ćwiczeniem zastosowania liczbowymi i łań cuch am i .
wskaźników wewnętrznych
#include "st dafx.h" using namespace Syste m; int main(arr ay<Syst em ; ;St ring A> Aargs ) { II Uzyskaj
dos tęp
do elementów tablicy za pomocą
wskaźn ika .
arr ayA data = {1 .5. 3.5. 6.7. 4.2. 2.l} ; inter iar_ptr pstart = &da t a[O J; inte ria r_pt r pend ~ &data[dat a->Length lJ; dauble sum= 0.0; whi le (pstart Ast rings = { L"Ahaj . widać l ą d ' '' .
L"Wychyl kielich a'''.
z
warto ściami
260
Visual C++ 2005. Od podstaw L"Nie trzę ś S i ę l ".
L" Nie rzucaj s łów na wiatr l "
l: for(interior_ptr<String pstrings = &strings[OJ pst rings- &strings[OJ < st ri ngs->Lengt h Consol e; ;WriteL i ne(*pstri ngs l; A>
++pstringsl
return O; Rezultat
działania tego
programu jest
następujący:
Łączna sum a elementów tabli cy dat a = 18 Ahoj , wid a ć lą d!
W ychyl kie l icha l Nie t r z ęś S i ę l
Nie rzucaj s łów na wiatr !
Jak to działa Po utworzeniu tablicy data
zawierającej
elementy typu doubl e definiujemy dwa
wskaźniki
wewnętrzne :
interior_ptr<doub le> pstart = &dat a[OJ; interior_ptr<double> pend ~ &dat a[dat a->Lengt h - 1J; Pierwsza z powyższych instrukcji tworzy wskaźnik pstart do typu doubl e i inicjalizuje go adresem pierwszego elementu tablicy - data[O]. Wskaźnik pend został zainicjalizowany adresem ostatniego elementu tablicy - dat a[dat a->Lengt h - 1]. Jako że wyrażenie dat a->Length oznacza liczbę elementów tablicy, odjęcie jeden od jego wartości daje w wyniku indeks ostatniego jej elementu. Pętla
whi le oblicza sumę elementów tablicy:
whil eCpstart pend.
pst art zawiera adres pierwszego elementu tablicy. Warpierwszego elementu uzyskujemy poprzez wyłuskanie wskaźnika za pomocą wyrażeni a *pstart, a jego wynik dodajemy do zmiennej sum. Następnie adres we wskaźniku jest zwięk szany o jeden za pomocą operatora H . Przy ostatniej iteracji pętli wskaźnik pstart zawiera adres ostatniego elementu, czyli taki sam jak wskaźnik pend. W związku z tym zwiększenie wskaźnika pstart sprawia, że warunek pętli daje wynik fa l se, ponieważ wskaźnik pst art stał się większy niż pend. Po zakończeniu działania pętli wartość zmiennej sumwysłana zostaje na wyjście, dzięki czemu mamy potwierdzenie, że pętla whi l e działa tak, jak powinna. Na
początku działania pętli wskaźnik
tość
Rozdział 4.• Następnie
tworzymy
tablicę
czterech
Tablice. łańcuchy znaków i wskaźniki
261
łańcuchów :
array<Stri ng A>A stri ngs = { l "Ahoj , w id a ć ląd ! ",
l "Wychyl kielicha !", LO Nie trzęś Sięl ",
LONie rzucaj słów na wiatr !" }; Pętla
z tych łańcuchów do wiersza poleceń : forCi nterior_ptr<String A> pstrings ~ &st ri ngs[ O] ; pst ri ngs-&strings [O] < stri ngs->length , ++pstrings) Consol e; :Wri tel i neC*pstri ngs); fo r
wysyła każdy
Pierwsze wyrażenie w warunku pętli for deklaruje wskaźnik wewnętrzny pst ri ngs i inicjaIizuje go adresem pierwszego elementu tablicy st ri ngs. Drugie wyrażenie , które decyduje , czy pętla kontynuuje dz iałanie , to: pstri ngs-&strings[O] < stri ngs ->length
Dopóki wskaźnik pst ri ngs zawiera adres prawidłowego elementu tablicy, różnica pomiędzy tym adresem a adresem pierwszego elementu w tablicy jest mniejs za ni ż liczba elementów w tablicy zwrócona przez wyrażen ie st ri ngs- >Length. Kiedy różnica ta jest równa liczbie elementów w tablicy, działanie pętli zostaje zakoń czone . Z danych wyjściowych wynika, że wszystko działa zgodnie z przewidywaniami. Wskaźników wewnętrznych naj c zęściej używa s i ę
obiektów na stercie CLR. Więcej na ten temat
do wskazywania obiektów, które są c zęścią w dalszej częśc i ks iążk i .
będz iemy mówić
Podsumowanie Znamy już wszystkie podstawowe typy wartości w C++, potrafimy tworzyć tablice tych typów i ich używać oraz tworzyć wskaźn ik i i z nich korzystać . Wspomnieli śmy także o referencjach. Oczywiście , nie powiedzieliśmy jeszcze wszystkiego na te tematy. Do tematów tablic, wskaźników i referencji wrócimy jeszcze w dalszej części książki . Poniżej znajduje się lista najważniejszych zagadnień poruszonych w tym rozdziale : •
Dzięki
zbiorem danych tego samego typu, posługując wymiar tablicy definiowany jest pomiędzy nawiasami kwadratowymi po nazwie tablicy w jej deklaracji . tablicom
możemy zarządzać
się prostą pojedynczą nazwą. Każdy
•
Każdy wymiar tablicy indeksowany jest od zera. W zwi ązku z tym tablicy ma indeks cztery .
piąty
•
Tablice można inicjalizować, wstaw i aj ąc w deklaracji umieszczone pom iędzy nawiasami klamrowymi .
•
Wskaźnik je st typem zmiennej , która zawiera adres innej zmiennej . Wskaźniki deklaruje się jako "wskaźn i k i do typu " i mo żna im przypisywać tylko adresy zmiennych o podanym typie.
element
wartości początkowe
262
Visual C++ 2005. Od podstaw •
Wskaźnik może wskazywać obiekt s tały . Można go ponownie przypisać do innego obiektu. Wskaźnik można również zdefiniować jako eonst i w takim przypadku nie można go ponownie przypisać .
•
Referencja jest aliasem zmiennej i może być używana w tych samychmiejscach co zmienna, którą wskazuje. Referencja musi zostać zainicjalizowana w momencie deklaracji .
•
Raz przypisanej referencji nie
•
Operator s i zeof zwraca liczbę bajtów zajmowanych przez obiekt podany jako jego argument. Jego argumentem może być zmienna lub nazwa typu otoczona nawiasami okrągłymi.
•
Operator new dynamicznie przydziela pamięć w wolnej przestrzeni w programach w natywnym C++. Po przydzieleniu pamięci zwraca wskaźnik do początku przydzielonego obszaru. Jeżeli pamięć z jakiegoś powodu nie może zostać przydz ielona, powstaje wyjątek i program zostaje zamknięty .
można przypisać
do innej zmiennej .
Mechanizm działania wskaźnika może być czasami trudny do zrozumienia, gdyż operuje on na różnych poziomach w jednym programie. Czasami działa jako adres, a czasami może działać z warto ściami przechowywanymi pod danym adresem . Bardzo ważne jest, aby dobrze zrozumieć i stotę tego mechanizmu, a więc mając jakiekolwiek problemy ze zrozumieniem sposobu działania w skaźników , należy przećwiczyć ich użycie na kilku przykładach , aby sprawnie się nimi posług iwać . Najważniejsze
zagadnienia, których
nauczyliśmy się
o programowaniu dla CLR, to:
•
W programach dla CLR pamięć przydzielana jest na oczyszczonej stercie za pomocą operatora genew.
•
Obiektom klasy referencji , a w szczególności obiektom klasy St ri ng pamięć
przydzielana jest zawsze na stercie CLR .
•
Pracując
•
CLR posiada własne typy tablicowe po siadające tablicowe w natywnym C++.
•
Tablice CLR zawsze
•
Uchwyt śledzący jest typem wskaźnika używanego do wskazywania zmiennych zdefiniowanych na stercie CLR. Uchwyt śledzący jest automatycznie aktualizowany, jeżeli to, do czego się odwołuje , zo stało przenie sione przez mechanizm usuwania
w programach CLR, używamy obiektów klasy Str i ng.
są tworzone
większą funkcjonalność niż
typy
na stercie CLR.
nieużytków .
•
Zmienne
odnoszące s i ę
do obiektów i tablic na stercie
są
zaw sze uchw ytami
śledzącymi .
•
Referencja śledząca podobna jest do referencji w natywnyrn C++, z tym wyjątkiem, że zawarty w niej adres jest automatycznie aktualizowany, j eż e l i obiekt przez nią wskazywany zostanie przeniesiony przez mechanizm usuwania nieużytków .
•
Wskaźnik wewnętrzny to typ wska źnika -C f-f-/Cl.L Można go stosować
do wykonywania tych samych operacji, które wykonuje się za pomocą
wskaźnika natywnego.
Rozdział 4.•
•
Tablice. łańcuchy znaków i wskaźniki
263
Adres zawarty we wskaźniku wewnętrznym można modyfikować za pomocą działań arytmetycznych i nadal utrzymać poprawny adres, nawet o dnosząc się do czego ś przechowywanego na sterc ie CLR .
Ćwiczenia Kod źródłowy wszystkich przykładów w tej książce oraz rozwiązania do ćwiczeń ze strony www.helion.pl.
można pobrać
1. Napisz program w natywn ym C++ pozwalający na podanie dowolnego zbioru liczb , które będą przechowywane w tab licy ulokowanej w obszarze pamięci wolnej . Program powinien wysyłać na wyjście po p i ę ć z podanych liczb, a na końcu podawać" ich średnią. Początkowo tablica powinna mieć rozmiar pięciu elementów. W razie potrzeby program powinien tworzyć nową tab licę z dodatkowymi pięcioma elementami oraz ko piować wartośc i ze starej tablicy do nowej .
2. Powtórz poprzednie
a.
Zadeklaruj
tab licę
pętli zamień
co
ćwiczenie,
ale z użyciem notacji
wskaźnikowej
znaków i zainicjalizuj ją do odpowiedniego na wie lką.
zamiast tablic.
łańcucha .
Za pomocą
drugą l i terę
Wskazówka: w zestawie znaków ASC II wartości wielkich liter są o 32 mniejsze niż ich małych odpowiedników .
4. Napisz program w C++/CLI tworzący tablicę zawierającą l osową l i czbę elementów typu i nt . Tablica powinna zawierać nie mniej niż 10 i nie więcej niż 20 elementów. Wartości elementów powinny być ta k że losowe i zawierać się w zbiorze od 100 do 1000. Zawartość eleme ntów wyświet l w porządku ma lejącym bez sortowania tablicy. Na przykład znaj dź najmniej szy element i go wyświet l, następnie kolejny najmniejszy itd.
Il. Napisz program w C++/CLI
generujący losową li czbę całkowitą większą od 10 000. na ekranie, a n astęp ni e wyświet l słowne odpowiedniki poszczególnych cyfr. Je że li na przy kład program wygenerował liczbę 345 678 , to wynik powinien być następujący : Wyświet l tę liczbę
Wartość w ygenerowa na to: 345678 t rzy cztery pi ęć s ześć sie dem osiem
8. Napisz program w C++/CLI, który stworzy tablicę
zawierającą następujące łańcuchy :
" Ko by ł a ma ma ł y bok ." "Kawa i wuzetka to zestaw obo wi ązko wy.
" Jeż leje lwa. paw leje l ż e j . "
"Ala ma kota. kot ma A lę. "
" P ę t a k a pętaj. a tępaka tęp . "
Program powinien przeanalizować po kolei wszystkie ł ań cu ch y i wyświetlić je z i nformacją, czy są one palindromami, czy nie (tzn. czy nie ma różnicy , czy s ię je czyta od przodu, czy od ty łu, pomij ając znaki interpu nkcyj ne).
264
Visual C++ 2005. Od podstaw
5 Wprowadzanie struktury do programu Do tej pory nie mieliśmy możliwości nadania naszym programom struktury modularnej, kod zawierał s i ę w pojedynczej funkcji mai n(), choć używali śmy j uż różnego rodzaju funkcj i bibliotecznych, a także funkcji należących do obiektów. Pisząc program w C++, należy za każdym razem od samego początku myśleć o jego modularnej budowie. Jak się niebawem przekon asz, dobre opanowanie technik imple mentacji funkcji jest niezbędne w programowaniu zorientowanym obiek towo w C++. W rozdziale tym nauczysz się:
ponieważ cały
•
Dekl arować
i pisać
własne
•
Definiować
argument y funkcji i ich
•
Przekazywać
tablice do i z funkcji .
•
Przekazywać
przez
•
Jak
•
Jak używ a ć referencj i jako argumentów funkcji oraz co oznacza przekazywanie przez wartość .
•
Jaki wpływ ma modyfikator const na argumenty funkcji .
•
Jak
•
S tosować rekurencję .
funkcje w C++. używać .
wartość .
przekazywać wskaźn iki
zwracać wartości
do funkcji .
z funkcj i.
Tematyka struktury programów w C++ jest bardzo szeroka, a więc aby nie n abawić się niestrawności , nie będziemy próbowa li połknąć wszystkiego za jednym razem. Po zapoznaniu się z podstawowymi zagadnieniami przejdziemy do n a s tępne go ro zdziału , w którym zagłębimy się w tę tematykę jeszcze bardziej . .
266
Visual C++ 2005. Od podstaw
Zrozumieć
lunkcje
Spójrzmy najpi erw na ogó l ną zas adę dział an i a funkcji . Funkcja jest blokiem kodu przeznaczonymdo wyko nywania okreś lo nego zadania. Ma ona nazwę , która j ą identyfikuje, a zarazem s łuży do j ej wywo ływan ia w programie. Nazwa funk cji jest globalna, ale nie musi być unikalna, o czym przekonamy s ię w n astępnym rozdziale. Ogólnie mó wi ąc , funkcje wykonuj ąc e ró żn e czyn nośc i powinn y mi e ć inne nazwy. Nazew nictwem funkcji rządzą te same zasady co nazewnictwem zmiennych. Tak więc n azwę funkcji stanowi sekwencja liter i cyfr. Pierwsza musi być litera, z tym że znak podkreśl en i a ró wni eż uznawany jest za lit erę. Nazwa funkcji powinna o dzwie rc ie d lać jej przeznaczenie. Na przykł ad fu n kcję licząc ą fasolki m ożna by był o n azw ać l i cz_fasol ki (). Informacje do funkcji przekazujemy za pomocą argumentów podawanych przy wywoływani u funkcji . Argumenty mu s zą odpow i adać parametrom, które z naj d ują się w defini cji funkcji. Podane argumenty zas tęp ują parametry używane w definicji funkcji , kiedy jest ona wykonywana. Wtedy kod takiej funkcji wykon ywany jest w taki sposób, jakby został napisany przy u życiu naszych argumentów . Na rysunku 5.1 widać , jaki jest zw iązek p om iędzy argumentami wywo ła nia funkcji a param etrami w jej defini cji .
Rysunek 5.1
Argument y
(out « add _ints( 2 , 3 );
II Warto ści
argum ent ów zastę p ują w definicji funkcji o d powi a dając e im paramet ry
lI
int add_ints( int i, int j ) Definiqa funkcji
'-----
-
Zwrócona
t
~
} T ----.. .,I return i + j ;
{
Parametry
I
wa rtość ~ 5
W powyższym przykładzi e funkcja zwraca sumę obu przekazanych do niej argumentów. Funkcja zwraca poj edynczą warto ś ć w miej scu, w którym zos tała wywołan a, lub nic nie zwraca, w zależności od tego, jak zos tała zdefiniowana. M oże s ię wydawać, że zwracanie pojedynczej wa rtośc i jest dużym ograniczeniem , ale wa rto ść ta może być na przykład ws kaźn i kiem zawieraj ącym adres tablicy. Więcej na temat zwracania danych z funkcji powiemy w dalszej częśc i tego rozdziału .
Rozdzial5.• Wprowadzanie struktury do programu
267
Do czego potrzebne są funkcje Jedną z głównych zalet funkcji jest to, że mogą one być wykonywane dowolną liczbę razy w różnych miejscach programu. Gdyby nie mo żliwo ść pakowania bloków kodu do funkcji , programy byłyby znacznie dłuższe, gdyż ten sam fragment kodu trzeba by było wielokrotnie powtarzać. Ale prawdziwym powodem tworzenia funkcji jest rozbicie programu na czę ści, którymi można z łatwości ą zarządzać , rozwijać je i testować . Wyobraźmy sobie naprawdę duży program - złożo ny z miliona wierszy kodu . Na pisanie programu takich rozmiarów bez funkcji byłoby niemożliwe. Funkcje pozwalają na tworzenie segmentów programu, a każda częś ć może być pisana i testowana oddzielnie przed połącze niem wszystkich w jedną cało ść . Pozwala to także na podzielenie pracy pomi ędzy kilka zespołów programistów , w których każdy programista odpowiada za ściśle okre ś loną czę ść projektu z dobrze zdefiniowanym funkcjonalnym interfejsem do reszty kod u.
Struktura funkcji Jak j uż
widzie liśm y, pisząc funkcję
ma i nO , funkcja składa się z nagłówka funkcji ją identypo którym na stępuje cia ło funkcji . Jest ono otoczone nawi asami klamrowymi i zawiera kod wykonywalny funkcji. Spójrzmy na przykład . Możemy napisać funkcję podnoszącą liczbę do danej potęgi, która oblicza wynik mnożenia liczby x przez siebie s a m ą n razy, to jest x", fi kującego,
II Funkcja podnoszą ca x do potęgi n. gdzie n j est większe lub równe O.
double powerC double x. int n)
li Nagłó wek fun kcj i .
{
II Cialo f unkcj i rozpo czyna s ię tutaj. .. II Wynik przechowywany jest tutaj .
double resul t ~ 1. O; for Ci nt i ~ 1; i (0)/2:
t ry
(
pdat a = new char[count] : cout « "P a mi ę ć zo s tał a przydzielona " « end l :
}
catc h(bad_al loc &ex)
{
cout
« « «
"Operacj a przydzielania pami ę c i za k oń cz ył a s ię ni epowodzentem . " "Informacj a od obiekt u wyją tk u j est nastę puj ą c a : " eX.what () « endl :
«
endl
}
delet e[] pdata .
return O:
Na moim komputerze rezultat d ziałania tego programu jest następujący:
Operacja przydz lel ania p amięc i zak o ńczyła s ię niepowodzeniem .
Informacja od obiektu wyj ą t k u je st n a s t ęp uj ąc a: bad al locati on
Je śli
masz bardzo
dużo pamięci
w swoim komputerze, to
możesz szczę śliwie uniknąć
tego
wyjątku.
Jak to działa Powyższy
ślony jest
program dynamicznie przydziela pamięć tablicy typu char[], której rozmiar okre przez zmienną count zdefiniowaną za pomocą następującego kodu :
si ze_t count
=
-s t at ic_cast<size_t>(0)/2 :
Rozmiar tablicy jest lic zbą całko w itą typu si ze_t, a więc zmienną count również zadeklaro waliśmy jak o tego typu. Wartość tej zmiennej obliczana jest w dość skomplikowanym wyraże niu. Wartość Ojest typu int , a więc wartość będąca wynikiem wyrażen ia st at ic_cast<si ze_t>(O ) to zero typu size_t . Zastosowanie operatora - powoduje odwrócenie wszystkich bitów
320
Visual C++ 2005. Od podstaw na przeciwne, dzięki czemu otrzymujemy w artoś ć typu S i ze_t, w której wszy stkie bity to l, co odpowiad a maksymalnej wartośc i przechowywanej przez typ S i ze_t , gdyż jest to typ bez znaku. Wartość ta jest w iększa od maksymalnej i l ośc i pamięci , którą może przydzielić ope rator new za jednym razem, a więc dzielimy ją przez 2, aby m ógł sobie z nią poradzić . Jest to jednak nadal bardzo duża warto ś ć i jeżeli nie posiadamy bardzo dużej ilości pamięci w kom puterze , żądanie przydzielenia pamięci zakończy się niepow odzeniem. Przydzielanie pamięci odbywa s i ę w bloku try. Jeżeli opera cja zakoń czy się powodzeniem, to otrzymamy potwierdzającą to wiadomoś ć , ale jeżeli - zgodnie z oczekiwaniami - zak oń czy się niepowodzeniem, to operator new spowoduje wyjątek typu bad_a11 oc. W wyniku tego wywołany zostanie kod klauzuli eateh. Funkcja wh at ( ) wywołana dla referencji ex do obiektu bad_a11oc zwraca łańcuch opisujący problem, który spowodował wyjątek. Rezult at tego wywołania funkcji widzimy na ekranie. Większość klas wyjątk ów ma zaimplementowaną funkcję what() , dostarczającą w postaci łańcucha inform acji na tem at powodu wystąpien i a wyjątku.
z obsługi wyjątków spowodowanych brakiem pamięci , musimy sposób na jej przywrócenie do wolnego obszaru. W większości przypadków zw i ąza ne jest to z dużym wysiłkiem włożonym w pracę nad zarządzaniem pamięcią w programie. Wysiłek ten jest rzadko podejmowany. Aby
odnieść jakąś korzyść
mieć
Przeładowywanie
funkcji
Pr zypu śćmy , że napisaliśmy funkcję znajdującą największą wartość
w tabl icy
zawieraj ącej
liczby typu doubl e: II Funkcj a zn ajdują ca
największą wartość
w tablicy liczb typu double.
doubl e ma xdoub leCdouble array[] . i nt len) (
double max = array[O] : Iort t nt i = 1: i < len: i++ )
i fCmax < array[ i ] )
ma x = array[ i ] :
ret urn max;
Chcemy teraz utworzyć funkcję znajdującą największą wartość w tablicy elementów typu long, a w ięc piszemy drugą funkcję podobną do poprzedniej:
long ma xlong Clong array[]. int len): Nazwy funkcji dobraliśmy w taki sposób, aby odzwierciedlały rodzaj wykonywanego zadania. Podej ś cie to jest jak najbardziej w porządku, gdy mamy do czynienia z dwiema funkcjami. Ale tej samej funkcji możemy potrzebować dla jeszcze innych typów argumentów. Szkoda, że dla każdej z nich musimy wymyślać inną nazwę. Lepiej by było, gdybyśmy mogli używać tej samej nazwy funkcji ma x( ) w odpowiedniej wersji w zależności od potrzeby. Pewnie nie będzie to zaskoczeniem, gdy powiem, że coś takiego jest możliwe. Mechanizm C++, który nam to umożliwia , nazywa się przeładowywaniem funkcji.
Rozdział 6.•
Ostrukturze programu -
ciąg
dalszy
321
Czym jest przeładowywanie funkcii
Przeładowywan ie
funkcji to mechanizm p ozwalający na użycie tej samej nazwy funkcji do zdefiniowania kilku funkcji o takich samych listach parametrów. Kiedy funkcja taka jest wywoływana, kompilator wybiera właściwą wersję na podstawie podanych argumentów. Oczywiście kompilator za każdym razem, wybierając jedną z wersji funkcji , musi jednoznacz nie określić , która ma zostać użyta, a wi ęc lista parametrów każdej z przeładowanych funkcji musi być niepowtarzalna. Kontynuując z funkcją max( l, możemy utworzyć funkcje przełado wane o nast ępujących prototypach: i nt max(i nt ar ray[ ] . int len): 10n9 ma x(long arra y[ ]. lnt len): double maxtdouble ar ray[ ] . i nt len) :
Wszystkie te funkcje
II Prototypy II funkcji II prz eciążonych.
mają taką samą nazwę,
można rozróżnić dzięki
temu,
że
ale inne listy parametrów. Przeciążone funkcje ich parametry są różnego typu lub mają różną liczbę pa
rametrów. Warto
zauważyć , że różne
wyższego
zbioru nie
typy zwracane nie
double ma x(long array[ ] . int len) :
Powód j est taki, prototyp :
że
odróżniają
możemy dodać poniższej
funkcji tej nie
można
od siebie wersji danej funkcji. Do po
funkcji :
II Nieprawidło we przeładowan ie.
by
było odróżni ć
od funkcji
po siadającej następujący
long ma xI long ar ray[] . i nt l en) :
Zdefiniowanie funkcji w taki sposób spowoduje, o błędzie :
że
kompilator
zgłosi następujący komunikat
err or C2556: ' double ma x(1ong [ J. i nt ) ' : over loaded funct i on dif fer s only by ret urn t ype from ' long max(lo ng [J . int ) '
i program się nie skompiluje. Zasady te mogą wydawać się niezbyt sensowne, dopóki nie przy pomnimy sobie, że możemy pisać instrukcje takie jak ta poniżej : long numbers[] ~ {L 2. 3. 3. 6. 7. 11. 50. 40}; i nt len = siz eof numbe r s/ sizeof number s[O]: maxInumber s . l en) :
Fakt, że wywołanie funkcji max( l w tym przypadku nie ma większego sensu, bo odrzucamy wynik, wcale nie oznacza, że jest ono niedozwolone. Gdyby typ zwracany mógł służyć j ako znak odróżniający, kompilator w poprzednim kodzie nie mógłby się zdecydować, czy wybrać wersję z typem zwracanym lon g, czy doubl e. Z tego też powodu typ zwracany nie może być stosowany jako cecha odróżniająca przeładowanych funkcji . Każda funkcja (nie tylko funkcje przeładowane) ma swoją sygnaturę, którą stanowi jej nazwa oraz lista parametrów. Wszystkie funkcje w programie muszą mieć niepowtarzalne sygnatury. W przeciwnym przypadku program się nie skompiluje.
322
VisIlai C++ 2005. Od pOIlstaw
R!lmI!!mI Stosowanie funkcji przeładowanych Przeładowywanie śmy.
funkcji prze ćwic zymy na przykładzie funkcji maxr ), którąjuż zdefiniowali Wypróbujemy przykł ad zaw ieraj ący trzy jej wersje dla tablic typów i nt , long oraz doub1e.
II Cw6_07.cpp
II Stosowanie funkcji
prze łado wanych.
#i nclude
using st d: :cout :
USing St d: .endl ;
int ma xt int arr ay[] . i nt len); long max(l ong ar ray[] . tnt len) : double max(double array[] . i nt len) ;
i nt lenmedium ~ sizeof med i um/ sizeof med ium[O] ;
i nt lenlarge ~ siz eof large/ si zeof l arge[O] ;
cout « endl « ma x(sma11 . lensma11 ) ;
cout « endl «ma x(medium . lenmedium) ;
cout « endl « max(large. lenlarge) ;
cout « endl ;
retu rn O;
II Maksyma łn a liczba liczb typu int. int ma xt tnt x[], int len)
{
i nt max ~ x[O ].
for (i nt i ~ l ; i < len: i ++ )
if(max < x[i ] )
max ~ xCi ] :
ret urn max;
II Maksym a/na liczba liczb typu long.
long max(long xC]. i nt len) {
long ma x ~ x[O] ;
for( i nt i ~ 1: i < len; i ++ )
i f( max < xCi ] )
max ~ x[i ] ;
retur n max;
II Maksym a/na liczba liczb typu double.
double ma x(doub le xC ]. int len)
{
double max
~
x[O],
Rozdzial6.• ostrukturze programu for(int i = 1; i < le n: i f'(max < xl i l )
max = xl i l:
ret urn max:
Program
działa
tak, jak
ciąg
dalszy
323
1++ )
się spodziewaliśmy,dając następujący rezultat:
34 2345 345.5
Jak lo działa W kodzie znajdują się prototypy wszystkich trzech przeładowanych funkcji ma xt ). W każ dej instrukcji wyjściowej kompilator na podstawie listy parametrów wybiera odpowiednią wersję funkcji max( l . Wszystko działa jak należy, gdyż każda z tych funkcji ma niepowtarzalną sygnaturę dzięki innej niż u pozostałych liście parametrów.
Kiedy stosować przeładowywanie 'Iunkcii Przeładowywanie
funkcji pozwala nam upewnić się, że nazwa funkcji opisuje wykonywaną i nie jest ona mylona z inform acjami zewnętrznymi, takimi jak typ przetwarzanych danych . Jest to zbliżone do tego, co się dzieje z podstawowymi operacjami w C++-. Aby dodać do siebie dwie liczby, zawsze używamy tego samego operatora , bez względu na typ operan dów. Nasza przeładowana funkcja ma x( l ma taką samą nazwę, bez względu na typ przetwarza nych danych . Powoduje to, że kod jest bardziej przejrzysty i łatwiej jest używać tych funkcji . funkcję
Cel przeciążania funkcji jest jasny : chodzi o to, aby móc wykonywa ć tę sam ą opera cję przy użyciu różnych operandów, używają c jednej naz11Y funkcji. Zatem za każdym razem, gdy mamy klikafunkcji wykonujqcych to samo zadanie, ale z różnymi typami argumentów, powinniśmy j e przeładować i użyć wspólnej dla wszystkich nazwy.
Szablony funkcji Ostatni przykład był dosyć uciążliwy pod tym względem , że dla każdej funkcji musieliśm y przepisywać ten sam kod, zmieniając tylko typ zmiennej i parametrów. Na szczę ście można tego uniknąć . Istnieje możliwość utworzenia ogólnego przepisu, na podstawie którego kompi lator będzie automatycznie tworzył funkcje , stosując różne typy parametrów. Kod definiujący taki przepis generuj ący określoną grupę funkcji nazywa się szablonem funkcji . Szablon funkcji posiada jeden lub większą liczbę parametrów typu, a określona funkcja generowana jest poprzez dostarczenie argumentu typu dla każdego z parametrów szablonu. W związku z tym wszystkie funkcje tworzone na podstawie jednego szablonu zawierają ten
324
Visual C++ 2005. Od podstaw sam podstawowy kod, który jest dopasowywany do indywidualnych potrzeb za pomocą poda nych argument ów. Aby zobaczyć, jak to działa w praktyce, możesz zd efiniować szablon funkcji max () z poprzedniego przykładu .
Stosowanie szablonu lunkcji P oniżej
znajduje
si ę
definicja szablonu dla funkcji ma x( ):
t emplat e T max(T xl l
i nt len)
(
T max = x[OJ:
t ort i nt i = 1: i < len: i++ )
if(max < x[i] )
ma x = x[iJ:
ret urn max:
S łowo
kluczowe template informuj e, że jest to sza blon funkcji. W trójkątnych nawiasach kluczowym t empl ate znaj d ują s i ę oddzielone przecinkami parametry typów, które s ą używane do utworzenia danej funk cji z szablonu . W tym przypadku podany został tylko jeden parametr określający typ T. S ł ow o kluczowe c l ass znajdując e s i ę przed T oznacza, że litera ta jest parametrem typu tego szablonu - c l ass jest rodzajowym terminem określ ającym typ. W dalszej części książki dowiemy się, że definiowanie klasy jest równoznaczne z definio waniem własnego typu danych. W rezultacie w C++ mamy typy fund am entalne, takie jak i nt i char, oraz typy zdefiniowane samodz ielnie. Warto zwrócić uwagę , że zamiast słowa klu czowego clas s do identyfikowania parametrów w szablonie funkcji można użyć słowa kluczo wego typename. W takim przypadku definicja szablonu wyglądałaby następująco: po
sł owie
template T max (T xC] . int len) (
T max = x[OJ:
for(i nt i = 1: i < len: i ++)
if (max < xl i l )
ma x = x[i J:
ret urn max:
Niektórzy programiści wolą u żywać s łowa kluczowego t ypename, jako że słowo kluczowe cl as s zazwyczaj oznacza typ zdefiniowany przez użytkownika, natomiast typename jest bar dziej neutralne i dzięki temu łatwiej z niego wywnioskować, ż e oznacza ono zarówno typy fundamentalne, j ak i zdefiniowane przez programi stę . W praktyce oba te słowa kluczowe uży wane są bardzo często. Wszystkie przypadki wystąp i e n ia' symbolu T w definicji szablonu funkcji zamieniane są na określony argument typu, taki jak l ong, podawany przy tworzeniu egzemplarza funkcji . J eśli własnoręczni e wstawimy l ong w szędzie tam , gdzie znajduje się litera T, to przekonamy się , że otrzymamy w pełni sprawną funkcję obliczając ą największą warto ś ć w tablicy typu l ong:
long max(l ong x[J . i nt len ) {
long ma x = x[OJ:
for (i nt i = 1: i < len: i ++)
Rozdział 6.•
Ostrukturze programu -
ciąg
dalszy
325
if(max < xCi ] )
max ~ xCi ] ;
ret urn max:
Tworzenie
określonej
funkcji z szablonu nazywa
się konkretyzacją.
Za każdym razem, gdy w programie używamy funkcji maxt ), kompilator sprawdza, czy funk cja odpowiadająca typowi argumentów, których użyliśmy do jej wywołania, rzeczywiście ist nieje. Jeżeli wymagana funkcja nie istnieje, kompilator tworzy ją, wstawiając typ argumentu użyty w wywołaniu funkcji w każde miejsce, w którym pojawia się parametr T w kodzie defi nicji szablonu. Możemy przećwiczyć szablon funkcji max() przy użyciu tej samej funkcji mai n( ) co w poprzednim przykładzie.
R!lmmJI Slosowanie szałllonu funkcji Poniższy kod stanowi zmodyfikowaną wersję popr zedniego szablonu funkcji ma x( ) :
II Cw6_08.cpp
II Stosowanie szablonów funkcji.
#i nel ude
using std ; ;cout ;
using st d; ;endl ;
II Szablon funkcji znajdującej najwię kszy element tablicy.
int lensmal l = sizeof sma l l /slzeof smal l [O] ;
int lenmedium = slzeof medi um/ si zeof medi um[O] :
i nt lenlarge = si zeof large/ si zeof large[O] :
cout « endl « max (sma ll . lensmall) ;
eout « endl « max(medium. l enmedium ) ;
eout « endl « max(large. lenlarge) :
eout « endl ;
ret urn O:
Program ten daje identyczny wynik jak w poprzedniej wersji .
przykładu
z wykorzystaniem
326
Visual C++ 2005. Od podstaw
Jak to IJziala Dla każdej instrukcji wy s yłającej na wyjści e najwi ększ ą w arto ść w tablicy twor zona jest z szablonu nowa wersja funkcji max(). Oczywi ście , j e że li dodamy je szcze jedną instrukcj ę wywołującą funkcję max() z jednym z typów wcześniej używany ch , to nowa wersja kodu nie zostanie wygen erow ana . Warto zauważyć , że używani e szablonów w żaden sposób nie wpływa na zmniej szenie roz miaru skompil owanego programu. Kompilator twor zy nową wersję kodu dla każdej funkcji, której chcem y uży ć . W rzec zywi stości używanie szablonów m oże nawet spowodować zw i ę k szenie rozmiaru programu , gdyż nowe funkcje m ogą by ć tworzone nawet w takich przy padkach, gdzie w zupełno ści wy starczyłaby odpowiedni a konwersja typów. Aby wymu si ć utworzenie określonych egzemplarzy szablonu , należy dołączy ć odpowiednią deklarację. Je żel i na przykład chcem y mie ć pewność, że zostanie utworzona funkcj a max ( ) w wersji z typem fl oat, to po defini cj i szablonu moglibyśmy zastosować poniżs z ą d eklarację:
fl oat max(fl oat . 1nt ) : Powyższa
deklaracja wymusi stworzenie tej wersji funkcji. Nie wnosi ona zbyt wiele do nasze go programu. Może s i ę jednak przydać , gdy wiem y, że może zosta ć wygenerowanych kilka wersji funkcji szablonowej, a my chcemy wymusi ć wygenerowanie podzbioru , którego planu jemy użyć z argumentami przekonwertowanymi w razie koni eczności do odpowiedni ego typu.
Przykład uiywania funkcji Nauczyliśmy się już
wielu podstawowych technik programowania w C++, włączając w to o funkcjach zdobytą w tym rozdziale . Po przebrni ęciu przez różn e narzędzia dos tępne w danym języku czasam i ni eł atwo jest zorientować s i ę jak one mają się do siebie. Nadszedł więc czas na stworzenie czegoś bardziej treściwego ni ż prosty program demonstracyjny. wiedzę
Rozpracujemy bardziej realistyczny przykład w celu zapoznani a si ę z m etodologią rozbijania problemu na poszczególne funkcje . Na proces ten składają s i ę : defin icja problemu do ro zwią zania, analiza problemu w celu okreś len ia, jak można dokonać jego implementacji w CH , oraz pisanie kodu. Stosuję tutaj podejście mające na celu zilustrowanie sposobu, w jaki funk cje, dz iałając razem, doprowadzają do wyniku końcowego. Nie jest to poradnik , jak stworzyć program.
Implementacja kalkulatora Przypuśćmy, że
potrzebujemy programu działającego jak kalkulator. Nie mam tu jednakna jednego z tych urząd zeń i ustrojstw naszpikowanych przyciskami i zaprojektowanych dla osób łatwo dających się zadowoli ć . Nasz program je st dla tych, którzy wiedzą, czego chcą z arytm etycznego punktu widz enia . Mo żna wpisać do niego c ał e wyrażenie arytmetyczne i natychmiast otrzymać wynik . Poniżej pokazane jest przykładowe wyrażenie, które będzie można do niego wpi s ać: my śli
Rozdział 6.•
2*3.14159*12.6*12 .6 / 2
+
Ostrukturze programu -
ciąg
dalszy
327
25.2*25.2
Aby uniknąć zbędnych komplikacji, program nie będzie akceptował nawiasów w wyrażeniach, a całe liczenie musi odbyć się w jednym wierszu. Będzie za to można umieszczać spacje w dowolnych miejscach, aby użytkownik mógł uatrakcyjnić wizualnie wprowadzane przez siebie dane. Wprowadzane wyrażenia mogą zawierać operatory mnożenia, dzielenia, doda wania i odejmowana, reprezentowane odpowiednio za pomocą symboli *, /, + oraz -. Wartość wyrażeń będzie obliczana z zastosowaniem normalnych zasad arytmetycznych, czyli na począt ku wykonywane są mnożenie i dzielenie, a po nich dodawanie i odejmowanie. Program będzie pozwalał na wykonywanie dowolnej liczby obliczeń, a jego zamknięcie nastąpi w chwili wprowadzenia pustego wiersza. Będzie także wyświetlał pomocne i przyjazne komu nikaty o błędach.
Analiza problemu Dobrym miejscem do rozpoczęcia jest wprowadzanie danych. Program przyjmuje wyrażenia arytmetyczne o dowolnej długości, które są zapisane w jednym wierszu. Mogą być one dowol nie skonstruowane w obrębie podanych warunków. Jako że nie da się z góry ustalić, co jest czym w takim wyrażeniu, musimy je najpierw wczytać jako łańcuch znaków, a następnie rozszyfrować jego strukturę wewnątrz programu. Możemy zdecydować, że obsługiwane będą łańcuchy o długości do 80 znaków, które będą przechowywane w tablicy zadeklarowanej za pomocą poniższych instrukcj i:
const int MAX ~ 80 ; char buffer[MAX] ;
II Maksymalna długość wyrażenia włącznie ze znakiem 'lO'. II Miejsce na wyrażenie, którego wartoś ć ma zostać obliczona.
Aby zmienić maksymalną liczbę znaków łańcucha przetwarzanego przez program, wystarczy tylko zmodyfikować wartość początkową zmiennej MAX. Musimy
zrozumieć podstawową strukturę
ściowym.
informacji, które Przeanalizujemy ją krok po kroku.
mogą pojawić się
w
łańcuchu
wej
go maksymalnie uprościć. W tym celu analizy usuwamy wszystkie spacje. Funkcję wykonującą tę czynność możemy nazwać eat spacesO. Funkcja ta przeszukuje całą zawartość bufora wejściowego czyli tablicę buffer [] - i nadpisuje wszystkie spacje. Proces ten wymaga użycia dwóch indek sów do tablicy buforowej i oraz J i rozpoczyna się na początku bufora. Zazwyczaj element j będzie przechowywany w lokalizacji i, a więc spacja znajdująca się w i zostanie nadpisana przez następny znak, który znajduje się w lokalizacji indeksowej j, która nie jest spacją. Dzia łania te przedstawiono na rysunku 6.2.
Przed przed
rozpoczęciem przetwarzania łańcucha należy rozpoczęciem
Proces ten polega na skopiowaniu zawartości tablicy do niej samej, odrzucając spacje. Na ry sunku 6.2 widać tablicę buffer przed kopiowaniem i po nim. Strzałki wskazują kopiowane znaki oraz miejsca, do których zostały one wstawione.
328
Visual C++ 2005. Od pOlIslaw Tablica buffer przed
operacją
samokopiowania
indeksj Indeks i nie jest zwiększanyw tych miejscach, gdyż zawierają one spacje, które zo stają nadpisane _ _ prze z znajdujące s ię po nich w tablicy buffer znaki nie będą ce spacjam i
indeks i Tablica buffer po operacji samokopiowania
Rysunek 6.2 Po usunięciu spacji z wyrażenia mo żemy zabrać się za obliczanie jego wartości. Definiujemy zatem funkcję expr() zwracającą wynik całego wyrażenia w buforze wejściowym . Aby podjąć decyzję , co dokładnie ma robi ć funkcja expr O, musimy dokładniej przyjrzeć się strukturze danych wejściowych. Operatory dodawania i odejmowania mają najniższy priorytet i wyko nywane są na samym końcu. Łańcuch wejściowy możemy traktować jako składający s i ę z jed nego lub większej liczby składników połączonych operatorami + lub -. Każdy z tych operato rów możemy określić słowem operdod. Za pomocą tej terminologii ogólną postać wyrażenia wejściowego możemy przedstawić w następujący sposób: wyrażenie :
sk ładnik
operdod
składn i k
... operdod
s k ła dn ik
Wyrażenie zawiera co najmniej jeden składnik i może mieć dowolną liczbę następujących po nim kombinacji "operdod składnik" . Jeżeli usunęliśmy wszystkie spacje, to po każdym skład niku może znajdować się jeden z trzech możliwych znaków:
• Znak \0
oznaczający , że doszliśmy
• Znak - oznaczający, już do tej pory części
do
końca łańcu cha .
że następny składnik należy odjąć
od
wartości
obliczonej
wyrażenia.
• Znak + oznaczający, że n astępny już do tej pory części wyra żen ia.
składnik należy dodać
do
wartości
obliczonej
Jeżeli po skład n i ku znajduje się coś innego, oznacza to, że należy wyświetlić komunikat o błę dzie i zakończyć program. Na rysunku 6.3 widoczna je st struktura przykładowego wyrażenia.
Rysunek 6.3
Koniec danych
operator dodawania lub odejmowania
operator dodawania lub odejmowania
wejściowych
Rozdzial6.• ostrukturze programu- ciąg dalszy
329
Następnie potrzebujemy bardziej szczegółowej i dokładnej definicji składnika. Składnik to szereg liczb połączonych operatorami * lub I . A zatem ogólna forma składnika przedstawia się następująco: s kła dn ik :
l ic zba operato rmd l i czba
oper atormd l i czba
operatormd reprezentuje zarówno operator mnożenia, jak i dzielenia. funkcję
Możemy zdefiniować
t erm() , która będzie obliczała wartość składnika. Funkcja ta będzie skanowała łań
cuch w poszukiwaniu pierwszego łańcucha liczbowego, po którym znajduje się operator mno żenia lub dzielenia i następna liczba. W momencie napotkania czegoś innego niż operat ormd, funkcja uznaje, że jest to operator dodawania lub odejmowania, i zwraca wartość obliczoną do tego momentu . Ostatnia rzecz, którą musimy zrobić, to znaleźć sposób na rozpoznawanie liczb. W celu mak symalnego uproszczenia kodu nasz program będzie rozpoznawał tylko liczby bez znaku. A więc liczba to zbiór cyfr, po których może wystąpić przecinek oddzielający część dziesiętną oraz jeszcze więcej cyfr. W celu określenia wartości liczby przeszukujemy bufor, aby odnaleźć cyfry . Je śli znajdziemy coś, co nie jest cyfrą, to sprawdzamy, czy jest to przecinek dziesiętny. Jeżeli nie jest, to oznacza to, że znak ten nie ma nic wspólnego z wartością liczby, i zwracamy to, co znaleźliśmy do tego momentu. Jeżeli natrafimy na przecinek, to szukamy więcej cyfr. Po natrafieniu na znak nie będący cyfrą mamy cał ą liczbę i ją zwracamy . Funkcję rozpoznającą liczby i je zwracającą nazwiemy numbe r ( ). Na rysunku 6.4 pokazano rozbicie przykładowego wyrażenia na składniki i liczby .
operator dodawania lub składnik odejmowania
składnik
r: .J~
(- - - - - A---~..,
~
(Y~k:aP k a cYf r~
liczba
liczba
mnożenie
liczba
~
Koniec danych wejściowych
Rysunek 6.4 W tej chwili mamy już wystarczająco dużo informacji na temat problemu, aby rozpocząć pisa nie kodu. Możemy zacząć od potrzebnych nam funkcji, które następnie połączymy w jeden program w funkcji main( ). Pierwszą i prawdopodobnie najłatwiejszą do napisania funkcją jest funkcja eat spaces (), która ma za zadanie usuwać wszystkie puste miejsca z łańcucha wej ściowego.
330
Visual C++ 2005. Od podslaw
Usuwanie spacji złańcucha Prototyp funkcji eat spaces ()
może być następuj ący :
void eat spaces(c har* str ):
II Funk cj a
usu wając a
spacje.
Funkcja ta nie musi nic zwracać, gdyż spacje można u sunąć, bezpośrednio modyfikując łań cuch za pomocą w skaźnika przekazanego jako argument. Sam mechanizm usuwania spacji je st bardzo prosty . Kopiujemy łańcu ch do samego siebie, zastępując spacje znakami, jak opi syw a l i ś my wcześniej. Poni żej
znajduje
II Funkcja
się przykładowy kod
usu wają ca
takiej funkcji:
spa cj e z łańcuch a.
vo i d eat spaces (char* st r)
{
i nt i = O: O: while ((*(st r + i ) = *(str + if( *(st r + i) 1= ' ') i nt j =
j ++)) I~
i++ .
' \ 0' )
II Kopiowanie do - indeks do lancucha. II Kopiowanie z - indeks do łańcu cha. II Pętla >rykonywana, j eżeli znak nie jest 10. II Zwiększaj i, dopóki nie IIjest spacją.
ret urn:
Jak działa ta funkcja Całe działanie
tej funkcji zostało zawarte w pętli whi Ie. Jej warunek kopiuje łańcuch, przeno znak znajdujący się w lokalizacji j do lokalizacji i, a na stępnie zw i ę ksza j do następnego znaku . Jeżeli skopiowany znak jest znakiem \ 0, oznacza to, że o siągn ięto koniec łańcuch a . sząc
Jedyną czynnością wyko n ywa ną
w instrukcji pętli jest zwiększanie i do następnego znaku, ostatni skopiow any znak nie był spacj ą. J eżeli był spacją, to i nic zostaje zwiększone, dzięki czemu spacja zostaje nadpisana przez znak skopiowany w następnym powtórzeniu pętl i .
je żeli
Jak jąca
widać,
nie było to trudne zadanie . Następna fu nkcja, którą napiszemy , to funkcja zwraca wynik obliczania wartości wyrażenia.
Obliczanie wartości wyrażenia Funkcja expr() zwraca wartość wyrażenia zawartego w a więc jej prototyp przedstawia się następująco: double expr(char* st r );
łańcuchu
II Funkcj a ob liczająca
podan ym jako argument,
wartość wyrażenia.
Zadeklarowana powyż ej funkcja przyjmuje jako argument łańcuch i zwraca wynik w postaci liczby typu doubl e. Proce s obliczania wartości wyrażenia przedstawić można na logicznym diagramie, utworzonym na podstawie naszych wcześniejszych rozważań na ten temat. Diagram ten widoczny je st na rysunku 6.5.
Rozdzial6.• ostrukturze programu -
ciąg
dalszy
331
Rysunek &.5 Znajdź wartość
pierws zego
wskaź n ika
Ustaw wartoś ć na wa rtość pierwszego składnika
wyrażenia
Następnym
Zwró ć wartość
znakie m jest 'lO'?
wyrażeni a
N a stępnym
Odejm ij wartoś ć
znakiem jest '-'?
następnego składnika
od
Dodaj
N a stępnym
znakiem jest '+'?
wartości wyrażen ia
warto ść
na stępnego składn ika
do
wartości w y ra że n i a
BŁĄD
Przy
użyciu
II Funkcja
podstawowej defin icj i tego mechanizmu
obliczająca wartość wyrażen ia
możemy napisać następującą funkcję:
ary tmetycznego.
double expr(cha r* st r) (
double value = 0.0: i nt i ndex ~ O:
II Do przechowywania wyniku. II Śledz i lokalizację bieżącego znaku.
value = termr st r . index) :
II Pobi erz pierwszy znak.
t or (: : )
II Pętla nieskończona, wszystko odbywa się II we wną trz.
switch(*(str + index++))
II Podej mij działan ia na podsta wie II bie ż qcego znaku .
case ' \0 " retu rn val ue:
II Jest eśmy na końc u łańc ucha, II a więc zwracamy, co mamy.
332
Visual C++ 2005. Od podstaw case v- ": val ue += t ermCst r, i ndex): break:
II Znaleziono znak +, a II nast ępny skladnik.
case ' - ' :
II Znal eziono znak -, a II odejmuje my II następny skladnik.
value -~ t erm(str . i ndex) : break : default : cout « end l « «
"Arrrgh l *#1i Tu j est
wię c
dodaj emy
wię c
II Jeśli doszliśmy tutaj , to II zna czy 10 , że lańcu ch jest II niepra widlowy. błą d!"
endl :
exit (l) ;
Jak dziala lafunkcja Biorąc pod uwagę, że funkcja ta potrafi przean alizować każde wyrażenie arytmetyczne, które raczymy jej podsunąć (pod warunkiem że używamy w nim operatorów z naszego zbioru), nie zawiera ona bardzo du żo kodu. Definiujemy zmienną typu i nt o nazwie i ndex, śledzącą na szą bieżącą lokalizację w łańcuchu. Zainicjali zowaliśmy ją wartością O, która odpowiada indeksowi pierws zego znaku w łańcuchu . Zdefiniowaliśmy także zmienną typu doubl e, w któ rej będziemy przechowywać wartość wyrażenia przeka zanego do funkcji w tablic y typu char o nazw ie st r.
Jako że każde wyrażenie musi mieć co najmniej jeden składnik, pierwsząnaszą czynnościąjesl pobranie wartości pierw szego składnika za pomocą funkcji t erm. ), którą dopiero napiszemy. W związku z tym istnieją trzy wym agania dotyczące funkcji t erm( ) :
l Powinna przyjmować wskaźnik typu char* oraz zmienną typu i nt jako parametry, Drugi parametr powinien
być
indeksem pierwszego znaku
składnika
dostarczonego
łańcucha .
2. Powinna uaktualniać wartość przekazywanego indek su, aby n a stępującemu
po ostatnim znaku
3. Powinna zwracać Resztę
odpowiadał
znakowi
składnika.
wartość składnika w
postaci liczby typu doubl e,
programu stanowi nieskończona pętla fo r . Wewn ątrz tej pętli wszelkie d zi ałania kontrolowane są przez instrukcję switch, która z kolei podejmuje decyzj e na podstawie bie żącego znaku w łańcuchu. Jeżeli j est to znak +, to wywoływana jest funkcja term ( ) w celu obliczenia wartoś ci następnego składn i ka wyrażenia i dodania jej do zmiennej val ue. Jeże li jest to -, to od zmiennej val ue odejmujemy wartoś ć zwróconą przez funkcj ę t erm t ). W przy padku znaku \ 0 zwrócona zostaje bieżąca wartość zmiennej val ue, gdyż oznacza to, że doszli śm y do końca łańcucha . Jeżeli napotkany znak jest jeszcze inny, wyświetlona zostaje repry menda dla użytkownika za wpi sanie niewła ściwych znaków i program kończy działanie,
Rozdział 6.•
Pętla
Oslrukturze programu -
ciąg
dalszy
333
dopóki znajduje znak + lub -. Każde wywołanie funkcji t er m( l powoduje zmiennej index do znaku następującego po składniku, którego wartość została obliczona, i powinien to być albo znak + czy -, albo znak końca łańcucha \ 0. A zatem funkcja może zakończyć działanie w normalny sposób po napotkaniu znaku \ 0 lub wyjąt kowo poprzez wywołanie funkcji exit (l . Przy składaniu całego programu należy pamiętać o dyrektywie #i ncl ude dołączającej plik nagłówkowy , który zawiera definicję funk cji exit ( l . powtarza
się,
przesunięcie wartości
Wyrażenie arytmetyczne moglibyśmy również przeanalizować za pomocą funkcji rekuren cyjnej . Definicję wyrażenia możemy także przedstawić w trochę inny sposób, określając je jako składnik lub składnik , po którym następuje wyrażenie . Jest to definicja rekurencyjna (ponieważ zawiera to, co jest opisywane) - często spotykany sposób definiowania struktur języków programowania. Jest ona tak samo elastyczna jak pierwsza, ale przy jej zastoso waniu otrzymalibyśmy rekurencyjną wersję funkcji expr ( l, zamiast - jak w powyższym przykładzie - używać pętli . To alternatywne podejście można potraktować jako ćwiczenie po ukończeniu wersji finalnej programu.
Obliczanie wartości składnika Funkcja t erm( l, przyjmując dwa argumenty w postaci łańcucha do zanalizowania oraz indeksu lokalizacji w łańcuchu , zwraca wartość składnika w postaci liczby typu doub l e. Są także inne sposoby dojścia do tego celu, ale ten jest bardzo prosty . W związku z tym prototyp funkcji t er m( l może wyglądać następująco: bieżącej
doub le t erm(char* str . int & i ndex) :
II Funkcj a analizują ca składn ik.
Drugi parametr określiliśmy jako referencję. Zrobiliśmy to, ponieważ chcemy, aby ta funk cja mogła modyfikować wartość zmiennej i ndex w programie wywołującym, ustawiając ją na znaku znajdującym się za ostatnim znakiem składnika, znalezionym w łańcuchu wejściowym . Moglibyśmy także zwrócić zmienną i ndex jako wartość, ale wtedy musielibyśmy zwrócić wartość składnika w jakiś inny sposób, a więc wybór ten wydaje s i ę całkiem uzasadniony. Sposób analizowania składnika ma podobną strukturę do tej, której użyliśmy dla wyrażenia . Składnik jest liczbą, po której prawdopodobnie następuje jedna lub więcej kombinacji opera torów mnożenia lub dzielenia z innymi liczbami . Definicję funkcji term( l możemy napisać w następujący sposób: II Funkcj a znajdująca
wartość składnika.
double t erm (char* str . int & i ndex) (
doub le value = 0.0: value ~ number (str. index) : II Powtarzaj
aż
II Przechowuj e wynik. II Pobierz p ierwszą cyfrę składnika.
do znalezienia odpowiedniego opera/ ara.
while((*(st r + i ndex)
~=
'* ' ) II (*(str + i ndex) ==
'I' ) )
(
ł i ndex) ~= '*' ) value *= number (str . ++index) ;
if( *( s t r
II Jeśli jest to operator mnożenia, II to pomnóż przez następną liczbę.
334
Visual C++ 2005. Od podstaw if (*(str + i ndex) == '/') val ue /= number (st r . ++l ndex);
II Jeśli j est to op erator dzielenia, II to podziel przez n astępną liczbę.
}
ret urn va lue ;
II Skończone.
więc
zwra camy, co mamy.
Jak działa ta funkcja doubl e o nazwie val ue, w której będz iemy prze Jako że składnik musi zawiera ć co najmni ej jed ną liczbę, pierw szą n aszą c zynności ą je st sprawdzenie wartości pierwszej liczby poprzez wywo łanie funkcji number ( ) i zapi sanie jej do zmiennej val ue. Zakładamy, że fu nkcja number O przyjmuj e jako argum enty łań cu ch oraz indeks lokalizacji w łańcuchu oraz zwrac a warto ść znalezionej liczby. Ze względu na fakt , że funkcja number( ) musi także uaktualnić indeks w łańcuchu do lokalizacji znajdującej si ę za znalez i on ą liczbą, drugi parametr znowu określim y jako referencję podczas pisania tej defini cji.
Najpi erw deklarujemy
z mie n n ą lokalną typu
chowywać warto ś ć bi eżąc ego składnika.
funkcji term( ) stanowi pętla whi l e, która powtarza się, dopóki nie znajdzie znaku * lub l . Jeżeli pętl a znajd zie w bi e żąc ej lokali zacji w łańcuchu znak *, to zwi ększy o l zmi enn ą i ndex, aby wskazywała ona początek następnej liczby, wywoła funkcję number( ) w celu spraw dzenia w arto ści następnej liczby , a nast ępnie p omno ży przez zwróconą w arto ś ć zawartość zmiennej val ue. Podobnie , j eże l i b ieżącym znakiem okaże się znak /, zmienna i ndex zostanie zwiększona o jeden, a zawarto ś ć zmiennej val ue zostanie podzielona przez wartość zwró coną przez funkcję number( ). Jako że funkcja number () automatycznie zmienia wartość zmiennej i ndex, aby wskazywała znak następuj ący po znalezionej liczbie, zmienna ta zostaje natych miast ponownie ustawiona na następny znak do stępny w łańcuchu w kolejnym powtórzeniu. Resztę
Pętla przerywa działanie w momencie napotkania znaku innego niż operator mnożenia lub dzielenia, podczas gdy bieżąca warto ść składnika przechowywana w zmiennej val ue zwrócona zostaje do programu wywołującego.
Ostatnią potrzebną nam funkcją analityczną jest bową
funkcja nurnber ( ), która znajduje
wartoś ć
licz
wszystkich liczb w łańcuchu .
Analizowanie liczby Biorą c
pod uwagę sposób , w jaki prototyp musi by ć następujący:
używaliśmy
double number (char* st r. int &index) : Określenie
argumentu
funkcji number O
w ewn ątrz
funkcji t ermO, jej
II Funkcja rozpoznająca liczby .
drugiego parametru jako referencji umo żl iwia funkcji aktualizowanie w programie wywołującym, a tego właśn ie nam potrzeba.
warto ści
bezpośrednio
W tym miejscu możemy użyć funkcji dostępnej w bibliotece standardowej C++. W pliku nagłówkowym znajdują się definicje kilku funkcji sprawdzających pojedyncze znaki. Zwracają one wartości typu i nt, gdzie liczby dodatni e odpow iadają warto ści logicznej true, a ujemne f al se. Cztery z tych funkcji zostały przedstawione w poniższej tabeli:
Rozdzial6. •
ostrukturze programu -
ciąg
dalszy
335
Funkcie wnag!ówku slUzące do sprawdzania pojedynczych znaków i nt is al pha(int e)
Zwraca tr ue, jeśli podany argument jest znakiem w przeciwnym przypadku.
l nt is upper(in t e)
Zwraca t rue, jeśli podan y argument jest wielką literą, lub false w przeciwnym przypadku.
i nt is lower( int e)
Zwraca true, j e ś li argument jest małą literą, lub false w przeciwnym przypadku.
l nt isdigit(int e)
Zwraca t rue, jeśli podany argument jest c yfrą, lub fa l se w przeciwnym przypadku.
należącym
do alfabetu , lub false
zdefiniowano więcej funkcji, ale nie będę ich tutaj szcze chcesz wiedzieć więcej na ich temat, to poszukaj frazy ; is routines " w bibliotece pomocy MSDN. W pliku
nagłówkowym
gółowo opisywał. Jeśli
W naszym programie potrzebujemy tylko ostatniej z opisanych powyżej funkcji. Należy zapa miętać, że funkcja isd i qit O sprawdza znaki , jak np. ,,9" (w standardzie ASCII znak 57 w no tacji dziesiętnej) , a nie numeryczną wartość 9, ponieważ dane wejściowe są w postaci łańcucha. Funkcję
numberC )
możemy zdefiniować
w
następujący
sposób:
II Funkcja rozpoz nająca liczby w lancuchu.
double number (ehar* str . int& lndex) {
double value = 0.0;
II Przechowuje wynik.
whi le(i sdigit (*(st r + index)) ) val ue - 10*va lue + (*(st r + index++ ) - ' O' ) ;
II Pętla zbierająca cyfry
if(*(st r + index)
,~
' .' )
retu rn val ue ; dou ble fact or = 1.0; whi le(i sdigit (*(st r + (++ index) )))
wiodące .
II Skoro jesteśmy tutaj, to znaczy,
II że napotkaliśmy znak,
II który nie jest cyfrą.
II Sprawdzamy. czy j est to prz ecinek II dziesiętny, II i jeśli nie, to zwracamy wart ość II zmiennej value. II Wsp ółczynnik miej sc dzies iętnych . II Powtarzaj. dopó ki są cyfry.
{
faetor *= 0.1; value = val ue + (*(st r + i ndex) - ' O') *faetor ;
II Zmniejsz współczynnik II dzies ięciokrotnie. II Dodaj miej sce po przecinku.
}
ret urn val ue ;
II Zakończenie p ętli i wynik.
Jak dZiała la flmkcja Deklarujemy zmienną lokalną val ue typu doubl e i przechowujemy w niej znalezioną liczbę. lnicjalizujemy ją wartością O. O, ponieważ cyfry będą dodawane w trakcie pracy pętli .
336
Visual C++ 2005. 011 podstaw w łańcuchu stanowi ciąg cyfr w postaci znaków ASCII, funkcja przechodzi przez liczby cyfra po cyfrze . Wykonywane jest to w dwóch etapach w pierwszym zbierane są cyfry przed przecinkiem, a w drugim, jeżeli zostanie znaleziony przecinek, cyfry za nim.
Jako
że liczbę
łańcuch, obliczając wartość
Pierwsza faza wykonywana jest w pętli whil e, która powtarza się, dopóki bieżący znak wska zywany przez zmienną i ndex jest cyfrą. Wartość cyfry jest sprawdzana i dodawana do zmien nej value w poniższej instrukcji pętli: val ue
~
lO*value
+
(*(st r
+ r ndex- «) -
'O' ) ;
Sposób, w jaki jest ona zbudowana, wymaga bliższego przyjrzenia się . Znaki ASCII mają od 48 odpowiadającej cyfrze ,,0" do 57 odpowiadającej cyfrze ,,9". Jeżeli zatem odejmiemy kod ASCII cyfry "O" od kodu innej cyfry, to przekonwertujemy ją na jej odpo wiednik w zapisie cyfrowym od do 9. Choć nie jest to konieczne, podwyrażenie *(str + i ndex++ ) - ' O' ; umieściliśmy w nawiasach, aby było bardziej przejrzyste. Zawartość zmien nej val ue mnożymy przez 10 w celu przesunięcia wartości o jedno miejsce po przecinku w lewo przed dodaniem wartości cyfry, ponieważ są one ustawione od lewej do prawej to znaczy najbardziej znaczące cyfry występują jako pierwsze. Proces ten został zilustrowany na rysunku 6.6. wartości
°
Cyfry w liczbie
~ 5 Kody ASCIIw zapisie
3
dziesiętnym
Wartość początkowa
Pierw sza cyfra
value
Druga cyfra
value
=O
=1O"value + (53 - 48) = 10"0 .0 + 5
=5,0
=10"value + (49 - 48) = 10"5 .0 + 1
= 51.0
Trzecia cyfra
value = 10"value + (51 - 48) 10"51.0 + 3
513.0
= =
Rvsunek 6.6 Jeżeli natrafimy na coś innego niż cyfra, to jest to albo przecinek, albo coś innego. Jeśli nie jest to przecinek, to znaczy, że skończyliśmy, i zwracamy zmienną val ue do wywołującego pro gramu. Jeśli jest to przecinek, to zbieramy znajdujące się za nim cyfry ułamkowe za pomocą drugi ej pętli . W pętli tej używamy zmiennej fac t or , której wartość początkowa wynosi LO, do ustawienia miejsca po przecinku dla bieżącej cyfry, a następnie zawartość tej zmiennej
Rozdzial6. •
ostrukturze programu -
ciąg
dalszy
337
mnożona jest przez O, l za każdym razem, gdy dodawana jest cyfra . Zatem pierwsza cyfra po przecinku mnożona jest przez O, l, druga przez 0,0 I, trzecia przez 0,00 l itd. Proces ten przed stawiony został na rysunku 6.7.
Cyfry części liczby
czę ści
Cyfry
całkowitej
ułamkowej
~
~
5
3
6
liczby
O
S
Kody ASCIIw zapisie dziesiętnym Przed przecinkiem dziesiętnym value 513.0 factor = 1.0
=
=
Pierwsza cyfra
factor O.l*factor value = value + factor*(54 4S) = 513.0 + 0.1*6 = 513.6
Druga cyfra
factor = O.l*factor value = value + factor*(49 - 4S) = 513.6 + 0.01*0 = 513.60
Trzecia cyfra
factor = O.l*factor
value = valu e + factor*(56 - 4S)
= 513.60 + O.OOl*S = 513.60S
Rysunek &.7 W momencie znalezienia znaku niebędącego cyfrą kończymy wykonywanie tej pętli , a więc po drugiej pętli zwracamy wartość zmiennej val ue. Mamy już prawie wszystko . Potrzebujemy jeszcze tylko funkcji mai n( ) do pobrania łańcucha wejściowego i uruchomienia całego procesu.
Składanie całego programu Instrukcje dyrektyw #i ncl ude możemy umieścić razem oraz programu wszystkie prototypy wykorzystywanych funkcji :
wypisać
II Cw6_09.cpp
II Program implem entujqcy kalkulat or.
#include #include #include usi ng st d: .ci n:
usi ng st d: :cout :
using st d: :endl ;
II Dla strumienia wejścia-wyjś cia .
II Dlafunkcji exiu) .
II DlaJunkcji isdigiti).
void eat spaces(cha r* st r) :
II Funkcja usuwajqca spacje.
na samym
początku
338
Visual C++ 2005. Od podstaw double expr (char* st r ) : doub le t erm (char* str. i nt &i ndex) : do uble number( char* st r . int& t ndex ) :
II Funkcja ob liczająca warto.~ć wyrażenia. II Funkcja ana liz ująca skladnik. II Funkcj a rozpoznająca liczby .
const i nt MAX= 80 :
II Maksymalna długos ć wyrażenia. II włą czni e ze zna kiem '\O'.
Zdefiniowaliśmy również zmi enną MAX,
przetwarzanym przez program
która
określa maksymalną liczbę
(włącznie z końcowym
znaków w wyrażeniu
znakiem \ 0).
funkcji main O i program jest skończony. Funkcja main() i, jeżeli jest on pusty, kończyć program. W przeciwnym przypadku nastąpi wywołanie funkcji expr( l obliczającej wartość wprowadzonego wyrażenia i wyśw ie tlającej wynik. Proces ten powinien powtarzać się w nieskończoność . Nie brzmi to zbyt groź nie, więc spróbujemy.
Następnie możemy dodać definicję
powinna
pobierać łańcuch
i nt mai n(l {
char buffe r[MAX] cout
« « « « «
~
{O} :
II Obsza r
wejś ciowy
dla
wyraż enia
endl "Wi t amy w na szym przyj aznymka lkul at orze. end l "Wpr o wad ź wyrazenie l ub pusty wi ersz. aby endl :
do obli czenia.
z a k o ń c zy ć. "
t or r : . ) {
ci n.get l ine(buff er, s tzeof bu rrer ) : eat spaces( buff er):
II Wczytaj lańcuch wejściowy, II Us uń z niego spa cje.
if ( !buff er[O] l ret urn O;
II Pusty wiersz p owoduje zamknięcie kalkulatora.
cout
II
« «
"\t = " « expr( bu ff er ) endl « end l :
Wyś wietl wartość wyrażenia .
Jak działa lafUllkcia W funkcji mai nO tworzymy tablicę typu char o nazwie buffer przyjmującą łańcuchy o dłu gości do 80 znaków (włącznie ze znakiem końcowym). Wyrażenie wczytywane jest wewnątrz n ieskończonej pętli for za pomocą funkcji wejściow ej getLi ne( l. Po wczytaniu łańcucha usuwamy z niego spa cje, wywołując funkcję eatsp aces( l. pozostałe czynności przeprowadzane przez funkcję mai n (l wykonywane są w pętl i. sprawdzenie, czy wprowadzony łańcuch nie jest pusty, czyli zawiera tylko znak \0, W przypadku pustego wiersza działanie programu zostaje zakończone oraz wyświetlona zostaje wartość łańcucha zwróconego przez funkcję expr( l.
Wszystkie
Następuje
Po wprowadzeniu wszystkich funkcji rezultat poniższego:
działania
programu powini en
być
podobny do
Rozdzial6.• ostrukturze programu -
ciąg
dalszJ
339
2 * 35 = 70 2/3 + 3/4 + 4/ 5 + 5/ 6 + 6/7 =
3.90714
+ 2.5' + 2.5*2.5 + 2.5*2.5*2 .5 = 25 .375 Możemy podać dowolną liczbę wyrażeń
snąć
Enter, aby
za kończy ć
do obliczenia, a kiedy nam
się
znudzi, wystarczy wci-
program.
Rozszerzanie programu Mając już działający
Czy nie byłoby
kalkulator,
miło, gdybyśmy
możemy zacząć myśleć
mogli
używać
o rozszerzaniu jego funkcjonalności. nawiasów? Nie jest to chyba aż takie trudne?
Pomyślmy,
w
jaki związek występuje pomiędzy tym, co może znajdowa ć się w nawiasach a analizą wyrażenia, którą przeprowadziliśmy do tej pory. Spójrzmy na przywyrażenie, które chc iel ibyś my przetworzyć :
w nawiasach w naszym oryginalnym twierdzeniu stan ow i ą część skład nika. To prawda, i nieważne , jakiego rodzaju obliczenia są wykonywane. Gdybyśmy mogli wstawić z powrotem do oryginalnego łańcucha wartość wyrażeń w nawiasach , to otrzymalibyśmy coś , z czym można sobie poradz ić . Wskazuje to najeden ze sposobów poradzenia sobie z nawiasami . Wyrażenie w nawiasach moglibyśmy traktować jak każd ą inną liczbę i zmodyfikować funkcję nurnber() , aby odnajdywała wartość tego , co s i ę pomiędzy nimi znajduje. Wydaje się to dobrym pomysłem , ale odnajdywanie wartości tego, co jest w nawiasach, wymaga trochę zastanowienia. Wyrażenie umieszczone w nawiasach jest doskonałym przykła dem pełnego wyrażenia. Mamy już funkcj ę , która oblicza wartość wyrażeń - expr ( ). Jeżeli uda nam się zmu si ć funkcję nurnber ( ) do zwrócenia nam zawartości nawiasów oraz wyłuska nia jej z łańcucha, to tak otrzymany podłańcuch moglibyśmy przesła ć do funkcji expr( ), dzięki czemu rekurencja znacznie uprościłaby nasz problem. Co więcej , nie musielibyśmy martwi ć się o zagnieżdżone nawiasy. Jako że w nawiasach zawsze znajduje się to, co okre śliliśmy jako wyrażenie, funkcja zajmuje się tym automatycznie. Rekurencja znowu górą. Spróbujmy przepisać II Funkcj a
funkcję
nurnbe r ( ), aby
rozpozn ająca wyrażenia w
rozpoznawała wyrażenia
nawiasa ch lub liczby w
łańc u chach.
doub le number (char* st r . i nt & index) double val ue = 0,0: i f( *(st r + index) ==
II Prz echowuj e wynik. ' (' l
II Po czątek naw iasu .
(
char* psubstr = O: psubstr = ext ract(st r . ++i ndex) : value = expr(psubstr) : delete[]psubst r :
II Wskaźnik dła podla ncucha. II Wyłuskaj łań cu ch w nawiasach. II Oblicz wartos ć podłan cucha. II Wyczyść wolną pam ięć.
w nawiasach.
340
Visual C++ 2005. Od podstaw re t urn valu e :
II Zwróć
wartość podłancucha .
whi l e( i sdi git (*(st r + i ndex) ) ) II Pętla zbierająca val ue = 10*val ue + (*(str + i ndex- - ) - ' O') :
if(*( st r + i ndex) ret urn val ue ;
I ~
'. ')
wiodące
cyf ry.
II Skoro jestesmy tutaj , to znaczy. że n apotkaliśmy znak, .11 który nie jest cyfrą. II Sp rawdzamy, czy j est to przecinek dzi es ięt ny. II i jeśli nie, to zwracamy wartoś ć zmiennej value.
doubl e fact or = 1.0: whi l e (i sdlgi t(* (st r + (++i ndex)) ) ) { fa ct or * ~ 0.1 : val ue = va l ue + (*(st r + i ndex) -
' O' )*f act or:
return val ue :
II Zakoń czen ie pętli i wynik.
II Wsp ółczynn ik miej sc dz ies ię tnych. II Powtarzaj , dopóki s ą cyfry. II Zmniejsz
wsp ółczyn n ik dzies ięciokrot n i e.
II Dodaj miej sce po przecinku .
To j eszcze j ednak nie wszystko , poniew aż nadal potrzebujemy funkcji ext ract ( ), ale o tym za moment.
Jak działa ta funkcja Zadziwiające ,
jak niewielkich zmian mu sieliśmy dokonać , aby dodać obsługę nawiasów. tutaj oszukujemy, bo używamy funkcji ext ractt ), której jeszcze nie stworzyliśmy . Ale mimo to dzi ęki dodaniu tylko j ednej funkcji zyskujemy ob sługę nawiasów zagnieżdżanych do dowolnej głęboko ści . To jest j ak polewa na na szym cie ście i na dodatek wszystko dzięki magicznemu działaniu rekurencji. Oczywiście trochę
Pierwszą czynnośc ią funkcji number ( ) j est tera z poszukanie nawiasu otwi erającego . Jeżel i taki się znajdzie, to wywołuj e ona funkcję ext r act( ) w celu wydobycia z oryginalnego łań cu cha podłańcucha otoczonego nawiasami. Adres tego wydobytego podłańcuch a przechowywany j est we wskaźniku psubst r , który następnie przekazuj emy jako argument do funkcji expr() w celu przetworzenia. Wyn ik przechowywany j est w zmiennej va l ue. Po zwolnien iu pam ięc i przydzielonej w wolnym obszarze w funkcji ext r act() (kiedy już ją zaimplementujemy) otrzymaną wartość zwracamy, jakby była zwykłą liczbą. O czywiści e , jeżel i nie ma żadnego nawiasu ot w ieraj ące go, funkcja number( ) kontynuuje swoje dz iałanie.
Wydobywanie podłańcucha Musimy teraz napisać funkcję ext ract ( ). Nie jest to zadan ie bardzo trudne, ale też i nie banalne. N ajwię c ej komplikacji powoduj e fakt , że każda para nawi asów może zawierać dodatkowe zagn i eżdżone naw iasy. W związku z tym nie możemy ograniczyć się tylko do odnalezienia pierwszego nawiasu otw i eraj ące go . Musim y sp raw dz ić, czy nie ma ich więcej , a w przypadku znalezienia ich wi ększej liczby ignorować odpowiadaj ące im nawiasy zamykające. Możemy tego dokonać , tworząc licznik lewych nawi asów znale zionych przez funkcj ę i zwi ększając go o I za k ażdym razem, gdy znajduj emy otwieraj ący nawias. Jeż el i liczn ik nie ma wartoś c i
RozdZiał 6.•
Ostrukturze programu -
ciąg
dalszy
341
zerowej, to odejmujemy od niego jeden za każdym razem, gdy znajdziemy nawias zamykający. Oczywiście, jeżeli licznik ma wartość O i natrafimy na nawias zamykający, to oznacza to, że doszliśmy do końca podłańcucha . Mechanizm wydobywania podłańcucha otoczonego nawiasami przedstawia rysunek 6.8.
Sygnalizuje początek
'(' count:
Znalezienie znaku')' w momencie, gdy licznik znaków '(' ma wa rtość zero, oznacza koniec wyrażen ia w nawiasach
pod/ańcucha
o kopiowanie
zarmen
~. 2
+
*
3
Pod/ańcuch,
(
S
-
2
)
/
2
*
(
,
,
+
9
)
\0
który z naj d owa ł się w nawiasach
Rysunek 6.8 Jako
że
wydobywany tutaj
podłańcuch
zawiera
podwyrażenia otoczone
nawiasami, funkcja
extract ( ) wywołana zostaje jeszcze raz w celu przetworzenia także tych
łańcuchów.
Zadaniem funkcji ext ract() jest również przydzielanie pamięci dla podłańcucha oraz zwracanie do niego wskaźnika . Oczywiście indeks bieżącej lokalizacj i w oryginalnym łańcuchu musi zostać ustawiony na znak z n aj d uj ący się za znalezionym podłańcuchem, w związku z czym jego parametr musi zostać zdefiniowany jako referencja. Biorąc pod uwagę powyższe rozważania, prototyp funkcji extract.O przedstawia się następująco:
cha r* extract (cha r* st r . i nt &i ndex) ; Możemy
teraz
IIFunkcj a
spróbować napisać definicję
II Funkcja wydobywająca podla ńcuch II (wymaga cstring).
wydobywają ca podlańcuchy.
funkcji .
znajdujący s ię pomiędzy
nawiasami
char* ext ract( char* str . i nt & index) {
char buffer [M AX] ; char* pst r = O: int numL = O; int bu fi ndex = index;
II Tymczasowe miejsce dla podlańcucha . II Wskaźnik do nowego podtancucha, II który ma zos tać zwrócony. II Licznik znalezionych lewych nawia sów. II Zapisanie początkowej war/ości index.
do {
buffe r[ i ndex - bufindex] - *(str swit ch(buffer[in dex - bu findex])
+
i ndex) ;
{
case ' )' : if(numL == O) {
siz e t size = i ndex - bufi ndex;
342
Visual C++ 2005. Od podstaw buffer [ index - bu findex] = ' \ 0' : l/'} ' zam ień na '10'. ++index: pstr = new char[i ndex - buf index] : if( 'pst r) {
cout
"Operacja przydzielania pa m i ęc l nie programzost a ł z ak o ń c z ony . ";
« «
p ow io d ła s i ę . "
exit(l ) ;
l
st rcpys tpst r , index-bufi ndex. buffe r ) ; II Skopiuj podłancuch do now ej pamięci.
ret urn pstr :
II Zwró ć podla ńcu ch w nowej pamię ci.
}
else num L-- ; break:
II Zmniej sz
case ' (': numL ++ ; break:
II Zwiększ
liczbę
liczbę
zn aków
't . które trzeba dop asowa ć .
znaków '(', któr e trzeba
dopasować.
)
} whi le( *(st r cout
«
+
i ndex-« ) ! = ' \ 0' ) :
" Wy jśc ie
być n i epr aw ldł owe
«
poza gran ice "
II Pętla zapobiegają ca przekroczeniu k ońca łań c/l cha.
wy r aże n ia
- wprowadzone dane
m u s l a ły
endl :
exi t Cl ) ;
__ l
ret urn pstr :
----'
Jak działa la funkcja Na początku deklarujemy nową tablicę, w której tymczasowo będziemy przechowywać łań cuch. Nie wiemy, jakiej będzie on długości , ale nie może być dłużs zy niż MAXznaków. Nie możemy zwrócić adresu tablicy buffer do funkcji wywołującej, gdyż jest ona lokalna i zostanie usunięta po zakończeniu działania funkcji . W związku z tym w obszarze wolnej pamięci musimy jej przydzielić odpowiednią ilość pamięci , kiedy będziemy wiedzieli , jakiego rozmiaru jest łańcuch. Robimy to, deklarując zmienną pstr typu wskaźnik do char , którą zwracamy przez wartość , gdy łańcuch znajduje się już w obszarze wolnej pamięci . Zmienna numL służy jako licznik lewych nawiasów w podłańcuchu (zgodnie z zasadami opisywanymi wcześniej). Wartość początkowa zmiennej l ndex (kiedy funkcja rozpoczyna działanie) przechowywana jest w zmiennej buf; ndex, której używamy w połączeniu ze zwi ęk szanymi wartościami zmi ennej; ndex do indeksowania tablicy buffer. Całą wykonywalną część
naszej funkcji stanowi w zasadzie jedna duża pętla d o-włu l e. Podkopiowany jest ze zmiennej st r do tablicy buffer po jednym znaku za każdym powtórzeniem . W każdym cyklu nast ępuje też sprawdzanie, czy kopiowany znak nie jest lewym nawiasem. Jeżeli tak, to zmienna numL zostaje zwiększona o jeden, a jeżeli znaleziony został prawy nawias i zmienna numL ma wartość różną od zero, to zostaje ona zmniejszona o jeden. W przy. padku znalezienia prawego nawiasu, gdy zmienna numL ma wartość Owiemy, że doszliśmy do końca podłańcucha. W zw iązku z tym w tablicy buffer znak ) zostaje zamieniony na \0, zostaje też przydzielona ilość pamięci wystarczająca do przechowania tego podłańcucha . łańcuch
Rozdział 6.•
Następnie
Ostrukturze programu -
ciąg dalszy
343
łańcuch znajdujący się w tablicy buffer do obszaru wolnej pamięci za new przy użyciu funkcji strcpy_s() zadeklarowanej w pliku nagłówkowym . Jest ona bezpieczną wersją starej funkcji strcpy() zadeklarowanej w tym samym nagłówku. Funkcja ta kopiuje łańcuch określony przez trzeci argument (buffer) do adresu
kopiujemy
pomocą operatora
określonego
lowego -
przez pierwszy argument (pst r) . Drugi argument
określa długość łańcucha
doce-
pstro
Je żeli zostaną wykonane
instrukcje z samego dołu pętli , to oznacza to, że doszliśmy do znaku \ 0 na końcu wyrażenia w str , nie znajdując prawego nawiasu odpowiadającego nawiasowi lewemu, a więc wyświetlamy komunikat o błędzie i zamykamy program.
Uruchamianie zmodyfikowanego programu Po zamienieniu funkcji number() w starej wersji programu, dołączeniu pliku nagłówkowego pomocą dyrektywy #incl ude oraz włączeniu prototypu i definicji nowej, przed chwilą napisanej funkcji extract O jesteśmy gotowi do zabawy z naszym niezwykle wszechstronnym kalkulatorem. Jeśli wszystko poskładaliśmy bez żadnych błędów , to możemy otrzymać następujący wynik :
za
Wit aj w naszym przyj aznym kalkulat orze. l ub pusty wiersz . aby
Przyjazny i pomocny komunikat w ostatnim wierszu danych na wyjściu został spowodowany wpisaniem przecinka zamiast kropki w powyższym wyrażeniu - powinno być 2.4. Jak widać, zyskaliśmy obsługę zagnieżdżonych do dowolnej głębokości nawiasów przy względnie niedużym stopniu komplikacji kodu. Wszystko to dzięki zdumiewającym możliwościom rekurencji.
Programowanie wC++/CLI Wszystko, co było powiedziane do tej pory na temat funkcji w natywnym C++, ma również zastosowanie do języka C++/CLI, pod warunkiem że typy parametrów oraz typy zwracane są typami fundamentalnymi, które - jak wiemy - są odpowiednikami typów klas wartości w programach CLR, uchwytów śledzących oraz referencji śledzących. Jak wiemy, na adresach przechowywanych w uchwytach śledzących nie można wykonywać działań arytmetycznych. W związku z tym techniki zastosowane do parametrów będących tablicami w natywnym C++, traktujące je jako wskaźniki, na których można wykonywać działania arytmetyczne,
344
Visual C++ 2005. Od podstaw nie mają zastosowania do C+ +/C LI. Znika wiele komplikacji , które mogą powstać wraz z przekazywani em argument ów do funkcji w natywnym C++, ale na nieuważnych wci ąż czeka je szcze jedna pułapka w C++/CLI. Wersja kalkulatora dla CLR pomoże nam zrozumieć sposób pisania funkcji w CH /CLI. Mechanizm wywoływania (throw) i przechwytywania (cat ch) wyjątków w programach CLR działa tak samo jak w programach w natywn ym C++, choć istnieją pewne różnice . Wyjątk i powodowane w programie w C++/CLI zawsz e muszą by ć zgłaszane za pomo c ą uchwytów śledzących . W konsekwencji, zgłaszane wyj ątki zawsze powinny być obiektami i w miarę mo ż liwości powinno się unikać zgłaszania literałów , a w szczególności literałów łańcuchowych . Rozważmy poniższy przykład kodu , w którym zgła sza ny i przechwytywan y jest wyjątek : t ry {
t hrow L" Z ł a p mn Ie .
je ~ l i
pot raf i sz . " '
}
catc hCSt r ing ex) { Console : :WriteLi neCL"St ri ng A
II
A :
Wyjąlek
nie zosianie lulaj prze chwycony.
(O}" .ex) .
}
Klauzula catch w powyższym kodzie nie mo że przechwycić zgłoszonego obiektu , ponieważ instrukcja t hrow zgłosiła wyjątek typu const wchar_t*, a nie typu St ri ng"". Aby móc przej ąć tak zgło s zo n y wyjątek , klauzula catc h pow inna wygląda ć następująco: t ry { th row L "Zła p mn ie.
jeś
l i potra f i sz . " ;
}
catc hCconst wc har_t* ex)
II W p o rządku -
wyją tek jest
odpo wiedniego typu.
{
Str i ng exc = gcnew Stri ngCex) ; Conso l e : :WriteLi ne tL"wchar_t ; {O }" . exc) : A
Tym razem klauzula catc h przechwyciła Aby
wyjątek, gd yż
jest on odpowiedniego typu.
do przechwycenia dla pierwszej wersji klauzuli catc h, musielikod bloku t ry:
zgłosi ć wyj ątek możliwy
b y śmy z m i en ić na stępująco
t ry { t hrow gcnew Stri ng ( L" Zła p mnie. je ś li potraf i sz . ") : } catch CString ex) II W porzqdku - wyją tek j est odpo wiedniego typu. { Consol e :WriteLi ner L"Stri ng {O j" .ex) ; } A
A
:
Teraz
wyjątek
odnoszący się
jest obiektem klasy St ri ng i został do łańcucha.
zgłoszony
jako typu St r inq", czyli uchwyt
Rozdział 6.•
Ostrukturze programu -
ciąg dalszy
345
W programach w C++/CLI możemy również korzystać z szablonów funkcji , ale zamiast nich tak zwanych funkcji generycznych (ang. generic functions) , które są bardzo podobne do szablonów, choć istnieją między nimi także pewne istotne różnice. możemy także używać
Funkcje generyczne że
funkcje generyczne na pierwszy rzut oka robią to samo co szablony funkcji i mogą to jednak różnią się one od siebie sposobem działania i różnice te sprawiają, że są one wartościowym dodatkiem w programach CLR. Gdy używamy szablonów, kompilator tworzy z nich kod źródłowy potrzebnych funkcji. Kod ten jest następnie kompilowany razem z resztą kodu programu. W niektórych przypadkach może to oznaczać wygenerowanie dużej ilości kodu i znaczne zwiększenie rozmiaru modułu wykonywalnego. Funkcje generyczne natomiast same są kompilowane i w momencie wywołania funkcji pasującej do specyfikacji funkcji ogólnej rzeczywiste typy zamieniane są na typy parametrów w czasie wykonywania programu. Podczas kompilacji nie jest generowany żaden dodatkowy kod i nie ma ryzyka zwiększenia objętości programu , tak jak w przypadku używania szablonów funkcji. Mimo
wydawać się zbędne,
Niektóre aspekty definiowania funkcji ogólnych uzależnione są od wiedzy na tematy, o których mowa dopiero w dalszych rozdziałach, ale wszystko w końcu stanie się jasne . Tutaj dostarczę tylko niezbędne wyjaśnienia nowych właściwości, a szczegółów dowiesz się póź niej w trakcie czytania książki. będzie
Deliniowanie funkcji generycznych Funkcję generyczną
definiujemy przy użyciu parametrów typu, które zostają za stąpione kiedy funkcja zostaje wywołana . Poniżej znajduje się przykładowa definicja funkcji generycznej : właściwymi typami,
generi c where T:IComparable T Ma xE lement (arrayA x) {
T ma x = x[OJ: fort int i = 1; i < x->Lengt h: 1 ++) if (max->Compa reTo(x[iJ ) < O) max = xCi J: return max : Powyższa funkcja generyczna wykonuje to samo zadanie co szablon funkcji w natywnym C++, który widzieliśmy wcześniej. Słowo kluczowe generic w pierwszym wierszu informuje, że dalej znajduje się definicja funkcji generycznej . W pierwszym wierszu znajduje się parametr typu określony symbolem T. Słowo kluczowe t ypename otoczone nawiasami trójkątnymi informuje, że znajdujący się po nim symbol T stanowi nazwę parametru typu w tej funkcji generycznej oraz że parametr ten zostanie zamieniony na rzeczywisty typ w momencie wywołania funkcji. W definicjach funkcji generycznych z wieloma parametrami typów nazwy tych parametrów umieszcza się w nawiasach trójkątnych i przed każdym z nich musi znaleźć się słowo kluczowe t ypena me oraz są one oddzielone od siebie przecinkami.
346
VisIlai C++ 2005. Od podstaw Słowo kluczowe where znaj duj ące si ę po nawiasie trójkątnym wp rowadza warunek dotyczący rzeczywistego typu, który może zostać wstawiony w miejsce symbolu T, kiedy używan a jest funkcja generyczna. Warunek w naszym przykładzie mówi, że typ, który ma zostać wstawiony w miejsce symbolu T, w funkcji ogólnej musi implementować interfejs IComparable. Interfejsam i zajmiemy się trochę p ó źn iej , a na razie wyjaśnię tylko , że oznacza to, iż typ ten musi definiować funkcję CompareTo( l, która pozwala na porównywanie dwóch obiektów tego typu. Bez tego warunku kompilator nie wi edziałby, jakiego rodzaju operacje mogą być wykonywane przy użyciu typu, który m oże zastąpić symbol T, poni eważ aż do momentu u życia funkcji ogólnej nie ma na jego temat ża dnych informacj i. Przy zastosowaniu tego warunku możemy za pomocą funkcji CompareTo () porównać wartość zmiennej max z dowolnym elementem tablicy. Funkcja CompareTo( )zwraca liczbę całkowitą mniejszą od zera, jeżeli obiekt, dla którego została wywołana (w tym przypadku ma x), jest mniejszy ni ż podany argument, zero, jeśl i jest równy argumentowi, lub liczbę większą od zera, jeżeli jest on większy od argumentu.
W drugim wierszu znajduje się nazwa funkcji generycznej Ma xEl ement , jej typ zwracany T oraz lista parametrów. Wygląd a to j ak zwykły nagłówek funkcji, z tą różnicą, że zawiera ogólny parametr typu T. Typ zwracany funkcji generycznej oraz typ elementu tablicy , który jest czę ścią specyfikacji typu parametru, są typu T, a więc oba te typy określone zostają w momencie użycia funkcji generycznej. Używanie hmkcii generycznych
Najprostszym sposobem wywołania funkcji generycznej jest użycie jej jak zwykłej funkcji. Na przykład funkcji generycznej MaxE l ement( ) z poprzedniego podrozdziału moglibyśmy użyć w następujący sposób :
array<double>A dat a = {1.5 . 3.5. 6. 7. 4.2 . 2.1} ; double ma xData = MaxE lement( dat a) : Kompilator w tym przypadku może wydedukować, że argumentem typu funkcji ogólnej jest doubl e, i wygenerować odpowiedni kod do wywołania tej funkcji . W czasie wykonywania tej funkcji, wszystkie miejsca wystąpienia symbolu T zastąpione zostają określeniem typu double. Jak już powiedziałem wcześniej, to nie jest to samo co funkcja szablonowa - podczas kompilacji nie są tworzone żadne egzemplarze funkcji. Kiedy zostaje wywołana, skompilowana funkcja generyczna potrafi przyjąć zamienniki argumentów typu . Warto zauważyć, że jeżeli do funkcji generycznej przekażemy jako argument literał łań cu chowy, kompilator dojdzie do wniosku, że typ argumentu to St ri ng bez względu na fakt, C"Ej jest to stała łańcuchowa typu "Witam! ", czy też typu L"Wi t am ! ''. A
,
Istnieje możliwość, że kompilator nie będzie mógł odgadnąć typu argumentu z wywołania funkcji generycznej. W takich przypadkach najlepiej jest podać argumenty typu jawnie w trójkątnych nawiasach po nazwie funkcji w wywołaniu. Na przykład wywołanie w poprzednim fragmencie kodu moglibyśmy zapisać następująco:
double ma xData = MaxEl ement<double>(dat a) : Przy jawnie
określonych argumentach
typu nie ma mowy o dwuznaczności .
Rozdzial6. - ostrukturze programu -
ciąg
dalszy
347
Istni eją pewne ograniczenia d o tyczące tego, co mo żna przek azywać jako argument typu do funkcji ogólnej . Argument ten nie może by ć typem klasowym natywne go C++ ani natywnym wskaźnikiem lub referencją, a także uchwytem do typu klasy wartości, takim j ak mt". Z tego wynika, że dozwolone są tylko typy klas wartoś ci, takie jak .j nt czy doub 1e, oraz uchwyty ś le dzące , takie jak Stri nq" (ale nie uchwyty do typów klas wartości).
Wypróbujmy to na przykładzie.
mmtmI Używanie funkcji generycznych Poniżs zy
kod definiuje trzy funkcje generyczne i z nich korzysta:
#include "st dafx.h" using namespace Syst em: II Funkcj a ge neryczna znajdująca
największy
element tablicy.
generic where T: IComparable T Ma xE lement( arrayAx) (
T max ~ x[O ] : for( i nt i ~ 1: i < x-c-Lenqt h: i ++ ) if( max·>CompareTo(x[i ] ) < O) max ~ x[i]: return ma x: II Funkcja generyczna
usu wająca
element z tablicy .
generic where T: IComparable arrayA RemoveElement( T element . arrayA dat a) {
arrayAnewData = gcnew array(dat a· >Lengt h . l ): i nt index ~ O: II Indeks do elementów tablicy newData. boal found = fal se: II Wskazuje, że zos tał znaleziony element do usuni ę cia.
for each(T item in dat a) ( II Sprawdzanie nieprawidlowego indeksu lub znalezionego elementu.
Console : .Writ eLi ne(L"Nie znaleziono element u do ret urn data : }
newDat a[index++]
=
it em :
us u ntęc ta " ) :
348
Visual C++ 2005. Od podstaw ret urn newDa ta : II Funkcja generyczna
wyświetlająca
elementy tablicy.
generic where T:IComparable void Lis tElements (arrayA data ) (
for each(T item in da ta) Consol e : Wrlte( L"{ O. l O)" . item); Conso l e: :Wr lteL iner ): int main(a rray<System : :Str ing A> Aargs ) {
array<double>Adat a = {1.5. 3.5. 6.7. 4.2. 2. l }: Console : :Writ eLi ne(L"Tabl ica zawiera:" ): Li st Element s(dat a); Console: :W r it e L i n e ( L" \ n N aj w i ęk s z y element = {O} \ n". MaxE l ement (dat a)) : array<double>A result = RemoveElement (MaxElement(data J. data) : Console : :Wri t eLine(L" Po us un ięc i u na jw ię ks zeg o element u t abl ica zawiera. ") , Li st Element s(result ); ar rayct nt >" numbers = {3, 12. 7. O. 10. U} :
Conso le : :WrlteLi ne(L"\nTabl ica zawiera:" ): List Elements(n umbe rs): Conso le : : W r it e L i n e ( L " \ n N a j w i ę k s zy el ement = (O}\ n" . MaxE lement (numbers )) : Console : :Wr1t eLine(L"\nPo u s u n i ęc i u naj w ię ks zego elementu t abl ica zawier a:"); Li st Elements( RemoveElement (MaxElement( numbers) . numbers )) : arrey-St r tnq" >" st rings = {L"Dwie ". L " g ł owy ". L"to ". t' nt e" . L"jedna"} : Console: :Wri t eL ine(L"\ nTablica zawiera ''' ); Li st Element s Cst ri ngs): Console : :WrlteLi n e C L " \ n N a j wi ę k szy element = {O }\n". MaxElement (st rings)): Conso le : :WriteLine(L"\ nPo us u n ięc i u na jw i ę ks zeg o elementu t abli ca zawie ra:"): ListElements (RemoveE lement( MaxElement (st ri ngs) . st ri ng s)) ; return O:
Wynik d ziałania tego programu jest następujący: Tabl ica zawier a: 1. 5 3.5 Najw iększy
Po
element
=
6.7
3.5
Tabl ica zaw1e ra: 3 12 Najwię k sz y
Po
element
=
7
elementu t abli ca zawiera: 4.2 2. 1 7
O
10
12
u s u n i ęc i u najwięk sze go
3
2.1
6.7
us u n ięc i u n a j w i ę k s z eg o
1. 5
4.2
elementu t abl ica zawie ra: O
10
11
11
Rozllzia.6.• Osll'uklurze programu Ta blica zawiera: Owie g łowy Naj w i ęk s z y
Po
to
nie
ciąg
dalszy
349
j edna
element = t o
usu nię c i u najw ięk s z eg o
Dwie
głowy
element u t abl ica zawiera: nie jedna
Jak lo działa Pierwszą funkcją generyczną zdefiniowaną
z
w tym kodzie jest funkcja MaxEl ement ( ), identyczna element tablicy, a więc nie
funkcją, którą widzieliśmy wcześniej, znajdującą największy
będę jej już
tutaj
opisywał .
Następna
funkcja ogólna RemoveElement s( ) usuwa element przekazany jako pierwszy argument z tablicy określonej przez drugi argument. Funkcja zwraca uchwyt do nowej tablicy powstałej w wyniku tej operacji. W pierwszych dwóch wierszach definicji funkcji widać, że zarówno typy parametrów, jak i typ zwracan y zostały okre ślone za pomocą symbolu T.
generic where T: IComparable arrayA RemoveEl ement(T element. arrayAdat a) Warunek przy symbolu Tjest taki sam jak w pierwszej funkcji generycznej, czyli narzuca, aby typ użyty jako argument typu implementował funkcję CompareTo( ), która pozwala na porównywanie obiektów tego typu. Drugi parametr i typ zwracany są uchwytami do tablicy elementów typu T. Pierwszy parametr jest po prostu typem T. Najpierw funkcja tworzy
tablicę
do przechowywania wyników:
arrayAnewOata = gcnew array(data->Length - l ) : Tablica newDa t a jest tego samego typu co drugi argument (tablica elementów typu T), ale ma o jeden element mniej, ponieważ jeden ma zostać usunięty z oryginalnej tablicy . Elementy z tablicy data do tablicy newDa t a kopiowane
int i ndex = O: boal found = false; for each(T lt em in data)
są w pętli
f or each:
II Indeks do elementów tablicy newData. II Wskazuje . że został znaleziony element do
( II Sprawdza nie nieprawidłowego indeks u lub znalezionego elementu,
Console : :Writ el1ne(L"Nie znaleziono element u do ret urn dat a:
u s un i ę c i a" ) :
usuni ę cia ,
350
Visual C++ 2005. Od podstaw newData[ index++]
~
i t em :
Skopiowane zostają wszystkie elementy z wyjątkiem jednego zidentyfikowanego przez pierwszy argument funkcji. Zmiennej i ndex używamy do zaznaczenia kolejnego elementu tablicy newData, który ma otrzymać następny element z tablicy data . Każdy element tablicy data zostaje skopiowany, chyba że j est on równy zmiennej el ement . W takim przypadku zm ienna f ound zostaje ustawiona na true i instrukcj a cont i nue przeskakuje do następnego powt órzenia. Istnieje możliwość , że w tablicy znajdzie się więcej elementów takich samych jak pierwszy argument. Zmienna found zapobiega pomijaniu w p ętli kolejnych elementów, które są takie same j ak el ement . Mamy także sprawdzanie zmiennej i ndex przekraczającej dozwolony limit indek sowanych elementów w tablicy newDa ta . Mogłoby się to zdarzy ć w przypadku, gdy w tablicy data nie ma ani jednego elementu równego pierwszemu argumentowi funkcji. W takiej sytuacj i zwracany jest tylko uchwyt do oryginalnej tablicy. Trzecia funkcj a generyczna tylko
wyświetla
elementy tablicy ar rey-T>:
gener ic voi d Lis tElements(ar rayAdata) (
for each(T it em i n data) Console: :Wrlte(L "{O,lO)" , item) ; Conso le: ;Wrl t eLt net i: Jest to j edna z nielicznych sytuacji , w których nic j est potrzebny żaden warunek dla parametru typu . Z obiektami niewiadomego typu można zrobić bardzo niewiele i dlatego też parametry typów funkcji generycznych zazwyczaj m aj ą warunki . Działanie funkcji jest bardzo prosteza pomocą pętli f or eac h każdy element tablicy wysyłany jest do wiersza pol eceń w polu o szerokości 10. Moglibyśmy zrobić to jeszcze lepiej, dodając parametr dla szerokości pola i tworząc łańcuch formatujący , którego moglibyśmy użyć jako pierwszego argumentu funkcji Write O w klasie Conso le. Moglibyśmy również dodać do pętli mechanizm wysyłający określoną liczbę elementów do wiersza na podstawie s ze roko ś c i pola. Funkcja ma i n( ) wykonuje te wszystkie funkcj e generyczne przy użyciu param etrów typów doubl e, int oraz St r tnq". A zatem widzimy , że wszystkie trzy funkcje generyczne mogądzia łać z typami wartośc i i uchwytami. W drugim i trzecim przykładzie funkcje generyczne zostały użyte łącznie w jednej instrukcji. Spójrzmy na poni ższą przykładową in strukcję użycia funkcji:
ListElement s(RemoveE lement( MaxElement (st rl ng s) . str i ngs) ) ; Pierwszy argument funkcji generycznej RemoveElement O jest wygenerowany przez funkcję generyczną M axE l ement ( ), a więc funkcje te mogą być używane w taki sam sposób jak zwyczajne funkcje . We wszystki ch przypadkach użycia funkcji generycznych kompilator może samodzielnie odgadnąć argumenty typu, ale je śli chcemy, możemy je określić jawnie. Na przykład poprzednią instrukcję moglibyśmy zapisać następująco :
List El ement s(RemoveElement <Stri ngA>(MaxEl ement <St rl ngA>(st rlngs ) . str ings) ) ;
Rozdział 6.•
Ostrukturze programu -
ciąg
dalszy
351
Kalkulator ClB Spróbujmy teraz przepisać nasz kalkulator w języku C++ /CLI. Przyjmiemy tę s amą strukturę programu oraz hierarch ię funkcji co w programie w natywnym C++, ale defini cje i deklaracje funkcji będąjuż napisane w CH /CLI. N a dobry początek zaczniemy od prototypów funkcji na samym początku pliku źródłowego (projekt ten ma nazwę Cw6.11): II Cw6_ 11.cpp: main project fi le. II Kalkul ator CLR obs ługujący nawiasy .
#include "stdafx.h" #i nclude
II D/a funkcji exiu) .
using namespace System ; Str i ngA eatspacesCStr i ngA st r ) ; doubl e expr(Str ingA str ); do uble t erm(Stri ngA st r . i nt A i ndex) ; double numberCStr i ngA str. i nt A i ndex) ; Stri ngA extract (St ringA str . int A index):
II Funkcj a u s uwając a spacje. II Funkcj a ob liczająca war/ oś ć wyrażen ia. II Funkcja a na liz ująca s k ładn i k. II Funkcja rozp o zn ają ca liczby. II Funkcja wydo bywają ca podtancuchy .
teraz uchwytami . Parametry łańcuchowe s ą typu St ri ng a parametr i ndex zap i suj ący bi eżące położenie w łańcuchu jest typu i nt ' . Oczywiście łańcuch zwracany jest jako uchwyt typu St r i ng Wszy stkie parametry
są
A
,
A
•
Implementacj a funkcji ma i n()
wygląda następująco :
i nt mai n(array<System : :Stri ng A> Aargs) {
St ri ng A buff er :
II Obszar
wejścio wy
do oblicza nia
war/ości wyrażeń .
Console: :Wri tel i net l "Wi t aj w naszym przyjaznym kal kulatorze. ") ; Console: :Writel i ne(l " Wp r owa d ź jaki e ś wyrazenie l ub pust y ~J i ersz . aby
z a k oń c zy ć . " ) ;
fort : :) {
buffer = eatspaces(Console: :Readl i ne() );
II Wczytaj wiersz dany ch
if (Stri ng: : IsNullOrEmptyCbuff er )) ret urn O:
II Pusty wiersz
Console : :Writ el i net t."
=
{O}\ n\n" .expr(buffer )): II
wejściowych .
koń czy pracę
kalkulatora.
Wyś lij na wyjście wartość wyrażenia .
}
ret urn O: łatwiejsz a do odczytania. Wewnątrz nieskończonej pętli for do klasy Stri ng IsNull Or Empty() . Funkcja ta zwraca t rue, jeżeli łańcuch przekazany jako argument jest nu11 lub zerowej długo ści, czyli robi dokładnie to, co jest nam potrzebne.
Funkcja ta jest o wiele krótsza i wywołuj emy funkcję należ ącą
352
Visual C++ 2005. Od podstaw
Usuwanie spacji złańcucha wejściowego u suwaj ąca
Funkcja
II Funkcja
równ ież jest
spacje
usuwająca
krótsza i pros tsza:
łań cucha .
spacje z
StringA eat spacesCStri ng A st r) { II Tablica przech o wująca
łań cllchy
bez spa cji.
array<wcha r_t >Achars = gcnew array<wchar_t>Cst r ->Lengt h); 1 nt lengt h = O; II Liczba znaków w tablicy. nieb ędąc e
II Skopiuj znaki
spa cjami do tablicy cha rs.
for eachCwc har_t ch in str ) if Cch 1= ' ' ) chars[l engt h++] = ch; II Zwróć
tab licę
chars jako
ła ńc uch.
ret urn gcnew St ringCc hars , O, lengt h); Naj pierw tworzy my tabl ic ę , w której zostanie umieszczony łań cuch poddany procesowi usuwania spacji , Jest to tablica eleme ntów typu wcha r _t , pon ieważ łańcuc hy w C++/CLI s kła dają się ze znaków Unieode. Proces usuwania spacj i j est bardzo pros ty - wszystkie znaki, które nie są spacjami, kopiujemy z łańcuch a st r do tablicy chars, li c z bę skopiowanych znaków przechowując w zmiennej l ength. Na koniec tworzy my nowy obiekt klasy St ri ng za p omocą konstruktora tej klasy, który two rzy obiek t z eleme ntów tablicy. Pierwszym argumentem przekazanym do konstruktora jes t tablica, która stanowi źródło znaków dla łańcuc ha, drugi argument to indeks pierwszego znaku z tablicy zawierającej łań cuch , a trzeci argume nt to c ałkowita liczba znaków w tablicy, które maj ą zostać u żyt e . W klasie St ri ng zdefiniowa nych je st więcej konstruktorów s łużących do tworzenia łań cu ch ó w na ró żn e sposo by.
Obliczanie wartości wyrazenia arytmetycznego P oni ż ej
znajduje
II Funkcja
się
imp lementacja funkcji
obliczająca wartość wyrażenia
double exprCSt r lngA str )
ob liczającej wartość wyrażen ia:
arytm etycznego .
{ II Śledzi położenie bieżącego znaku.
int i ndex = O; A
double value
~
term Cst r , index);
II Pobierz pierwszy element.
whileC*i ndex < str ->Leng t h) {
switchCst r[ *index ])
II Wybierz
działanie
zgodne z
b i eżą cym
znaki em.
{
case '+' ; ++ C*index) ; val ue += t ermCstr , index) ; break;
II Znaleziono znak +, II a wi ę c zwiększ ws kaźn ik index o jeden i dodaj II następny składn ik.
case ' - ' ; ++ C*i ndex);
II Znaleziono znak -, a więc II zmni ej sz wskaźnik index o jeden i dodaj
Rozdzial6.• ostrukturze programu value -= t erm(st r . l ndex) : break: defaul t:
ciąg
dalszy
353
II następny składnik.
II Wykonanie lego kodu oznacza, II wyrażenie jest nieprawid/owe.
Console: :Wr lteLl ne( L"Ar rrgh!*#! I Tu Jest extt t l i:
że
wprowadzone
b ł ą d .v n") :
}
ret urn va lue : Zmienna i ndex została zadeklarowana jako uchwyt, ponieważ chcemy ją przekazać do funkcji t erm ( ), która z kolei ma zmodyfikować oryginalną zmienną. Gdybyśmy zmienną i ndex zadeklarowali po prostu jako typ i nt, to funkcja te rm() otrzymałaby tylko kopię jej wartości i nie mogłaby zmienić oryginalnej zmiennej . Deklaracja zmiennej i ndex spowodowała zgłoszenie przez kompilator komunikatu ostrzegawczego, ponieważ instrukcja ta polega na autopakowaniu (ang. autoboxing) wartości O w celu utworzenia obiektu klasy wartości I nt32, do którego odnosi się uchwyt. Ostrzeżenie jest zgłaszane, ponieważ często można się natknąć na instrukcje, w których uchwyt jest inicjalizowany wartością zerową. Oczywiście, aby to zrobić, należy zamiast Ojako wartości począt kowej użyć null ptr. Jeżeli chcemy, aby ostrzeżenie się nie pojawiało, możemy tę instrukcję przepisać następująco: i nt
Ą
i ndex
Powyższa
go
~
gcnew i nt( O) ;
instrukcja jawnie używa konstruktora do utworzenia obiektu i zainicjalizowania O, dzięki czemu kompilator nie zgłasza żadnego ostrzeżenia .
wartością
Po przetworzeniu pierwszego składnika za pomocą funkcji term() pętla whi l e przeszukuje w celu znalezienia operatora + lub -, po którym znajduje się następny składnik. Instrukcja switch identyfikuje i przetwarza te operatory, Jeśli przyzwyczailiśmy się do natywnego C++, to możemy czuć pokusę napisania instrukcji case w instrukcji switch trochę inaczej, na
łańcuch
przykład :
II Nieprawid/owy kodt! Nie
działa!!
case '+' : value +~ t erm(st r . ++(*index));
II Znaleziono znak +, a więc
II zwiększamy zmienną index o jeden i dodajemy II następny składnik.
break: Oczywiście
zwykle zapisalibyśmy ten kod bez pierwszego komentarza. Kod ten jest nieale dlaczego? Funkcja term( )jako drugiego argumentu spodziewa się uchwytu typu i nt Ą i to zostało jej tutaj podane, mimo że nie jest to to, czego byśmy się spodziewali. Kompilator powoduje obliczenie wartości wyrażenia ++( *i ndex ) oraz przechowanie wyniku w lokalizacji tymczasowej . Wyrażenie to rzeczywiście zwiększa wartość wskazywaną przez i ndex, ale uchwyt przekazywany do funkcji t erm ( ) jest uchwytem do lokalizacji tymczasowej, przechowującej wynik obliczonego wyrażenia, a nie uchwytem i ndex. Uchwyt ten został utworzony poprzez samopakowanie wartości przechowywanej w lokalizacji tymczasowej. Kiedy funkcja te rm( ) aktualizuje wartość wskazywaną przez uchwyt do niej przekazany, aktualizowana jest lokalizacja tymczasowa, a nie lokalizacja wskazywana przez i ndex. A zatem prawidłowy ,
354
Visual C++ 2005. Od podstaw wszystkie aktualizacje indeksu łańcucha wykonane w funkcji t er m( ) zo stają utracone. Jeśl i oczekuje sz, że funkcja uaktualni zmienną w wywołującym programie, to nie możesz używać wyrażenia jako jej argumentu - zawsze w takich przypadkach używaj uchwytu do nazwy zmiennej.
Sprawdzanie wartości składnika Podobnie jak w wersji w natywnym C++, funkcja term( ) prze szukuje łańcuch przekazany do niej jako pierwszy argument, zaczynając od znaku o indeksie wskazywanym przez drugi argument. II Funkcja
sprawdzająca wartość składn ika.
double term(Stri ng st r . int A
A
index)
(
double value
number( st r . index) ;
~
II Powtarzaj, dopóki
while(*i ndex
Lengt h)
(
if (st r[*index] == L'* ' )
II Jeś li znajdziesz znak mnożenia,
(
++(*index) ; value *= number (st r. i ndex);
II zwiększ index i II i pomnóż przez
następną liczbę .
}
else if( st r[*index] == L' j ' )
II Jeśli znajdziesz znak dzielenia,
{
++ (*i ndex): value j~ number (str . i ndex) ;
II zwiększ index II i p odziel przez
następną liczbę.
}
el se break;
II
Wyjdź
z pętli.
} II Skończone, a
wi ęc
zw racamy, co
uzyska l iśmy.
ret urn va l ue: funkcji number( ) w celu sprawdzenia wartości pierwszej liczby lub wydobycia w nawiasie w składn iku funkcja przeszukuje łańcuch za pomocą pętli whi le. Pętla ta kontynuuje działanie, gdy w łańcuchu cały czas dostępne są znaki, dopóki nie zostanie odnaleziony operator * lub / poprzedzający jakąś inną liczbę czy wyrażenie w nawiasie.
Po
wywołaniu
wyrażenia
Sprawdzanie wartości liczby Funkcja number ( ) wydobywa i oblicza wartość wyrażenia w nawiasie, jeżeli takie zostanie znalezione. W przeciwnym przypadku określa wartość następnej liczby w łańcuchu : II Funk cja
rozpoznają ca l iczbę .
double number (Stri ng st r . i nt i ndex) A
A
(
double val ue = 0.0: II Poszukiwanie
wyrażenia w
i f (st r[*i ndex]
==
II Do przechowywania wyniku . nawi asach.
L' (' )
II Początek nawiasu.
Rozdział 6.
• Ostrukturze programu -
++ (*indexl, Str i ng substr = extract( str , i ndexl : ret urn expr(substr l : A
II Pętla zbierająca
whil e((*index
Lengt hl && Char: . Lsflt qi t t str . *i ndex))
{
value = 10 .0*value + Char : :Get Numer icVal ue(st r[ (*i ndexl] l : ++ (*i ndexl : niebędący cyfrą.
II Znaleziono znak
if(( *i ndex -- st r ->Lengt hl II st r[ *index]
1=
' .
'l
II A więc poszukuj emy przecinka II dziesiętnego . II Jeśli nie ma, z wracamy zmienną II value.
ret urn va l ue: double factor = 1.0 : ++(*1ndex):
II Współczynn ik dla miejsc po p rzecinku. II Przesunięci e do cyf ry.
II Powtarzaj . dopóki są cyfry.
while( (*i ndex
Lengt hl && Char:: IsDigi t (st r . *indexl l
{
factor *= 0.1; II Zmniej sz wspólczynn ik dzies ięciokrotn ie. va l ue = value + Char: :Get Numeri cVa l ue(st r[ *i ndex]l *factor ; li Dodaj miejsce II p o przeci nku.
++( *i ndexl : ret urn va l ue;
II Koniec pętli -
skoń cz one.
Podobnie j ak w wersji w natywnym C++, funkcja ext ra ct() została użyta do wydobycia nawiasów, a wydobyty łańcuch zostaje przekazany do funkcji expr ( ) w celu obliczenia jego wartości. Jeżeli żadne wyrażenie w nawiasach nie zostanie znalezione (na co wskazuje brak otwierającego nawiasu) , łańcuch wejściowy jest skanowany w poszukiwaniu liczby , która składa się z szeregu zera lub w iększej liczby cyfr , po których nastę puje opcjonalny przecinek dziesiętny plus cyfry składające się na część ułamkową. Funkcja IsDigi t O pochodząca z klasy Char zwraca wartość t ru e ,jeżeli znak jest cyfrą, lub fa lse w przeciwnym przypadku. Znak ten znajduje się w łańcuchu przekazanym do funkcji jako pierwszy argument w lokalizacji wskazywanej przez drugi argument. Istnieje także inna wersja funkcji IsDigit O przyjmująca pojedynczy argument typu wchar_t ; moglibyśmy jej użyć z argumentem str [*i ndex]. Pochodząca z klasy Cha r funkcja GetNumeri cValueO zwraca jako typ doub1e wartość znaku cyfry Unicode, która przekazywana jest jako argument. Istnieje także inna wersja tej funkcj i, do której można przekazać uchwyt do łańcucha oraz indeks określający konkretny znak . wyrażen ia spomiędzy
Wydobywanie wyrażenia wnawiasach Funkcję
ext ract ()
tować następująco :
zwracającą podłańcuch znajdujący się
w nawiasach
możemy
zaimplemen-
356
VisUalC++ 2005. Od podstaw wydobywająca podlańcuch IV
II Funkcj a
na wiasach.
St ri ng A extract (Str i ng A st r . 1nt A index) ( II Tym czasowe miej sce dla podlań cu cha .
arr ay<wcha r_t>A buffer = gcnew array<wc har_t>(st r- >Length ): Stri ngA subst r : II Podlancuch. ktory ma zosta ć zwrócony. i nt numL = O; II Licznik znalezionych lewych nawiasów . i nt bufi ndex = *1ndex: II Zachowaj wart oś ć po czątkową wskaźnika index. whi le(*lndex < st r ->Length) {
bu ffer[*index - bufindex] swi t ch(st r[*i ndex])
~
st r[*i ndex] ;
(
case ') ': if(numL == O) {
array<wchar_t>A subst rChars = gcnew array<wcha r_t >(*l ndex - bufi ndex): st r->CopyTo (buf index. subst rChars . O. substrChars->Length ): subst r = gcnew Str i ng( substrChars): ++( *i ndex): ret urn subst r :
II Zwróć
lań cuch
w nowej pam ięci .
}
el se numL -- : break: case . (' : numL++ : break :
W tym przypadku znowu strategia jest taka sama jak przy użyciu natywnego C++, ale różni w szczegółach . W celu znalezienia pasującego nawiasu zamykającego funkcja w zmie nej Ln um zapisuje liczbę nowych nawiasów otwierających. Podłańcuch zostaje wydobyt gdy zostaje odnaleziony nawias zamykający oraz wartość licznika lewych nawia sów Lnumw nosi zero. Podłańcuch ten zostaje skopiowany do tablicy substrCha rs za pomocą funkcji Cop To( ) dla obiektu klasy Str i ng o nazwie str. Funkcja kopiuje znaki , rozpoczynając od znal znajdującego się w miejscu określonym przez pierwszy argument, do tablicy określon w drugim argumencie. Trzeci argument określa element, od którego zacząć operację w tabli docelowej, a czwarty określa liczbę znaków do skopiowania. Łańcuch zwrócony w wynil wydobywania tworzymy za pomocą konstruktora klasy St ri ng, który z wszystkich element ć tablicy buduje obiekt - subst rCha rs , przekazywany jako argument. tkwią
Po
złożeniu
wszystkich tych funkcji w jedną całoś ć w projekcie konsolowym CLR otrzymar C++ /CLI kalkulatora, działającą pod kontrolą CLR. Rezultat powinien b taki sam j ak przy użyciu natywnego C+ +.
implementację dokładnie
Rozdział 6.•
Ostrukturze programu -
ciąg ltalsz,
357
Podsumowanie Mamy już szeroką wiedzę na temat tworzenia i stosowania funkcji. Użyliśmy wskaźnika do funkcji w praktycznym kontekście w celu obsłużenia sytuacji wyjątkowej związanej z brakiem pamięci w obszarze wolnym. Zastosowaliśmy także przeładowywanie funkcji w celu zaimplementowania zestawu funkcji wykonujących te same zadania, ale z parametrami różnych typów . Więcej na temat przeładowywania funkcji dowiemy się w następnych rozdziałach . Poniżej
znajduje
się
lista
najważniejszych zagadnień
poruszonych w tym rozdziale:
•
Wskaźnik do funkcji przechowuje jej adres oraz informacje parametrów oraz typu zwracanego przez funkcję.
•
Wskaźnika do funkcji można użyć do przechowywania adresu dowolnej funkcji z właściwym typem zwracanym oraz liczbą i typem parametrów.
•
Wskaźnik
do funkcji
Wyjątkiem
liczby i typu
można wykorzystać
Wskaźnik można również przekazać
•
dotyczące
do jej wywołania w adresie, który zawiera. do funkcji jako jej argument.
jest sposób sygnalizowania błędu w programie, tak od kodu pozostałych operacji .
że
kod
obsługi
tego
błędu można oddzielić
•
Wyjątki wywołuje się
•
Kod mogący powodować wyjątki powinien być umieszczony w obrębie bloku t ry, a kod obsługujący określony wyjątek w klauzuli cat ch znajdującej się bezpośrednio po bloku t ry. Po bloku try może wystąpić kilka klauzul cat ch, z których każda przechwytuje wyjątek innego typu.
•
Funkcje przeładowane to funkcje o takiej samej nazwie, ale różnej
•
Kiedy wywoływana jest przeładowana funkcja, ta, która ma zostać wywołana,
wybierana jest przez kompilator na podstawie liczby oraz typów podanych argumentów.
•
za
pomocą
instrukcji z użyciem
słowa
kluczowego throw.
liście
parametrów .
Szablon funkcji jest przepisem na automatyczne wygenerowanie funkcji przeładowanych.
•
Szablon funkcji ma co najnmiej jeden argument, który jest zmienną typu. Egzemplarz funkcji (tzn. definicja funkcji) tworzony jest przez kompilator przy każdym wywołaniu funkcji , które odpowiada unikalnemu zestawowi argumentów typu dla szablonu.
•
Kompilator
można zmusić
funkcję, którą chcemy Zdobyliśmy także
do utworzenia egzemplarza funkcji z szablonu, w deklaracji prototypu.
określając
nieco doświadczenia w używaniu wielu funkcji w programie, tworząc przykalkulator. Należy jednak pamiętać, że wszystkie dotychczasowe techniki stosowania funkcji były wykorzystywane w kontekście tradycyjnego programowania proceduralnego. Kiedy zaczniemy programowanie w podejściu zorientowanym obiektowo , to nadal będziemy bardzo często używać funkcji, ale nasze podejście do struktury programu oraz projektowania rozwiązań problemów ulegnie radykalnej zmianie.
kładowy
358
Visual C++ 2005. Od podstaw
Ćwiczenia Kod
ź ró d łowy
wszystk ich
przykład ów
w tej
książce
oraz
rozwiązania
do
ćwiczeń można
pobrać ze strony www.helion.pl.
l
Przyjrzyj
się pon iższej
funkcji:
i nt ascVal (s i ze_t i . const char* p)
{ II Drukuj wartos ć AS CII znaku. i f ( Ip II i > st rlen(p))
ret urn - l ; el se retu rn p[ i]:
Napisz program wywołujący tę funkcję poprzez wskaźnik i potwierdzający, że d ziała . Do wykonania tego zadania będz ie potrzebna dyrektywa #i nc l ude dołączająca nagłówek w celu umo żliwienia korzystan ia z funkcji st r l en( l-
2. Napisz rodzinę funkcji przeciążonych o nazwie equal O , które przyjmują dwa argumenty tego samego typu , zw racające l , jeżeli argumenty te są równe, lub Ow przeciwnym przypadku. Napis z wersje z typami argumentów char, i nt, doub l e oraz char*. Do sprawdzani a, czy łańcuch y są równe , wykorzystaj funkcję st rcmp( l z biblioteki wykonawczej . Jeżeli nie wiesz , jak u żyć funkcji st rcmp( l, poszukaj informacji na ten temat w internecie. Potrzebna będzie dyrektywa #include dołączająca nagłówek do programu. Napisz kod sprawdzający, czy w ywoływan e są właściwe wersje funkcji.
a.
Jeżeli
do kalkulatora w obecnej postaci podamy nieprawidłowy łańcuch , to ukaże komunikat o błędzie , ale nie informujący o miejscu jego wystąpienia. Napisz procedurę drukującą wprowadzony łańcuch i umieszczającą znak daszka (A) pod znakiem, który spowodował błąd , jak pon iżej : się
12
+
4.2*3
.. Dodaj do kalkulatora operator potęgowania C"), umieszczając go na równi z operatorami * i /. Jakie są ograniczenia takiej implementacji i jak można sobie z nimi poradzić?
S. Dla zaawansowanych: rozszerz kalkulator, aby obsługi wał funkcje trygonometryczne i inne funkcje matematyczne, pozw alając na wpisywanie
wyrażeń
takich jak:
2 * s i n(O .6)
Wszystkie funkcje z biblioteki mat h pracują na radianach. Stwórz własn e wersje funkcji trygonometrycznych pozwalające na używanie stopni, na przykład: 2
* sind (30)
7 Deliniowanie własnych
typÓW danych
Rozdział ten poświęcony jest tworzeniu własnych typów danych, które służą do rozwiązy wania pewnych określonych problemów. Będziemy także mówić o tworzeniu obiektów, co stanowi podwaliny programowania zorientowanego obiektowo. Początkującym obiekty mogą wydawać się trochę tajemnicze, ale - jak przekonamy się w tym rozdziale - są one po prostu egzemplarzami naszych własnych typów danych . się :
W rozdziale tym dowiesz •
Czym
są struktury
i jak się ich używa.
•
Czym
są
•
Jakie
•
Jak
•
Jak kontrolować
•
Czym
•
Czym jest konstruktor
•
Jak
•
Czym jest konstruktor kopiujący i jak wygląda jego implementacja.
•
Jaka jest różnica pomiędzy klasami w C++/CLI a klasami w natywnym C++.
•
Jakie
•
Czym
są
•
Czym
sąpola
•
Czym jest konstruktor statyczny.
klasy i j ak
się
ich
używa.
są podstawowe składniki
się
klasy oraz jak się definiuje typy klasowe .
tworzy i używa obiektów klas. dostęp
są konstruktory
używać
do
składowych
i jak się je tworzy. domyślny.
referencji w
kontekście
właściwości mają klasy
pola
klasy .
literałowe
klas.
w C++/CLl oraz jak się je definiuje i ich używa.
oraz jak się je definiuje i ich używa.
i nitonly oraz jak sięje definiuje i ich używa .
360
Visual C++ 2005. Od podstaw
S'lruktury W jęZykU C++
Struktura to typ definiowany przez programistę za pomocą słowa kluczowego st ruct. Słowo to pochodzi jeszcze z języka C, a C++ przejął je oraz rozszerzył jego zakres. W C++ strukturę można zastąpić klasą, gdyż wszystko, co można za jej pomocą osiągnąć, można również zro bić, używając klas . Jednak ze względu na fakt , że system Windows został napisany w języku C, zanim zaczęto szerzej używać C++, słowo kluczowe struct jest wszechobecne w progra mowaniu dla tego systemu. Struktury są do dziś używane, dlatego należy je znać. Najpierw zajmiemy się strukturami (w stylu C), a później przejdziemy do oferujących większe możli wości klas.
Czym jest slruktura Prawie wszystkie zmienne, które widzieliśmy do tej pory, mogły przechowywać dane jednego typu - liczbę , znak lub tablicę elementów tego samego typu. Prawdziwy świat jest jednak trochę bardziej skomplikowany. Do opisania dowolnego obiektu fizycznego, nawet w mini malnym stopniu, potrzebujemy co najmniej kilku jednostek danych. Pomyślmy na przykład , ile informacji trzeba podać, aby opisać tak prosty przedmiot jak książka. Możemy podać autora, wydawcę, datę wydania, liczbę stron, cenę, tematykę oraz numer ISBN i nie jest to bynajm niej koniec listy. Do przechowywania każdego z wymienionych parametrów potrzebnych do opisania książki możemy zdefiniować oddzielną zmienną, ale najlepiej by było , gdybyśmy mieli jeden typ danych, na przykład KSIAZKA, który reprezentowałby wszystkie te typy . Jestem pewien, że się nie zdziwisz, kiedy powiem, że do tego właśnie celu używa się struktur.
Definiowanie struktury Pozostańmy
przy przykładzie z książką. Przypuśćmy, że w jej definicji chcemy umieścić nastę informacje: tytuł, autor, wydawca oraz rok publikacji. Strukturę do przechowywania tych informacji możemy zdefiniować w następujący sposób: pujące
st ruct KSI AZ KA {
cha r Tyt ul [BO);
char Aut or[ BO) ;
cha r Wydawca[BO);
i nt Rok;
}; Powyższy kod nie tworzy żadnych zmiennych, ale definiuje nowy ich typ o nazwie KSI AZ KA. W definicji tej użyliśmy słowa kluczowego struct , a obiekty składające się na naszą książkę podaliśmy pomiędzy nawiasami klamrowymi. Warto zauważyć, że każdy wiersz zawierający definicję obiektu struktury zakończony jest średnikiem oraz że pojawia się on także po klamrze zamykającej . Elementy struktury mogą być dowolnego typu, z wyjątkiem takiego samegojak struktura. Nie można umieścić elementu typu KSIAZKA w definicji struktury KSI AZKA. Może się wydawać, że jest to pewne ograniczenie, ale zamiast tego do definicji możemy wstawić wskaź nik do zmiennej typu KS IAZKA, o czym przekonamy się już niebawem .
Rozdział 7.•
Deliniowanie własnych typÓW danych
361
Elementy Tytu l , Autor , Wydawc a oraz Rok, znajdujące s ię pomiędzy nawiasami klamrowymi , nazywają s i ę składowymi lub polami struktury KS IAZ KA. Każdy obiekt typu KSIAZKA zawiera pola Tytu l , Autor, Wydawca oraz Rok. Zmienne typu KSIAZKA możem y teraz tworzyć w dokładn ie taki sam sposób jak każde inne zmienne: KSI AZKA Powiesc :
II Deklaracja zmi ennej Powiesc typu KSIAZKA.
w powyż szym kodzie zad ek l aro wa l iś my zm i e n n ą o nazwie Pawi esc, której możemy używać do przech owywania informacji o ksi ążc e . Jedyne, czego teraz potrzebujemy, to nauczyć si ę umieszc zania informacji w poszczególnych składowych, które składają się na zmienną typu KS I AZKA.
Inicializowanie struktury Pierwszym sposobem dostar czenia danych do składowych struktury jest zdefini owanie warto śc i początkowych w jej deklaracji. Przypuśćmy, że zmienną Powt esc chcieliśmy za in i cja l i zow ać danymi dotyczącymi jednej z naszych ulubionych książek - Programowanie dla opornyc h, wydanej w 1981 roku przez wydawnictwo Rynsztok. Jest to historia faceta, który bohatersko pisał kod programu, nie wychodząc z igloo. Jak się pewnie domyśl asz , książka ta stała się inspi racj ą znanego hitu kasowe go w Hollywood, zatytu łow aneg o Przeminęło z wiadrem. Autorem był niejaki l.C. Palec , do którego należy także trzytomowe nowatorskie dzieło Przewodnik konesera po spinaczach do papieru. Maj ąc takie bogactwo wiedzy, możemy przystąpi ć do pisa nia deklaracji zmiennej Pawi esc: KSIAZKA Powi esc
~
{ "Programowanie dl a opor nych". " I .C. Pa l ec" . "Wydawni ct wo Rynsztok" . 1981
II II II II
Wartość początko wa składo wej
Tytul. Aut or. Wartość począ tko wa skladowej Wydawca. Wartos ć począ tko wa skladowej Rok.
Wartość początko wa s kłado wej
}: Warto ści początkowe umieszczone zostały między nawiasami klamrowymi i oddzielone od siebie przecinkami w podobny sposób jak przy podawaniu wartości początkowych dla ele mentów tablicy . Tak jak w tablicach kolejność wartości początkowych musi być o czywiście taka sama jak kol ejność odpowi ad ających im pól w defini cji struktury.
Uzyskiwanie dostępu do pól strukturV W celu uzyskania dostępu do pól struktury możemy posłuży ć się operatorem wyboru składo wej , który ma postać kropki. Aby odnieść s ię do określon ej s kład owej, piszemy nazwę zmien nej struktury, po niej kropkę, a następnie nazwę składowej , do której chcemy uzyskać dostęp. Aby zmienić zawartość składowej Rok struktury Pawi esc, możem y po służyć się n astępującą instrukcją:
Powiesc .Rok
~
1988:
362
Visual C++ 2005. Od podstaw Powyższa instrukcja spowodowałaby ustawienie wartości pola Rok na 1988. Ze składowej struktury możemy korzystać w dokładnie taki sam sposób jak z każdej innej zmiennej takiego samego typu co to pole. Aby zwiększyć wartość pola Rok o dwa , możemy użyć następującej instrukcji :
Powiesc .Rok
+=
2;
Powyższa każdej
instrukcja zwiększa innej zmiennej.
wartoś ć
pola Rok struktury w taki sam sposób jak w przypadku
~ Uiywallie slrukll.lr Sposób uzyskiwania dostępu do pól struktury przećwiczymy teraz na innym przykłado wym programie konsolowym. Przypuśćmy, że chcemy napisać program dotyczący niektó rych elementów znajdujących się na podwórku , takich jak te, które zostały przedstawione na rysunku 7.1.
Rysunek 7.1
Dom Współrzędne położen ia
0,0
~--+--+- 70 ----+----+---~ o
.
o
co
30-H-~
Basen +---+-+---1-
70
-------+-+i
10
Współrzędne położenia
100,120
Rozdział 7.•
Definiowanie własnych typÓW danych
363
Współrzędne O, O postanowiłem umieścić w lewym górnym rogu podwórka. Prawy dolny róg ma współrzędne 100, 120. A zatem pierwsza współrzędna określa położenie w poziomie wzglę dem lewego górnego rogu (wartości rosną od lewej do prawej), a druga współrzędna określa położenie w pionie w odniesieniu do tego samego punktu (wartości rosną od góry do dołu).
Na rysunku 7.1 widać również położenie względem lewego górnego rogu podwórka basenu oraz dwóch chat. Jako że podwórko, chaty oraz basen są prostokątami , możemy zdefiniować typ st ruct reprezentujący dowolny z tych obiektów:
st ruc t RECTANGLE {
int int int int
Left: Top; Right; Bottom;
II Para współrzędnych II punktu znajdującego s i ę w lewym górnym rogu. II Para współrzędnych II punktu znajdującego się w prawym dolnym rogu .
}:
Pierwsze dwa pola struktury RECTANGL E odpowiadają współrzędnym lewego górnego rogu prostokąta, a pozostałe dwie współrzędnym prawego dolnego rogu . Struktury tej użyjemy w prostym programie wykonującym pewne operacje na obiektach z podwórka: II Cw?_Ol .cpp
II Ćwiczenie struktur na podwórku.
#include using st d; :cout ; using std.rcndl : II Definicja struktury reprezentującej prostokąty.
struct RECTANGLE {
int Left ; int Top :
II Para współrzędnych II punktu znajdującego się w łewym górnym rogu.
int Right ; int Bottom ;
II Para współrzędnych II punktu znajdującego się w prawym dolnym rogu II podwórka.
}; II Prototyp funkcji
obliczającejpowierzchnięprostokąta.
long Area(RECTANGL E& aRect): II Prototyp funkcji przesuwającejprostokąt.
void MoveRect(RECTANGLE& aRect . int x. int
y );
int ma in(void) {
RECTANGLE Ya rd ~ { O. O. 100. 120 l: RECTANGLE Pool = { 30. 40. 70. 80 l. RECTANGLE Hutl . Hut2; Hutl .Left = 70;
Hutl .Top = 10:
Hut l.Right = Hut l .Left Hut l.Bottom ~ 30:
Hut2 = Hutl;
+
25;
II Definicja przypisująca Hut2
wartość
Hutl.
364
Visual C++ 2005. Od podstaw MoveRect CHut 2, 10, 90):
ret urn O: II Funk cj a o b liczając a powierzchnię pros toką ta .
10ng AreaCRECTANGLE& aRect ) (
retu rn CaRect ,R ight . aRect .Left )*(aRect ,Bot tom - aRect .Top) : II Funk cja p rzesuwająca prostokąt.
void MoveRect CRECTANGL E&aRect, int x. int y) {
int 1ength = aRect .Right aRect .Left : int widt h = aRect .Bot tom aRect Top:
II Pobi erz długość prostokąta . II Pobi erz szerokość prostokąta .
aRect. Left = x: aRect .Top = y: aRect ,Ri ght = X + 1engt h:
II Ustaw lewy górny punkt II w nowej łokaliza cji . II Wsp ółrzędne punktu pra wego dołn ego rogu II ob licz jako II przyr ost od nowego położenia,
aRect .Bot t om = y
+
width:
return: Rezultat
działania
tego programu przedstawia się
następująco:
chaty Hut 2 t o 10,90 i 35,110
Powi erzchnia podwórka wynosl 1200 0
Powierzchnia basenu wynosi 1600
W s pó ł r z ęd n e
Jak to działa Zauważ, że
definicja struktury znajduje s ię w powyższym przykładzie w zasięgu globalnym, w zakładce Class View projektu, Dzięki takiemu zlokalizowaniu tej definicji zmienne typu RECTAINGLE możemy deklarować w dowolnym miejscu pliku .cpp. W programie złożonym z większej ilości kodu takie definicje zostałyby umieszczone w pliku z rozszerzeniem .h, a następnie - w razie potrzeby - dołączone za pomocą dyrektywy #lOC ' l ude do każdego pliku .cpp . Możemy ją także znaleźć
Rozdzial7.• Definiowanie własnych typÓW danych
365
Do przetwarzania obiektów typu RECTANGL E zdefiniowali śmy dwie funkcje. Funkcja Area () oblicza powierzchnię obiektu typu RECTANGLE, który przekazujemy jako argument w postaci referencji jako iloczyn długości i szeroko ści, gdzie długo ść to różni c a pomiędzy poziomymi położeniami definiujących punktów, a sz erokość to różnica pomiędzy pionowymi położe niami definiuj ących punktów . Dzięki przekazaniu referencji kod dzi ała nieco szybciej , gdyż argument nie j est kopiowany. Funkcja MoveRect() modyfikuje defin iujące punkty obiektu RECTANGLE w celu umieszczenia go w punkcie o współrzędnych x, y, które przekazywane są do niej jako argumenty. Mówiąc o położeniu obiektu RECTANGLE, mam na myśli położenie jeg o lewego górnego wierzchołka . Jako że obiekt RECTANG LE został przekazany jako referencj a, funkcja może b ezpośrednio modyfikowa ć zawartość jeg o pól. Po obliczeniu długości i szero kości przekazanego obiektu pola Left i Top zostają odpowiedn io ustawione na x i y, a warto śc i pól Ri ght i Bottomsą obliczane poprzez zw iększenie x i y o długo ś ć i szeroko ś ć oryginalnego obiektu RECTANGLE. W funkcji mai n( )
zainicjal izowali śmy
zmienne typu RECTANGLE o nazwach Yard i Pool warto które widać na rysunku 7.1. Zmienna Hut l reprezentuje chatę znajdu jącą si ę w prawym górnym rogu ilustracj i, a jej pola zostały ustawione na właściwe wartości za pomocą instrukcji przypisania. Zmienna Hut2, odpowiadając a chacie po lewej stronie na dole rysunku, najpierw zost ał a ustawiona na taką samą warto ś ć j ak zmienna Hutl za pom o cą poniższej instrukcj i przypis ania: ści ami współrzędnych,
Hut 2 = Hutl :
II Defin icja p rzypisująca Hut2
wartość
Hutl .
Instrukcja ta spowoduje skopiowanie wartości pól zmiennej Hutl do odpowiadających im pól zmiennej Hut 2. Strukturę danego typu można przypisać tylko do innej struktury takiego samego typu. Nie można b ezpo średnio zwiększać struktury ani używać j ej w wyrażeni ach arytmetycznych. Aby
zmieni ć położenie
chaty Hut 2 do jej miejsca na dole po lewej stronie podwórza, wywo MoveRect ( ), jako argumenty podaj ąc żądane współrzędne . Ten pokrętny spo sób uzyskiwania współrzędnych chaty Hut 2 jest całkowicie niepotrzebny i służy wyłącznie jako przykład użycia struktury jako argumentu funkcji . łujemy funkcję
Pomoc mechanizmu Intellisense wpracy ze strukturami Jakjuż prawdopodobnie udało Ci się zauważyć, edytor w Visual C++ 2005 jest bardzo inteli gentny - zna na przykład typy wszystkich zmiennych. Jeżeli najedziemy kursorem na jakąś zmi enną w oknie edytora, to pokaże s i ę mała chmurka zjej definicją. Funkcj a ta może być także bardzo pomocna w pracy ze strukturami (a także klasami , o czym się wkrótce przeko namy), p oni eważ zna ona typy nie tylko zwykłych zmiennych, lecz takż e typy składowych należących do zmiennej struktury określonego typu. Jeżeli Twój komputer jest wystarczająco szybki, to po wpisaniu operatora wyboru składowej po nazwie zmiennej strukturalnej edytor wy świetli chmurkę z awierającą listę wszystkich s kład owych . Kliknięcie jednej z nich spo woduje pojawienie się komentarza, znajdującego s ię w oryginalnej definicji tej struktury, dzięki czemu wiemy, do czego ona służy. Sposób działania tego mechanizmu przedstawiono na rysun ku 7.2, na którym widoczny jest fragment kodu powyższego programu.
366
Visual C++ 2005. Od podstaw
Rysunek 7.2
00
ii
61
v o id Hove:Re ct ( RECT ANGL E & age c t ,
52 63 64
(
Funkc ja p r
i nt
l eng t h
i nt
e t.d t. b
65
66 ';;7
1
6::69 !
701 71; , ,
aRe ct. Left e ne c c . Top
. . . .
e.Rect. Right a Rec t
.1
~e~ u ~ ~ JąCd.
pr a~t ok ~ t
.
r n t; x ,
i nt
oRec t . Right - enec c . Lett ; e nec c . Bo t t om - enec c . TOp;
y)
/ / Pob i e r z.
fI Pob le rz
długość: p r c a r.o k qc a . a ae r o ko a ć p r o s t c k ąt a .
// TJst ,aIJ l evy g or: n y pu nkt.
x;
.x+
fI
y;
ii
le ngth:
n o a e j l o kal i aec j i . Łr z ę dne p u n k tu pra wego dol
"jl ap ó
. ~ ~ " I RECTANGLE : :Botttlm " Left RiJhl >ł Top
V
punktu znajdując ego s ię w praw ym dolnym rogu podwórka. File: cw7 D1a.cpp
Jest to bardzo ważny argument za stosowaniem krótkich i treściwych komentarzy do kodu. Dwukrotne kliknięcie lub naciśnięcie klawisza Enter w momencie, gdy jeden z elementów listy jest podświetlony spowoduje automatyczne jego wstawienie po operatorze wyboru skła dowej , likwidując w ten sposób jedno ze źródeł literówek w kodzie. Wspaniale, prawda? Jeśli chcemy, to możemy niektóre właściwości mechanizmu lntellisense lub cały ten mecha nizm wyłączyć w menu Tools/Options, ale wydaje mi się, że jedyna sytuacja, w której może być to potrzebne, to zbyt wolny komputer niepozwalający na efektywne jego wykorzystanie. Właściwości Statement completion (uzupełnianie instrukcji), można włączyć lub wyłączyć po prawej stronie karty edytora C/C++. Aby przywrócić te opcje po ich wyłączeniu można użyć menu Edit lub dokonać tego za pomocą klawiatury. Na przykład wciśnięcie kombinacji klawiszy Ctr/ +J powoduje pokazanie się pól obiektu znajdującego się pod kursorem. Edytor pokazuje również listę parametrów funkcji podczas wpisywania kodu do jej wywoływania lista ta pokazuje się po wpisaniu otwierającego nawiasu zawierającego argumenty. Właściwość ta jest szczególnie przydatna w pracy z funkcjami bibliotecznymi, ponieważ bardzo trudno jest zapamiętać ich wszystkie parametry. Oczywiście, w kodzie programu musi się już znaj dować dyrektywa #i nc1ude dołączająca odpowiedni plik nagłówkowy . Bez tego edytor nie wiedziałby, co to jest za funkcja. Więcej na temat pomocy ze strony edytora dowiemy s i ę przy okazji omawiania klas .
Po tej krótkiej i niezwykJe
interesującej
dygresji
przejdźmy
z powrotem do struktur.
Struk'lura HECY W programach dla systemu Windows bardzo często wykorzystuje się prostokąty. Z tego też powodu w pliku nagłówkowym windows.h znajduje się predefiniowana struktura RECT. Jej definicja jest dokładnie taka sama jak definicja struktury zdefiniowanej przez nas w ostatnim przykładzie:
st ruct RECT {
i nt 1ef t : i nt to p:
II Para współrz ędnych
II górn ego lewego wierzchołka.
i nt r ig ht: i nt bot t om;
II Para wspó łrzędnych
II prawego dolnego wierz ch ołka.
} :
-
Rozdzial7.• Definiowanie własnJch tJPÓW danJch
367
Struktura ta jest zazwyczaj używana do definiowania w różnych celach prostokątnych obsza rów na ekranie. Jako że struktura RECT jest tak często używana, plik nagłówkowy windows.h zawiera również prototypy kilku funkcji służących do manipulowania prostokątami oraz ich modyfikacji . Na przykład dostępna jest funkcja In fl ateRect< ) zwiększająca rozmiar prosto kąta oraz funkcja Equal Rect ( ) porównująca dwa prostokąty. W bibliotece MFC zdefiniowana jest także klasa CRect, która jest ekwiwalentem struktury RECT. Kiedy poznasz już klasy, to z pewnością będziesz z niej korzystać częściej niż ze struk tury RECT. Klasa CRect dostarcza wiele funkcji do manipulowania prostokątami, z których bar dzo często się korzysta, pisząc programy dla systemu operacyjnego Windows przy użyciu bi bliotek MFC .
Używanie wskaźników ze strukturami Jak się można było spodziewać, do zmiennych strukturalnych można tworzyć wskaźniki. W rzeczywistości wiele funkcji zadeklarowanych w pliku nagłówkowym windows. h, które pracują z obiektami RECT, wymaga jako argumentów wskaźników do struktur RECT, ponieważ pozwalają one na uniknięcie kopiowania całej struktury w momencie przekazania argumentu do funkcji.
RECT* pRect = NULL:
II Definicja
Zakładając, że zdefiniowaliśmyobiekt wić
w normalny sposób za
pRect = &aRect :
wskaźnika
do RECr
RECT (aRect) , wskaźnik do tej zmiennej pobrania adresu:
możemy
usta
pomocą operatora
II Ustaw
wskaźnik
na adres zmiennej aRect.
dowiedzieliśmy się
podczas wprowadzania koncepcji struktury, struktura nie może za pola tego samego typu co ona sama, ale może zawierać wskaźnik do struktury tego sa mego typu co ona. Na przykład:
Jak
wierać
st ruct List Element {
RECT aRect: ListElement* pNext :
II Pole RECr struktury. II Ws kaźn ik do elementu listy .
}:
Pierwszy element struktury Li stEl ement jest typu RECT, a drugi jest wskaźnikiem do struk tury typu Li stEl ement - takiego samego typu jak struktura właśnie definiowana (należy pamiętać, że element ten nie jest typu Li st El ement, a typu wskaźnik do Li stEl ement). Pozwala to na utworzenie takiego łańcucha obiektów typu Li st El ement, w którym każdy obiekt struk tury Li stEl ement może zawierać adres następnego obiektu Li stEl ement. Ostatni obiekt będzie miał wskaźnik zerowy. Zostało to zilustrowane na rysunku 7.3. Każda ramka na diagramie reprezentuje obiekt typu Li st El ement . W polu pNext każdego obiektu, z wyjątkiem ostatniego zawierającego zero , znajduje się adres następnego obiektu w łańcuchu . Tego rodzaju struktury nazywają się listami powiązanymi . Ich zaletą jest to, że jeżeli znamy adres pierwszego elementu listy, to możemy znaleźć wszystkie pozostałe. Jest to szczególnie ważne w przypadku dynamicznego tworzenia zmiennych, ponieważ za pomocą
368
Visual C++ 2005. Od podstaw
Rysunek 7.3
lE1
lE2
lE3
pola: aRect pnext = &lE2 -
pola : aRect pnext = &lE3 -
pola : aRect pnext = &lE4
I---
-l
~lE5
lE4
pola: aRect pnext = &lE5 -
-
pola: aRect pnext =0
Brak dalszych elementów
listy powiązanej można je wszystkie śledzić. Za każdym razem, gdy tworzona jest nowa zmien na, zostaje ona po prostu dołączona na końcu listy poprzez dodanie jej adresu do składowej pNext ostatniego elementu łańcucha.
Uzyskiwanie dostępu do pól struktury poprzez wskaźnik Przyjrzyjmy s ię poniższym instrukcjom:
RECT aRect ~ { O. O. 100. 100 }: RECT* pRect ~ &a Rect: Pierwsza z nich definiuje obiekt aRect jako obiekt typu RECT, pierwszą parę jego pól inicjali zując wartościami (O, O), a drugą parę wartościami (100, 100). Druga instrukcja deklaruje pRect jako wskaźnik do typu Rect oraz inicjalizuje go adresem obiektu aRect . Dostęp do pól obiektu aRect można uzyskać poprzez wskaźnik za pomocą następującej instrukcji:
(*pRect) ,Top
+~
10:
II Zwiększ pole Top o 10.
Umieszczenie części instrukcji wyłuskującej wskaźnik w nawiasach było tutaj konieczne, ponieważ operator wyboru składowej ma większy priorytet niż operator wyłuskania. Bez nawiasów wskaźnik zostałby potraktowany jako struktura i nastąpiłaby próba wyłuskania tej składowej, a więc instrukcja nie dałaby się skompilować. Po wykonaniu powyższej instruk cji składowa Top będzie miała wartość 10 i oczywiście pozostałe składowe zostaną zmienione. Użyta tutaj metoda uzyskiwania dostępu do pól struktury poprzez wskaźnik wydaje się nie co nieporęczna. Jako że operacje tego typu w języku C++ mają miejsce bardzo często, został utworzony specjalny operator pozwalający wyrazić to samo w o wiele bardziej czytelnej i intu icyjnej formie . Zajmiemy się teraz tym operatorem.
Operator pośredniego
dostępu
do składowych
pośredniego dostępu do składowych (- » służy do uzyskiwania dostępu poprzez do pól struktury. Operator ten wyglądem przypomina niewielką strzałkę (-» i zbu dowany jest z symbolu większości (» poprzedzonego znakiem odejmowania ( -). Instrukcję, w której uzyskiwaliśmy dostęp do pola Top obiektu aRect, przy użyciu tego operatora możemy przepisać w sposób następujący:
Operator wskaźnik
Rozdział 7.•
pRect->Top
+=
10 :
Deliniowanie własnych typów danych II Zw iększpo le
369
Top 0 10.
Jak w i d ać, instrukcja ta jes t o wiele bardziej tre ściwa. Operatora pośredniego do stępu do skła dowych używ a s i ę także w przyp adku klas i do koń ca k siążki zetkniemy s i ę z nim je szcze wielokrotnie.
Typy danych. obiekty. klasy i egzemplarze Zanim przejdziemy do języka, skład n i oraz technik programowania klas, spróbujemy s i ę, jak nasza dotychczasowa wiedza ma się do koncepcji klas.
przyjrze ć
Do tej pory uczyli śmy s i ę, że w natywnym C++ mo żn a tworzyć zmienne dowolnego rodzaju z fundament alnych typów danych : i nt, l onq, doubl e itd. Dowiedzieli śmy się także, że za pomocą s ło w a klu czowego st ruct m ożna defini o w a ć struktury, kt órych na stępnie mo żna używać jako typy zmiennych r epre z entujących zbiór kilku innych zmiennych. Zmienne typów fundamentalnych nie pozwalają na adekwatne odwzorowywanie obiekt ów św i ata rzeczywi stego (ani też wymyślonych obiektów). Trudno by było na przykład odwzorować pud ełk o za pomocą typu i nt, ale właściwo ści takiego obiektu m ożn a zdefiniować w polach struktury. Wymiary takiego pudełka można zdefiniować w zmiennych l ength, widt h oraz hei ght, które n a stępnie poł ączymy w jedną struktur ę o nazwie Box:
str uct Box {
doub le lengt h: double wi dth: doub le height : }: Mając taką definicję nowego typu danych o nazwi e Box, można d efini ować zmienne tego typu w taki sam sposób jak zmienne typów podstawowy ch. Można następnie tworzyć, manipulow ać i niszczyć tyle obiektów typu Sox, ile potrzebujemy. Oznacza to, że za pomo cą słowa kluczowego stru ct możemy nadawać naszym obiektom pożądane właściwości oraz używa ć ich jako fundamentów naszych programów. Czy to właśnie na tym polega programowanie zorientowane obiektowo?
Nie do końca. Programowanie zorientowane obiektowo (ang. Object Oriented Programming OOP) opiera si ę na kilku tilarach (kapsułkowanie, polimorfizm oraz dzied ziczenie), a to, co widzieliśmy do tej pory, nie odpowiada tym technikom . Nie przejmuj s ię , jeże l i nie rozumiesz, co oznaczają wym ienione terminy -tymi technikami będziemy się zajmować do końca rozdziału , a nawet k siążk i. Koncepcja struktur w C++ zo stała po suni ęta o wiele dalej niż jej oryginalna wersja w C do niej d ołączona koncep cja klasy, ści śle zw iązan a z program owani em zorientowanym obiektowo . Pojęcie klasy, która pozwala na tworzenie własnych typów danych i używanie ich podobnie jak typów natywny ch, jest jedną z fundamentalnych cech języka C++. Słowem kluczowym opisuj ącym ten koncept jest słowo c l ass. Słowa kluczowe st ruct i c l ass w C++ mają prawie identy czne znaczen ie, z wyj ątki e m kontroli dostępu do skład owych , o których
została
370
Visual C++ 2005. Od podstaw dowiemy si ę w dalszej c zęści tego rozdziału. Słowo kluczowe st ruet w CH pozo stało w celu utrzymania wstecznej zgodności z językiem C. Wszystko, czego można dokonać przy użyciu słowa kluczowego struet , można zrobić za pomocą ela ss - a nawet więcej .
}: Definiując klasę CSox, w rzeczywisto ści tworzymy nowy typ danych , podobnie jak w przy padku definicji struktury Box. Jedyną różn icą w tym przypadku jest użycie zamiast słowa kluczowego st ruet słowa el ass oraz słowa kluczowego publ i e po średniku poprzedzającym definicję składowych klasy. Zmienne definiowane jako część klasy noszą nazwę zmiennych składowych kłasy, ponieważ są zmiennymi zawierającymi dane skł ad aj ąc e się na klasę.
Nadali śmy
naszej klasie nazwę CSox, a nie Box, Mogliśmy oczywiście nazwać ją także Sox, ale konwencja MFC mówi, że wszystkie nazwy klas powinny być poprzedzone literą C- dobrze jest wyrobić sobie nawyk stosowani a takiego nazewnictwa. Zmienne składowe klasy w MFC są natomiast poprzedzane przedrostkiem m_w celu odróżnienia ich od innych zmiennych. Będę się trzymał tej konwencji . Należy jednak pamiętać, że w innych kontekstach, w których możemy używać C++ oraz zwłaszcza w C++/CLI, zasady te mogą nie obowiązywać . Cza sami konwencje nazywania klas i ich zmiennych składowych mogą być inne, a nawet może w ogóle takich konwencji nie być . kluczowe publ i e stanowi wskazówkę dotyczącą różnic pomiędzy strukturą i klasą. Jego sprawia, że zmienne składowe klasy są ogólnodo stępne, w podobny sposób jak pola struktury, choć te drugie są do stępne domyślnie . Jak przekonamy się trochę później w tym rozdziale, istnieje możliwo ść ograniczenia dostępu do zmiennych składowych klasy. Słowo
użycie
Możemy zdefiniować zmienną o
nazw ie big Box reprezentującą egzemplarz klasy CBox:
CBox bigBox: Odbywa się to tak samo jak w przypadku deklarowania zmiennej typu st ruet lub ogólnie jakiejkolwiek innej zmiennej. Po zdefiniowaniu klasy CSox deklaracje zmiennych tego typu są standardowe.
Pierwsza klasa Pojęcie
klasy zostało wymyślone przez pewnego Anglika w celu uszczęśliwienia całego narodu. Powstało ono na podstawie teorii, że ludzie, którzy znają swoje miejsce i funkcję w społeczeństwie, mają o wiele większe szanse na bezpieczne i wygodne życie niż ci, którzy go nie znają. Słynny Dane Bjame Stroustrup, który stworzył j ęzyk C++, bez wątpienia zdobył gruntowną wiedzę na temat koncepcji klasowych, studiując na uniwersytecie w Cambridge w Anglii, i zastosował tę wiedzę w bardzo udany sposób w swoim nowym języku.
Rozdzial7. • Deliniowanie własnych typÓW danych
371
Klasa w języku C++ jest bardzo podobna do angielskiej koncepcji pod tym względem , że ma ona zazwyczaj ściśle określoną rolę oraz zestaw dozwolonych czynności. Różni się jednak od klasy angielskiej pod tym względem, że ma bardzo silny wydźwięk "socjalny", koncen trując się na ważności klas pracujących. W rzeczywistości czasami klasa w C++ jest wręcz przeciwieństwem ideałów angielskich, ponieważ, jak się przekonamy, klasy pracujące w C++ żyją na koszt klas, które nic nie robią.
Operacje na klasach W języku C++ można tworzyć nowe typy danych w postaci klas w celu reprezentowan ia obiektów dowolnego typu. Jak się niebawem przekonamy , zastosowanie klas (i struktur) nie ogranicza się wyłącznie do przechowywania danych. Można także definiować funkcje skła dowe, a nawet operacje działające pomiędzy obiektami klasy przy użyciu standardowych operatorów C++. Możemy na przykład zdefiniować klasę CBox w taki sposób, że poniższe instrukcje będą działały oraz miały takie znaczenie, jakie chcemy, aby miały :
CBox boxl : CBox box2: if( boxl
>
box2)
II Zapełnij
większe pudełko .
boxl , f ill ( ) :
else
box2. rn ():
Możemy również zaimplementować
nia, a nawet mnożenia w kontekście pudełek .
pudełek
-
jako część klasy CBox operacje dodawania, odejmowa tak naprawdę każdą operację , która ma jakikolwiek sens
To, o czym teraz mówię, jest niewiarygodnie potężną techniką, która stanowi ogromny zwrot w podejściu do programowania. Zamiast rozbijać problem na części w kategoriach kompute rowych typów danych (liczby całkowite, liczby zmiennopozycyjne itd.) i następnie pisać program , będziemy programować w kategoriach typów danych związanych z problemem, mówiąc inaczej - w kategoriach klasowych. Klasy te mogą mieć nazwy CPracowani k, CKowboj lub CSe r albo CBa rszcz, z których każda została zaprojektowana w celu rozwiązania jednego rodzaju problemu. Dopełnieniem są tutaj funkcje i operatory niezbędne do manipulowania egzemplarzami tych nowych typów danych . Projektowanie programu w tym przypadku rozpoczynamy od podjęcia decyzji, jakich typów danych potrzebować będziemy dla tego konkretnego programu. Kod będziemy pisać , mając na myśli operacje na specyficznych dla tego programu problemach , np. CTrumny lub CKowboj e.
Terminologia Przed przejściem do omówienia kolejnych zagadnień związanych z klasami w C++ zrobimy mate podsumowanie terminów, których będę często używał : • Klasa to zdefiniowany przez
programistę
typ danych .
• Programowanie zorientowane obiektowo (OOP) to styl programowania oparty na koncepcji tworzenia własnych typów danych w postaci klas.
372
Visual C++ 2005. Od podstaw • Operacja deklarowania obiektu typu klasow ego czasami nazywana jest tworzeniem egzemplarza, gdyż w j ej wyniku powstaje nowy egzemplarz kla sy. • Egzemplarze klasy noszą nazwę obiektów. • Koncepcja obiektu zawierającego w swojej definicji dane wraz z funkcjami na nich operującymi nosi nazwę kapsułkowania .
Kiedy przejdziemy do szczegółów programowania zorientowanego obiektowo, może się ono czasami wydawać nieco skomplikowane. W takich przypadkach najlepiej powrócić na chwilę do podstaw, przypomnieć sobie, do czego tak naprawdę służą obiekty, a wszystko stanie s ię na powrót jasne. A służą one do pisania programów w kategoriach obiektów specyficznych dla sedna problemu. Wszystkie narzędzia związane z klasami są po to, by pisanie programów było jak najbardziej zrozumiałe i elastyczne. Przejdźmy więc do klas.
Zrozumieć klasy Klasa jest określeniem typu danych zdefiniowanego przez programistę . Mogą one zawiera ć zmienne składowe w postaci zmiennych typów podst awowych lub innych typów zdefiniowa nych przez programistę. Zmienne składowe klasy mogą być pojedynczymi elementami da nych , tablicami , wska źnikami, tablicami wskaźników prawie ka żdego rodzaju lub obiektów czy innych klas. Pozwala nam to na bardzo dużą elastyczność, jeśli chodzi o to, co mogą zawie rać klasy . W klasach mogą także znajdować się funkcje, które operują na obiektach tych klas, uzyskując dostęp do zawartych w nich zmiennych składowych. A zatem klasa łączy definicje danych elementarnych, które składają się na obiekt, ze sposobami manipulowania danymi należącymi do poszczególnych obiektów klasy. Dane i funkcje wewnątrz klasy nazywają się składowymi klasy. Elementy danych należące do klasy nazywają się zmiennymi składowymi, zaś funkcje należące do klasy - funkcjami składowymi klasy. Funkcje składowe klasy czasami nazywane s ą metodami. W tej książce nie będę używał tego terminu, ale warto wied zieć, że taka nazwa również istnieje, gdyż możemy się na nią natknąć gdzi eś indziej. Dane składowe klasy bywają także nazywane polami i ta terminologia jest używana w odnie sieniu do języka C++/CLI, w związku z czym będę z niej również czasami korzystał. Definiując klasę, definiuje się plan typu danych. Nie jest to w rzeczywistości definicja żadnych danych, ale definicja tego , co oznacza dana nazwa klasy, tzn . co będzie zawierał obiekttej klasy orazjakie operacje można na nim wykonywać. To tak samo, jakbyśmy napisali opispod stawowego typu doubl e. Nie byłaby to prawdziwa zmienna typu doubl e, ale definicja tego, jak się go tworzy i z niego korzysta . W celu utworzenia zmiennej jednego z podstawowych typów używamy instrukcji deklaracji . Jak się przekonamy, z klasami j est dokładnie tak samo.
Rozdział 7.•
Definiowanie własnych typÓW danych
373
Deliniowanie klasy Spójrzmy jeszcze raz na przykładową klasę, o której wspominałem wcześniej - klasę pudełek.
Typ danych CBox zdefin iowaliśmy za pomocą słowa kluczowego cl ass w następujący sposób:
class CBox (
publ te : double m_Lengt h: doubl e m_Widt h: double m_H eight :
II Długo s ć p udelka w centymetrach. II Szerok ość pudelka w centymetrach. II Wysoko ś ć pudelka w centymetrach.
}:
Nazwa klasy znajduje się po słowie kluczowym cl as s, a pomiędzy nawiasami klamrowymi zdefiniowane zostały trzy zmienne składowe klasy . Zmienne składowe klasy definiuje się za pomocą instrukcji deklaracji, które już dobrze znamy i lubimy. Cała definicja klasy zakoń czona j est średnikiem . Nazwy wszystkich zmiennych składowych klasy mają zasięg lokalny w tej klasie. W związku z tym takich samych nazw jak w tej klasie można używać również w innych miejscach programu, bez obawy, że dojdzie do konfliktu nazw .
Konirola doslępu Wklasie Słowo
kluczowe publ i c przypomina trochę etykietę, ale w rzeczywistości jest czymś więcej . ono atrybuty dostępu składowych klasy, które się po nim znajdują. Określenie skła dowychjako publicznych (publ t e) oznacza, że dostęp do nich w obiekcie tej klasy można uzyskać z dowolnego miejsca znajdującego się w zasięgu obiektu klasy, do któr ego należą. Składowe klasy można także określić jako prywatne (pri vate) lub chronione (p rot ect ed). Jeżeli w ogóle nie określimy rodzaju dostępu, to domyślnie zostanie zastosowany atrybut pri vat e (jest to jedyna różnica pomiędzy klasą i strukturą w C++ - domyślny określnik dostępu do składowych struktury to publ te ). Efektom zastosowania tych słów kluczowych w definicji klasy przyjrzymy się trochę później. Określa
Deklarowanie obiektów klasy Obiekt klasy deklaruje się za pomocą deklaracji takjego samego typu jak w przypadku obiek tów typów podstawowych. W związku z tym obiekty klasy CBox możemy zadeklarować za pomocą następujących instrukcji :
CBox boxl: CBox box2 : Oczywiście każdy
II Deklaracj a obiektu box l typu CBox. II Deklaracj a obiektu box2 typu CBox.
z tych obiektów (boxl i box2) to przedstawione na rysunku 7.4 .
będzie miał własne
dane
składowe. Zostało
Nazwa obiektu boxl oznacza cały obiekt, włączając w to wszystkie trzy jego zmienne skła dowe, choć zmienne te nie zostały zainicjalizowane żadnymi wartościami (zawierają po prostu jakieś przypadkowe wartości). Musimy zatem nauczyć się uzyskiwać do nich dostęp w celu nadawania im określonych wartości.
374
Visual C++ 2005. Od podstaw boxl
Rysunek 7.4 (
box2
A-
m Length
m Width
8 bajtów
8 bajtów
I
(
m Height \
8 bajtów
I
I
A
m Length
m Width
8 bajtów
8 bajtów
m Height "
8 bajtów I
Uzyskiwanie dostępu do zmiennych składowych klasy
do zmiennych składowych klasy możemy posłużyć się operatorem do składowej, którego używaliśmy do uzyskiwania dostępu do skła. dowych struktury. Aby zatem ustawić wartość składowej m_Height obiektu box2 na przykład m wartość 18. O, możemy posłużyć się następującą instrukcją: W celu uzyskania
dostępu
bezpośredniego dostępu
box2.m Height = 18.0:
II Ustawianie
wartości
zmiennej składowej.
Dostęp
do zmiennej składowej można w ten sposób uzyskać w funkcji znajdującej się poza a to tylko dlatego, że obiekt m_Hei ght został określony z dostępem publicznym . Gdyby dostęp został określony jako prywatny lub chroniony, to instrukcja taka nie dałaby się skom pilować. Niedługo zetkniemy się z podobnymi sytuacjami. klasą,
RIlmII!I Pierwsze użycie klas Sprawdzimy, czy potrafimy używać klas podobnie jak struktur. Do tego celu program konsolowy :
II Defini cja zmiennych II obiektu box2 II w kategoriach box I.
375
s kłado wyc h
II Obliczanie pojemności pudełka box I.
boxVolume
=
boxl. m_Helght*boxl .m_Lengt h*box l .m_Width :
cout « end l
« " PO jemność
p ude ł ka
boxl
=
« boxVolume :
cout « endl
« "Suma boków pud e ł k a box2 wynosi "
« box2 .m_Height+ box2 .m_Lengt h+ box2.m_W idt h
« " centymet rów . ":
cout « endl « "Ob iekt CBox zajmuje « S i zeof boxl « " ba jty . ":
II
Wyświetlanie
rozmiaru pudełka w pamięc i.
cout « endl : return O: Podczas wpisywania kodu funkcj i ma i n( ) edytor za każdym razem, gdy używamy operatora wyboru składowej po nazwie obiektu klasy, powinien wyświetlać listę nazw składowych. Żądaną składową z listy wybieramy za pomocą dwukrotnego jej kliknięcia . Jeżeli najedziemy na chw i lę kursorem na jedną ze zmiennych w kodzie, to pokaże się jej typ.
Jak lo llziała Wróćmy
tworzenie programów konsolowych, aby przypomnieć definiowania projektów konsolowych. Wszystko tutaj działa tak, jakbyśmy się spodziewali, że będzie działało przy użyciu struktur. Definicja klasy znajduje się poza funkcją ma i n( ), dzięki czemu ma zasięg globalny. Możemy zatem deklarować obiekty w dowolnej funkcji w programie, a klasa takiego obiektu będzie pojawiać się w zakładce C/ass View po skompilowaniu programu. sobie
na
chwilę
do
części opisującej
prawidłowy sposób
W obrębie funkcji main O zadeklarowaliśmy dwa obiekty typu CBax - boxl i box2. Oczywiście, podobnie jak zmienne typów podstawowych, obiekty boxl i box2 mają zasięg lokalny w funk cji ma i n( ). Obiekty typów klasowych podlegają takim samym zasadom dotyczącym zasięgu co zmienne typów podstawowych (jak np. zmienna baxVal ume użyta w powyższym programie). Pierwsze trzy instrukcje przypisania ustawiają wartości zmiennych składowych obiektu boxl . W następnych trzech instrukcjach definiujemy w kategoriach zmiennych składowych obiektu boxl wartości zmiennych składowych obiektu bax2. Następnie
boxl , będącą iloczynem wszyst obiektu. Tak obli czona wartość zostaje następnie wysłana na ekran. Później wysyłamy na wyjście sumę wartości zmiennych składowych obiektu bax2, podając wyrażenie dodające posz czególne składniki wprost w instrukcji wyjściowej. Ostatnią czynno ścią programu jest wysłanie na ekran liczby bajtów zajmowanych przez obiekt boxl , którą uzyskujemy za pomocą operatora s i zeof.
kich trzech
mamy
instrukcję obliczającą pojemność pudełka
składowych
376
Visual C++ 2005. Od podstaw Po uruchomieniu tego programu
powinniśmy otrzymać następujący rezultat:
Po je mnoś ć pu d e łk a boxl ~ 33696
Suma bok ów pud ełka box2 wynosi 66,5 cent ymetrów,
Obiekt CBox zajmuje 24 bajty.
Ostatni wiersz informuje, że obiekt boxl zajmuje 24 bajty pamięci . Liczba ta wynosi tyle, mamy trzy zmienne składowe, z których każda zajmuje osiem bajtów. Instrukcja odpowiedzialna za wyświetlenie ostatniego wiersza mogłaby z powodzeniem zostać zapisana
ponieważ
następująco :
cout « endl II Wyświetl ilość pamięci zajmowaną przez pudelko. cc "Oblekt CBox zajmuje « sizeof (C Box) « " bajty," ; W powyższym kodzie jako operandu operatora s i zeof w nawiasach użyłem nazwy typu, a nie nazwy określonego obiektu. Jak pamiętamy z rozdziału 4., jest to standardowa składnia ope ratora s i zeof. Przykład
ten demonstruje mechanizm uzyskiwania dostępu do publicznych zmiennych skła dowych klasy. Widać w nim także, że zmienne te mogą być używane w taki sam sposób jak zwykłe zmienne. Teraz jesteśmy już gotowi na następny przełom i zajęcie się funkcjami skła dowymi klasy.
funkcie składowe klasy W celu zaobserwowania, w jaki sposób uzyskuje się dostęp do zmiennych składowych klasy z wnętrza funkcji składowych, stworzymy przykładowy program, rozszerzając klasę CBox poprzez dodanie do niej funkcji składowej obliczającej pojemność obiektu CBox. II Cw7_03.cpp
II Obliczanie objętości pudelka za pomocąfunkcji składowej.
#i ncl ude ciost ream>
using std : :cout;
usi ng st d: endl ;
class CBox
II Definicja klasy o globalnym zasięgu.
{
publ i c: doub le m_Lengt h, double m_Widt h; double m_Height ; II Funkcja
II Długość pudełka w centymetrach. II Szerokoś ć pudełka w centymetrach. II Wysokość pudelka w centymetrach.
obliczająca pojemnoś ć pudełka ,
double Vol ume () {
};
i nt mai n() {
CBox boxl ;
II Deklara cja obiektu boxl typu Cbox.
Rozdział7 .•
CBox box2: double boxVol ume
Definiowanie własnych typÓW danych
377
II Deklaracja obiektu box2 typu CBox. ~
O O.
II Przechowuje pojemnosć pudełka.
box1 m_He i ght = 18.0 : box1.m_Length = 78 .0: box1 .m_Widt h = 24.0;
II Definicja wartości
II zmiennych składowych
II obiektu box l .
box2 .m_Height ~ box1.m_Height 10. II Definicja zmiennych skladowych box2 .m_Length = box1.m_Lengt h/2. 0: Il obieklU box2 box2.m_Widt h = O 25*box1.m_Length: II w kategoriach boxl. boxVol ume = box1.Vol ume(), cout « end l «
"Po j emno ść p ude łk a
II Obliczanie
box1 = "
«
objętosci pudelka
boxl.
boxVolume:
cout « endl
cout
"Po je mno ść p ud e łk a
box2
« «
box2.Vol ume();
« « «
endl
"Obiekt CBox zajmuj e
S l zeof box1 « " baj t y
=
"
cout « endl . return O:
Jak to działa Nowy kod dodany do definicji klasy CBox został umieszczony na szarym tle . Stanowi on tylko definicję funkcji Vo lumet), która jest funkcją składową klasy. Ma ona taki sam atrybut dostępu jak zmienne składowe - pub l i c. Jest tak ze względu na fakt, że wszystkie składowe klasy mają taki atrybut dostępu, jaki został przed nimi określony, aż do momentu podania innego atrybutu . Funkcja vol ume() zwraca pojemność obiektu CBox w postaci liczby typu doubl e. Wyrażenie w instrukcji return jest po prostu iloczynem trzech wartości składowych klasy.
Nie ma potrzeby przyporządkowywania nazw zmiennych składowych klasy podczas uzy skiwania dostępu do nich z poziomu funkcji składowych. Nazwy zmiennych składowych występujące bez żadnego kwalifikatora odnoszą się do składowych obiektu, którego funk cjajest aktualnie wykonywana. Funkcja składowa Vo l ume () używana jest we fragmentach kodu znajdujących się na szarym tle w funkcji mai n() po inicjalizacji zmiennych składowych (tak jak w pierwszym przykładzie). Używanie tej samej nazwy zmiennej w funkcji mainr ) nie stanowi żadnego problemu. Aby wywołać funkcję składową określonego obiektu, należy napisać nazwę tego obiektu, po niej kropkę, a następnie nazwę funkcji składowej . Jak wspominałem wcześniej, funkcja automa tycznie uzyskuje dostęp do zmiennych składowych obiektu, dla którego została wywołana. W związku z tym za pierwszym razem, gdy ją wywołujemy, funkcja Volume() oblicza pojem ność obiektu boxl. Używanie tylko nazwy zmiennej składowej zawsze oznacza odniesienie się do zmiennych składowych obiektu, dla którego funkcja składowa została wywołana.
378
Visual C++ 2005. Od podstaw Funkcja składowa została wywołana po raz drugi bezpośrednio wewnątrz instrukcji wyjścio wej, obliczając pojemność pudełka box2. Wykonanie tego przykładu da następujący rezultat:
boxl = 33696 box2 = 6084 Ob iekt CBox zajmuje 24 baj ty . P ojemn o ś ć p u deł k a P Ojemnoś ć p u deł ka
obiekt CBox nadal zajmuje tę samą liczbę bajtów. Dodanie funkcji do klasy nie powoduje zwiększenia rozmiaru obiektów. Oczywiście funkcja ta musi być przechowywana gdzieś w pamięci, ale istnieje tylko jeden jej egzemplarz bez względu na to, ile obiektów klasy zostało utworzonych. Pamięć zajmowana przez funkcję składową nie jest zaliczana do wartości zwracanej przez operator s i zeof, zwracający liczbę bajtów zajmo wanych przez obiekt. Warto
zwrócić uwagę, że
składowej
Nazwy zmiennych składowych klasy w funkcji składowej odnoszą się automatycznie do zmiennych składowych tego obiektu, który został użyty do wywołania tej funkcji . Funkcję składową można wywołać tylko dla określonego obiektu danej klasy. W tym przypadku zo stało to dokonane za pomocą operatora bezpośredniego dostępu do składowej z nazwą obiektu. Jeżeli
w programie znajdzie się wywołanie funkcji składowej bez określenia nazyry obiektu, dla którego jest wywoływana, programu nie będzie można skompilować.
Umieiscowienie definicii funkcii składowei Definicja funkcji składowej nie musi znajdować się wewnątrz definicji klasy . Jeżeli chcemy umieścić ją poza definicją klasy, to w klasie należy zapisać tylko jej prototyp. Nasza po przednia klasa przepisana z definicją funkcji składowej poza nią wygląda następująco:
class CBox
II Definicja klasy w zasięgu globalnym.
(
publ ic: doub le do ub le dou ble dou ble
m_ Length: m_Width: m_Heig ht: Vol ume(void) :
II Dlugość pudelka w centymetrach. II Szerokość pudelka w centymetrach. II Wysokość pudelka w centymetrach. II Prototyp funkcji skladowej.
[:
Teraz pozostaje jeszcze napisanie definicji funkcji. Ze względu na fakt, że znajduje się ona poza klasą, musimy w jakiś sposób poinformować kompilator, że funkcja ta należy do klasy CBox. Dokonujemy tego, stawiając przed nazwą funkcji nazwę klasy i rozdzielając obie nazwy operatorem zasięgu, którym są dwa znajdujące się obok siebie dwukropki (: :). Definicja funk cji wyglądałaby teraz następująco : II Funkcja
obliczająca pojemność
doub le CBox: :Vol ume ( ) { }
pudelka.
Rozdział 7.•
Definiowanie własnych typÓW danych
379
Funkcja ta daje taki sam wynik jak jej poprzednia wersja , choć program nie jest już dokładnie taki sam. W drugim przypadku wszelkie wywołania funkcji traktowane są w sposób już nam znany. Jeśli natomiast definicję funkcji umieścimy wewnątrz definicji klasy, jak na przykład w ćwiczeniu Cw7_03.cpp, kompilator niejawnie potraktuje jąjako funkcję inline.
fllDkcie inline Przy zastosowaniu funkcji inline kompilator próbuje rozwinąć kod ciała funkcji w miejscu jej W ten sposób unikamy strat spowodowanych wywoływaniem funkcji, co sprawia , że program staje się szybszy. Zostało to zilustrowane na rysunku 7.5.
wywołania.
int main(void)
Funkcja zadeklarowana jako inline w klasie
Rysunek 7.5
inline void functionO {body}
~
Kornpilator w rniejsca pojawien ia się wywołań funk cji __ inline wstaw ia kod ich ciała . odpowiedn io dostosowany w celu un ikn ięcia problemów z nazwamii zasięg iem zmiennych
{body}
~1Ii
~{bOdY} Oczywi ście
kompilator sprawdza również, czy takie rozwinięcie funkcji nie spowoduje nazwami oraz zasięgiem zmiennych.
żadnych problemów z
Kompilator niekiedy może sobie nie poradzić z przeprowadzeniem operacji rozwijania kodu (np. w przypadku funkcji rekurencyjnych, dla których pozyskaliśmy adres) , ale z reguły nie ma żadnych problemów. Technikę tę najlepiej stosować z krótkimi, prostymi funkcjami, takimi jak nasza funkcja Vol ume( ) w klasie CBox, ponieważ funkcje takie wykonywane są znacznie szybciej, a wstawienie ich kodu nie wpływa za bardzo na rozmiar modułu wykonywalnego. Jeżeli
definicja danej funkcji znajduje się poza definicją klasy, kompilator traktuje ją jak i jej wywoływanie odbywa się normalnie. Jeśli jednak chcemy, to możemy sprawić, aby kompilator w miarę możliwości traktował także taką funkcję jako funkcję inline. Aby to zrobić, na początku nagłówka funkcji stawiamy słowo kluczowe i n'l i ne. W związku z tym definicja naszej funkcji wyglądałaby następująco: zwykłą funkcję
Przy zastosowaniu takiej definicji funkcji program byłby dokładnie taki sam jak w wersji oryginalnej. W ten sposób definicje funkcji możemy wyrzucić poza definicje klas i zachować korzyści płynące z prędkości wykonywania dzięki zastosowaniu funkcji inline.
380
Visual C++ 2005. Od podstaw Ten sam efekt uzyskamy, stawiając słowo kluczowe i nl i ne nawet przed zwykłymi funkcjami w programie, niemającymi nic wspólnego z klasami . Najeży jednak pamiętać, że technika ta jest najbardziej przydatna w przypadku krótkich i prostych funkcji . Musimy teraz obiektu klasy .
dowiedzieć się trochę więcej
na temat tego , co
się
dzieje podczas deklaracji
Konstruktory klas W poprzednim przykładowym programie zadeklarowaliśmy dwa obiekty klasy CBox o na zwach boxl i box2, a następnie każdej ich zmiennej skład owej przypisaliśmy po kolei warto ści początkowe . Podejście takie nie jest s atysfakcjonujące z kilku pow odów . Po pierwsze , łatwo w taki sposób pominąć przy inicjalizacji j edną ze zmiennych składowy ch, w szczególności gdy mamy do czynienia z większymi od naszej klasami. Inicjalizacja zmiennych s kłado wych kilku obiektów złożonej klasy mogłaby zaj ąć kilka stron wierszy instrukcji przypis ania. Ostatni powód to fakt, że definiując składowe z atrybutem dostępu innym niż publ i c, pozbawiamy się dostępu do nich spoza klasy . Musi być na to jakiś lepszy sposób - i jest. Nazywa się on konstruktorem klasy.
Czym lest konstruktor Konstruktor klasy to specj alna funkcja w klasie , która jest wywoływan a podc zas tworzenia nowego obiektu klasy . Umożliwia on ini cjalizację obiektów podczas ich tworzenia oraz za pewnia , że zmienne składowe zaw i eraj ą wyłącznie prawidłowe dane . Klasa może mieć kilka konstruktorów pozwalających tworzyć obiekty na różne sposoby. Przy nadawaniu nazw konstruktorom klas nie mamy żadnego pola manewru - zawsze nazy tak samo jak klasa , w której zostały zdefiniowane. Na przykład funk cja CBox() jest konstruktorem klasy CSox. Konstruktor nie posiada typu zwracanego. Nadanie konstruktorowi typu zwracanego j est błędem, nawet jeżeli określimy go jako voi d. Gł ównym przeznaczeniem konstruktorów jest nadawanie wartości początkowych zmiennym składowym klasy, a więc typ zwracany nie jest ani potrzebny, ani dozwolony. wają się
~ Dodawanie konstruktora do klasy CBOK Rozszerzymy
n aszą klasę
Cbox o konstruktor.
II Cw7_04.cpp
II Zastosow anie konstrukt ora.
#inc l ude
using st d: :cout:
using st d: :endl :
class CBox {
II Definicja klasy w zas ięgu globalnym.
Rozdzial1.• Oetiniowanie własnych typów danych publ ic double m_Length; double m_Width; double m_Height ;
381
II D/ugość p udelka w centymetrach. II Szerokoś ć pudel/w w centymetrach. II Wysoko ść pudelka w centymetrach.
II Defin icja konstruktora.
CBox(do uble l v. doub le bv. double hv) (
cout « endl « "Konst rukt or m_Length = l v: m_Wi dth = bv; m_He ight = hv:
zo s t a ł wywo ł a ny . " ;
II Ustawianie wartoś ci
II zmien ny ch s kładowych.
II Funk cj a obliczająca pojemn ość pudelka.
doub le Vol ume() ( )
};
i nt mai n() CBox boxl(78.0.24.0.18.0) ; CBox cigarBox(8.0.5 .0.1.0);
II Deklaracj a i inicjalizacj a obiektu boxl . II Dek/aracj a i inicja liza cj a obiektu cigarBox .
double boxVo l ume = 0.0 ;
II Przechowuj e pojemność pudelka.
boxVol ume ~ boxl .Volunet) : cout « end l
II Obliczanie obję toś c i p udelka boxl .
cout
«
"Pojemn o ść pu d e łka
« « «
end l
" P o j emn o ś ć p u d e ł ka
boxl
=
"
«
boxVolume:
ci garBox - "
ci garBox.Vol ume( ) ;
cout « endl ;
ret urn O;
Jak to działa Konstruktorowi CBox() przekazane zostały trzy parametry typu doubl e, odpow i adaj ąc e warto ściom początkowym zmiennych składowych mL enqt h, m_Width oraz m_Hei ght obiektu klasy CBox. Pierwsza instruk cja konstruktora wyświetla komunikat informujący, że nastąpiło wy wołanie konstruktora. W programie przezn aczonym do użytku nie umieś cilibyśmy takiej instrukcji, ale jest to często stosowana praktyka podczas fazy testowej programu, g dyż pozwala zor i ento w ać si ę, w którym momencie zost ał wywołany konstruktor. Będę jej często używał w celach testowych . Kod w ciele konstruktora jest bardzo prosty. Przyp isuj e on argumenty przekazane do niego podczas wywoływani a do odpowiednich zmiennych składowych. W razie potrzeby możn a d odać jeszcze sprawdzanie popr awności argum entów i tego, czy nie mają wartości ujemnych. Tworząc prawdziwy program użytkowy, pewnie byśmy to zrobili, ale tutaj naszym głównym celem jest przyjrzenie s i ę sposobowi działania całego mechanizmu.
382
Visual C++ 2005.011 podstaw W obrębie funkcji mai n() zadeklarowaliśmy obiekt boxl, podając wartości początkowe dla zmiennych składowych w kolejności m_Length, m_Width oraz m_Hei ght. Znajdują się one w na wiasach po nazwie obiektu. Do tej inicjalizacji zastosowaliśmy notację funkcjonalną, która j ak już widzieliśmy w rozdziale 2. - może być również z powodzeniem stosowana do inicja lizacji zwykłych zmiennych typów podstawowych. Zadeklarowaliśmy także jeszcze jeden obiekt klasy CBox o nazwie cigar Box, który również ma wartości początkowe . Poj emność pudełka boxl liczona jest podobnie jak w poprzednim przykładzie za pomocą funkcji Vo l ume (), a wynik wysyłany na wyj ś ci e. Pojemność pud ełka cigar Box również zostaje wyświetlona. Rezultat działania programu jest następujący:
Konstrukt or Konst ruktor
z o st a ł wywoł a ny .
zo st ał wywoł a ny .
Poj emn o ś ć pu d e ł k a Poj emno ś ć pu d e ł k a
box1 = 33696
cigarBox = 40
Pierwsze dwa wiersze to inform acja o dwukrotnym wywołaniu konstruktora CBox() , jeden raz dla każdego zadeklarowanego obiektu. Dostarczony przez nas w definicji klasy konstruktor wywoływany jest automatycznie podczas deklaracji obiektu klasy CBox, a więc oba obiekty tej klasy inicjalizowane są wartościami p oczątkowymi podanymi w deklaracji. Wartości te zostały przekazane do konstruktora w postaci argumentów w tak iej samej kolejności , w j akiej zo stały napisane w deklaracji. Jak widać , poj emność pudełka boxl jest taka sama jak wcze śniej, a pojemnoś ć pudełka ci garBox wygląda podejrzanie, jakby była iloczynem jego boków i całe szczęście .
Konstruktor domyślny Dodajmy do naszego ostatniego programu
deklarację
obiektu box2, której
używali śmy
wcze
ś n i ej:
CBox box2 :
II Deklaracja obiektu box2 typu CBox.
Obiekt box2 w powyższym kodzie pozostawiliśmy bez wartości początkowych . Kiedy spró bujemy skompilować program z tą instrukcją, to otrzymamy następujący komunikat o błędzie :
error C2512: 'CBox' : no appropri at e default const ructor avail able Oznacza to, że kompilator poszukuje konstruktora domyślnego (czasami nazywanego kon struktorem bezargumentowym, ponieważ nie wymaga żadnych argumentów podczas wywc> ływania) dla obiektu box2, gdyż nie podaliśmy żadnych wartośc i początkowych dla zmiennych składowych. Konstruktorowi domyślnemu nie trzeba przekazywać żadnych argum entów. Będzie on wtedy konstruktorem nieposiadającym żadnych parametrów w definicji lub kon struktorem, którego wszystkie argumenty są opcjonalne. Ale przeci eż instrukcja ta była cał kowicie poprawna w programie Cw7_02.cpp. Czemu więc niejest teraz? W poprzednim przykładzie został u żyty konstruktor domy ślny dostarczony przez kompila tor ze względu na fakt , że my nie podali śmy żadnego konstruktora. Jako że w tym programie podaliśmy już konstruktor, kompilator uznał, że postanowiliśmy zatroszczyć się o wszystko sami i nie podal i śmy konstruktora domyślnego. W związku z tym , jeżeli nadal chcemy tworzyć deklaracje obiektów klasy Cbox, nie podając wartości początkowych, to musimy na własną
Rozdział 7.•
Definiowanie wlasnych lypÓw danych
383
rękę dołączyć konstruktor domyślny. Jak dokładnie wygląda konstruktor domyślny? W naj prostszym przypadku jest to po prostu konstruktor nieprzyjmujący żadnych argumentów nie musi on nawet nic robić :
CBox()
II Konstruktor domyślny.
II Calkowity brak instrukcji.
{}
Przyjrzyjmy
się
takiemu konstruktorowi w akcji.
EIIlmIIiI Dostarczanie konstruktora Ilomyślnego Do ostatniej wersji naszego programu dodamy nasz domyślny konstruktor, deklarację obiektu box2 oraz oryginalne przypisania dla zmiennych składowych obiektu box2. Musimy trochę rozbudować nasz konstruktor domyślny, aby było wiadomo, kiedy został wywołany . Poniżej znajduje się zmodyfikowana wersja programu: IICw7_05.cpp
VislJal C++ 2005. Oli pOlJslaw CBox box1(78.0.24.0.18. 0) : CBox box2: CBox cigarBox(8.0. 5.0. 1.0) :
II Deklara cja i inicj alizacj a obiektu box1. II Deklaracja obiektu box2 bez wartoś c i po czątkowej. II Deklaracj a i inicjalizacja obiektu cigarBox.
doub le boxVolume = 0.0:
II Przechowuj e pojemnoś ć pu de łka.
boxVol ume = box1 .Vo lume() ; cout « end l
II Obliczanie objętoś ci pudelka boxl.
·P o j em no ~ć p u d e ł k a
«
box 1 =
«
boxVolume:
box2 .m_Height = boxl. m_Height - 10: II Definicj a składowych box2.m_Length = boxl. m_Length / 2. O; Il obiektu box2 box2 .m_Width = O.25*box1.m_Lengt h: II w katego riach obiektu box l . cout
« « «
cout
«
endl . Poj emnoś ć pudełk a box2 = box2.Vo lume() : end l « «
· P oj emn o ~ ć p u de ł k a
cigarBox
=
cigarBox.Vol ume():
cout « sndl: ret urn O:
Jak to działa Teraz, gdy dos tarc zyli śmy własny konstruktor d omyślny, podczas kompil acji nie zos tały zgło szone żadne komunikaty o błędach i wszy stko d zi ała jak n ale ży. Wynik d zi ałania programu jes t następuj ąc y :
Konst ruktor Konstruktor Konstru ktor
z o st a ł wywo ł a ny . domyś lny z o st ał wywo ła ny. z os ta ł wywo ła ny .
Poj emn o ~ ć p u de ł k a
Poj em n o ~ ć p u d e ł k a Po jemno~ć p u d e ł k a
box1 = 33696 box2 ~ 6084 cigarBox = 40
J edyną c zynn o ś cią wykon ywaną
przez kon struktor d om y ślny j est wy św i etl eni e komunio katu. Jak w i d ać , zo st ał on w y w oł any w momencie zadeklarowania ob iektu box2. Objęto ś c i wszystkich trzech obiektów klasy CBox s ą poprawne, co oznacza , że reszt a programu d zi ała bez zarzutów. Na przykładzie tego programu dowiedziel i śmy s ię, że konstruktory można p rzeciążać podob nie j ak funk cje (patrz: rozd z iał 6.) Zd efiniow ali śmy przed c h w i lą dwa konstrukt ory, które różn iły s ię tylko li stą parametrów. Jeden z nich ma trzy param etry typu doubl e, a drugi nie ma żad nyc h parametrów.
Rozdział 7••
Definiowanie własnych typÓW lianych
385
Przypisywanie domyślnych wartości
parametrom umieszczonym wklasach
Kiedy opisywałem funkcje, pokazałem sposób podawania wartości domyślnych dla parame trów funkcji w jej prototypie. To samo możemy zrobi ć ze składowymi klasy, włącznie z kon struktorami. Jeżeli definicję funkcji składowej um ieścimy wewnątrz definicj i klasy, to wartośc i domyślne jej parametrów m o żemy podać w na gł ówku funkcji . Jeżeli natomiast w defini cji klasy podamy tylko prototyp, to domyślne wartości parametrów powinny zostać umieszc zone w protot ypie. Gdyby śmy nicję
chcieli, aby domyślni e każdy obiekt CBox miał wszystkie boki długości l , to defi klasy w ostatnim przykładzie zmienilibyśmy w na stępujący sposób:
cl ass CBox II Definicja klasy o zas ięgu globalnym.
{ publ tc : doubl e m_Lengt h : double m Widt h : double m He ight : II Definicja konstrukt ora. CBox( double l v ~ 1. 0 . double bv .
~
1.0 . doubl e hv
~
1. 0)
{ cout « endl « m_Lengt h ~ l v : m_Wi dt h = bv: m_He ight ~ hv :
"Konstr ukt or
zo s t ał
wywoł any . " : II Usta wianie wartości Ilzmienny ch składowyc h.
} II Defin icja konstruktora domyś ln ego. CBox ()
{ cout «
endl «
" Konst ru kt or
domyś l n y z os t a ł wywo ł a ny
} II Funkcja o bliczająca pojemność pudełka . doubl e Vol ume()
{
}:
Co
się
zgło si
stanie, jeżeli dokonamy tej zmiany w ostatnim przykładzie? Oczywi ści e kompilator inny komunikat o błędzie. Mi ędzy innymi także ten , który widać poniżej :
wa r ning C4520' ' CBox' : mu lti pl e defaul t const r uct or s specifi ed er r or C2668: ' CBox: :CBox ' · emoi quous cal l t o over loa ded func t i on
Oznacza to, że kompilator nie wie, który z tych dwóch konstruktorów powinien być wywo łany - ten, dla którego parametrów podaliśmy zbiór warto ści domyślnych, czy ten, który nie przyjmuje żadnych parametrów. Dzieje się tak, gdyż deklaracja obiektu box2 wymaga kon struktora bez parametrów, a teraz każdy z konstruktorów może zostać wywołany bez para metrów. Rozwiązaniem , które natychm iast się nasuwa, jest pozbycie się konstruktora nie przyjmującego żadnych parametrów. W rzeczywistości będzie to korzystne. Po usunięciu tego konstruktora składowe każdego obiektu klasy CBox zadeklarowanego bez podawania żadnych wartości początkowych zostaną automatycznie zainicjalizowane wartością I.
386
Visual C++ 2005. Od pOlIslaw
~ Dostarczanie wartości Ilomyślnych lila argumentów konstruktora Spójrzmy teraz na
przykładow y
prograin
ilu struj ący powyższe
zagadnienie :
II Cw7_D6.cpp
II Dostarczanie wartości domyślnych dla argume ntów konstruktora.
#i ncl ude usi ng st d: :cout ; usi ng std: :endl; class CBox
II Definicja klasy w
zas ięgzi
globalnym.
{
pu bl i c: doub le m_L engt h: doub le mWidth: doub le m_Hei ght :
II Dł ugosć pudelka w centymetrach. II Sze rokość pudelka w centy metrach. II Wys okoś ć p udelka w centymetrach.
II Defin icj a konstruktora.
CBox(double l v = 1.0. doub le bv = 1.0. double hv = 1.0) (
cout « end l « "Konst rukt or m_Length = l v: m_Widt h = bv; m_He ight = hv; obliczająca pojemnos ć
II Funkcja
zosta ł wywo ł any .
";
II Ustawianie wartoś ci II zmiennych składo wyc h.
pudelka.
do ub le Vol ume() { } };
i nt mai n() (
CBox box2:
II Deklaracja obiektu box2 - brak wartosci
domyślnej.
cout « endl « «
"Poj em no ś ć p ude ł k a
box2
=
box2 .Vo l ume () :
cout « end l ; ret urn O:
Jak lo działa Zdefiniowal i śmy tylko jedną, niezainicjalizowaną zmienną klasy CBox - box2, ponieważ to wystarczy nam do naszych celów demonstracyjnych. Program w tej wersji daje na stępujący rezultat:
Konst rukt or
zo s t a ł wywo łany.
Po j em n o ś ć pu d eł ka
box2
~
l
Rozdział 7••
Z tego wynika, wiając wartości
Definiowanie własnych typÓW danych
że
konstruktor z domyślnymi wartościami parametrów obiektów, które nie mają wartości początkowych.
działa
387
poprawnie, usta
Nie oznacza to jednak, że powyższy przykład jest jedynym, a nawet zalecanym sposobem implementacji konstruktora domy ślnego, Wielokrotnie zdarzy się, że nie będziemy chcieli przypisywać wartości domyślnych w taki sposób - wtedy będzie trzeba napisać oddzielny konstruktor domyślny . Może się nawet zdarzyć , że nie będziemy chcieli, aby w ogóle działał jakikolwiek konstruktor domyślny, mimo że mamy zdefiniowany inny konstruktor. Takie po dejście zapewnia, że wszystkie zadeklarowane obiekty klasy muszą mieć wartości jawnie okre ślone w ich deklaracji.
Używanie listJ inicjalizacJjnej wkonstruktorze W dotychczasowych przykładach zmienne składowe obiektów klasy inicjalizowaliśmyza przypisania. Istnieje także inna technika, której można użyć do tego celu lista inicjalizacyjna konstruktora. Sposób jej użycia zademonstruję na zmodyfikowanej wersji konstruktora klasy CBox: pomocąjawnego
II Defini cja konstruktora z użyciem listy inicjaliza cyjnej .
Sposób, w jaki ta definicja konstruktora została zapisana, sugeruje, że powinna ona znajdo w obrębie definicji klasy. W tym przypadku wartości zmiennych składowych nie są ustawiane za pomocą instrukcji przypisania w ciele konstruktora. Podobnie jak w deklaracji, są one określone jako wartości początkowe za pomocą notacji funkcjonalnej oraz występują w liście inicjalizacyjnej jako część nagłówka funkcji. Na przykład zmienna składowa m_Length jest inicjalizowana wartością l v. Ten sposób może być o wiele bardziej wydajny niż przypi sania, które stosowaliśmy wcześniej . Jeżeli tę wersję konstruktora wstawimy w miejsce kon struktora w poprzednim programie, to stwierdzimy, że działa ona tak samo . wać się
Należy zauważyć , że
lista inicjalizacyjna oddzielona jest od listy parametrów dwukropkiem, a poszczególne elementy listy inicjalizacyjnej rozdzielane są przecinkami. Ta technika ini cjalizowania parametrów w konstruktorze jest bardzo ważna, gdyż - jak się później prze konamy - stanowi jedyny sposób ustawiania wartości niektórych typów składowych obiektu . Technika z użyciem listy inicjalizacyjnej jest też bardzo często stosowana w bibliotece MFC.
Prywatne składowe klasy Posiadanie konstruktora ustawiającego wartości składowe obiektu klasy przy jednoczesnym zezwoleniu dowolnej części programu na dostęp do wnętrza obiektu zakrawa na absurd. To tak jakby znaleźć genialnego chirurga, jak dr Zbój, który swoje umiejętności szlifował przez
388
Visual C++ 2005. Od podstaw wiele lat, do zrobienia porządku w naszym brzuchu, a potem pozwolić tam zajrzeć lokalnemu hydraulikowi albo murarzowi. Bez wątpienia potrzebujemy jakiejś ochrony dla naszych skł a dowych klasy. Ochronę taką możemy sobie zapewnić, stosując słowo kluczowe private w definicji zmien nych składowych klasy. Do zmiennych składowych zadeklarowanych jako prywatne (pri vate) z reguły dostęp mają tylko funkcje składowe tej samej klasy . Jest jeden wyjątek od tej reguły, ale tym zajmiemy się później . Zwykła funkcja nie ma możliwości bezpośredniego dostępu do zmiennej składowej klasy. Pokazano to na rysunku 7.6.
Rysunek 1.&
Obiekt klasy
OK
public Zmienne składowe ~
I-
~
s
" Volume () :
II Wyswietl pojemnoś ć obiektu wskazywanego prz ez
wskaźn ik pB ox.
W powyższej instrukcji znowu u żyty został operator pośredniego dostępu do s kład o wyc h. Jest to typowa, stosowana przez większość programistów notacja tego typu operacji. Od tej pory będę j ej u żywał cały czas.
~ Wskaźniki do klas Zrobimy sobie dodatkowe ćw i czen ie z zastosowania operatora pośredniego do stępu do skła dowych. Jako podstawę wykorzystamy kod programu Cw7_ lO.cpp i wprowadzimy do niego pewne modyfika cje. II Cw7j 3.cpp
II Ćwicze nie zastosowa nia operatora pośredn iego dostępu do skladowych klasy.
#i nc l ude
using st d: :cout :
USlng st d: :endl :
class CBox
II Definicj a klasy o zas ięgu g lobalny m.
{
publ ic:
II Definicj a konstrukt ora.
CBox(double l v = 1.0. double bv = 1.0. double hv
~
1.0)
{
cout « endl « "Konstruktor m_Length = l v: m Wid t h = bv:
m_Height
=
zo stał wywo ła ny . " :
II Ustawianie wartości
II zmi ennych skladowych.
hv:
II Funk cja oh/iczająca pojemność pudelka.
doub le Vol ume() con st
{
II Funkcja po ró wnująca dwa pudelka, która zwra ca lnie (1),
II jeżeli pierwsze jest wieksz e niż drugie, oraz false (O) w przeciwnym przypadku .
int Compare(CBox* pBox) const
{
ret urn thl S->Volume() > pBox- >Vol ume ( ):
408
Visual C++ 2005. Od podstaw pri vate : doub le m_Lengt h; doub le mWid t h: doub le m=Height :
II Dlugość pudelka w centymetrach. II Szerokoś ć pudelka w centym etrach. II Wysokość pudelka w centymetrach.
}:
int mai n() CBox boxes[5] : CBox mat ch(2.2. 1.1. 0.5); CBox clgar (8 O. 5 O. 1. O). CBox* pBl ~ &c igar ; CBox* pB2 ~ O:
II Deklaracja tablicy obiektów klasy CBox. II Deklaracja obiektu match.
II Deklaracja obiektu cigar.
Il l nicj alizacj a ws kaźnika do adresu obiektu cigar.
Il l nicj alizacj a wskaźnika do klasy CBox wartos etą nul/.
cout « endl « "Adres obiekt u cigar t o " « pBl II Wyświetlanie adresów. « endl « " Poj em n ość pudełka cigar to .. « pBl->Vo l ume () : II Pojemn oś ć wskazywanych obiekt ów. pB2 ~ &matc h; i f( pB2->Compare(pBl) II Porównywanie p oprzez cout « endl « " P u dełk o mat ch jest wi ę ksze ni ż cigar"; el se cout « end l « " P u d e ł k o match Jest mn iej sze l ub równe cigar": pBl ~ boxes: boxes[ 2] ~ matc h; cout « endl «
" Poj emność p ude ł k a
boxes[2] t o "
wskaźniki.
II Ustawienie na adres tablicy.
II Ustawienie trzeciego elementu tablicy na match.
II Dostęp poprzez wskaźn ik.
« (pB l + 2)->Vol ume() ;
cout « endl : ret urn O; Wykonanie
powyższego
programu da rezultat podobny do poniższego :
Konst rukt or zos t a ł wywo ł a ny .
Konst rukt or z os t a ł wywoła ny .
Konstruktor zos t a ł wywoł any .
Konst ruktor zos tał wywo ła ny .
Konst rukt or zo stał wywołany .
Konstruktor zo s ta ł wywoła ny .
Konst rukt or zos t ał wywoła ny .
Adres obiektu cigar to 0012FE20
P oj e mność pu de ł k a cigar t o 40
P u de ł ko m atc h jest mniej sze l ub równe cigar
Pojemność pu de łk a boxes[2] to 1.21
Oczywiście
adres obiektu c i gar na każdym komputerz e może
być
inny.
Rozdzial7.• Definiowanie własnych typÓW danych
409
Jak to działa Jedyna zmiana, której dokonaliśmy w klasie, nie ma wielkiego znaczenia. Polegała ona na modyfikacji funkcji Compa re() w taki sposób, aby przyjmowała j ako argument wsk aźnik do obiektu klasy CBox. Dodatkowo, kiedy mamy j uż w iedzę na temat stałych funkcji s kł a dowych, dekl arujemy ją za pomocą słówa kluczowego const, ponieważ nie wpływa ona na zmianę zawarto ści obiektu. W funkcji ma i n() na różne sposoby użyte zo stały wskaźniki do obiektów klasy CBox. W obrębie funkcji main() deklarujemy dwa w skaźn iki do obiektów klasy CBox po deklaracji tabl icy Boxes oraz obiekty klasy Cbox: cigar i mat ch. Pierwszy wskaźnik pBI zo stał zainicjali zowany adresem obiektu ciga r, a drugi (pB2) wartością NULL. Wszystkie te użycia wskaźników są dokładnie takie same, jak gdybyśmy mieli do czynienia ze wskaźnikami do typów pod stawowych. Fakt, ż e używamy wskaźnika do typu, który sami stworzyliśmy , nie robi żadnej różnicy.
Wskaźnik pBI w połączen iu z operatorem pośredniego dostępu do s kład owyc h u żyty został do obliczenia pojemności obiektu przez niego wskazywanego. Wynik jest później wysyłany na wyjście . Następnie wskaźnikowi pB2 przypisujemy adres obiektu match i obu wskaźników używamy do wywołania funkcji Compare(). Jako że argument funkcji Compare() jest wskaźni kiem do obiektu klasy CBox, funkcja ta w wywołaniu funkcji Vol ume O dla obiektu wykorzy stuje operator pośredniego dostępu do składowych .
pBI podczas korzystania z niego przy wyborze składowej mo ustawiamy wskaźnik pBI na adres pierwszego elementu tablicy typu CBox - boxes. W tym przypadku wybieramy trzeci element tabli cy i obli czamy jego pojemność . Jest ona taka sama jak pojemność obiektu mat ch. Aby
pokazać , że
na
wskaźniku
żemy zastosować arytmetykę adresową,
Z danych na wyjśc iu wynika, że konstruktor obi ektów klasy CBo x został wywołany siedem razy : pięć dla elementów tablicy Boxes plus po jednym razie dla obiektów ciga r i match. Mówiąc ogólnie, pomiędzy używaniem wskaźnika
takiego jak np. doubl e, nie ma
do obiektów klasy i do typu podstawowego,
ża d nej różnicy .
Referencie do obiektów Referencje stają s ię naprawdę przydatne, kiedy używamy ich z klasami . Podobn ie jak w przy padku wskaźników, nie ma żadnej różnicy pom iędzy stosowani em referencji do obiektów klas a stosowaniem ich do zmiennych typów podstawowych. Na przykład referencję do obiektu ci gar możemy utworzyć za pomocą poniższej instrukcji:
CBox&rciga r
=
cigar :
II Definicja ref erencji do obiektu cigar.
Aby użyć referencji do obliczenia pojemności obiektu ci gar , wystarczy użyć tylko nazwy refe rencji w miejscu, gdzie powinna pojawić się nazwa obiektu:
cout
«
rcigar.Volume();
II Wyś lij p oprzez II ob iektu ciga r.
ws kaźn ik na wyjście pojemność
410
Visual C++ 2005. Od podslaw Jak prawdopodobnie pamiętasz, referencje działająjak aliasy obiektów, do których w związku z czym używa się ich dokładnie tak samo ja k nazw oryg inalnych.
się odnoszą,
Implemenlacia konslruklora kopiUiącegO Znaczenie referencji jest naprawdę widoc zne dopiero w kontekś cie argumentów do funkcji i wartoś c i przez nie zwracanych, a w szcz egó l no ś c i funkcji będących składowymi klas. Na dobry początek powróćmy do kwestii konstruktora kopiującego . Chwilowo odkładamy na bok kwestię , kiedy potrzebujemy napisać własny konstruktor kopiujący, a koncentrujemy się na tym, jak go napi sać . Analizując problem , posłużę się klasą CBox j ako przykładem. Konstruktor kop iujący to konstruktor, który tworzy obiekt, inicjalizując go innym istn iejącym obiektem tej samej klasy . W związku z tym musi on akceptować jako argumenty obiekty tej klasy. Jego prototyp moglibyśmy napisać następująco:
już
CBox(CBox init B); Spójrzmy, co
się
stanie, gdy
wywołamy
taki konstrukto r.
Przypuśćmy , że
mamy
następującą
dekl arację:
CBox myBox = cigar : Wygeneruje ona
następujące wywołanie
kon struktora kopiującego:
CBox : :CBox(cigar): Wydaje się , że kod jest w pełn i właśc iwy , dopóki nie zdamy sobie sprawy, że argument jest przekazywany przez wartość. Oznacza to, że przed przekazaniem obiektu ci gar kompilator musi zrobić jego kopię W związku z tym kompilator wywołuje konstruktor kopiujący w celu utworzenia kopii argumentu użytego do wywołania konstruktora kopiującego . Niestety, jako że obiekt ten przekazywany jest przez wartość , to wywołanie również wymaga zrobienia kopii argumentu, a więc zostaje wywołany konstruktor kopiujący i tak c ały czas. Rozwi ązaniem
- jestem pewien , że udało Ci się do tego doj ść samodzielnie - jest u życie parametru referencyjnego typu const . Prototyp konstruktora możemy zapisać następująco :
CBox(const CBox& init B): Teraz argument konstruktora kopiującego nie musi być kopiowany. Jest on użyty do zaini cjalizowania parametru referencyjnego, a więc nie następuje żadne kopiowanie. Jak pami ętam y z częś ci, w której była mowa o referencjach, jeżeli parametr funkcji je st referencją, to argu ment podczas wywołania funkcji nie jest kopiowany. Funkcja ta uzyskuje bezpośredni do stęp do zmiennej argumentu. Kwalifikator const zapewnia, że argument ten nie zostanie w żaden sposób zmodyfikowany przez funkcję. Jest to jeszcze j edno ważne zastosowanie kwalifikatora const. Zawsz e po winno się dekla rować parametr ref erencyjny funkcji jako const . chyba że funkcja ma go zmodyfiko wać.
Implementacja takiego konstruktora kopiującego
może wygl ąda ć następuj ąco:
Rozdzial7.• Definiowanie własnych typÓW danych
411
CBox : :CBox(const CBox&initB) {
m_Length = initB .m_Length:
m_W idt h = ini t B. m_Widt h:
m_Height = initB .m_Height :
Definicja konstruktora kopiującego narzuca umieszczenie go poza definicją klasy. W związku z tym przed nazwą konstruktora musi znajdować się kwalifikator w postaci nazwy klasy z dodatkiem operatora zasięgu. Każda składowa tworzonego obiektu inicjalizowana jest odpowiednią składową obiektu przekazanego jako argument. Oczywiście, do ustawienia war tości obiektu z takim samym powodzeniem możemy użyć listy inicjalizacyjnej. Przypadek ten nie jest przykładem tego, kiedy możemy potrzebować użyć konstruktora . Jak już widzieliśmy, domyślny konstruktor kopiujący działał bez zarzutów z obiektami klasy CBox. Kwestie, po co i kiedy używać własnego konstruktora kopiującego, poruszę w następnym rozdziale. kopiującego.
Programowanie wC++/CLI Język
C++/CLI posiada własne typy struct i cl ass. W rzeczywistości język ten pozwala na dwóch różnych typów st ruct i c l ass, które mają różne właściwości. Są to typy value st ruct i val ue cl ass oraz ref st ruct i ref cla ss. Każda para słów stanowi odrębne słowo kluczowe (value str uct , re f struct, value cla ss oraz ref cl ass) oznaczające co innego niż st ruct i cl ass . val ue i ref samodzielnie nie są słowami kluczowymi. Podobnie jak w natywnym CH, jedyną różnicą pomiędzy strukturami i klasami jest to, że składowe struktury są domyślnie publiczne, a składowe klasy prywatne. Największą różnicą pomiędzy klasami wartości (lub strukturami wartości ) oraz klasami referencji (lub strukturami referen cji) jest to, że zmienne typów klas wartości zawierają własne dane, natomiast zmienne dostępu do typów klas referencyjnych muszą być uchwytami, a zatem muszą zawierać adres y. definicję
Warto zwrócić
uwagę , że
funkcje składowe w C++/CLI nie mogą być deklarowane jako const. natywnym CH i C++/CLI jest to, że wskaźnik t his w niesta tycznej funkcj i składowej typu klasowego T j est wewnętrznym wskaźnikiem typu i nt e r i or_pt r, a wskaźnik t hi s w typie klasowym referencyjnym Tjest uchwytem typu T". Należy o tym pamiętać przy zwracaniu wskaźnika t hi s z funkcji w C++ /CLI lub podczas zapisywania go do zmiennej lokalnej. Sąjeszcze trzy inne ograniczenia, które mają zastoso wanie zarówno do klas wartości, jak i klas referencji: Następną różnicą pomiędzy
• Klasa wartości lub klasa referencji nie może zawierać pól w natywnym C++ lub typów klas w natywnym C++.
będących
tablicami
• Nie można używać funkcji zaprzyjaźnionych. • Klasa wartości lub referencji nie bitowymi .
może zawierać składowych będących
polami
412
Visual C++ 2005. 0(1 podstaw Jak już dowiedzieliśmy się w rozdziale 4., nazwy typów fundamentalnych, takie jak i nt i da ubl e, są skrótowym określeniem typów klasy wartości w programach CLR. Podczas deklaracji elementu danych typu klasy wartości pamięć dla niego zostanie przydzielona na stosie, ale obiekty klasy wartości można tworzyć również na stercie za pomocą operatora genew. W takim przypadku zmienna używana do uzyskiwania dostępu do takiego obiektu musi być uchwy tem. Na przykład: dauble pi ~ 3.142: i nt A lucky = 9cnew int(7); dauble A twa = 2.0 : Każdej
o
II Zmienna pi przechowywana jest na stosie .
/r tucky jest uchwytem i wartość 7 przechowywana jest na stercie.
II twa jest uchwytem i wartość 2. Oprzechowywana jest na stercie.
z tych zmiennych można użyć w działaniach arytmetycznych, ale należy pamiętać uchwytu za pomocą operatora * w celu uzyskania dostępu do wartości . Na
wyłuskaniu
przykład:
Cansa l e: :Wri tel i net l "2pi
~
{O}". *twa*pi l:
równie dobrze nasze działanie mogliśmy zapisać pi**twa i wynik byłby po prawny, ale lepiej jest w takim przypadku zastosować nawiasy i napisać pi*(*two), gdyż zwięk szają one czytelność kodu .
Zwróć uwagę, że
Deliniowanie typÓW klas wartości Nie będę opisywał typów value struet (strukturalnych) i typów value elass (klasowych) oddzielnie, ponieważ jedyna różnica pomiędzy nimi polega na tym, że składowe strukturalne domyślnie są publiczne, a składowe klasowe prywatne . Klasa wartości ma być względnie pro stą klasą umożliwiającą definiowanie nowych prymitywnych typów, których można używać w sposób podobny do typów fundamentalnych. Aby jednak móc w pełni korzystać z tych moż liwości, trzeba najpierw zaznajomić się z zagadnieniem przeciążania operatorów, o którym będzie mowa dopiero w następnym rozdziale. Zmienna typu klasy wartości tworzona jest na stosie i przechowuje wartość bezpośrednio, ale - jak już widzieliśmy---do typów skalarnych na stercie CLR możemy odnosić się za pomocą uchwytu śledzącego. Spójrzmy na przykład definicji prostej klasy II Klasa
reprezentująca
wartości:
wzrost.
(a lue class Hei 9ht private: II Zapisuje w ost w metrach i centymetrach.
t
i nt metry: i nt .centymetry:
publi c:
II Tworzenie
rostu z
wartości w
centymetrach.
Hei ghU i nt cm) {
metry = cmllOO:
centymetry ~ cm%100:
} II Tworzenie wzrostu z metrów i centymetrów.
HeighUint m. int cm) : netrytm) . centymetry(cm){}
};
Rozdzial7.• Deliniowanie wlasnych typÓW danych
413
Powyższy
kod definiuje klasę wartości o nazwie Hei ght. Ma ona dwa pola prywatne typu i nt, wzrost w metrach i centymetrach. Klasa posiada dwa konstruktory - jeden tworzący obiekt klasy Hei ght z liczby centymetrów podanej jako argument, a drugi obiekt klasy Hei ght z metrów i centymetrów podanych jako argumenty. Drugi z nich powinien jeszcze sprawdzać, czy podana jako argument liczba centymetrów jest mniejsza niż 100, ale pozo stawiam to'Czytelnikowi do własnego dostosowania. Aby utworzyć zmienną typu Hei ght,
które
zapisują
możemy posłużyć się następującą instrukcją :
Height t all = Height Cl, 80):
II Wzrost wynosi l metr 80 centym etrów.
Powyższa
instrukcja tworzy zmienną ta ll zawierającą obiekt klasy Height reprezentujący l metr 80 centymetrów. Utworzenie tego obiektu wymagało wywołania konstruktora z dwoma parametrami.
He ight baseHei ght: Powyższa instrukcja tworzy zmienną baseHei ght , która automatycznie zostanie zainicjali zowana wartością O. Klasa Height nie posiada konstruktora bezargumentowego i ze względu na fakt, że jest to klasa wartości, nie można go dostarczyć w definicji tej klasy. Konstruktor bezargumentowy zostanie dołączony automatycznie do klasy wartości podstawowych i zaini cjalizuje on wszystkie pola wartości do wartości równej O oraz wszystkie pola, które są uchwy tami do nullptr. Tego konstruktora nie można zastąpić własnym. Wartość zmiennej base He i ght zostanie utworzona właśnie przez ten domyślny konstruktor.
Istnieje jeszcze kilka innych • W definicji klasy nie
ogran iczeń dotyczących zawartości
można umieszczać
konstruktora
• Operatora przypisania w klasie wartości nie operatorów będzie mowa w rozdziale 8.).
klasy
wartości :
kopiującego.
można przesłonić
(o
przesłanianiu
Obiekty klasy wartości są zawsze kopiowane poprzez kopiowanie pól, a przypisanie jednego obiektu klasy wartości do drugiego wykonywane jest w ten sam sposób. Przeznaczeniem klas wartości jest reprezentowanie prostych obiektów definiowanych za pomocą ograniczonej ilości danych. W związku z tym w przypadku obiektów niepasujących do tego opisu lub w przypad kach, w których te ograniczenia sprawiają problemy, należy do reprezentacji obiektów używać klas referencyjnych. \ Przetestujmy naszą klasę wartości Heigh t:
~ Definiowanie iuiywanie klasy wartości Poniżej
znajduje
się
kod
ćwiczenia
#i ncl ude "st dafx.h" usi ng namespace System : II Klasa
reprezentująca
wzrost.
zastosowania klasy
wartości
skalarnych Hei ght:
414
Visual C++ 2005. Od podstaw val ue class Hei ght
{
pri vat e:
II Zap isyw anie wzrostu w metrach i centymetrach.
i nt metry. i nt centymetry : publ ic: II Tworzenie wzrostu z
wartoś ci
w centymetrach.
Hei ght(i nt cm) (
met ry = cm/lOO: cent ymet ry = cm% l OO: II Tworzenie wzrostu z metrów i centymetrów.
Hei ght(i nt m. i nt cm) : met ry(m). cent ymet ry(cm) {l
l: int main(array<Syste m: :Stri ng
Ą> Ą a r g s )
(
Height myHeight = Hei ght (1.70):
H eig h t yourHeight = He ight (160): Hei ght hisHei ght = *yourHei ght : Ą
Consol e: : \~ r i t e L i ne(L"M6j wzrost t o {O)" . myHeightl:
Console: .WriteL i ne(L"Twój wz rost t o (O)". yournet ght) :
Console : :WriteLi ne(L" Jego wzrost t o (Ol ". hisHeight ) :
ret urn o:
Uruchomienie tego programu da
następujący
rezultat:
Mój wzrost t o Height
Twój wz rost to Height
Jego wzrost to Height
Jak lo działa Wynik jest raczej mało ciekawy i pewnie spodziewaliśmy się czegoś więcej, ale wrócimy do tego trochę później. W funkcji mai n( ) utworzyliśmy trzy zmienne za pomocą następujących instrukcji: Height myHeight = Height(1 .70) :
He i ę h t " yourHeight = Hei ght( 60):
Height hisHeight = *yourHeight:
Pierwsza zmienna jest typu Hei ght , a więc obiektowi reprezentującemu wzrost l metr 70 centymetrów została przydzielona pamięć na stosie. Druga zmienna jest uchwytem typu He;g ht "', a więc obiektowi reprezentującemu wzrost l metr 60 centymetrów została przydzie lona pamięć na stercie CLR. Trzecia zmienna jest jeszcze jedną zmienną stanowiącą kopi ę obiektu, do którego odnosi s i ę obiekt your Hei ght. Jako że yourHeight jest uchwytem, przed przypisaniem go do zmiennej hi sHei ght musimy go najpierw wyłuskać . W wyniku tego hi s Hei ght zawiera kopię obiektu, do którego odnosi się yourHeight . Zmienne klasy wartości zaw sze zawierają unikalny obiekt, a więc dwie takie zmienne nic mogą odnosić się do tego samego
Rozdział7 .•
Definiowanie własnych typÓW danych
415
obiektu. Przypisywanie jednej zmiennej typu klasy wartości do innej zaws ze związane jest z kopiowaniem. Oczywiście kilka uchwytów może odnosić się do jednego obiektu , a przypisa nie wartości jednego uchwytu do drugiego jest po prostu skopiowaniem adresu (lub wartości nul l ptr) zjednego uchwytu do drugiego. W wyniku tego oba obiekty wskazują ten sam obiekt. Dane wysyłane są na wyj ście za pomocą trzech wywołań funkcji Consol e : :Wri tel i ne( ). Nie stety nie zostały wysłane wartości obiektów klasy wartości , a po prostu nazwa klasy . Jak to się stało? Oczekując , że zostaną wyświetlone wartości , byliśmy optymistami. Skąd kompilator miał wiedzieć, w jaki sposób je zaprezentować? Obiekty klasy Hei ght zawierają dwie wartości. Która z nich powinna zostać zaprezentowana na ekranie? W klasie musi być do stępny mecha nizm udostępniania danej wartości w danym kontekście.
Funkcja ToSlringlJ wklasie Każda
klasa defini owana w C++/CLl ma funkcję ToStri ng( ) (więcej na ten temat powiem rozdziale, kiedy będę omawiał dziedziczenie), która zwraca uchwyt do łańcucha reprezentującego obiekt klasy. Kompilator wywołuje funkcję ToStri ng() dla obiektu, kiedy stwierdza, że potrzebna jest łańcuchowa reprezentacja obiektu. Funkcję t ę można wywołać w razie potrzeby jawnie. Na przykład : w
następnym
double pi = 3.142 : Console : :WriteLi ne(pi .ToSt ri ng()) : Powyższa instrukcja wysyła na wyj ście wartość zmiennej pi w postaci łańcucha. Łańcuch ten jest dostarczony przez funkcję ToSt ri ngO zdefiniowaną w klasie System: :Double . Oczywiście ten sam wynik otrzymalibyśmy bez jawnego wywołania tej funkcji.
Funkcja ToSt ri ng( ) w domyślnej wersji , którą otrzymujemy w klasie Hei ght, wysyła na wyj śc ie tylko nazwę klasy, ponieważ nie ma sposobu dowiedzenia się z góry, która wartość po winna zostać zwrócona jako łańcuch dla obiektu naszego typu klasowego. Aby funkcja Con so l e : :Wri t el i ne () wysłała na wyj ście właściwą warto ść w poprzednim przykładzie, należy dodać funkcję ToSt r ing () do klasy Height, która zaprezentuje wartość obiektu w takiej formie, w jakiej chcemy. Poniżej
znajduj e
II Klasa
s ię
klasa z
reprezentują ca
dodan ą funkcją ToSt
r i ng ( ):
wzrost.
va l ue class Height
{
pri vate :
II Zapisuj e wzrost w metra ch i centymetrach.
int metry :
i nt centymetry :
publ ic
II Tworzenie wzrostu z
wa rtoś ci
w centymetrach.
Hei ght (i nt cm) {
met ry = cm/lOO :
centymet ry = cm%100:
} II Tworzenie wzros tu z metrów i centymetrów.
He ight (i nt m. i nt cm ) : met ry(m ) . cent ymet ry(cm){ }
416
Visual C++ 2005. Od podstaw II Tworzenie
łańcu ch o wej
reprezent acji obiektu.
vir tual Str ing ToStri ng() overrlde
A
{
ret urn met ry
+
L" met r
"+
centymet ry
+
L" centymetrów" ;
};
Kombinacja słowa kluczowego vi r t ual znajduj ącego się przed typem zwra canym funkcji ToSt r i ng() oraz słowa kluczowego over r i de znajdującego się po li ście parametrów funkcji wska zuje, że ta wersja funkcji ToSt ri ng ( ) przesłania domyślną wersję tej funkcji w klasie. Dużo więcej na ten temat powiemy sobie w rozdziale 8. Funkcja ToSt r ing( ) w nowej wersji wysyła na wyjście łańcuch wyrażający wzrost w metrach i centymetrach. Jeżeli dodamy tę funkcję do definicji klasy w poprzednim przykładzie , to rezultat uruchomien ia programu będzi e następujący :
MÓJ wz rost t o l metr 70 centymetrów
Twój wzrost t o l met r 60 centymet rów
Jego wzrost t o l met r 60 centymetrów
Teraz wynik jest j uż bliższy spodziewanemu. Z danych na wyj ściu widać , że funkcja Wri t e Li ne( ) całkiem dobrze radzi sobie z obiektem na stercie CLR, do którego odnosimy się poprzez uchwyt yourHei ght,jak również z utworzonymi na stosie obiektami myHei ght i hi sHei ght.
Pola Iileralowe Współczynnik 100, które go używali śmy do konwersji metrów na centymetry i odwrotnie , jest trochę problematyczny. Stanowi on przykład tak zwanej ,,magicznej liczby", czyli liczby, której znaczenia lub pochodzenia czytaj ący kod musi się w jak i ś sposób domy śl ić. W tym przypadku oczywiste jest, co znaczy liczba 100, ale w wielu innych przypadkach pochodzenie stał ej liczbowej wcale nie jest takie jasne. W C++/CLI dostępne jest narzędzie zwane polem literaiowym (ang. litera l field) , służące do wprowadzania nazwanych stałych do klasy, co rozwi ązuje ten problem . Poniższy kod prezentuje, w jaki sposób można pozbyć s ię mag icznej liczby z kodu wjednoargumentowym konstruktorze w klasie Heigh t :
val ue class Height
{
private;
II Zap isuje wzros t w metrach i centymetrach.
int met ry;
int centymet ry:
l it eral int cmNaMetr ; 100:
publ i c :
II Tworzenie wzrostu z
warto ści
w centymetrach.
HeightC i nt cm) met ry = cm / cmNaMetr ;
centymetry = cm %cmNaMet r ;
}
II Tworzenie wzrostu z metrów i centymetrów.
Height (i nt m. i nt cm) : metry( m) , centymetry (cm) {} II Tworzen ie reprezentacj i
łańcu chowej
obiektu.
vi rtual Stri ng ToSt ri ng( ) overr ide A
{
Rozdzial7.• Definiowanie własnych typÓW danych. ret urn met ry": L" metr
"+
centymet ry
+
417
L" cent ymetrów";
}
};
Teraz konstruktor używa nazwy cmNaMetr zamiast liczby 100, dzięki czemu nie ma wątpliwości, co się dzieje w kodzie . . Warto ść pola literałowego można zdefiniować w kategoriach innych pól literałowych, pod warunkiem że nazwy pól, których mamy zamiar użyć do określenia tej wartości, zostały zde finiowane pierwsze. Na przykład :
va l ue class Height
{
II Jakiś kod...
l it eral int i nchesPerFoot ~ 12 ;
l it eral double mi l l lmet ersPerlnch = 25.4:
l it eral double mill imet ersPerFoot = inchesPerFoot *mi lli met ersPerl nch:
II Jakiś kod...
W powyższym kodzie zdefiniowaliśmy wartość pola literałowego mi 11 imetersPe rFoot jako iloczyn dwóch pozostałych pól literałowych. Gdybyśmy przenieśli definicję pola mi 11ime tersPe rFoot przed którekolwiek z pozostałych dwóch pól, to kodu nie można by skompilować .
Deliniowanie IYPÓW relerencyjnych Klasa referencyjna ma możliwości podobne do klasy w natywnym C++ oraz nie ma ograni czeń właściwych klasie wartości. W prz eciwieństwie jednak do klasy w natywnym C++, klasa referencyjna nie posiada domyślnego konstruktora kopiującego ani domyślnego operatora przypisania. Jeżeli chcemy, aby nasza klasa obsługiwała który ś z tych operatorów, musimy w tym celu jawnie dodać odpowiedni ą funkcję - jak tego dokonać , dowiemy się w następnym rozdziale . Klasę referencyjną definiujemy za pomocą słowa kluczowego ref c1ass - oba słowa roz dzielone co najmniej jedną spacj ą reprezentują pojedyncze słowo kluczowe. Poniżej znajduje się klasa CBox z programu Cw7_07, zdefiniowana ponownie jako klasa referen cyjna.
Console: :Writ ell neCL"Konst rukt or bezargument owy
z o stał wyw oł any ,
} II Defini cja konstruktora p rzy
użyciu
listy inicj alizacyjn ej .
BoxCdoub le l v. doub le bv. doub le hv ): Length Cl vl , Widt hCbv). Height Chv) Console: :W r it eLineCL" Konst rukt or } II Funkcja
obliczającapojemność pu delka.
double Vol umeC)
zosta ł wywo ł any ." ) ;
") ;
418
Visual C++ 2005. Od podstaw {
return Length*Wi dth*Height:
}
pri vate : double Length: double Width : doub le He ight :
II D/ugość pudełka w centymetrach. II Szerokość pudełka w centym etrach. II Wysokość pudełka w centymetrach.
}:
Warto zwrócić uwagę, że sprzed nazwy klasy usunąłem przedrostek C oraz przedrostek m_ sprzed nazw pól, gdyż notacja ta nie jest zalecana dla klas pisanych w C++/CLI. Dla para metrów funkcji i kon struktorów klas w C++/CLl nie można podawać wartośc i domy ślnych , a więc czynności te w klasie Box wykonać musi konstruktor bezargumentowy. Konstruktor bezargumentowy zainicjalizował wszystkie trzy prywatne pola klasy wartośc iami l . O.
~ Stosowanie typU referencyjnego W poniższym kodzie zastosowano
klasę
Box, o której
mówiliśmy wcześniej.
#l ncl ude "stdafx.h" using names pace System : ref class Box (
} II Definicja konstruktora przy użyciu listy inicjalizacyj nej.
Box Cdou ble lv, double bv, doub le hv) : Lengt hCl v) , Widt hC bv ) , HeightC hv) Conso le : :WriteLi neCL"Konst rukt or II Funkcj a
zos t a ł wywo ła ny .
"):
ob liczają ca pojemnoś ć p udelka.
double Volume()
{
ret urn Lengt h*Wldt h*Height :
}
pri vat e: double Length: double Width: double Height :
II D/ugość pudełka w centymetrach, II Szerokość pudełka w centymetrach. II Wysokość pudełka w centymetrach.
}:
i nt mai nCarray<Sy stem : :Stri ng {
A>
A
args)
Rozdzial7.• Box" asox:
Dełiniowalliewłasnych typÓW danych
419
II Uchwyt typu Bor".
Box newBox ~ gcnew Box(10. 15. 20):
aBox = gcnew Box; II ln icj alizacj a domyślnym i wartościami klasy Box.
Console : :WriteLi ne( L"Domy śln a po jemno ś ć obiektu klasy Box wynos i (O r .
aBox->Vol ume( »;
Console ; ;Wr iteLi net L"Poj emno ś ć nowego obiekt u kl asy Box wynosi (Ol".
newBox- >Vol ume (» ;
ret urn o: A
Rezultat
działania
tego programu jest następujący:
Konst ruk tor z o s ta ł wywoł a ny .
Konst rukt or bezargument owy zosta ł wywo łany .
Domyś lna pojemność ob iekt u klasy Box w ynosi 1
P o j emn o ś ć now ego obiektu klasy Box wynos i 3000
Jak to działa Pierwsza instrukcja w funkcji mai n() tworzy uchwyt do obiektu klasy Box.
Box aBox: A
II Uchwyt typu Box/:
Powyższa
instrukcja nie tworzy żadnego obiektu, tylko uchwyt śledzący aBox. Zmienna aBox zainicjalizowana wartością nul l ptr, a więc nic jeszcze nie wskazuje. Z kolei zmienna typu klasowego zawsze zawiera jakiś obiekt. domyślnie została
Następna
instrukcj a tworzy uchwyt do nowego obiektu klasy Box.
Box newBox = gcnew Box(10 . 15. 20 ): A
Konstruktor przyjmujący trzy argumenty zostaje wywołany w celu utworzen ia na stercie obiektu klasy Box, ajego adres przechowywany jest w uchwycie newBox. Jak wiadomo, obiekty typu re f cl ass zawsze tworzone są na stercie CLR i odnosi się do nich zawsz e za pomocą uchwytów. Tworzymy obiekt klasy Box, wywołując konstruktor bezargumentowy oraz przechowując jego adres w zmiennej aBox.
aBox = gcnew Box: Wartości
Na
II Inicjalizacja za pomocą Box.
pól Length , Wi dt h oraz Hei ght
zakończenie wysyłamy
na
zostają
ustawione na l . O.
wyjście pojemności
obu utworzonych obiektów.
Console: :WriteL ine(L "Domyślna po j emn o ść obiekt u klasy Box wynosi [D}".
aBox->Vol ume(»;
Console: : W r it eL i n e( L "Po jemność obiekt u klasy Box wynos i (Ol " . newBox->Volume(» :
Ze względu na fakt, że aBox i newBox s ą uchwytami, w celu obiektów, do których się odnoszą, używamy operatora - >.
wywołania
funkcji Vo l umeO dla
420
Visual C++ 2005. Od podstaw
Właściwości
klasy
Właściwość
jest s kładową klasy wartości lub klasy referencyjnej, do której dostęp uzyskuje w taki sam sposób jak do zwykłego pola, ale nie jest ona polem . Główną różnicą pomiędzy właściwością i polem jest to, że nazwa pola odno si się do lokalizacji przechowującej dane, a nazwa właściwości nie - wyw ołuje ona funkcję . Wartość właściwości odszukuje się i ustawia za pomocą funkcji dostępowych, odpowiednio get ( ) i set O . A zatem używając na zwy właś ciwości w celu pozyskania jej wartości, tak naprawdę wywołujemy dla niej funkcję dostępową get () , a gdy używamy nazwy właściwości po prawej stronie instrukcji przypisania, wywołujemy funkcję setO. Właściwość definiująca tylko funkcję getO zwana jest właści wością tylko do odczytu, ponieważ funkcja set O, która ustawia wartości, jest niedostępna. Właściwość może mieć także zdefiniowanątylko funkcję set() i w takim przypadku nazywa się wlaściwością tyłko do zapisu. się
Klasa może mieć dwa rodzaje właściwości: właściwości skalarne oraz właściwości indek sowane . Właściwości skalarne to pojedyncze wartości, do których dostęp uzyskiwany jest za pomocą nazwy, natomiast właściwości indeksowane to zbiory wartości, do których dostęp uzyskuje się za pomocą indeksu umieszczanego w kwadratowych nawiasach po nazwie wła ściwości. Klasa St r i ng ma właściwość skalarną Lengt h, która zwraca liczbę znaków w łańcu chu. Dostęp do właściwości Lengt h obiektu klasy St r i ng o nazwie st r uzyskamy za pomocą wyrażenia st r-> Length , ponieważ str jest uchwytem. Oczywiście, w celu uzyskania dostępu do właściwości o nazwie MyProp obiektu klasy warto ści przechowywanego w zmiennej val użylibyśmy wyrażenia val . MyPr op, podobnie jak w przypadku uzyskiwania dostępu do pól. Właściwość łańcucha Length jest przykładem właściwości tylko do odczytu, ponieważ nie ma ona zdefiniowanej funkcji set ( ) - nie można ustawić długości łańcucha, gdyż obiekty klasy Str i ng są niezmienne. Klasa St ri ng pozwala również na dostęp do poszczególnych znaków łańcucha w postaci właściwości indeksowanych. Dostęp do trzeciej właściwości indeksowanej łańcucha o nazwie st r uzyskalibyśmy za pomocą zapisu str [2J, który odpowiada trzeciemu znakowi łańcucha. Właściwości mogą być
skojarzone z określonym obiektem i jemu właściwe. W takim przy egzemplarzy. Właściwość Lengt h obiektu jest przykładem właściwości egzemplarza. Właściwość można także określić słowem kluczowym st at i c, w którym to przypadku właściwość ta będzie skojarzona z klasą, a jej wartość będzie taka sama dla wszystkich obiektów. Przyjrzyjmy się właściwościom trochę dokładniej . padku zwane
są właściwościami
Oeliniowanie właściwości skalarnych Właściwość skalarna ma pojedynczą wartość i definiuje się ją w klasie za pomocą słowa kluczowego propert y. Funkcja get ( ) właściwości skalarnej musi mieć taki sam typ zwracany jak typ właściwości , a funkcja set () musi mieć parametr tego samego typu co właściwość. Poniżej znajduje się przykładowa właściwość w klasie wartości Hei ght , którą widzieliśmy już wcześniej:
val ue class Height
{
pri vate:
II Zapisuje wzrost w stop ach i calach.
l
nt feet. :
Rozdział 7.•
Definiowanie własnych typÓW danych
421
i nt t nches:
l it eral int inchesPerFoot = 12:
l itera l double inchesToMet ers ~ 2.54/ 100;
pub l i c :
II Tworzenie wzrostu z cali.
Height(i nt ins)
{
feet ~ ins 1 inchesPerFoot :
inches = ins % inchesPerFoot:
}
II Tworzenie wzrostu ze stóp i cali.
He ight (int ft . mt t ns: II Wzrost w metrach jako
feet (ft ). tnchesCins ri}
wlasciwos ć .
prope rty double met ers
{
II Zwraca
wartość właściwości.
double get ()
{
ret urn inchesToMete rs*(feet*inchesPerFoot+i nches):
}
II Tutaj znajdowałaby s ię defini cjafunkcji seu) ... II Utwórz
reprezentację łańcuchową
obiektu.
vl rt ual Stri ng ToSt ring( ) override
A
{
ret urn feet + L" feet
" + t nches +
L"
i nches":
}
}:
Od tej pory klasa Hei ght zawiera właściwość o nazwie met er s. Definicja funkcji get () tej właściwości znajduje się pomiędzy nawiasami umieszczonymi po jej nazwie . Moglibyśmy również umieścić tutaj funkcję set () tej właściwości, gdyby taka istniała . Należy zwrócić uwagę , że po nawiasach klamrowych, w których zawiera się definicja funkcji get () i sett ), nie ma średnika . Funkcja get( ) właściwości meter s używa nowej składowej typu literałowego i nchesToMeters, która służy do konwersji wzrostu w calach na wzrost w metrach. Uzyskanie dostępu do właściwości metres obiektu typu Height udostępnia wartość wzrostu w metrach . Poniżej znajduje się przykład:
Hei ght ht = Height (6. 8): II Wzrost 6 stóp i osiem cali.
Console: 'Wr iteLine(L" W zrost wynosi {O} met rów" . ht ->met ers) :
Druga z powyższych instrukcji wyrażenia
wysyła
na wyjście
wartość
obiektu ht w metrach za
pomocą
ht ->meters.
Funkcji getO i set () nie musimy definiować wewnątrz klasy. Definicje ich można umieścić poza definicją klasy w pliku o rozszerzeniu .cpp . Na przykład definicja właściwości met er s w klasie Height mogłaby wyglądać następująco:
value class He ight
{
II Kod jak
wcześniej. ..
publ i c: II Kodjak
wcześniej...
422
Visual C++ 2005. Od podstaw II Wzrost w metrach.
property double meters {
double getO : II Defini cja funk cji sett} II Kod j ak
II Zwraca właś ciwości zn ajdowałaby s ię
wartoś ć właściwości.
tutaj...
wcześn ie). ..
};
Funkcja get ( ) dla właściwości meters jest teraz zadeklarowana, ale jej definicja nie znajduje się w obrębie klasy Hei ght, a więc musimy ją dostarczyć ze źródła zewnętrznego . W definicji funkcji get ( ) w pliku Height .cpp musi znaleźć się odpowiedni kwalifikator w postaci nazwy klasy oraz nazwy właściwości . W związku z tym definicja wygląda następująco:
Height : :met ers; .qet O
{
ret urn inchesToMet ers*(feet*lnchesPerFoot+inches ) :
}
Kwalifikator Hei ght informuje, że funkcja ta należy do klasy Heig ht , a kwalifikator meters, że należy ona do właściwości met ers w tej klasie. Właściwości można oczywiście definiować także przykład
takiej
dla klas referencyjnych.
Poniżej
znajduje się
właściwości :
ref class Wei ght
{
pri vate:
int l bs:
int oz:
pub l te :
propert y int pounds
{
int get O { ret urn l bs: } vord set( int va lue) { lbs = va lue:
}
property i nt ounces
{
i nt get O { return oz; }
void set (int va l ue) ( oz = va l ue:
):
W powyższym kodzie właściwości pound s i ounces umożliwiają dostęp do pól prywatny ch ei ght możemy nadać wartości , a następnie uzyskać do nich l bs i oz. Właściwo ściom obiektu W dostęp w następujący sposób :
W eight wt = gcnew W eight :
wt->pounds = 162:
wt ->ounces = 12:
Console: :Writ eLi ne(L"W eight t s {O} lbs (l) oz.". wt ->pounds. wt->ounces ):
A
Zmienna uzyskująca dostęp do obiektów typu re f cl ass zawsze jest uchwytem , a więc aby uzy skać dostęp do właściwości obiektu typu referencyjnego, należy używać operatora o>.
Rozdział 7••
Definiowanie wlilsnJch tJPÓW danJch
423
Uproszczone właściwości skalarne Można zdefi niować właściwość ska larną
klasy bez podawania definicj i funkcji get ( ) i set ( ) W celu zdefi niowa nia takiej właściwośc i nal eży opuśc ić klamry zaw ierające definicje funkcji get ( ) i set r ) oraz deklaracj ę właściwości zakończyć średnikiem . P on i ż ej znaj duje się przy kład o w a klasa ska larna z uproszczonymi właściwościam i skalarnymi:
nazywa si ę to
u p ro sz c zo n ą właściwością ska larną.
value class Point (
pub l ic: property int x: property int y:
II II
Właśc i woś ć
Właśc iwość
uproszczona. uproszczona.
vlrtual Stri ng ToSt ring() override
A
{
return L"("
+ X+
L". "
+
y
+
L")":
II Zwraca "(x,y)".
}
): Do myś l ne definicje funkcji get( ) i set ( ) s ą dostarczane automatyczni e dla ka żd ej uprosz czonej właściwośc i skalarnej. Zwraca ona wa rtość właściwośc i oraz ustawia j ej warto ść na argument typu okreś lo nego dla właści wości. Przestrzeń prywatna została zaalokowana w celu umieszczenia wartości właści wośc i w miejs cu n i ed o s tępnym z zew nątrz.
Spójrzmy na kilka właści wośc i skalarnych w akcji .
~ Uzywanie właściwości skalarnych W kodzie tym użyte
zostały
trzy klasy - dwie klasy w artośc i oraz jedna klasa referencyjna.
II Cw7_ 16.cpp: main projectfile.
II Używanie właśc iwości skalarnych.
#i ncl ude "st dafx.h" usi ng namespace Syst em: II Klasa
definiująca
wzrost oso by.
va l ue class Height (
pri vate: II Zapisuje wzrost w stopac h i calach.
int feet :
int inches :
l i t eral int inches PerFoot = 12:
l it eral doub le inchesToMet ers = 2.54/100 :
publi c: II Tworzy wzrost z
wartości
zmi ennej inches.
Height (int ins) {
feet = ins / inchesPerFoot :
inches = i ns %inchesPerFoot ;
424
Visual C++ 2005. Od podstaw
II Tworzy wzrost z
wartości
zmiennych feet i inches.
He ight( l nt ft . int ins) : feet(ft). lnche s(in s){} II Wzrost w metrach.
property dou ble mete rs
II
Właściwość
skalarna.
{
II Zwraca
wartość właściwości.
doub le get()
{
ret urn inchesToMeters*( feet* inchesPer Foot+i nches ),
}
II Definicja funkcji sett)
II Tworzenie
łańcuchowej
właściwości znajdowałaby się
tutaj...
reprezentacji obiektu .
virtua l String ToString () override
A
{
return feet + L" stóp "+ inches + L" cale":
}
}: II Klasa
definiująca wagę
osoby.
va lue cl ass Weight {
private : int lbs:
int oz:
lit eral l nt ouncesPerPound = 16:
literal dou ble lbsToKg = 1 0/2.2:
publlC: Wel ght(i nt pounds , i nt ounces) {
lbs = pounds.
oz ~ ounces :
property int pound s
II
Właściwość
skalarna.
II
Właściwość
skalarna.
II
Właściwość
skalarna.
(
i nt get( ) { return lbs: }
voi d set (int value) { l bs = value;
property i nt ounces (
i nt get () ( ret urn oz; }
vOld set (i nt value) { oz = value;
property doubl e kilograms {
double get() { return l bsToKg*C1 bs + oz/ounces PerPoundJ;
Rozdzial7.• Definiowanie własnych typÓW danych vi rtual Str i ng A ToString() override
{ ret urn lbs + L" funtów " + oz + L" uncjl": l
};
II Klasa defin iująca
osobę.
ref class Person (
pri vate:
He ight ht :
We ight wt :
publ i c:
property St ri ngA Name :
II Uproszczo na
wlasc iwosć
ska larna.
eight w) : ht th ) . wt (w)
Person(St ri ng A name . Hei ght h. W (
Name = name: Height getHeight() { ret urn ht :
W ei ght getWeight(){ ret urn wt ;
};
i nt main(array<Syste m: :St ri ng A> Aargs) (
Weight hl sWeight = Weight (185 . 7);
He ight hi sHeight = Height (6. 3) :
PersonA him = gcnew Person(L"F red" , hisHeight . his Wei ght):
W eight herW eight ~ Weight( 105, 3):
Height herHeight = Height(5 . 2) ;
PersonA her ~ gcnew Person(L"Freda ", herHeight , herW eight):
Console; :Wri t el i neCL"To Jest (O}", her->Name): Console : :WriteLi ne(l "Ona waży {O :F2 } kil ogramów. ", her ->get Weight() .ki lograms ): Console : :WriteLi ne(l "Ma wzrostu {O}. czyli {l:F2} metrów." . her->get Height() ,her->getHe ight( ) .met ers) : Console : :WriteLi ne(L"To jest (Ol ", him->Name) ;
Console: :Wri t eL i ne(l"On waży (O} ," , him->get W eight () :
Console: :WriteLine(L"Ma wzrostu {O} . czyl i {l :F2} met rów." ,
him- >getHeight ( ),him- >getHeight () .meters): ret urn O:
Wynik dział ania tego programu jest następujący: To jest Freda
Ona waży 47,73 kilogramów .
Ma wzrostu 5 stóp 2 cale , czyli 1,57 met rów .
To j est Fred
On waży 185 funt ów 7 uncJ i .
Ma wzrostu 6 st óp 3 cale , czyli 1.91 met rów .
425
426
Visual C++ 2005. Od podstaw
Jak to IJziala Dwie klasy wartości Hei ght i Wei ght d efin iują wzrost i wagę osoby. Klas a Person ma dwa pola typu Height i Weight, które przechowują wzrost i wagę osoby . Imi ę osoby przechowy wane j est w uproszczonej właściwości Name, ni eposiadającej jawnej definicj i funkcji get() i set ( ). W związku z tym właściwość ta posi ada domyślne funkcje get ( ) i set ( ). Dwie pierwsze instrukcje w funkcji mai n() definiują obiekty Hei ght i Widt h, za rych następnie defin iujemy mężczyznę - hi m:
pomo cą
któ
Wei ght hi sWei ght = WeightC185 . 7) :
He i ght hi sHei ght = Hei ght C6. 3);
Person" hi m = gcnew Per sonCL"Fred". hi sHei ght . hi sWei ght ) ,
Hei ght i Wei ght są klasami wartości , a więc zmienne tych typów przechowują w arto ści bez p ośrednio . Perso n jest kl asą referencyjną, a więc h i mjest uchwytem. Pierwszy argument prze kazywany do konstruktora klasy Person jest literałem łańcuchowym, a więc komp ilator tworzy z niego obiekt klasy Str i nq, który następnie przek azuje jako argument. Drugi i trzec i argu
ment to obiekty klasy wartości i tworzymy je w dwóch pierwszych instrukcjach. O czywiście ich kopie są przesyłane jako argumenty ze względu na mechanizm przekazywania przez wartoś ć dla argumentów funkcji . Wewn ątrz konstruktora klasy Perso n przypi sanie ustaw ia w artość parametru Name, zaś wart o ści dwóch pól ht i wt są ustawiane za pomocą listy inicj alizacyjnej. Jedynym sposobem ustawienia wła ściwo ści jest niejawne wywołanie jej funkcji set ( ) . Wła ś c i w oś ć nie może b yć inicj alizowan a w li ście inicjal izuj ąc ej konstruktora. Podobne trzy instrukcje jak dla obiektu him zostały napisane dla obiektu her. Spośród dwóch obiektów klasy Per son, znajduj ących się na stercie, najpierw wysyłamy na ekran informacje o niej Cher ) za pomocą poniż szych instrukcji: Console :Wr iteLi neCL"To je st ( O)" . her ->Name);
Console : :Wr iteLi neCL "Ona wa ży {O :F2} ki log r amów .".
her ->get Wel ght C) .ki l ogr ams) ;
Console :Wr iteLi neCL"Ma wzrostu (O} . czy l i { l:F2} metrów. " .
her ->get Hei ght C) .her ->get Hei ght C) .met er s) :
W pierwszej instrukcji uzyskujemy dostęp do właściwości Name obiektu wskazywanego przez uchwyt her za pomocą wyrażeni a her - >Name. W rezultacie otrzymujemy uchwyt do łańcucha zwróconego przez funkcję właściw o ści get( ) , a więc do łańcu ch a typu St ri nq". W drugiej instrukcji uzyskujemy d o stęp do właściwości ki l ogr ams pola wt obiektu wska zywanego prz ez her za pomocą wyrażenia her ->getWei ght ( ) . k i l ogr ams. C zę ść wyrażenia her ->getWei ght zwraca kopię pola wt i s łuży do uzyskania dostępu do właściwoś ci ki l ogr ams. W ten sposób wartość zwrócona przez funkcję get () dla właściwo ś ci kil ogr ams staje się war tością drugiego argumentu funkcji Wr i t el i neO . W trzeciej instrukcji wyjściowej drugi argument stanowi wynik wyrażenia her ->get Wei ght O, które zwraca kopię pola hL Aby odpowiednio dostosować dane wyjściowe, kompilator wywo łuje funkcję ToSt ri ng () dla obiektu, dzięki czemu wyrażenie to jest równoznaczne z wyraże niem her- >get Wei ght ( ) .ToSt ri ng( ), wi ęc - jeśli chcemy - to możemy to sami zapisać w ten sposób. Trzecim argumentem funkcji Wr i t el i ne( ) jest właściwo ść meters obiektu Hei ght, która jest zwracana przez funkcję get Hei ght ( ) zastosowaną do obiektu her klasy Per son.
Rozdział 7.•
Deliniowanie własn~ch
I~pów dan~ch
427
Pozostałe trzy instrukcje wyjściowe wysyłają na wyjście informacje o obiekcie him w podobny sposób jak przy her. W tym przypadku waga osoby została utworzona za pomocą niejawnego wywołania funkcji ToSt r i ng( ) dla pola wt obiektu hi m.
Definiowanie właściwości indeksowanych Właściwości
indeksowane to zbiór wartości właściwości w klasie, do których dostęp uzyskuje za pomocą indeksów podawanych w nawiasach kwadratowych, podobnie jak w przypadku elementów tablicy. Do tej pory używaliśmy właściwości indeksowanych łańcuchów, ponie waż klasa Stri ng udostępnia znaki łańcucha w postaci właściwości indeksowanych . Jak już się orientujemy, jeżeli st r jest uchwytem do obiektu klasy Str i ng, to wyrażenie str [4] daje dostęp do wartości piątej właściwości indeksowanej, co odpowiada piątemu znakowi w łańcuchu . Właściwość, do której dostęp uzyskuje się poprzez podanie indeksu w nawiasach kwadra towych po nazwie zmiennej odnoszącej się do obiektu, nazywa się właściwością indeksowaną domyślną. Właściwość indeksowana posiadająca nazwę zwana jest nazwaną właściwością się
indeksowaną,
Poniżej
znajduje
się
klasa
zawierająca domyślną właściwość indeksowaną:
ref class Name (
pri vat e: II Przechowuje imiona jako elem enty tablicy. array<Stri nq" >" Names ; public: Name( .ar ray- St.r i nq">" nenes ) Names(names) {} II
Wiasciwos ć
indeksowana zwracająca dowolne
imię.
property Str i ng default [i nt] A
( II Odzyskuj e
wartość
wlasciw osci indekso wanej.
Stri ng get (i nt i ndex) A
(
i f (i ndex >~ Names->Lengt h) t hrow gcnew Excepti on (L"Za ret urn Names[i ndex] ;
duż y
l ndeks");
l: Przeznaczeniem klasy Name jest przechowywanie imion osób w postaci tablicy. Konstruktor przyjmuje arbitralną liczbę argumentów typu St r tnq", które następnie zapisywane są w polu Names, dzięki czemu obiekt Name może zawierać dowolną liczbę imion . Właściwość
indeksowana w tym przypadku to domyślna właściwość indeksowana, ponieważ jest określone za pomocą słowa kluczowego default. Gdybyśmy w tym miejscu podali wprost imię, to byłaby to właściwość indeksowana nazwana. Nawiasy kwadratowe znajdujące się po słowie kluczowym def ault wskazują, że rzeczywiście jest to domyślna właściwość indeksowana, a znajdująca się pomiędzy nimi nazwa typu - w naszym przypadku i nt określa typ wartości indeksów , którego należy użyć podczas poszukiwania wartości właści wości. Typ indeksu nie musi być liczbowy, a w celu uzyskania dostępu do wartości właściwo ści indeksowanych można mieć więcej niżjeden parametr indeksowy. imię
428
Visual C++ 2005. Od podstaw Jeżeli
do właściwości indeksowanej dostęp uzyskuje się za pomocą pojedynczego indeksu, funkcja get ( ) musi mieć parametr określający ten indeks, który jest takiego samego typu co typ podany w nawiasach kwadratowych po nazwie właściwości. Funkcja set() w takim przy padku wymaga dwóch parametrów: pierwszym jest indeks, a drugim nowa wartość, na którą ma zostać ustawiona właściwość o indeksie podanym w pierwszym parametrze. Przyjrzyjmy
się właściwościom
indeksowanym w praktyce.
~ Uzywanie domyślnei właściwości indeksowanei W poniższym kodzie
wykorzystałem
i nieco
rozszerzyłem klasę
Name :
#lncl ude "stdafx.h" using namespace System : ref class Name (
privat e:
array<StringA>A Names:
publ i c:
Name ( . . .array<Strin gA>A names) II
Właś ciwość
skalarna
Names (names ) {}
określająca liczbę
imion.
property int NameCount
{
int get () {ret urn Names->Length : }
}
II
Właściwość
indeksowana zwracająca imiona.
property Str ingA default[ int J (
St ri ngA get(int i ndex)
(
if(i ndex >= Names->Length )
throw gcnew Exception (L "Za return Names[indexJ:
d u ży
indeks "),
}
}:
int ma ln(arr ay<System: :Stri ng A> Aargs) [
Name A myName
~
gcnew Name(L"Ebenezer". L"lsaiah". L"Ezra".
l.'Tru ęo" .
L"Whelkwh i stle"). II Tworzenie listy imion.
for(int i = O : i < myName->NameCount ; i++ ) C ons ol e: :Writ eLin e(L"lmię numer {O } to {l )". i+l, myName[i]); ret urn O;
Rozdział 7.
Rezultat
działania powyższego
numer numer numer numer numer
Imi ę Imi ę I mi ę I mi ę
I mi ę
l 2 3 4 5
to to to to to
• Deliniowanie własnych typÓW danych
429
programu jest następujący :
Ebenezer
Isai ah
Ezra
Inigo
W hel kwh i st le
Jak lo działa Klasa Name w tym przykładzie zasadniczo niczym się nie różni od swojej poprzedniej wer sji. Jedyną różnicąjest dodana właściwość skalarna o nazwie NameCount , która zwraca liczbę imion w obiekcie Name. W funkcji mai n( ) najpierw tworzymy obiekt Name zawierający pięć imion: Name myName
Lista parametrów konstruktora klasy name zaczyna się od elipsy, a więc konstruktor przyj muje dowolną liczbę argumentów. Argumenty podane podczas wywoływania konstruktora przechowywane będą w tablicy names. W związku z tym inicjalizacja pola Names elementami tablicy names powoduje, że pole Names odnosi się do tablicy names. W poprzedniej instrukcji do konstruktora przekazaliśmy pięć argumentów, a więc pole Names obiektu, który wskazuje uchwyt myName, jest tablicą pięciu elementów. Dostęp listę
do właściwości obiektu myName uzyskujemy w imion zawartych w obiekcie:
pętli
f or , za
pomocą
której tworzymy
for (i nt i ~ O ; l < myName->NameCount ; i++ )
Console : :WriteLi ne(L"Name {O} t s ( l }" . i +1. myName[i]) :
Pętlę
pomocą wartości właściwości NameCount . Bez niej nie wiedzieli ile imion powinno zostać wyświetlonych. Ostatni argument funkcji Wr iteLi ne() we wnątrz pętli uzyskuje dostęp do właściwości indeksowanej o indeksie i . Jak widać , uzyskanie dostępu do domyślnej właściwości indeksowanej wymaga jedynie podania indeksu w nawia sach kwadratowych po nazwie zmiennej myName. Z danych na wyjściu wynika, że właściwości indeksowane działają należycie .
for kontrolujemy za
byśmy,
indeksowana jest tylko do odczytu, ponieważ klasa Name zawiera tylko get () dla tej właściwości . Aby umożliwić zmianę właściwości, możemy dodać defi funkcji set () dla domyślnej właściwości indeksowanej, jak poniżej:
Nasza
właściwość
funkcję nicję
ref class Name
{
II Kodjak II
wcześniej. ..
Właściwość
indeksowana
zwracają ca
imiona.
property String default [ int ] A
(
Stri ng get (int index) A
(
if(index >~ Names->Length) throw gcnew Exception(L" Za retur n Names[i ndex] ;
du ży
indek s") ;
430
Visual C++ 2005. Od podstaw void set(i nt index. St r ing name ) A
(
if( i ndex >~ Names- >Length)
th row gcnew Except ion(L"Za Names[index] = name ;
d u ży
indeks") :
}: Mając możliwość ustawić wartość
ustawiania warto ści właściwości indeksowanych, w funkcji mai n( ) ostatniej właś ciwości indeksowanej:
możemy
NameA my Name = gcnew Name(L"Ebenezer ". L"Isaiah" . L"Ezra".
L"Ini go". L" Whel kwhi st l e") ;
myName[myName->NameCount - 1] = L"Oberwur st" : II Zmian a ostatniej właś ciwości indekso wanej.
II Tworzenie listy imion.
for (i nt i ~ O ; i < myName->NameCount . i++)
Console : :WriteLi ne ( L"Imi ę nume r {O} to (l)" . i+l. myName[i ]) ;
Dodając
ten fragment kodu w danych wyjściowych nowej wersji programu, zobaczymy, że zaktualizowane przez ostatnią instrukcję przypisującą war właściwości w indeksie lTIyl~ a me ->NameCount -1.
ostatnie tość
imię rzeczywiście zostało
Możemy także dodać
do klasy
nazw aną właściwość indeksowaną:
ref class Name ( II Kod j ak poprzednio... II
Właściwość
indekso wana
zwracająca inicjały.
property wcha r_t Init i als[ i nt] (
wcha r_t get (lnt index) (
l f (index > ~ Names->Lengt h)
throw gcnew Exception(L"Za ret urn Names[i ndex][O]:
d u ży
indeks");
}; Właściwość
indeksowana ma nazwę Ini tial s, ponieważ zwraca ona pi erwszą literę imienia przekazanego za pomocą indeksu. Nazwaną właściwość indeksowaną przekazuje się w podob ny sposób jak właściwość indeksowanądomyślną, ale zamiast słowa kluczowego def ault stawiamy nazwę właściwości. Ponowne skompilowanie i uruchomienie programu da I mi ę
I mi ę I mi ę Imi ę
I m ię
numer numer numer numer numer
1 2 3 4 5
to to to to to
Ebenezer Isaiah Ezra Inigo Oberwurst
Irucja ł y : E. 1. E. 1. O.
następujący
rezultat:
Rozdział 1.
• Definiowanie własnych typÓW danych
431
Inicjały z o s tały wy świetlone dz ięki
w
pętli
uzyskaniu dostępu do nazwanej właściwo śc i indeksowanej for. Z danych na ekranie wynika, że wszystko d zi ała jak n ależy.
Bardziei zlożone wlaściwości indeksowane Jak ju ż
w spomin ałem , właściwo ści
indeksowane można zdefiniow a ć w taki sposób, że aby trzeba poda ć więcej ni ż jeden indeks oraz indeksy te muszą być liczbami. Poni żej znajduje się przykładowa klasa zawi erająca takie właściwosci: uzy skać dostęp
enumclass II Klasa
do ich
wartości ,
Day { Pon i edz i a ł e k .
defin iują ca
wtorek.
Ś rod a .
Czwa rte k.
P i ąte k .
Sobot a. Niedziela} :
sklep.
ref class Shop
{
publ ic:
property Str i ng A Dpening[Day . St ringAJ {
II Godziny otwarcia sklepu.
St ri ng A get (Day day. St ringA AmOrPm)
{
switch(day)
(
case Day: :Sobota: lf (AmOrPm == l "rano") ret urn l" 9:00": el se ret urn l "14 :30 ": break: case Day: ' Nl edzie la : ret urn l "zamkniet e": break: defau lt : i HAmOrPm = = l" rano ") ret urn l "9:30": else ret urn l "14:00" : break:
II Godziny otwarcia w sobotę : II rano j est od 9:00. II popoludnie zaczyna s ię o 2:30. II Godziny otwarcia w n iedzielę : II zamkn ięte cały dzień .
II Godziny otwarcia II od pon iedziałku do piątku : II rano j est od 9:30. II popołudnie zaczyn a s ię 2.00.
};
W klas ie repre zentującej sklep znajduje się właściwość indeksowana określ ająca god ziny otwarcia sklepu. Pierwszy indeks jest wartością wyliczeniową typu Day identyfikującą dzień tygodnia, a drugi jest uchwytem do łańcucha określającego , czy jest rano, czy wieczór. War tość właściwoś ci Openi ng obiektu Shop możemy wysłać na wyjście w następujący sposób :
ShopA shop ~ gcnew Shop :
Consol e: :Writ el i net shopc-Cpent ng[Day: :Sobota . l "po
po ł u d n i
u"J) :
Pierwsza z tych instrukcji tworzy obiekt o nazwie Shop, a druga wyświetla godziny otwarcia sklepu w sobotę po połudn iu . Jak widać , obie wartości indeksowe stawia się w nawiasach kwa dratowych i rozdziela przecinkiem. Rezultatem pierw szej instrukcji jest łańcuch " 14:30".
432
Visual C++ 2005. Od podstaw Jeżeli
potrafisz zn aleźć dla nich zastosowanie, możesz w klasie zd efiniow ać indeksowan e z trzema indeksami lub nawet więk s z ą ich liczbą.
wo ści
także właści
Właściwości statyczne Właściwości
statyczne są podobne do statycznych składowy ch klasy , ponieważ również deje dla klasy i są takie same dla wszystkich obiekt ów tej klasy. Aby zdefiniowaś, właściwoś ć jako staty cz ną, należy do j ej definicji dodać słowo kluc zowe st at i c. Poniżej znajduje się przykładowa definicja właściwości statycznej w klasie Length , którą widzieliśmy się
finiuje
ju ż wcześniej :
val ue cl ass Lengt h { II Kod jak poprzednio...
publ ic: stat i c property
S t r i ng
Ą
Units
( St ri n g
Ą
get () { ret urn l"met ry i cent ymet ry": }
}
}:
Jest to prosta właściwość statyczna, która udostępnia jednostki przyjmowane przez klasę jako łańcuchy . Do stęp do właściwości statycznej uzyskujemy, dod ając do jej nazwy kwalifikator w postaci nazwy klasy, podobnie jak do każdej innej statycznej składowej klasy : Consol e : :Wr iteline(L" Jednostk i w kl asi e: (O}. " . l engt h: .Uni t s ) :
Statyczne właściwośc i klasy i stnieją bez względu na fakt, czy z o s tały utworzone jakie ś jej obiekty, czy nie. Odróżnia je to od właściwości egzemplarzy, które są specyficzne dla każdego obiektu typu klasowego. Oczywiście, mając zdefiniowany obiekt klasy, dostęp do właści wo ści statycznej można uzyskać przy u życiu nazwy zmiennej. Mając na przykład obiekt klasy Length o nazwie l en, wartość właściwości statycznej Uni ts możemy wysłać na wyjście za pomocą następującej instrukcji : Console: :Wr i t el i ne(L"Jednostki klasy (O}.". l en .Um t s ) :
W celu uzyskania dostępu do właściwości statycznej w klasie referencyjnej poprzez uchwyt do obiektu tego typu należałoby użyć operatora - > .
Zarezerwowane nazwy właściwości Mimo że właściwo ści nie są tym samym co pola, ich wartości muszą jednak być gdzieś przechowywane, a zatem musi być jaki ś sposób określania lokalizacji tych danych. Wewnątrz właściwości tworzone są nazwy dla potrzebnych lokalizacji przechowujących dane . Nazwy te są zarezerwowane w klasie zawierającej te właśc iwo ści , a więc nie można ich używać do innych celów . Jeżeli
w klasie zdefin iujemy
będzie używa ć
właściwo ść skalarną lub indeksowaną o
nazwie NAM E, to nazwy zarezerwowane w tej klasie , w związku z czym nie można ich do innych celów. Obie nazwy są zarezerwowan e bez względu na to, czy obie
get_NAME oraz set _NAME
będą
Rozdział 7. •
Definiowanie własnych typÓW danych
433
funkcje właściwości zostały zdefiniowane. Po zdefiniowaniu domyślnej właściwości indeksowej w klasie zarezerwowane zostają nazwy get_ It emoraz set _Item. Prawdopodobieństwo występowania zarezerwowanych nazw zawierających znak podkreślenia jest ważnym powodem do unikania tego znaku we własnych nazwach w programach C++/CLI.
-
Pola inilonly
Pola literałowe są wygodnym sposobem wprowadzania stałych do klasy , ale ograniczone są faktem, że ich wartość musi być znana w momencie kompilacji programu. W klasach w C++/ CLI dostępne są pola i niton1y, które są zmiennymi inicjalizowanymi w konstruktorze. Poniżej znajduje się przykładowe pole i niton1y w szkieletowej wersji klasy Length: va l ue class Length (
pri vat e: int feet : int inches : publ te : i nit only i nt inchesPerFoot :
II Pole initonly .
II Konstruktor. Length(i nt ft .
i nt ins ) : feet(ft) . i nchest t ns ) . inchesPerFoot (12)
II Inicj a/iza cj a p ól. II Ini cj a/iza cja p ola initonly.
{} }:
W tym kodzie pole i nit on1y nosi nazwę i nchesPerFoot i jest inicjalizowane w liście inicjalizującej konstruktora . Mamy tutaj przykład niestatycznego pola i niton1 y. Każdy obiekt będzie miał własną kopię, podobnie jak w przypadku zwykłych pól, feet oraz i nches. Oczywiście największą różnicą pomiędzy polami typu i nit on l y i zwykłymi polami jest to, że nie można zmienić wartości pola i nit on1y - jest ona stała od momentu jej inicjalizacji . Należy zwrócić uwagę, że nie można podać wartości początkowej dla niestatycznego pola i niton l y podczas jego deklaracji. Oznacza to, że wszystkie tego typu pola muszą być inicjalizowane w konstruktorze. Niestatycznych pól i ni t on1y nie musimy można to zrobić w jego ciele: Length(i nt ft. i nt i ns) feet ( ft), i nches(i ns ) .
inicjalizować
w
liście
inicjalizacyjnej konstruktora -
II Inicj a/izacja pól.
{
inchesPerFoot
=
12:
II Inicja/izacja pola initonly.
}
W powyższym fragmencie kodu pole zostało zainicjalizowane w ciele konstruktora. Normalnie niestatycznego pola i niton1 y przekazalibyśmy do konstruktora jako argument zamiast w postaci literału, jak zrobil i śmy tutaj, ponieważ najważniejszą cechą takich pól jest to, że są one specyficzne dla danego egzemplarza. Jeżeli podczas pisania kodu znana jest wartość, to równie dobrze można użyć pola literałowego .
wartość
434
Wisual C++ 2005. Od polista w Pole i ni t onl y w klasie można również zdefiniować jako statyczne, w którym to przypadku jest dostępne dla wszystkich składowych klasy i jeżeli jest ono zarazem publiczne, to dostęp do niego można uzyskać, dodając do jego nazwy kwalifikator w postaci nazwy klasy. Pole i nchesPerFoot byłoby o wiele bardziej przydatne, gdyby było statycznym polem i nitonly - jego wartość na pewno nie powinna zmieniać się w różnych obiektach. Poniżej znajduje Się nowa wersja klasy Length z użyciem statycznego pola ini to nly:
va l ue class Length {
privat e: int feet : i nt inches: publte : i ni t only stati c int i nchesPerFoot
=
12:
II Staty czne p ole initonly .
II Konstruktor.
Length(i nt ft . i nt i ns ) : feet (ft) . inches(i ns )
II Inicjaliza cja pól.
{}
}:
Teraz pole i nchesPerFoot jest statyczne, a jego wartość została określona w deklaracji, a nic w liście inicjalizacyjnej konstruktora. Należy pamiętać , że w konstruktorze nie można ustawiać wartości pól statycznych. Zastanawiając się nad tym trochę głębiej , dojdziemy do wniosku, że jest to całkiem logiczne, ponieważ pola statyczne są dostępne dla wszystkich obiektów klasy i w związku z tym ustawianie wartości takiego pola za każdym razem, gdy wywoływany jest konstruktor, nie byłoby na miejscu. Wygląda
na to, że mamy z powrotem pola i nitonly, które można inicjalizować tylko podczas procesu kompilacji, chociaż równie dobrze tę czynność mogłyby wykonać pola literałowe. Istnieje jednak jeszcze jeden sposób inicjalizowania statycznych pól i nitonl y w trakcie działania programu - poprzez konstruktor statyczny.
Konstruktor statyczny Konstruktor statyczny deklaruje się za pomocą słowa kluczowego stat ic. Jego przeznaczeniemjest inicjalizacja statycznych pól i statycznych pól initonly. Konstruktor statyczny nie ma żadnych parametrów i nie może mieć listy inicjalizacyjnej . Konstruktor statyczny jest zawsze prywatny, bez względu na to, czy znajduje się w publicznej, czy prywatnej części klasy. Konstruktor statyczny można definiować dla klas wartości i klas referencyjnych. Konstruktora statycznego nie można wywołać bezpośrednio - jest on wywoływany automatycznie przed wywołaniem zwykłego konstruktora. Wszystkie pola mające wartości początkowe określone w ich definicjach są inicjalizowane przed wykonaniem konstruktora statycznego. Poniżej znajduje się przykładowa inicjalizacja pola i nit onl y w klasie Length za pomocą konstruktora statycznego:
value class Length {
privat e : int feet : i nt inches :
Rozdział 7.•
Deliniowanie własnych typÓW danych
435
II Konstruktor statyczny.
st at ic Lengt h() ( inchesPerFoot
=
12:
publ i c:
init only sta t ic int inchesPerFoot : II Konstruktor.
...
II Statyczne p ole initonly.
Length(int f t , int i ns ) feet r f't) . i nchest tn s)
II Jnicjalizacja p ól.
{}
Użycie konstruktora statycznego w tym przykładzie nie jest w niczym lepsze od jawnej inicjalizacji pola i nc hesPe rFoot , ale należy pamiętać o znaczącej różn icy - teraz inicjalizacja odbywa się w trakcie działania programu, dzięki czemu wartość może zostać wprowadzona ze źródła zewnętrznego .
na temat kla s w języku C++ . Do końca książki o nich coraz więcej . Poniżej znajduje się lista najważniejszych poruszanych w tym rozdziale:
b ędziemy dow iadywać się zagadnień
•
Klasa umożl iwia definiowanie własnego typu danych. Może ona odzwierciedl ać dowolny typ obiektów, których wymaga ro związanie danego problemu .
•
Klasa może zawierać zmienne składowe i funkcje składowe. Funkcje składowe klasy mają zawsze wolny dostęp do zmiennych składowych tej samej klasy. Zmienne składo we klasy w CH/CLI nazywają się polami.
•
Obiekty klasy tworzy s ię i inicjalizuje za pomocą funkcji zwanych konstruktorami. Wywoływane są one automatycznie w momencie napotkania deklaracji obiektu. Konstruktory można przeładowywać w celu uzyskania obiektów inicjalizowanych na różne sposoby.
•
Klasy w programach w C++/CLI mogą być klasami lub klasami referencyjnymi (ang. refclasses).
•
Zmienne typu klasy wartości przechowują dane bezpośrednio , natomiast zmienne odnoszące się do obiektów należących do klasy referencyjnej są zawsze uchwytami.
•
Dla klasy w C++/CLI można statyczne składowe klasy.
•
Składowe klasy można określić jako publiczne (p ubl i c) i wtedy są do stępne dla wszystkich funkcji w programie. Można także określić je jako prywatne (pr i vate) i wtedy dostęp do nich mają tylko funkcje składowe lub zaprzyjaźnione klasy .
•
klasy można zdefiniować jako statyczne. Istnieje tylko jeden egzemplarz statycznej, który jest współdzielony przez wszystkie egzemplarze klasy, bez względu na liczbę utworzonych jej obiektów.
Składowe
każdej składowej
zdefiniować
wartości
(ang . value classes)
konstruktor statyczny
inicjalizujący
436
Visual C++ 2005. Od podslaw •
Każdy
•
W niestatycznych funkcjach składowych typu klasy wartości wskaźnik th i s jest wskaźnikiem wewnętrznym, natomi~st w klasie referencyjnej jest on uchwytem.
•
Funkcja składowa zadeklarowana jako const posiada wskaźnik const t his, a zatem nie może modyfikować składowych obiektu klasy, dla którego została wywołana . Funkcja ta może wywoływać tylko te funkcje składowe, które zostały zadeklarowane jako const .
•
Dla obiektu klasy zadeklarowanego jako const składowe zadeklarowane jako const.
•
Funkcji składowych klas jako const .
•
Stosowanie jako argumentów funkcji referencji do obiektów pozwala na zaoszczędzenie czasu przy przekazywaniu do funkcji złożonych obiektów.
•
Parametr konstruktora kopiującego, który jest konstruktorem obiektu zainicjalizowanego istniejącym obiektem tej samej klasy, musi być określony jako referencja typu const .
niestatyczny obiekt klasy zawiera dla którego została wywołana funkcja.
wartości
wskaźnik
th i s,
wskazujący bieżący
można wywoływać
oraz klas referencyjnych nie
obiekt,
tylko funkcje
można deklarować
• ' W klasie wartości nie można zdefiniować konstruktora kopiującego, ponieważ kopiowanie obiektów klasy wartości zawsze odbywa się metodą pole po polu.
Ćwiczenia Kod źródłowy wszystkich listingów w tej ze strony www.helion.pl.
l
książce
oraz rozwiązania do ćwiczeń
można pobra ć
o nazwie Sampl e zaw ierającą dwie jednostki danych typu Napisz program deklarujący dwa obiekty typu Sampl e o nazwach a i b. Ustaw wartości dan ych należących do obiektu a, a następnie sprawdź, czy można je przekopiować do b za pomocą prostego przypisania.
Zdefiniuj
s t ruktu rę
całkow itego.
2. Do struktury Sampl e z poprzedniego
ćwiczenia dodaj składową typu char* o nazwie s Ptr. Po wprowadzeniu danych do a utwórz dynamicznie bufor łańcuchowy zainicjalizowany łańcuchem "Witaj świecie! " oraz ustaw wskaźnik a . sPt r na ten łańcuch . Skopiuj a do b. Co się dzieje, gdy zmieniasz zawartość bufora znakowego wskazywanego przez wskaźnik a . s Pt r, a następnie wysyłasz na ekran zawartość łańcucha wskazywanego przez b. sPt r? Wyjaśnij, co się dzieje. Jak można to obejść?
8. Utwórz
funkcję, którajako argument przyjmuje wskaźnik do obiektu klasy Sample oraz wysyła na ekran wartości składowych przekazanego do niej w ten sposób obiektu klasy Samp l e. Przetestuj tę funkcję , rozszerzając program stworzony w poprzednim ćwiczen iu .
Rozdział 7. •
Definiowanie własnych typÓW danych
437
.. Zdefiniuj klasę o nazwie CRecord z dwiema składowymi prywatnymi przechowującymi imiona o długości do 14 znaków oraz liczbę całkow itą. Zdefin iuj funkcję składową klasy CRecor d o nazwie getRecord( ), która będzie ustawiała wartości składowych , wczytując dane z klawiatury , oraz funkcję składową put kecordt ), aby wywołujący program mógł wykryć, kiedy została psdana liczba o warto ści zerowej . Przete stuj swoją klasę w funkcji mai nt ), która wczytuje i wysyła na wyj ście obiekty klasy CReco rd do momentu podania liczby zerowej.
I. Napisz
klasę o nazwie CTrace, której można u żyć do wy świetlania w trakcie wykonywania programu, kiedy następuje wej śc ie do poszczególnych bloków i wyj ście z nich. Klasa powinna wysyłać na ekran komunikaty podobne do poniższych :
do funkcj i 'fI' do bloku ' if ' wyjśc i e z bloku 'if' wyj śc ie z f unkcj i 'fI ' we jś cie
wejśc i e
8.
Znajdź sposób na automatyczną kontrolę wcinania wierszy w poprzednim ćwiczeniu, tak aby dane na ekranie prezentowały się następująco:
do funkcji 'fl' do bloku ' ; f ' wyj ści e z bloku ' if' wyjśc ie z f unkcji ' fl ' weJ ś c ie
wejście
7. Zdefiniuj klasę reprezentującą stos liczb całkowitych . Stos ten jest zbiorem elementów pozwalającym
na dodawanie i usuwanie elementów tylko z jednej strony oraz dz iała na zasadzie "pierwszy do, ostatni na zewnątrz". Jeżeli na przykład stos zawiera liczby 10,4 , 16,20, funkcja pop() zwróciłaby 10, a stos zawierałby lic zby 4, 16, 20. Uruchomienie pusht IS) dałoby w wyniku stos 13,4, 16,20. Aby dostać się do elementu, który nie jest na samej górze, należy wpierw usunąć wszystkie znajdujące się nad nim elementy. W klasie powinny być zaimplementowane funkcje pop ( ) i push( ) oraz funkcja pr i nt( ) do sprawdzania zawartości stosu. Listę elementów przechowuj wewnętrznie w postaci tablicy. Napisz program sprawdzający, czy klasa działa poprawnie.
.. Co się stanie z Twoim rozwiązaniem z poprzedniego ćwiczenia, gdy spróbujesz za pomocą funkcji pop( ) usunąć ze stosu więcej elementów, niż on zawiera? Potrafisz znaleźć dobre wyjście z tej sytuacji ? Czasami przydałaby się funkcja pozwalająca na dostęp do liczby na samej górze bez jej usuwania. Zaimplementuj do tego celu funkcję peek( ). .. Powtórz ćwiczenie 4., ale jako program konsolowy CLR z referencyjnych.
użyciem
klas
438
Visual C++ 2005. Od podsław
...
8
Więcei na temat klas
W rozdziale tym poszerzymy wiedzę na temat klas. Dowiemy się , jak można sprawić, aby zachowanie obiektów było bliższe zachowaniu typów podstawowych w C++. W rozdziale tym dowiesz się : • Czym
są destruktory
klas oraz kiedy i do czego
są one
potrzebne.
• W jaki sposób implementowany jest destruktor klasy. • Jak w wolnym obszarze przydzielać pamięć składowym klas w natywnym c++ oraz jak je usuwać , kiedy nie sąjuż potrzebne. • Kiedy zachodzi
konieczność
napisania konstruktora
kopiującego
klasy.
• Czym są unie i do czego służą. • Jak
sprawić,
• Czym • Jak
aby obiekty klasy
są szablony
działały
z operatorami C++, takimi jak + czy
*.
klas oraz jak się je definiuje i ich używa .
przeładowywać
operatory w klasach w C++/CLI.
Destruktory klas Mimo że w tytule napisane jest "destruktory klas", podrozdział ten poświęcony jest również dynamicznemu przydzielaniu pamięci. Przydzielając pamięć składowym klasy w obszarze pamięci wolnej, jesteśmy zobowiązani do użycia destruktora, oczywiście w połączeniu z kon struktorem. Poza tym - jak dowiemy się później w tym rozdziale - dynamiczne przydzie lanie pamięci składowym klasy pociąga za sobą konieczność napisania własnego konstruktora kopiującego.
440
Visual C++ 2005. Od podstaw
Czym iesl deslruklor
Destruktor to funkcja ni szcząca obiekt, kied y nie jest już potrzebny lub znajdzie s i ę poza zas ięg iem . Destruktor j est wywoływany automatycznie w chwili, gdy obiekt znajduje s i ę poza zasięgiem . Niszczenie obiektu polega na zwolnieniu pamięci zajmowanej przez jego s kładowe (z wyjąt k ie m składowych statyczn ych , które is t n i ej ą, nawet gdy nie istni ej ą ż a d n e obiekty klasy ). Destruktor danej klasy jest jej funk cją s kła d ową o takiej samej nazw ie jak ona, kt órą charakteryzuje znajdujący się z przodu znak tyldy (-). Destruktor klasy nie zwraca żadnej war tości i nie ma zdefiniowanych parametrów. Prototyp destruktora dla klasy CBox przedstawia si ę nast ępująco:
-CBox () :
II Prototyp destruktora klasy.
Ze względu na to, że destruktor nie ma parametrów, w jednej klasie jeden destruktor. Określenie wartości
może znaj dować si ę
tylko
zwracanej lub podanie parametrów destruktora jest błędem.
Deslruklor domyślny Wszystkie obiekty tworzone przez nas do tej pory były niszczone automatycznie przez destruk tor domyślny klasy. Jest on generowany przez kompilator zawsze wtedy , gdy programista nie zdefin iuje własnego destruktora. Destruktor domyślny nie usuwa obiektów ani składowych obiektów, którym została przydzielona pam ięć w obszarze wolnej pamięc i przez operator new. Jeżeli w konstruktorze skladowym klasy dynamicznie zostało przydz ielone miejs ce w pamięci, to konieczne jest zdefiniowanie własnego destruktora jawnie używającego operatora del ete, zw alniającego pamięć przyd zieloną przez konstruktor za pomocą operatora new, podobnie jak w przypadku zwykłych zmiennych. Przydałoby się trochę praktyki w pisaniu destruktorów, a w i ęc spój rzmy na poniższy listing.
~ Prosty destruktor Aby zdobyć rozeznanie, kiedy wyw oływany jest destruktor klasy, mo żemy go sie CBox. Poniżej znajduje s i ę kod zawierający klasę CBox z destruktorem. II Cw8_01.cpp
II Klasa z j a wnym destrukt orem.
#i ncl ude
uSlng st d: :cout:
us m q st d: :endl :
cla ss CBox
II Defin icj a klasy o zasi ęgu globalny m.
(
publ i c: II Definicja destrukt ora.
- CBox( ) (
caut « "Dest rukt or zast al wywal any." « end l :
umieścić
w kla
Rozdzial8.•
Więcej na lemal klas
441
II Definicja kons truktora .
CBox( double l v = 1.0. doub le wv = 1.0. double hv = 1.0): m_Length(l v) . m_W idt h(wv) , m_Height (hv ) cout « endl
II Funkcja
«
"Konstruktor
zo s tał wywo ł any .
obliczająca pojemność pudełka.
doub le Vo lume () const
{
return m_Lengt h*m_W idt h*rn_He ight:
}
II Funkcja porównująca dwa pudelka, zwracająca wartość true,
IIjeżeli pi erwsze jest większe od drogiego, oraz fa lse w przeciwnym przypadku.
i nt compare(CBox* pBox ) con st (
return thi s->Volume() > pBox->Volume( ): pri va t e :
doub le m_Length: doub le m_Widt h: double m_Height:
II Deklaracja tablicy zawierającej obiekty klasy CBox .
II Dek laracja obiekt u cigar.
II Dek laracja obiektu match.
II Inicjalizacja wskaźnika do adresu obiek tu cigar.
II lni cjalizacja wskaźnika do CBox wartoś cią null.
cout « endl
« «
" P oJ emn o ść p u d e ł ka
pB l ->Vo lume( ):
pB2 = boxes: boxes[2] = match: cout « endl
« «
cigar wynosi ..
II Pojemność wskazywanego obie ktu.
II Ustawienie na adres tablicy .
II Ustawienie trzeci ego elementu na
wartoś ć
obiektu matc h.
boxes[2] wynosi ..
2)->Volume(): II Uzyskiwanie dostępu poprzez wskaźnik.
"P o jemn o ś ć p ud e ł k a
(pB2
+
cout « endl :
return O:
Jak to działa Jedynym zadaniem destruktora klasy CBox jest wyś w i etle n i e komunikatu informuj ąc eg o , zo s tał on wywołany. Wyn ik d zi ałan ia powyżs zeg o programu jest nas tępuj ący :
Konst ruktor Konstrukt or Konstrukt or
z o s ta ł wywo ł any .
zos ta ł wywo łany .
z os ta ł wywo łany .
że
442
Visual C++ 2005. 011 podstaw Konstruktor Konstrukt or Konstru kt or Konstru kt or
z o s ta ł wywo łany.
zo st ał wywo łany.
z os t a ł wywo ł a ny .
zost a ł wywo ł a ny.
P o j em n o ś ć pu d e ł k a Po j em n o ś ć p u de łka
Dest ruktor Dest ruktor Destruktor Dest ruktor Dest rukt or Destruktor Destruktor
cigar wynosi 40
boxes[ 2] wynosi 1.21
zo s t a ł wywo ł any .
zo st a ł wywo ła ny .
zo s ta ł wywo ła ny.
z o s t ał wyw o ł any .
z ost a ł wywo ł any.
z ost a ł wywo ł any.
zo s tał wywo ł any.
Na końcu programu destruktor został wywołany po jednym razie dla każdego istniejącego obiektu. Każdemu wywołaniu konstruktora na początku towarzyszy wywołanie destruktora na końcu . W tym przypadku nie ma potrzeby jawnego wywoływania destruktora. Kiedy obiekt klasy wychodzi poza zasięg , kompilator powoduje automatyczne wywołanie destruktora dla klasy. W naszym programie wywołania destruktora mają miejsce po zakończeniu wyko nywania funkcji matn: l, co sprawia, że istnieje duże prawdopodobieństwo, iż błąd w destrukto rze spowoduje załamanie programu w chwili , gdy funkcja main( l bezpiecznie zakończy już działanie.
Destruktory idynamiczne przydzielanie pamięci Często spotykaną czynnością jest potrzeba dynamicznego przydzielania pamięci składowym klasy. Do przydzielania pamięci składowym obiektu można używać operatora new w konstruk torze. W takim przypadku należy liczyć się z tym, że odpowiedzialność za jej zwalnianie poprzez dostarczenie odpowiedniego destruktora , gdy obiekt nie jest już potrzebny - spoczy wa na nas. Zdefiniujmy najpierw prostą klasę, w której możemy to zrobić.
Przypuśćmy, że
chcemy zdefiniować klasę, w której każdy obiekt stanowi pewnego rodzaju komunikat, na przykład łańcuch tekstowy. Klasa ta powinna maksymalnie efektywnie wyko rzystywać zasoby pamięci , a więc zamiast definiować składową w postaci tablicy elementów typu char, mogącej przechowywać łańcuchy o największ ej wymaganej długości , pamięć dla komunikatu w obszarze wolnej pamięci przydzielać będziemy w momencie utworzenia obiektu. Poniżej znajduje się definicja tej klasy: II Listing 08.0/.
class CMess age {
pri vate: char* pmessage; pub l ic:
II Wska źnik do obiektu zawierającego
II Funk cja wyswietlajqca komunikat.
void Show lt () const
{
cout
«
endl
«
pmessage;
}
II Definicja konstruktora.
CMessage(const char* te xt = "Komu nikat {
do myślny")
łańcuch
tekstowy.
Rozdzial8.•
Więcei na
temat klas
443
pmessage = new char [ st r l en(tex t) + 1J: II Przydz ielenie pamięci dla tekstu . strc py( pmessage, t ext ): II Skopiowanie teksiu do nowej lokal izacj i.
} - CMessage() :
II Prototyp destruktora.
};
zdefiniowana tylko jedna zmienna składowa - pmessage, która jest zarazem do łańcucha tekstowego. Została o na zdefiniowana w prywatnej sekcji klasy , nie ma do niej dostępu z zewnątrz.
W klasie
została
wskaźnikiem
a
więc
W sekcji publicznej klasy znajduje się funkcja składowa Showlt ( l , której zadaniem jest wy świetlanie zawarto ści obiektu klasy CM essage na ekranie . Zdefiniowany został także konstruk tor oraz prototyp destruktora klasy - - CMessage( l , o którym za chwilę . Konstruktor klasy wymaga jako argumentu łańcucha, ale jeżeli żaden nie zostanie przekazany, to używa łańcucha domyślnego określonego w parametrze. Konstruktor sprawdza długość łańcucha podanego jako argument, wyłączając końcowy znak NULL za pomocą bibliotecznej funkcji st r l en( ). Aby konstruktor mógł użyć tej funkcji, potrzebne jest dołączenie pliku nagłówkowego za pomocą dyrektywy #i ncl ude. Konstruktor określa niezbędną do przechowania łańcucha liczbę bajtów wolnej pamięci , dodaj ąc l do wartości zwróconej przez funkcję st r-len t ). Oczywiście, jeżeli przydzielanie pamięci nie p owiedzie s ię, to zostanie zgłoszony wyjątek, ktory spowoduje zamknięcie programu. Aby z takiej sytuacji wyjść w bardziej elegancki sposób, można ten wyjątek przechwycić w bloku konstruktora (zagadnienia zw iązane z obsługą błędów braku pamięci zostały opisane w rozdziale 6.). Mając ju ż przydzieloną pamięć
dla łańcucha za pomoc ą operatora new, używamy zdefiniowanej w pliku nagłówkowym funkcji bibliotecznej st rcpyr i, aby do obszaru przydzielonej mu pamięci skopiować łańcuch przekazany jako argument do konstruktora. Funkcja strcpy( l kopiuje łańcuch określony przez drugi argument wskaźnikowy do adresu za wartego w pierwszym argumencie wskaźnikowym. również
Potrzebujemy teraz destruktora, który zwalniałby pamięć przydzieloną dla komunikatu. Jeżeli go nie dostarczymy, to nie będzie sposobu na zwolnienie pamięci przydzielonej obiektowi. Użycie tej klasy w takiej postaci jak teraz w programie z dużą liczbą tworzonych obiektów klasy CMessage spowodowałoby stopniowe zajęcie c ałej wolnej pamięci i w końcu załamanie pro gramu . Często zdarza s ię to w takich warunkach, w których nie jest to wcale oczywiste. Mo głoby się na przykład wydawać, że przy tworzeniu tymczasowego obiektu klasy CMe s sage w funkcji wielokrotnie wywoływanej w programie obiekty te są niszczone w momenc ie zwra cania przez funkcj ę wartośc i . Tak się dzieje, ale p amięć w obszarze wolnym i tak nie jest zwal niana. A zatem za każdym wywołan iem funkcji coraz więcej pamięci zostaje zajęte przez usu nięte obiekty klasy CMes sage . Kod destruktora klasy CMess age przedstawia
s ię na stępująco:
II Lisiing OB,02.
II Destruktor zwalniają cy pamięć przydzieloną za pomocą ope ralora new.
CMessage: :-CMessage( ) { cout « "Dest rukt or
z os t a ł wywoł a n y ."
II Tylko po lo, aby
ś ledz ić,
co s ię dziej e.
444
Visual C++ 2005. Od podstaw «
endl :
delet e[ ] pmessage:
II Zwolnienie pamięci przydzielonej II wskaźnikowi.
Ze względu na to, że definicja destruktora znajduje się na zewnątrz klasy, konieczne było podanie kwalifikatora w postaci nazwy tej klasy - CMessage. Jedyne, co robi destruktor, to wyświetlanie komunikatu, dzięki któremu wiemy, co się dzieje, a następnie zwolnienie pa mięci wskazywanej przez wskaźnik składowy pmes sage za pomocą operatora del ete. Zauważ, że po operatorze de l ete zostały umieszczone nawiasy kwadratowe, które potrzebne są ze względu na to, iż usuwana jest tablica (typu char ).
~ Zastosowanie klasy CMessage Prze ćwic zymy
zasto sowanie klasy CMess age na
poniższym
krótkim
przykładzie:
II CwS_02.cpp II Użycie destru ktora do zwolnienia pam ięci . #i ncl ude II Dla strumienia wejścia-wyjścia. #i ncl ude II Dla funkcji strlen() i strcpy().
using std : :cout : using std : :endl : II Wstaw tutaj defi n icję klasy CMessage (listing CwS_Ol) . II Wstaw tutaj t nt (
II Ręczn e usunięcie obiek tu utworzonego za p om o cą operatora new.
ret urn O: Nie zapomnij w miejsce komentarzy wstawić odpowiednich partii kodu zawierających defini cje klasy CMes sage i destruktora, gdyż bez nich programu nie będzie można skompilować .
Jak lo działa Na początku funkcji main () zadeklarowaliśmy i zainicjalizowaliśmy obiekt klasy CMess age o nazwie motto w standardowy sposób. W drugiej deklaracji zdefiniowaliśmy wskaźnik do obiektu klasy CMess age o nazwie pMoraz za pomocą operatora new przydzieliliśmy pamięć obiektowi klasy CMessage, wskazywanemu przez ten w skaźnik. Wykonanie operatora new spo woduje wywołanie konstruktora klasy CMes sage, który z kolei ponownie wywołuje operator
Rozdzial8.•
Więcej na temat klas
445
new w celu przydzielenia pamięci dla komunikatu tekstowego wskazywanego przez należący do klasy wskaźnik pmessag e. Skompilowanie i uruchomienie tego programu da następujący rezultat: Lepiej późno n i ź wca le.
Wszyscy s ą sobie równ i .
Dest rukt or zos t a ł wywo ł a ny .
Na ekranie mamy informację, że destruktor został wywołany tylko jeden raz, mimo że utwo rzone zostały dwa obiekty kla sy CMes sage. Wcześniej wspominałem, że kompilator nie jest odpowiedzialny za obiekty utworzone w obszarze wolnej pamięci. Kompilator wywołał de struktor dla obiektu motto, ponieważ jest to normalny obiekt automatyczny, mimo że pamięć dla tej składowej została przydzielona przez konstruktor w obszarze wolnej pamięci. Obiekt wskazywany przez wskaźnik pMjest inny . Pamięć dla tego ob iektu została przydzielona w ob szarze wolnej pamięci, a więc musi zostać zwolniona za pomocą operatora del ete. W tym celu należy usunąć komentarz sprzed instrukcji return w funkcji matn t ): II delete p M;
II Ręczne
usunięcie
obiektu utworzonego za pomocq ope ratora new.
Ponowne skompilowanie i uruchomienie programu da
następujący
wynik:
Lepi ej późno ni ż wca le .
Wszyscy są sobie równ l
Dest ruktor zost a ł wywoł a ny.
Destrukto r z o stał wywoła n y.
Teraz nasz destruktor został wywołany dwa razy. Jest to pod pewnym względem zaskakujące. Operator del ete usuwa pamięć przydzieloną za pomocą operatora new w funkcji main( ) zwalnia tylko pamięć wskazywaną przez wskaźnik pM. Ze względu na to, że wskaźnik pM wska zuje obiekt klasy CI~e s sa g e, operator de l ete wywołuje także destruktor, aby zwolnić pamięć zajmowanąprzez składowe obiektu. A więc za każdym razem, gdy do usunięcia obiektu utwo rzonego dynamicznie za pomocą operatora new używamy operatora de l ete, wywoływany jest destruktor klasy dla tego obiektu przed zwolnieniem pamięci przez niego zajmowanej.
Implementacja konstruktora kopiującego Gdy przydzielamy dynamicznie pamięć składowym klasy, w obszarze wolnej pamięci czają się na nas demony. W przypadku klasy CMessage domyślny konstruktor kopiujący jest zupełnie niewystarczający. Przypuśćmy, że napisaliśmy następujące instrukcje: CMessage mottol( "Promi eniowanie zabi j a twoj e geny. ") ; CMes sage motto2(mottol) ; II Wywolanie domyśln ego konstruktora kopiujqcego. Efektem działania domyślnego konstruktora kopiującego jest skopiowanie adresu przecho wywanego we wskaźniku będącym składową klasy z obiektu mottol do obiektu motto2, ponie waż proces kopiowania zaimplementowany przez domyślny konstruktor kopiujący polega na prostym skopiowaniu wartości przechowywanych w składowych oryginalnego obiektu do nowego obiektu. W konsekwencji tylko jeden łańcuch tekstowy jest współdzielonyprzez dwa obiekty. Pokazano to na rysunku 8.1 .
zostanie zmodyfikowany za pośrednictwem któregokolwiek z tych dwóch będą także w tym drugim, ponieważ dzielą one ten sam łańcuch. Jeżeli obiekt mottol zostanie zniszczony, to wskaźnik w motto2 będzie wskazywał zwolniony obszar pamięci , który może zostać wykorzystany do czegoś innego. To na pewno spowoduje chaos. Oczywiście, ten sam problem pojawi się w momencie zniszczenia obiektu rnotto2. W ta kim przypadku obiekt mottol zawierałby wskaźnik do nieistniejącego łańcucha tekstowego. obiektów, zmiany widoczne
Rozwiązaniem tego problemu może być dostarczenie konstruktora kopiującego w miejsce konstruktora domyślnego. Jego implementacja mogłaby znaleźć się w sekcj i publicznej klasy, jak widać poniżej:
CMessage(const CMessage& initM )
II Definicja konstruktora kopiującego.
( II Przydzielenie pam ięci dla
lań cu ch a
tekstowego.
pmessage = new char[ str len(inl t M.pmessage)
+ l ]:
II Kopiowanie tekstu do noweg o obszaru pamięci.
st rcpy(pmessage. i nit M. pmessage): Pamiętamy z poprzedniego rozdziału, że aby uniknąć nieskończonej spirali wywołań konstruk tora kopiującego, parametr musi być określony jako stała referencja. Zdefiniowany powyżej konstruktor kopiujący wpierw przydziela odpowiednią ilość pamięci dla łańcucha w obiekcie in itM, przechowującego adres w składowej nowego obiektu, a następnie kopiuje ten łańcuch z obiektu inicjalizującego. Teraz nowy obiekt jest identyczny ze starym, ale całkowicie od niego niezależny .
Rozdział 8.• Więcej na temat
klas
447
sobie j ednak, że możesz już czuć się bezpiecznie i że nie musisz zawracać sobie konstruktorem kopiującym dzięki zainicjalizowaniu jednego obiektu klasy CMessage innyrn jej obiektem. W mrokach obszaru wolnej pamięci czai się jeszcze jeden demon, który tylko czeka na okazję do zadania C i ciosu w najbardziej nieoczekiwanym momencie. Przyj rzyjmy się poniższej instrukcji:
Nie
myśl
głowy
CMessage t hought( "Dobrze w domu DisplayMessage(t hought ) ;
by ć
z
mamą." ) ;
II Wyw oianiefunkcji w celu wysiania
II komun ikatu na wyjś cie.
gdzie funkcja Di s pl ayMes s age ( ) jest zdefiniowana
następująco;
void Di splayMessage(CMessage l ocalMsg) (
cout
« «
end l « " C h c ę wsm powiedzi e ć . localMsg.Showlt ( ) ;
że :
"
retur n;
Czyż nie wygląda to doskonale? Co mogłoby tutaj być źle? Ten błąd to katastrofa i nic więcej! To, co robi funkcja Di spl ayMessa ge() , w rzeczywistości jest niedorzeczne. Problem dotyczy parametru. Parametr jest obiektem klasy CMes sage, a więc argument w wywołaniu przekazy wany jest przez wartość. Przy użyciu domyślnego konstruktora kopiującego kolejność wyda rzeń jest następująca:
l
Tworzony jest obiekt thought z pamięcią dla komunikatu "Dobrze w domu być z mamą. " , przydzieloną w obszarze wolnej pamięci.
2.
Wywołana zostaje funkcja Di s pl ayMessage ( ) oraz - ponieważ argument przekazany jest przez wartość - tworzona jest kopia obiektu l ocal Msg za pomocą domyślnego konstruktora kopiującego. Od tej chwili wskaźnik w kopii wskazuje na ten sam łańcuch w obszarze pamięci wolnej co obiekt oryginalny.
8. Pod koniec wykonywania funkcj i obiekt lokalny wychodzi poza zasięg, a
więc
zostaje wywołany destruktor klasy CMessage . Powoduje to usunięcie obiektu lokalnego . (kopii) poprzez zwolnienie pamięci wskazywanej przez wskaźnik pmessage.
4. W czasie zwracania wartości z funkcji Di s pl ayMes sage( ) wskaźnik w oryginalnym obiekcie th ought nadal wskazuje obszar pamięci, która dopiero co została wyczyszczona. Przy następnej próbie użycia oryginalnego obiektu (lub nawet jeśli z niego nie skorzystamy, ponieważ musi on zostać wcześniej czy później usunięty) program będzie zachowywał się w dziwny i tajemniczy sposób. Wszelkie wywołania funkcji przekazujących przez wartość obiekty klas zawierających skła dowe definiowane dynamicznie powodują problemy. W związku z tym możemy podać stupro centową złotą zasadę:
Przydzielając dynamicznie pamięć składowej klasy w natywnym C++, zawsze implementuj konstruktor kopiujący.
448
Visual C++ 2005. Od podstaw
Dzielenie pamięci
pomiędzy
zmiennymi
z czasów, gdy 64 K stanowiło dużą ilość pamięci , w C++ istnieje moż na współdzielenie tego samego obszaru pamięci przez więcej ni ż jedną zmienną (ale oczywiście nie w tym samym czasie). Twór ten nazywa się unią i istnieją cztery podstawowe sposoby jego wykorzystania:
Jako relikt
przeszłości
liwość pozwalająca
•
D zięki użyciu unii mo żna sprawić, że zmienna A będzie zajmowała blok pamięci w jednym miejscu programu, który jest następnie wykorzystywany przez zmienną Binnego typu, ponieważ zmienna Anie jest już potrzebna. Nie zalecam takiego zastosowania unii, gdyż związane z tym ryzyko spowodowania błędu jest o wiele więks ze niż korzyści. Ten sam efekt mo żna u zyskać, przyd zielając pamięć dynamicznie.
• W programie może dojść do sytuacji, w której potrzebna jest duża tablica danych, ale przed wykonaniem programu nie wiadomo, jakiego typu będą to dane. Stanie się to jasne dopiero po wprowadzeniu ich z zewnątrz. Takiego zastosowania unii również nie polecam, ponieważ to samo możemy osiągnąć za pomocą kilku wskaźników różnego typu i dynamicznego przydzielania pamięci. • Trzeci sposób zastosowania unii może czasami się przydać - kiedy chcemy zinterpretować te same dane na dwa lub więcej różnych sposobów. Może mieć to miejsce w przypadku, gdy dysponujemy zmienną typu l ong i chcemy ją potraktować jako dwie wartości typu s hort . System Windows czasami pakuje po dwie wartości typu short do pojedynczego parametru typu l ong przekazywanego do funkcji. Innym przykładem jest sytuacja, gdy chcemy potraktować blok pamięci zawierający dane liczbowe jako łańcuch bajtów, aby je gdzieś przen ieść. • Unii można także u żyć jako sposobu przekazywania obiektu lub wartości, gdy nie wiadomo z góry, jakiego typu one będą. Unia może przechowywać dane dowolnego typu.
Definiowanie unii Unię definiuje się za pomocą słowa kluczowego union. Jej konkretnym przykładzie:
unio n share LD {
doubl e dval :
l ong lval:
}: Powyższy
definicję najłatwiej zrozumieć
na
II Wspoldzielenie pamięci przez typy fang i double .
kod definiuje unię typu shareLD, która może przechowywać zmienne typu l ong i doubl e, zajmujące ten sam obszar pamięci . Nazwa typu unii najczęściej zwana jest etykietą. Instrukcja ta jest podobna do definicji klasy w tym , że nie zdefiniowaliśmy jeszcze egzem plarza unii, a więc na razie nie mamy jeszcze żadnych zmiennych. Po zadeklarowaniu typu unii można za pomocą deklaracji definiować jej egzemplarze. Na przykład :
Rozdział 8.
Więcej
•
na lemal klas
449
shareLD myUnion; Powyższy
kod definiuje egzemplarz typu unii s har eLD, którą zdefiniowaliśmy Egzemplarz ten mogliśmy również zdefiniować wewnątrz instrukcji definiującej umo n
shareLD
Wspołdzi eleni e pamięci
II
wcześniej. samą unię:
przez typy long i double.
{
double dval :
long lval :
} myUn ion : W celu odwołania się do składowej unii używamy operatora bezpośredniego dostępu do skła dowej (kropki) z nazwą egzemplarza unii , podobnie jak przy uzyskiwaniu dostępu do skła dowych klasy. Poniższa instrukcja ustawiłaby zmienną typu l ong o nazwie l val na wartość 100 w egzemplarzu unii MyUnion:
myUn ion .l va l = 100:
II
Używanie sk łado wej
unii .
Późniejsze użycie
w programie podobnej instrukcji inicjalizującej zmienną typu doubl e o nazwie dval powoduje nadpisanie zmiennej l val. Największy problem związany z wyko rzystaniem unii do przechowywania danych różnego typu w tym samym obszarze pamięci spowodowany jest sposobem jej działania - trzeba znaleźć jakiś sposób określenia, która składowa jest aktualnie wykorzystywana. Zazwyczaj dokonuje się tego poprzez utrzymywanie dodatkowej zmiennej, która służy jako wskaźnik typu przechowywanej wartości. Unia nie jest ograniczona tylko do dwóch wartości . Ten sam obszar pamięci może współdzielić kilka różnych zmiennych. Ilość pamięci zajmowanej przez unię jest równa ilości pamięci po trzebnej do przechowywania jej największej składowej . Załóżmy na przykład, że zdefiniowa l iśmy następującą unię :
union sha reDLF {
double dva l :
long l val :
f loat fval:
} ui nst = {1.5 }: Egzemplarz unii shar eDLF zajmuje osiem bajtów, co uwidoczn ione
Rvsunek 8.2
- - -- - 8 bajtów -
-
-
-
zostało
Q:gJ-------,-------cq :,
Ival
,, ,,
:,
., ..
~
:
fval
,,------~y-----~ dval
na rysunku 8.2.
450
VisIlai C++ 2005. 011 pOIlstaw
w powy ższym przykładzie zdefiniowali śm y egzempl arz unii ui nst oraz jej etykietę . plarz
zo stał równ i eż
zainicj alizowany
warto ś ci ą
Egzem
l .5.
W dekla racji egze mplarza unii zainicjalizowa ć
można
tylko j ej pierwszą składową.
Unie anonimowe Unię można zad ekl arow a ć ta kż e
bez podawania nazw y jej typu. Egzemplarz takiej unii j est deklarowany automatycznie. Przypu ś ćm y , że zadeklarowali śm y p on iższ ą unię : union {
char* pva 1;
double dval :
i ong lval :
}: P owyższa
instrukcja definiuje zarówno unię bez nazwy, jak i jej egzemp larz, równ ież bez nazwy. Dzięki temu do zmiennych w niej zawartych możn a odwoływa ć s i ę za pomo cą samych nazw , jakie zostały im nadane w unii - pva l , dva l , l val . Taki spos ób definicji unii może być wygo dniejs zy od standardowego z pod an ą nazwą typu, ale trzeba uw ażać , aby nie pomi e s zać zwykłych zmiennych ze zmiennymi s k ła d o wy m i unii. Składo we takiej unii nadal współdz i el ą ten sam obsz ar pamięci . Aby zi l ust ro w ać, jak d z iała po wyżs za anonimowa unia przy uży c iu s kła do wej typu doub Te, można na pi s ać poni żs zą instrukcję :
dva l
~
99.5:
II
Użycie s k ładowej
anonimowej linii.
Jak widać , nic nie odróżnia zmiennej dva l od zwykłych zmiennych. U żywaj ąc unii anonimo wych, m ożna przyjąć pewne konwencj e nazewnicze, dzięki którym skł adowe uni i będą lepiej widoczne, a co za tym idzie - istnieje mniejsze ryzyko, że nasz kod zos tanie ź le zrozumiany.
Unie wklasach i strukturach Egzemplarz uni i można umie ści ć w klasie lub strukturze. Je żeli zami erzasz prze chowywa ć dane różnego typu w ró żnym czasie, zazwyczaj koniec zne jest utrzym ywanie zmiennej skła dowej klasy wskazuj ącej rodzaj przechowywanej wartości w unii. Użyci e unii jako s kładowych klas lub stru ktur nie przynosi zazwyczaj wielkich korzyści.
Przeładowywanie operatorów Przeładowywanie
operatorów je st bardzo wa żn ą techniką, g dy ż um ożliw ia ona w spółpracę standardowych opera torów C++, takich jak + lub *, z obiektami typów dany ch stworzonymi przez p rogrami stę . Pozwala ona na napisanie funkcji redefin iując ej dany operator w taki sposób, aby wykon ywał on okreś lone czy nności , kiedy jest u żywany z obiektami jakiej ś klasy. Na
Rozllzial8. •
Więcej na temat klas
451
przykład moglibyśmy przedefiniować operator > w taki sposób, że kiedy zostanie użyty z obiek tami klasy (Box, któ rą widzieliśmy w poprzednim rozdziale, zwróci warto ś ć t rue, jeżeli pierw szy z nich ma wi ęk szą p ojemność ni ż drug i.
Przeładowywanie
nie pozwala na definiowanie własnych operatorów ani na zm ianę priorytetów operator ma tę samą kolejność wykonywania podczas oblicza nia warto ści wyrażenia co je go odpowiednik bez przeładow ywani a . Ta be lę pierw s zeń stw a operatorów można zn a l eźć w rozdzi ale 2. tej ksi ążki lub w bibliotece MSDN .
już i stn i ejących . Przeładowan y
Mimo
że
Poniżej
nie wszystkie operatory można przeładowywać, ograniczenia nie znajduje s i ę lista operatorów, których nie można przeładowywać :
Operator z a S l ę g u
Operat or warunkowy Operat or b ezpośr edn i eg o do s tę p u do sk ł a dow ej
Ope rat or size-of Operat or wy łu s kan i a wsk aźnlka do s k ł a dowej kl asy
są
zbyt dotkliwe.
? :
si zeof
*
Ze wszystkimi pozostałymi możemy s i ę bawi ć , co daje nam c ałkiem spore możliwoś ci . Oczy wiście , dobrze jest upewnić się, że nasze wersje operatorów standardowych są spójne z ich normalnym użyciem albo przynajmn iej ich sposób działan ia jest wy starczająco intui cyjny . Nie b yłoby zbyt sensownym posun ię c iem utworzenie dla klasy przeładowanego operatora +, który mnożyłby jej obiekty . Sposób dzi ał ania operatorów przeł adowanych najłatwiej zrozu mieć na przykład zie , a wi ę c zaimplementuj emy teraz to, o czym przed chwilą m ówiłem operat or w iększości > dla klasy Cbox.
ImlJlementacia przeładowanegO operatora W celu zaimp lementowania przeł adowanego operatora dla klasy należy napisać s p e cj a l n ą funkcję. Zakładając, że jest ona s kładową klasy CBox, deklaracja funkcji przeładowuj ąc ej ope rator > w obrębie tej klasy wyg ląd a na stępująco :
cl ass CBox {
publ i c: bool ope rat or>(CBox&aBox) const ;
II Prz eladowanie opera tora
większośc i .
II Reszta definicji klasy ...
};
W p owyższym kodzie słowem kluczowym jest oper ator. Słowo to w połąc zen iu z symbolem lub nazwą operatora, w naszym przypadku >, definiuje funkcj ę operatora . Nasza funkcja na zywa s ię operatorc-O . Funkcję operatora można nap i sać , umie szczając , bąd ź nie, spacj ę po m i ędzy sło wem kluczowym oper ato r a samym operatorem , dopóki nie poj awiaj ą się żadne dwuznacznoś ci. Wątpli wo ś ci co do znaczeni a mogą pojawić s ię przy u życiu operatorów w postaci nazw, a nie symboli, takich jak np. new czy del ete. Gdyby śmy napisali operato rnew lub oper at ordel et e, to p owstałyby zwykłe funk cje, gdyż są to prawidłow e nazwy funkcji. A więc pisząc definicję funkcji jednego z tych operatorów, po słowie kluczowym opera t or zawsze należy wst awi ć sp acj ę . Z au w aż , że funkcj a oper ator>( ) zadeklarowana zo s tała jako const , ponieważ nie modyfikuje ona zmiennych składowych klasy .
452
Visual C++ 2005. Od podstaw W zdefiniowanej funk cji oper ator >() prawy operand operatora zdefiniowany jest przez pa rametr funkcji . Lewy natomiast jest zdefiniowany niejawnie przez wskaźnik thi s. Jeśli mamy wi ęc poni żs zą in strukcj ę w arunkow ą i f : if(boxl cout
>
«
box2)
endl « "boxl j est
box2";
w i ęk s z y n iż
wyrażenie znajdujące się
w naw iasach po s łowi e kluczowym i f spowoduje funkcji operatora i jest równo znaczne z poniższym w ywołaniem funk cji :
wywołanie naszej
boxl .operat or>(box2) ; Powiązania pomiędzy zostały
RYSunek 8.3
obiektami klas y CBo x w przedstawione na rysunku 8.3.
wyrażeniu
a parametrami funk cj i operatora
if{ boxl > box2 )
Argum en t fun kcji
1
!
bool CBox::operator>(const CBox& aBox) const
l Obiekt w skazyw any przez w sk a źn ik thts ł
return (this ->VolumeOl > (aBox.VolumeOl;
Spójrzmy teraz, w jaki sposób
działa
kod funk cj i ope r at or> ( ):
II Funkcj a opera tora większośc i porównująca II pojemności dwóch ob iektów klasy CBox.
bool CBox: :opera to r>(const CBox& aBox) const
{
ret urn t hi s- >Vol ume( ) > aBox.Vol ume () ;
Podali śmy do funkcji parametr w postaci referencji w celu unikni ęcia niepotrzebnego kopio wania w momencie jej wywołan ia . Ze względu na fakt, że funkcja nie zmienia zawarto ści obiektu, dla którego zostanie wywołana, zadeklarowaliśmy jąjako stałą. Gdyby śmy tego nie zrobili, nie moglibyśmy używać naszego operatora do porównywania obiektów typu const klasy CBox. Wyrażenie re turn oblicza za p omoc ą funkcji Vo l ume () pojemność obiektu klasy CBox, wska zywanego przez wskaźnik t hi s, a następnie porównuje otrzymany wynik z pojemnością obiektu aBox przy u życiu operatora >. Podstawowy operator > zwraca wartość typu ca ł kow i tego (nie logiczną) , a wi ęc jeżeli pojemność obiektu klasy CBox wskazywanego przez wsk aźnik t hi s je st większa niż pojemność obiektu aBox przekazanego jako argument referencyjny, zostaje zwrócon a warto ść l , a w przeciwnym przypadku o. Wartość zwrócona z operacji porówny wania zostanie automatycznie przekonwertowana do typu zwracanego funkcji operatora, czyli do typu logicznego .
Rozdział 8.• Więcej na temat klas
~ Przeładowywanie P rzećwi c zymy
operatorów
zastosow anie funkcji oper at or> ( ) na przykładow ym programie:
II CwS 03.cpp II Ćwi;;en ie zastosowa nia przeładowan ego opera tora większosc i .
pri vate: dou ble m_Lengt h: deub le m_Widt h: double m_Hel ght : }: II Fu nkcja op eratora większości porównująca
II poj emnosoi dwóch obiektó w klasy Clłox.
boo l CSox: 'ope rator>(const CBex& aBox) const (
retu rn t hi s->Vo l ume() > aBox.Velume( ): int mai n( ) (
CBex smal l Bex (4.0. 2.0, 1.0) ;
CBex medi umBox(10.0. 4.0. 2.0) ;
CBex bigBox(30 .0. 20 .0. 40 .0):
lf( mediumBox > smal l Bex )
cout « end l
II Dlugos ć pu delka w centymetrach. II Szero koś ć pude lka w centyme trach. II Wysokoś ć p ude lka w centymetrac h.
453
454
Visual C++ 2005. Od podstaw «
" Pu d e ł k o
mediumBox jest
wię k s z e
niz smal l Box." ·
lf (medl umBox > blgBox) cout « endl « " Pu d eł k o mediumBox j est wi ę ksz e niz bigBox. "; else cout « endl « "Pu d ełk o medlUmBox nie jest w i ęks ze ni ż bigBox" ; cout « endl ; ret urn O;
Prototyp funkcji oper at or >( ) znaj duj e się w sekcji publicznej klasy. Jako że definicja funkcji znajduje s ię ju ż poza k l asą, nie stanie s ię ona dom yślnie funkcją inline. Jest to całkowi ci e z a leż n e od progr ami sty . Równie dobrze mogliśmy definicję tę wstawić do definicj i klasy w miejsce prototypu funkcji. W takim przypadku nie b yłoby potrzeby używania przed funkcją kwa lifikatora w postaci CSox ; .. Jak zapewne pamiętasz , jest to konieczne zawsze wtedy , gdy funkcja składowa jest zdefiniowana poza obrębem definicji klasy, gdyż informuje kompilat or, do której klasy dana funkcja nal e ży . W funkcji mai n( ) znaj d ują s i ę dwie instrukcj e warunkowe i f, w których został użyty operator > ze s kład o wym i klasy. Jego użycie powoduje automatyczne wywołanie operatora przełado wanego . J eżeli chce my m i eć tego potw ierd zenie , to możemy dodać instrukcję wyjściow ą do funkcj i op eratora. Wyn ik działania tego programu jest następujący:
Konst ruktor z o s t a ł wywo ł a ny .
Konst ruktor zosta ł wy woł a ny .
Konstruktor zos t ał wy wo ł any .
P ud e ł k o med i umBox j est wi ę k s z e n i ż sm al l Box.
P u d e ł k o med i umBox nie j est w i ęk s z e n iż bigBox.
Destruktor z o sta ł wywo ł any .
Dest ruktor z o sta ł wywoł any .
Dest ruktor zo sta ł wywo ł any
Z danych na ekrani e wynika, że instrukcj e warunk ow e i f z funkcją operatora. A więc wydaje się, że przedstawienie w kategoriach obiektowych jest rozsądnym pomy słem .
dzi ałają p rawidłowo
rozwiązan i a
w połączeniu problemów klasy CSox
Implementacja pelnej obSlUgi operatora Nadal jest wiele rzeczy, który ch nie możemy zrob i ć przy u życiu naszej funkcj i operator-O. Definicja rozwiązania problemu w kategoriach obiektów klasy CSox mogłaby równie dobrze zawiera ć następujące instrukcj e:
if( aBox > 200 ) II Rób coś...
Rozdział 8.• Więcej na
lemaIklas
455
Nasza funkcja nie por adzi so bie z czymś takim. Prób a u życi a wyra że nia porównuj ącego obiekt klasy CBox z wartości ą li czbową zako ńczy s ię zgłoszeniem przez kom pi lator komunikatu o błę dzie. W celu umo żliw ien ia wykonyw ani a takich operacj i należał ob y napisać funkcj ę ope r a t or >( ) w wersji przeł ad owanej. Dod ani e o bsłu g i wy ra ż e n ia, które przed c hw i lą w i dz i e liś my , j est bar dzo funk cj i w ewnątrz kl asy wyglądałaby n a stępująco :
łatw e .
Dekl ar acj a
II Porównanie obiektu klasy CBox ze s ta lą.
bool ope rato r>Cconst double&val ue) const : Defin icja ta powinna p oj awić s ię w definicji klasy . Prawy ope ra nd operatora > odpowi ada par ametrowi funk cji . Obiekt klasy CBox, który jest tutaj lewyrn operande m, przekazany zostaje niej awnie w postaci w skaźnika t hi s . Implementacja tego przeładow aneg o operatora j est instrukcj i w ciele funkcj i: II Funkcj a poró wnują c a obiekt klasy CRox ze
równi eż łatwa .
Wymaga on a tylk o jednej
s talą .
boo l CBox: .operat or>Cconst double&val ue) const (
retu rn t his->VolumeC) > va l ue: P ro ś ci ej już Z
ch yb a by ć nie mo że ? A le nadal mam y pewne pr obl emy z u ży ciem operatora > obi ektami klasy CBox. Równie dobrze możemy z ec h c i eć napisać instrukcj ę podobną do
pon i ższej :
if (200
>
aBox)
II Rób coś ... M o żesz powiedzi e ć , że
da si ę to zro b i ć , impl em entując funkcj ę operatora operator« ) pr zyj prawy arg um ent typu doub l e, a na stępn ie przepi suj ąc p o w yż s z ą i n s t ru k cję w taki sposób, aby jej używała - i masz rację . Rzecz ywi ści e , imp leme ntacja operatora < może być wy magan a do por ównywani a obiektów klasy CBox, ale implementacja o bsługi typu obiektu nie powinna w ża de n sztuczny spo sób ograni cz a ć sposobu, w j aki możn a u żywać obi ekt ów w wyra żeniach . Ich użycie powinno być jak najbardziej naturalne. Probl emem jest kwesti a, j ak tego dokon a ć . muj ącą
S k łado wa
funk cj a ope rato ra zaw sze dostarcza lew y arg um ent w post aci wskaźnik a t hi s . w tym przyp adku lewy argument jest typu doubl e, nie można tej funkcji zaimp lemen tow ać jako fu nk cji s kła do wej . P ozostaj ą nam dwa wyj ści a: zwykła funkcja lub funkcj a za przyjaźniona . Ze w zględu na fakt, że nie pot rzebujemy do stęp u do prywatnych s kł ad o wy c h klasy, nie musimy stosować funkcji zaprzyjaźn io nej , a w ięc przełado wany opera tor > mo żem y za i m p leme nto wać z lewym argumentem typu doubl e j ako zwy kł ą funkcj ę . Pr ototyp tej funk cji - umi eszczonej oczyw i śc i e poza defini cj ą klasy , poni eważ nie jest on a funkcj ą s kła dową
Jako
że
wygląda następuj ąco:
bool ope rat or>Cconst doubl e& val ue. const CBox&aBox); Impl em ent acj a
wyg l ąda n a stępuj ąc o :
456
Visual C++ 2005. Od podstaw II Funkcja
porównują ca s ta lą
z obiektem klasy CBox.
bool operato r>(cons t doubl e& val ue . const CBox&aBox) (
ret urn val ue > aBox.Vol ume( ) : Jak ju ż wiemy , zwykła funkcja (a także zaprzyj aźn i o n a w takim przypadku) uzyskuje dostęp do składowych obiektu za p omocą operatora be zpo średniego dostępu do składowej oraz na zwy obiektu. Oczywiście , zwykła funkcj a ma do stęp tylko do składowych znajdujących się w sekcji publicznej . Funkcja s kł adow a Vol ume( ) jest publiczna, a więc możemy ją tutaj bez problemu użyć. Jeżeli
w klasie nie byłob y publ icznej funkcji Vol umst ), to do uzyskan ia bezpośredniego do do prywatnych składowych m ogl ibyśmy użyć funkcji zaprzyjaźnionej . Innym wyjśc iem byłoby dostarczenie zestawu funkcji skład owych zwracaj ących wartości prywatnych zmien nych składowych oraz użycie ich w zwykł ej funkcj i w celu implem enta cji porównywania. stępu
Rm!II!!UI Pelne Jlrzeładowanie operatora> Wszystko, o czym mówiliśmy do tej pory, złożymy w jedną całość, aby
zobaczyć, jak to działa :
II Cw8_04.cpp
II Implementacj a pełnego p rze łado wania operatora w iększos ci.
#i ncl ude II Dla strumienia wejścia-wyjścia.
using st d: .cout: us i nq st d: .endl : class CBox
II Defini cj a klasy o zas ięgu globalnym.
(
pub l l C: II Definicj a konstruktora.
CBox(double l v = 1.0. doub le wv = 1.0. double hv = 1.0) : m_Lengt h(l v) . m_Widt h(wv) . m_He ight (hv) cout « end l « "Konstr ukto r II Funkcja
obliczająca pojemnos ć
zos ta ł wywo ł a ny
pu delka.
doub le Volume() const ( ) II Funkcja operato ra większosci porówn ująca II pojemnoś ci obiektów klasy CBox.
bool operator>(const CBox& aBox) const (
ret urn t his->Vol umeO > aBox .Vol umeO : II Funkcja p o ró wn ująca obiekt klasy CBox ze sta łą .
bool operat op (const doubl e&val ue) const {
Rozdzial8.•
Więcej na
temat klas
457
ret urn thi s->Vol ume() > value: II Definicja destrukto ra.
II Długoś ć p ude łka w centymetrach. II Szerokość pudełka w centymetrach. II Wys okość pudelka w centymetrach.
}:
i nt operator- rconst double&va l ue. const CBox& asox) : II Prototyp funkcji. mt
ma i n()
{
CBox sma11 Box (4. O. 2.O. 1. O):
CBox medi umBox(lO. O. 4.0. 2.0):
i f (mediumBox > sma l lBox)
cout « endl
« "Pud eł k o medl umBox jest
w ięk s ze
ni t sma ll Box
i f (mediumBox > 50.0)
cout « endl
«
" P ojemn oś ć p u d e ł k a
els e
cout « endl
« " P o j emn o ś ć i f( lO .O> sma l l Box )
cout « endl
« "Po j emn o ś ć else
cout « endl
« "Po j emn o ś ć
medi umBox j est
wi ęk sza
ni t 50 . '"
pud ełka
mediumBox nie jes t
pude ł k a
sma11Box jes t mni ej sza
p u d e łk a
sma llBox nie jes t mniej sza
w i ęk s z a
ni ż
ni z 50 .";
10.
ni ż
lO .
cout « endl :
ret urn O:
II Funkcja porówn ująca stałą z obiektem klasy CBox.
i nt ope rator >(con st double& va lue . const CBox&aBox)
{
return value > aBox .Volume () ;
Jak lo działa Zwróć uwagę ,
w którym miejscu znajduje się prototyp funkcji oper at or>( ) w zwykłej wersji. Musi ona zn ajd o wa ć się po defin icj i klasy , po nieważ odnos i się do obi ektu klasy CBox na liście parametrów . Jeżel i um i e śc im y ją przed d efi n icją klasy , to programu nie b ęd zi e m o żn a s kompi lować .
458
Visual C++ 2005. Od podstaw Istnieje sposób na umieszczeni e j ej na początku programu po dyrektywie #i ncl ude: za pom oc ą niekompletnej deklaracji klasy. Powinna ona znaj do w ać się przed prototypem i wy gląd a na st ępująco :
class CBox: mt operato r-tconst doubl e& val ue . CBox& aBox) :
II Niekompletna deklaracja klasy. II Prototyp fu nkcji.
Powyższa niekompletna deklara cja klasy wskazuj e komp ilatorowi, że CBox j est klasą, co wystar cza, aby po zwolił on na poprawne przetworzenie prototypu funkcji . Jest to możliwe, poni eważ wie, że CBox j est zdefiniowanym przez u żytkownika typem, który zostanie określony p ó źn i ej .
Mechanizm ten j est t akż e ni ezb ędny w sy tuacjach, gdy mam y dwi e kla sy i każda z nich zawiera skład ow ą w postaci w skaźn ika do obiektu tej dru giej kla sy. Ka żda z nich wymaga, aby ta druga b ył a zadeklarowana j ako pierwsza. Tego typu sytu ację patową można ro zw i ą zać za pom ocą niek ompletnej dekl aracj i klasy. Rezultat
d ziałani a p owyżs ze go
programu j est
n astępuj ąc y:
Konstru ktor zo s t a ł wywo ł a ny .
Konst ruktor zost a ł wywoła ny .
P u de ł ko medlum Box Jest wi ę k s z e n i ż smal l Box.
P O jem no ś ć p u d eł k a m edi umBox j est wi ę k sza n iż 50
P O j emn o ś ć p ud e ł k a sma l l Box Jest m nlejSZa n i ż 10 .
Dest rJktor zo s t a ł wywo ł a ny .
Dest rJkt or zos tał wywo ł a ny
Po komunikatach o wywołaniu konstruktora w związ ku z tworzen iem ob iektów sma l iBox i med: umBox zn aj d uj ą się wiers ze wy słane z instrukcji warunkowych 'j f - każda z nich dz iała tak, jak się spodziewaliśmy. Pierwsza z nich wywołuje funkcję operatora, która jest składową kla sy i działa z dwoma obiektam i klasy CBox. Druga natomiast wywołuje funkcję składową z parametrem typu doubl e. Wyrażenie w trzeciej instruk cji warunkowej i f wywołuje funkcj ę operatora, kt ór ą zai mp l e m e n towa l iś m y jako zwy kłą fu nkcję . Tak się składa , ż e obie funkcj e operatora, które są s kła d o wy m i klasy, m ogliśmy zdefin iować j ako zwykłe funkcje , ponieważ wymagają one do stępu tylko do funkcji s kład o w ej Vo lume( ), która je st pub liczna.
W p odobny sposó b jak przedstawiony po wyżej można za implem en to wać dowolny opera tor porównania. R óżnice pomiędzy nimi powinny do tyczyć mniej znaczących szcz egółów, a ogólne podejś cie pozostaje takie samo.
Przeładowywanie Jeżeli
operatora przypisania
nie dost arczymy dla klasy funkcji przeład owane g o operatora przyp isania, to kornpi lator dostarczy jego domyślną wersję. Funkcja ta w w er sji domyśln ej po prostu wykonuje proces kopiowani a wszystkich s kła d ow yc h , podobny do tego, który wyk onywany jest przez dom yślny konstru ktor kopiujący . Nie można jednak myli ć domyślnego kon struktora kop i ują cego z domyślnym operatorem przypi sania. D om yś lny kon struktor kopiuj ący wywoływany je st przez deklar acj ę obiektu klasy, który jest inicjalizowany już i stn iejąc ym obiektem tej klasy
Rozdział 8.• Więcej na
temat klas
459
lub za p om o c ą pr zekazan ia do funkcj i j a k iegoś obiektu przez w art ość . Nato m iast domy ślny operator przypisania wywoływany jest, kiedy zarówno po prawej, jak i po lewej stronie instruk cji przypisania zn ajduj ą s ię obiekty tej samej kla sy. W przypadku klasy CBox d omyślny operator prz ypis ania działa bez zarzutów, ale w przypadku klas z awi e rających obszary p ami ę ci dla s kła dowyc h alokowan ych dynami cznie n ale ży u w a ż nie przyjrzeć s ię ich wy magan iom . Pomini ę cie op erato ra przypisania w taki ej sy tuacji m o że d oprow ad z i ć do p oważn ych za burze ń d ziałani a programu. Wróćmy na chw ilę do klasy CMessage, której używali śmy przy okazji omawiania konstrukto rów kop iuj ący ch. P ami ętam y , że miała ona zmienną składową pme ssage, która był a wskaźni kiem do łańcu ch a . Rozważmy teraz , jaki skutek wywołałby w j ej przyp adku dom yślny operator kop iuj ący. Przypu ś ćmy , że mi eli śmy dwa egzemplarze tej kla sy m ott ol i motto2. M o glibyśmy s p ró bować u staw i ć składowe egze mplarza m otto2 na wartości składo wych egze mplarza mottol za p om o c ą domy ślneg o operatora przypisania, jak poniżej:
motto2 = mottol :
II
Użyc ie domyśln ego
operatora p rzyp isan ia.
ope ratora przypisania w tym przypadku b ędzi e taki sa m, ja k gdy kon struktora kopiującego . To będzie katastrofa! Jako ż e k ażdy z tych obiektó w posiada w ska źnik do tego samego łańcuch a , jego zmiana dla jednego obi ektu po woduje zmi anę dla obu. Dru gim problemem jest to, że je żeli jeden z tych obiektó w zos tan ie zniszczo ny, to destruktor w yczy ści pamięć u żywaną do przechowywani a ł ań cuch a , a więc drugi obiekt będzi e zaw i e rał ws k aźn i k do obszaru p amięci , który m o że być ju ż używany do cze goś ca łki e m inn ego.
Efekt
u życia do myś l nego
byśmy użyli dom yślnego
To, czego potrzebuj emy, to aby operator przypi sani a żącego do obiektu docelowego.
Rozwiązanie
s ko p iow ał
tekst do obszaru
p ami ęci
nale
problemu
Problem ten możemy rozwiązać za pomocą własnej funkcji operatora przyp isania. Za kładamy, że zos tała ona zdefiniowana wewnątrz definicji kla sy: II Przełado wany ope rator przypisania dla obiektu klasy CMessage.
CMessage&operat or=(const CMes sage& aMessl ( II Zwo lnienie pamięc i dla pi erwszej operacji.
delet e[] pmessage: pmessage = new cha r[ st rlen(aMess .pmessagel II Skopiowanie
łań cu cha
+
l] :
drugiego opera ndu do pierwszego.
st rcpy (this ->pmessage. aMess .pmessage) : II Zwrócenie referencj i do p ierwszego operandu.
ret urn *t hlS: Przypi sanie może wydawa ć s ię pro ste , ale jest kilka szczegółów, na które n ale ży zwrócić uwagę . Warto zauważyć, że funkcja operatora przypisan ia zw raca referencj ę . Na pielwszy rzut oka może nie być oczywiste , dlaczego tak s i ę dziej e - przec ież funk cj a doprowadza operację
460
Visual C++ 2005. Od podstaw przypisania do samego końca i obiekt z prawej strony zostaj e przekopiowany do tego, który jest z lewej. Na pierwszy rzut oka może się wydawać, że nie ma potrzeby zwracania czegokol wiek, ale musimy bliżej przyjrzeć się temu, w jaki sposób mógłby zostać użyty operator. Istnieje prawdopodobieństwo, że będziemy musieli użyć wyniku operacji przypisania po prawej stronie jakiegoś wyrażenia. Przyjrzyjmy się następującej instrukcji :
mot t ol
~
motto2 = mot t o3;
Jako że operator przypisania jest wykonywany od prawej strony do lewej , najpierw zostanie wykonana operacja przypisania obiektu motto3 do obiektu motto2. A więc powyższą instrukcję możemy przedstawić następująco:
mott al = (mott o2.operat or=(motto3) ) ; Rezultat wywołania funkcji operatora znajduje a więc ostatecznie instrukcja ma postać :
się
tutaj po prawej stronie znaku
równości,
mott ol. operat or=(motto2.operator=(motto3) ) ; A zatem , jeżeli to ma działać, to na pewno coś musi zostać zwrócone. Wywołanie funkcji ope r ator=() pomiędzy nawiasami musi zwrócić obiekt, który będzie mógł zostać użyty w innym wywołaniu tej funkcji. W tym przypadku wystarczyłoby zwrócenie typu CMessag e lub CM es sa ge&, a więc referencja nie jest tu obowiązkowa, ale musi zostać zwrócony przynajmniej obiekt klasy CMes sage. Z drugiej jednak strony
(mottal
~
mot to2)
~
rozważmy poniższy przykład:
mot t o3;
Jest to w pełni prawidłowy kod (nawiasy zostały użyte w celu upewnienia się, że przypisa nie po lewej stronie zostanie wykonane jako pierwsze). Kod ten można przekształcić do na stępującej postaci :
(motto l.operator =(motto2) ) = mott o3; Po wyrażeniu pozostałej operacji przypisania w postaci jawnego dowanej otrzymujemy:
wywołania funkcji przeła
(m ot t ol. op e r a t o r =(mo tto2)) . op e r a t o r ~(mot t o3);
Powstała nam teraz sytuacja, w której obiekt zwrócony przez funkcję oper at or =( ) zostaje użyty do wywołania funkcji operat or-O . Jeżeli typem zwracanym jest tylko CMes sag e, to kod ten jest nieprawidłowy, ponieważ w rzeczywistości zwracana jest tymczasowa kopia oryginal nego obiektu, a kompilator nie zezwala na wywołanie funkcji za pomocą obiektu tymczaso wego . Inaczej mówiąc, wartość zwracana, kiedy typem zwracanym jest Cme ssage, nie jest l val ue. Jedynym sposobem na sprawienie, aby takie coś chciało się skompilować i działać poprawnie, jest zwrócenie referencji, która jest typu l val ue. W związku z tym jedynym moż liwym typem zwracanym, jeżeli chcemy zapewnić pełną elastyczność użycia operatora przy pisania z naszą klasą, jest typ CMessag e&.
Rozdział 8.• Więcei na temat
klas
461
Zauważ, ż e język C++ nie nakłada żadnych ograniczeń co do akcept owanych typów zwra canych lub typów parametrów operatora przypisania. Rozsądnie jest jednak zadeklarować ten operator w sposób przed c h w i l ą przeze mnie opisany, jeżeli chcemy, aby nasze funkcje operatora przypi sania obsługiw ał y normalne użyci e operacji przypisani a w C++ .
o którym należy pamiętać, to fakt , że każd y obiekt ma z góry przydzieloną dla ł a ńcucha , a więc p ierwszą rzeczą, jaką musi zrobi ć funkcja operatora, je st wy czyszczenie pamięci przydzielonej dla pierw szego obiektu oraz ponowne przydzielen ie od powiedniej jej ilo ści dla łańcuch a tekstowego należącego do drugiego obiektu . Po wykon aniu tych czynno ś ci ła ńcuch z drugiego obiektu może zostać skopiowany do nowego obszaru pa mię ci, należącego teraz do pierwszego obiektu. Drugi
szc żegół ,
pamię ć
Nadal jednak je st jeden defekt w tej funkcji operatora. Co
si ę
stanie , gdy napiszemy
poniższą
instrukcję ?
~t o1 = mott ol: Oczywiście
nigdy nie napisalibyśmy cze goś tak głupiego , ale może Jak na przykład w poniższ ej instrukcji:
się
to zdarzyć w przypadku
u żywania wskaźników .
motto1
~
*pMess:
pMess wskazuje obiekt mott ol, to otrzymamy wyrażenie identyczne z tym W takiej sytuacji funkcja operatora w obecnej postaci wyczyściłaby pamięć przy dzi eloną dla obiektu motto l , przyd zieliła trochę wię cej na podstawie długo ści właśnie usu ni ętego łańcu cha , a następnie sp ró b owała s k op i ow ać starą pami ęć , która do tej pory m oże być już nieprawidł owa . Można ten problem rozwiązać , sprawdzając identyczność lewego i pra wego operandu na po czątku funkcji. W związku z tym nasza funkcja operat or=( ) wygląda na Jeżeli wskaźnik
powyżej.
stęp uj ąc o:
II Prz eładowany opera tor przypisania dla obiektów klasy CMessage .
CMessage&ope rato r=(const CMessage& aMess) if (t his ~~ &a Mess ) ret urn *this ;
II Sp rawdź adres. jeś li laki sam, II z wróć pierwszy ope rand.
II Zwolnienie pamię c i dla pierwszego opera ndu.
delet eC J pmessage : pmessage ~ new charCst rle n(aMess .pmessage) +lJ: II Skopiowanie
łań cuch a
drugiego opera ndu do pierws zego .
st rcpy(t his- >pmessage. aMess .pmessage); II Zwrócenie refe rencj i do pierwszego opera ndu.
return *t his ; Powyższy
kod został napisany przy defini cji klasy.
założeniu, że
defini cja funkcji znajduje
się
w
obrębie
462
Visual C++ 2005. Od podstaw
lmmjI Przeładowywanieoperalora przypisania Pozbierajmy wszystko, o czy m m ów i li ś m y do tej pory w jeden program. Dodamy do klasy funkcj ę Rese t() , która konwertuje komunikat na łańc uc h gwiazdek . II Cw8_0 5.cpp II Szlifowanie przeladowywanla_o-,-p_e_ra_t_o r_a_k_o-,--p_io_H_'a_n_ia_.
.
--J
#i nclude
#include
uSlng std: :cout ;
ustne st d: .endl :
cl ass CMessage (
pr i vat e: char* pmessage ;
II
Wska źnik
do
lańcu ch a
obiektu.
puol iC: II Funk cja wy swietlajqca komunikat.
vo i d Showlt () const
{
cout
end l
«
«
pmessage ;
}
IIFunkcj a
konwertują ca
komunikat na *.
vo id Reset( ) (
char* t emp = omessage:
v/hil e(*t emp)
*(t emp++ ) = '*'.
II Przela dowany ope ra tor p rzypisania dla obi ektów klasy CMessage .
CMes sage&
ope r a t o r ~( c o n s t
CMessage&aMess)
{
i f (t his == &a Mess) ret urn *t his :
II Sp ra wdzanie adresów. jeś li są takie sam e, II zwró ć pi erw szy operand.
II Zwo lnienie pamię ci dla pierwszego op erandu .
delet e[J pmessage:
pmessage ~ new cha r[ st rlen(aMess .pmessage) +lJ:
II Skop iowanie
lań cucha
drugiego ope ra ndu do pierwszego.
strcpy(thls- >pmessage. aMes s .pmessage ) : II Zwrócenie ref erencji do pierwszego operandu .
ret urn *th i s; II Definicja konstruktora.
CMessage(const char* t ext = "Komumkat
d omy ś lny" )
{
pmessage = new char[ st r l enr t ext) +1 J: st rcoy(pmessage. t ext ) : II Destruktor
zwalniający p ami ęć przydzieloną
II Przydzieleni e pamięci dla tekstu. II Skopio wanie tekstu do nowego obszaru pamięci.
przez operator new .
Rozdzial8. •
Więcej
na temal klas
463
-CMessage ( ) {
cout
«
"Dest ruk tor zost ał
«
endl :
wyw o ł a ny . "
ae1et e[] pmessage;
II Śledz i, co s ię dziej e. II Z wolnienie pamię ci p rzydzie lonej
wskaźn iko wi .
};
int :na in( ) {
CMessage mottoH "G ł u p iemu CMessage mott o2 ;
szc zęści
e sprzyja. ") :
cout « "mot t o2 zaWlera
motto2.Showlt ( );
cout « enol ;
motto2 = mott ol ;
II Użyj no wego ope ratora przypisania.
cout « "mott o2 zawiera
motto2.Showlt( );
cout « endl ;
mottol .Res et ( );
II Ustawianie mo/lo} na + nie
II ma lVP~V W1J na mottoI ,
cout « "mottol zawiera teraz
mott ol .Showlt ();
cout « endl ;
cout « "motto2 na dal zaWlera
motto2.Showlt ();
cout « end1;
ret urn O. Z danych na ekr an ie wynika, ż e wszystko dz i ała należy ci e , bez żadn y c h powiązań po m i ęd zy komuni katami obu obiektów, z wyj ątkiem sytuacji, w których jawnie ustawiamy je jako takie same;
motto2 zaWlera
Komuni ka t domy ś lny
mot t o2 zaWle ra G ł up l em u sz c zę śc i e sprzYJa .
mot t ol zawiera t eraz
***************** **************
mot to2 nadal zawiera
G ł up i emu s z c z ę ś c i e sprzYJ a.
Dest ruktor zosta ł wywoła ny .
Dest ruktor z os ta ł wywo ła ny .
W związku z powyższym możemy utworzyć jeszcze j edną złotą zasadę :
Zawsze implementuj operator.przypisania, gdy .dynamicznie przydzielasz pamięć zmien nym składowym klasy.
464
Visual C++ 2005. Od podstaw Mając zaimplementowan y operator przypi sani a zastan ówm y s i ę, co dzieje s i ę z operatorami typu +=. Nie dział aj ą, chyba że je także zaimpl ementujem y. Dla każdego operatora w postaci op=, którego chcemy u ży ć z naszą klas ą, musimy nap i s ać o d dzi e l n ą funkcję operatora .
Przeładowywanie Zaj miemy
si ę
operatora dodawania
teraz
p rz eład owywaniem
operatora dodawania dla klasy CSox. Jest to bardzo z tworzeniem i zwracaniem nowego obiektu. Obiekt ten będzi e to z naczyć w naszej defini cji) dwóch obiektów klasy CSox, które są
interesujące , gdyż związane jest sumą (cokolwiek jego operandami.
będzi e
Co więc mamy na myśli, m ówi ąc o sumie dwóch obiektó w? Istni eje co najmniej kilka od powiedzi na to pytan ie, ale dla naszych potrzeb wystarczy co ś prostego. Sumę dwóch obiek tów klasy CSox zdefiniujemy j ako obiekt klasy CSox wy starczaj ąc o duży, aby pomi eści ć dwa p o zo s tałe obiek ty ( p u de ł ka) um ieszczone jeden na drugim . Możemy tego dokonać poprzez dodanie do nowego obiektu składowej m_Length , której warto ś ć b ędzie równa wartości większej s kładowej m_Lengt h dwóch dodawan ych obiektów. W podobny spos ób utworzymy zmie nną s kładową m_Wi dt h. Zmienna s kładowa m_Hei ght będzie s umą zmiennych składowych m_Hei ght dodawanych obiektów. W ten sposób powstanie obiekt klasy CSox mogący pomie ści ć dwa inne obiekty tej samej klasy. Nie jest to może rozwiązanie najbardziej opty malne, ale na nasze potrzeby wystarczające . Zmien i aj ąc konstruktor, sprawimy także, że skła dowa m_Length obiektu klasy CSox zawsze będzie wi ęk sza lub równa s kł ad o wej m_W id th . Om awianą wersję powyższa
operatora dodawania najłatwiej przedstaw i ć w formie g raficznej . koncepcja został a przedstaw iona na rysunku 8.4.
Cała
Jako że potrzebujem y bezpo średn ieg o dostępu do s kład owych klasy, funkcję operator+( ) zde finiujem y jako funkcję s k ładową. Deklaracja tej funkcji w obrębi e defin icji klasy wygląda nast ępuj ąco:
~ Box
operator+(const CBox&aBox) const:
II Funk cj a
dodają ca
dwa obiekty klasy CBox.
Parametr z d e fin i ow al i ś my jako referencję w celu uniknięcia niep otrzebn ego kopiowania pra wego argum entu w momencie wywołania funk cji. Słowo kluczowe const zastosowane zostało ze w zględu na fakt, że funkcja w żade n sposób nie mod yfikuje przyjmowanych argumentów. Jeżeli nie zadeklarujemy param etru j ako s tałej referencji, to kompil ator nie pozwol i na prze kazan ie do funkcj i stałeg o obiektu, co z kolei uniemożliwiłoby zastosowanie jako prawego operandu operatora + stałego obiektu klasy CSox. Fu nkcj ę również zadeklarowaliśmy jako stałą, g dyż nie wpływa ona w ża den sposób na obiekt, dla którego jest wywoływana. Bez tego lewy operand o peratora + nie mógłby być stałym obiektem klasy CSox. Definicja funkcji ope rat or+()
wygląda n astępująco:
II Funkcj a dodająca dwa obiekty klasy CBox .
CBox CBox : :operat or+(const CBox&aBox) const { II Nowy obiekt ma dlugość i
szerokoś ć większego
obiektu oraz
s umę
ich wysokości.
return CBox( m_Length > aBox.m_Length ? m_Lengt h:aBox.m_Lengt h. mWidth > aBox.m Widt h ? mWldt h:aBox.mW i dt h.
Rozdzia. 8. • L. _
~L=30
T
l
r-
. ~ Maksymalna ~ - - dłuqo ś ć "
T :1
boxl
W =20
H
'"
15
~
L = 25
na łemał klas
465
~
box2
W = 25
~=
Więcej
H = 10
"------------"
- - - - · - -1- - - - - - - - -
I--
Maksymalna s ze rokość
, T l ', ---->- W =25
Suma wysoko ści
lL = 30
Y
---J
boxl+box2
,
~ H
, ,,
=15+10
=25 ~~
Rysunek 8.4
Lokalny obiekt klasy CBox konstruujemy z b ieżącego obiektu (*t hi s) i obiektu przekazanego jako argument - aBox. Należy pamiętać, że w procesie zwracania tworzona jest tymczasowa kopia obiektu lokalnego i to ona jest zwracana z powrotem do funkeji wywołuj ącej , a nie obiekt lokalny u sunięty podczas zwrotu funkcji .
RmnlIiI Ćwiczenie dodawania W poniż szym programie zobaczymy, jak działa nasz
p rzeładowany
operator dodawania:
II Cw8_06.cpp II Dodawanie obie któw klasy CBox.
#i nclude usi ng st d: :cout:
us mq st d: :endl :
II Dla strumienia
wejśc ia-wyjścia.
466
Visual C++ 2005. Od podstaw class CBox
II Definicja klasy o zas ięgu g loba lnym.
{
publ r e :
l0
II Defin icj a konstruktora.
CBox( double lv = 1.0. double wv
1.0. double hv
=
1.0): m_Height( hv)
(
mLengt h = lv > Wy? lv: Wy; m)idt h = wv < lv? wv l v:
I
II Fu nkcja
II Upewnienie s ię, że Il length > = width.
obliczająca p ojemność pudełka.
doubl e Vol ume( ) const
{
} II Funkcj a ope ratora większoś ci
II p orównująca pojemności obiektów klasy CBox.
int CBox : :operat or>( const CBox&aBox) const
{
ret urn thi s->Vol ume( ) > aBox. Volume():
}
II Funkcj a porówn ująca obiekt klasy CBox ze stalą .
i nt operat or>(const double&val ue) con st (
ret urn Volume() > value; II Funkcj a dodająca dwa obiekty klasy CBox.
CBox operat or+(eonst CBox& aBox) const ( II Nowy obiekt zloż ony z dłu ż szej dlugosci i sze rokości oraz sumy
wys okości.
ret urn CBox(m_Length > aBox.m_Lengt h7 m_Lengt h:aBox .m_Lengt h. m_Width > aBox .m_Widt h7 m_Widt h:aBox.m_Widt h. m_He ight + aBox.m_Height ): II Fu nkcj a po kazująca wymia ry pudelka.
II Długoś ć pudełka w centymetrach. II Szerokość pudelka w centymetrach. II Wys okość pudelka w centymetrach.
}:
int operat or>(const dauble&va l ue. const CBax&aBox); i nt ma in()
{
II Prototyp funkcji.
RozdziałB.
CBox CBox CBox CBox
•
Więcei na temat klas
467
sma llBox(4.0, 2.0, 1.0) ; med i umBox(10 .0, 4 O. 2.0) ;
aBox:
bBox:
aBox ~ smal l Box + med i umBox;
cout « "Wymi ary obiektu aBox :
aBox.ShowBox() ;
bBox = aBox + smal lBox + mediumBox:
ymiary obiekt u bBox:
cout « "W bBox.ShowBox() ,
ret urn O: II Funkcja porównująca s talą z obiektem klasy
Clłox.
int operato r>(const double&val ue, const CBox&aBox)
{
ret urn val ue > aBox .Vol ume();
}
Do klasy CBox będziem y jeszcze w tym rozdziale wracać kilka razy, a więc warto sobie ten fragment, gdyż będzie on jeszcze potrzebny.
zapamiętać
Jak lo działa Dla potrzeb tego programu zmieniłem nieco składowe klasy CBox. Jako że nie był nam tym razem potrzebny, usunęliśmy destruktor tej klasy oraz zmodyfikowaliśmy konstruktor w taki sposób, że składowa m_Length nie może być mniejsza niż m_Widt h. Dzięki informacji, że dłu gość pudełka obiektu nigdy nie jest mniejsza od jego szerokości , operacja dodawania jest nieco łatwiejsza. Dodałem również funkcję ShowBox() w celu wy świetlenia na ekranie wymiarów obiektu klasy CBox. Dzięki tej funkcji będziemy mogli s i ę zorientować , czy nasz przeładowany operator dodawania działa zgodnie z oczekiwaniami. Rezultat wykonania tego programu jest następujący:
W ymia ry obiektu aBox: 10 4 3 W ymia ry obiekt u bBox: 10 4 6 Dane wyj ś ciowe wskazują, że wszystko się zgadza i - jak widać - funkcja działa również z wielokrotnymi operacjami dodawania w wyrażeniu. W celu obliczenia wymiarów obiektu bBox przeładowany operator dodawania został wywołany dwukrotnie. Tę samą operację
staci funkcji
dodawan ia dla naszej klasy można było również Jej prototyp jest następujący :
zaimplementować
w po
zaprzyjaźnionej .
friend CBox operat or+(const CBox& aBox . const CBox& bBox ); Proces obliczania wyniku byłby dokładnie taki sam, poza koniecznością użycia operatora bez pośredniego dostępu do składowej w celu uzyskania składowych użytych jako oba argumenty funkcji . Podejście to miałoby identyczny skutek jak pierwsza wersja funkcji .
468
Visual C++ 2005. Od podstaw
Przeładowywanie
operatorów inkrementacii idekrementacii
Przed stawi ę
teraz krótk o mechanizm przeł ado wywan ia operatorów inkrementacj i i dekre mentacj i w klasie, poni eważ m aj ą one pewne specjalne wła ś ciwości o dróżn i aj ące j e od innych operatorów jednoargumentowych . Trzeba znaleźć jaki ś sposób na poradzenie sobie z tym, że operatory te mogą występować w dwóch formach (przyrostkowej i przedrostkowej ) i że efekt ich dział ani a w zależn o ś ci od użyt ej formy j est inny. W natywnym C++ implem entacja prze ładow anych operatorów inkrementacji i dekrementacji je st inna dla ka żdej z ich form . Poniżej znaj duje się przykładowy kod d efiniujący te operatory dla klasy o nazwie Length :
class Lengt h {
orivate: doub le len: publi c Lengt h&ooerat or++( ): const Leng t h operator++ (i nt):
II Przedrostkowy operator inkrementacj i. II Przyr ostkowy operator inkrementacj i.
Length&operator --( ) ; con st Lengt h ooerat or--( i nt) :
II Przedrostkowy operator inkrementacji. II Przyrostkowy operator dekrem entacj i.
II D l ugość klasy.
II Reszta kodu klasy...
P owyżs za pro sta kla sa za kł ad a, że długo ść przechowyw ana jest w zmiennej typu doub1e. W rzec zywi stoś ci klasę tę można by było trochę bardziej rozbudowa ć, ale celem p owyższej j est tylko prezentacja sposobu prz eład owyw an ia operatorów inkrementa cji i dekrementacji. Przedrostkow ą i przyrostkową form ę operatorów m ożemy rozróżni ć poprzez l istę parametrów. Operator w fonni e przedrostkowej nie ma żadnyc h parametrów, a w fOJ111ie przyrostkowej ope rator ma param etr typu i nt . Parametr w przyrostkowej fonn ie operatora został zdefiniowany w yłączni e w celu odróżni enia go od operatora w formie przedrostkow ej i nie jest w żade n inny sposób używ any w implementacji funkcj i.
Operatory inkrementacji i dekrement acji w fonni e przedrostkowej zwięks zają lub z mn iej szają operand przed użyciem go w wyrażeniu, a wię c zwracana jest tylko referencja do b ieżąc ego obiektu po jego zwiększeniu lub zmniejszeniu. Przy u życiu formy przyrostkowej operand jest zmniejszany lub zw iększa ny dopiero po użyciu go w wyrażeniu. Efekt ten uzyskuje s ię poprzez utworzenie nowego obiektu, który jest kopią bieżące go obiektu , przed zwiększeniem bie żącego oraz zwr ócenie kopii po zmodyfikowa niu tego ob iektu .
Szablony klas W roz dziale 6. definiow ali śmy szabl on y funk cji , które a.utomatycznie g enerowały funkcje różni ące s ię typem przyjmo wanych argumentów lub typem zwracanym . W C++ istnieje po dobny mechan izm dla klas. Szablon klasy nie jest sam w sobie klasą. Jest czy mś w rodzaju " przepisu" na klasę, w edłu g którego kompilator generuje kod klasy. Jak widać na rysunku 8.5,
Rozdział 8•• Więcej na
T jest parametrem, dla którego
podaje się wartość argumentu, która
jest nazwą typu. Każdy nowy typ
podany w postaci argumentu powoduje utworzenie nowej klasy
wart o ś ć
{
int m_Value;
pa ramet ru T ok reś lo n a ja ko in.
c1ass CExample Wart o ś ć
{
pa rame t ru T
IE-----okr e ślo n a jako double -
T m_Value;
szablon klasy
469
c1ass CExample
template c1ass CExample {
temat klas
Wa r tośl
-
---+I
egzemplarze klasy
double m_Value;
para metr u T okre śl o na jako cSox
'" c1ass CExample { CBox m_Value;
Klasa tworzona jest przy u ży ciu
podanej wa rto ś ci w st awionej w mi ejsce
.«
...---- II
pa ram e tru T w sza b lo nie
~ ~ I~
.. .-•
L-
-'
Rysunek 8.5 podobnie do szablonów funkcji - klasę, którą chcemy utworzyć, okre typ parametru (T w tym przypadku) znajdującego się pomiędzy nawiasami trójkątnymi w szablonie. W wyniku tych czynności powstaje nowa klasa, zwana egzemplarzem szablonu klasy. Proces tworzenia klasy z szablonu nazywa się tworzeniem egzemplarza. szablony klas
działają
ślamy , podając
Właściwa defin icja klasy generowana jest w momencie tworzenia obiektu szablonu klasy dla określonego typu. W zwi ązku z tym można utworzyć dowolną liczbę różnych klas z jed nego szablonu klasy. Najlepiej zrozumiesz to, patrząc na konkretny przykład .
Definiowanie szablonu klasy Definiowanie szablonu klasy przedstawię na bardzo prostym przykładzie. Nie będę kompli rzeczy, martwiąc się zbytnio o błędy, które mogą powstać w przypadku nieprawidło wego jego użycia. Przypu śćmy , że chcemy zdefiniować kilka klas do przechowywania pewnej liczby próbek danych jakiegoś rodzaju i że w każdej funkcji musi znajdowa ć się funkcja Max() określająca maksymalnąwartość przechowywanych danych. Funkcja ta podobna jest do tej, którą widzieliśmy w rozdziale 6., przy okazji omawiania szablonów funkcji . Poniżej znaj duje się definicja szablonu klasy generującego klasę CS arnpl es mogącą przechowywać dane dowolnego typu. kował
t emplate class CSamples (
publ i c:
II Defin icj a konstruktora przyjmującego
tablicę
CSamples( const T values[ J . int caunt)
{
prób ek danych.
470
Visual C++ 2005. Od podstaw m Free = count < 100? count:100: f6r(i nt i = O: i < mJ ree: i +ł ) m_Va l ues [ i] = val ues[i] :
II Nie przekr aczaj rozmiarów tablicy. II Przechowuje liczb ę próbek.
} II Konstruktor przyjmujący pojedynczą p róbkę.
CSamples(const T&va lue) {
m_Values[O] = value: mJree = 1:
II Prz echowuj e próbkę. II Nast ępna jest wolna.
II Defa ult construct or CSamples(){ m_Free ~ O
II Nic nie jest przecho wywane. II a więc pierwsza jest wolna .
II Funkcja dodająca próbkę.
bool Add(const T&va lue) {
bool OK = m_Free < 100: if (OK) m_Val ues[m_Free++] = value: return OK: II Funk cja
II Wskazuj e. że jest wolne miejsce. II Prawda,
w ięc
zap isz
wartość.
spra wdzająca n ajwiększą pró bkę.
T Max( ) const ( II Ustaw pierwszą pró bkę lub Oj ako maks imum.
T t heMax = m_ Free t or rmt i
?
m_Va lues[ OJ
= l: i < mFree: i++) i f (m_Val ues[i ] > t heMax) t heMax = m_Va lues[l] ; ret urn t heMax:
O: II Sprowdź wszystki e pr óbki . II Zapisz
dowolną większą prćbkę .
}
pri vat e:
T m_Values[l OO]: t nt mJ ree:
II Tablica prze ch owują ca dane.
/r Indeks wolnej lokaliza cji
II w tablicy m_Values.
}:
Celem zaznaczenia, że definiujemy szablon klasy, a nie zwykłą klasę, przed słowem kluczo wym cl ass oraz nazwą klasy CSampl es umieściliśmy słowo kluczowe t empl at e oraz parametr typu T w trójkątnych nawiasach. Składnia ta jest identyczna ze składnią, której używaliśmy do definiowania szablonów funkcji w rozdziale 6. Parametr Tjest zmienną typu, która zostaje zastąpiona właściwą nazwą typu podczas deklaracji obiektu klasy . Każde pojawienie się w definicji klasy parametru Tjest zastępowane typem podanym w jej deklaracji. W ten sposób tworzona jest definicja klasy odpowiadająca temu typowi. Można podać dowolny typ (pod stawowy lub klasowy), ale musi on oczywiście mieścić się w granicach rozsądku w kontekście szablonów klas. Każdy typ klasy używany do utworzenia egzemplarza klasy z szablonu musi m ieć zdefiniowane wszystkie operatory, które będą wykorzystywane przez funkcje składowe z takimi obiektami. Jeżeli na przykład dana klasa nie ma zaimplementowanej funkcji ope rator-- t ), to nie będzie działała z szablonem klasy CSamp1es . Do tego jeszcze wrócimy trochę później.
Rozdział8.
•
Więcei na
temat klas
471
Wracając
do przykładu, typ tablicy, w której przechowywane sąpróbki, został określony za symbolu T. Dzięki temu tablica ta będzie przechowywała dane takiego typu, jaki podamy dla symbolu T podczas deklaracji obiektu klasy CSamp l es. Jak widać , parametr typu T użyty został w dwóch konstruktorach klasy oraz w funkcjach Add( ) i Max( ). Każdy egzem plarz tego parametru zostaje zastąpiony podczas tworzenia obiektu klasy za pomocą szablonu . pomocą
obiekt, obiekt z pojedynczą próbką oraz obiekt zainicj alizowany Funkcja Add() pozwala na dodawanie po jednej jednostce do obiektu. Można by było tę funkcję przeładować w celu dodania tablicy jednostek. Szablon klasy zawiera pod stawowy środek zapobiegający przed przekroczeniem pojemności tablicy m values w funkcji Add ( ) oraz w konstruktorze przyjmującym tablicę próbek.
Konstruktory
tworzą pusty
tablicąjednostek.
Jak już wcześniej powiedziałem , teoretycznie można tworzyć obiekty klasy CSamples obsłu gujące wszystkie typy danych : typ i nt, doubl e lub jakikolwiek zdefiniowany przez programistę typ klasowy. W praktyce jednak nie zawsze da się wszystko skompilować i nie zawsze działa to tak, jak przewidywaliśmy. Wszystko zależy od tego, co robi definicja szablonu . Dany sza blon zazwyczaj działa prawidłowo tylko z określonym zestawem typów. Na przykład funkcja Max ( ) wymaga dostępności operatora >, bez względu na przetwarzany typ danych . Jeżeli go nie ma, to programu nie będzie można skompilować. Oczywiście, zazwyczaj będziesz defi niować szablon działający tylko z niektórym i typami, ale nie ma sposobu na wprowadzenie ograniczeń co do typu stosowanego do szablonu.
Funkcje składowe wszablonaełl klas Może się zdarzyć, że
zechcemy umieścić definicję funkcji składowej szablonu klasy poza szablonu. Sposób wykonania tego zadania nie jest wcale oczywisty, a więc przyj rzymy się temu bliżej. Deklarację funkcji w szablonie klasy umieszczamy w normalny sposób. Na przykład: definicją
template
class CSamples
{
II Reszta de lnic 'i sza blonu...
T Max( ) const:
II Funkcja
znajdująca największą próbkę.
II Reszta defini cji szablonu...
Powyższy
kod deklaruje funkcję Max( ) jako składową klasy, ale jej nie definiuje. Teraz musimy oddzielny szablon funkcji dla definicji funkcji składowej. Musimy użyć nazwy sza blonu klasy z parametrami w nawiasach trójkątnych w celu zidentyfikowania szablonu klasy, do której szablon funkcji należy:
utworzyć
template
T CSamples: :Max() const
{
T t heMax
=
m_Val ues[O ];
for( int i = 1: i < mJ ree; i++) if (m_Val ues[i ] > t heMax) t heMax = mVal ues[i] :
II Ustaw pierwsząpró bkęjako maksimum. II Sprawdź wszystkie próbki . II Zapisz
dowolną większą p rćbkę.
472
Visual C++ 2005. Od podstaw ret urn theMax: Składnię szablonu funkcji widzieliśmy już w rozdziale 6. Ze względu na fakt, że ten sza blon funkcji tworzony jest dla funkcji będącej składową szablonu klasy z parametrem T, defi nicja szablonu funkcji powinna m ieć tutaj takie same parametry jak definicja szablonu klasy. W tym przypadku jest tylko jeden parametr (T), ale może ich być więcej. Jeżeli szablon klasy m iałby dwa lub większą liczbę parametrów , to tyle samo miałby szablon definiujący funkcję składową,
N al eży zwró ci ć uwagę , że
nazwa parametru T razem z nazwą klasy zostały umieszczone przed operatorem zasięgu . Jest to konieczne - parametry są podstawą do identyfikacji klasy, do któ rej n al eży funkcja utworzona z szablonu. Podczas tworzenia egzemplarza szablonu klasy ty pem jest CSamp 1es-T> z nazwą typu przypisaną do symbolu T. Podany typ zostaje wstawiony do szablonu klasy w celu wygenerowania definicji klasy oraz do szablonu funkcji w celu wyge nerowania definicji funkcji Ma x() w tej klasie. Każda klasa utworzona z szablonu musi mi e ć własną definicję funkcj i M ax O. Defin iowanie konstruktora lub destruktora poza definicją szablonu klasy wygląda podobnie. Poniżej znajduje się przykładowa defmicja konstruktora przyjmującego t ablicę próbek:
t emplat e CSamples: :CSamplesC T val ues[ ] . i nt count) (
m_Free = count < IDO? count :lOO ;
II Nie przekraczaj rozmiarów tablicy.
tor rt nt i = O: i < mFree : i++ ) m_Values[ i ] = va lues[i ] :
II Zap isz
liczbę próbek.
Klasa, do której należy konstruktor, jest określona w szablonie w podobny sposób jak w przy padku zwykłej funkcji składowej. Warto zwrócić uwagę , że nazwa konstruktora nie wymaga określenia parametru (jest to po prostu CSamp 1es), ale potrzebny jest za to kwalifikator w po staci typu szabl onu klasy CSamp 1es- T>. Parametru z nazwą szablonu klasy używa się tylko przed operatorem zasięgu.
Tworzenie obiektów klasy szablonu funk cji zdefiniowanej za pomocą szablonu funkcji, kompilator potrafi tę z użytych typów argumentów. Parametr typu szablonu funkcji jest nie jawnie zdefiniowany przez określone użycie danej funkcji . Szablony klas są trochę inne. W celu utworzenia obiektu na podstawie szablonu klasy w deklaracji zawsze należy po d a ć parametr typu po nazwie klasy . Kiedy
używamy
funkcję wygenerować
N a przykład obiekt CSamp 1es sposób:
obsługujący
typ doub1e można
zadeklarować
w
następujący
CSamples<double> myDat aCI O.O ) : Powyższy
kod definiuje ob iekt typu CSample s<double>, który może przechowywać jednostki typu doubl e. Obiekt ten po utworzeniu będzie przechowywał jednąjednostkę o wartości 10. O.
Rozdział 8.• Więcei na temat klas
473
~ Tworzenie szablonu klasy Możem y ut w o rzy ć
obiek t z szablonu CSampl es-> przechowujący obiekty klasy CBox. To klasa CBox ma zaimp le mentowaną funkcję operato r>( ) p rzeładowującą ope rator większośc i. Nasz szab lon klas y p rz eć w i c zymy z fun k cją mai n() na po n iższym kodzie:
działa, po nieważ
II Cw8_ 07.cpp
II Używan ie szablonu klasy.
#include
using st d: :cout :
using std: :end l :
II Wstaw tutaj definicję klasy CBox z p rog ramu CwS_0 6.cpp .
II Definicja szab lonu klasy CSamp les.
t emplate class CSamples (
publi c. II Konstruktory
CSamp les( const T va l uesE ] . i nt count i :
CSamples(const T&va lue) :
CSamp les() { m_Free = O; }
bool Add (const T&va lue) : T Max() const : pri vate : T m_ValuesE IOO]: int mJ ree:
II Wstawianie jakiejś warto soi. II Oblic za nie maksimum .
II Tab lica przec h owująca próbki. II Indeks wolnej lokalizacj i w tabli cy m Values .
}: II Defini cja sz ablonu konstruktora przyjmującego
ta blicę próbek.
t emplate CSamples: :CSamples(const T val ues[]. i nt count ) (
m_Free = count < 100? count :l OO: for(i nt i = O: i < m Free: 1++)
m_Va l ues[i ] = va l ues[i ] :
II Nie przekraczaj rozmiaru tablicy .
II Przechowuje licz b ę próbek.
II Kons trukto r przyjmujący pojedynczą jednostkę .
t emp late CSamples: :CSamples(const T&val ue) {
m_Values[O] rnJree = 1;
=
va l ue ;
II Zapi suj e próbkę .
II Następn a j es t wolna.
II Funk cja dodaja ca jednostkę.
templat e bool CSamples : :Add(const T&valuel (
bool OK = m_Free < 100; i HOK) m_Va lues[m_Free++] = value: return OK:
II Wskaz uje, II Prawda.
ż e jest
wię c
woln e miejsce.
zap isz
wartość.
II Funk cj a znajdująca maksymalną jednos tkę .
temp lat e T CSamples: :Max( ) const {
T t heMa x = mFree? mVal ues[O] ; O:
II Usla w pierwszą próbkę lub O jako maksimum.
474
Visual C++ 2005. Od podstaw for (int i = 1; i < mFree: i++) lf(m_Val ueS[l] > theMax) t heMax = m_Val ues[ i] : return t heMax :
II Sprawdź wszystkie p róbki. II Przechowuje dowolną większą p ró bkę.
int ma in() (
CSox boxes[ ]
II Utwórz tablicę z obiektów p udelek.
CBox(8.O. 5.O. 2.O) . II Zainicja lizuj je... CBox(S .O. 4.0. 6.0). CBox( 4.0. 3.0. 3.0) }; II Tworzen ie obiektu CSamp les w celu przechowywania obiektow klasy CBox.
CSamples myBoxes(boxes. si zeof boxes / siz eof CBox); CBox maxBox = myBoxes.Max() : cout « endl « " N a jw ięk sze pud e ł k o ma « ma xBox.Volume()
«
II Pobierz naj większy obiekt II i wyś wie t l j ego pojemność.
p o j emn o ś ć "
endl :
ret urn O; W miejsce komentarza na początku programu należy wstawić definicję klasy CBox z programu Cw8_o6.cpp . Nie musimy przejmować się funkcją operator>( l po zwalającą porównywać obiekty z wartościami typu doubl e, ponieważ ten przykład jej nie wymaga. Wszystkie funkcje składowe szablonu, z wyjątkiem konstruktora domyślnego, zostały zdefiniowane za pomocą odrębny ch szablonów funkcj i w celu pokazania na pelnym przykładzie, jak się to robi. W funkcji mai nO utworzyliśmy tablicę trzech obiektów klasy CBox, którą nast ępnie wykorzy staliśmy do zainicjalizowania obiektu klasy CSa rnp l es mogącego przechowywać obiekty klasy CBox. Definicja obiektu klasy CSamp l es jest w zasadzie taka sama, jak gdyby była to zwykla klasa, ale z dodatkiem parametru typu w trójkątnych nawiasach umieszczonych po nazwie szablonu klasy . Program generuje
następujący
N aj w i ęk sze pud eł ko
ma
wynik:
pojem ność
120
Zauważ, ż e
tworzeniu egzemplarza szablonu klasy nie towarzyszy tworzenie egzemplarza sza blonów funkcji s kła d ow ych . Kompilator tworzy egzemplarze tylko tych szablonów funkcji składowych, które zostaną rzeczywi ście wywołane w programie. W rzeczywistości funkcje te mogą nawet zawierać błędy i dopóki ich nie wywołujemy , kompilator nie zgłosi komunikatu o błędzie . Możemy to sprawdzić na przykładzie. Wprowadź kilka błędów do szablonu funkcj i składowej Add(l . Program nadal się kompiluje i działa bez zarzutów, ponieważ nie jest w nim wywoływana funkcja Add ( l . Możesz spróbować dokonać działo
modyfikacji powyższego programu i sprawdzić, co s i ę przy tworzeniu egzemplarzy klasy przy użyciu szablonu z różnymi typami.
Możesz się zdziwić, widząc,
będzie
co się dzieje po dodaniu instrukcji wyjściowych do konstruk torów klasy. Konstruktor klasy CBox jest wywoływany aż J03 razy! Przyjrzyjmy się. co się
Rozdział 8.
•
Więcej
na temat klas
475
dzieje w funkcji main( J. Najpierw tworzona jes t tablica trzech obiektów klasy CBox, a wię c mamy trzy wywolania. Następnie tworzymy obiekt klasy CSamp 7es w celu ich przechowy wania, ale obiekt klasy CSamp 7es zawiera 100 zmiennych typu CBox, a wię c konstruktor domyślny zostaj e wywolany 100 razy - raz dla każdego elementu tablicy. Oczywiś cie, obiekt maxBox zostanie utworzony przez domyślny konstruktor kopiujący dostarczony przez kompilator.
Szablony klas zwieloma paramelrami Używanie
wielu parametrów typu w szablonie klasy je st prostym rozszerzeniem wcześniej w którym używaliśmy jednego parametru. Każdego z parametrów typu mo żemy używać w dowolnym miejscu definicj i szablonu. Spójrzmy na przykład definicji sza blonu klasy z dwoma parametrami typu : szego
przykładu,
t emplat e
class CExampleClass
{
II Zmienne sk ładowe klasy.
private :
Tl m_Va luel.
T2 m_Va lue2:
II Reszta definicji szabl onu...
}:
Typy obu pokazanych zmiennych składowych klasy są określone przez typy podane w miejsce parametrów w momencie tworzenia egzemplarza obiektu. Parametry w szablonach klas nie są ograniczone do typów. Można także używać parametrów, za które w definicji klasy mają być podstawione stałe lub wyrażenia stałe. W naszym szablonie CSampl es arbitralnie zdefiniowaliśmy tabli cę m_Values zawierającą 100 elementów. Możemy również pozwolić użytkownikowi, aby to on okre ślił rozmiar tablicy podczas tworzenia obiektu. Definicja takiego szablonu przedstawia się następująco:
template class CSamples rivat e : T mVal ues[SizeJ: int mJree: pub l ic:
II Tablica przechowująca pr óbki. II Indeks woln ej loka lizacj i w tablicy m_Valu es.
II Definicja konstruktora przyjmują cego tablicę próbek. CSamples(const T values[J. t nt count ) [
mFree = cou nt < Size? count: Size:
II Nie przekraczaj rozmiar ów tablicy.
for(int i ~ O: i < mJ ree: i ++ ) m_Va l ues [i J = val ues[i J:
II Przechowuj e
II Konstrukt or przyjmują cy pojedynczą próbkę .
CSamples(const T&va l ue)
{
m_Val ues[O J = value:
II Zapisz próbkę.
liczb ę
pr óbek.
476
Visual C++ 2005. Od podstaw m Free = 1: II Konstruktor
II Następnajes t wolna.
domyślny.
CSamples () {
mJ ree = O;
II Nic nie j est p rzecho wywane, a Il jest wolna.
więc p ierwsza
} II Funk cja
dodająca próbkę.
int Add (const T&val ue) int OK = m Free < Size ;
II Wskazuje, że jest wolne miej sce.
lf nie mo żna użyć w wyrażeniu z obiektem typu CSamples<double . 20>. Tworząc
egzemplarze sza blonów, trzeba zachować szc ze gó l ną ostrożność przy z u życiem operatorów porównywani a. Przyjrzyjmy s i ę poniżs zej instrukcji:
CSamples y ? 10
20 > M yType( );
II Źle'
wyrażeniach
Rozdział 8.• Więcej na
temat klas
477
Powyższego kodu nie będzie można poprawnie skompilować , gdyż znajdujący się przed y znak> jest traktowany jako zamykający trójkątny nawias. Instrukcja to powinna zostać zapi sana następująco:
CSamples y ? 10
20) > MyType();
II Dobrze...
Dzięki okrągłym nawiasom unikniemy pomieszania wyrażenia będącego drugim argumentem szablonu z nawiasem trójkątnym.
Używanie klas Omówiliśmy wszystkie podstawowe zagadnienia związane z tworzeniem klas w natywnym C++, dobrze by było, gdybyśmy teraz nauczyli się je stosować do rozwiązywania problemów programistycznych. Przykład, którym się posłużę, będzi e bardzo prosty, aby utrzymać rozsądną liczbę stron w tej książce . W związku z tym posłużę się rozszerzoną wersją klasy CBox.
Interfejs klasy Implementacja rozszerzonej wersji klasy CBox powinna zbiec się z wyjaśnieniem pojęcia inter fejsu klasy. Planujemy utworzyć zestaw narzędzi dla każdego, kto chce pracować na obiek tach klasy CBox, a więc musimy stworzyć zestaw funkcji, który będzie stanowił interfejs do świata "pudełek" . Jako że interfejs jest jedynym sposobem pracy z obiektami klasy CBox, musi on zostać zaprojektowany w taki sposób, aby jak najlepiej odpowiadał na potrzeby tego, kto chce z nimi pracować . Jego implementacja powinna zapewniać maksymalną o chronę przed nieprawidłowym użyciem klasy lub przypadkowymi błędami . Pierwszą rzeczą,
chcem y
nad którą należy się zastanowić, projektując klasę , jest natura problemu, który Na podstawie tych przemyśleń należy zdecydować , jaką funkcjonalnoś ć w interfejsie klasy.
rozwiązać .
zapewnić
Definiowanie problemu Główną funkcją pudełka
jest przechowywanie obiektów różnego rodzaju , a więc krótko pakowanie. Utworzymy klasę , która złagodzi problem y z pakowaniem, a następ nie zobaczymy, w jaki sposób można jej użyć . Zakład amy , że zawsze będziemy pakować obiekty klasy CBox do innych obiektów klasy CBox, ponieważ j eż e li chcemy zapakować do pudełka cukierki, to każdy cukierek można przedstawić jako idealny obiekt klasy CBox. Pod stawowe operacje, które nasza klasa powinna wykonywać, to:
mówiąc -
• Obliczanie pojemności pudełka. Jest to podstawowa właściwość obiektu klasy CBox i mamy już jej implementację . • Porównywanie pojemności dwóch obiektów klasy CBox w celu określenia, który jest większy. Dobrze by było zaimplementować pełną obsługę operatorów porównywania obiektów klasy CBox. Do tej pory mamy implementację operatora >.
478
Visual C++ 2005. Od podslaw • Porównywanie pojemności obiektu klasy CBox z określoną warto ścią i odwrotnie. Mamy już taką operację zaimplementowanądla operatora >, ale będziemy potrzebować także obsługi pozostałych operatorów porównywania. • Dodawanie dwóch obiektów klasy CBex, w wyniku czego powstanie nowy obiekt tej klasy zawierający oba obiekty składowe . W wyniku tej operacji powstanie obiekt o pojemności co najmniej równej sumie pojemno ści s kł ad o wy c h obiektów. Mamy już wersję , która przeładowuje operator +. • Mnożenie obiektu klasy CBex przez liczbę całkowitą i odwrotnie, w wyniku czego powstanie nowy obiekt zawierający określoną liczbę obiektów. To już jest projektowanie pudła. •
Okraś lenie , ile obiektów klasy CBex danego rozmiaru można upakować w innym obiekcie klasy CBex o podanym rozmiarze. Do wykonania tego trzeba po służyć się operatorem dzielenia , a więc będziemy musieli przeładować operator /.
•
Określanie pojemności
wolnego miejsca pozostałego po upakowaniu maksymalnej liczby obiektów klasy CBex o danym rozmiarze.
Lepiej już się zatrzymam! Bez wątpieniajest wiele innych bardzo pożytecznych funkcji, ale w dobrze pojętym interesie drzew poprzestaniemy na tych, które wymieniłem . Oczywiście będziemy jeszcze potrzebować różnych funkcji pomocniczych, takich jak uzyskiwanie dostępu do wymiarów pudełek.
Implementacja klasy wziąć pod uwagę, jaki poziom bezpieczeństwa chcemy zapewnić naszej klasie przed rodzaju błędami . Podstawowa klasa zdefiniowana w celu ilustracji różnych aspektów klasy może posłużyć jako punkt wyjścia, ale niektóre punkty musimy przemy śleć dokładniej . Konstruktor klasy ma taką słabość , że nie sprawdza, czy podane wymiary obiektu są prawidło we, a więc na początek dobrze by było dodać sprawdzanie, czy mamy prawidłowe obiekty. Podstawową klasę możemy ponownie zdefiniować w następujący sposób:
Musimy różnego
class CBox
II Definicj a klasy o z as ięgu globalnym.
(
publ ic: II Definicja konstruktora.
CBox(doub1e 1v ; 1.0. doub1e wv ; 1.0. double hv
~
1.0)
(
1v ; 1v wv ~ wv hv ~ hv
=, ==, < oraz =< W taki sposób, aby dział ały one przy z oboma operandami w postaci klasy CBox, jak również gdy jeden operand jest obiek tem, a drugi wartością typu doubl e. Możemy je zaimplementowaćjako zwykłe funk cje glo balne, ponieważ nie muszą one być funkcjami składowymi. Funkcje porównujące pojemność dwóch obiektów klasy CBox możemy napi sać , opierając s i ę na funkcji porównującej pojemność obiektu klasy CBox z wartością typu doubl e, a więc zaczniemy od tej drugiej . Na początku możemy powtórzyć funkcję operator>(), którą napisaliśmy wcześniej; użyciu
II Funk cja
spra wdzająca.
czy
stała jest
>
niż
obiek t klasy CBox.
i nt operat or>(const dauble&value. const CBox&aBox) (
ret urn va l ue > aBox,Val ume() ,
W podobny sposób II Funkcja
możemy teraz napisać fu nkcj ę
sprawdzająca,
czy
sta ła j est
aBox; } W razie zmiany kolejności argumentów w przeładowaną na j ed n ą z powyższych .
wywołaniu
nowej funkcji zmieniamy tylko
funkcj ę
Funk cje impl em entujące operato ry >= i należy wstawić =. Nie ma potrzeby p rzepisywać ich tutaj ponownie . Funk cje oper at or==() również są bardz o podobne: II Funkcja sprawdzająca, czy
i nt
op e rato r ~ ~(const
s ta ła
równa j est poj emnosci obiektu klasy CBox.
double& va lue. const CBox&aBox)
(
retu rn val ue == aBox.Vo l ume (); } II Funkcja sprawdzająca, czy obiekt klasy CBox j est równy stalej.
i nt ope rator ==(const CBox& aBox . const double&value)
{
ret urn value == aBox;
}
W ten sposób mamy
pełny
zestaw operatorów porównywania dla obiektów klasy CBox. Należy tylko wtedy, gdy wyrażenia dają wyniki właściwego typu, ich używać także z innymi przeładowanymi operatorami.
pamiętać , że działają one praw idłowo dzięki
czemu
można
~ączenie obiektów klasy CBOK Zajmiemy się teraz kwestią operatorów +, * oraz %w podanej kolejności. Prototyp operacji dodawania, którą utworzyliśmy w program ie CwS_06.cpp, wygląda nastepująco :
CBox operator+(const CBox& aBox);
II Funkcja dodająca dwa obiekty klasy Cllox.
Mimo że oryginalna implementacja nie jest idealnym rozwiązaniem, to użyjemy jej tutaj, aby uniknąć nadmiernej komplikacji klasy . Lepsza wersja sprawdzałaby, czy operand y nie mają którejś ze ścian o takich samych wym iarach , oraz łączyłaby je właśnie tymi śc ia nami , ale kod takiej operacji byłby trochę skomplikowany. Oczywiście, gdyby miał to być program o praktycznym zastosowaniu, to obe cną wersję tej operacji można by było p óźni ej za stąpić jej ulepszoną wersją, a programy używające tej oryginalnej nadal działałyby, bez kon ieczn ości dokonywania w nich jakichkolwiek zmian . Oddzielenie interfejsu do klasy od jej implemen tacji jest wyznacznikiem dobrego stylu programowania w C++ . Zauważ, że
nie
miało
ze względu na wygodę pominąłem operator odejmowania. To roztropne przeocze miejsce, aby uniknąć komplikacji związanych z implementacją tego operatora. Je żeli
Rozdział 8.• Więcei na temat klas
481
n aprawdę
masz o c h o tę i wydaje Ci si ę to rozs ądnym pomy słem, mo żes z dać mu szansę - ale musisz podj ąć decyzj ę , co zro b ić w przypadku, gdy wynik będzie liczb ą uj emną. J eżeli pozwo lisz na wykon ywani e takic h dzi ałań , musisz sp raw dz ić, któr y wym iar lub które wym iary będ ą ujemne i co zro b ić z takim obiektem w dalszych operacjach.
Operacja mnożenia jest bardzo prosta. Pole ga na utwor zeniu obiektu zaw i e r ającego n obiek tów, gdz ie n jest mnożn iki em. Najpros tszym rozwi ązani em byłoby zapakowanie zmiennych m_Lengt h i m_Wi dt h obiektu oraz p omn ożeni e wys o ko śc i przez n w celu otrzymania now ego obiektu klasy CBox. Możemy być jednak sprytniejsi i sprawdzi ć, czy mnożnik jest li c zbą parzy s tą, a j eżeli je st, u stawić pudełka jedno obok drugiego, podwajając wartoś ć zmienn ej m_Wi dt h oraz mnożąc warto ś ć zmiennej m_Hei ght przez połowę n. Mechanizm ten został przedstawiony na rysunku 8.6. Oc zywi ście ,
nie musimy s p rawdzać , co w nowym obiekcie jest większe - długo ś ć czy szero konstruktor automatyc znie to sprawdzi. Fu nkcj ę operato r*( ) możemy zapi s ać jako fu nkcj ę s kł ad o w ą z lewym operandem w postaci obiektu klasy CBox: kość , p onieważ
II Operator m nożen ia klasy CBox this*n
CBox operator *(int n) const { i f ( n % 2)
return CBox(m_Lengt h. m~Wi d t h. n*m_Height) ; else return CBox(m_Lengt h. 2.0*m_Width. (n/2)*m_Height ):
II Mnożn ik n j est nieparzysty. II Mno ż nik n j est parzysty.
W powyższym kodzie do sprawdzania par zyst oś ci mn o żnika n u żyl i śm y operatora %. J eżeli n je st nieparzysty, to wynik d ziałan i a n % 2 wynosi l i instrukcja warunko wa if zwraca war to ść t r ue. Je ż eli n jest parzysty, to wynik dział ania n % w wynosi O i instrukcja zwraca war tość f a1se. Mo żemy
tera z wła śnie napisanej funkcji uży ć do implementacji wersji z lewym operan dem w postaci liczby całkowitej. Możemy ją zap is ać jako zwykłą funkcję (a niejako funk cję s kład ową):
II Operator mnożenia klasy CBox n *aBox.
CBox operator*(i nt n. const CBox& aBox) {
ret urn aBox*n; tylko kolejno ś ć operand ów, dzięki czemu mogli jej poprzedniej wersji. Na tym kończy się zestaw operatorów aryt metyczn ych dla klasy CBox, które zdefiniowali śmy . Na koni ec zajmiemy się jesz cze dw oma analityc znymi funkcjami operatorów - operat or I ( ) oraz oper at or%( ) .
Y Analizowanie obiektów klasy CBol Jak już powiedziałem, operacja dzielenia określa liczbę obiektów klasy CBox, identyc znych z obiektem podanym jako prawy operand, który może pomieścić obiekt klasy CBox podany jako lewy operand . Dla zachowania prostoty zakładamy, że wszystkie obiekty klasy CBox są usta wione w pozycji pionowej. Dodatkowo zakładamy, ż e wszystkie te obiekty ustawione są w taki sposób , że ich długości występują w jednej linii . Bez tych założeń kod byłby bardzo skomplikowany. Powstaje problem określen ia , ile obiektów znajdujących s i ę po prawej stronie operatora można umieścić na jednej warstw ie, a następnie określenia, ile warstw możemy uzyskać w obiekcie znajdującym się po lewej stronie operatora. Kod ten
możemy zapisać
w postaci funkcji
składowej :
Rozdział 8.
•
Więcei na temat klas
483
i nt operato r/ Ceonst CBox& aBox) {
i nt tel = O;
II Zmienna tymczasowa przech owująca liczbę w płaszczyźnie poziomej II w j eden sposób, II Zmie nna tymczaso wa przecho wująca liczbę w płaszczyźnie II w drugi sp osób ,
i nt t eZ = O;
t el = statie_east C (m_Lengt h / aBox,m_Length ))* st at le_east CCm_Widt h / aBox,m_Wi dt h)) ; t eZ = stat le_east C Cm_Length st at ie_east C Cm_Wi dth
II Dopasowanie II wjeden sposób
aBox,m_Widt hJ)* aBox.m_Length)) ;
II i w drug i sposób.
II Zwraca najlepsze dopa sowanie.
ret urn sta t ie_eastC Cm_Height/ aBox ,m_HeightJ*C te l>teZ ? t el
t eZ)) ;
Powyższa
funkcja sprawdza, ile obiektów klasy CBox podanych po prawej stronie operatora na warstwie , wyrównując je względem długości obiektu podanego jako lewy operand. Wynik zapisywany jest do zmienn ej t el. Następnie sprawdzane jest, ile obiektów zmie ści się na warstwie, na której obiekty z prawej strony operatora zo staną umieszczone wzdłuż boku stanowiące go szerokość obiektu podanego jako lewy operand. Na koniec war tość większej z dwóch zmiennych t el i t eZ mnożymy przez liczbę warstw, które możemy upakować , i zwracam y wynik . Proces ten został przedstawiony na rysunku 8.7. można umieścić
\
1\
aBox
W=6
[::l 5olutiOn 'ewa_os' 8 .] a ewa_oa 8
x
m
(l project)
~ Header Files
[!li Box.h
l!lI
BoxOperators.h
L:I Resource Flles 8
(C) sourre Files
~ Box.cpp
'8 BoxOperators ,cpp ~ CWS_OS .cpp
'§!Soiut!
B
GlB CWO_OO .. Glaba I Functions and Vambles . ~ Macros and Constants
''!s m . " "-CBaK('O ,d) ~ ... CBoK(dauble Iv = 1.OOOJOO, doub le wv '.. Ge1Height (void) Ge1Length (vald) " GeIWidth (void) '.. Volume (vo id)
L
~ operator + (void)
: .ił m_Height .fi
.#
_
m_Length m_Width
II Dla funkcji strlent) i strcpy i).
#incl ude "CandyBox.h" II Dla klas CBox i CCandyBox.
using st d: :cout :
usi ng std : :endl :
int main( ) (
CBox myBox(4.0,3.0 .2.0) ; II Tworzenie CCandyBox myCandyBox:
CCandyBox myM i ntBo x ( "M i ętówki cienkie j ak l istek" ):
obiektu klasy CBox .
II Tworzenie ob iektu klasy
IICCandy Box.
cout « endl « "Obiekt myBox zajmuje " « S i zeof myBox II Pokazuje, ile pamię ci « " ba j ty" « endl II wymagają te obiekty. « "Obiekt m yCandyBox zajmuje " « sizeof myCandyBox « " ba jt y" « end l « "Obiekt myMintBox zajmuje" « si zeof myMintBox « " ba j ty";
523
524
Visual C++ 2005. Od podstaw cout
« «
end l "P o j emno ś ć
obiekt u myM intBox wynos i
«
myM int Box.Vol uneO : II Sprawdzanie II poj emnoś c i obiektu klasy CCandyBox.
cout « end1: retu rn O: Rezultat
działania powyższego
programu jest następujący:
Obiekt myBox zajmuje 24 bajt y Obiekt myCandyBox zajmuje 32 baj ty Obiekt myMint Box zaj muje 32 baj t y POJemno ś ć obiektu m yM i ntBox wynosi
Jak to działa Interesujące
dodatkowe dane na wyjściu znajdują się w ostatnim wierszu. Pokazuje on war przez funkcję Vo l ume() znajdującą s i ę teraz w sekcji publicznej klasy bazowej. Wewnątrz funkcji pochodnej operuj e ona na składowych tej klasy , które zostały odziedzi czone z klasy bazowej. Jest ona pełną składową klasy pochodnej , a więc można jej swobodnie używać z obiektami tej klasy . tość obliczoną
Pojemność
obiektu klasy pochodnej wynosi 1, ponieważ podczas tworzenia obiektu klasy CCandyBox najpierw został wywołany konstruktor domyślny CBox() (w celu utworzenia częś ci obiektu złożonej z klasy bazowej), który ustawia domyślnie wymiary obiektów klasy CBox na 1.
Działanie konstruktora
wklasie pochodnej
tak jak powiedziałem, konstruktory klasy bazowej nie są dziedziczone przez klasę to są one nadal obecne w klasie bazow ej i za ich pomocą tworzona jest ta częś ć obiektu klasy pochodnej, która zawiera składowe klasy bazowej . Dzieje się tak , poni ewa ż utworzenie bazowej c zęści obiektu klasy pochodnej należy do konstruktora klasy bazowej, a nie do konstruktora klasy pochodnej. Dodatkowo widzieliśmy już, że prywatne składowe klasy bazowej są niedostępne w obiekcie klasy pochodnej, mimo że są dziedziczone, a więc odpowiedzialność za nie spada na konstruktory klasy bazowej. Mimo
że ,
pochodną,
Domyślny
konstruktor klasy bazowej został w ostatnim przykładzie wywołany automatycznie w celu utworzenia bazowej części obiektu klasy pochodnej, ale nie musi się to odbywać w ten sposób. Możemy spowodować wywołanie określonego konstruktora klasy bazowej z konstruk tora klasy pochodnej. Dzięki temu możem y zainicjalizować zmienne składowe klasy bazo wej za pomocą konstruktora innego niż domyślny lub wybra ć konkretny konstruktor klasy, w zależności od danych przekazanych do konstruktora klasy pochodnej .
~ WYWOłYW811ie konslruklorów Jak to działa, prześledzimy na zmodyfikowanej wersji poprzedniego przykładu. Aby klasa po chodna była użyteczna, musimy dostarczyć dla niej konstruktor pozwalający na określenie wym iarów obiektu. W tym celu możemy dodać do klasy pochodnej dodatkowy konstruktor
Rozdział 9.
• Dziedziczenie i funkcje wirtualne
i jawnie w ywoł ać konstruktor klasy bazowej w celu ustawienia wych dziedziczonych z klasy bazowej. Plik nagłówkowy Box.h w projekc ie Cw9_03 ma II Plik
nagłó wkowy
wartości
zmiennych
następującą zawarto ść :
Box.h w proj ekcie Cw9 03
#pragma once
#include
using std : :cout :
using std : :endl :
class CBox (
pub l ic : II Konstruktor klasy bazo wej.
CBoxC double l v ~ 1.0. doubl e wv = 1.0. double hv = 1.0):
mLengt hCl v) , mWidt h(wv ), mHeightChv )
( cout« endl « "Konst rukt or klasy CBox z o st a ł-wy wo ł any . " ;
II Funkcja
ob liczająca pojemność
obiektu klasy CBox.
double Vol umeC ) const
{ ret urn m_Lengt h*m_Wldt h*m_Height :
pri vat e:
doubl e m_Length :
double m_Width:
double m_Height:
}: Zawartość
pliku
nagłówkowego
CandyBox.h proj ektu Cw9_03 j est
następująca :
II Plik n agłówkowy CandyB ox.h proj ektu Cw9 03.
#pragma once
#incl ude
#incl ude "Box.h"
usi ng st d: :cout :
usi ng st d:: endl : class CCandyBox: public CBox (
publ ic:
char* m_Conte nts :
II Konstrukt or usta wiający wymiary i zawart ość
II za pom ocąjawnego wywołania konstrukt ora klasy CBox.
CCandyBox(double lv , double
WV .
double hv. cha r* st r = "Cukierek" )
:CBox(lv. WV , hv)
cout « endl « "Konst rukt or constru ct or2 klasy CCandyBox m_Contents = new char[ str len(st r ) + l J:
strcpy_s(m_Cont ents. str len(m_Cont ents), st r) ;
II Konstruktor ustaw iaj acy zawa rtość
II automatycznie wywo łuje domyślny konstrukt or klasy CBox.
CCandyBox(char* st r = "Cukie rek" )
zo s ta ł wywołany. " :
525 składo
526
Visual C++ 2005. Od podstaw
cout « endl « "Konst rukt or const ructorl klasy CCa ndyBox m_Cont ent s ~ new char[ str len(st r) + l ];
st rcpy_s(m_Conte nts . strlen(m_Contents), st r) ;
,
z o s t ał wywo ł a n y .
}
-CCandyBox( ) { delete[] m_Cont ents ;
II Destrukt or.
};
Dyrektywa #incl ude dołączająca plik nagłówkowy oraz dwie deklaracje using nie są tutaj niezbędne, ponieważ plik nagłówkowy Box.h zawiera ten sam kod. Umieszczenie tych instrukcji w tym miejscu oznacza, że gdyb yśmy usunęli je z pliku nagłówkowego Box.h , ponieważ nie byłyby tam już potrzebne , to nadal nie mielibyśmy żad nych kłopotów z kompi l acj ąpliku CandyBox .h. Zawartość
pliku
źródłowego
Cw9_03.cpp je st następuj ąca :
II Cw9_ 03,cpp II Wywoływanie konstruktora kłasy bazowej za pom ocą konstruktora klasy po chodnej.
#1ncl ude II Dla operacji wejśc ia-wyjśc ia.
#i nc l ude II Dla fu nkcj i strlent) i strcpy() .
#include "Candy Box .h" II Dla klas CBox i CCandyBox,
using st d; :cout ;
using st d: :endl ;
int mai n( ) {
CBox myBox(4.0, 3.0. 2,0);
CCandyBox myCandyBox;
CCandyBox myMi ntBox(1.0. 2,0. 3.0.
"M i ętówk i
cienkie j ak l i st ek,");
cout « « « « « « « cout « «
endl "Obiekt myBox zajmuje " « sizeof myBox II Pokazuje, ile pamięci .. ba jt y" « endl II zajmują te obiekty. "Obiekt myCandyBox zajmuje .. « S izeof myCandyBox .. ba jty" « endl "Obiekt myMint Box zajmuje " « sizeof myMint Box .. bajty"; endl "Po j em n o ś ć obiekt u m yMint Box wynosi " II Sp rawdzanie pojemnoś ci « myMintBox,Volume(); II obiektu klasy CCandyBox. cout « endl ;
ret urn O;
Jak to lJziała Poza dodatkowym konstruktorem w klasie pochodnej, do każdego konstruktora dodali śmy instrukcję wyjściow ą, dzięki czemu wiemy, kiedy są one wywoływane. Jawne wywołan ie konstruktora klasy CBox znajduj e s ię po średniku w nagłówku funkcji konstruktora klasy pochodnej. Jak możn a zauw ażyć , notacja ta jest identyczna z notacją używaną do inicjali zacj i składowych w konstrukt orze:
Rozdział 9.•
II
Wywoływan ie
Dziedziczenie i funkcie wirtualne
527
konstruktora klasy bazow ej.
CCandyBox(double lv. double wv . doub le hv, cha r* st r= "Candy" ): CBox(lv . wv . hv )
Jest to w pełni zgodne z tym, co robimy tutaj, ponieważ w zasadzie inicjalizujemy podobiekt klasy pochodnej klasy CBox. W pierwszym przypadku jawnie wywołujemy konstruktor do myślny dla składowych typu doubl e znajdujących się na liście inicjalizacyjnej : n1_Length, m_Width oraz m_Hei ght. W drugim przypadku wywołujemy konstruktor klasy CBox. Powoduje to wywołanie wybranego konstruktora klasy CBox przed wywołaniem operatora klasy CCandyBox. Rezultat wykonania tego programu jest następujący:
Konstrukto r klasy CBox zo s t a ł wywoła ny .
Konst ruktor klasy CBox zo s t a ł wywo ła ny .
Konst ruktor const ructor l klasy CCandyBox Konst ruktor klasy CBox zo s ta ł wywo ł a ny .
Konstruk tor con st ruct or2 klasy CCandyBox Obie kt myBox zajmuj e 24 bajty
Obiekt myCa ndyBox zajmuj e 32 bajty
Obiekt myMint Box zajmu je 32 bajt y
P Ojemn o ś ć obi ekt u myM i ntBox wynosl 6
Wywołania
konstruktorów
objaśniono
w
zos t ał wywo ła ny.
zo s t a ł wywo ła ny .
poniższej
Dane na ekranie
tabeli:
Tworzony obiekt
Konstruktor klasy CBox został
wyw ołany .
myBox
Konstruktor klasy CBox został
wywołany.
myCandyBox
Konstruktor const ructorl klasy CCandyBox został Konstruktor klasy CBox zo stał
wywołan y.
myM i nt Box
wyw ołan y .
Konstruktor const ruct or2 klasyCCandyBox został
myCandyBox
wyw ołan y.
myMlntBox
Pierwszy wiersz danych wyjściowych otrzymaliśmy dzięki wywołaniu konstruktora klasy pochodzącego od deklaracji obiektu klasy CBox o nazwie myBox. Drugi wiersz danych wyjś ciowych powstał w wyniku automatycznego wywołania konstruktora klasy bazowej, spo wodowanego deklaracją obiektu klasy CCandyBox o nazwie myCandyBox.
CBox,
Zauważ, że
konstruktor klasy bazowej j est wywoo/wany zawsze przed konstruktorem klasy pochodnej.
Następny
powstał dz ięki naszej wersji domyślnego konstruktora klasy pochodnej obiektu myCa ndyBox. Konstruktor ten został wywołany , gdyż obiekt nie jest zainicjalizowany. Czwarty wiersz danych wyjściowy ch powstał w wyniku jawnego wywo łan ia konstruktora klasy CBox w naszym nowym konstruktorze dla obiektów klasy CCandyBox. Wartości argumentów określających wym iary obiektu klasy CCandyBox zostały przekazane do konstruktora klasy bazowej. Dalej mamy wiersz wysłany na wyjście przez sam konstruktor nowej klasy pochodnej, a więc ponownie wywoływane są konstruktory klasy bazowej, po czym następuje wywołani e konstruktora klasy pochodnej.
wiersz
wywołanego dla
528
Visual C++ 2005. Od podstaw Z tego, co widzieli śmy do tej pory, powinno być jasne, że każdemu wywołaniu konstruktora klasy pochodnej towarzyszy wywołanie konstruktora klasy bazowej w celu utworzenia bazo wej częśc i obiektu klasy pochodnej . J eżeli nie określimy konstruktora klasy bazowej, który ma zostać użyty, kompil ator wywoła konstruktor domyślny klasy bazowej . Ostatni wiersz tabeli pokazuje, że inicjalizacja bazowej częś ci obiektu myMi nt Box działa pra widłowo - prywatne składowe zostały zain icjalizowane przez konstruktor klasy CBox. Posiadanie prywatnych składowych klasy bazowej dostępnych tylko dla funkcji składowych klasy bazowej nie zawsze jest wygodne . Może si ę zdarzyć, że będziemy chcieli mieć prywatne składowe klasy bazowej , do który ch dostęp można uzyskać z wnętrza klasy pochodnej. Jak nietrudno się domyślić, w C++ istnieje taka możliwość .
Deklarowanie chronionych składowych klasy dostępu do składowych klasy pub l ic i pri vat e istnieje jeszc ze jeden prot ected. Wewnątrz klasy słowo kluczowe prot ect ed ma taki sam efekt jak słowo kluczowe pri vate : do chronionych (protec ted) składowych klasy dostęp można uzyskać wyłącznie za
Poza specyfikatorami
pomocą
funkcji s kł ad o wy c h tej samej klasy (a także funkcji składowych klasy zaprzyjaź o klasach zaprzyjaźnionych będzie mowa w dalszej części tego rozdzi ału) . Przy użyciu słowa kluczowego protect ed definicję klasy CBox moglibyśmy ponownie zdefiniować w następujący sposób :
nionej -
II Plik
nagłówkowy
Box.h w proj ekcie Cw9 04,
#pragma once
#i ncl ude
using std : :cout:
using st d: :endl :
class CBox
{
puhl t c :
II Konstruktor klasy bazowej.
CBox(doub le l v = 1.0. double wv = 1,0. double hv = l O): m_Length(l v) . m_Width(wv). m_He ight (hv ) { cout« endl « "Konstru ktor klasy CBox zos ta ł wywo ł a ny , ": II Destruktor klasy CBox - do sledzenia
wywołań,
{ cout « "Destruktor klasy CBox
zo s ta ł wywo ła ny"
-csc« )
« endl: }
protected:
double m Lengt h:
double m)idth;
doub le m_Height :
}:
Od tej pory zmienne składowe są nadal prywatne , ponieważ nie mają do nich dostępu funkcje globalne, ale s ą one dostępne dla funkcji skł ado wych klasy pochodnej .
zwykłe
Rozdzial9. • Dziedziczenie i funkcie wirtualne
529
Rm!I!mI Zastosowanie chronionych składowych Zastosowaniu chro nionych zmiennych składowyc h przyj rzymy si ę dokładniej , używając tej wersji klasy CBox do utworzenia nowej wersji klasy CCandyBox, która bę dzi e uzys kiwała dostęp do składowych klasy bazowej poprzez włas ną fu nkcję składową Vol uret ): II Plik
nagl ćwkowy
CandyBox. h w projekci e Cw9 04.
#pragma ance
#include "Box.h"
#include
us ing st d: :cout ;
using st d: :endl ;
class CCandyBox : publ ic CBox
{
publ i c:
cha r* m_Cont ent s ;
II Funkcja klasy pochodnej
obliczająca pojemność.
double Volume() const
{ retu rn mLengt h*mWldth*mHeight ; }
II Kons trukto r ustawiają cy wym iary i zawartość
II za pomocąjawnego wywołania konstruktora klasy CBox .
CCa ndyBox(doubl e l v. doub le wv. doub le hv. cha r* st r ~ "Ca ndy ")
:CBox(l v. wv . hv) II Konstruktor. cout « endl « "Konstrukt or construct or2 klasy CCandyBox zosta l m_Cont ent s = new cha r[ str len(str) + l ] ;
st rcpy_s(m_Contents . strl en(m_Cont ent s ). str);
wywo ła ny . ";
II Konstruktor ustawiający zawartość
II aut omatycznie wywoluje domyślny konstrukt or klasy CBox.
CCandyBox(char* str ~ "Cukierek") II Konstruktor.
{
cout « endl « "Konstrukt or const ruct orl klasy CCa ndyBox m_Content s = new char[ st rlen(st r) + l ] ;
cout « "Destruktor klasy CCa ndyBox zast al delet e[] m_Cont ents .
wywoła n y . "
« endl ;
};
Kod funkcji mai n() projektu ew9_04 wyg ląda jak p on i żej : II Cw9_04.cpp
II Używanie specyfikatora
#incl ude #incl ude #incl ude "CandyBox h"
dostępu protected.
II Dla strum ienia wejścia -wyjś cia .
II Dla funkcj i strlen() i strcpy ().
II Dla klas CBox i CCandyBox.
530
Visual C++ 2005. Od podstaw usi ng st d: :cout :
usi ng st d: :end l :
int ma in() {
CCandyBox myCandyBox :
CCa ndyBox myToff eeBox(2. 3. 4.
" Ciągną c e s i ę
toff i "):
cout « endl « "Poj emn o ś ć obiektu myCandyBox wynosi " « myCa ndyBox.Vo l ume() « endl « "P o j emn o ś ć obiektu rnyToffeeBox wynosi" « rnyToff eeBox.Vol ume() : II coul < < endl < < my ToffeeBox.m~englh; II Usunięci e komentarza z lego wiersza spowoduje
bląd.
cout « endl :
ret urn O:
Jak to działa Powyższy
program oblicza pojemność dwóch obiektów klasy CCandyBox poprzez wywołanie funkcj i Vol ume( ), która jest składową klasy pochodnej . Funkcja ta wykonuje obliczenia dzięki uzyskaniu dostępu do odziedziczonych zmiennych składowych m_Length, m_Wi dt h, m_Height. Składowe w klasie bazowej zostały zadeklarowane jako chronione i takie też są w klasie po chodnej. Rezultat działania tego programu widoczny jest poniżej :
Konst ruktor klasy CBox zos t a ł wywo ł any .
Konst ruk tor const ructorl klasy CCandyBox zos ta ł Konstruk tor klasy CBox z os t a ł wywo ł any .
Konst ruktor const ructor2 klasy CCandyBox z os t a ł PO j emn o ś ć obiekt u rnyCandyBox w ynosi 1
P o j emno ś ć obiekt u rnyToffeeBox w ynosi 24
Destrukt or klasy CCandyBox z o s ta ł wywoł a ny .
Dest ruktor klasy CBox z o st ał wyw o ł a n y.
Destrukt or klasy CCandyBox z o s t a ł wywo ła ny.
Destruktor klasy CBox zo s t a ł wywo ł any .
wywo ł a ny .
wywoł a ny .
Z danych na ekranie wynika, że pojemność obu obiektów klasy CCandyBox została obliczona prawidłowo. Pierwszy obiekt ma domyślne wymiary ustawione przez domyślny konstruktor klasy CBox, a więc jego pojemność wynosi I. Drugi obiekt ma wymiary zdefiniowane jako wartości początkowe w jego deklaracji. Na
wyjściu
pokazana została także kolejność wywołań konstruktora i destruktora, dzięki czemu jak każdy obiekt klasy pochodnej jest niszczony w dwóch etapach.
możn a prześledzić,
Destroktory obiektu klasy pochodnej >tyWorywane są w odwrotnej kolejności w stosunku do konstruktorów tego obiektu. Ta ogólna zasada ma zastosowanie zawsze. Najp ierw wyworywany jest konstrukt or klasy bazowej, a nast ępn ie konstruktor klasy po chodnej. Przy niszczeniu obiektu natomiast pierwszy >tyWo~vany j est destruktor klasy pochodnej, a dopiero po nim destroktor kimy bazowej.
Rozdział 9.
• Dziedziczenie ilunkcie wirtualne
531
Aby si ę przekonać, że chronione składowe klasy bazowej pozostają chronione w klasie pochod nej, można usunąć komentarz z instrukcji poprzedzającej in strukcję return w funkcji ma int ). Jeśli to zrobimy, otrzymamy od kompilatora następujący komunikat o błędzie : err or C2248: ' CBox : :m_Length ' : cannot access pr ot ected member decl ar ed i n cl ass ' CBox'
Komunikat ten informuje,
że
zmienna
składowa
m_Length jest
niedostępna.
Poziom dostępu do dziedziczonych składowych klasy Jak wiemy, jeżeli nie podamy specyfikatora dostępu dla klasy bazowej w definicji klasy po chodnej, to domyślnie zostanie zastosowany specyfikator pr i va te. W efekcie tego odziedzi czone z klasy bazowej publiczne i chronione składowe w klasie pochodnej stają się składowymi prywatnymi. Składowe prywatne klasy bazowej pozostają w niej prywatne i są niedostępne dla funkcji składowych klasy pochodnej . W rzeczywisto ś ci pozostają one prywatne w klasie bazowej bez względu na to, jak klasa bazowa jest określona w definicji klasy pochodnej. Użyliśmy także słowa
kluczowego publ i C jako specyfikatora klasy bazowej. Dzięki temu klasy bazowej zachowują swój poziom dostępności w klasie pochodnej, a więc skła dowe publiczne nadal są publiczne, a chronione są nadal chronione. składowe
Ostatnią możliwościąjest
zadeklarowanie klasy bazowej jako chronionej. W takim przypadku odziedziczone publiczne składowe klasy bazowej w klasie pochodnej stają się chronione. Składowe chronione i prywatne klasy bazowej zachowują swój poziom dostępności w klasie pochodnej. Zasady te zostały podsumowane na rysunku 9.3. Powyższe reguły mogą wydawać się
szych
stwierdzeń
nieco zawiłe, ale można je sprowadzić do trzech na temat dziedziczonych składowych klasy pochodnej:
poniż
Składowe klasy bazowej zadeklarowane jako prywatne nigdy nie w klasie pochodnej.
są dostępne
• Zdefiniowanie klasy bazowej jako publicznej nie zmienia poziomu jej składowych w klasie pochodnej .
dostępności
•
• Zdefiniowanie klasy bazowej jako chronionej powoduje, stają się chronione w klasie pochodnej.
że jej
publiczne
składowe
Możliwość zmieniania poziomu dostępności odziedziczonych składowych w klasie pochod nej daje pewien stopień elastyczności, ale należy pamiętać , że nie można zwiększyć poziomu dostępności określonego w klasie bazowej. Można go tylko zmniejszyć . Oznacza to, że klasa bazowa musi zawierać publiczne składowe, jeżeli chcemy mieć możliwość zmiany poziomu dostępności w klasie pochodnej . Może się wydawać, że zaprzeczamy tym samym koncepcji hermetyzacji danych w klasie, która zakłada ochronę ich przed nieautoryzowanym do stępem, ale jak się przekonamy, często się zdarza, że klasy bazowe definiowane są w taki sposób wy łącznie po to, aby służyły jako baza do tworzenia innych klas. Nie planuje się tworzenia obiek tów bezpośrednio z tych klas.
532
Visnal C++ 2005. Od podstaw
RysUnek 9.3
c1ass CABox:p u blic CBox [
r---dziedzi czona jako _ _ public: ~
dz iedziczona ja ko _ _ protected:
c1assCBox {
public:
======~=!=,...,
l
protected:
I
~~vat e:
c1ass CBBox:protected CBox (
dziedziczona jako_ protected:
======~-l dziedziczona jako - . protected:
Brak dostępu
c1ass CCBox:private CBox [
-
dziedziczona jako _ _ private:
'------- dziedziczona ja ko _
private:
Konstruktor kopiująCY wklasie pochodnej Należy pam ięta ć, że
konstruktor kopiujący wywoływany jest auto matycznie w momencie deklaracji obiektu inicjalizowanego innym obiektem tej samej klasy . Spójrzmy na poni ższe instrukcje:
CBox myBox(2.0. 3.0. 4.0): CBox copyBox(myBox) :
II Wywołuj e konstruktor.
II Wyw oluje konstruktor kopiujący.
Pierwsza z powyższych instrukcji wywołuje konstruktor przyjmujący trzy argumenty typu double, a druga wywołuje konstruktor kopiujący. Jeżeli nie dostarczymy własnego konstruktora kop iującego , to kompilator wywoła domyślny konstruktor kopiujący składowa po składowej obiekt inicjalizujący do odpowiadających im składowych nowego obiektu. Aby móc śledzi ć, co s ię dzieje podczas wykonywania operacji, w klasie CBox można umieścić własną wersję konstruktora kopiującego. Następnie klasa ta może posłużyć jako baza do zdefiniowania klasy CCandyBox. II Plik nagłówko wy Box.h w proj ekcie Cw9 05.
#pragma once
#include
using std: :cout :
using st d: :endl :
cla ss CBox {
II Defini cj a klasy bazowej.
Rozdział 9.
• Dziedziczenie i funkcie wirtualne
533
publ ic: II Konstrukt or klasy bazowej.
CBox(doub le l v = 1.0. double wv = 1.0. doubl e hv = 1.0):
.m_Lengt h(l v) . m_Widt hCwv) . m_Hei ght Chv)
l cout « end l « "Konst rukt or klasy CBox z os tał wyw ołan y ." :
II Konstrukt or kopiujący.
CBoxCconst CBox&i nltB) (
cout « endl « "Konst rukt or m_Length ~ init B. m_Length ;
m_Width = initB.m_Widt h;
m_Height = init B.m_Height ;
kopi ujący
klasy CBox
II Destrokt or klasy CBox - do ś ledzen ia
wywołań .
-CBoxC)
l cout « "Dest ruktor klasy CBox
z os ta ł wywo ła ny . "
z o st ał wywoła ny ." ;
« endl ; }
protect ed:
double m_Length ;
doub le m_Width;
double m_He ight ;
}; Należy również
w tym miejscu przypomnieć sobie, że parametr konstruktora kopiującego musi być referencją, aby uniknąć nieskończonej liczby jego samowywołań . Proces ten zostałby spowodowany koniecznością skopiowania argumentu przenoszonego przez wartość. Konstruk tor kopiujący w naszym przykładzie w momencie wywołania wysyła na ekran komunikat, dzięki czemu wiemy, kiedy został on wywołany. Poniżej
II Plik
znajduje
się
nagłówkowy
nowa wersja klasy CCa ndyBox:
CandyBox.h w proj ekcie Cw9 05.
#pragma once
#i ncl ude "Box. h"
#include
usi ng st d: 'cout ;
usi ng std : :endl ;
class CCa ndyBox: publ ic CBox
l
publ l C:
char* m_Content s ;
II Funkcj a klasy pochodnej
obliczająca pojemność.
double Volume C) const
( retur n m_Length*m_Width*m_Hei ght ; }
II Konstrukt or ustawiają cy wym iary i zawarto ść
II za pomocąjawnego wywo łan ia konstrukt ora klasy CBox.
CCandyBoxCdouble l v. double wv . double hv. cha r* str = "Candy")
:CBox Cl v . wv. hv) II Konstrukt or.
cout « endl « "Konst ruktor const ruct or2 klasy CCandyBox mConte nts = new cha r[ st rlenCs tr ) + l J ;
z o s t ał wywoł a n y . " ;
534
VislJal C++ 2005. Od podstaw st rcpy_s(m_Cont ent s . st rlen(m_Cont ent s ). st r) ; II Konstru ktor ustawiający zawartość
II automatycznie wywo luje domyślny konstruktor klasy CBox .
CCandyBox(cha r* st r = "Cukt erek") II Konstrukto r.
(
cout « endl « "Konstrukt or const ructor l kl asy CCandyBox m_Cont ent s = new char[ st rlen(st r) + l ] : strcpy_s(m_Contents . str len(m_Contents) . st r ) : -CCandyBox( )
z o s ta ł wywo ł any .
II Destrukt or.
{
cout « "Dest rukt or klasy CCandyBox delet e[] m_Content s:
zo s ta ł wywo ł a ny . "
end l;
«
}:
W powyższym kodz ie brakuj e jeszcze konstruktora jego wer sji dostarczonej przez kompil ator.
kop iującego ,
a
w i ęc będziem y pol egać
na
~ Konstruktor kOpiujący wklasie pochodnei Prz ed chwil ą zdefin iowany konstruktor kop iuj ący gram ie: II Cw9_05.cpp
II Używa n ie konstrukt ora
kopi ującego
#i nclude #include #incl ude "CandyBox.h" using st d: :cout ;
II Dek laracj a i inicj alizacja . II Użyc ie konstruktora kopiującego.
cout « end l « « « «
"Po jem no ś ć
obiektu chocBox wynosi "
«
chocBox.Vol ume()
endl " Poj emno ś ć
obiekt u choco lateBox wynosi "
«
choco late Box.Vol ume()
endl;
ret urn O:
Jak to działa [lub czemu nie działa) Kiedy uruchomimy wersję te stową tego programu , to poza spodziewanymi danymi wymi zobaczymy okno dial ogowe widocz ne na rysunku 9.4 .
wyjści o
Rozdzial9. • Dziedziczenie i funkcje wirtualne
535
RV51mek9.4
Program: ,..
FlIe: dbgdel,cpp lIne : 52
For h fiJrmatlon on row 'fO'J p-ogram can cause sn sssertco fai""e, ss e the Visua l C++ documenta tiÓrlon asssrts . (l'ress Retry to debug !he application) ,
I
Przerw ij
I Porów p-óbę I
19"oruj
Kliknię cie przycisku Przerwij spowoduje zniknię cie okna dialogowego i ukaże s i ę nam spo dziewane okno konsoli z wynikiem działani a programu. Z danych w tym oknie wynika, że kon struktor kop iujący wygenerowany przez kompilator dla klasy pochodnej automatycznie wy wołał konstruktor kopiujący klasy bazowej .
Jak jednak widać, nie wszystko jest w porządku . W tym przypadku konstruktor kopiujący wygenerowany prze z kompilator spraw ia probl emy, poni eważ obszar pamięci wskazywany przez skład ową klasy pochodnej m_Content s w drugim zadeklarowanym obiek cie wskazuje ten sam obszar pamięc i co w pierwszym obiekcie. Kiedy jeden z tych obiektów zostaje znisz czony (kiedy wychodzi poza zasięg na końcu funkcji main ()), to zostaje zwolniona pamięć zajmowana przez tekst. Podczas niszczenia drugiego obiektu destruktor próbuje zwolnić pa mięć, która została już zwolniona prze z destruktor niszczący poprzedni obiekt - i to jest powod em pojawienia się komunikatu o błęd zie w oknie dialo gowym .
~ Naprawianie blędlJ zoperatorem kopiuiącym Z probl emem tym możemy sobie poradzi ć , dodając poni żs zy kod konstruktora do sekcj i publicznej pochodnej klasy CCandyBox w projekcie ew9_05:
kopiującego
II Konstrukt or kop iujący klasy pochodn ej.
CCandyBox(const CCandyBox&i nitCB) {
cout
«
endl
«
"Konst rukt or
II Zarezerwuj
nową pamięć .
m_Content s
=
II Kop iowanie
k o p i u ją cy
klasy CCandyBox
new char[ st rlen(lnit CB .m_Cont ents )
l
J:
łańcucha.
st rcpy_s(m_Conte nt s . st rlen(i nit CB.m_Contents) Aby
+
zo stał wywo ł a ny.
+
l . initCB.m_Conte ntsJ:
przekonać się ,
chomi ć
jak teraz działa nowy konstruktor kopiujący, należy powyżs zy kod uru z tą samą funk cją mai n( ) co w ostatnim przykładzie .
Jak lo działa Teraz program zach owuje
się
znacznie lepiej i daje
następujący
rezultat:
536
VisualC++ 2005. Od podstaw Kons tru ktor klasy CBox z o s t a ł wywo ł a ny . Konstruktor constructor2 klasy CCandyBox zost ał wywo ł a n y. Konstru ktor klasy CBox zost a ł wywo ł a ny. Konst ruktor k op i u jący klasy CBox z os t a ł wyw ołan y . Po j emn o ś ć obiekt u choc Box w ynosi 24 Po j emn o ś ć ob iekt u chocol ateBox wynosi l Dest rukt or klasy CCandyBox z os t ał wywo ł a ny . Dest rukt or klasy CBox zost ał wywo ł a ny . Dest rukt or klasy CCandyBox z o s t a ł wywo ł a ny . Dest rukt or klasy CBox z o s t a ł wywo ł a ny. A jednak cały czas coś jest nie tak. W trzecim wierszu danych wyjściowych pojawia się infor macja, że dla części obiektu choco1ateBox tworzonej na podstawie klasy CBox wywołany zost ał konstruktor domyślny zamiast konstruktora kopiującego. W konsekwencji obiekt ten ma domyśln e wymiary zamiast wymiarów obiektu in icjalizującego, a więc pojemność jest nie prawidłowa. Powodem zaistniałej sytuacji jest fakt, że pisząc konstruktor dla obiektu klasy pochodnej, j esteśmy odpowiedzialni za prawidłową inicjal izację składowych klasy pochodnej. Do nich zali czają się także składowe odziedziczone. Rozwiązaniem
klasy w
liści e
jący wygląda
tego problemu jest wywołanie konstruktora kopiującego dla bazowej częśc i inicj alizacyjnej konstruktora kopiującego klasy CCandyBox. Konstruktor kopiu wtedy następująco :
Teraz konstruktor kopiujący klasy CBox wywoływany jest z obiektem klasy i ni t CB. Przeka zana do niego zostaje tylko bazowa część obiektu, a więc wszystko działa jak powinno. Jeżeli do ostatniego listingu dodamy wywołanie konstruktora kopiującego klasy bazowej , rezultat działania programu będzie następujący :
Konstruktor klasy CSox zost ał wywoła ny . Konstrukto r constructo r2 klasy CCandyBox z ost ał wy wo ł a ny . Konstrukto r kop iują cy kl asy CBox zosta ł wyw o ła ny . Konstru ktor kO P1UjąCY klasy CCandyBox zost a ł wywoł any . Poj emn o ś ć obiektu chocBox wynosi 24 Po j emn oś ć obiekt u chocolat eBox w ynosi 24 Dest rukt or klasy CCandyBox z o st ał wywoła ny. Dest rukt or klasy CBox zos t a ł wywo łan y . Dest ruktor klasy CCandyBox z o s t a ł wywo ła ny . Dest rukt or klasy CBox zost ał wywo ła ny . Z danych na ekranie wynika, że wszystkie konstruktory i destruktory zostały wywołane w pra widłowej kolejnośc i , a konstruktor kopiujący części bazowej obiektu choco1ateBox wywołany został przed konstruktorem kopiującym klasy CCandyBox. Pojemnoś ć obiektu chocol at eBox klasy pochodnej jest teraz taka sama jak poj emność obiektu i n icj a l i zuj ące go, czyli prawidłowa.
Rozdzial9. • Dziedziczenie i1unkcie wirtualne W związku z powyższym
537
możemy sformułować następną złotą zas adę:
Pisząc
dowolnego rodzaju konstruktor klasy pochodnej, jesteśmy odpowiedzialni za ini wszystkich sk ładowych obiektu tej klasy, włączn ie ze wszys tkimi składowym i • odziedziczonymi. cja liz ację
Składowe
klasy jako przyjaciele
funkcje zaprzyjaźnione klas . Dzi ęki temu za ich wolny dostęp do wszystkich składowych klasy. O czywiście nie ma powodu, dla którego funkcja zaprzyj aźniona nie mogłaby być składową innej klasy.
W rozdziale 7.
nauczyli śmy się deklarow a ć
pomocą mogliśmy uzyskać żadnego
Przypuśćm y , że
mamy
zdefiniowaną klasę
CBot t l e reprezentuj ącą butelkę:
cl ass CBottle (
pub1ic: CBot t le (double hei ght . double dlamete r) (
m_He ight = height: m_O iamete r = diameter : )
private : doub le m_He ight ; do uble m_Oia met er ;
II Wys okoś ć butelki. II Śre dn ica butelki .
};
Teraz potrzebujemy klasy reprezentującej opakowanie dla dwunastu butelek, którego wymiary automatycznie są dopasowywane do butelek określonego rodzaju. Klasę tę możemy zdefinio wać następująco :
II Wysok ość butelki. II Cztery rz ędy... 11 ...p o trzy butelki.
II Długość karto nu. II Szerokość kartonu. II Wysok ość karton u.
l: Konstruktor w powyższej klasie ustawia wysokość na taką samą wartość jak wysokość bute lek, które będą przechowywane w tym kartonie. Długość i szerokość ustawiane są w oparciu o ś red n i cę butelki w taki sposób, aby zmieściło się ich pięć w pudełku.
538
lisual C++ 2005. Od podstaw Jak już wiemy, takie coś nam nic zadziała . Zmienne składowe klasy CBott l e są prywatne, a zatem nicdostępne dla konstruktora klasy CCa rt on. Wiemy również , że deklaracja z użyciem słowa kluczowego f ri end w klasie CBottl e naprawi ten błąd:
cla ss CSott le (
publ lC:
CBott le(doub le height . double di ameter )
(
m_Height = helght:
m_Diamete r = diameter :
pr ivat e: doub le m_Helght : double mDi ameter:
II Wysokość butelki. II Średnica butelki.
II Wpuszczamy konstruktor kartonu .
fr iend CCarton ' :CCart on(const CBot t le&aBot t le) : }:
Jedyna różnica pomiędzy deklaracją fri end w powyższym kodzie i w rozdziale 7. jest taka, że tutaj trzeba podać nazwę klasy oraz operator zasięgu z nazwą funkcji zaprzyjaźnionej w celu jej identyfikacji. Aby kod ten można było skompilować , kompilator musi mieć in formacje na temat konstruktora klasy CCart on. W związku z tym przed definicją klasy CBot t l e należałoby umieścić dyrektywę #i ncl ude dołączającą plik nagłówkowy zawierający defi nicję klasy CCarton.
Klasy zaprzyjaźnione Możemy także pozwolić
wszystkim funkcjom składowym j ednej klasy uzyskać dostęp do innej klasy, deklarując jąjako klasę zaprzyjaźnioną. Aby CCa rt on jako klasę zaprzyjaźnionąklasy CBott l e, należy dodać do definicji
wszystkich zmiennych zdefiniować klasę
tej klasy
deklarację
składowych
fri end:
f riend CCart on: Dzięki umieszczeniu powyższej deklaracji w klasie CBott le wszystkie funkcje CCarton mają dostęp do wszystkich zmiennych składowych klasy CBott l e.
składowe
klasy
Ograniczenia klas zaprzyjaźnionYCh Przyjaźń pomiędzy klasami nie jest odwzajemniana. To, że klasa CCarton jest zaprzyjaźniona z klasą CBot 11e, nie oznacza, że klasa CBott l e jest zaprzyjaźniona z klasą CCa rt on. Jeżeli chce my, aby tak było , to musimy dodać deklarację fri end dla klasy CBott le do klasy CCarton.
Przyjaźń pomiędzy
klasami nie jest dziedziczona. Jeżeli zdefiniujemy inną klasę na bazie klasy klasy CCar t on nie będą miały dostępu do jej zmiennych składowych, nawet do zmiennych odziedziczonych z klasy CBot t l e.
CBott l e, to
składowe
Rozdzial9. • Dziedziczenie i funkcje wirtualne
539
Funkcje wirtualne
Przyjrzymy się bliżej zachowan iu odziedziczonych funkcji składowy c h oraz ich związkom z funkcjami składowym i klasy pochodnej. Do klasy CBox możemy d odać funkcję wysyłającą na ekran pojemność obiektu tej klasy. W uproszczeniu klasa ta wygląda n as t ępując o: II Plik
nagłówko wy
Box.h w proj ekcie Cw9_06.
#pragma once #i ncl ude using std : :cout : usi ng std : :endl: class CBox
II Klasa bazowa.
{
publ i c:
II Funkcj a pokazująca pojemność obiektu .
void ShowVolume() const
{
cout
« «
II Funkcja
endl
"Pojenno ś ć użytkowa
ob liczająca pojemność
obiekt u CBox wynoSl " « Vol umeO ;
obiektu klasy CBox.
double Volume( ) const { ret urn m_Length*m_Wi dt h*m_Height : } II Konstruktor.
CBox(doub le l v = 1.0. double wv = 1.0. doub l e hv = 1.0)
:m_Length( l v). m_Widt h(wv). m_Hel ght(hv ) {}
prot ect ed:
doubl e m_Length:
doubl e m_Widt h;
double m_Hel ght :
l: pojemność dowolnego obiektu klasy CBox możemy wysłać na ekran za pomocą wywo dla niego funkcji ShowVol ume t ). Konstruktor ustawia wartości zmiennych składowych w liście inicjalizacyjnej, dzięki czemu niepotrzebne są żadne instrukcje w ciele funkcji. Zmienne składowe są takie same jak wcze śniej i zostały zdefiniowane jako chronione, a więc są dostępne dla funkcji składowych wszystkich klas pochodnych.
Teraz łania
Przypu śćmy, że
chcemy utworzyć klasę pochodną reprezentującą pudełko do przechowywania o nazwie CG l assBox. Zawartość tego pud ełka jest delikatna i w związku z tym, że doda wany jest do niego ochronny materiał pakujący, pojemność jest mniejsza niż pojemność bazo wego obiektu klasy CBox. Potrzebujemy zatem innej funkcji Vol ume O , która będzie uwzględ niała tę nową sytuację . Dodamy ją do klasy pochodnej : szkła
II Plik
nagłówkowy
GlassB ox.h w p rojekcie Cw9_06 .
#pragma once #include "Box.h"
540
Visual C++ 2005. Od podstaw el ass CG lassBox. publ ic CBox
II Klasa p ochodna .
(
publ iC : II Funkcja obliczająca p ojemność' obiektu klasy CGlassBox II zostawiająca 15% na materiał wypełniający.
do ub le Vol umeC) eonst { ret urn O.85*m_Lengt h*m_W idt h*m_He ight: } II Konstruktor.
CG lassBoxC double l v. doubl e wv, double hv) . CBox( lv . wv. hv ){}
}:
w tej klasi e pochodnej mogłoby być więcej dodatkowych składowych, ale nie zbytnio komplikować kodu i skoncentrujemy się na razie na spo sobie działania funkcji składowych. Konstruktor klasy pochodnej wywołuje tylko konstruktor klasy bazowej w swojej liści e inicjalizacyjnej w celu ustawienia wartości zmiennych składowych . W jego ciele nie są potrzebne żadne instrukcje. Bazową wersję funkcj i Vol ume ( ) przesłoniliśmy jej nową wersją, aby móc podczas wywoływania odziedziczonej funkcji ShowVo l ume( ) dla obiektu klasy CGl assBox wywołać wersję funkcji Vol ume( ) zdefiniowaną w klasie pochodnej .
Z
pewnością
będziemy
~ Zastosowanie Ollziellziczonei fllllkcii Widzimy teraz, jak nasza klasa pochodna działa w praktyce . Możemy ją łatwo wypróbować, obiekt klasy bazowej ora z obiekt klasy pochodnej o tak ich samych wymiarach, a następnie sprawdzając , czy obliczane są prawidłowe pojemności. Wykonująca to funkcja ma i n( ) wygl ąda następuj ąco : tworząc
II Cw9-,-06.cpp
II Zachowanie odziedziczonych funkcj i w klasie pochodnej.
#i nel ude
#i nel ude "GlassBox.h" usi ng st d: :eout :
usinq std : .endl :
II Dla kłas CBox i CGlassBox.
i nt main( ) {
CBox myBox(2.O. 3 O. 4. O) : CG lassBox myG l assBox(2.O. 3. O. 4.O):
II Deklaracja obiektu klasy bazo wej. II Dekl aracja obiektu klasy pocho dnej - takie same II wymiary.
myBox.ShowVol ume() : myGlassBox.ShowVolume () :
II Pokaż pojem ność obiektu kłasy bazow ej . II Pokaż p ojemność' obiektu klasy pochodnej.
eout « endl :
return O:
Jak lo działa Uruchomienie powyższego programu da P Ojemno ś ć u ży t k owa P oj e m n o ś ć użyt k owa
następuj ący
obiekt u CBox wynosi 24 obiekt u CBox wynosi 24
rezultat:
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
541
Rezultat ten nie tylko j est nudny i powtarzający si ę , jest także kata strofą! Program dzi ała cał kowicie inaczej, niż oczekiwali śmy , i j edynym interesuj ącym w tym wszystkim problemem jest pow ód takiego stanu rzeczy. Najwyraźniej fakt, że drugi e wywołani e doty czy obiektu klasy pochodnej CG l ass Box, nie jest w ogóle brany pod uwagę. Dowodem jest n ieprawidłowa po jemn o ś ć na wyj ś ciu. Pojemn o ś ć obiektu klasy CG l as sBox powinna być zdecydowanie mniej sza od poj emnośc i obiektu kla sy bazowej CBox o tak ich samych wymiarach . Powodem, dla którego o t rzy ma li ś my nieprawidłowy wynik, jest to, że wywołanie funkcji Vol ume( l w funkcji ShowVol ume( lwykonywane j est przez kompilator jeden raz dla wszystkich i że wywoływana jest wersja tej funkcji zdefiniowana w klasie bazow ej . Funkcj a ShowVo l ume ( l jest funkcj ą klasy bazowej i podczas kompilacji klasy CBox wywołanie funk cji Vol ume() je st traktowane jako wywołanie funkcji Vol ume ( l należącej do klasy bazowej . Kompilator nie wie nic na temat żadnej innej funkcji Vo l ume( l. Nazywa się to wiązaniem statycznym (ang. static resolution) wywołania funkcji, ponieważ wywołan i e to jest ustalone przed wykonaniem pro gramu . Czasami nazywa si ę to także wczesnym wiązaniem (ang. early binding), ponieważ wybrana funkcja Vo l ume ( l jest zw iąza n a z wywoła nie m funkcji ShowVo l ume ( l podczas kom pilacji programu. programie mieliśmy nadzieję, że kwestia dotycząca wyb oru konkretnego wy funkcji Vol ume ( l w danym przypadku zostanie rozstrzygnięta w czasie wykonywania programu. Operacja ta nazywa się wiązaniem dynamicznym (ang. dynamie binding) lub wią zaniem późnym (ang. lale binding). Chcemy, aby właściwa wersja funkcji Vo l ume ( ) wywo ływana przez funkcję ShowVo l ume ( ) była określana przez rodzaj przetwarzanego obiektu, a nie określ an a z góry przez kompilator przed uruchomieniem programu. W
powyższym
wołania
Bez wątpienia nie zdziwi Ci ę fakt, że w C++ można takiego czego ś dokonać - w przeciwnym przypadku cała ta dysku sja byłaby na próżno. W tym celu musimy użyć czegoś , co nazywa s ię funkcją wirtualną (ang . virtualfunctioni.
Czym jest lunkeja wirtualna Funkcja wirtualna to funk cj a składowa klasy zadeklarowana przy użyciu s łow a kluczowego vi rtua l . Jeżeli w klasie bazowej zadeklarujemy funkcję wirtualną, a w klasie pochodnej znaj duje s i ę jej inna defini cja, to jest to znak dla kompil atora, że nie chcemy, aby funkcja ta była wi ązana statycznie. To, czego chcemy, to, aby wyb ór wersji funkcji wywoływanej w progra mie był dokonywany na pod stawie rodzaju obiektu, dla którego jest ona wywoływana .
~ Naprawa klasy eOlassBol Aby program działał tak, j ak się spod ziewal i śmy, należy tylko dodać sł owo kluczowe vi rt ual do definicji funkcji VOl ume ( l w obu klasach. Wypróbujemy to w nowym projekcie Cw9_07. P oni żej widoczna jest definicja klasy CBox: II Plik nagłówkowy Box.h w projekcie Cw9 07.
#pragma ance
#include
usi ng st d:: cout :
uswg st d: :endl :
542
lisual C++ 2005. Od podstaw class CBox
II Klasa bazo wa.
(
publ i c:
II Funkcja pokazująca pojemność obiektu.
void ShowVolume() const (
cout « end l «
" Pojemno ś ć u ży t k owa
klasy CBox wynosl "
II Funkcia oblicza '
Volume():
«
CBox .
virt ua l double Vo l ume() const II Kons trukto r.
CBox(double l v = 1.0, double wv = 1.0, do uble hv = 1.0) :m_Lengt h(l v), m_Widt h(wv) , m_Helght (hv ) {} protect ed:
double m_Lengt h:
doub le m_Wi dth:
doub le m_Height :
}:
Plik GlassBox.h zawiera następujący kod : II Plik
nagło wko wy Glass łłox. h
w projekcie Cw9_07.
#pragma once
#i nc l ude "Box .h"
cl ass CGlass Box : pub lic CBox
II Klasa pochodna.
(
publ ic. II Funkcja ob liczająca poje mność obiektu klasy CGlass Box.
II zostawia ' c 15% na material w e łniai c .
virtual double Volume() const
II Konstruktor.
CGla ssBox(double l v, double wv. doub le hv): CBox(l v.
II Dek laracja obiektu klasy bazowej. II Deklaracj a obiekw klasy pochodnej - lakie same II wymiary .
Rozdzial9.• Dziedziczenie i funkcie wirtualne myBox.ShowVol ume( ); myGlassBox ShowVo l ume( ) ;
543
II Pokaż pojemn oś ć obiektu klasy bazowej. II Poka ż pojemność obiektu klasy pochodnej.
(out « end1: ret urn O;
Jak to dziala Uruchomienie tego programu z dodatkiem tylko jednego do definicji funkcji Vol ume ( ) da następujący rezultat: Po j emno ś ć użytkowa Po jem no ś ć u żyt k owa
małego słowa
kluczowego vi rt ual
klasy CBox wynosi 24 klasy CBox wynosi 20.4
Teraz program najwyraźniej działa zgodnie z naszymi oczekiwaniami. Pierwsze wywołanie funkcji ShowVol ume ( ) w obiekcie klasy CBox o nazwie myBox powoduje wywołanie funkcji Vol u rre () w wersji dla klasy CBox. Drugie wywołanie obiektu klasy CG l ass Box o nazwie myGl ass Box wywołuje wersję tej funkcji zdefiniowaną w klasie pochodnej . Warto zauważyć , że mimo iż wstawiliśmy słowo kluczowe vi rt ual do definicji funkcji Vo l ume( )w klasie pochodnej, to nie było to konieczne. Definicja wersji klasy bazowej funkcji jako wirtualnej byłaby wystarczająca . Gorąco jednak zachęcam do stosowania słowa kluczo wego vi r tu al także w definicjach funkcji w klasach pochodnych, gdyż dzi ęki temu osoba czytająca nasz kod może się łatwo zorientować , że dana funkcja jest wirtualna, a co za tym idzie, że jest ona wybierana dynamicznie. Aby funkcja zachowywała się jak funkcja wirtualna, musi mieć taką samą nazwę, listę para metrów oraz typ zwracany we wszy stkich klasach pochodnych co w klasie bazowej . Dodat kowo, jeżeli funkcja klasy bazowej jest zadeklarowana jako const, to funkcja klasy pochodnej również musi być const . Jeżeli funkcje będą miały inne parametry lub typy zwracane albo jedna z nich zostanie zadeklarowana jako const , a druga nie, to mechanizm funkcji wirtualnych nie zadziała . W takim przypadku funkcja operuje z wiązaniem statycznym ustalonym w cza sie kompilacji . Mechanizm funkcji wirtualnych daje niezwykle duże możliwości. Czasami można usłyszeć termin polimorfizm w kontekście programowania zorientowanego obiektowo - odno si się on do możliwośc i funkcji wirtualnych. Coś, co jest polimorficzne, może występować pod wieloma postaciami, jak na przykład wilkołak, dr Jekyll czy polityk przed wyborami i po wyborach. Wywołanie funkcji wirtualnej daje różne efekty w zależności od rodzaju obiektu , dla którego została wywołana.
Warto zauważyć,
że funkcja
Vo lume( ) w klasie pochodn ej CGl assBox w rz eczywistości klasy pochodnej wersję tej funkcji w klasie bazowej. Aby odwołać się do funk cji Vo lume ( ) w klasie bazowej z fu nkcj i z klasy pochodnej, należy posłużyć się operatorem zas ięgu w następujący sposób : CBox : : Vo lume( ).
przesłania funkcjom
544
Visual C++ 2005. Od podstaw
Używanie wskaźników
do obieklówklas
Używanie w skaźników
z obiektami klasy bazowej i klasy pochodnej jest bardzo ważną tech do obiektu klasy bazowej można przypisa ć adres obiektu klasy pochodnej lub klasy bazowej. W ten sposób można użyć wskaźnika typu wskaźnik do obiektu klasy bazowej, aby uzyskać różne zachowania z funkcjami wirtualnymi w zależności od rodzaju obiektu wskazywanego przez wskaźnik. Stanie się to jaśniejsze, kiedy przyjrzymy się temu na przykładzie. niką. Wskaźnikowi
~ Wskaźniki do klas bazowych ipochodnych W programie tym będziemy używali tych samych klas co w poprzednim, ale dokonamy pew nych modyfikacji funkcji mai nO , aby używała wskaźnika do obiektu klasy bazowej. Utwórz projekt o nazwie Cw9_08 i utwórz w nim pliki Box.h i GlassBox.h o takiej samej zawartości jak w poprzednim projekcie. Pliki te można skopiować z folderu projektu Cw9_07 . Doda wanie istniejącego pliku do projektu j est proste. Należy kliknąć prawym przyciskiem Cw9_08 w zakładce Solution Explorer, a następnie wybrać z menu Add/Existing Item, Potem wybie ramy plik nagłówkowy, który chcemy dodać do projektu. Po dodaniu plików nagłówkowych zajmujemy się modyfikacjąpliku Cw9_OS.cpp : II Cw9_OS.cpp
II Ustawianie wskaźnika na adres obiektu klasy bazow ej. II Wyświetlanie pojemności obiektu klasy bazowej. II Ustawianie wskaźnika na obiekt klasy po chodnej. II Wyświetlanie pojemności obiektu klasy poch odnej.
do obiektów klasy bazowej.
eout « endl :
ret urn O:
Jak to działa Klasy są takie same jak w projekcie Cw9 _07, ale funkcja main( ) została zmodyfikowana w taki sposób, że do wywołania funkcji ShowVol ume ( ) używa wskaźn ika . Jako że używamy wskaźnika, to przy wywołaniu funkcji posługujemy się operatorem pośredniego dostępu do składowej. Funkcja ShowVol ume ( ) wywoływana jest dwa razy i oba te wywołania wykonywane
Rozdzial9. • DziedzIczenie I lunkcje wirtualne są
545
tego samego wskaźnika pBox do obiektu klasy bazowej. Za pierwszym razem zawiera adres obiektu klasy bazowej myBox, a za drugim razem adres obiektu klasy pochodnej myGl assBox. przy
użyciu
wskaźnik
Rezultat
działania
programu jest
P o jemnoś ć u żyt k owa Poj emn ość u ży tkowa
następujący:
klasy CBox wynosi 24 klasy CBox wynos i 20 .4
Rezultat ten j est identyczny z rezultatem poprzedniego programu, w którym w funkcji zasto sowaliśmy rzeczywiste obiekty.
wywołaniu
Z tego przykładu można wyciągnąć wniosek, że mechanizm funkcji wirtualnych działa równ ie dobrze poprzez wskaźnik do obiektu klasy bazowej, gdzie określona funkcja wybierana jest na podstawie typu wskazywanego obiektu. Pokazano to na rysunku 9.5. pBox->Sh owVolumeO
I
Ws ka ź n i k
this jest
ustawiony na pBox
!
c1assCBox
{.,.l /
virtual double Volume
void ShowVolumeO co nst {
(o ut « end l
« · PoJ e m n o ść użytkowa
« Vo lum e {);
Oconst
/
obiektu klasy CBox wynosi " Ws kaźni k pBoxw skazują cy
_ _ o biekt klasy CBox
---
===============
pBox w skazują cy o bie kt klasy CGlassBox
Ws kaź nik
c1assC
ssBox
virtual double Volume
Oconst
(...l
R.sunek 9.5 Oznacza to, że nawet gdy nie znamy dokładnie typu obiektu wskazywanego w programie przez wskaźn ik klasy bazowej (na przykład kiedy do funkcji jako argument przekazywany jest wskaźnik), to mechanizm funkcji wirtualnych przypilnuje, aby wywołana została właściwa funkcja , Daje to ogromne możliwości , a więc musisz to dobrze zrozumieć . Polimorfizm sta nowi fundamentalny mechanizm w języku C++, którego będziesz często używać.
Używanie Jeżeli
relerencii zlunkciami wirtualnymi
zdefiniujemy funkcję z parametrem będącym referencją do klasy bazowej , to będziemy mogli do niej przekazywać jako argumenty obiekty klasy pochodnej. Podczas wykonywania tej funkcji odpowiednia funkcja wirtualna dla przekazanego obiektu wybierana jest automa tycznie . Możemy to zaobserwować, modyfikując funkcję mai n() w ostatnim przykładz ie, aby wywoływała funkcję z parametrem w postaci referencji .
546
Wisnal C++ 2005. Od podstaw
~ Używanie relerencii zlunkejami wirtualnymi Przenieśm y wywoł anie funkcję
funkc ji ShowVo l ume( l do oddzielnej funkcji i wywołajmy z funkcj i ma i n( l :
II Cw9_09.cpp
II Używanie referencji do
wywo ływania funkcji
tę oddzi e ln ą
wirtualnych.
#include #incl ude "GlassBox.h·· usi ng st d: :cout : usi ng st d: :end l ;
II Dekla racja obiektu klasy bazo wej. II Deklaracja obiektu klasy p ochodnej o takich samych II wymiarach.
II Wysianie na wyjście pojemności obiek tu klasy bazowej. II Wysianie na wyjście pojemnośc i obiektu klasy pochodnej.
cout « end l ; ret urn O:
I
(Oid Output(const CBox& aBox)
~Sh owV01Ume ( ) : Pliki Box.h i GlassBox.h
- -=
mają t aką s amą zawartość
jak w poprzednim projekcie.
Jak lo działa Teraz fu nkcj a mai n( l składa się przede wszystkim z dwóch wywołań fun kcj i Output O . Pierwsze wywołanie z argumentem w postaci obiektu klasy bazowej , a drugie z argumentem w postaci obiektu klasy pochodnej . Jako że parametr jest referencjądo klasy bazowej, funkcja Out put( l przyjmuje j ako argument obiekty obu klas, po czym następuje wywołanie odpo wiedniej funkcj i wirtualnej Vol ume( l - w zależności od obiektu inicj alizującego referencję. Rezultat działania tego programu jest identyczny z poprzednim, co j est dowodem na to, mechanizm funkcji wirtualnych rzeczywiście działa z parametrami w postaci referencji.
że
Niepelne delinicie klas Na początku poprzedniego przykładu znajduje się deklaracja prototypu funkcji Output( l . Aby móc przetworzyć tę deklarację, kompilator potrzebuje dostępu do definicji klasy CBox, ponie waż parametr jest typu CBox&. W tym przypadku definicja klasy CBox jest już dostępna, ponie
Rozdział 9.•
waż dołączyliśmy
zawiera
za
pomocą
dyrektywy #i ncl ude plik
l -i nc l ude dołączającą plik
dyrektywę
Dziedziczenie i funkcje wirtualne nagłówkowy
nagł ówkowy
547
GlassBox.h, który z kolei
Box.h.
Może s i ę
jednak zdarzyć, że będziemy mieli tak ą deklarację, ale nie będzie możliwo ści do definicji klasy w taki sposób. Wtedy będziemy potrzebować jakiegoś innego sposobu na okre śleni e , że nazwa CBox odnosi się do typu klasowego. W tym celu możemy posłużyć się niekompletną definicją klasy CBox umieszczon ą przed prototypem funkcji Outp ut t ). Instruk cja niek ompletnej defini cj i klasy CBax jest bardzo prosta:
łączen ia
clas s CBox; Instrukcja ta identyfikuje CBax jako nazwę klasy, która jeszcze nie zos tał a zdefiniowana. Kom pilatorowi to wystarczy , gdy ż wie on, że jest to nazwa klasy, co pozwala mu na przetwo rzenie prototypu funkcji Outp ut () . Je żeli nie podamy w jaki ś sposób informacji , że CBax jest nazwą klasy , to komp ilator zgłosi komunikat o błędzie .
Funkcje CZysto wirtualne Możliwe, ż e będziemy chcieli umieścić funkcję w irtualną w klasie bazowej , aby móc j ą póź niej przedefiniować w klasie pochodnej celem dopasowania do obiektów tej klasy, ale w klasie bazowej nie można nadać tej funkcji żadnej znaczącej definicji .
Na przykład możemy mieć klasę CCant ai ner, która mogłaby służyć jako klasa bazowa dla defi nicji klasy CBax lub CBattle, a nawet CTeapat . Klasa CConta i ner nie miałaby żadnych zmien nych składow ych , ale my moglibyśmy zechcieć dostarczyć funkcję wirtualną Vol ume ( ) dla wszystkich klas pochodnych. Jako że klas a CCant ai ner nie zawiera żadnych zmiennych składowych, a co za tym idzie - nie ma żadnych wymiarów, nie można napisa ć żadnej sen sownej definicji dla funkcji Va l ume ( ) w tej klasie. Można jednak zdefiniować tę klasę z funk cją Vol ume () w następujący sposób: II Plik
n agłó wko wy
Container.h projektu Cw9_ l O.
#pragma once
#i nel ude
usi ng st d: :eout:
uSl ng st d: :endl :
elass CCont ai ner
II Generyczna klasa bazowa do tworzenia konteneró w.
(
publ i c:
II Funkcja ob liczająca pojemnos ć - brak tresci.
II Jest to funkcj a czys to wirtualna. co zostało oznaczone przez '= O'
vi rtual do uble Volume () eonst II Funkcj a
~
O:
wyświetlająca pojemn ość .
virt ual void ShowVol ume( ) eonst (
eout « end l
« " P oj emno ś ć wynosi " « Volume():
};
548
Visual C++ 2005. Od podstaw Instrukcja d efiniująca funkcj ę w i rtualną Vol ume () określa jąj ako pustą poprz ez znak równości i zero znajdujące s i ę po nagłówku funkcji . Funkcja taka nosi nazw ę funkcji czysto wirtualnej. Każd a klasa pochodna tej klasy musi zaw ierać definicję funkcji Vo l ume () lub ponownie defi niować jąjako fun kcj ę czysto wirtualną. Jako że funkcję Vo l ume ( ) zad eklarowaliśmy jako const, jej implementacja w klasach pochodnych musi być również const . Należy pamiętać , że funkcje o takiej samej nazwie i l iście parametró w, z kt órych jedna jest const, a druga nie, s ą różnymi funkcjam i. Inaczej mówiąc, za pomocą słowa kluczowego const można przeładować funkcję.
Nasza klasa zawiera również funkcję ShawVol ume () , która wyświetla pojemność ob iektów klasy pochodnej . Ze względu na fakt, że jest wirtualna, można ją podmienić w klasie pochod nej. Ale gdyby nie była, to wywołana zostałaby jej wersja dostarczona w klasie bazowej .
Klasy abstrakcyjne czysto wirtualną zwana jest klasą abstrakcyjną. Nazy wa s ię ona nie można zdefiniować ob iektu klasy zawierającej funkcję wirtualną. Istniej e ona tylko w celu definiowania j ej klas pochodnych. J eżeli w klasie pochodnej funkcja czysto wirtualna klasy bazowej jest również czysto wirtualna, to klasa ta także jest abstrakcyjna. Klasa
zawierająca funkcję
abstrakcyjną, ponieważ
Na podstawie powyższego przykładu klasy CConta ine r można wyciągnąć ni esłuszny wniosek, że klasa abstrakcyjna nie może mieć zmiennych składowych. Klasa abstrakcyjna może mieć zarówno zmienne, jak i funkcje składowe. Jedynym wyznacznikiem tego, że kla sa jest abs trakcyjna, jest obecność w niej funkcji czy sto wirtualnej . Pon adto klasa abstrakcyjna może zawierać więcej n iż j edną funkcję wirtualną. W takim przypadku w klasie pochodnej muszą znajdować s i ę definicje wszystkich funkcji czysto wirtualnych kla sy bazowej albo stanie si ę ona również klasą abstrakcyjną. Jeżeli w klasie pochodnej zapomnimy określić funkcj ę skła dową Vol ume () jako const , to będzie ona nadal klasą abstrakcyjną, ponieważ będzie zawie r ała czysto wirtualną funkcję składową Vol ume ( ) typu const, jak również funkcję Vol ume () , która nie jest typu const.
~ Klasa abstrakcyjna Możemy zaimplementować klasę
CCan reprezentującą puszkę piwa albo coca-coli razem z ory Obie te klasy są pochodnymi zdefiniowanej wcześniej klasy CCont a i ner. Definicja klasy CBox j ako podklasy klasy CCantai ne r wygląda następująco:
ginalną klasą CBox.
II Plik naglówkowy Box.h w projekci e Cw9 10.
# ragma ance
#incl ude "Cant ai ner.h" #incl ude
using std : :cout :
uSlng st d: :endl ;
cl ass CBox: publ ic CCont ai ner publi c:
II Dolącza definicję klasy CConlainer.
II Klasa pochodna.
Rozdział 9.•
Dziedziczenie ifunkcie wirtualne
549
II Funkcj a pokazująca pojemność obiektu.
virt ua l void ShowVo l ume() const
{
cout « endl
« " Poj em n o ś ć
u żyt k owa
obiektu klasy CSox wynosi " « Volume( ) :
II Funkcja obliczajqca pojemn oś ć obiektu klasy CBox.
vi rt ua l double Vol ume( ) const
{ ret urn m_Length*m_Widt h*m_He lght : }
II Konstruktor.
CSox( do uble l v = 1.0. double wv = 1.0. double hv = 1.0)
:m_Lengt h(l v) . m_Widt h(wv ) . m_Helght(hv){ }
protected :
double m_Lengt h:
doub le m_Widt h;
double m_Height,
};
Wiersze na białym tle są takie same jak w poprzedniej wersji klasy CBox. W zasadzie klasa ta jest taka sama jak poprzednio, z wyjątkiem miejsca, w którym określiliśmy, że jest ona pochod nąklasy CConta iner. Funkcja VolumeO w tej klasie została w pełni zdefiniowana (jest to waru nek konieczny, jeżeli chcemy używać tej klasy do tworzenia obiektów). Drugim wyjściem, jakie nam pozostaje , jest określenie jej jako funkcji czysto wirtualnej , jako że została ona w ten sposób zdefiniowana w klasie bazowej, ale wtedy nie moglibyśmy tworzyć obiektów klasy CBox. Definicja klasy CCan znajduje
si ę
w pliku
nagłówkowym
Can.h i jest następująca :
II Plik n agłówkowy Can.h w projekcie Cw9 _la.
#pragma ance
#i nc l ude "Canta i ner. h" exte rn const dauble PI .
II Dołą cza defini cję klasy CCo ntainer.
II Zmienna Pl jest zdefini owan a gdzieś indziej.
class CCan: publ ic CCantainer (
publ ic: II Fun kcj a
ob liczająca pojemność puszki.
virt ual dauble Val ume( ) canst
( retur n 0.25*PI*m- Di ameter*m- Diamete r*m- Hei ght;
II Konstruktor.
CCa n(double hv = 4.0. double dv
=
2.0): m_Height (hv) , m_Diamete r (dv){}
pratected :
do uble m_Height :
dauble m_Diamet er :
I:
W klasie CCan znaj duje się równ ież definicja funkcji Vo7ume() oparta naformule hnr2, gdzie h oznacza wysokość puszki, a r promień puszki w przekroju poprze cznym. Pojem nos ć obliczana jest jako iloczyn wysokości i pola podstawy puszki. Wyrażenie w definicji funkcji zakłada, że stała globalna PI j est zdefi niowana. Z tego względu umieściliśmy na początku instrukcję extern informujqcq, że PI j est zmienną globalną typu const doub7e,
550
Visnal C++ 2005. Od podstaw zos tała zdefin iowana w innym miej scu - w tym p rogramie jej definicja znaj duj e w pliku źródlowym Cw9_ 1O.cpp. Warto równ ież zauważyć, że przedefiniowana zos tała
która się
funk cja ShowVo7ume ( ) w klasie CBox, ale nie w klasie CCan. Rezultat tego zobaczymy, kiedy uruchomimy program i obejrzymy dane wyjś cio we . Powyższe
możemy przetestować
klasy
rając ego funkcję
przy
użyciu następującego
pliku
źródłowego
zawie
main( ):
II Cw9j O. cpp II Używanie klasy abstrakcyjnej.
#lncl ude "Box. h" #incl ude "Can.h" #i ncl ude using st d: :cout : using std: .endl :
II Dla klas CBox i CContainer. II Dla klasy CCan (i CContainer). II Dla strumienia wejścia -wyjś cia .
const doubl e
II Definicja globalna zmie nnej Pl.
PI~
3.14159265:
int ma i n(void) { II Wskaźnik do abstrakcyj nej klasy bazowej II zainicjalizowany adrese m obiektu klasy CBox .
CContainer* pC1 = new CBox (2.0, 3.0. 4.0): II Wskaźn ik do abstrakcyjnej klasy bazowej II zain icjalizowany adrese m obiektu klasy CCan.
CContai ner* pC2
=
new CCan (6 5. 3.0) ;
pC 1->ShowVol ume (); pC2 ->ShowVo l ume() : cout « endl;
II Wysyłanie na wyjście pojemności obu II wskazywa nych obiek tów.
delete pC! : delete pC2:
II Czyszczen ie obszaru wolnej II ...
pamięci.
retu rn O:
Jak lo działa W programie tym zadeklarowaliśmy dwa wskaźniki do klasy bazowej CCanta i ner. Mimo że nie możemy tworzyć obiektów klasy CCanta i ne r (ponieważ jest to klasa abstrakcyjna), to możemy definiować wskaźniki do tej klasy, w których możemy następnie przechowywać ad resy obiektów klas pochodnych. W rzeczywistości w takim wskaźniku można przechowywać adres dowolnego obiektu klasy będącej pośrednią lub bezpośrednią pochodną klasy CConati ner, Wskaźnikowi pC I zostaje przypisany adres obiektu klasy CBox, utworzone go w obszarze wol nej pamięci przez operator new. Dru giemu wskaźnikowi w podobny sposób zostaje przypi sany adres obiektu klasy CCa n. Oczywiście
ze
musimy po
zakoń czeniu
de7ete .
względu
na fakt, że obiekty klasy pochodnej są tworzone dynamicznie, pra cy z nimi wyczyś ci ć wolną pamięć za pomo cą operatora
Rozdział 9.•
Rezultat
działania powyższego
Dziedziczenie i funkcje wirtualne
551
programu jest następujący:
obi ektu klasy CSox wynosl 24 wynos i 45 .9458
P oj emno ś ć u ż y t k owa P oj e mno ś ć
Jako że funkcję ShowVo l ume( ) zdefiniowaliśmy w klasie CBox, wersja tej funkcji w klasie po chodnej wywoływana jest dla obiektu klasy CBox. Nie z d e fi ni o wa l iś my jej w klasie CCa n, a wi ęc dla obiektu tej klasy wywoływana jest wersja tej funkcji znajdująca się w klasie ba zowej , z której dziedziczy klasa CCan. Ze wz gl ęd u na fakt, że funkcja wirtualna Vol umeO zo stała zaimplementowana w obu klasach pochodnych (jest to konieczne, gdyż jest to funkcja czysto wirtualna w klasie bazowej) , jej wersja jest wybierana w czasie działania programu poprzez wskazanie wersji należącej do klasy wskazywanego obiektu. W związku z tym dla w skaźnika pCl wywoływana jest wersja z klasy CBox, a dla wskaźnika pC2 wersja z klasy CCan. Dzięki temu za każdym razem otrzymujemy prawidłowy wynik. tylko jednego wskaźnika i przypisać mu adres obiektu klasy dla obiektu klasy CBox funkcji Vol ume ( ). Wskaźnik klasy bazowej może zawierać adres obiektu dowolnej klasy pochodnej , nawet j eże li je st ich kilka. W ten sposób uzyskujemy automatyczny wybór właściwej funkcji wirtualnej spośró d całego zestawu klas pochodnych. Nieźle , prawda? Równie dobrze
CCa n (po
mogliśm y u żyć
wywołaniu
Pośrellnie klasy bazowe Na początku rozdziału stwierdziłem, że klasa bazowa podkla sy może być pochodną jes zcze innej klasy. Zilustrujemy to na podstawie nieznacznie zmodyfikowanego kodu poprzedniego przykładu, a dodatkowo zobaczymy sposób użycia funkcji wirtualnych w przypadku dwupo ziomowego dziedziczenia.
R!lmIIjI Dziedziczenie wielopoziomowe Jedyne, co musimy
zrobić,
to
kładu . Powiązania pomiędzy
dodać definicj ę
tymi klasami
klasy CGl assBox do klas z poprzedni ego przy zilustrowane na rysunku 9.6.
zostały
Klasa CGl assBox - dokładnie tak samo jak poprzednio - jest pochodną klasy CBox, ale usu nęliśmy z klasy CG l assBox funkcję ShowVo 1umeO, aby pokazać, że jej wersja z klasy bazowej nadal działa poprzez klasę pochodną. Zgodnie z hierarchią widoczną na powyższym rysunku klasa CConat i ner jest pośrednią klasą bazową klasy CG l assBox oraz bezpośrednią klasą bazową klas CBox i CCa n. Plik
nagłówkowy
GlassBox.h w naszym projekcie zawiera następującą treść :
II Plik naglówkowy GlassBox.h w pr ojek cie Cw9_II .
#pragma once #incl ude "Box.h"
II Do lącza klas ę CBox.
class CGl assSox : publi c CSox
II Klasa pochodna.
{
publ t e :
552
VislJal C++ 2005. Od podstaw
Bezpośrednia klasa bazowa klasy CCan
Bezpośrednia klas a ' bazowa klasy CBo x Pośred nla klasa b azowa klasy CGlassBox
c1assCContainer
Bardziej ogólne
~
/
cia ss CCan
Bezpośrednia klasa
bazowa klasy CGlassBox
c1assCBox
\
c1assCGlassBox
Bardziej wyspecjalizowane
Rysunek 9.6 II Funkcj a obliczająca pojemność obiektu klasy CGlassBox
II pozosta wiają ca / 5% miej sca na materiał wype łn iający.
virt ua l doub le Volume() const
( ret urn 0.85*m_Length*m_Width*m_Height :
II Konstruktor.
CGl assBox( doubl e l v . doub le
Wy.
doubl e hv) : CBox(lv .
Wy .
hv){ }
}:
Pliki
nagłówkowe
Container.h, Can.h, i Box.h
Plik źródłowy nowego projektu z funkcją mai n() w hierarchii ma następującą treść:
#include "Can.h" // Dla klas CCan i Ctlo ntainer).
#i ncl ude "Gl assBox.h" I/ Dla klasy CGlassBox (i CBox oraz CContainer). #i nc l ude I/ Dla strumienia wejśc ia-wyjścia.
using st d: :cout : using st d: .endl : const double PI
=
3.14159265:
// Globalna defini cja zmienn ej PI.
int mai n() ( I/
Wskaźnik
do abstrakcyjn ej klasy bazowej zainicjalizowany adresem obiektu klasy CBox.
CContai ner* pC1 = new CBox(2 .0, 3.0. 4.0):
Rozdział 9.
• Dziellziczenie i funkcje wirtualne
553
CCa n myCan(6.5 . 3. O) ; II Definicja obiektu klasy CCan. CGlassBox myG lassBox(2.D. 3. D. 4.O): II Definicja obiektu klasy CGlassBox. pCl ->ShowVol ume(): delete pcl:
II Wysianie na ekran pojemności obiektu klasy CBox.
II Czyszczenie obszaru wolnej pamięci,
II zainicjalizowan ego adresem obiektu klasy CCan.
pCl = &myCan: II Ustaw wskaźnik na adres obiektu myCan.
pCl->ShowVo l ume( ) ; II Wyślij na wyjście pojemność obiektu klasy CCan.
pC l ~ &myGlas sBox: pCl->ShowVolume():
II Ustaw wskaźn ik na adr es obiektu myGla ssBox.
II Wyślij na wyj scie pojemność obiektu klasy CGlassBox.
cout « end l : return O;
Jak to dziala W programie tym mamy trzypoziomową hierarchię widoczną na rysunku 9.6, w której klasa CConta i ner jest klasą abstrakcyjną, ponieważ zawiera czysto wirtualną funkcję Vo l ume(). Funkcja ma i n( ) wywołuje funkcję ShowVo l ume ( ) trzykrotnie za każdym razem, używając tego samego wskaźnika do klasy bazowej , ale w każdym przypadku zawierającego adres obiektu innej klasy. Jako że funkcja ShowVo l ume( ) nie została zdefiniowana w żadnej z tutejszych klas pochodnych, za każdym razem wywoływana jest jej bazowa wersja . Oddzielne odgałęzienie od klasy bazowej CConat i ner definiuje klasę pochodną CCa n. Rezultat działania programu jest następujący:
obiekt u klasy CBox wynosi 24
wynosi 45.9458
u żyt k owa obiekt u klasy CBox wynosi 20,4
Po j emn o ś ć u żyt k owa P o j emn o ś ć Pojemno ść
Z danych wyjściowych wynika, z trzech wersji funkcj i Vo l ume ( ).
że
w
zależności
od typu obiektu
wywoływana jest
jedna
Zauważ. że prz ed przypisaniem nowego adresu do wskaźnika należy najpierw usunąć obiekt klasy CBox z obszaru wolnej pamięci. Jeżeli tego nie zrobimy, nie będziemy mogli lryczyś cić pamięci. ponieważ nie mielibyśmy żadnego zapisu adresu oryginalnego obiektu. Błąd ten można łatwo popełnić przy ponownym przyp isywaniu adresów wskaźnikom oraz używaniu obszaru wolnej pamięci.
Wirtualne destruktory Jednym z problemów, które mogą się pojawić podczas pracy z obiektami klas pochodnych przy użyciu wskaźników do klasy bazowej , jest wywołanie niewłaściwego destruktora. Mo żemy to zaobserwować, modyfikując poprzedni program.
554
Visual C++ 2005. Od pOlislaw
~ Wywoływanie niewłaściwego destruktora Aby dowiedzieć się, który destruktor jest wywoływany do niszczenia każdego obiektu, wystar czy do każdej klasy w poprzednim projekcie dodać destruktor wysyłający na ekran odpowiedni komunikat. Plik nagłówkowy Container.h wygląda zatem następująco: II Plik naglówkowy Container.h w projekcie Cw9 12.
#pragma once
#i nclude
using std: :cout;
using std : :endl:
class CContainer
II Bazowa klasa generyczna dla poszczególnych kontenerów.
(
public. II Destruktor.
-CConta iner()
{ cout « "Destruk tor klasy CConta iner
został wywoła ny ." «
end l : }
II Funkcja obliczająca pojemność - brak treści.
II Jest to funkcja czysto wirtualna, co zostało oznaczone przez '= O'.
virtua l double Volume() const = O;
II Funkcja
wyświetlająca pojemność.
vir tual void ShowVo lume () const (
cout
end l
« «
"Pojem ność
wynosi "
Vol ume() ;
«
}; Zawartość
II Plik
pliku
nagłówkowego
nagłówkowy
Can.h jest następująca:
Can.h w projekcie Cw9 12.
#pragma once
#incl ude "Conta i ner.h" exte rn cons t double PI;
-CGlass Box () { cout « "Dest rukt or klasy CGlassBox
zo sta ł wy'tlO ł a ny ."
II Funkcja obliczająca pojemność obiektu klasy CGlassBox II pozostawiają ca 15% miej sca na materiał wype łn iający.
virt ua l doub le Volume() const { ret urn 0.B5*m_l engt h*m_Widt h*m_Height: }
«
endl : }
555
556
Visnal C++ 2005. Od podstaw II Konstruktor.
CGlassBox(double l v, double
WY,
double hv): CBox(lv.
Wy.
hv){}
}:
Zawartość
ostatniego pliku
źródłowego
-
Cw9_12 .cpp jest następująca:
II Cw9 _12.cpp
II Wywolywanie destruktorów w klasach pochodnych
II przy użyciu obiektów poprzez wskaźnik klasy bazowej.
#1 nc l ude "Box. h" #i nclude "Cen. h" #i nclude "Gl assBox. h" #i nc l ude using std ' :cout:
usi ng st d: end l :
II Dla II Dla II Dla II Dla
const double PI = 3. 14159265 ;
II Globalna definicja zmiennej PI.
klas CBox i Ctlontoiner.
klasy CCan (i CContainer).
klasy CGlassBox (oraz CBox i CContainer).
strumienia wejscia-wyjscta.
int main()
{
II
Wskaźnik
do abstrakcyjnej klasy bazowej zainicjalizowany adresem obiektu klasy CBox.
CContainer* pCl = new CBox(2.0, 3.0. 4.0); CCan myCa n(6.5. 3. O): CGl assBox myGl assBox(2 O, 3 O. 4.O);
II Definicja obiektu klasy CCan.
II Definicja obiektu klasy CGlassBox.
Cl->ShowVolume( );
cout « endl « "Delete CBox"
delete pc!;
pC l = new CG lassBox(4.0. 5.0, 6.0); II Dynamiczne utworzenie obiektu klasy CGlassBox
pC l->ShowVolume(); II ... i wysianie na wyjście jego pojemności...
cout « endl « "Delete CGlassBox" « end l:
delet e pC l; 11...orazjego usunięcie.
pCl = &myCa n; pC l ->ShowVolume();
II Ustaw wskaźnik na adres obiektu myCan.
II Wyślij na wyjście pojemność obiektu klasy CCan.
pCl ~ &myGlassBox, pCl ->ShowVol ume() :
II Ustaw wskaźnik na adres obiektu myGlassBox.
II Wyslij na wyjście pojemnosć obiektu klasy CGlassBox.
cout « endl ;
return O:
Jak to działa Poza dodaniem do każdej klasy destruktora wysyłającego na wyjście komunikat infonnujący o efekcie wykonanej czynności, wprowadzonych zostało kilka zmian w funkcji ma i n() . Poja wiły się dodatkowe instrukcje dynamicznie tworzące obiekt klasy CGl assBox, wysyłające na ekran jego pojemność oraz go usuwające. Jest także komunikat informujący o usunięciu dy namicznie utworzonego obiektu klasy CBox. Rezultat działania tego programu widoczny jest poniżej:
Rozdział 9.
• Dziedziczenie ilunkcie wirtualne
557
P oje mn o ś ć uzyt kowa obiekt u klasy CBox wynosi 24 Delet e CBox Dest ruktor klasy CCont ai ner zost ał wywo łany . Pojemność u ż y t k owa obiekt u klasy CBox w ynosi 102 Delet e CGlassBox Dest ruktor klasy CConta iner zo s ta ł wywoła ny.
wynosi 45.9458 oblektu kl asy CBox wynos i 20.4 Dest ruktor klasy CGlass Box z o s t ał wywo ł a ny. Dest rukt or klasy CBox zo stał wywo ł a ny . Dest ruktor klasy CCont ai ner zos tał wywo łany . Dest ruktor klasy CCan zo sta ł wywo ł a ny . Dest ruktor klasy CCont ainer zos t ał wywoła ny . Pojemn o ś ć
P o j emno ść użyt k owa
Z powyższych danych wynika, że w momencie usunięci a obiektu klasy CBox wskazywanego przez wskaźnik pCl wywoływany jest destruktor klasy bazowej CCont ai ner , ale nie ma infor macji na temat wywołania destruktora klasy CBox. Podobnie w momencie usunięcia dodanego przez nas obiektu klasy CG la ssBox wywołany zostaje destruktor klasy bazowej CCont ai ner , a nie destruktor klasy CG l assBox lub klasy CBox. W przypadku pozostałych obiektów poprawne wywołania destruktora mają miejsce, gdy najpierw wywołany jest konstruktor klasy pochod nej, a po nim następuje wywołanie konstruktora klasy bazowej. Dla pierwszego obiektu klasy CGl assBax utworzonego w deklaracj i wywołane zostały trzy destruktory: jako pierwszy wywo łany został destruktor klasy pochodnej, po nim destruktor bezpośredniej klasy nadrzędnej , a na końcu destruktor pośredniej klasy nadrzędnej. Wszystkie problemy związane są z obiektami tworzonymi w obszarze pamięci wolnej. W obu przypadkach wywoływany jest niewłaściwy konstruktor. Powodem tego jest to, że wiązanie do destruktorów jest tworzone statycznie w czasie kompilacji . W przypadku obiektów auto matycznych nie stanowi to problemu - kompilator wie, czym one s ą, i powoduje wywoła nie odpowiednich destruktorów. W przypadku obiektów tworzonych dynamicznie , do których dostęp uzyskuje się poprzez wskaźnik, rzecz wygląda inaczej. Jedyną informacją, którą dys ponuje kompilator w momencie wykonywania operacji usuwania (del et e), jest to, że wskaź nik jest typem wskaźnika do klasy bazowej. Kompilator nie zna typu obiektu rzeczywiście wskazywanego przez wskaźnik, ponieważ jest on określany w czasie działania programu. A zatem jedyne, co robi kompilator, to upewnienie się , że operacja usuwania zostanie usta wiona w celu wywołania destruktora klasy bazowej. W prawdziwym programie może to spo wodować wiele problemów zależnych od natury samych obiektów (na przykład fragmenty obiektów porozrzucane po obszarze wolnej pamięci ) . Rozwiązanie tego problemu jest proste. Musimy spraw i ć, aby wywołania były dokonywane dynamicznie - w czasie wykonywania programu. Możemy to zorganizować, stosuj ąc w swo ich klasach destruktory wirtualne. Jak już wspominałem przy okazji pierwszego omawia nia funkcji wirtualnych, wystarczy jako wirtualną zadeklarować tylko funkcję w klasie bazo wej, aby wszy stkie funkcje o takiej samej nazwie, li ści e parametrów i typie zwracanym we wszystkich klasach pochodnych były również wirtualne . Ta zasada ma zastosowanie zarówno do destruktorów, jak i zwykłych funkcji składowych . Do definicji destruktora w klasie CCan ta i ner zdefiniowanej w pliku nagłówkowym Conatiner.h należy dodać słowo kluczowe vi r tual. Definicja tej klasy powinna wyglądać następująco:
558
Visual C++ 2005. Od podstaw class CContai ner
II Bazowa klasa generycz na dla poszczegó lny ch kontenerów.
(
pub l i c: II Destruktor.
virt ual -CContal ner()
{cout« "Dest rukto r klasy CCont ai ner
zo sta ł wywo ł any . "
«endl: }
II Reszta klasy jak wcześn iej.
); Dzięki
tej zmianie destruktory we wszystkich klasach pochodnych automatycznie również wirtualne , mimo że nie określiliśmy ich jako takie w sposób jawny. Oczywiście, jeżeli chcemy, aby kod był jeszcze bardziej przejrzysty, możemy słowo kluczowe vi rt ual dodać do wszy stki ch destruktorów . staną s ię
Po dokonaniu
powyższych modyfikacji
rezultat
działania programu j est następujący;
Poj emno ść użyt k ow a ob iektu klasy CBox wynos i 24
De let e CBox
Destrukto r klasy CBox z o s ta ł wywo ła ny .
Destru ktor kl asy CCont ainer z o st ał wywo ła ny .
Po j em n o ś ć u ż yt k ow a ob iektu klasy CBox wynos i 102
Delet e CGl assBox
Dest rukt or kl asy CGlassBox zos ta ł wywo ła ny.
Dest ruktor klasy CBox zos t a ł wywoł a ny.
Dest ruktor klasy CContainer z os ta ł wywo ł a ny .
wynosi 45.9458
obiekt u kl asy CBox wynosi 20 .4
Dest rukt or klasy CGl assBox zost ał wyw ołan y.
Destruktor klasy CBox zost ał wy wo łany .
Dest rukt or klasy CContai ner z os t ał wywoł a ny.
Dest ruktor klasy CCan z o s t a ł wywo ł a n y .
Dest rukt or klasy CContainer zo s ta ł wywo ł a ny .
P o jemn o ś ć
Poj e m no ść u żyt k owa
Jak widać , wszystkie obiekty są teraz niszczone przez destruktory w odpowi edniej kolejnoś ci. Niszczenie obiektów dynamicznych powoduje wywołanie destruktorów w takiej samej kolej ności jak obiektów automatycznych w programie. W tym momencie powst aje pytanie: "Czy konstruktory można deklarować jako wirtualne?". brzmi : "nie" - tylko destruktory i inne funkc je składowe.
Odpowiedź
Dobrym nawykiem jest zawsze deklarowanie destruktora klasy bazowej jako wirtual nego - oczywiście, gdy korzysta s ię z mechanizmu dziedziczenia. Destruktory wirtualne klas działają nieco wolniej , ale w większości przypadków j est to całkowicie niezauważalne. Zastosowanie destruktorów wirtualnych daje gwaran cję , że obiekty zos taną zniszczon e we właściwy sposób i dzięki temu udaje się uniknąć załamań programu, które mogą nastą pić w innym przypadku.
Rozdzial9. - Dziedziczenie i funkcje wirtualne
559
Rzutowanie pomiędzy typami klasowymi
Wiemy już, w jaki sposób można zapisać adres obiektu klasy pochodnej w zmiennej typu klasy bazowej - np. zmienna typu CCont ai ner* może przechowywać adres obiektu klasy CBox. Czy można zatem, mając adres przechowywany we wskaźniku typu CContai ner*, rzutować go do typu CBox*? Odpowiedź brzmi: "tak" - do tego celu służy operator dynami c_cast. Poniżej znajduje się przykład jego zastosowania:
CConta i ner* pContai ner = new CGl assBox( 2.0. 3.0, 4.0);
CBox* pBox = dynami e_east ( pContainer) ;
CGlassBox* pGlassBox = dynamie east ( pContainer ) ;
Pierwsza instrukcja zapisuje adres obiektu klasy CGlassBox utworzonego na stercie we wskaź niku klasy bazowej typu CCont ai ner*. Druga instrukcja rzutuje wskaźnik pCont ai ner do góry w hierarchii klasowej do typu CBox*. Trzecia instrukcja rzutuje adres przechowywany we wskaźniku pContai ner do jego rzeczywistego typu - CGl assBox*. Operatora dynam ic_cast można używać nie tylko do wskaźników, ale i referencji. Różnica operatorami dynami c_cast i stat ic_cast jest taka, że ten pierwszy sprawdza po prawność rzutowania w czasie działania programu, a drugi nie. Jeżeli operacja wykonywana przez operator dynamic_cast jest niepoprawna, to zwracana jest wartość nul l. W przypadku użycia operatora stat ic_cast kompilator całkowicie polega na programiście, jeśli chodzi o poprawność wykonywanej operacji, a więc lepiej jest zawsze używać operatora dynamic_ cast do rzutowania w górę i w dół hierarchii oraz sprawdzać, czy nie zostaje zwrócona wartość nu 11 , jeżeli chcemy uniknąć nagłego zakończenia programu, które może nastąpić w wyniku użycia zerowego wskaźnika. pomiędzy
Klasy zagnieżdżone Definicję
jednej klasy można umieścić wewnątrz definicji innej klasy i nazywa się to zagnież klas. Klasa zagnieżdżona jest statyczną składową klasy ją zawierającej i - podob nie jak inne składowe - podlega działaniu specyfikatorów dostępu do składowych . Jeżeli zagnieżdżoną klasę umieścimy w prywatnej sekcji innej klasy, to dostęp do niej będzie można uzyskać wyłącznie z wnętrza klasy ją zawierającej . Jeżeli natomiast klasę zagnieżdżoną okre ślimy jako publiczną, to dostęp do niej można będzie uzyskać także spoza klasy ją zawie rającej, ale przy użyciu specyfikatora w postaci nazwy klasy zewnętrznej . dżaniem
Klasa zagnieżdżona ma dostęp do wszystkich statycznych składowych klasy nadrzędnej . Do wszystkich składowych egzemplarza dostęp można uzyskać poprzez obiekt typu klasy nad rzędnej, wskaźnik lub referencję do tego obiektu. Klasa nadrzędna ma dostęp tylko do publicz nych składowych klasy zagnieżdżonej . Ale w klasach zagnieżdżonych, które znajdują się w sekcji prywatnej klasy nadrzędnej, składowe często deklaruje się jako publiczne, aby umożli wić wolny dostęp do nich funkcjom klasy nadrzędnej.
560
ViSUiII C++ 2005. Od podstaw Zagnieżdżanie klas jest szczególnie przydatne w przypadkach, gdy chcemy zdefiniować typ, który będzie używany wyłącznie w obrębie innego typu, przy czym klasa zagnieżdżona może być zadeklarowana jako prywatna. Na poniższym listingu przedstawiono przykład zagnież dżania klas:
II Stos prze c howują cy obiekty klasy CBox.
class CStack {
pri vate: II Definicj a elementó w do p rzechowywania na stosie.
struct C!tem {
CBox* pBox; Cltem* pNext:
II II
Ws kaźnik Wskaźnik
do obiektu w tym węźle. do następn ego elementu stosu lub null.
II Utworzeni e nowego obiektu i umieszczenie go II na samej górze.
II Wyrzucenie obiektu ze stosu.
CBox* Pop() {
i f (pTop == O) ret urn O: CBox* pBox = pTop ->pBox: Clt em* pTemp = pTop: pTop = pTop->pNext ; delete pTemp;
II Jeżeli s tos jest p usty, II zwra ca null. II Pobierz obiekt Box z elementu. II Zapisuje adres elementu znajdują cego s ię II na samej górze. II Ustawiani e następnego elem entu II na samej gó rze. II Usunięcie starego elementu z sam ej góry l/ze sterty.
retur n pBox: };
Klasa CStack definiuje stos do przechowywania obiektów klasy CBox. Aby być całkowicie pre cyzyjnym , to przechowuje on wskaźn iki do obiektów klasy CBox, a więc wskazywane obiekty nadal są zależne od kodu robiącego użytek z klasy CSta ck. Zagnieżdżona struktura Clte rn definiuje elementy przechowywane na stosie. Zdecydowałem się zdefiniować I te rn jako zagnieżdżoną strukturę, a nie klasę, gdyż składowe struktury 'są domyślnie publiczne. Mogliby śmy zdefiniować CI t em jako klasę, a następnie określić jej składowe jako publiczne, aby były dostępne dla funkcji klasy CSt ack. Stos został zaimplementowany jako zbiór obiektów struk tury CItem, gdzie każdy obiekt tej struktury przechowuje wskaźnik do obiektu klasy CBox wraz z adresem następnego obiektu Cltem położonego niżej na stosie. Funkcja Push( ) w klasie CSt ad przesuwa obiekt klasy CBox w stronę wierzchołka stosu, a funkcja Pop( ) usuwa obiekt ze stosu.
Rozdział 9.
• Dziedziczenie i funkcie wirtualne
561
Dodawanie obiektu do stosu polega na utworzeniu nowego obiektu struktury Cl t em, zawie rającego adres przechowywanego obiektu oraz adres poprzedniego elementu, który b ył na wierzchołku stosu - przy dodawaniu pierwszego elementu do stosu jest to wartość zerowa. Usuwanie obiektu ze stosu zwraca adres obiektu w elemencie pTop. Usunięty zostaje element z wierzchołka i jego miejsce zastępuje element, który znajdował się pod nim . Zoba czmy, jak to działa .
l1I!!mm Używanie zagnieżdżonych klas W programie tym zostały wykorzystane klasy CCont ai ner, CBox oraz CGl assBox z projektu 9_12. Tworzymy więc pusty projekt konsolowy WIN32 o nazwie Cw9_13 oraz dodajemy do niego pliki nagłówkowe zawierające defini cje wymienionych klas. Następnie dodajemy do projektu plik nagłówkowy Stack.h zawierający defin icję klasy CStack, którą mamy wyżej, oraz plik Cw9 j 3.cpp o następującej zawartości: II Cw9_13. cpp
II Używanie zagnieżdżonych klas do zdefiniowania s tosu.
#incl ude "Box.h" II Dla klas CBox i CContainer.
#incl ude "GlassBox.h" II Dla klasy CGlassBox (i CBox oraz CContainer).
#inc l ude "Stack.h" II Dla klasy reprezentują c ej stos z zagn ieżdżoną
II strukturą Item.
#include usi ng std : :cout ;
using st d: :endl :
II Dla strumienia
wejścia-wyjścia.
int mainO (
CBox* pBoxes[]
new CBox(2.0. 3.0. new CGlassBox(2.0. new CBox( 4.0. 5.0. new CGl assBox( 4.0.
=
4.0).
3.0. 4.0).
6.0).
5. 0. 6.0)
};
cout« " Pude łka w t ablicy m a j ą for Ci nt i = O ; iShowVolume() ;
na st ę pu j ą c e poj em nośc i : " ;
II Wysyłan ie na wyjście pojemności pudełka.
cout « endl « endl
« "Dodawanie pu deł e k do st osu.
« endl :
CStack* pSt ack = new CStack ; for (i nt i = O : iPush(pBoxes[i]) ;
II Utworzenie stosu .
cout « "Usuwanie p u d eł ek ze st osu prezent uje j e w odwróconej for (i nt i = O : iPop ()- >ShowVol ume ( ): cout « endl :
return O:
k ol ej n ości
:";
562
lisual C++ 2005. Od podstaw Rezultat działania tego programujest następujący : Pudełka
w tabl icy
ma J ą n a s tę puj ą c e po jemnośc i :
Pojemność użytkowa Pojemność użyt kowa Po jem no ś ć użytk owa Pojemność u ży t k owa
obiektu obiektu obie ktu obiektu
klasy klasy klasy klasy
CBox wynosi CBox wynosi CBox wynosi CBox wynosi
24
20 .4
120
102
Dod awa nie p ud e łek do stosu .. .
Usuwanie pu dełek ze stosu prezentuje je w odwrotnej P ojemno ść u ży tkowa obiekt u klasy CBox wynosi 102
P o j emn o ś ć u ży tkowa obiektu klasy CBox w ynosi 120
Pojemność użytkowa obiekt u klasy CBox wynosi 20 4
Po j emn o ś ć użytk owa obiektu klasy CBox wynos i 24
k olej nośc i :
Jak to działa do obiektów klasy CBox, dzięki czemu każdy element tablicy adres obiektu klasy CBox lub adres dowolnego typu pochodnego od klasy CBox. Tablica jest inicjalizowanaadresami czterech obiektów utworzonych na stercie:
Tworzymy
tablicę wskaźników
może przechowywać
CBox* pBoxes[]
=
(
new CBox(2.0. 3.0. new CGlassBoxC2 .0. new CBoxC4 .0. 5.0. new CGlassBox C4.0,
4.0) .
3.0 . 4.0).
6.0) ,
5.0 . 6.0)
}; Wśród obiektów znajdują się dwa obiekty klasy CBox i dwa obiekty klasy CGl assBox o takich samychwymiarach co obiekty klasy CBox.
Po liście pojemności wszystkich czterech obiektów tworzymy obiekt klasy CStack i umiesz czamy na stosie obiekty za pomocą pętli for: CStack* pSt ack = new CStack ; for Ci nt i = O ; iPush(pBoxes[ i]) ;
II Utworzenie stosu.
Każdy element tablicy pBoxes umieszczany jest na stosie poprzez przekazanie tablicy elementu jako argumentu funkcji Push() obiektu klasy CStack. W rezultacie pierwszy element tablicy znajduje się na samymdole stosu, a ostatni na samej górze.
Obiekty ze stosu usuwamy za pomocą innej
pętli
for :
for Ci nt i = O ; i PopC)->ShowVolume();
Funkcja Pop( )zwraca adres elementu znajdującego się na samej górze stosu. Za pomocą tego adresu wywoływana jest dla obiektu funkcja ShowVol ume O . Jako że ostatni element był na wierzchu stosu, pętla ustawia pojemności obiektów w odwróconej kolejności. Z danych na wyjściu widać, że klasa CSt ack rzeczywiście implementuje stos przy użyciu zagnieżdżonej struktury definiującej elementy do przechowywania na stosie.
Rozdział 9.
• Dziedziczenie i lunkcie wirtualne
563
Programowanie wC++/CLI
Wszystkie klasy w C++/CLI, włącznie z klasami zdefiniowanym i przez programi s tę , są do myślnie pochodne. Dzieje s i ę tak ze względu na fakt, że zarówno klasy warto śc i, jak i klasy referencyjne są poch odnymi klasy bazowej Syst em: :Object. Oznacza to, że klasy wartości i klasy referencyjne d ziedzi czą od klasy Sys t em: :Objeet, a zatem to, co je łączy, to wspólne możliwości odziedziczone po tej klasie. Jako że klasa ToSt ri ng() w klasie System: :Obj ect jest zdefiniowana jako funkcja wirtualna, można ją prze słoni ć we własnej klasie i wywoływać polimorficznie, kiedy jest to wymagane. To właśnie robili śmy za k ażdym razem, gdy wywo ływali śmy tę funkcj ę w klasach w poprzedn ich rozdziałach . Klasa bazowa wszystkich typów klas wartoś ci System : :Object jest również odpow iedzialn a za pakowanie (ang. boxing) i odpakowywanie (ang. unboxing) warto ści typów fundamental nych. Proce s ten sprawia, że warto śc i typów fundamentalnych mogą zachowyw a ć się jak obiekty, ale mogą być używane w działan iach liczbowych bez obciążenia spowodowanego byciem obiektem. Wartości typów fundamentalnych są przechowywane po prostu jako warto śc i dla celów standardowych działań i są konwertowane do obiekt ów wskazywanych przez uchwyt typu System: :Object ", kiedy muszą zachowywać się jak obiekty. W ten sposób mamy możliwość traktow ania warto ści fundamentalnych jako obiektów, kiedy tego potrzebujemy, pom ijając wady, które mają obiekty .
Dziedziczenie wC++ICLI klasy wartości mają zawsze klasę Syst em: :Object jako bazową, nie mo żna zdefi klasy wartości na bazie istniejącej klasy. Mówiąc inaczej, definiując klasę wartości, nie możemy określi ć jej klasy bazowej . Oznac za to, że polim orfizm w przypadku klas warto ś ci ograniczony jest do funkcji wirtualnych klasy System: :Obj ect . Poniższa tabela zawiera funkcje wirtualne dziedziczone przez klasy wartości po klasie Syst em: :Obj ect : Mimo
że
n iować
Funkcja
Opis
St ring ToSt ringC )
Zwraca ła ń cu chow ą r epre z en t a cję ob iektu . Implementacj a tej funkcji w klasie System : :Obj ect zwraca nazw ę klasy j ako łańcu ch . Zazwyczaj we własnych klasach funk cję tę dostosowuje się w taki sposób, aby z wr ac ała łań cuchow ą reprezentację w art o ści obiektu .
bool Equa lsCObject ob i)
Porównuje bie żący obiekt z obiektem obi i zwraca t rue, jeżeli są one równe, lub fa l se w przeciwnym przypadku. Równość w tej funkcji oznacza równ o ść referen cyjną to znaczy obiekty s ą tym samym obiektem. Najczę ściej funkcję tę modyfikuje się, aby zwracała t rue, gdy bieżący obiekt ma taką s a mą wartoś ć co argum ent inaczej m ówiąc , kiedy pola są równe .
int Get Ha shCodeC)
Zwr aca
A
A
liczbę całkow itą będącą kodem miesz ającym.
Kody mies zające klu cze do prze chowywania obiektów w kolekcji przechowującej (klucz, obiekt) pary. N a st ępnie obiekty w takiej kolekcji są odnajdowan e poprzez podanie klucza użytego podczas zapisywania obiektu. używane są jako
564
Visnal C++ 2005. Od podstaw Oczyw i śc ie ze względu na fakt, że System: :Object jest klasą bazową także dla klas referen cyjnych , funkcje te można przesłaniać również w tych klasach. Klasę referencyjn ą można utworzyć jako pochodną innej klasy referencyjnej w taki sam spo sób, jak definiowali śmy klasy pochodne w natywnym C++. Spróbujemy teraz dokonać ponow nej implementacji projektu Cw9_12 jako programu w C++/CLI i w ten sposób zobaczymy równi eż zagnieżdżanie klas w programach CLR. Możemy zacząć od klasy Cont ai ner : nagłówkowy
II Plik
Contain er.h w projekcie Cw9_14.
#pragma once
using namespace Syst em:
II Abstrakcyjna klasa bazowa dla poszczególny ch konten erów.
ref class Contain er abst ract (
publ ic:
II Funkcja ob liczająca pojemność - brak treści.
II Funkcja ta zos ta ła zdefiniowana jako abstrakcyjnafunkcja wirtualna,
II na co wskazuj e słowo kluczowe 'abstract'.
vi rt ua l double Vo l ume() abstract : II Funkcja
wyświe tlająca pojemność.
virtual void ShowVol ume() {
Console: :Wr it el ine(l "Pojemno ś ć wynos i (O}". Volume() ):
}
}:
Pierws ze, na co trzeba zwróci ć uwagę , to słowo kluczowe abstra ct po nazwie klasy . Jeżeli klasa w C++/CLI zawiera odpowiedn ik funkcji czysto wirtualnej w natywnym C++, to należy ją zdefiniowa ć jako abstrakcyjną (abstraet). Jednak jako abstrakcyjną można również okre ślić klasę niezawierającą żadnych funkcji abstrakcyjnych, co powoduje, że nie można tworzyć obiektów tej klasy. Słowo kluczowe abst ract pojawiło się także na końcu deklaracji funkcji składowej Volume( l , co oznacza, że funkcja ta zo stała zdefiniowana dla tej klasy. Na końcu deklaracji funkcji składowej Vo l ume() można także dodać zapis ~O , podobnie jak w natywnym C++, ale nie jest to tutaj wymagane. Zarówno funkcja Vol ume( l , jak i ShowVo l ume() są funkcjami wirtualnymi, a więc mogą być wywoływane polimorficznie dla obiektów typów klasowych pochodzących od klasy Cont ai ner. Definicja klasy Box przedstawia się II Plik
n agłówkowy
następująco:
Box.h w proj ekcie Cw9_J4.
#pragma once #include "Cont ainer .h"
II Dołącza definicję klasy Container.
ref class Box
II Klasa pochodna.
Conta iner
(
publ ic. II Funkcja pokozująca pojemnoś ć obiektu.
virtual void ShowVolume( ) overri de {
Console : :Writ el i ne( L" P oj emno ś ć Volume() ):
użyt kowa
ob iektu klasy Box wynosi {O}" .
Rozdział 9.
• Dziedziczenie i lunkcje wirtualne
565
II Funkcj a obliczająca pojemność obiektu klasy Box.
vi rtual doub le VolumeC ) override { return m_Length*m_Width*m_Height ; }
II Konstruk tor.
BoxC)
m_Lengt hCl .O ). m_W idt hCl .O). m_HeightC l Oli }
II Konstruktor.
BoxCdouble l v. doubl e wy . double hv)
: m_LengthCl v) . m_Width Cwv). m_HeightChv){ }
prot ect ed:
doubl e m_Length:
double m_Wi dt h:
double m_Height :
l: Klasa bazowa klasy referencyjnej jest zawsze publiczna i słowo kluczowe pub l i c jest sto sowane domyślnie . Można taką klasę bazową zdefiniować jako publiczną w sposób jawny, ale nie jest to konieczne . Klasa bazowa klasy referencyjnej nie może być inna niż publiczna. Jako że w przeciwieństwie do wersji klasy w natywnym C++, tutaj nie możemy podać wartości domyślnych parametrów, zdefiniowaliśmy konstruktor bezargumentowy inicjalizujący wszyst kie trzy pola wartością l . O. KJasa Box( ) definiuje funkcję Vol ume( ) jako zmodyfikowanąwer sję jej wersji odziedziczonej po klasie bazowej. Aby przesłonić funkcję klasy bazowej , należy zawsze używać słowa kluczowego overri de. Gdyby klasa Box nie implementowała funkcji Vol ume(), to byłaby klasą abstrakcyjną i konieczne byłoby określenie jej jako takiej, aby można było ją skompilować .
Poniżej
II Plik
znajduje się kod klasy GlassBox: nagłówkowy
GlassBox .h w projekcie Cw9_14.
#pragma once #incl ude .. Box.h"
II Dołqcza klasę Box.
ref cla ss Glass Box : Box
II Klasa p ochodna.
{
publ ic: II Funk cja obliczająca pojemność obiektu Glasslłox, II poz ostawiają ca 15% na materiał wypełniający.
vi rtual double Vol umeC) overr ide { ret urn O.85*m_Lengt h*m_Widt h*m_Height ; } II Konstruktor.
GlassBoxC double lv. double
WY.
double hv) : BoxC l v.
Wy.
hv){}
}:
Klasą bazowąjest klasa Box, która domyślnie jest publiczna. Reszta klasy niczym nie różni od poprzedniej wersj i. Poniższy
kod definiuje
klasę
St ack:
II Plik nagłówkowy Stack.h dla pr ojektu Cw9_ 14.
II Stos przechowujący obiekty dowolnej kłasy referen cyjnej.
#pragma once
się
566
Visual C++ 2005. Od podstaw ref clas s St ack
{
pri vat e:
II Definicja elementów do przechowywania na stosie.
ref struct It ern {
Obj ect A Obj : It .em" Next ;
II Uchwyt do obiektu w tym elemencie. II Uchwy t do elementu na stosie lub nullptr.
II Konstruktor.
It ern(Object A obj . It ern A next l : Obj (obj l, Next (nextl{}
}:
It ern Top: A
II Uchwy t do elementu, który znajduj e s ię na wierzc hu.
public: II Dodani e obiektu do stosu.
void Push(Object obj l A
(
Top = gcnew It ern(obj . Top):
II
Usun ięcie
II Utworze nie nowego elementu i umieszczenie go II na wierzchu.
obiektu ze stosu.
Object " Pop( l (
if( Top == nu l l ptr ) ret urn nu11 ot r :
II Jeżeli stos jest p usty,
Il zwraca nułlptr.
Objeet Aobj = Top ->Obj ; Top = Top->Next : ret urn obj;
II Pobi eranie obiektu z elementu.
II Wstawienie następn ego elementu na wierzch.
} }:
Pierwsza różnica, na którą należy zwrócić uwagę, to fakt, że parametry funkcji oraz pola są teraz uchwytami, ponieważ mamy do czynienia z obiektami klasy referencyjnej . Wewnętrzna struktura It em przechowuje uchwyt typu Coj ect ", dzięki czemu obiekty dowolnej klasy CLR mogą być przechowywane na stosie. Oznacza to, że można przechowywać zarówno obiekty klas wartości , jak i klas referencyjnych, co stanowi znaczne ulepszenie w stosunku do klasy eSt ack w natywnyrn C+t. Nie trzeba zaprzątać sobie głowy usuwaniem obiektów, kiedy zostaje wywołana funkcja Pop( l, ponieważ system usuwania nieużytków zrobi to za nas. Poniżej
znajduje
się
lista
ró żnic pomiędzy
• Tylko klasy referen cyjne
mogą być
klasami w natywnym C++ i w C++/CLI: typami klas pochodnych.
• Klasa bazowa klasy pochodnej typu referencyjnego jest zawsze publiczna. • Funkcja nieposiadająca żadnej definicji klasy referencyjnej jest funkcją abstrakcyjną i musi być zadeklarowana przy u życiu słowa kluczowego abst raet. lub w ięcej funkcji abstrakcyjnych musi zosta ć jawnie jako abstrakcyjna poprzez postawienie słowa kluczowego abst ract po jej nazwie.
• Klasa
zawierająca jedną
okre ślona
RozdzlaJ 9.• Dziedziczenie i funkcje wirtualne Klasa niezawierająca żadnych funkcji abstrakcyjnych może być abstrakcyjna. Nie można two rzyć egzemplarzy takiej klasy.
• •
przed
funkcj ą n adpisującą funkcję odziedziczoną po
561
określona jako
klasie bazowej należy postawić
słowo kluczowe over r-tde,
Jedyne, czego potrzebujemy do wypróbowania naszych klas, to projekt konsolowy CLR z definicją funkcji mai n() .
~ Używallie pochodnych klas referencyjnych Utwórz nowy projekt CLR o nazw ie Cw9 _14 i dodaj do niego klasy zdefiniowane powyżej . dodaj plik źródłowy Cw9_14.cpp o na stępującej treści :
Następnie
II Cw9_J4 .cpp : main p roject file. II Definiowanie stosu za p omocą zagnieżdżonych klas.
#i nc lude #incl ude #incl ude #include
"stdafx.h" "Box.h" "Gla ssBox.h" "St ack.h"
II Dla klas Box i Container. II Dla klasy GlassBox (i Box oraz Container). II Dla klasy reprezen tującej stos z zagnieżdżoną struktura item.
using namespace Syst em: int mainCarray<Syst em: :St ring Ą > Ą a r g s ) I ar ray< BoxĄ >Ą boxes = ( gcnew BoxC 2.0. 3.0. gcnew Glass BoxC2 .0. gcnew Box C4. 0. 5.0. gcnew GlassBoxC4 .0,
4.0). 3.0. 4.0) , 6.0) . 5.0. 6.0)
}:
Console: : Wr i te L i ne C L " P ude łka w t ablicy ma j ą n a st ę puj ą c e pojem no~ ci :"): for eachC Box box in boxes ) box ->ShowVo l umet ): II Wysyłanie na wyjście pojemności pudelka . A
Console: :WriteLineCL" \nDodawanie st ack = gcnew St ack: for eac h CBo x box in boxes) st ack->PushC box):
Sta ck
Ą
pude łe k
do st osu. . . n ) ;
II Utworzenie stosu.
Ą
Console : :Writ eLi neC L"Usuwanie p u de łek ze st osu prezent uje je w odwróconej Ob j ect item; while CCitem = st ack->PopC) ) != null ptr ) Ą
sa fe_cas t C item) - > S howVo lume C) ;
Console: :WriteLineCL" \nWst awi anie li czb ca łkow ityc h do sto su:"); t orr int i = 2 : i < ~1 2 : i += 2) (
Conso le : :Write CL"{0.5j" .i ); stack->PushC i) :
ko le j no~ c i : ") :
568
Visual C++ 2005. Od podstaw Console: :WriteLi neCL"\ n\ Rezultat u sunięcia l iczb c a ł k ow i tyc h ze st osu je st n as tę p u j ący: " ); whileC Citem = stack ->Pop()) != null ptr ) Console: :Writ eCL "{O, 5}" . item) : Console : :Writ eLi ne(): return O: Rezultat
działania
Pudeł k a
tego programu jest następujący:
w t abl icy
Pojemnoś ć u żytkow a P oj emno ś ć u żytkow a
Pojemność u żytkowa Poj emn o ś ć u ży t k owa
mają na stępują ce pojemno ś c i :
obiektu obiekt u obiektu obiekt u
klasy klasy klasy klasy
Sox wynosi Box wynosi Box wynosi Box wynos i
24 20.4 120 102
Dodawanie pudełek do stosu. . . Usuwani e pud ełek ze st osu prezentuje je w odwróconej Pojemn o ść u ży tk owa obiekt u klasy Box wynos i 102 Pojemność u żytkowa obie kt u klasy Box w ynosi 120 Pojemno ś ć u żyt kowa obiektu klasy Box w ynosi 20,4 Poj em no ść u żytkowa obiektu klasy Box wynos i 24 Wstawianie li czb ca ł k owity c h do st osu: 2 4 6 8 10 12 Rezultat u sunięcia liczb c a łkowi tyc h ze stosu j est 12 10 8 6 4 2
Jako że klasy Box i Gl assBox są klasami referencyjnymi, ich obiekty tworzymy na stercie CLR za pomocą operatora genew. Adresy tych obiektów inicjalizują elementy tablicy boxes. Następnie
tworzymy obiekt St aek i ustawiamy łańcuchy na stosie: A II Utworzenie stosu. St ack st ack = gcnew St ack : for each CBoxA box i n boxes) st ack ->PushCbox):
Parametr funkcji Push( ) jest typu Ooject", a więc funkcja przyjmuje jako argument dowolny typ klasowy. Pętla for eaeh ustaw ia po kolei wszystkie elementy tablicy boxes na stosie. Usuwanie elementów ze stosu odbywa się w pętli whi l e: Ooject " item:
whil eC (item = st ack->PopC)) 1= nu ll ptr) safe cast Ci tem)->ShowVo l ume C);
Rozdział 9.
• Dziedziczenie i funkcje wirtualne
569
Warunek p ętli przechowuje wartość zwróconą przez funkcję Pop( ) wywołaną dla obiektu klasy stack w elemencie item i porównuje ją z null ptr. Instrukcja w ciele pętli whi l e wykonywana jest, dopóki itemnie zostanie ustawion y na nul l ptr. Wewnątrz pętli uchwyt przechowywany w itemjest rzutowany do typu Cont atne r" . Zmienna item jest typu Object i ponieważ w klasie Object nie zdefiniowano funkcji ShowVolume ( ), nie można jej wywoływać przy użyciu uchwytu tego typu. Aby wywołać tę funkcję polimorficznie, musimy użyć uchwytu typu klasy bazowej, w której funkcja ta jest zadeklarowana jako składowa wirtualna. Dzięki rzutowaniu uchwytu do typu Corrt at ner" możemy wywołać funkcję ShowVol umeO polimorficznie. Dzięki temu funkcja ta jest wybierana dla ostatecznego typu klasowego obiektu wskazywanego przez uchwyt . W tym przypadku ten sam efekt można było uzyskać, rzutując item do typu Box Użyli śmy tutaj rzutowania bezpiecznego (saf e_cast), ponieważ odbywa się ono w górę hierarchii, a w takich przypadkach najlepiej jest sprawdzać rzutowanie. Operator safe_cast sprawdza poprawność rzutowania i, jeżeli konwersja się nie powied zie, powoduje wyjątek typu Syst em: : Inval i dCastException. Mogliśmy także zastosować operator dynamic_cast, ale w programach CLR lepiej jest używać operatora sa fe _cast . A
A
•
Klasy interfejsowe Definicja klasy interfejsowej jest podobna do definicji klasy referencyjnej , ale jest to całkiem inny koncept. Interfejs to klasa określająca zestaw funkcji do zaimplementowania przez inne klasy w celu dostarczenia standardowego sposobu realizowania określonej funkcjonalności. Zarówno klasy wartości, jak i klasy referencyjne mogą implementować interfejsy. Interfejs nie definiuje żadnej ze swoich funkcji składowych - są one definiowane przez klasy implementujące interfejs . Spotkaliśmy się już z interfejsem System: :Comparab l e w kontekście funkcji generycznych, gdzie interfejs IComparab le określil iśmy jako ograniczenie. Interfejs ICompar abl e określa funkcję Compa reTo() służącą do porównywania obiektów. W związku z tym wszystkie klasy implementujące ten interfejs mają ten sam mechanizm porównywania obiektów. Interfejs implementowany przez klasę określa się w podobny sposób jak klasę bazową. Poniżej znajduje się przykładowa implementacja interfejsu System: : ICompar able przez klasę CBox z poprzedniego projektu :
ref class Box : Cont ai ner, IComparable
II Klasa pochodna.
publi c: II Funkcja
określona
prz ez interfej s IComparable.
vir tual int Compa reTo(Object A obj) (
i f CVolumeC) < safe_cast Cobj) ->Vo l ume( )) ret urn -l ; else if CVol umeC) > safe_castCobj )->Vol umeC)) ret urn l. else ret urn O: II Reszta klasy jak poprzednio...
}:
570
Visual C++ 2005. Od podstaw Po nazwie interfejsu znajduje się nazwa klasy bazowej Contai ner. Gdyby nie było klasy bazowej, to byłaby tu tylko sama nazwa interfejsu. Klasa referencyjna może mieć tylko jedną klasę bazow ą, ale może implementować dowolną liczbę interfejsów. Klasa musi definiować wszystkie funkcje określone przez wszystkie interfejsy, które implementuje. Interfejs l Compara b1e określa tylko jedną funkcję, ale może ich być dowolna liczba . Teraz klasa Box definiuje funkcj ę Compare To( ) z taką samą sygnaturą, jaką interfejs IComparabl e określa dla tej funkcji . Ze względu na fakt, że argument funkcji CompareTo() jest typu Dbject musimy dokonać rzutowania do typu Box przed uzyskaniem dostępu do składowych wskazywanego przez niego obiektu klasy Box. A
,
A
Definiowanie klas interlejsowych Klasę interfejsową można zdefin iować
za pomocą słowa kluczowego i nterface Cl ass lub i nt erface struct . Bez względu na to, którego z tych słów kluczowych użyjemy do definicji interfejsu, wszystkie jego składowe są zawsze publiczne i nie mogą być inne. Składow e interfejsu mogą być funkcjami, włącznie z operatorami, właściwościami , polami statycznymi oraz zdarzeniami - o nich wsz ystkich b ędziemy jeszcze mówić w tym rozdziale.Interfejs może zawierać także konstruktor statyczny oraz definicje zagnieżdżonych klas dowolnego typu. Pomimo takiej różnorodności możliwych składowych większość interfejsów jest względnie prosta. Warto zauważyć , że tworzenie interfejsu na bazie innego interfejsu zasadniczo odbywa się tak samo jak w przypadku tworzenia jednej klasy referencyjnej na bazie innej klasy referencyjnej . Na przykład:
int erface class IControl le r : ITeleVl son . IRecorder { II Pola interfejsu IContr ol/er...
}:
Interfejs IContro11er zawiera własne pola , a także dziedziczy je po interfejsach !Tel evi ste n i l Recorder . Klasa implementująca interfejs IContro ller musi definiować funkcje składowe interfejsów IController, l I el evi s to n oraz I Recor der. W projekcie Cw9_ 14 zamiast bazowej klasy Contai ner takiego interfejsu wyglądałaby następująco :
mogliśmy użyć
interfejsu. Definicja
II Plik naglówkowy IContainer.h w proj ekcie Cw9_15.
#p ragma once int er face class ICont ainer (
double Vo l ume (): void ShowVol ume( ):
II Funkcja II Funkcja
obliczająca pojemn ość.
wyświet lająca pojemność .
}:
W C++/CLI istnieje konwencja, według której nazwy interfejsów rozpoczynają się wielką literą I - dlatego nazwa naszego interfejsu to ICont ain er . Interfejs ten zawiera dwie składowe: funkcję Vol ume () oraz funkcję ShowVol ume( ), które są publiczne, ponieważ wszystkie składowe interfejsu są publiczne. Obie funkcje są abstrakcyjne, ponieważ interfejs nigdy nie zawiera definicji funkcji - można do znajdujących się w interfejsie funkcji dodać słowo kluczowe abstraet, ale nie ma takiej potrzeby . Funkcje obiektowe w definicji interfejsu mogą być określone jako wirtualne lub abstrakcyjne, ale nie jest to konieczne, gdyż i tak są one takie.
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
571
Każd a jeżel i
klasa implementując a interfej s rContainer musi implementować obie jego funk cje , nie chcemy, aby była klasą a bs trakcyj ną. Spójrzmy na d e finicję klasy Box:
II Plik naglówkowy Box.h w proj ekcie Cw9 15.
#pragma once #include "ICont ai ner .h"
II Dotacz a defi nicję interfejsu .
using namespace System: ref class Box : ICont ai ner pub1ic: II Funkcia
vi rt ual void ShowVol ume() Console : : Wr i te L i n e C L " PoJem ność Vo lume C) );
u ży tk owa
obiektu klasy CBox wynosi {O}" ,
II Funkc 'a oblicza ' ca o 'emnos ć obiektu klas Box.
Nazwa inter fejsu znajduje si ę po dwukropku w pierwszym wiers zu defini cji klasy, tak jakby to była klasa bazowa. Oczywi ści e m ogłaby to być klasa bazowa. Wtedy nazwa interfejsu występow ałaby po nazw ie klasy bazo wej i była od niej oddziel ona przecinkiem. Kla sa może implementować wiele interfejsów - nazwy poszczególnych interfejsów oddzielane są przecinkami. Klasa Box musi implementować obie funkcje składowe klasy interfejsowej If.ontamer. W przeciwnym przypadku będzie klasą abstrakcyjną i musi zostać jako taka zadeklarowana. Do definicji funkcj i w klasie Box nie dołączono słowa kluczowego overr i de, pon ieważ istniejące funkcj e nie są w niej przesłaniane , a implementowane po raz pierwszy . Klasa Gl ass Box jest pochodną klasy Box, a zatem dziedziczy i mp le m e n tacj ę interfejsu rCont a-j ner. W klasie Gl assBox nie są wymagane żadne zmiany do wprowadzenia klasy interfejsowej rCont ainer .
572
Visual C++ 2005. Od podstaw Interfejs rCont ai ner
pełn i tę samą rolę
co klasa bazowa w polimorfizmie. W uchwycie typu adres obiektu dowolnego typu klasowego implementują cego interfejs. A zatem uchwytu typu rCont ai ner można użyć do wskazywania obiektów typu Box lub Gl assBox oraz wymuszania zachowania polimorficznego podczas wywoływania funkcji, które są składowym i klasy interfejsowej . Wypróbujmy to.
rContai ner
mo żna przechowywać
~ Implementacja klasy interfeisuwej Utwórz projekt konsolowy CLR o nazwie Cw9 _15 i dodaj do niego pliki nagłówkowe IContainer .h oraz Box.h o poniższej tre ści . Następnie dodaj także kopie plików Stack.h oraz GlassBox.h z projektu Cw9_14 . Na koniec zmodyfikuj zawartość pliku źródłowego Cw9_15 w następujący sposób : II Cw9_1 5.cpp: main project fi le.
#i nclude #include #i nclude #incl ude
"stdafx.h" "Box.h" "GlassBox.h" "St ack.h"
II Dla klas Box i /Conta iner. II Dla klasy GlassBox (i Box oraz / Container). II Dla klasy reprezentują c ej stos z zagn ieżdżoną II s t ru k tu rą /tem.
using name space Syst em; int main(arr ay<System : :String A> Aargs) {
Console: : W r i t e L i n e ( L " Pu d e ł k a w t abl icy maj ą na st ę puj ą ce poj emno ś ci :" ) ; for each(IConta inerA cont ai ner in cont ainers) conta t ner ->ShowVol ume( ); II Wysyłani e na wyjście pojemnośc i pudełka. Console : :WriteL ine(L"\ nDodawanie
pud eł ek
do st osu.. .");
Stec k" stack = gcnew Stack; II Utworzenie stosu. for each (ICont ai ner A conta iner in cont ai ners ) sta ck· >Push(container) ;
Console : :Writ eLine( L" Usuwanie pud eł e k ze st osu prezentuje je w odwróconej Object " item; whil e«(it em = st ack->Pop()) != null ptr l safe_cast(item) ->ShowVol ume( ); Console : :Writ eLine(); ret urn O;
k ol ejn o ś c i
:" );
Rozdział 9.•
Rezultat
działania
P ude ł k a
Dziedziczenie i funkcje wirtualne
513
tego programu jest następujący:
w t ablicy
Pojemność u żytkowa
Pojemność użyt ko wa Pojemn ość u żyt k owa
Poj emn o ś ć u ży t k owa
ma ją nastę puj ące pojem no ś c i:
obiekt u obiektu obiekt u obie kt u
klasy klasy kla sy klasy
CBox wynosi CBox wynosi CBox wynosi CBox wynos l
24 20 .4 120 102
Dodawanie pu deł ek do st osu.. Usuwani e pud e ł ek ze st osu prezent uje je w odwróconej Poj emn o ść u żytk owa obiektu klasy CBox wynosi 102 Pojem n ość u żytk owa obiekt u klasy CBox w ynosi 120 Po jemno ść uży t kow a obiekt u klasy CBox wynosi 20.4 Poj emn o ś ć użyt k owa obiektu klasy CBox wynosi 24
kolejno ści :
Jak to działa Utworzyliśmy tablicę
elementów typu rContai ner i zainicjalizowaliśmy jej elementy adresami obiektów klas Box i GlassBox: A
Klasy Box i Gl assBox implementują interfejs rContain er, dzięki czemu adresy obiektów tych typów możemy przechowywać w zmiennych będących uchwytami do rContaine r . Zaletą takiego podejścia jest możliwość polimorficznego wywoływania funkcji składowych interfejsu rCont ai ner . Listę pojemności
obiektów klas Box i GlassBox tworzymy w pętli f or each: for each(ICont ai ner A container in containers) II Wysyłan ie na wyjście pojemnoś ci pu del/w. cont ai ner- >ShowVol ume():
Ciało pętli
pokazuje polimorfizm w akcji. Wywoływana jest w nim funkcja ShowVo l ume() dla obiektu wskazywanego przez conta i ner , co widać w danych wyjściowych.
określon ego
Elementy tablicy conta i ner s ustawiamy na stosie w dokładnie taki sam sposób jak w poprzednim przykładzie. Usuwanie elementów ze stosu również odbywa się podobnie jak poprzednio:
Object item: whil e(i t em = stack- >Pop() ) 1= nu llpt r) safe_cast (it em) ->ShowVo lume() : A
C i ało pętl i
pokazuje, że za pomocą operatora saf e_cast można rzutować uchwyt do typu interfejsowego w dokładnie taki sam sposób jak w przypadku typu klasy referencyjn ej. Następ nie możemy użyć tego uchwytu do polimorficznego wywołania funkcji ShowVo l tmeO . Klasy interfejsowe są nie tylko wygodnym sposobem definiowania zestawów funkcji represtandardowe interfejsy klas, ale także potężnym mechanizmem pozwalającym na zastosowanie polimorfizmu w programach.
zentujących
574
Yisual C++ 2005. Od podstaw
Klasy i asemblacje Program napisany w języku C++/CLI zawsze rezyduje w jednej lub większej liczbie asemblacji , a więc klasy w C++/CLI rezydują w asemblacjach. Wszystkie klasy definiowane do tej pory zawierały się w prostej pojedynczej asemblacji w postaci pliku wykonywalnego, ale można także tworzyć asemblacje zawierające własne klasy biblioteczne. W języku C++/CLI dla klas dostępne są specyfikatory widoczności, które określają, czy dana klasa jest dostępna na zewnątrz asemblacji , w której się znajduje, a która nosi nazwę asembla cji nadrzędn ej . Poza znanymi z natywnego C++ specyfikatorami dostępu do składowych publ te, pri vate oraz protec ted, w C++/CLI dostępne są jeszcze inne specyfikatory określające miejsce, z którego można uzyskać dostęp do składowych w różnych asemblacjach.
Specyfikatory widoczności klas i interfejsów klasy, interfejsu lub wyliczenia można określić jako pri vat e lub publ i c. Klasa publiczna jest widoczna i dostępna na zewnątrz asemblacji, w której się znajduje. Klasy prywatne natomiast dostępne są tylko wewnątrz nadrzędnej asemblacji . Aby określić klasę jako publiczną, należy posłużyć się słowem kluczowym publ i c: Widoczność niezagnieżdżonej
pub1 ie interfaee elass IContai ner { II Kod interfejsu...
};
Kontener ICont ai ner w powyższym przykładzie jest dostępny w asemblacji zewnętrznej, gdyż został zdefiniowany jako publiczny. Pominięcie słowa kluczowego pub l i c spowodowałoby, że interfejs ten byłby domyślnie prywatny i dostępny wyłącznie wewnątrz swojej nadrzędnej asemblacji. Klasę, wyliczenie i interfejs można jawnie zdefiniować jako prywatne, ale nie jest to konieczne.
Specylikatory dostęplI do składowych klas i interfejsów W C++/CLI dostępne są trzy dodatkowe specyfikatory dostępu do składowych klasy; interna l , publ i c prot ect ed i pri vat e protected. Efekt ich działani a został objaśniony w komentarzach do poniższej definicji klasy:
publ t e ref e1ass MyC1ass
II Klasa widoczna poza
asemblacją .
{
pub1t e : dla klas
wewnątrz
i na zewnątrz asembla cji
n adrz ędn ej.
II Składowe dostępne są dla klas
wewnątrz
i na zewnątrz asemblacji
n adrzędnej.
II Skladowe
do stępne są
internal ; publi C proteeted; II Składo we dostępne są dla typów po chodnych klasy MyClass p oza Il oraz dla klas wewnątrz asembla cji nadrzędnej.
asemblacją nadrz ędną
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
575
private prat ect ed: II Składowe dostępne dla typów p ochodnych klasy MyClass
wewną trz
asemblacj i nadrz ędn ej.
}: Oczywiście ,
aby specyfikatory do stępu do składowych umożliwiały dostęp spoza asemblacji klasa musi być publiczna. W przypadku słów kluczowych składających się z dwóch członów, takich jak pri vat e protected, mniej restrykcyjne słowo odnosi się do wnętrza asemblacji, a bardziej restrykcyjne do tego, co jest na zewnątrz. Kolejnoś ć członów można zmieniać . W związku z tym słowo kluczowe protecte d pri vate ma takie samo znaczenie co pri vat e nadrzędnej,
prote ct ed. tych słów klu czowych , trzeba mieć program zawierający więcej niż jedną Przerobimy więc projekt Cw9_15 do postaci asembla cji biblioteki klas oraz utworzymy asemblację programu korzystającego z tej biblioteki .
Aby móc
użyć
asemblację.
~. Tworzenie biblioteki klas W celu utworzenia biblioteki klas utworzymy najpierw za pomocą szablonu Class Library projekt CLR o nazwie Cw9_16lib. Po utworzeniu projekt zawiera plik nagłówkowy Cw9_ 16lib.h o następującej treści :
#pragma ance using namespace System: namespace Cw9_16l i b { publi c ref class Classl { II Tutaj pow inny znaleźć s ię metody tej klasy . }:
Biblioteka klas ma swoj ą własną przestrzeń nam i w naszym przypadku jest to domy ślnie Cwg_161i b. Jeżeli chcemy, to można tę nazwę zmienić na coś bardziej odpowiedniego. Nazwy klas w bibliotece opatrzone są kwalifikatorem w postaci nazwy przestrzeni nazw, a zatem w każdym źródłowym pliku ze w nętrznym korzystającym z którejkolwiek klasy z biblioteki musi znajd ować s i ę dyrektywa using dla nazwy tej przestrzeni nazw. Definicje klas mających znajdować się w bibliotece wstawiane są pomiędzy nawiasami klamrowymi w przestrzeni nazw namespace. W przestrzeni nazw została już domyślnie utworzona klasa referencyjna, ale zamienimy ją własnymi klasami . Warto zwróci ć uwagę , że klasa C1assl jest publiczna. Wszystkie klasy, które mają być widoczne w innych asemblacjach, muszą być zdefiniowane jako pub l i c. W pliku Cw9_16lib.h dokonujemy II Cw9 16lib.h
#pragma ance using namespace Syst em:
następujących
modyfikacj i:
576
Visnal C++ 2005. Od podstaw names pace Cw9_161ib II Plik n agłó wko wy IContainer.h z Cw9_16.
publ iC int erface cla ss IContainer {
virtua l doub le Volume () : virtual void ShowVol ume() :
II Fun kcj a obliczająca pojemność. II Fun kcj a wysyłająca pojemność na
wyjście .
·l : II Plik nagló wko wy Box. h z Cw9_ 16.
public ref clas s Sox : IContai ner {
pub l ic: II Funkcja
pokazująca pojemność
obiektu.
virtual void ShowVo l ume() {
Console : :Writ eLine (L " Po jemność Volume() ); II Funkcja
obliczająca pojemność
uży tkowa
obiekt u klasy CBox wynosi {O)" .
obiektu klasy Box.
virtua l double Vo lume( ) ( return m_Length*m_Width*m_Height; II Konstruktor.
doub le hv) m_Length(lv ) . m_Wi dth(wv) . m_Height(hv){l
pub l ic protected : double m_Length : doub le m_Widt h: double m_Height :
l: II Plik naglówkowy Stack.h z Cw9_ 16.
publiC ref class Stack {
privat e : II Definiowanie elementów do ustawie nia na stos ie.
ref struct It em {
Object Obj: l t em" Next ; A
II Uchwyt do obiektu w tym elemencie. II Uchwyt do następn ego ele mentu na stosie lub nullptr.
II Kons truktor.
Item (Object obj. Ite m next ): Obj( obj ) . Next (next ){ l A
A
l: It em Top: A
II Uchwy t do elementu
publ ic: II Dodawanie obiektu do stosu.
void Push(Object obj) A
znajdującego się
na wierzc hu.
Rozdział 9.
Top = gcnew Item(obj . Top) : II
Usunię cie
• Dziedziczenie i funkcje wirtualne
577
II Utworzenie nowego elem entu i wstawienie go na wierz ch.
obiektu ze stosu .
Obj eet A Pop() {
i f (Top == nul l pt r ) return nu ll pt r :
II Jeż eli stos jest p usty, II zwróć nullptr.
Objeet Aobj = Top->Obj ; Top = Top- >Next : ret urn obj;
II Pobierz obiekt z elementu. II Ustaw następny element na wierz chu.
} }: }
W bibliotec e znajdują się teraz klasa interfejsowa ICont ai ner, klasa Box oraz klasa St ack. Zmiany dokonane w stosunku do ich poprzednich wersji zostały wyróżnione na szarym tle. Każda z tych klas jest teraz publiczna, dzięki czemu s ą one dostępne z asemblacji zewnętrz nych . Pola klasy Box są publiczne chron ione (pub l i c prot ect ed), co oznacza, ż e w klasie pochodnej są one dziedziczone jako pola chronione, ale są publiczne dla klas znajdujących się w asemblacji nadrzędnej . Do pól tych nie odnosimy się jednak z innych klas wewnątrz asemblacji nadrzędnej, a więc mogliśmy pozostawić je w tym przypadku jako chronione. Jeżeli
projekt skompilował się poprawnie , to asemblacja zawierająca bibliotekę klas znajduje w pliku o nazwie Cw9_16lib.dll. Jest on umieszczony w folderze debug w katalogu projektu , jeżeli skompilowana została wersja testowa, lub w folderze releas e w katalogu projektu, jeżeli skompilowana została wersja ostateczna. Rozszerzenie .dl! oznacza, że jest to biblioteka dołączana dynamicznie (ang. dynamie link library) , czyli DLL. Potrzebujemy teraz jeszcze jednego projektu , który korzystałby z naszej biblioteki . si ę
~ Uzywanie IJilIlioleki klas Dodaj nowy projekt konsolowy CLR o nazwie Cw9 j 6 w taki sam sposób jak zawsze. Następ nie do pliku źródłowego Cw9 _16. epp wprowadź następujący kod :
#i ncl ude "stdafx.h" #i ncl ude "GlassBox .h" #usi ng usi ng names ace System ; usi ng namespace Cw9 161i b; i nt main (array<System : :St ri ng A> Aargs) (
Visual C++ 2005. Od podstaw gcnew Gl assBox(4.0. 5.0. 6.0) };
Console: : WriteLine(L"Pud ełk a w t ablicy ma j ą na st ępując e p ojemn o śc i: ") : for each(IContalner A contai ner in cont ai ners) II Wysyłanie na wyjście p ojemności pudełka.
cont ai ner->ShowVol ume ( ): Conso le: :Writ eLi ne(L"\ nDodawanie
pu deł ek
do st osu. . . "):
Stac k" stack = gcnew St ack; II UM orzenie stosu. for each (IContainer A container i n conta i ners ) st ack->Push(cont ainer):
Console : :WriteL i ne( L"PUsuwanie pud e ł e k ze st osu prezentuje je w odwróconej Object " item: wh ile «item - st ack->Pop( )) 1= nullptrl safe_cast (item)->ShowVolume() :
k o l ej n oś ci
:") :
Console:: Writ eLl ne() ; ret urn O, Musimy jeszcze dodać do projektu plik nagłówkowy GlassBox.h, którego kod jest identyczny jak w projekcie Cw9_15. Można ten plik skopiować do katalogu tego projektu i dodać go, klikając prawym przyciskiem myszy w zakładce Solution Explorer Header Fi/es, a następnie wybierając z menu kontekstowego Add/Existing Item. Oczywiście klasa Gl assBox jest pochodną klasy Box, a więc kompilator musi wiedzieć, gdzie można znaleźć definicję tej klasy. W tym przypadku znajduje się ona w bibliotece, którą stworzyliśmy w poprzednim projekcie. W związku z tym do pliku nagłówkowego GlassBox.h po dyrektywie #pragma once dodajemy poniższą dyrektywę:
#usi ng Nazwa klasy Box jest zdefiniowana w przestrzeni nazw Cwg_161 i b, a zatem musimy jeszcze dodać instrukcję us i ng dla tej przestrzeni nazw po dyrektywie #us ing:
usi ng namespace Cw9 16lib ; Aby kompilator mógł odnaleźć naszą bibliotekę, należy skopiować plik Cw9_16lib.dll z katalogu debug rozwiązania Cw9 j 6lib do folderu projektu Cw9_16. Można także podać pełną ścieżkę dostępu do asemblacji za pomocą dyrektywy #usi ng, ale najczęściej wszystkie biblioteki klas używane przez program umieszcza się w tym samym folderze, w którym znajduje się plik wykonywalny programu. Łatwo się poplątać w tych wszystkich nazwach folderów . Plik Cw9_16lib.dll znajduje się w folderze debug rozwiązania Cw9_16Iib, a nie w folderze debug projektu Cw9_16lib. Plik biblioteczny kopiujemy do folderu projektu Cw9_16. Upewnij się, że plik .dll znajduje się we właściwym folderze, w przeciwnym przypadku biblioteka nie zostanie odnaleziona. Jako tywę
że
klasy w
zewnętrznej
asemblacji
są
we
własnej
przestrzeni nazw,
dodaliśmy
dyrek-
usi ng dla nazwy przestrzeni nazw Cwg_161 i b. Gdybyśmy nie użyli tej dyrektywy, to musielibyśmy używać nazw IContainer, Box oraz Stack z kwalifikatorem w postaci nazwy przestrzeni nazw . Zamiast na przykład pisać Box, musielibyśmy pisać Cwg_161 i b; ; Box.
Rozdział 9.
• Dziedziczenie i funkcje wirtualne
579
Reszta kodu jest identyczna jak funkcji ma i nO w projekcie Cw9_ I5 . Nie są potrzebne żadne zmiany , ponieważ używamy klas z zewnętrznych asemblacji . Rezultat działania tego programu jest taki sam jak programu z projektu Cw9_15 .
Deliniowanie nowych funkcji Wiemy j uż, w jaki sposób używa s ię słowa kluczowego overr i de do prze słaniania funkcji klasy bazowej. Funkcje w klasie pochodnej można również zdefiniować jako nowe (new). Taka funkcja przesłania funkcję klasy bazowej mającą taką s a mą s ygnatu rę i nie zachowuje się polimorficznie. Poniżej znajduje się defmicja funkcji Vo l ume( ) jako nowej w klasie NewBox, która jest pochodnąklasy Box:
ref class NewBox
Box
II Klasa p ochodna .
o b liczająca pojemn o ś ć
obiektu Newlł ox.
{
publ t e: II Nowa f unkcja
vi rt ual double Volume() new { return 0.5*m_Lengt h*m_Widt h*m_Height ; } II Ko nstruktor.
NewBox(double l v. doub le
Wy.
double hv): Box(lv .
Wy.
hv ){}
};
Ta wersja funkcji
przesłania wersję
wywołamy funkcję
Vo l ume() za
funkcji Vo l ume() zdefiniowaną w klasie Box. Jeżeli zatem uchwytu typu NewBox to zostanie wywołana jej
pomocą
A
,
nowa wersja.
NewBox newBox ~ gcnew NewBox(2.0. 3.0. 4.0); Console: :Wr iteLi ne(newBox->Vol ume () ): II Wynik A
Rezultat wynosi 12, ponieważ funkcja Vol umeO o d z i e dz i c zył a klasa NextBox po klasie Box.
wynos i 12.
przesłon iła
jej
polimorficzną wersję , którą
Nowa funkcja Vo l ume( ) nie jest funkcją polimorficzną, a więc przy wywołaniach polimorficznych za pomocą uchwytu do typu klasy bazowej nowa wersja nie jest wywoływana. Na przykład:
Jedyna polimorficzna funkcja Vol ume() w klasie NewBox to funkcja odziedziczona po klasie Box i to właśnie ona zo st ał a wywołana w tym przypadku.
Delegaty izdarzenia Zdarzenie jest składową klasy , która umożliwia obiektowi zasygnalizowanie wystąpienia określonego zdarzenia. Proces sygnalizowania zdarzenia obejmuje deJegaty. Kliknięcie przyciskiem myszy jest przykładem typowego zdarzenia, a obiekt, od którego pochodzi to zdarzenie, zasygnalizowałby je poprzez wywołanie jednej lub większej liczby funkcji odpowiedzialnych za jego obsługę . Najpierw przyjrzymy się delegatom, a następnie przejdziemy do zdarzeń.
580
Visual C++ 2005. Od podstaw Koncepcja delegatu jest bardzo prosta - jest to obiekt zawierający jeden lub większą liczbę do funkcji o danej liśc ie parametrów i danych typach zwracanych . A zatem delegat w C++/CLI spełnia podobne zadania jak wskaźnik funkcji w natywnym C++. Mimo że sama koncep cja delegatów jest prosta , to szczegóły dotyczące ich tworzenia i używan ia mogą być nieco zaw i łe, a więc czas się skoncentrować . wskaźników
Deklarowanie delegatów Deklaracja delegatu wygląda jak prototyp funkcji poprzedzony słowem kluczo wym de l egat e, ale w rzeczywistości definiuje ona dwie rzeczy: nazwę typu referencyjnego dla obiektu delegatu oraz list ę parametrów i typ zwracany funkcj i, które mogą być skojarzone z tym deleg atem. Typ referencyjn y delegatu jest pochodną klasy Syst em: :Del egat e, a więc zawsze dziedz iczy składowe tej klasy. Deklaracja delegatu wygląda jak prototyp funkcji poprzedzony słowem kluczowym del egate, ale w rzeczywistoś ci definiuje typ referencyjny delegatu oraz sygnaturę funkcji, które mogą być z tym delegatem skojarzone. Poniżej znajduje s ię przykładowa deklaracja delegatu: publ ic del egate voi d Ha ndler(int val ue):
II Deklaracj a delegatu .
Powyższy kod definiuje typ referencyjny delegatu Handler , który jest pochodną klasy Sys tern : :Del egat e. Obiekt typu Handler może zawierać wskaźniki do jednej lub większej liczby funkcji mających jeden parametr typu i nt oraz typ zwracany voi d. Funkcje wskazywane przez delegat mogą być funkcjami obiektowymi lub funk cjami statycznymi.
Tworzenie delegatów Mając już
zdefiniowany typ delegatu , możemy tworzy ć obiekty delegatu tego typu. W przypadku delegatów mamy do wyboru dwa konstruktory: konstruktor przyjmujący jeden argument i konstruktor przyjmujący dwa argumenty. Argument konstruktora delegatu przyjmującego tylko jeden argument mu si być statyc zną funkcją skład ową klasy lub funkcją globalną, której typ zwracany i lista parametrów zo s tały określone w deklaracji delegatu. Przypu śćmy, że zdefiniowaliśmy klasę o nazwie Handl erCl ass : publ i c ref cl ass HandlerCl ass { publ ic: stat ic void Funl(i nt m) { Console : :Wri t eLi ne(L"Funkcj a Functionl
z
wart oś ci ą
{O)" . m):
wywala na z
wa rt ośc ią
{O)" . m) :
voi d Fun3(i nt m) { Consol e: :Wri t eLi ne(L"Funkcj a Function3 zastal a wywal ana z
wa rt o ś ci ą
{O )" .
war to ś c i ą
{Ol " ,
stat ic void Fun2(i nt m) { Console : :Wr iteLi ne(L"Funkcja Funct i on2
z o s t a ła wywoła n a
zo s ta ła
m-valu e) : }
voi d Fun4(i nt m) { Console : :WriteLine(L"Funkcj a Funct i on4 m-value ) : }
zo st a ł a wywoł ana
z
l
Rozdzial9. • Dziedziczenie i funkcje wirtualne
581
HandlerClassC) :val ueCU{} Ha ndle rClassCi nt m) .valu eu n{} protect ed: int val ue: }: Powyższa klasa ma cztery funkcje z parametrem typu i nt oraz typem zwracanym voi d. Dwie z nich to funkcje obiektowe, a drugi e dwie to funkcje statyczne. W klasie znajdują się także dwa konstruktory, z których jeden jest bezargumentowy. Klasa ta nie robi nic z wyjątkiem wy świetlania informacji, która funkcja została wywołana , i w przypadku funkcji obiektowych - informacji na temat obiektu.
Delegat Handl er
możemy utworzyć
Handler handler A
=
za pomocą następującego kodu :
gcnew Ha ndler CHandlerClass: :Funl):
II Obiekt delegatu.
Obiekt handl er zawiera adres funkcji statycznej Funl należącej do klasy Hand l erCl ass. W momencie wywołania delegatu wywoływana jest funkcja Handl erCl ass: :Funl () za pomocą argumentu takiego samego jak przekazany w wywołaniu delegatu . Wywołan ie delegatu można zap isać następująco :
handler ->Invoke(90); Powyższy
kod wywołuje wszystkie funkcje znajdujące s i ę na liście wywoławczej dele gatu han dler. W tym przypadku lista wywoławcza zawiera tylko jedną funkcję - Handl er Class : : Funl O, a więc rezultat jest następujący:
Funkcj a Funct ionl Delegat
z os t a ła wywo ła na
można także wywołać
z
wa rt oś c ią
90
za pomocą poniższej instrukcji:
handler (90): Jest to skrócona wersja poprzedniej instrukcji , która jawnie wywoływała Tego sposobu wywoływania delegatu używa się najczęściej .
funkcję
I nvoke O .
Operator + jest dla typów delegatowych przeładowany w celu umoż liwienia połączenia list dwóch delegatów w nowy obiekt delegatu. Na przykład listę wywoławczą delegatu ha nd l er możemy zmodyfikować za pomocą poniższej instrukcj i:
wywoławczych
handler
+=
gcnew Handler (HandlerCl ass: :Fun2):
Zmienna handl er odnosi się teraz do obiektu delegatu z listą wywoławczą zawierającą dwie funkcje: Funl i Fun 2. Jest to j ednak nowy obiekt delegatu. Listy wywoławczej delegatu nie można zmieniać, a w ięc operator + działa tu w podobny sposób jak z obiektami klasy St r ing zawsze tworzony jest nowy obiekt. Ponownie możemy wywołać deklarację za pomocą poniż szej instrukcji :
handler (BO ); Teraz rezultat jest następujący:
Funkcja Functi on l Funkcja Functi on2
zo s t a ła wywołan a zos ta ła wywoła n a
z z
wartoś ci ą war t o ś c ią
BO BO
582
lisnal C++ 2005. Od podstaw Obie funkcj e, które znalazły s ię na liście wywoławczej , w jakiej były dod ane do ob iektu delegatu. . Można
efektywnie
usunąć
wpis z listy
wywoławczej
zos tały wywołan e
delegatu za
w takiej
pomocą
kolejności ,
operatora - :
hand l er -= gcnew Handler(Hand lerClass: :Funl) : Powyższy
kod tworzy nowy obiekt delegatu , na którego l iście wywoławczej znajduje się tylko funk cja HandlerCl ass: :Fun2( ), ponieważ w efekcie jego działania usuwane są funk cj e listy wywoławczej z prawej strony z listy delegatu handl er oraz tworzony nowy obiekt wskazujący funkcje, które pozostają. Należy zwrócić uwagę, że
lista wywolawcza delegatu musi zawierać co najmniej j eden
wskaźnik do funk cji. Jeżeli za pomocą operatora odejmowania usunięte zostaną wszystkie wskaźn iki do funkcji, to rezultat b ędzie nu 77pt r.
W konstruktorze delegatu z dwoma parametrami pierwszy argument je st referencją do ob iektu na stercie CLR, a drugi obiekt jest adre sem funkcji obiektowej typu tamtego obiektu. A zatem poniższy konstruktor tworzy delegat zawierający wskaźnik do funkcji obiektowej określonej prz ez drugi argument obiektu wskazanego przez pierwszy argument:
HandlerC lass obj = gcnew HandlerClass; Handler hand ler2 = gcnew Hand ler (obj . &Hand le rC lass : :Fun3J; A
A
Pierwsza instrukcja tworzy obiekt, a druga delegat wskazujący funkcję Fun3( ) obiektu obj klasy Handl er Cl ass. Delegacja spodziewa się argumentu typu i nt , a ją wywołać za pomocą poniższej instrukcji:
należącą
do
więc można
handler2(70J ; W rezultacie zostaje wywołana funkcja obiektu obj Fun3() z argumentem o jest następujący:
Fun kCJ a Funct ion3
zo s ta ł a
wywolana z
wa rto ści ą
wartości
70. Wynik
71
Warto ść przechowywana w polu Value obiektu obj to l, ponieważ obiekt ten został utworzony za pomocą konstruktora domyślnego. Instrukcja w ciele funkcji Fun3( ) dodaje wartoś ć pola va l ue do argumentu funkcji - stąd wynik 71.
Ze
względu
można
je
na fakt, że listy
wywoławcze
delegatów handler i handl er2
są takiego
samego typu,
połączyć:
Ha ndl er handler = gcnew Ha ndl er (HandlerCl ass: :Funl J; handl er += gcnew Handl er (Handl erCl ass: :Fun2J; A
II Obiek/ de/ega/u.
Handl erCl ass obj = gcnew HandlerC lass: Handler ha ndler2 = gcnew Handler (obj. &Handl erCl ass : :Fun3J: hand le r += handler2; A
A
W powyższym kodzie ponownie tworzony jest obiekt handl er w skazujący delegat zawi eraj ący do statycznych funkcji Funl () i Fun2() . Następn ie tworzymy nowy delegat wskazywany przez obiekt handl er, zawierający wspomniane funkcje statyczne plus funkcję obiektową Fun3() obiektu obj. Delegat ten możemy teraz wywołać za pomocą poniższej instrukcji: wskaźniki
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
583
handler(50l:
Rezultat tego wywołania jest Funkcj a Funct ionl Funkcj a Funct ion2 Funkcja Funct ion3
następujący:
zo stał a wywoł an a
z
warto śc i ą
zo st ał a wywoła n a zwar t o ś c l ą z o s t a ł a wywoła na
z
wart o ś c i ą
50 50 51
Jak widać, wywołanie delegatu spowodowało wywołanie dwóch funkcji statycznych orazfunkcji Fun3( ) będącej składową obiektu obj . A zatem najednej liście wywoławczej de1egatu można używać zarówno funkcji statycznych,jak i niestatycznych. Złożymy
teraz kilka fragmentów kodu w jedną całość i sprawdzimy, czy to rzeczywiście działa.
Rllml!:mI Tworzenie i wywoływanie deleuatu Poniższy
kodjest zbieraniną fragmentów, które
widzieliśmy , odkąd zaczęliśmy mówić
legatach:
#lnclude "st dafx.h" using namespace Syst em: publi C ref class Ha ndl erCl ass {
publ i c: st ati c void Funl (int m) { Console : :WriteLine(L" Funkcj a Funct ionl
zo s ta ł a wywołan a
z
wart o ścią
{O)" . ml :
stat ic void Fun2(int m) {Console : :Wri t eLine(L"Funkcj a Funct ion2
z o st ał a wywoł a n a
z
wart o ś c i ą
{Ol " , ml : l
wywo ł an a
z
wa rto śc i ą
{O)" .
z os t a ł a wywo ł an a
z
war t o ś c i ą
{O}" ,
void Fun3(int m) { Console: :WriteLine(L "Funkcja Funct ion3 zos ta ła m-val ue) :
l
vOld Fun4(int ml { Console : :Writ eLi ne(L"Funkcj a Function4 m-val ue) :
l
HandlerCl ass( ):val ue(l ){} HandlerCla ss(int ml:val ue(m l{} protected: int val ue: }:
public delegate void Ha nd ler (int valuel; i nt mai n(array<System: :String A> Aargsl {
II Deklaracja delegatu.
o de-
584
VisIlai C++ 2005. Od podstaw Handler hand ler = gcnew Ha ndler CHandlerClass : .Funl ) : // Obiekt delegatu. Console: :WriteLineCL"Delegac ja z j ednym w s kaźni k iem do funkcj i st atycznej :") ; handler ->Invoke(90): A
handler += gcnew Hand le rCHa ndlerC lass: :Fun2); Console: :WriteLi neCL"\ nDelegacja z dwoma ws ka ź n i k a m l do funkcji sta tycznych:"); handler ->Invoke(80); Handle rCl ass obj = gcnew HandlerClass ; Handler handler2 = gcnew Hand ler Cobj, &HandlerClass ; :Fun3); ha ndl er += handler2; Console: :Writ eLine(L"\nDe legacja z trzema ws kaź n i ka mi do funkcj i :" ) ; handler (70); A
A
Console : :Wrl tel1ne(L"\ nSkracanie li sty wywo ł awcze j .. ."): handler - = gcnew Handler (HandlerClass: :Funl ); Console : :WriteL ine (L"\nDelegacj a ze ws k aź nika mi do jednej funk cji st atycznej funkcji obiekt owej: "); hand l ert 60); Rezultat
1
jednej
działania powyższego programu jest następujący:
Delegacj a z jednym ws k aźn lk iem do funkcji statycznej : Funkcja Functionl z o s t ała wywołana z wartością 90 Delegacja z dwoma w skaźnikami do fun kc j i statycznych: Funkcj a Functionl zo stała wywołana z wa rtością 80 Funkcj a Functi on2 z os t a ł a wywołana z wart oś cią 80 Delega cja z t rzema ws k a źnik a mi do funkc j i : Funkcj a Functi on l z ost a ł a wywołana z wa rto ś cią 70 Funkcj a Funct ion2 zo s t a ł a wywoł ana z wa rto ś cią 70 Funkcja Funct ion3 zos ta ł a wywo ł a n a z wa r t ośc i ą 71 Skracanie l isty
wywoł awc z ej
...
Delegacj a ze w s ka ź n i k a m i do jednej funkcj i statyc znej i j ednej funkcj i obiekt owej : Funkcja Funct ion2 z os t a ła wywo ł a n a z warto ś c i ą 60 Funkcja Funct ion3 zo s t a ła wywo ła n a z warto śc ią 61
Jak to działa Wszystkie operacje w funkcji mai n() widzieliśmy już wcześniej, Delegat został wywołany jawnie za pomocą funkcji I nvcke t ) przy użyciu uchwytu delegatu z listą argumentów. Z danych na ekranie wynika, że wszystko działa prawidłowo. Mimo że delegat w powyższym przykładzie może zawierać wskaźniki do funkcji jednoargumentowych, de legaty mogą wskazywać funkcje o dowolnej liczbie argumentów, Na przykład deklaracja delegatu może być następująca:
delegate void M yHandl er Cdoubl e x. St ri ng description ); A
RozlJział 9.
• Dzi8IJziczenie i funkcie wirlualne
585
P owyższa instrukcja deklaruje delegat MyHandl er , który może wskazywać tylko funkcje z typem zwracan ym voi d oraz dwoma parametrami - pierwszy typu doubl e, a drugi typu St r inq".
Delegatv niewiązane Delegaty, których przykład y oglądal i śmy do tej pory, były delegatami wiązanymi. Są one nazywane wiązan ymi, ponieważ mają stały zestaw funkcji w swoich listach wywoławczych. Można tworzyć także delegaty n iewiązane . Delegat taki wskazuje funkcję ob i ektową o okreś l o n ej liście parametrów i typie zwracanym dla danego typu obiektu. A zatem za pomocą jednego delegatu można wywołać funk cję obiektową dla dowolnego obiektu o k reś l o n e g o typu . Pon iżej znajduje się przykład deklaracji delegatu niewiązanego :
publl C delegat e void UBHa ndl er (ThisCl ass
A •
int value):
określa
typ wskaźnika t hi s, dla którego delegat typu UBHandl er może Funkcja ta musi mieć jeden parametr typu i nt oraz typ zwracany voi d. A zatem delegat typu UBHandl er może wywoływać funkcje dla dowolnego obiektu typu Thi sClass. Na pierwszy rzut oka wydaje się to dość sporym ograniczeniem, ale w rzeczywistości okazuje si ę bardzo przydatne. Można na przykład użyć tego deleg atu do wywołania funkcji dla wszystkich elementów tablicy, które s ą typu Thi sC l as s". Pierwszy argument
wywołać funkcję obiektową.
Delegat typu UBHandl er
UBHandl er ubh A
=
można utworzyć
za pomocą poniższego kodu:
gcnew UBHandler (&Th isCl ass : :Sum):
Argument konstruktora jest adresem funkcji klasy Thi sC l ass parametrów i typ zwracany. Poniżej
znajduje
s ię
posiadając ej wym aganą li stę
definicja klasy Thi sCl ass:
publ iC ref class ThisC lass {
publ lC: void Sum(i nt n. St r ing st r ) { Console : :Wr it eLine(L"Suma = {O)" . va lue A
+
n):
void Product Cint n. St ri ng st r ) { Console: :WriteLine(L" I1aczyn = {O )". val ue*nl : } A
ThisCl ass (doub le v) pri vate: double value:
valueCv ){}
}:
Funkcja Sum C) jest publi czn ą funkcją składową klasy Th i sCl ass, a zatem wywołanie delegatu ubh spowoduje wywołanie funkcji Sum() dla wszystkich obiektów tego typu klasowego. W wywołaniu delegatu niew iązanego pierwszy argument jest obiektem, dla którego funkcje z listy wywoławczej będą wywoływane, a następne argumenty są argumentami tych funkcji. Poni żej znajduje się przykładowe wywołanie delegatu ubh :
586
Visual C++ 2005. Od podstaw Thi sClassA obj = gcnew Thi sCl ass(99 .0): ubhrobj . S):
Pierwszy argument jest uchwytem do obiektu klas y Thi sCl as s utworzonego na stercie CLR poprzez przekazanie do konstruktora klasy wartości 99 . O. Drugi argument wywołania delegatu ubh to 5. W rezultacie zostani e wywołana funkcja Sum() z argumentem 5 dla obiektu wskazywanego przez obj . Del egaty niewiązane można łączyć za pomo cą operatora +, tworząc delegaty mogące wykilka funkcji. O czywi ście wszystkie funkcje muszą być kompatybilne z tym dele gatem. A więc dla delegatu ubh funkcje te musiałyby być funk cjami obiektowymi klasy Thi sCl ass, mającymi jeden parametr typu i nt oraz typ zwracany voi d. Poniżej znajduje się przykład:
wołać
ubh
+~
gcnew UBHa ndler(&Th isCl ass: :Product ) :
Wywołanie nowego delegatu wskazywanego przez ubh powoduje wywołanie zarówno funkcji Sum( ), jak i ProductC ) dla obiektu typu Thi sCl as s. Zobaczmy, jak to działa .
R!lml!::mI Używanie delegatów niewiązanYCh W przykładzie tym wykorzystano fragm enty kodu z poprzednich monstrowania działania delegatów niewiązanych :
podrozdziałów w
#i ncl ude "st dafx. h" using namespace System : publiC ref class Th isClass {
public: void Sum(i nt n) { Con sole . :WriteLi ne(L"Suma = {O} void Product(int n) { Console : :WriteLi ne t tT l oczyn = {O}
va l ue+n) : } val ue*n) :}
ThisClass(double v) : va lue(v){} pri vat e: do ub le value: }:
publ ic delegate void UBHa nd ler( Th isClass A, i nt va lue) : int mai n(array<Syst em: :St ring A> Aargs) {
Jak to działa zadeklarowany za pomocą poniższej instrukcji : publi c delegate void UBHandler(ThisClass A, int va l ue):
Typ delegatu UBHandl er
został
Obiekt y delegatu UBHa ndl er są delegatami n iewiązanymi mogącymi wywoływać funkcje obiektow e dla obiektów typu ThisCl ass , które mająjeden parametr typu i nt oraz typ zwracany voi d. Definicja klasy Thi sCl ass jest tutaj taka sama jak w poprzednim programie . Zawiera ona dwie funkcje obiektowe - Sum( ) i Produet ( ), Funkcje te mają parametr typu i nt oraz zwracają typ voi d, a wię c obie mogą by ć wywoływane przez delegaty typu UBHandl er . Tablicę
uchwytów do obiektów klasy Thi sClass w funkcji mai n() instrukcji :
Visual C++ 2005. Od podstaw Każd y z p ięc iu obiektów na liście inicjalizacyjnej zawiera inną wartość typu doubl e. Dz ięki temu łatwo jest w danych wyj ściowych rozpoznać obiekt, dla którego zostały wywołane funkcje Sum( ) i Produet ( ).
Obiekt delegatu tworzymy za pomocą poniższej instrukcji : UBHandler ubh A
=
gcnew UBHandler (&Thi sC las s : :Sum) ;
II Utworzenie obiektu delegatu .
Wywołanie obiektu delegatu wskazywanego przez uchwyt ubh powoduje wywołan ie funkcji Sum( ) dla wszystkich obiektów typu Thi sCl ass i jest to robione dla każdego obiektu w tablicy thi ngs:
for each(Thi sCl ass t hing i n t hi ngs) A
ubht t tnn q. 3) : Pętla
pętli delegat jest z elementem tej tablicy jako pierwszym argumentem. To powoduje wywołanie funkcj i Sum() dla obi ektu thi ng z argumentem 3. W ten sposób utworzonych zostaje pięć pierwszych wiersz y danych wyj ściowych .
f or each przechodzi przez wszystkie elementy tablicy t hi ngs. W ciele
wywoływany
N astęp n i e
ubh
+=
tworzymy nowy delegat:
gcnew UBHandle r (&Thi sCla ss : :Product ) :
II Dodawanie funkcji do delegatu.
Powyższa
instrukcj a tworzy nowy delegat typu UBHandl er , w skazujący funkcj ę Product O, oraz łączy go z istniejącym delegatem wskazywanym przez ubh. Rezultatem jest nowy delegat po siad ający na swojej liście wywoławczej wskaźniki do funkcji Sum() i Pr oduct ( ). Ostatnia pętla wywołuje delegat ubh dla każdego elementu tablicy t hi ngs z argumentem 2. W rezultacie zarówno funkcja Sum( ), jak i Pr oduct ( ) zostaną wywołane dla każdego obiektu klasy Thi sCl ass z argumentem 2. W ten sposób powstaje następne dziesięć wierszy danych wyjściowych .
delegatów w bardzo prostych przykładach, to do starczają one niedo programów. Można na przykład przekazać niewiązany delegat do funkcji jako argument, aby umożliwić tej funkcji wywoływanie różnych kombinacji funkcji obiektowych w różnym czasie - a więc delegat może działać jako selektor funkcji. Kolejnoś ć wywoływania funkcji przez delegat odpowiada ich kolejności na liście wywoławczej , a więc za pomocą delegatu można kontrolować kolejność wywoływania funkcji . Mimo
że
my
używali śmy
zwykłej elastyczności
Tworzenie zdarzeń Jak już wcześniej pisałem, sygnalizowanie zdarzenia angażuje delegat, który zawiera wskaź niki do funkcji mających być wywołanymi w momencie wystąpienia zdarzenia. Większoś ć zdarzeń w programach to zdarzeni a pochodzące od elementów sterujących, takich jak przyciski lub elementy menu, i są one wynikiem interakcji użytkownika z programem, chociaż można także zdefiniować i uru chamiać zdarzenia we własnym kodzie programu. Zdarzenie jest składową klasy referencyjnej , którą definiuje event oraz nazwy klasy delegatu:
się
za
pomocą s łowa
kluczowego
Rozdzial9. • Dziedziczenie i funkcie wirtualne public delegate void
Oo or Handler CS tr i ng
Ą
589
st r);
II Klasa ze zda rzeniem.
pu blic ref class Ooor (
publ ic: II Zdarzenie wywo lujące fu nkcje skojarzo ne II z obiektami delegatu DoorHandl er.
event
O o or Ha nd l er
Knock :
Ą
II Funk cja uruchamiajqca zdarz enia.
void TriggerEvent s C) (
Knock ("Alfred" ); Knock( "Janina"); }:
Klasa Ooor ma zdarz enie składowe o nazwie Knock, które odpowiada delegatowi typu Ooor Ha ndl er. Knock jest obiektową składową klasy, ale zdarzenia można także defin iować jako składowe statyczne za pomocą słowa kluczowego st at i c. Można także zadeklarować zdarzenie wirtualne. Kiedy uruchomione zostaje zdarzenie Knock, to może ono wywoływać funkcje z listą parametrów i typem zwracanym określonymi przez delegację OoorHandl e. W klasie Ooor znajduje się także funkcja publiczna Tri ggerEvent C) uruchamiająca dwa zdarzenia Knock - każde z innymi argumentami. Argumenty przekazywane są do funkcji, które zostały zarejestrowane w celu otrzymywania powiadomień o zdarzeniach Knock. Jak widać, uruchamianie zdarzenia odbywa się w taki sam sposób jak wywoływanie delegatu. Poni żej
znajduje
się przykładowa definicja
klasy
obsługującej
zdarzenia Knock:
publ ic ref class AnswerOoor (
public: void
Im ln CS tri ng
name)
Ą
(
Console: :Wrlt eLi neCL"W ejdt {O}. drzwi
są
otwarte. ".name):
}
void
I mO u t C S t r i ng
Ą
name)
{
Conso le : :WriteLi neC L"Odej dt (O) . ni e ma mnie w domu .".name) ; } }:
Klas a An swerOoor ma dwie publiczne funkcje składowe, które mogą potencjalnie obsłużyć zdarzenie Knock, ponieważ obie mają listy parametrów i typ zwracany taki sam jak podano w deklaracji delegatu OoorHandl e r . Przed zarejestrowaniem funkcji do otrzymywania powiadomień o zdarzeniach Knock nale ży utworzyć obiekt Ooa r : O oo r
Ą
door
=
gcnew Ooor ;
590
Visual C++ 2005. Od podstaw Teraz m ożna zarejestrować funk cję , aby w obiekcie Ooor w następujący sposób :
otrzymywała
powiadomienia o zdarzeniach Knock
An swerDoor answer = gcnewAnswe rDoor ; door ->Knock += gcnew OoorHandle rC answer . &AnswerDoor ; ; Imln); A
Pierwsza z powyższych instrukcji tworzy obiekt typu AnswerOoor - jest to konieczne, poniefunkcje Iml n( ) i ImOut () nie są statycznymi skład owymi klasy. Następn ie dodajemy egzemplarz typu delegatu OoorHandl er do składowej Knock obiektu Ooor . Jest to analogiczne z procesem dodawania wskaźników do funkcji delegatu. W podobny sposób można dodawać następne funkcje uchwytowe do wywołania w momencie uruchomienia zdarzenia Knock. Zobaczymy na przykładzie, jak to działa .
waż
~ Obsluga zdarzeń Poniższy
program definiuje, uruchamia i obsługuje zdarzenia za
pomocą
wc ześniej :
II Cw9_19.cpp : main p roj ect jile. II Definiowani e, uruchamianie i obsługa zdarzeń .
#include "st dafx.h" using namespace Syst em; publ lC delegate vo id DoorHand lerC St r i ng st r) ; A
II Klasa ze zdarzeniem składowym .
publ ic ref class Ooor {
publ t e : II Zdarzenie wywołujące f unkcje skojarzone z Ilo biektem delegatu Doorlła ndler.
event OoorHandler Knock; A
II Funkcja
uruch amiająca
zdarzenia.
void TriggerEventsC) (
KnockiL"Alfred"); KnockCL "Janina"); }
l; II Klasa
dejiniująca funkcje obs ługujące
zdarzenia Knock.
publi c ref class AnswerDoor {
pub li e: void Iml n(St rlng name ) A
{ C on so le ; ;W r iteL i neC L"Wejd ź
{Ol . drzwi
są
ot wart e." .name) ;
l void ImOut( St ri ng name) A
{ Conso l e:; W r it e L i n eC L " Od e j d ź
(O l . nie ma mnie w domu.".name) ;
klas stworzonych
Rozdzial9. • Dziedziczenie i lunkcje wirtualne
591
} }:
i nt ma i n(array<System : :Str ing A> Aargs) {
Door door ~ gcnew Door : AnswerDoor A an swe r = gcnew Answe rDoo r : A
II Dodawanie procedury obsług i dla zdarzenia Kno ck
door ->Knock -~ gcnew OoorHa ndler(answer , &AnswerOoor: : Imln); door- >Knock += gcnew DoorHandler( answer . &AnswerDoor: :ImDut ) ; door ->TriggerEvent s() : II Uruchomienie zdarzeń Kn ock. ret urn O: Rezultat
działania
tego programu widoczny jest poniżej:
Alfred. drzwi są otwarte. Jani na . drzwi są otwarte . Od e j d ź Alfred. nie m a mnie w domu. Od e j d ź Janina, nie m a mnie w domu. W e j dź W ejdź
Jak to działa Najpierw tworzone
są dwa
obiekty w funkcji ma i n( ):
Door door = gcnew Ooor : AnswerDoor A answer = gcnew AnswerDoor; A
Obiekt door ma zdarzenie składowe Knock, a obiekt answer ma funkcje składowe, które zarejestrować do wywoływania dla zdarzeń Knock. Następna
można
instrukcja rejestruje funkcję składową I mln( ) obiektu answer do otrzymywania zdarzeniach Knock dla obiektu door:
powiadomień o
door ->K noc k += gcnew DoorHandler(a nswer . &AnswerDoor : :Imln): Jeśli będzie ływane,
to miało jakiś sens, to można zarejestrować kiedy uruchamiane jest zdarzenie Knock.
Następna
także
inne funkcje, aby
były
wywo-
instrukcja wywołuje funkcję składową obiektu door Tri ggerEvents( ):
door->Tr iggerEvent s () ;
II Uruchomienie zdarze ń Knock.
W rezultacie powstają dwa zdarzenia Knock - jedno z argumentem Al fred, a drugie z argumentem Janin a. W wyniku tego funkcja Imlnt ) zostaje wywołana dwukrotnie - po jednym razie dla każdego zdarzenia, co daje dwa pierwsze wiersze danych wyjściowych .
592
VisIlai C++ 2005. Od pollstaw Oczywi ście możemy potrz ebować różnych
w zależności od cji mat nt ):
okoliczności .
reakcj i na dane zdarzenie w różnych sytua cjach, Takiej sytuacj i dotyczą właśnie następne trzy instrukcje w funk-
door->Knock -~ gcnew DoorHandler (answe r, &Answe rDoor: : Iml n); door->Knock +~ gc new DoorHandler (answer, &Answe rDoor : :ImOut ) ; door ->TriggerEvent s( ); II Uruchomienie zda rze ń Knock. Pierwsza z powyższych instruk cji usuwa wskaź n i k do funkcji Iml ru ) ze zdarzenia, a druga instrukcja rejestruje funkcję l mOut ( ) dla obiektu answer w celu otrzymywania przez n iąpowia domień o zdarzeniu. Kiedy zdarzenie Knock zostaje uruchomione przez trzecią instrukcję, to wywoływan a jest funkcja ImOut( ) i rezultat jest nieco inny.
Finalizatory idestruktory wklasach referencyjnych Destruktor klasy referencyjnej definiuje się w taki sam sposób j ak konstruktor w natywnym C++. Destruktor klasy referen cyjnej wywoływany jest w momencie, gdy uchwyt wychodzi poza zasi ęg lub obiekt jest częścią innego obiektu , który jest właśnie niszczony. Do uchwytu do klasy referencyjnej można także zastosować operator del ete , czego skutkiem będzi e wywołanie destruktora. Głównym powodem implementacji destruktora dla klasy w natywnym C++ jest usuwanie danych alokowanych na stercie. Oczywiście w przypadku klas referencyjnych nie jest to problemem, a więc w tego typu klasach istnieje o wiele mniejsza potrzeba definiowania destruktorów. Destruktor może być w klasie referencyjnej potrzebny, gdy j ej obiekty używają innych zasobów, które nie są zarządzane przez mechanizm usuwania nieużytków takich jak pliki , które muszą zostać zamknięte w odpowiedniej kolejności, kiedy obiekt jest niszczony. Tego typu zasoby można także czyści ć za pomocą innego rodzaju składowej klasy, zwanej finalizatorem . Finalizator jest specjalną funkcją składową klasy referencyjnej , która jest wywoływana automatycznie przez mechanizm usuwania nieużytków podczas niszczenia obiektu. Należy pamię tać, że finalizator nie zostanie wywołany dla obiektu klasy, jeżeli destruktor został wywołany j awnie lub w wyniku zastosowania operatora del ete . W klasach pochodnych finalizat ory s ą wywoływane w takiej samej kolejno ści , w jaki ej byłyby wywoływane destruktory. A zatem jako pierwszy wywoływany jest finalizator klasy znajdującej się w hierarchii najwyżej , a następnie finalizatory klas pochodnych. Finalizator klasy na naj niż szym poziomie jest wywoły wany jako ostatni. Klasę
z finalizatorem definiuje
s ię
w następujący sposób:
Dubl ic ref class MyCla ss ( II Definicj a fi nalizatora.
IMyClass() { II Kod czyszczenia, kiedy obiekt jes t niszczony ...
} II Reszta definicji klasy...
};
Rozdział 9.•
Dziedziczenie ifunkcje wirtualne
593
Funkcję finalizatora w klasie definiuje s ię w podobny sposób jak destruktor, ale zamiast znaku tyldy (-) przed nazwą destruktora używa się wykrzyknika ( !). Podobnie jak w przypadku destruktora, dla final izatora nie można podawać typu zwracanego, a s pecyfikator dostępu jest ignorow any. Sposób d ziałania destruktorów i final izatorów przedstawimy na krótkim przykładzie .
~ Finalizatory idestruktory Poniżs zy
program pokazuje, kiedy
są wywoływan e
destruktory i finalizatory :
#incl ude "stdafx.h" using namespace Syst em. ref class MyClass (
publ lC: II Konstrukto r.
M yCl assCint n)
val ueC n){}
II Destruktor .
-MyCla ss() (
Conso le: :Wri te Li neC "Destru kt or obiekt u C{O)) klasy M yClass val ue):
z o s t a ł wywoła ny".
II Finalizator.
IM yClass() (
Console: :Writ eLineC"Fi nali zat or obiektu C{ O}) klasy M yCl ass value): }
private: lnt val ue : }:
int mai nCarray<Syst em: :St ring A> Aargs ) {
M yClassAobJl = gcnew M yCl assCl ): M yCl assAobJ2 = gcnew M yClass(2); M yCl assA obj3 = gcnew M yClass(3): delete obj 1: obJ2->-MyClassC) , Conso le : :WriteLi neC L"Kon iec programu ."): ret urn O;
zo s t a ł wywo ła ny .".
594
Visual C++ 2005. Od podstaw Rezultat działania tego programu jest następujący:
Dest rukt or obiekt u (l ) klasy M yCl ass zo s ta ł wywoła ny .
Dest rukt or obiektu (2) klasy M yCl ass zos t a ł wywala ny. Koni ec programu. Final izat or obiekt u (3) klasy M yCl ass zos ta ł wywal any.
Jak to llziała Klasa MyClass ma konstruktor, destruktor i finalizator. Destruktor i finalizator wysyłają tylko komunikaty na wyjście , dzięki którym wiemy, kiedy każdy z nich został wywołany. Dodatkowo wiadomo jest, dla którego obiektu wywołany został finalizator lub destruktor, ponieważ wysyłają one na wyj ś cie także wartość pola val ue. W funk cji ma i n( l tworzone są trzy obiekty typu MyClass różniące się przechowywanymi wartościami 1, 2 i 3. Dla obiektu obj 1 można zastosować operator del et e, a następn ie jawnie wywołać destruktor dla obiektu obj2. W rezultacie otrzymujemy dwa pierwsze wiersze danych wyjściowych wygenerowanych przez wywołania destruktora dla obiektów. Następny
wiersz danych wyjściowych został utworzony przez instrukcję poprzedzającą inr et urn w funkcji main ( l . A zatem ostatni wiersz danych wyjściowych, wygenerowany przez finalizator dla obiektu obj3, znajduje s ię już po zakończeniu funkcji mai n( l. Z danych wyjściowych wyn ika, że konstruktory są wywoływane w momencie usuwania obiektu lub jawnego wywoływaniajego destruktora. Destruktor obiektu zostaje wykon any . Operacje te wstrzymują wykonanie finalizatorów dla obiektów. Obiekt wskazywany przez obj 3 jest niszczony przez mechanizm usuwania nieużytków, gdy program kończy działanie, a finalizator wywoływany jest w celu wyczyszczenia wszelkich niezarządzanych zasobów. strukcję
Jeżeli
zatem klasa ma zarówno finalizator, jak i destruktor, to w czasie niszczenia obiektu wytylko jeden z nich. Destruktor jest wywoływany, gdy niszczenie obiektu zostało zaprogramowane, a fmalizator, gdy obiekt w sposób naturalny wychodzi poza zasięg . W związ ku z tym można dojść do wniosku , że jeżeli po zniszczeniu obiektów pamięć jest czyszczona za pomo c ą finalizatora, nie powinno się jawnie usuwać obiektów. woływany jest
Jeżeli usuniemy komentarz w funkcji mai nt l z instrukcji, które niszczą obiekty obj l i obj2, to zobaczymy, że finalizatory tych obiektów wywoływane są przez mechanizm usuwania nieu żytków, kiedy program kończy działanie. Jeżeli natomiast usuniemy finalizator z klasy MyClass, to stwierdzimy, że destruktor obiektu obj3 nie zostanie wywołany przez mechanizm usuwania nieużytków, a więc pamięć nie jest czyszczona. Nasuwa się zatem wniosek, że jeżeli chcemy, aby niezarządzane zasoby używane przez obiekt były obsłużone bez względu na to, w jaki sposób kończy się czas życia obiektu, powinniśmy zaimplementować w klasie zarówno destruktor, jak i finalizator.
Klasy generyczne W języku C++/CLI istnieje możliwość definiowania klas generycznych. Określona klasa konkretyzowana jest z klasy generycznej w czasie działania programu. Można definiować generyczne klasy wartości, generyczne klasy referencyjne, generyczne klasy interfejsowe oraz
Rozdzial9.• Dziedziczenie ilunkcie wirtualne
595
generyczne delcgaty. Klasę generyczną definiuje się za pomocą jednego lub większej liczby parametrów typu w podobny sposób jak w przypadku funkcji generycznych, o których pisałem w rozdziale 6. Poniżej
znajduje
się przykładowa definicja
klasy generycznej Stack z projektu e w9_14:
II Plik naglówkowy Stack.h w projekcie Cw9 j l. II Generyczna klasa reprezentują c a stos.
gener ic ref class Stack {
privat e: II Definicj a elementów do przechowywania na stosie.
ref st ruct !t ern (
T ObJ : It ern Ne xt : A
II Uchwy t do obiektu w tym elemencie. II Uchwy t do n astęp n ego elementu na stosie lub nullpt r.
II Konstruktor.
Item(T obj. It em" next i : Obj (obj ), Next (next) {) }:
It ern Top: A
II Uchwyt do elementu znajdującego się na wierzchu.
publ t e : II Usta wianie obiektu na stosie.
void Pu sh(T obj ) {
Top = gcnew It ern( obj, Top):
II Utworzenie nowego elementu i ustawienie go na samej górze.
} II Usuwanie obie ktu ze stos u.
T Pop () (
i f( Top == nu l l pt r ) retu rn H ):
II Jeżeli stos j est pu sty, II zwróci wartość zerową.
T obj = Top->Obj : Top = Top- >Next : ret urn obj;
II Pobieranie obiektu z elem ent u. II Wstawienie następn ego elementu na wierzch.
} }:
Klasa ta w wersji generycznej ma parametr typu T. Należy zwrócić uwagę, że zamiast słowa kluczowego typename do określenia parametru można by było użyć słowa kluczowego c lass w tym kontekście nie ma między nimi żadnej różnicy . Argument typu zostaje podstawiony w miejsce parametru T, kiedy używany jest typ klasy generycznej . Parametr T zamieniany jest na argument typu poprzez definicję klasy, a wię c główną zaletą w porównaniu z oryginalną wersją klasy jest fakt, że klasa generyczna jest o wiele bezpieczniejsza, nie tracąc przy tym na elastyczności. Funkcja składowa oryginalnej klasy Push( ) przyjmuje każdy uchwyt, a więc można by było na jednym stosie umieścić obiekty różnego typu, jak M yCl as SA, Str i ng czy jakiegokolwiek innego . A
596
Visual C++ 2005. Od podstaw Spójrzmy na implementację funkcji Popt ) . W wersji oryginalnej zwracała ona nul l pt r, j eże l i element na wierzchu stosu był zerowy. Dla parametru typu nie można jednak zwrócić null pt r, ponieważ argument typu może być typem wartośc i . Rozwiązaniem j est zwrócenie Te l, czyli wywołanie konstruktora bezargumentowego dla typu T. Daje to w wyniku ekwiwalent Odla typu wartości i null ptr dla uchwytu . Zauważ, że
na parametry typu klas generycznych można nakładać ograniczenia za pomocą kluczow ego where w taki sam sposób jak w przypadku funkcji gen erycznych, omawianych w rozdziale 6.
słowa
z typu generycznego przechowującego uchwyty do obiektów klasy Bax można utworz yć stos w następujący sposób:
Argument typu znajduje się w trójkątnym nawiasie. Instrukcja ta tworzy na stercie CLR obiekt St ack-Box"> , Obiekt ten pozwala na ustawianie uchwytów typu Bax na stosie, a także wszelkich innych uchwytów, których bezpo średnią lub po średnią klasą bazową jest klasa Box. Wypróbujemy to na zmodyfikowanej wersji programu Cw9_14 . A
~ Uźywanie generycznegO typU klasowego Utwórz nowy program konsolowy CLR o nazwie Cw9j l i skopiuj do niego pliki nagłówkowe Container.h, Box.h oraz GlassBox.h z projektu Cw9_14. Dodaj te pliki do projektu, klikając prawym przyciskiem myszy Header Fil es w zakładce Solution Explorer i wybi erając Add/Existing ltem z menu kontekstowego. Następnie dodaj do projektu nowy plik nagłówkowy Stack.h i wpisz do niego definicję generycznej klasy St ack, która została utworzona powyżej. Nie zapomnij o dodaniu dyrektywy #pr agma ance na początku pliku. II Cw9_2 I.cpp : main proj ect file. II
Używan ie
#include #include #incl ude #include
klasy
zagnieżdżonej
do definicji stosu .
"stdafx.h" "Box.h" "Gl assBox.h" "St ack.h"
II Dla klas Box i Container. II Dla klasy GlassBox (oraz Box i Container). II Dla ge nerycznej klasy reprezentującej stos.
using names pace System: int mai n(array<System : :St r ing A> Aargs) (
Conso1e: :W riteLi ne ( L" P u d e ł ka w t ab1icy
ma j ą n as t ępu j ą c e
pojemnośc i :"):
Rozdział 9.
• Dziedziczenie ifunkcie wirtualne
for each(BoxA box in boxes) box ->ShowVolume() ;
II
Con sole : ;Writ eLi ne(L"\nDodawa nie
pu dełek
Wysyła nie
na
wyjście pojemnoś ci pudełka.
do stosu. .. "):
StackA st ack = gcnew Stack; for each(BoxA box i n boxes ) st ack->Push(box);
II Utworze nie stosu.
Console: ;WriteLine( L"Usuwani e pudel ek ze st osu prezent uje je w odwróconej BoxA item; whil e«item = st ack->Pop() ) 1= nul l pt r ) safe_cast(it em)->ShowVol ume ( );
k o l e j n o ś ci :" ) ;
II Wypróbowywanie generycznego typu Stack przechowującego liczby ca łkowite.
Stack" numbers = gcnew Stack; II Utworzenie stosu.
Console: :Writ eLi ne(L"\ nDodawanie for (i nt i = 2 ; iPop ()) !- O) Console: ;Write( L" {O,5} ".number );
c a ł k ow ityc h
ze st osu daje wwyniku ' '') ;
Console : :WriteLi net i : ret urn O;
Rezultat działan ia powyższego programujest n astępujący : Pud e ł ka
w t abl icy
Po jem no ś ć użytkowa Pojemno ść u żytkowa Pojemno ś ć u ż ytkowa Pojemność użytkowa
maj ą n astę p uj ąc e pojemn ośc i:
obiektu obiektu obiektu obiektu
klasy klasy klasy klasy
Box wynosi Box wynos i Box wynosi Box wynos i
24 20.4 120 102
Dodawanie pude ł e k do st osu.,. Usuwanle pud e łe k ze stosu prezentuje je w odwróconej Poj emn o ść użyt k owa obiektu klasy Box w ynosl 102 Poj em no ść uż yt k owa obiektu klasy Box w ynosi 120 Po jem n o ś ć użytkowa obiektu klasy Box wynosi 20 ,4 P ojem n o ść użytkowa obiektu klasy Box w ynos i 24 Dod awanie pude łe k do st osu. . . 2 4 6 8 10 12 li czb c ał k ow ityc h ze stosu daje wwyniku: 10 8 6 4 2
U s u n i ęc i e
12
kol ejno ś c i :
597
598
Visual C++ 2005. Od podstaw
Jak lo działa Stos przechowujący uchwyty do obiektów klasy Box definiowany jest w
poniższej
instrukcji:
II Utworze nie stosu.
Parametrem typu jest Box"; a w ięc stos przechowuje uchwyty do obiektów klasy Box lub uchwyty do obiektów klasy Gl ass Box. Kod wstawiający obiekty na stos i u suwający je ze stosu niczym nie różni się od tego w projekcie 9_14 . Różnica polega na tym, że nie mo żna by było na stos wstawić obiektu , jeżeli typ nie miałby jako pośredniej lub bezpo średniej klasy bazowej klasy Box. A zatem typ generyczny gwarantuj e, że wszystkie obiekty są obiektami klasy Box. Przechowywanie liczb
całkowitych
wymaga utworzenia nowego obiektu Stack :
StackA numbers = gcnew Stack ;
II Utworzenie stos u.
W oryginalnej wersj i ten sam niegeneryczny obiekt Stac k przechowywał referencje do obiektów klasy Box i liczby całkowit e , pokazując w ten sposób , że działania związane ze stosem nie były bezpieczne dla typów. Tutaj określiliśmy argum ent typu dla klasy generycznej jako typ wartości i nt . Dzięki temu funkcja Push() przyjmuje tylko obiekty tego typu. Pętle usuwające
obiekty ze stosu pokazują, że zwracanie T() w funkcji Pop() Odla typu int i null pt r dla typu uchwytowego Box" .
rzeczywiście
daje
Generyczne klasy inter1ejsowe Interfejsy generyczne definiuje się w podobny spo sób co generyczne klasy referencyjne. Ponadto generyczną klasę referencyjną można zdefiniować na bazie interfejsu generycznego. W celu zademonstrowania, jak to działa , możemy zdefiniowa ć interfejs generyczny, który może być zaimplementowany przez klasę generyczną Stack . Poniżej znajduje się definicja interfejsu generycznego: II Interf ej s dla
dzia lań
na stosie.
generic publ ic int erf ace class ISt ack {
vaid Push(T abj) : T PapO :
II Wstawianie elem entu na stos.
}: Powyższy
interfejs ma dwie funkcje i usuwania ich.
identyfikujące
Definicja generycznej klasy Stack
implementującej
stępująca:
generic ref class Stack : IStack {
private ; II Defini owanie elem entów do przechowywania na stosie.
ref struct Item {
operacje wstawiania elementów na stos
interfejs generyczny ISt ack jest na-
Rozdział 9.
T Obj : It em" Next .
• Dziedziczenie i funkcie wirtualne
599
II Uchwyt do obiektu w tym elemencie. II Uchwy t do następnego obiektu na stosi e lub nullptr.
II Konstrukt or.
Item(T obJ . It em next) : Obj(obj) . Next(next){ } A
}:
It em Top: A
II Uchwyt do obiektu znajdującego s ię na wierzchu .
public: II Wstawienie obiektu na s tos.
virt ual void Pus h(T obj ) Top = gcnew It em (obj . Top) :
II
Us un ięcie
II Utworzenie nowego obiektu i umies zczenie go II na wierzchu .
obiektu ze stos u.
vir tual T Pop() if (Top == null pt r ) ret urn TO:
II Jeżeli stos jest pusty, II zwróć wartość zerową.
T obj = Top->Obj : Top = Top->Next : return obj:
II Pobranie obiektu z elementu. IlUstawienie n ast ępn ego elementu na wierzchu.
}:
Zmiany w stosunku do poprzedniej wersji definicji klasy generycznej St ack zostały wyróż nione na szarym tle. W pierwszym wierszu definicji klasy generycznej parametr typu T został użyty jako argument typu interfejsu IStack , a więc argument typu użyty dla egzemplarza klasy St ack ma zastosowanie również do interfejsu. Funkcje Push () i Pop() muszą być teraz zdefiniowane w klasie jako wirtualne, ponieważ są one wirtualne w interfejsie. Jeśli chcesz zobaczyć, jak program działa z interfejsem generycznym, możesz dodać do poprzedniego przykładu plik nagłówkowy zawierający interfejs I St ack oraz poprawi ć definicję klasy generycznej St ack, tak aby była zgodna z tym programem, a następnie dokonać ponownej kompilacji.
Kolekcie generyczne Kolekcja to klasa organizująca i przechowująca obiekty w określony sposób. Lista powiązana i stos są typowymi przykładami kolekcji . Przestrzeń nazw System : :Co 11 ect i ons : :Gener i c zawiera szeroki zestaw kolekcji generycznych implementujących kolekcje o ściśle określonych typach. Dostępne kolekcje generyczne są pokazane w tabeli na następnej stronie .
Nie będę zagłębiał się w szczegóły dotyczące powyższych klas, ale krótko przedstawię trzy z nich, które mogą być najbardziej przydatne. Dla uproszczenia w przykładach będę używał typów wartościowych , chociaż kolekcje działają również z typami referencyjnymi.
600
VisIlai C++ 2005. Od podstaw
Typ
OpiS
Li st
Przechowuje elementy typu T w prostej automatycznie powiększać.
LinkedLi st
Przechowuje elementy typu T w
Stack
Przechowuje elementy typu T na stosie, który stanowi mechanizm przechowywania typu .first-in last-out" .
Queue
Przechowuje elementy typu T w kolejce, która je st mechanizmem przechowywania typu .first-in first-out" ,
Dict iona ry
Przechowuje pary
liście
klu cz-wartość,
li ście ,
która w razie potrzeby
podwójnie
gdzie klucze
może się
wiązanej.
są typu
K, a wartości typu V.
List -lista generyczna Li st definiuje listę generyczną, która w razie potrzeby automatycznie zw ię ks za swoje rozmiary . Elementy do tej listy można dodawać za pomocą funkcji Add( ), a dostęp do nich można uzyskać za pomocą indeksów - podobnie jak w tablicach. Poniżej znajduje się definicja listy przechowującej warto ści typu i nt: List numbers = gcnew List: Powyższa
lista ma określoną pojemność domyślną, ale można znajduje się definicja listy o pojemności 500:
podać własną wymaganą
war-
tość . Poniżej
Li st numbers = gcnew List<SOO>; Obiekty do listy dodaje
się
za pomocą funkcji Add():
for(i nt i = O ; iAdd( 2*i+1); Powyższy kod dodaje 1000 liczb całkowitych do listy nurnbers. Lista powiększa się automatycznie, jeżeli jej pojemność jest mniejsza niż 1000. Aby dodać element do istniejącej już listy, można użyć funkcji Inse rt( ) w celu dodania elementu określonego przez drugi argument w miejscu wskazanym przez indeks w pierwszym argumencie. Elementy listy są indeksowane od zera, podobnie jak w tablicach. Zawartość
listy
można zsumować:
i nt sum= O; for t i nt l = O : i Count : i ++) sum += numbers[i ] : Count jest właściwością zwracającą bieżącą liczb ę elementów w liście . Dostęp do elementów listy można uzyskać poprzez domyślną właściwość indeksowaną. W ten sposób można także pobierać i ustawiać wartości. Należy zauważyć, że za pomocą domyślnej właściwości indeksowanej nie można zwiększyć pojemności listy. W przypadku użycia indeksu spoza bieżącego zbioru elementów listy wywołany zostaje wyjątek.
Rozdział 9.
Elementy listy
można zsumować także
w
• Dziedziczenie ifunkcie wirtualne
następujący
601
sposób :
for eachCi nt n in numbers) sum+=n ;
Istnieje wiele funkcji, których można używać do operacji na listach. Należą do nich funkcje usuwające elementy, sortujące elementy, a także przeszukujące zawartość listy.
LinkedList - generYCZna lista podwójnie wiązana listę wiązaną z przednimi i wstecznymi wskaźnikami , a więc można w obie strony. Poniżej znajduje się definicja listy wiązanej przechowującej liczby zmiennopozycyjne:
Li nked Li st definiuje
ją iterować
Link edList<doub l e > ~
Wartości
do tej listy
values = gcnew Li nkedL i st<double>;
można dodać
w
następujący
sposób:
forCint i = O ; iAddLastC2 .5*i);
Funkcja AddLast( ) dodaje elementy na końcu listy. Aby należy posłużyć się funkcją AddF i rst( l .
dodać
element na
początku
listy,
Funkcja Fi nd (l zwraca uchwyt typu Li nkedLi sttłode-T>" do wierzchołka listy zawierającego która jest przekazywana jako argument do funkcji Fi nd( ). Uchwytu tego można użyć do wstawienia nowej wartości przed znalezionym wierzchołkiem lub po nim. Na przykład :
wartość,
Li nkedL i stNadeA nade
~
val
ues->Fi nd C20 .O) ; II Znajdź wierzchołek zawierający II
ifCnade 1= nullp tr ) va l ues->AddBeforeCnade. 19 9):
wartość
II Wstaw
20.0.
19.9 przed tym wierzchołkiem.
Pierwsza z powyższych instrukcji znajduje wierzchołek zawierający wartość 20. O. Jeżeli taki nie istnieje, funkcja Fi nd( l zwraca wartość null ptr . Ostatnia instrukcja, wykonywana, jeżeli wierzchołek node nie ma wartości zerowej, dodaje nową wartość 19.9 przed tym wierzchoł kiem. Za pomocą funkcji AddAfter ( l można dodać nową wartość za danym wierzchołkiem. Przeszukiwanie listy powiązanej jest względnie wolne, ponieważ konieczne jest przejście po kolei przez wszystkie elementy. Elementy listy
można zsumować
w
następujący
sposób:
dauble sumd = O; for eachC dauble v i n values) sumd +~ v; Pętla for each przechodzi przez wszystkie elementy listy i zapisuje ich w zmiennej sumd. Właściwość
łączną wartość
liczbę elementów w liście powiązanej, a właściwości Head i Tai l odpowiednio pierwszego i ostatniego elementu. Właściwości Fi rst i Last mają takie samo działanie jak Head i Tai l.
Count zwraca
zwracają wartości
602
Visual C++ 2005. Od podstaw
Oiclionary - generyczny slownik przechowuiący pary klucz-warlość Kolekcja generyczna Dict ionary wymaga podania dwóch argumentów typu - pierwszy z nich określa typ klucza, a drugi typ wartości skojarzonej z tym kluczem. Słownik jest szczególnie przydatny w przypadku, gdy dysponujemy parami obiektów , które chcemy przechowywać , gdzie jeden obiekt jest kluczem dostępowym do drugiego obiektu. Przykładem pary klucz-wartość , którą możemy zechcieć przechowywa ć w słowniku , mo że być nazwisko i numer telefoniczny, ponieważ numeru telefonicznego zazwyczaj poszukuje się za pomocą nazwiska jako klucza. Przypu ś ćmy, że mamy zdefiniowane klasy Name i PhoneNumber zaw ieraj ące odpowi adając e sobie nazwiska i numery telefonów . Słow n i k przechowując y pary nazwisko-numer można zdefi n iować w następujący sposób : C===DietionarYA phonebook = genew Diet ionary: A
Dwa argumenty typu to Name" oraz PhoneNumber a zatem klucz jest uchwytem do nazwiska, a wartoś ć uchwytem do numeru telefonicznego. A
,
Nowy wpis do
sł o w n i ka
phonebook można
dodać
w następujący sposób:
Na me name ~ genew Name("Jim". "Jones") : PhoneNumber number = genew PhoneNumber (914. 316. 2233): phonebook->Add( name . number ): II Dodawani e do słown ika pary nazwisko -numer. A
A
Aby pobra ć wartość danego wpisu w słowniku, wanej - na przykład:
należy użyć domyślnej właściwości
indekso-
try {
PhoneNumber t heNumber A
=
phonebook[name ] :
}
eate h(KeyNot FoundFoundException kn fe) A
(
Console: :Write Line(knfe) :
Klucz podawany jest jako wartość indeksu domyślnej właściwości indeksowanej , która w tym przypadku jest uchwytem do obiektu Name. Jeżeli klucz istnieje, zwracana jest jego warto ść . J eż el i klucz nie zostanie odnaleziony w kolekcji, to powodowany jest wyjątek typu KeyNot FoundExcept i on. W związku z tym przy każdej próbie uzyskania dostępu do wartości klucza, który może nie istni eć , kod powinien znajdować się w bloku try . Wła ś ciwo ś ć
śc iwoś ć
keys obiektu Di ct iona ry zwraca kolekcję zawierającą klucze sło w n i ka, a wła Val ues kolekcję zawieraj ącąjego wartości . Właściwość Count zwraca liczbę par klucz-
-wartość znajdujących s ię
w słowniku.
Spójrzmy teraz na omawiane zagadnienia w przykładowym programie.
#i nc l ude "st dafx. h" usi ng namespace Syst em: using namespace Syste m: :Col lect ions : :Generic :
II Dla kolekcj i generycz nych.
II Klasa zawierają ca nazw isk o.
ref class Name {
publ te: NameCSt ring namel , St ri ng name2) : Fi rst Cname1), Second(name2){} virt ual St rinq" ToSt rlng() over ride{ ret urn Fl rst +L" "+Second:} privat e: Str lngA First: St ri ng A Second: A
A
}: II Klasa zawierająca numer telef oniczny ,
ref class PhoneNumber {
publ te: PhoneNumber Cint area. int local, i nt number ): AreaCarea) ,Local Clocal ). Number Cnumber ){} vir t ual Str ingA ToSt r ing() ov erride { ret urn Area +L" "+Local +L " "+Number : } private: i nt Area: int Local : int Number: }:
int main(array<Syst em : .Str ing A> Aargs ) (
II
Użyc ie
listy List « I> .
Console: :WriteL ineCL"Tworzenie l ist y l iczb Li st A numbers = gcnew Li st : for Ci nt i = O : iAddC2*i+l ):
ca ł k ow ityc h
Lt st -T> ; " ) :
II Sum owanie zawartoset listy.
int sum = O: for Cint i = O : iCount : i++) sum+= numbers [i J: Console : : ~ r ite L ineCL"Suma = (O)" , sum): II
Użyc ie
listy LinkedList.
Console: :WriteLi neC L" \nTwo rzenie li st y Li nkedLis t podwój nych wart o ś c i :" ): LinkedList <double>Ava l ues = gcnew Li nk edL i st <double> : forC int i = O : iAddLastC 2.5*i) : doub le sumd = 0.0:
&03
604
VisIlai C++ 2005. Od podstaw for each(double v in values) sumd +~ v; Conso le: ;WriteLine(L"Suma
=
{O }", sumd);
Li nkedListNode<double>A node = va lues->Fi nd (20.O) ; values->AddBefore(node, 19.9); values->AddAfter(values->F ind(30.0), 30.1);
II Znajdź wierzchołek zawierający 20.0.
II Ponownie zsumuj zawartość listy powiązanej.
sumd = O O; for each(double v in values) sumd += v; Console : :WriteLine(L"Suma po dodaniu II
Użycie słownika
wartości
{O }", sumd);
Dictionary,
Console: :Wri t eL i ne(L "\nTworzenie słow nika Dictionary par nazwisko -numer:"); DictionaryA phonebook = gcnew Dictionary; II Dodawanie par nazwisko-numer do
słownika,
Name A name = gcnew Name("Jan" , "Kowals ki"); PhoneNumber A number = gcnew PhoneNumber(914, 316 , 2233 ); phonebook->Add (name, number); phonebook->Add(gcnew Name( "Stanisław" , "Nowa k"), gcnew PhoneNumber(l23, 234.3456) ) ; phonebook->Add Cgcnew Name( "Mari a", "Kwi atkowska" ), gcnew PhoneNumber(515 ,224,6864) ) ;
II Lista wszystkich numerów.
Conso le: :WriteL i neC L"Li sta wszystkich numerów :" ) ; for each(PhoneNumber A number in phonebook->Values) Console: :Wri teLine(number); II Lista nazwisk i numerów.
Console: :WriteLine(L"Uzyskanie do stępu do kluczy w celu s por ząd zen i a listy ws zystkich par na zwisko-numer:"); for each(Name A name i n phonebook->Keys) Console :WriteLineCL"{O } : {l }", name , phonebook[name]); retur n O; Tworzenie listy licz b całkowityc h L1St : Suma = 1000000 Tworzenie l isty LinkedL ist podwójnych Suma = 1248750 Suma po dodan iu wartośc i = 1248800
wartości:
Tworzen ie s łownika Dict ionary par nazwisko-numer: Lis ta wszystkich numerów: 914 316 2233 123 234 3456 515 224 6864 Uzyskanie dost ępu do kluczy w celu sporządzenia listy wszystk ich par nazwisko-numer: Jan Kowal ski : 914 316 2233 Stanisław Nowak: 123 234 3456 Maria Kwiat kowska : 515 224 6864
Rozdzial9. • Dziedziczenie i funkcje wirtualne
&05
Jak lo działa Zwróć uwagę na dyrektywę using names pa ee dla przestrzeni nazw Syste m: : Coll eet i ons: : Gener i e. Jest ona niezbędna , jeżeli chcemy używać generycznych kolekcji z określeniem w pełni zakwalifikowanych nazw klas .
W pierwszym bloku kodu w funkcji mai n() użyta została kolekcja Li st , której kod jest taki sam jak w poprzednim podrozdziale. Tworzy ona klasę przechowującą liczby całkowite w liście, a następnie zapisuje do niej 1000 liczb. Pętla sumująca zawartość listy odszukuj e dane za pomocą domyślnej właściwości indeksowanej. W tym miejscu można by także zastosować pętlę for eaeh. Nie zapomnij - domyślna wartość indeksowana uzyskuje dostęp tylko do elementów znajdujących się już w liście. Za pomocą domyślnej wartości indeksowanej można zmienić wartość istniejącego elementu listy, ale nie można za jej pomocą dodać nowego elementu . W celu dodania elementu na końcu listy należy użyć funkcji Add ( l . Aby wstawić element w określonym indeksie, należy użyć funkcji In ser t( l. Następny
blok kodu w funkcji main ( l demonstruje użycie kolekcji Lin kedL i st do sortowania typu doubl e. Wartości typu doub l e dodawane są na końcu listy powiązanej za pomocą funkcji AddLast( l w pętli f or. Wartości odnajdywane i sumowane są w pętli f or eaeh. Zauważ , że w liście powiązanej brakuje domyślnej wartości indeksowanej dającej dostęp do elementów listy. W kodzie zostały także użyte funkcje Find() , AddBefo r e() oraz AddAfter () w celu demonstracji dodawania nowych elementów w określonym miejscu w liście. wartości
Ostatni blok kodu w funkcji mai n( l pokazuje sposób użycia kolekcji Dict iona ry do przechowywania numerów telefonicznych z kluczami w postaci nazwisk. Klasy Name i PhoneNumber implementują funkcję ToSt ri ng() przesłaniającąjej odziedziczoną wersję w celu umożliwie nia funkcji Consol e: :WriteLine () wysłania na wyjście odpowiednich reprezentacji obiektów tych typów. Do słownika phonebook dodano trzy pary nazwisko-numer. Następnie sporządzona zostaje lista numerów znajdujących się w słowniku. Służy do tego pętla fo r eaeh przechodząca przez wartości zawarte w obiekcie kolekcji, zwróconym przez właściwość Va l ues słownika phonebook . Ostatnia pętla przechodzi przez nazwiska w kolekcji zwrócone przez właściwość Keys i uzyskuje dostęp do wartoś ci słownika phonebook za pomocą domyślnej właściwości indeksowanej . Nie ma tutaj potrzeby stosowania bloku t ry, ponieważ mamy pewność, że wszystkie klucze w kolekcji Keys są obecne w słown iku - jeżeli nie są, to oznacza to, że istnieją poważne błędy w implementacji klasy generycznej Diet i ona ry !
Podsumowanie Rozdział
ten omawia najważniejsze zagadnienia dotyczące mechanizmu dziedziczenia w klasach w natywnym C++ oraz w klasach w C++/CLI. Najważniejsze informacje, które nale ży zapamiętać, zostały wyszczególnione poniżej . •
Klasa pochodna dziedziczy wszystkie składowe klasy bazowej z wyjątkiem konstruktorów , destruktora oraz przeładowanego operatora przypisania.
&0&
Visual C++ 2005. Od podstaw •
Składowe klasy bazowej zadeklarowane jako prywatne nie są dostępne w klasach pochodnych. Aby uzyskać efekt podobny do użycia słowa kluczowego pr i vate, ale z zachowaniem dostępu do składowych w klasach pochodnych, należy zamiast niego użyć słowa kluczowego protected.
•
Kla sę bazową
•
Pisząc konstruktor klasy pochodnej, należy pamiętać o prawidłowej inicjalizacji zmiennych składowych klasy bazowej, a także klasy pochodnej .
•
Funkcje w klasie bazowej można deklarować jako wirtualne. Pozwala to na ponowne zdefiniowanie tych funkcji w klasach pochodnych oraz ich wybór w zależności od typu obiektu , dla którego nastąpiło wywołanie funkcji.
•
Destruktor w klasie bazowej w natywnym C++, zawierającej funkcje wirtualne, należy deklarować jako wirtualny. Zapewnia to wybór odpowiedniego destruktora dla dynamicznie tworzonych obiektów klas pochodnych.
•
W natywnym C++ jedna klasa może być zaprzyjaźniona z inną klasą. W takim przypadku wszystkie funkcje składowe klasy zaprzyjaźnionej mają dostęp do składowych tej drugiej klasy . Jeżeli klasa Ajest zaprzyjaźniona z klasą B, to nie oznacza to, że klasa Bjest zaprzyjaźniona z klasą A- chyba że tak wynika z jej deklaracji.
•
dla klasy pochodnej można określ ić za pomocą słowa kluczowego publ i c, pr i vate lub protect ed. W przypadku niepodania specyfikatora domyślnie stosowany jest pri vat e. Poziom dostępu do odziedziczonych składowych może być modyfikowany w zależności od użytego słowa kluczowego dla klasy bazowej.
Funkcja wirtualna w klasie bazowej w natywnym C++ może być czysto wirtualna, na końcu jej deklaracji zostanie umieszczony zapis = O. Klasa taka staje się abstrakcyjna i nie można tworzyć jej obiektów. We wszystkich klasach pochodnych wszystkie funkcje wirtualne muszą być zdefiniowane. W przeciwnym razie są one klasami abstrakcyjnymi.
jeśli
•
W C++/CLI klasa referencyjna może być pochodną innej klasy referencyjnej . Klasy wartości nie mogą być pochodnymi.
•
zbiór deklaracji funkcji publicznych reprezentujących klasy referencyjne . Interfejs może zawierać funkcje publiczne, zdarzenia oraz właściwości . Interfejs może również definiować statyczne zmienne składowe, funkcje, zdarzenia i właściwości. Wszystkie one są dziedziczone przez klasę implementującą interfejs . Interfejs to klasa
zawierająca
określoną funkcjonalność, którą mogą implementować
•
Interfejs może być obu interfejsów.
klasą pochodną innego
•
Delegat to obiekt zawierający jeden lub więcej wskaźników do funkcji o takim samym typie zwracanym i listach parametrów. Wywołanie delegatu powoduje wywołanie wszystkich funkcji przez niego wskazywanych.
•
Zdarzenie składowe klasy sygnalizuje zdarzenie poprzez wywołanie jednej lub większej liczby funkcji należących do procedury obsługi zdarzenia, które zostały zarejestrowane dla tego zdarzenia.
interfejsu,
zawierającą składowe
Rozdział 9.•
Dziedziczenie i tunkcie wirtualne
•
Klasa generyczna jest typem parametrycznym, którego egzemplarze tworzone są w czasie działania programu. Argumenty przekazywane do parametrów podczas konkretyzacji typu generycznego mogą być typami klas wartości lub typami klas referencyjnych.
•
Przestrzeń
•
607
nazw System: :Co1 1ect ions: :Generic zawiera kolekcje generyczne, będące klasami definiującymi bezpieczne dla typów kolekcje obiektów dowolnego typu CH /CLI. W C++/CLI można utworzyć bibliotekę klas w oddzielnej asemblacji i umieścić w pliku z rozszerzeniem .dl! .
ją
Omówiliśmy już wszystkie ważniejsze właściwości języków C++ ANSVISO oraz C++/CLI. Bardzo ważne jest, aby biegle posługiwać się mechanizmami definiowania i tworzenia klas pochodnych oraz zrozumieć proce s dziedziczenia w obu tych językach . Programowanie dla systemu Windows w środowisku Visual C++ 2005 wymaga znajomości tych technik oraz umiejętności posługiwania się nimi.
Ćwiczenia Kod źródłowy wszystkich listingów w tej książce oraz rozwiązania do ze strony www.helion.pl. 1 Co jest nie tak z
poniższym
ćwiczeń można pobrać
kodem?
class CBadCla ss (
private : int len: char* p: public: CBadCl ass(con st cha r* st r) pt st r ). len(str len(p) ) {} CBadClass(){} }:
2.
Przypuśćmy, że
mamy
klasy pochodne
reprezentujące różne
poniższą klasę
o nazwie CPta k i chcemy od niej ptaki :
class CPt ak (
prote ct ed: int rozpiet oscSkrzyde l : int wiel koscJaja: i nt predkoscLot u: int wysokoscLotu: publ ic: virtual void lot () { wysokoscLot u ~ 100; } }:
utworzyć
&08
Visuał
C++ 2005. Od podstaw Czy miałoby sens utworzenie klasy pochodnej CJast rzab od tej klasy? A klasy CSt r us? Uzasadnij swoje odpowiedzi. Stwórz hierarchię klas pochodnych, która poradzi sobie z tymi dwoma gatunk ami ptaków.
I.
Mając poniższą klasę :
cl ass CBase
( pr ot ect ed : i nt m_anlnt ; publi c: CBase ( int n) : m_anlnt ( n) { cout « virt ual void Pr i nt ( ) const = O:
"Konstrukto r podstewosyx n" :
i Jakiego rodzaju klasąjest klasa CBa se i dlaczego? Utwórz klasę po chodną klasy CBa se, która będzi e ustawiała wartość swojej odziedziczonej zmiennej przechowującej liczbę całkow i tą oraz drukowała ją na życzenie . Napisz program sprawdzający poprawność Twoj ej klasy.
l
Drzewo binarne to struktura składająca się z wi erzchołków , z których każdy zawiera wskaźnik do lewego i prawego wierzchołka oraz p orcję danych. Struktura ta została przedstawiona na rysunku 9.7. Drzewo ma początek w w ierzchołku pnia, który jest punktem początkowym do uzyskiwania dostępu do wierzchołków drz ewa. Jeden lub dwa w sk aźniki w wierzchołku mogą być zerowe. Drzewo na rysunku 9.7 zostało zorganizowane w taki sposób, że wartość każdego wierzchołka jest zawsze większa lub równa warto ści wierzchołka zn ajdującego się po jego lewej stronie oraz mniejsza lub równa wartości wierzchołka po prawej stronie. Przedstaw w postaci klasy w natywnym C++ takie drzewo binarne przechowujące liczby całkowite. Musisz także zdefiniować klasę Nade, ale może ona być zagnieżdżona w klasi e Bin aryTree. Napisz program t estujący działanie Twojej klasy Bin aryTree poprzez zapisanie w nim określonej sekwencji liczb całkowitych oraz ich odszukanie i wysłanie na wyjście w rosnącej kolejności. Wskazówka: nie bój
s..
s i ę u żywać
rekurencji.
Zaimplementuj ćwiczenie 4. jako program CLR . Jeżeli nie udało Ci tego ćw i c zen i a, to skorzystaj z odpowiedzi.
się ukończyć
8. Zdefiniuj generyczną klasę BinaryTree dla każdego typu, który implementuje interfejs Icamparabl e, oraz zademonstruj, jak działa, popr zez użycie egzemplarzy klasy generycznej do przechowywania i odszukiwania kilku losowych liczb całkowitych oraz elementów poniższej tablicy : ar ray< Strl ng Ą >A wor ds = { L"Suckces" . L"to" . L"przec hodzeni a" . L"od" . L" jedne J " , t' bez". t'utrety". L" entuzjazmu" } :
7.
Wyślij wartości
L" zd o l n o ś ć ". L"po r a ż k i ' ,
L" z a p ał u".
L"do" .
L"do" . L"n ast epnej " . l." a" . L "t a k ż e " .
odszukane w drzewie binarnym do wiers za poleceń .
Rozdział 9.• Wierzchołek Wartość
Dziedziczellie i funkcie wirtualne
pnia
= 120
lewy wskaźnik prawy wskaźnik wierzchołka
I
w ierzchołka
Wierzchołek/
~ Wierzchołek
Wartość=43
Wartość
lewy wska źnik I prawy wska źn ik
lewy wskaźnik I prawy wskaźnik wierzchołka
Wierzchołek null
wierzchołka
wierzchołka
Wartość
= 17
I prawy w skaźni
null
w ierzchołka I
Wart oś ć
null
I
~Wierzchołek Wartość
= 57
I prawy wskaźni k
~Wierzchołek = 24 null
Uporządkowane
Rysunek 9.7
wierz chołk a
~Wierzchołek
/
Wartoś ć
= 437
null
w ierzchołka
~Wierzchołek Wartość =88
null
I
drzewo binarne
null
I
= 766 null
689
610
Visual C++ 2005. Od podstaw
10
Debugowanie
Rozw iązując ć w i cze n i a
w poprzednich rozdz iała ch, prawdopodobnie niera z zdarzyło Ci s i ę z błędami w kodzie. Rozdział ten po święcony jest wykorzystaniu wbudowany ch w Visual C++ 2005 narzędzi wspomagających w tej walce programistę . Poznasz także kilka dodatkowych narzędzi, za pomocą których można znajdować i eliminować błędy z programów. Ponadto nauczysz się kilku sposobów pisania specjalnego kodu w programach, który pomaga stoczyć bitwę
odnajdywać niepraw idłowośc i .
W rozdzi ale tym dowie sz s ię: • Jak uruchomić program pod kontrolą programu uruchomierriowego.Idebuggera) / " wbudowanego w Visual C++ 2005. / "
• Jak
wykonać
• Jak
ś ledz ić
• Jak
ś l e d z i ć warto ść wyrażenia
kod programu instrukcja po instrukcji.
lub zmienia ć
wartości
zmiennych w programa ch.
w progr amie .
• Czym jest stos wywołań . • Czym
są
asercje i j ak je
• Jak
dodawać
• Jak
wykrywać
s to s ować
kod specjalnie wycieki
do sprawdzania kodu .
wspomagający
pamięci
znajdowanie
błędów.
w programach w C++.
• Jak używać narzędzi ś l e dz en i a wykonywania oraz generować dane programu uruchomieniowego w programach w C++/CLI.
wyjściowe
Co ZnaCZY debugowanie Debugowanie to proces znajdowania i usuwania błędów (ang. bugs) programu. Niewątpliwie wiesz już, że debugowanie j est integralną częścią procesu programowania - j est to zestaw, jak to się mówi, obowiązkowy . Fakty dotyczące błędów w programach brzmią raczej pesymi stycznie:
612
Visual C++ 2005. Od podstaw •
Każdy program, który nie jest dziecinnie prosty, zawiera jakieś błędy . Jeżeli chcemy, aby program był niezawodny i efektywny, trzeba wykryć te błędy, znaleźć je i wyeliminować . Zwróćmy uwagę na trzy wymienione fazy . Po pierwsze, błąd w programie wcale nie musi być widoczny od razu . Nawet jeśli jest widoczny, to możemy nie wiedzieć , gdzie się on znajduje w kodzie. A nawet jeżeli z grubsza wiemy , gdzie leży źródło problemu, to i tak możemy mieć trudności z określeniem, co dokładnie jest nie tak, a tym samym z wyeliminowaniem go.
• Wiele programów zawiera wszystkich testów .
•
błędy
nawet po ich
ukończeniu
i przeprowadzeniu
Błędy
w programie mogą pozostać ukryte. Program taki moż e działać prawidłowo nawet przez kilka lat. Zazwyczaj ujawniają się one w najbardziej nieodpowiednim momencie.
• Programy przekraczające pewne rozmiary i pewien stopień komplikacji zawsze zawierają błędy, bez względu na to, ile czasu i wysiłku poświęcono na ich testowanie. Rozmiar i stopień komplikacji programu, które gwarantują obecność błędów, nie są ściśle określone, ale Visual C++ 2005 i system operacyjny, którego używasz, z pewnością do tej kategori i należą! Jeśli należysz
do tych bardziej nerwowych osób, lepiej nie rozmyślaj zbytnio nad ostatnim punktem, w szczególności jeżeli często latasz samolotami lub regularnie znajdujesz się w po bliżu jakichkolwiek proces ów uzależnionych od właściwej pracy komputerów. To mo że się poważnie odbić na Twoim zdrowiu w razie jakiejś awarii . /
błędów
wyłapanych
k~;lidowania
Wiele potencjalnych zostaje w fazach kompilacji i I programu, choć zawsze może kilka z nich się prześlizgnąć, nawet gdy udało się utworzyć moduł wykonywalny programu . Niestety , pomimo że błędy w programach sąnieuniknione, tak samo jak śmierć czy podatki, to debugowanie nie jest rozpoznanądziedziną nauki . Można jednak przyjąć podejście strukturalne do eliminowania błędów. Aby znajdowanie błędów w progra mie było jak najmniej bolesne, można zastosować jedną z czterech strategii : • Nie odkrywaj ponownie Ameryki . Postaraj się opanować i stosować narzędzia biblioteczne dostarczane jako część środowiska Visual C++ 2005 (lub komponenty innych komercyjnych produktów, do których masz dostęp) . Dzięki temu program składa się z możliwie dużej ilości już przetestowanego kodu . • Rozwijaj i testuj kod stopniowo. Dzięki indywidualnemu testowaniu każdej ważniejszej klasy i funkcji oraz stopniowemu składaniu programu z przetestowanych komponentów proces tworzenia programu może być znacznie łatwiejszy i pozbawiony wielu trudnych do odnalezienia błędów. • Pisz kod defensywny - to znaczy pisz kod , który broni się przed potencjalnymi błędami. Na przykład deklaruj funkcje składowe klas w natywnym C++, które nie modyfikują obiektu jako typu eonst. Używaj parametrów typu eonst , gdzie jest to uzasadnione. Nie używaj w kodzie tak zwanych magicznych liczb - obiekty stałe definiuj z odpowiednimi wartościami. • Od samego początku dodawaj kod debugujący, który sprawdza i waliduje dane oraz warunki w programie. Przyjrzymy się temu trochę później w tym rozdziale .
Rozdział 10.
• Debugowallie
613
Ze względu na fakt, że bardzo ważne jest, aby programy zawierały tak małą liczbę błędów , na ludzkie możliwości, Visual C++ 2005 dostarcza potężną broń oraz narzędzia służące do znajdowania błędów . Zanim jednak przejdziemy do nich, przyjrzymy się dokładnie procesowi powstawania błędów.
jaką pozwalają
BlędJ OprOgramOwania Oczywiście, głównym źródłem błędów
w programie jest programista. Błędy popełniane przez od zwykłych literówek (wciśnięcie niewłaściwego klawisza) po zastosowanie nieprawidłowej logiki. Mnie też trudno uwierzyć , że robię takie głupie błędy, ale jako że nikt jeszcze nie wpadł na sensowny pomysł, co innego mogłoby być ich źródłem , musi to być prawda. Istota ludzka to stworzenie podatne na przyzwyczajenia, a więc prawdopodob nie odkryjesz, że niektóre błędy powtarzasz wielokrotnie. Co gorsze, niektóre błędy są dla innych oczywiste, a dla nas niewidoczne. W ten sposób komputery dają nam lekcję pokory. Błędy , które może popełnić programista, a których skutkiem są błędy w programie, można podzielić na dwa rodzaje : programistę rozciągają się
•
Błędy składni - powstają w wyniku zastosowania nieprawidłowej składni w instrukcjach. Przykładami mogą tutaj być pominięcie średnika na końcu instrukcji albo użycie dwukropka w miejscu, gdzie powinien być przecinek. Błędami składni nie trzeba się zbytnio przejmować. Kompilator rozpoznaje je wszystkie i zazwyczaj podaje całkiem precyzyjne wskazówki co do źródła problemu, a więc łatwo się ich pozbyć.
•
Błędy semantyczne - w przypadku tych błędów kod jest poprawny składniowo, ale nie robi tego, czego oczekujemy. Kompilator nie wie, co było naszym zamiarem, a więc nie może wykryć błędów semantycznych. Jednak częstą wskazówką wystąpienia takiego rodzaju błędu jest niespodziewane kończenie działania programu. Przeznaczeniem narzędzi debugowania w Visual C++ 2005 jest pomóc programiście w odnalezieniu błędów semantycznych. Mogą one być bardzo subtelne i trudne do odnalezienia, na przykład gdy program czasami generuje nieprawidłowe wyniki lub ulega nieregularnym przerwaniom. Prawdopodobnie najtrudniejsze są błędy w programach wielowątkowych, w których równoległe ścieżki wykonywania programu nie są właściwie sterowane.
Oczywiście , w środowisku systemowym, w którym pracujemy, także są błędy (włącznie z Vi sual C++ 2005), ale jest to ostatnie miejsce, które należy podejrzewać, kiedy program nie chce działać . Nawet kiedy mamy pewność, że to musi być kompilator lub system operacyjny, to w dziewięciu przypadkach na dziesięć nie mamy racji. Oczywiście w Visual C++ 2005 są błędy i jeżeli chcesz być na bieżąco w tych, które znaleziono do tej pory, razem z dostępnymi poprawkami, informacji możesz poszukać na witrynie Microsoftu poświęconej Visual C++ (http://msdn.microsojt.com/visualcl) lub - jeżeli stać Cię na subskrypcję biuletynu Microsoft Development Network - możesz co kwartał otrzymywać poprawki najnowszych błędów.
Dobrze jest zrobić sobie listę błędów odkrytych we własnym kodzie do późniejszego wglądu. Analizując nowy kod pod kątem błędów popełnianych wcześniej, często można znacznie zre dukować czas potrzebny do usunięcia błędów z nowych projektów.
614
Visual C++ 2005. Od pOdstaw Z natury programowania wynika, ż e różnorodność błędów jest nieskończona. Ale istnieje kilka ich rodzajów, które są szczeg ólnie często spotykane. Możliwe, że już o nich wiesz , ale i tak zachęcam Cię do rzucenia na nie okiem.
Najczęściej · spotykane blędy Użytecznym sposobem katalogowania błędów jest przyporządkowywanie ich do objawów, jakie wywołują, ponieważ tak wygląda nasze pierwsze zetkn i ę c i e się z nimi. Poniższa lista pię ciu często spotykanych objawów nie jest w żadnym przypadku wyczerpująca. Z pewności ą w miarę zbierania doświadczeni a można do niej dodać wiele punktów:
Nieudana próba inicjalizacji zmiennej . N ieskończona pętla .
Nieprawidłowy wskaźnik.
Dwukrotne zwalni anie tej samej
pamięci .
Nieudana próba implementacji lub Nieprawidłowe
błąd
destruktora.
dane ze strumienia
Wczytywanie za pomocą operatora ekstrakcji i funkcji get l mer l.
wyniki -
Błąd
wejściowego Nieprawidłow e
typograficzny:
-=
zamiast
==
lub i zamiast j itd.
Nieudana próba inicj alizacj i zmienn ej. Przekroczenie zas ięgu typu
całkowitego .
Nieprawidłowy wskaźnik. Pominięcie
jak wiele różnego rodzaju błędów mogą spowodować nieprawidłowe oraz jak różnorodne mogą być tego objawy . Są one chyba naj częstszym źródłem trudnych do zlokalizowania błędów, a więc zawsze należy dokładnie sprawdzać wszystkie
Warto
zwrócić uwagę,
instrukcji br eak w instruk cj i swi tch.
wskaźniki
Rozdział 10.
operacje z nimi
• DebuDowanie
615
związane. Dzięki
wskaźników można uniknąć
wiedzy na temat sposobów powstawania nieprawidłowych wielu pułapek. Najczęstszymi powodam i powstawania niepra
widłowych ws kaźn i kó w są:
• Nieudana inicjalizacja wskaźn ika podczas jego deklaracji. • Nieudana próba ustawienia wskaźnika do obszaru podczas usuwania przydzielonej pamięci . • Zwracanie adresu zmiennej lokalnej przez
pamięci
wolnej na warto ść nu11
funkcję .
• Nieudana próba implementacji konstruktora kopiującego i operatora przypisania w klasach przydzielających pamięć w wolnym obszarze. Nawet jeśli dopilnujemy wszystkiego, o czym mowa powyżej, w kodzie nadal pozostaną błędy . Przyjrzyjmy się zatem narzędziom dostarczanym przez Visual C++ 2005 w celu usprawnien ia procesu ich znajdowania.
Podstawowe operacje debugowania Do tej pory wielokrotnie tworzyliśmy wersję testową tworzonych programów, ale mimo tego nie używaliśmy debuggera. Debugger to program pozwalający na kontrolę wykonywania pro gramu poprzez wykonywanie kodu wiersz po wierszu lub tylko jego określonej części. W każ dym punkcie zatrzymania dcbuggera, można sprawdzić, a nawet zmienić wartości zmiennych przed kontynuowaniem operacji. Można także zmienić kod źródłowy , skompilować go ponow nie, a następnie jeszcze raz uruchomić program od początku . Kod źródłowy można modyfiko wać nawet podczas wykonywania programu krok po kroku. Przed przejściem do następnego kroku po pojawieniu się modyfikacji w kodzie debugger automatycznie ponownie kompiluje program przed wykonaniem następnej instrukcji . podstawowe możliwości debugowania Visual C++ 2005 , dostępnego w tym debuggera użyjemy z programem , co do działania którego jesteśmy raczej pewni. Następnie będziemy mogli tylko pociągać za d źwignie, aby zobaczy ć , jak wszystko działa. Weźmy prosty przykład z rozdziału 4., w którym użyto wskaźników: Aby
zrozumieć
środowisku
II Cw4 05.cpp
II Ćwiaen ie użycia wskaźn ików.
#incl ude·
us ing std: :CDUt ;
us i ng st d: : endl:
using st d: .hex:
usi ng st d: .dec :
tnt mairu ) {
long* pnumber = NULL : long numberl = 55. number2 pnumber = &numberl : *p number += 11 ;
II Deklaracja i inicjalizacja wskaźnika .
~
99 :
II Zapisyw anie adresu do wskaźnika . II Zwiększen ie zmiennej numb eri o 11.
616
Visual C++ 2005. Od podstaw cout « endl
« "numberI = " « number l
«" &numberl = « hex« pnumber;
pnumber = &number 2 ; numberI = *pnumber*lO ;
II Zmiana wskaźnika na adres zmiennej number2. II Pomnożenie zmiennej numb er Z przez /0 .
cout « endl
« "number l = «dec « numberl
« pnumber = " « hex « pnumber
« *pnumber ~ " « dec « *pnumber ;
cout « endl ;
r et urn O:
Jeżeli masz ten projekt w swoim systemie, to wystarczy go teraz tylko nym razie trzeba będzie go wprowadzić jeszcze raz.
otworzyć.
W przeciw
Kiedy piszemy program, który nie zachowuje się tak jak powinien, debugger pozwala na jego prze śledzenie krok po kroku w celu odnalezienia źródła i natury problemu oraz analizy stanu danych programu na każdym etapie wykonywania. Kod z powyższego listingu wykonamy instrukcja po instrukcji w celu prześledzenia zawartości interesujących nas zmiennych . Tym razem chcemy przyjrzeć się wskaźnikowi pnumber , treści przez ten wskaźnik wskazywanej (* pnumber) oraz zmiennym number l i number2. Najpierw należy się upewnić , że program został skompilowany w konfiguracji Win32 Debug, a nie Win32 Release (konfiguracja Debug jest domyślna, chyba że ją sami zmienimy). Konfi guracja kompilacji programu korzysta z ustawień, które można obejrzeć, wybierając z menu opcję Project/Properties . Bieżąca konfiguracja kompilacji projektu widoczna jest w dwóch znajdujących się obok siebie listach na standardowym pasku narzędzi. Aby włączyć lub wyłą czyć określony pasek narzędzi, należy prawym przyciskiem myszy kliknąć pasek standardowy, a następnie zaznaczyć wybrany pasek na li ście lub usunąć jego zaznaczenie . Upewnij się , że pasek narzędzi Debug jest włączony. Pojawia się on automatycznie w momencie rozpoczęcie pracy debuggera, ale dobrze by było zapoznać się z nim wcześniej - zanim uruchomimy debugger. Konfigurację kompilacji można zmienić poprzez rozwinięcie listy rozwijanej i wy branie innej konfiguracji. Można do tego celu użyć opcji menu Build/Configuration Manager . Standardowy pasek narzędzi pokazano na rysunku 10.1. - !Wi132
' i1deks
Rysunek 10.1 do czego służą poszczególne elementy paska narzędzi, wystarczy na nie kursorem myszy. Wtedy pojawi się chmurka z objaśnieniem funkcji poszczególnych przycisków.
Aby
dowiedzieć się ,
najechać
Konfiguracja Debug powoduje dodanie podczas kompilacji do modułu wykonywalnego pro gramu dodatkowych informacji, które umożliwiają późniejsze użycie narzędzi debugowania. Informacje te przechowywane są w pliku z rozszerzeniem .p db znajdującym się w folderze Debug projektu . W wersji ostatecznej brak tych informacji , gdyż stanowią one niepotrzebne
Rozdzial10.• Debugowanie
617
dodatkowe obciążenie , zbędne w wersji finalnej programu . Visu al C++ 2005 w wersji Pro fessionallub Enterprise optymalizuje nawet kod programu podczas kompilacji wersji osta tecznej. W wersji testowej optymalizacja nie jest wykonywana, gdyż może być związana ze zmianą kolejności elementów kodu źródłowego w celu zyskania na efektywności lub nawet całkowitym pomijaniem zbędnych partii kodu. Ze względu na fakt, że optymalizacja zaburza odwzorowanie w kodzie maszynowym kodu źródłowego , może ona spowodować co najmniej zamieszanie przy uruchamianiu programu krok po kroku . Pasek
narzędzi
Debug pokazano na rysunku 10.2.
Rysunek 10.2
Debuq "
Sprawdzając
.' .
','
.~
.... :. .y,:'-x '
·l
chmurki podpowiadające przeznaczenie poszczególnych przycisków paska, można do czego służą - niektórych z nich niedługo będziemy używać. Przy użyciu przykładowego programu z rozdziału 4. nie uda nam się przetestować wszystkich narzędzi debugowania, ale wypróbujemy niektóre ważniejsze z nich. Po zapoznaniu się z tech niką uruchamiania programu krok po kroku za pomocą debuggera nauczymy się korzystać z innych jego narzędzi na programie zawierającym już błędy. wstępnie dowiedzieć się
Oebugger można uruchomić, klikając pierwszy po lewej przycisk na pasku narzędzi Debug i wybierając z menu Debug/Start Debugging lub naciskając klawisz F5. W tym przykładzie sugeruję użyć przycisku z paska narzędzi . Oebugger może działać w jednym z dwóch try bów - wykonując kod krok po kroku (co zazwyczaj oznacza wykonywanie kodu instrukcja po instrukcji) lub wykonując kod do określonego miejsca. Miejsce, w którym debugger powi nien się zatrzymać, można oznaczyć poprzez umieszczenie w nim kursora lub w bardziej uży teczny sposób poprzez wyznaczenie tzw. punktu wstrzymania (ang. breakpoint). Zobaczmy, jak definiuje się punkty wstrzymania.
Ustawianie punktów wstrzymania Punkt wstrzymania to miejsce w programie, w którym debugger automatycznie zawiesza wykonywanie. Można określić wiele punktów wstrzymania i uruchomić program, za trzymując go w interesujących , wybranych przez nas miejscach. W każdym punkcie wstrzy mania można podejrzeć zawartość zmiennych w programie i ją zmienić , jeżeli nie zawierają wartości takich jak powinny. Program z projektu Cw4_05 będziemy uruchamiali po jednej instrukcji, ale w przypadku dużych programów podejście takie nie jest praktyczne . Zazwyczaj zachodzi potrzeba przyjrzenia się określonej partii programu, co do której są jakieś podej rzenia. W związku z tym punkty wstrzymania zazwyczaj ustawia się w miejscach, w których jest podejrzenie wystąpienia błędu. Uruchomiony program zatrzymuje się w miejscu ozna czonym przez pierwszy punkt wstrzymania. Następnie dalszą część kodu można wykonywać instrukcja po instrukcji.
Aby ustawić punkt wstrzymania dla określonej instrukcji, wystarczy na początku wiersza kodu źródłowego kliknąć w szarej kolumnie po lewej stronie numeru wiersza. Pojawi się okrągły czerwony symbol zwany glifem sygnalizujący obecność punktu wstrzymania w tym
618
Visual C++ 2005. Od podstaw wierszu. Aby punkt wstrzymania usunąć , należy dwukrotnie kliknąć symbol glifu. Na rysun ku 10.3 widoczne jest okno edytora z dwoma punktami wstrzymania zaznaczonymi w pro gramie Cw4_05.
05.cppL
Cw4
(Global 5cope)
lii
~I
-
l ;I I I
CM4_ 0 S . c PP
~
Ć ~ i c z e n l e uz y c i a ", "k a Źn lkó~.
3 'l
# l nc lud e
.-
... x
t;l
"'mainO
=!~:
< io~ t r e am>
'I
us t n q nemeapece 5t d ;
5
6· - int 7 (
O
main ()
Lo nq" p numbe r
8 9 10 11 12 13
I o nę
nUl\"lber l
p n umb e r
.
.
-
NULL ;
55. n umbe r
«
1/ De k l arac ja i i n i c J ::Il i za c j a
.
II ż a p t evva me ad r e s u do tlskai n i ka . II ZtJięk:!lzFni.:: zn'l1e n ne:J numbe r j. o 1lo
. . c-c .
& m .unb er 1
li :
u e k e ź n t ka .
99 ;
&nUIl1b e:r 1 ;
e o u t -c e e nd! « " n umbe r t
15
.
+= 11 ;
TJ:lnumbe r
H
ż
=
L
n wnb er l
«
he K «
pn1..UTll::l er ;
h~ J
O
I I zma a n e, tJs kainika n: "SI. actr e a znu en ne j n nmb e r 2 . I I P om n o ż e n i e zmi e n ne j nurobe r ż pcz. ez 10 .
= &n umb e r 2 ; number 1 = .., p m .unb e r '"" 10 :
17 " 18 19
20
pnumo e r'
co ut; names[j] ) ph rase = " wię c ej n i ż "; el se i f( names [ i] ~= names[j ] ) II Zbyt eczna, ale wywolujefun kcję op erator==(). phrase = " równe "; j Name = new char[names[ j]. get NameLengt h()+1] ; II Tablica przechowująca nazwisko . cout « endl « names[i] .getName(iName) « " jest" « phrase « names[ j ] .get Name(j Name) : cout « endl :
return O:
Funkcja i ni t ( ) pobiera następujące po sobie kombinacje imion i nazwisk z tablicy nazw w celu inicjalizacji obiektów tablicy Name. Nazwy są generowane w grupach po 25, ale nam tutaj potrzeba tylko la .
Rozdzial10.• DebuDowanie
Odnajdywanie następnego
641
błędu
Jeżeli
uruchomimy program pod kontrolą debuggera za pomocą przycisku Start Debuging na pasku narzędzi, to ulegnie on znowu załamaniu . Ukaże się okno widoczne na rysunku 10.18. znajdującego się
Ryslmek 10.18
M1Ct OSOrt
vrsuel Studio
-
~
.
,\ lklhondled except jon ot OxOOi l lb69 in CwIO_win.oxo : OXCOOOOOFD:
..lJ stockovorf1aw .
Komunikat w oknie informuje , że została przekroczona ilość dostępnej pamięci na stosie . Jeżeli klikniemy przycisk Break, to w oknie Cal! Stack będziemy mogli zobaczyć, co spowodowało tę sytuację . W kodzie znajdują się następujące po sobie wywołania funkcji ope retor-O , a więc musi ona wywoływać samą siebie. Jeśl i zajrzymy do kodu, to zobaczymy dlaczego : jest lite rówka. Ten wiersz w ciele funkcji powinien wyglądać następująco:
ret urn name
names[ j) ) phrase ~ " wi ęc ej ni t '': else i f(names[ i J ~~ names[j J) II Zbyteczna, ale wywo łuje funkcję opera tor vv- t}. phrase ~ " równe "; j Name ~ new char[names[j J .get NameLengt h()+1] ; II Tablica przechowująca nazwisko. cout « end l « names[i)getName(l Name ) « " jest " « phrase « names[jJ. getName(jName) ; }
cout « end l ; ret urn O: Aby jeszcze bardziej zmn iejs zyć ilość danych wyjśc i owych , mo żna wyłączyć ich ś l e d zen i e poprzez um ieszczenie komentarza prz ed symbolami kon troln ymi w pliku na główkowym DebugStujJ.h: II Deb ugStujJh - kontrola debugowania.
#pragma ance
#ifdef _OE8UG II#deji ne CONS TRUC TOR]RACE II#deji ne FUNCTJON TRACE
II Wys lij na wyjś cie znak wywołan ia konstruktora . II Śledź wywo ła n ia fu nkcji.
#endif Możem y s ko mp i lo w ać
ponownie proj ekt i uruchomić go jeszcze raz .
Jak to działa Program
dzi ała
następuj ą
zgodnie z oczekiwaniami. Otrzymaliśm y raport, że w programie rzeczywi ście wycieki pamięc i, oraz li stę obiektów znajdujących si ę w obszarze wolnej p am i ę ci programu . Początek danych wygenerowanych przez narzędzie debug owania obszaru
na końcu woln ej p am i ęci
wygląda n astępuj ąco:
Detected memory leaks! Dumping objects -> {148} norma l block at Ox003A5F28. 16 bytes long. Data : < > CDCDCDCD CD CD CD CD CD CD CDCD CD CD CD CD {147} norma1 block at Ox003A5EE8 . 16 bytes long. Data ; <Emi ly St ei nbeck > 45 60 69 6C 79 20 53 74 65 69 6E 62 65 63 68 00 {146} norma1 block at Ox003A5EA8 . 13 bytes long.
Data; <Emi ly Mi ll er > 45 60 69 6C 79 20 40 69 6C 6C 65 72 00
A ich
końc owa część
to :
{125} normal block at Ox003A59C8. 8 bytes long.
Dat a: 44 69 63 68 65 6E 73 00
648
Visual C++ 2005. Od podstaw {124 } normal block at Ox003A5990 . 8 bytes long. Data: 43 68 61 72 6C 65 73 00 {123} normal block at Ox003A5958. 12 byt es long. Dat a: 49 76 6F 72 20 48 6F 72 74 6F 6E 00 {122} normal block at Ox003A5920. 7 bytes long. Dat a: 48 6F 72 74 6F 6E 00 {121} normal block at Ox003A58E8 . 5 bytes long. Dat a: 49 76 6F 72 00 Object dump complet e. Obiekty, które raport podaje, że zostały w obszarze pamięci wolnej, prezentowane są w kolej od obiektu alokowanego najpóźniej do obiektu alokowanego najwcześniej. Z danych wyj ściowych jasno wynika , że klasa Name przydziela pamięć swoim zmiennym s kła d owym , ale nigdy jej nie zwalnia. Ostatnie trzy obiekty odpowiadają tablicy pName alokowanej w funk cji mai n( ) oraz zmiennych składowych obiektu myName. Bloki pełnych nazw są alokowane w funkcji ma i n() i one także zostały w obszarze wolnej pamięci. Problem, z którym boryka się nasza klasa, to fakt, że zapomnieliśmy o podstawowych zasadach dotyczących klas dyna micznie przydzielających pamięć . Klasy takie zawsze powinny zawierać destruktor, konstruk tor kopiujący oraz operator przypisania. Deklaracja klasy powinna wyglądać na stępująco : ności
class Name (
publl C: Na me() : Name(const char* pFi rst . const char* pSecond): Name(const Na me& rNa me ) : - Name():
II Konstruktor domyś lny. II Konstruktor. II Konstruktor kopiujący.
II Destruktor.
char* get Name(char* pName) const : i nt getNameLengt h() const:
II Pobierz im ię i nazw isko. II Spra wdź łączną długość imienia i nazwiska.
Definicja konstruktora kopiującego może wyglądać następująco:
Name : : Name(const Name& rName) {
pFirstname = new char[st r len(rName. pFi rst name )+l J: st rcpy(pF irst name. rName.pFir st name): pSurname = new char[str len(rName pSurname)+lJ : st rcpy(pSurname. rName.pSurname): Destruktor musi
zwolnić pamięć
tylko dwóch zmiennych
II Przydzielenie pamięci dla imienia II ijego skop iowa nie. II To sam o dla nazwiska...
składowych:
Rozdział 10.
• Oebugowanie
649
Name: :- Name() {
delete [] pFi rst name; delete [] pSurname; W operatorze przypisania są równe:
należy jak
zwykle
przewidzieć sytuację,
w której lewa i prawa strona
Name& Name; :operat or=(const Name&rName )
{
if (t his
~~
&rName )
II Jeżeli lewa strona równa
IIj est prawej ,
II zwróć tylko ten obiekt.
ret urn *t his ; delete[ ] pFi rst name ; pFirst name = new cha r[ st r len(rName .pFirstname)+l] ; st rcpy (pFi rst name . rName.pFir st name): deleteC] pSurname; pSurname = new charCst r len(rName.pSurname)+l ] : st rcpy(pSurname . rName.pSurname); ret urn *this : Powinniśmy także skłonić domyślny
nie przydziela
do
II To samo dla nazwiska...
prawidłowej
pracy konstruktor domyślny. Jeżeli konstruktor w wolnym obszarze, to istnieje ryzyko, że destruktor mylnie która nie została przydzielona w wolnym obszarze. Należy go
pamięci
będzie próbował usunąć pamięć, zmodyfikować
II Przydzielenie pamięci dla imienia II ijego skopiowanie.
do postaci:
Name : :Name() {
#ifdef CONSTRUCTOR TRACE
II Śledzen ie wywol':;-ń konstruktora. cout « "\nKonstrukt or domyś lny
klasy Name
zo s tał wywo ł a ny .
":
#end i f II Przydzielenie pamięcijednoelementowej tablicy przech owującej pus te łańcuchy.
pFirst name = new char[l ] : pSu rname = new charl ll : pFirstname[O ] = pSurname[O] = ' \ 0' :
II Przechowuj e znak null.
Jeżeli
w funkcji ma i n( ) dodamy instrukcje usuwające przydzielaną tam dynamicznie pamięć, program powinien działać bez żadnych komunikatów o wyciekach pamięci . W funkcji ma i nt ), na końcu wewnętrznej pętli for kontrolowanej przez j, należy dodać poniższą instrukcję:
delete[ ] j Name: Dodatkowo na końcu
pętli zewnętrznej,
kontrolowanej przez i , należy
dodać :
delete [] i Name: Wreszcie, nadaj musimy zwolnić pamięć przydzieloną pName po
delete[ ] pName:
pętlach
w funkcji mai n( ):
650
Visual C++ 2005. Od podstaw
Oebugowanie prOgramÓW wC++/CLI
W programowaniu w C++-/CLI życie je st prostsze. W programach pisanych dla CLR nie wyst ę pują żadne problemy zw i ązane z uszkodzon ymi wskaźnikami lub wyciekami pamięci , dzięki czemu problemy debug owania są znacznie mniejsze w porównaniu z natywnym C++-. Punkty wstrzymania i punkty ś ledzen ia w programach CLR ustawia si ę tak sam o ja k w programach pisanych w natywnym C++-. Dla kodu w C++/CLI istnieje specjalna opcja, która powstrzy muje debugger przed przechodzeniem przez kod biblioteczny. Jeżeli z menu Tools wybierzemy Options, pojawi się okno dialogowe . J e żeli w oknie tym wybierzemy Debugging, a n astępnie General, to ukaże się nam okno z opcjami. Zostało ono pokaz ane na rysunku 10.19. Dzi ęki wybr aniu opcji wyróżnion ych na rysunku 10.19 mamy pewność, że debugger przej dzie tylko przez nasze wł asne instrukcje, a kod biblioteczny wykona w normalny sposób.
Uzywanie klas Oebug i Trace Klasy Debug i Traee w przestrzeni nazw Syst em: :Di agnos t i es służą do śledzeni a wykonywania programu dla celów debugowania. Możliwości obu tych klas są identyczne. Różnica m iędzy nimi polega na tym, że funkcje klasy Tr aee są kompilowane takż e w wersji finalnej programu, a funkcje klasy Debug nie. W związku z tym funkcji klasy Debug można używać do debugo wania kodu, a funkcji klasy Tr aee, kiedy chcemy uzyskać informacje ś l edzen ia w finalnej wersji programu dla celów kontroli wydajności lub diagnostycznych oraz konserwacyjnych. Można także decydować , czy kod ś l ed zący ma zostać włączony do programu podczas komp ilacji. Options
--- - ·- l
&1 Envronment III Projscts and sc joto ns IJ)
Soorce Conlrol
t1Jf)<J Generai -
-
-
-
-
-
-
-
-
-
-
-
D
eJ Enable ad-jress-Ievel debugglng D Show disassembly If source is not avallable eJ Enable breakpoint fIItBrs
l
Natlve
Ii Iii
Symbols
. aJ Device Tools HTML Designer
i al Windows Forms Designer
-
--
~
eJ Break ali processes when one process breeks
ifi · Database Tools
Gereral Edlt and CCntlru e m t-In-Time
--'-'-
'0 Ask bebe deletlng all breakpoints
i.tJ Text EditDr &I Debugging
-
D
Br eak when excepMn s cross AppDomalnor managedfnative bourd a
Enable Ihe excepton ass tstant ~"' , JT1\ Jh d j 'lC: ::Jl :t;:;c"- en :,;;,;-;::,; '"'d
(:: J e~ ... epr,cns
11\ IW'W.ląfMElJIM'€G@iGM§j
o Show al! memters for non-user objects in var tebles wndows (V~
eJ Wam if no user code on launch eJ Enableproper ty evauatlon and olher implc lt forc t o n calls eJ Cali ToSlr lngO on objects in variabies windows (CI only)
ID
Enable source server support
D ~';I~;t~~~ :~:~~ '1~;".d'J;;':~;;~~~::~ ~;;~ i~~; '~~;
~
I
eJ Require source files to exactly malI:h Ihe ongn al verson ~trl ~~t ~ i1 n W'~::\iw lP.,t tn. thA Im=:~ryjnw
l I
OK
II
..,
l )j l~ Cancel
Rysunek 10.19 Ze
względu
na fakt, że funkcje i inne s kł adowe klas Debug i Traee są identyczne, ich możliwości na podstawie klasy Debug.
będę opi sywał
Rozdział 10.•
Oebugowanie
651
Generowanie wyników Dane wyjściowe można wygenerować za pomocą funkcji Oebug : :Wri t el i ne( l i Debug: :Wri t e( l , które wysyłająje do miejsca przeznaczenia. Różnica m iędzy tym i funk cjami polega na tym, że pierwsza z nich wysyła dodatkowo znak nowego wiersza, a druga nie. Obie te funkcje wy stępują w czterech przeładowanych wersjach. W przykładach używam funkcji Wr it e( l , ale funkcja Wri tel i net l ma takie same listy par ametrów:
Funkcja
OpiS
Debug: :Wri t e(St ri ngA komunikat ) Debug : :Wri t e(St ri ngA komunikat. St ri ngA kategoria) Debug: :Wri te (Object A war t o ś ć )
Wysyła
komunikat do miejsca przeznaczenia.
Wysyła do miejsca przeznaczenia n a zwę kategorii i komunikat. Nazwa kategorii służy do organizacji w ysyłanych danych. Wysyła łańcuch
zwrócony przez wa rto ść - > ToSt ring () do miejsca
przeznaczenia. Debug : :Wri t e(Obj ect A wa rtość. St ringA kategoria )
Wysyła nazwę
kategorii z ł ańcuchem zwróconym przez
do miejsca przeznaczenia.
wa rtość ->ToS tr i ng()
Funkcje WriteIf () oraz Writel i neI f( l l i ne t l w klasie Debug:
są
warunkowymi wersjami funkcji Write ( l i Write
Funkcja
Opis
Debug : :Wri t e lf (bool warunek . St ri ngA komunika t )
Wysyła komunikat do miejsca przeznaczenia, j eże li warunekjest t rue. W przeciwnym razie żadne dane nie są generowane.
Debug: :Writel f (bool warunek . Str i ng A komunikat . String A kategori a)
jeżeli
Debug : :Wri te lf(bool warunek . Objeet A wartość ) Debug : :Wri telf (bool warunek . Obj eetA wa r to ść . String A kategoria)
Wy syła
do miejsca przeznaczenia nazwę kategorii i komunikat, warunek jest spełniony . W przeciwnym razie żadne dane nie są generowane.
Wysyła łańcuch zwrócony przez wa rt o ś ć - > T o S tri n g () do miejsca przeznaczenia, jeżeli warunek zostanie speł n io ny . W przeciwnymrazie żadne dane nie są generowane.
Wysyła nazwę
kategorii z łańcuchem zwróconym przez do miejscaprzeznaczenia, jeżeli warunek zostanie W przeciwnymrazie żadne dane nie s ą generowane.
wart o ść- > ToSt ri ng()
spełniony .
Jak widać, funkcje WriteIf () i Writ eLin e If () mają dodatkowy parametr typu logicznego (bool) na początku listy parametrów, której używają funkcje Write ( l i Writel i net l. Argument ten decyduje o tym, czy dane wyjściowe zostaną wygenerowane, czy nie. Dane wyj ściowe można także wysłać za w dwóch przeładowanych wersjach:
Funkcja Print (StringA komunikat) Prin t (St ringA fonnat . . .. arrayA args )
pomocą
funkcji debug : : Pr i nt r l , która
występuje
Opis Wysyła
komunikatdo miejsca przeznaczenia ze znakiem nowego wiersza.
Działa w taki sam sposóbjak formatowane dane wyjściowe z funkcją
Conso1e: :Wri t el i neC ). Łańcu ch format okre śl a sposób prezentacji na
wyjściu znajdujących się po nim argumentów.
652
Visual C++ 2005. Od podstaw
Ustawianie mieisca przeznaczenia dla danych wViśclowvch D omy ślnie
komunikaty s ą wys ył an e do okn a wyj ś ciowego ID E, ale możn a to zmieni ć za obiektu nasłuchującego , który jest obiektem kierującym dane debugowania i śle dzenia do jednego lub większej liczby miej sc przeznaczenia. Poniżej znajduje się przykład obiektu nasłuchującego , kierującego dan e debugowania do standardowego strumienia wyj-
pomocą
ś c i o w e g o:
Text Wri t erTraceL i st ener l i st ener Debug : :Li st eners->Add(li st ener ) : A
=
gcnew Text Wri t erTraceLi st ener ( Console : :Out ) ;
Pierwsza instrukcja tworzy obiekt Text Wri t erTra ceLi s tener, który kieruje dane wyjściowe do standardowego strumienia wyj ściowego zwracanego przez właściwość statyczną Out klasy Console. Właściwośc i I n oraz Er ror klasy Console zwracają odp owiednio standardowy strum ień wejś ciowy i strumień błędów . Właśc iwość Li stene rs w klas ie Debug zwraca kolekcję obiektów nasłuchujących danych wyjściowych debugowania, a więc instrukcja ta dodaje obiekt nasłuchujący do kolekcji. Można dodać następne obiekty nasłuchujące w celu skierowania danych wyj ściowych w jeszcze inne miejsce (np. do pliku ).
WCięcia
wdanych wViśclowvch
Wcięcia w danych wyj ściowych debugowania i śledzenia można kontrolować. Jest to szczególnie przyd atne w sytuacjach, gdy funkcje wywoływane są na różnych poziomach. Dzięki zastosowaniu wcięcia danych wyj ściowych na początku wykonywania funkcji oraz usunięciu go przed opuszczeni em funkcji łatwo można zidentyfikować dane wyj ściowe debugowania lub ś ledzen ia . Widoczny jest także poziom zagłębienia wywołania funkcji z rozmiaru wcięcia danych wyj ściowych.
Aby
zwiększyć wcięcie
o jeden (jednostka wcięcia domyślnie odpowiada czterem spacjom ), Indent O należącą do klasy Debug, j ak poniżej:
należy wywołać funkcję statyczną
Debug : : Indent O : Aby
II Zwiększen ie
zmni ejszyć wcięcie
o jeden,
Debug: :Uni ndent ():
wc ięcia
o jeden.
należy wywołać statycznąfunkcję
II Zmniej szenie
wcię cia
Uni ndent ( ):
o jeden.
Bieżący
poziom wcięcia jest przechowywany przez statyczną właściwość Indentlevel klasy Debug. Można za j ej pomocą sprawdzić lub ustawić poziom w cięcia. Na przykład:
Debug: : Indentlevel Powyższa
=
2*Debug: :Indent Level ;
instrukcja podwaja
bieżący
poziom wcięcia dla
następnych
danych debugowania.
Liczba spacj i w pojedynczej jednostce wcięcia przechowywana j est w statycznej właśc iwości Indent Si ze klasy Debug. Można sprawdzić bieżący rozmiar wcięcia i zmi eni ć go. Na przykład:
Con sole : : W r i t eL i ne (LBi e t ą c a jednost ka w c i ęcia = (O j ". Debug: :Indent Si ze) ; Debug: :lndentSi ze = 2: II Ustawianie jednostki wcięcia na dwie spacje .
Rozdziall0.• Debugowanie
653
Kontrolowanie llanych wyiściowyCh Przełączniki śledzenia umożliwi ają włącz ani e
debugowan ia lub
śledzenia. I stnieją
i wyłączanie dowoln ych danych wyj ściowych dwa rodz aje przełączników ś l e dze n ia, z których można
korzystać :
•
Obiekty referencyjnej klasy Boo l eanSwitch umożliwiają włączanie i wyłączanie segmentów danych wyjściowych w za leżnośc i od stanu przełącznik a.
•
Ob ie kty referencyjnej kla sy Tr aceSwi t ch dostarczają bardziej wyrafinowanego mechanizmu kontrolnego, pon ieważ każdy obiekt tej klasy ma cztery właściwości, które odpowiadaj ą czterem poziomom kontrolnym instrukcji wyjś ciowych.
Poniżej znajduje s i ę przykładowy obiekt klasy Boal eanSwitch, jako statyczna skła d ow a klasy:
kontrolujący
dane
wyjś c i owe
publ iC ref class M yCl ass {
privat e : statlc
Boo l e a nSwi tc h ~
errors = gcnew B oo lea n Swit c hlL " Przełą czn i k Er ror", L"kont rol uj e komunikaty bł ęd ów . " ) :
publ t e : void Dolt l ) { II Kod ...
l f (errors->Enabled) Debug: : W r H e L i n e l L" B ł ąd w funkcji Do l tt )" ) : II
Więcej
kodu...
} II Reszta klasy ...
}: Powyż szy kod pokazuje obiekt er ro rs jako statyczną składową klasy M yCl ass . Pierwszy argument przekazywany do konstruktora Boa l eanSwitch to nazwa przełącznika używan ego do inicjalizacji właśc iwości Di spl ayName, a drugi argument ustawia wartość właściwości Descr i pt i on przełącznika . Jest jeszcze jeden konstruktor przyjmujący trzeci argument typu St r i nq" , który ustawia właściwo ść Val ue przełącznika. Właściwość
fa l se . Aby
Enabl ed przełącznika Boal eanSwi t ch je st typu logicznego i domy ślnie ma warto ś ć na true, należy odpowiednio zmienić jej wartość :
ustaw ić ją
errors ->Enabled = true: Funkcja Do l t O w klasie MyClass wysyła komunikat debugowania o padku , gdy przełącznik e r rors jest włączony.
błędzie
tylko w przy-
Klasa referencyjna Tr aceSwi t ch ma dwa kon struktory, których parametry s ą takie same ja k kon struktorów klasy Boa l eanSw i t ch. Obiekt klasy TraceSwit ch tworzymy w n ast ępujący sposób:
654
Visual C++ 2005. Od podslaw TraceSwitch t raceCtrl A
gcnew TraceSwitch(L"Update".
L" Śl edzi
operac je
uaktualniania." ): Pierwszy argument tego konstruktora ustawia wia wartość właściwości Descript i on. Właściwość
Di splayName, a drugi usta-
Level obiektu TraceSwitch jest typu klasowego wyliczeniowego TraceleveL Wła
ściwość tę można ustawiać
wych
wartość właściwości
na jedną z
poniższych wartości
w celu kontroli danych
wyjścio
śledzenia:
Wartość
Opis
Tracelevel : :Off
Brak danych
Tracel evel : :Info
Komunikaty informacyjne,
Tracel evel : :W arning
Ostrzeże,nia
Tracel evel : :Error
Komunikaty o błędach .
Tracel evel : .Verbose
Wszystkie rodzaje komunikatów .
śledzenia . ostrzeżenia
i komunikaty o błędach .
i komunikaty o błędach.
Podana wartość określa rodzaj generowanych danych. Aby otrzymać komunikaty wszystkich rodzajów, należy właściwość ustawić w następujący sposób:
traceCtrl->Level = Tracel evel : :Verbose:
To, czy komunikat
określonego rodzaju powinien być wysyłany przez nasz kod śledzący i debugujący, określamy za pomocą sprawdzenia stanu jednej z czterech właściwości typu logicznego obiektu TraceSwitch:
Właściwość
Opis
TraceVerbose
Zwraca
wartoś ć
t rue, gdy wszystkie rodzaje komunikatów mają być wysyłane.
Tracelnfo
Zwraca
warto ść
t rue, gdy komunikaty informacyjne mają być wysyłane.
TraceWarning
Zwraca
warto ść
t rue, gdy
TraceError
Zwraca
wartość
t rue, gdy komunikaty o błędach
ostrzeżenia mają być wysyłane .
mają być wysyłane .
Ze znaczenia wartości tych właściwości widać, że ustawienie właściwości Level oznacza również ustawienie stanów tych właściwości . Jeżeli na przykład właściwość Level ustawimy na TraceLevel : :Warni nq, to właściwości TraceWarni nq i TraceError zostaną ustawione na true, a TraceVerbose i Trace Info na fa l se. Aby
zdecydować,
czy
wysłać
na
wyjście określony
komunikat, wystarczy
sprawdzić
odpo-
wiednią właściwość:
if (t raceCt r l- >T raceWarning) riteLtnett.To jest ost at nie Debug : :W Powyższy
komunikat zostanie
t raceCtrl ma
wartość
true.
wysłany
ost rz eż eni e!") :
na
wyjście
tylko wtedy, gdy
właściwość
TraceWarning
Rozdział 10.
• Debugowanie
655
Asercie Klasy Oebug() i Assert () zawierają statyczną funkcj ę Asser t ( ), której możliwoś ci podobne są do funkcji Assert () w natywnym C++. Pierwszym argumentem funkcji Debuq: :Assert () jest w artość logiczna lub wyrażenie , które zmusza program do wygenerowania asercji, kiedy argument ma warto ść f al se. Stos wywołań jest pokazany w oknie widocznym na rysunku 10.20.
at TraceTesl,Fu nc O d: \translat irn s\hel ion\ ivor hor1ons vlsUal c++ 200S-"'rzykładY\c:W 10_0 3\CW 10_03\c:w lO _03 . c pp (50)
at Tr aceTestFunB O d :\translations\h ellon\ ivor hor1Dns vlsual c++
2Ó05-..,rzyklady\c:w l O_03\cwlO_03\cwlO_03.cpp(39) at TraceTesl,FunA O d:\trans latlons\he lion\ivo r hOr tons vlsUal c++
2005\przyklady\c:w1O_03\c:wl O_!13\cw lO_03.cpp(213) at <Module>, main(5 tr ing[J args) d:\translatirns\h ehon\r,.or horton s visual c++
2005\przyklady\c:wl O_03\c:w 10_!13\cwl O_03.cpp(l7) . at <Module> .mainCRT5 tar tup5 trArray(5tr h:jll argurrents) f :v-tm\ vctools\c:rCbld\seICx 86\c:rt\sr c\ mcrlE xe.cpp(324)
Na rysunku 10.20 pokazano asercję wygenerowaną przez n astępny przykład. Kiedy program generuje asercję dla stosu wywołań prezentowanych w oknie dialogowym , to pokazuje numery wierszy w kodzie oraz nazwy funkcji , które są w tym momencie wykonyw ane. W tym przypadku, wł ącznie z funkcją mai nt ), wykonywane są cztery funkcje . W przypadku wystąpienia asercj i możliwe s ą trzy kierunki działania . Kliknięcie przycisku Przerwij spowoduje natychmiastowe zamknięcie programu, kliknięcie Ignoruj pozwala programowi kontynuow a ć działan ie, a Ponó w prób ę umo żliwi a wykonanie programu w trybie debugowani a. Dostępne są
trzy
przeładowane
wer sje funkcji Asser t ( ) :
Funkcja
OpiS
Oebug : :Assert ( bool warunek )
Je żeli
warunek ma wartość f al se, pojawi a stos wyw ołań.
się
okno dialo gowe
pokazuj ące bieżący
Debug : :Assert (bool warunek . St ri ng
A
komunikat) Debug : :Asse rt Cbool warunek . St ri ng komunikat , St r i ngA s zczegó ły ) Najłatwiej
kładowego
A
Podobnie j ak pow yżej, ale z komun ikatem w okn ie dialogowym nad informacją o stosie w ywoł ań .
Podobnie jak powyższa wer sja , ale z dodatki em w okni e dial ogowym.
jest to zrozumieć, patrząc na działający przykład. Poniżej znajduje programu, pokazujący kod debugowania i ś l edzen i a w akcj i.
s zczegółów
si ę
kod przy-
656
Visual C++ 2005. Od podstaw
RI1mmiI Slosowanie kodu debugowania i śledzenia
Poniższy program służy tylko jako ćwiczenie niektórych funkcji debugowania i ś ledze nia, które opisałem powyżej. Utwórz nowy projekt konsolowy CLR i w pliku Cw10_03.cpp um ieść nas tępującą tre ś ć:
#i ncl ude "s t dafx. h" using namespa ce Syst em; usi ng namespace Syst em: : Oiagnost i cs: publ i c r ef class Tr aceTest
( publ ic: TraceTest( i nt n) :v alue(n ) { } proper t y Tr acelevel Level
{ VOl d set (T r aceLevel 1eve1) {sw->Leve1 = 1eve1; } Trace l evel get (){ re t ur n sw->Level : }
voi d FunA( )
{ ++val ue; Tr ace: : I ndent O: Tr ace : :Writ eL l n e ( L " P o c z ą t e k f unkcj i FunA. " ) ; if ( sw- >Tr acelnfo) Oebug : :Wr iteLi ne(L "Funkcj a FunA w t rakc le dzi a ła nia , .. U) ; FunB() : Tr ace: :WriteL i net L" K o ń c z en i e fu nkcj i FunA." ) . Tr ace : :Uni ndent ( ) ;
void FunB()
( Tr ace . : I ndent O: Tra ce: :Wr iteL i n e( L "P o c ząte k funk cji FunB." ) ; i f( sw->TraceWa rn ing) Oeb ug:: W riteL i ne( L "O s trzeżen ie w f unkcji FunB.. . FunC( ) ; Tr ace : ,Writ e L i neCL"K oń cz en ie f unkcj i FunB. " ) ; Trace : .Uni ndent ( ) ;
voi d FunC() ( Tr ace: : I ndent O : Tr ace : :Wr iteLi neCL" P o cz ąt e k fu nkcj i FunC." ) ; i f(s w->Tr aceError ) Oebug: : Writ eL ine (L" Bł ą d w fu nkcji FunC.. . " ) ; Debug : :Asser t ( val ue < 4) ; Tr ace : : W r it e L i n e ( L " Ko ń c z e n i e fu nkcj i FunC." ) ;
U )
;
RozdziallO. • Oebugowanie
657
Trace: .Urn ndent O: }
pri vate: int value; static TraceSwit chA sw = gcnew T r a c e Sw l t c h (L " P r z eł ą cz ni k Sledzeni a. wyjS ciowe ś ledz en ia . "l:
L" Kont roluje dane
}:
int ma i n(array<System : :Str ing A> Aargs) i II Skierowani e danych
wyjś cio wych
do wiersza poleceń .
TextWrite rTraceL1st ener A l ist ener Debug : ;Li steners->Add(l iste ner l :
Console: : W r it e Lin e (L " P oc z ą t e k te stu debugowania i ś Iedzenia . . . ") : for each(TraceLevel level m levels ) {
obj ->Level = l eve l : II Ustawianie pozi omu dla komunikatów. Conso le: :Writ eLine(L"\nPoziom ś ledzenta t o {O}" , obj -r-l.evel l : obj ->FunA() : ret urn O' }
Uruchomienie tego programu powoduje pojawienie s ię okna asercji. Możemy w takim przypadku kontynuować bądź przerwa ć działan ie programu lub uruchomić go ponownie w trybie debugowania, wybierając odpowiedni przyci sk w oknie . W zależności od tego, co zrobimy w momencie szego programu jest następujący: P oc z ąt e k
Poziom
test u debugowania i
to Off funkcj i FunA . Poc ząte k funkcji FunB . P o c z ą t ek fun kc j i FunC . Ko ń cze n ie funkcj i FunC. Końc ze n ie funkcJ1 FunB. Ko ńcz en ie funkcj i Fu nA, ś l edz e nia
Po c z ąt e k
PoziomSledzenia to Error Poc zątek funkcj i FunA. Po c z ą t e k funkcj i FunB . P o c z ą t ek funkcj i FunC. Błą d w funk cj i FunC. . . Ko ń c z en i e funkcji FunC. Koń c z e nie funkcj i FunB. K o ń cze nie fun kcj i FunA.
ś le dze nia
.. .
wyświetlenia
asercji, rezultat
działania powyż
658
Visual C++ 2005. 011 podstaw Poziom ś led zen i a to W arning Po c zątek funkcj i FunA. Poc z ąt e k funkcji FunB. Ost r ze że n ie w fu n~ c j i FunB. . . P oc z ąt e k fu n ~c j i FunC. B łą d w funk cj i FunC.. Ko ńcz e ni e funkcji FunC . Ko ń czeni e fun kcj i FunB . Ko ń cz en i e fu n ~ c ji FunA. Poziom ś l ed z en i a t o Verbose Po c z ą t e k funkcj i FunA. Funkcj a FunA w tra kcie d zi ał a n i a .
Poc z ą t e k funkcji FunB. O s t rz eż e n i e w funkc j i FunB. . . P oc zą t e k fun~c j i FunC. B ł ąd w funkcji FunC . . .
Jak to IJziała Klasa Tr aceTest defin iuje trzy funkcje obiektowe: FunA(), FunB( ) i FunC ( ). Każda z tych funkcji zawiera wywołanie funkcji Trace: : Indent( ) zwiększającej wcięcie danych debugowania oraz wywołanie funkcji Debug: :Wr iteLi ne() śledzącej początek i koniec każdej funkcji . Funkcja Tr ace: :Uni ndent () jest wywoływana bezpośrednio przed wyjściem z każdej funkcji w celu przywrócenia wcięcia do poziomu sprzed wywołania funkcji . Klasa Tr aceTest definiuje prywatną składową klasy TraceSwitch o nazwie sw, która kontroluje poziom wyświetlanych danych debugowania. Każda z tych trzech funkcji wywołuje również funkcję Debug: :W rit eLi ne() w celu wydania komunikatu w zależności od poziomu ustawionego w s k ła d ow ej sw. Funkcja FunA( ) zwiększa wartość będącą składową obiektu klasy za każdym razem , gdy jest wywoływana , a funkcja FunC () generuje asercję, jeżeli wartość przekracza 3. W funkcji mai n( ) tworzony jest obiekt TextWri te r Tr acel i ste ner , który kieruje dane debugowan ia i śledzen ia do wiersza poleceń:
TextWri t erTraceL i st ener A l i st ener = gcnew Text Writ erTraceL i st ener ( Console: :Out J: Nast ępnie
do kolekcji obiektów
nasłuchujących w
klasie Debug dodajemy obiekt Li stener:
Debug: :L1 st eners->Add(l i st ener ): Powoduj e to, że dane w yj ś c i o w e debugowania i śledzenia strumienia wyjś ciow ego - Consol e: :Out . Tworzymy tablicę obiektów klasy Tr aceLeve l , które danych wyj ściowych debugowania i śled zeni a :
arr ayA level s
= (
są
kierowane do standardowego
re pre zen tuj ą różne
poziomy kontroli
Tracel evel : :Off. Tracel evel . :Error. Tra cel evel : :W arni ng .TraceLevel : :Verbose) :
Po utworzeniu obiektu klasy TraceLeve l j ego poziom
śledzenia
ustawiany jest w pętli f or each:
Rozdział 10.
f or each (T rac et.evel l evel
l
• Debugowanie
659
n l evel s J
{ obj ->Level ~ l evel ; II Ustawianie p oziomu dla komun ikatów. Con sol e: :Writ eLi ne( L"\ nPozi om ś led ze n i a t o {D} ". obj c- Level ) : obj - >FunA() :
Poziom ustawiany je st poprzez właś ciwoś ć Level obiektu obj. Powoduje to ustawienie wła ś c iwoś c i Level w składowej klasy TraceSwitch sw, która jest używana w funkcjach obiektowych do kontroli danych wyj ści owych . Z danych na
wyj ś ciu
wynika,
że
ustawione
w ci ę cie
ma wpływ na dane generowane przez funk-
cj ę
WriteLi ne( ) zarówno w klasie oebug, jak i Trace. W ida ć także sposób, w j aki poziom ustawiony w s kład ow ej klasy TraceSwi t ch wpływa na dane wyjściow e . Kiedy zmienna skł adowa obiektu klasy TraceTest obj os i ąga wartoś ć 4, funkcja FunC() generuje asercję. Uruchom program kilkakrotnie, aby logowym asercji.
sprawdzić
efekt
kliknięcia każdego
z trzech przyci sków w okni e dia-
Podsumowanie Debu gowanie jest bardzo obszern ym zaga dnieniem. W Visual C++ 200 5 dostępnych j est o wiele więcej narzędzi wspomagających ten proces, ni ż przed stawiłem w tej książc e . Jeżeli udało Ci s i ę dobrze opanować zagadnienia opisane przeze mnie w tym rozdziale, to nie będz ie sz mieć problemów z poszerzeni em tej wiedzy za pom ocą dokum entacji Visual C++ 2005 . Wpisując do wyszukiwarki s łowo " debugowanie" , możn a zna leźć wiele dodatk owych informacji. Poni żej
•
znajduje
s ię
lista
najw ażniejs zych zagadni eń
poruszanych w tym rozdziale :
Funkcj i bibliotecznej asse rt( ) zadeklarowanej w pliku n agłówk owym do sprawdzania warunków logicznych, które zawsze powinny m ieć wartość t rue w programach w natywnym C++ . można używać
•
Symb ol preprocesora _NoEBUGj est automatycznie zdefiniowany w wersji testowej programu w natywn ym C++. Brak jego defini cji w wer sji ostate cznej.
•
Można d odać własny kod debu guj ący dla symbolu _NOE BUG, który zamy ka s i ę pomi ędzy dyrektywami #i fdef oraz #endi f . Kod tak i do łączan y j est tylko do
wersji testowej programu . •
Plik n a główkowy crtdbg.h dostarcza deklaracj e funkcj i um ożliwiający ch debu gowanie operacji na obszarze wolnej pamięci.
•
Odp owiednio u stawiaj ąc zna cznik _crtDbgFl ag, mo żn a włąc zyć automatyczne sprawdzanie programu w celu znalezienia wyc ieków pamięci .
•
W celu sk ierowania w odpowiednie mi ej sce komunikatów z funkcji debuguj ących obszar wolnej pamię ci należy wywołać funkcje _CrtSetReportMode( ) i Jrt SetReportFi l e( ).
•
Operacj a debu gow ania przy użyciu punktów wstr zymani a i punktów w C++/CLI przeb iega w taki sam sposób jak w natywnym C++.
ś ledzen ia
660
Visual C++ 2005. Od podstaw •
Klasy Debug i Assert zdefiniowane w przestrzeni nazw Syst em: :Di agnost i es dostarczają funkcji śledzących wykonywanie programu i generowanie danych debugowania w programach CLR.
•
Statyczna funkcja Assert () w klasach Debug i Traee umożliwia asercje w programach CLR.
Dysponując wiedzą dotyczącą debugowania, możemy jemniczenia: programowania dla systemu Windows!
przejść
do
najwyższ ego
stopnia wta-
11 Zaloienia programowania dla systemu Windows W tym rozdziale poznasz podstawowe koncepcje, które mają zastosowanie w każdym programie dla systemu Windows utworzonym w C++. Zaczniemy od utworzenia bardzo prostego przykładu , korzystającego bezpośrednio z API systemu operacyjnego Windows. To pozwoli Ci zrozumieć, co dzieje się "pod powierzchni ą" aplikacji dla Windowsa . Wiedza ta przyda się, gdy będziesz tworzył aplikacje za pomocą bardziej wyrafinowanych narzędzi dostępnych w Visual C++ 2005. Następnie zobaczysz, co można uzyskać, tworząc program dla Windowsa z wykorzystaniem Microsoft Foundation Classes, bardziej znanych jako MFC. Na koniec, korzystając z biblioteki Windows Forms (formularzy Windows), utworzysz podstawowy program, który będzie uruchamiany w CLR. A zatem po przeczytaniu tego rozdziału będziesz wiedział, na czym polegają te trzy podej ścia do tworzenia aplikacji dla Windowsa. W tym rozdziale nauczysz się: •
Podstawowej struktury okna.
•
API systemu Windows oraz sposobów jego wykorzystania.
•
Komunikatów Windowsa oraz sposobów ich obsługi .
•
Notacji stosowanej w programach dla systemu Windows.
•
Podstawowej struktury programu dla systemu Windows .
•
Jak
•
Microsoft Foundation Classes.
•
Podstawowych elementów programu opartego na MFC.
•
Biblioteki Windows Forms.
•
Podstawowych elementów aplikacji
utworzyć
prosty program, korzystając z API Windows, oraz jak on
korzystającej
z Windows Forms.
działa.
662
Visual C++ 2005. Od podstaw
Podstawy programowania dla systemu Windows W Visual C++ 2005 dostępne
są trzy
sposoby tworzenia interaktywnej aplika cji dla Wind ows a:
•
Korzystani e z AP! Windows. Je st to podstawowy interfejs sys temu operacyj nego Win dows, um o żli wiający komunikację mi ędzy sys temem a ap l i ka cj ą wykonywaną pod j ego ko ntro l ą.
•
Kor zystanie z bibl ioteki Microso ft Foundation C lasses, bard ziej znanej jako MFC. Jest to zes taw klas C++, który obej muje W indows API.
•
Korzystan ie z Windows Forms. Je st to op art y na formul arzach sposób tworzenia aplikacj i uruchamian ych w C LR.
Te trzy spo sob y zo stały wym ienione w kol ejn oś ci od wy magając e g o najwięks zego nakł adu prac y podczas programowania do wy m agaj ącego najmniej. Kor zystanie z Windows APT wymaga napisania całego kodu - mu sisz sa m n ap i s a ć kod dla wszystkich eleme ntów tworzą cych graficzny interfejs u żytkownika (GUT) . Przy tworzeniu aplikacji MF C otrzy muj emy p ewną pomoc dzięki wbud ow anemu GU l, w którym w formularzu wybieramy kontrolki, a nas tęp n i e programujemy jedynie interakcj e z użytkown ik iem ; jednak wc i ąż wyrnaga to napisania sporej ilości kodu. Przy tworzeniu aplikacj i z wyko rzystaniem Windows Forms można utworzyć cały GW, włącznie z głównym okne m aplikacji, graficzn ie gromadząc kontrolki, z których korzysta użytkownik. Nal eży j edyni e umie ś ci ć kontrolki w wybranych przez siebie miejscach formularza okna, a kod , który będzie j e tworzył , zos tan ie au tomatyc znie wyge ne rowany. Korzystanie z Window s Form s jest j ak dotąd najszy bszy m i najprostszym sposobem tworzenia aplika cji, ponieważ il o ść kodu , który n al e ży samo dzielnie napisać, jest zde cydowanie mniejsza ni ż p rzy pozostałych dwóch met odach. Kod dla aplikacji Windows Fom1S kor zy sta rów nież ze wszystkic h zalet wykonywa nia w CLR . Korzystanie. z MFC wy mag a więcej wy siłku ni ż używanie formularzy Wind ows , j edn ak daje w ięk sz ą kontrol ę nad sposo bem, w jaki tworzony je st GUI, a także tworzy program uruch amiany natywnie na Pc. P oniewa ż bezpo średnie korzystanie z Windows API j est naj bardzi ej pracochłonną met od ą two rzenia aplikacj i, nie będ ę jej o m aw i ał s zcz egóło w o . Utw orzysz jednak prostą apli ka cję k o rzy stającą z Wind ows API , ab yś miał m o żli w o ść pozn ania podstaw mechanizmu, z którego ko rzy s taj ą wszy stk ie aplikacje pracujące w sys temie operacyj nym . W tym rozdziale poznasz podstawy ws zystki ch trzech spo sobów tworzen ia aplikacj i dla Windowsa, natomi ast w dalszej częś c i k siążki poznasz s zc zegóły używan i a MfC i W indows Forms. Oczywiści e , w C++ m o żn a równ i eż tw orzy ć aplikacje n i ewyrnagając e systemu operacyjnego Windows i to podej ś cie je st stosowane na przykład w grach komputerowych , gdzie wymagana je st najwięks z a wydaj noś ć grafiki. C hoć jest to bard zo interesuj ące , wł a ś ci w e omówienie tego tematu wymagałob y napisania całej ks iążki, więc nie będę go dalej ro zwij ał. Zanim przejdziemy do przykładów dla tego rozdziału, przypomnę term in ol og ię u ż yw an ą do opisu okna apli kacji . W rozdz iale l. utworzyłeś już program dla systemu Wind ows, nie pi sząc przy tym ani jednego w iersza kodu, więc do omówienia elementów tw orząc y ch okno u żyj ę okn a utworzon ego przez tamt en kod.
Rozdziałl1.
•
Założenia
programowania dla systemu Windows
663
Elementy okna zna sz większo ś ć (j eżeli nie wszystkie) podstawowych elem ent ów inte rfejsu programu dla systemu Windows. Mimo to o mów ię je , aby nasze rozumieni e terminów było t ożsam e . Naj lepszym sposo bem na poznanie elementów okna jest przyjrzen ie s ię je dnemu. Rysunek 11.1 przed stawia opisaną wersj ę okna wyś w i etlanego przez przykł ad z ro zd zi ału l. Z
p e wn o ścią
użytkownika
Uchwyt rozmiaru
Rysunek 11.1 Okno macierzyste apl ikacji MDI
Krawędź
Okno pot omn e ap likacji MDC Obszar klient a okn a po to mnego Ikona ok na potomnego Obszar klient a okna macierzystego Pasek n arz ędzi
Przycisk Zamknij
Pasek men u Ikona na pasku
Przycisk Mak symalizuj
na rzędz i
Przycisk Minimali zuj
Pasek st anu
I
Tekst w pasku tytułu
Tekst w pasku t ytułu okna potomn ego
W rzeczyw i stości ten przykład utw orzył dwa okna. W i ęk s ze okn o, zawi eraj ąc e pasek menu i narzędzi , jest oknem główn ym lub macierzystym, a mniejs ze okno jes t oknem potomnym okna główne go. Okno potomn e m ożna za mknąć poprzez dwukrotne klikn ięc ie ikony paska tytułowego zn aj d uj ącej s i ę w lewym górnym rogu okna potomnego, nie z a my kając przy tym okna głów nego . Natomiast zamkn ięcie okna głó w n ego spowoduje automatyczne zam kn i ęc ie okna potomnego. Dzieje s ię tak dlatego, że okno potomne należy do okna głównego i jest od niego zależne . Jak się przekonasz, gł ówne okn o mo że p o s i adać kilka okien potomnych . Najbardziej podstawowymi elemen tami typowego okna s ą jego obramowanie i pasek tytułu , w którym wyświ e tlana jest nadana nazwa, ikona paska tytułu , poj awi ająca się w lewym rogu paska tytułu, oraz obszar klienta , który jest obszarem w ś rod ku okna, niewykorzystywanym przez pasek t ytułu i obramow anie . Wszystk ie te elementy można bez przeszkód umieścić w program ie dla systemu Windows. Jak się przekonas z, jedyn e, co musisz zrobi ć , to poda ć jak iś tekst dla paska tytułu .
664
Visual C++ 2005. Od podstaw Obramowanie okre śla wymi ary okna , którego rozmi ar może być ustawiony na stałe lub z moż liwo ś cią zmiany. Je żeli obramowanie umożl iwi a zm i an ę rozmiaru, mo żn a tego dokonać poprzez przeciąganie ram ki okn a. Okno może również po siadać uchwyt zmiany rozmiaru, dzięki któremu można zmieniać rozmiar okna , za chowując przy tym jego proporcje - współczyn nik szerokości do wysoko ś ci . Podczas definiowani a okna można określić wyg ląd i zachowanie obramowania. Większość okien będzie również po s iadała w prawym górnym rogu przyciski mak symalizacji , minimalizacji i zamkni ęci a . Pozwalają one na p owięk s zenie okn a do rozmi arów pełnego ekr anu , zmniej szenie do ikony lub na jego zamknięcie. Po klikn ięciu lewym przyciskiem myszy ikony paska tytułu pojawi si ę standardowe menu, zwane menu systemowym lub menu sterowania, umożl iwi ające zmianę lub zam kn i ęc i e okna. Menu sys temowe pojawia s i ę również po klikni ęciu prawym przyciskiem myszy paska tytułowego okna. Choć nie jest to konieczne, umieszczenie ikon y paska tytułu we wszy stkich głównych oknach programu jest dobrym pomysłem. Zam ieszczenie ikon y pask a ty tu ł u pozwala łatwo zam kn ąć program, gdy coś nie działa podcza s debugowania . Ob szar klienta j est częśc i ą okna, w której przeważnie program ma pisać tekst lub grafik ę. W tym celu obszar klienta j est adresowany tak samo jak podwórko , które widziałeś na rysunku 7.1 w rozdziale 7. Lewy górny róg obszaru klienta ma współrzędne (O, O) z wymiarem x zwięk szającym się od lewej do prawej , a y od gó ry do d ołu . Pas ek menu nie j est wymagany, ale prawdopodobnie jest naj częs tszym sposo bem sterowania aplikacją. Każde menu w pasku menu po klikni ęc iu wyświetla li stę rozwijan ą elementów menu. Zawa rto ść menu i wy gląd wielu obiektów wyświetlanych w oknie - takich jak ikony w pasku narzędzi , kursor i wiele innych - są defini owane przez plik zasobów. Poznasz więcej plików zasobów, gdy przejdziemy do tworzenia bardzi ej złożonych programów dla systemu Wind ows. Pasek n arzędzi za wiera zestaw ikon, które przeważnie są alternatywnym sposobem do stępu do najczęś ciej wykorzystywanych opcji menu . Ponieważ za w ie rają one gra fi cz ną w skazówkę co do ich funkcj i, często ułatwiają i przyspi e szają pracę z programem. Zanim przejdę dalej , chciałbym zastrzec jedną rzecz dotyc ząc ą term inologi i. Uży t kow n icy zwykle p ojmuj ą okno jako to, co pojawi a s i ę na ekranie i ma obramowanie, co o czyw iś ci e jest prawdą, jednak to jest tylko jeden rodzaj okna. W systemi e Windows okno jest ogólnym poję c iem obejmującym cały zbiór elementów. W rzeczywi stości w zasadzie każdy wyświe tlany element jest oknem - na przykład okno dialogowe lub przycisk. Odnosząc się do obiektów, będę przew ażn ie stosował terminol ogi ę , która opi suje, czy m one są, czyli na przykład przyciskami , okn ami dialogowymi itp. Mu sisz jednak pamiętać , że są one równie ż oknam i, ponieważ możes z z nimi robi ć wszy stko to, co ze zwykłym okn em na przykład możesz rysowa ć na prz ycisku.
Programy dla Windowsa i system operacyjny Gdy tw orzysz program dla sys temu Windows, jest on podporządkowan y sys temo wi operacyjn emu i to Windows ma k ontrol ę . Program nie musi sobie r adzi ć bezpośredni o ze sp rzętem , bo cała je go zewnętrzna komunikacj a przechodzi przez Windows. Gdy korzystasz z programu dla Windowsa, oddziałujesz gł ó w n i e na system Wind ows, który w Twoim imieniu komun ikuje s i ę z aplik acją. Można pow iedzieć , że program dla Wind ows a jest ogonem, system Wind ows j est psem , a program poru sza s ię jedynie wtedy, gdy W indows mu każe.
Rozdział 11.
• Zalozenia programowania dla sltstemu Windows
665
tak z wielu powodów. Po pierwsze i najważniejsze, program w zasadzie zawsze zasoby komputera z innymi programami, które mogą być uruchomione w tym samym czasie. Windows musi mieć nadrzędną kontrolę, aby zarządzać wsp ółdzieleniem zasobów komputera. Gdyby jedna aplikacja mogła mieć nadrzędną kontrolę w środowisku Window s, programowanie byłoby zdecydowanie bardziej skomplikowane, ponieważ trzeba by zadbać o możliwość działania innych programów i zapobiec utracie informacji przeznaczonych dla innych aplikacji. Drugim powodem, dla którego Windows ma kontrolę , jest fakt, że Windows dostarcza standardowy interfejs użytkownika i musi wymagać przestrzegania tego standardu. Na ekranie można wyśw ietlać informacje jedynie za pomocą narzędzi dostarczanych przez system Windows i tylko po autoryzacji.
Dzieje
się
w sp ółdzieli
Programy sterowane zdarzeniami W rozdziale l . dowiedziałeś się, że program dla systemu Windows jest sterowany zdarzen iami, program dla Windowsa przeważnie czeka, aż coś się zdarzy. Znaczna część kodu wymaganego przez aplikację dla Windowsa służy do przetwarzania zdarzeń, które są spowodowane przez zewnętrzne działania u żytkownika, ale mimo to niektóre działania niezwiązane bezpoś rednio z aplikacją mogą wymagać wykonania części kodu programu. J eżeli na przykład użytkownik przeciągnie aktywne okno innej aplikacji obok Twojego programu i poprzez to odsłoni część obszaru klienta okna Twojej aplikacji , musi ona przerysować tę część okna. więc
Komunikaty Windowsa Zdarzenia w aplikacji dla Windowsa to wystąpienia na przykład takich sytuacji jak kliknięcie myszą przez użytkownika, naciśnięcie klawisza czy osiągnięcie zera przez licznik czasu . System operacyjny Windows umieszcza każde zdarzenie w komunikacie i zamieszcza komunikat w kolejce komunikatów programu, dla którego przeznaczony jest dany komunikat. A zatem komunikat Windowsa jest po prostu zapisem danych dotyczących zdarzenia, a kolejka komunikatów aplikacji jest sekwencją takich komunikatów oczekujących na przetworzenie przez aplikację. Poprzez wysłanie komunikatu system Windows może powiedzieć programowi, że coś musi zostać zrobione, że jakieś informacje są dostępne lub że nastąpiło kliknięcie myszą. Jeżeli program jest poprawnie utworzony, w odpowiedni sposób zareaguje na komunikat. Istnieje wiele rodzajów komunikatów i mogą one pojawiać się bardzo często na przykład podczas przemieszczania myszy kilka razy na sekundę. Program dla systemu Windows musi zawierać funkcję przeznac zoną do obsługi tych komunikatów. Funkcja często nazywana jest WndProc e) lub Wi ndowProc e), jednak nie musi mieć ona jakiejś szczególnej nazwy, ponieważ Windows ma dostęp do funkcji poprzez utworzony przez Ciebie wskaźnik do funkcji. A zatem wysłanie komunikatu do programu sprowadza się do wywołania przez system Windows funkcji określonej przez programistę (przeważnie W i nProc e)) i przesłania programowi wymaganych danych za pośrednictwem argumentów tej funkcji . W funkcji WinProc() musisz na podstawie dostarczonych danych samodzielnie określić typ komunikatu oraz reakcję programu. Na szczęście nie trzeba pisać kodu do obsługi każdego komunikatu. Można wybrać te, które faktycznie interesują nasz program, i obsłużyć je w dowolny sposób , a resztę przesłać z powrotem
666
VislJal C++ 2005. Od podstaw do Windowsa . Komunikat można przesłać z powrotem do Windowsa poprzez wywołanie standardowej funkcji DefWi ndowP rocO systemu Windows, która dostarcza domyślne przetwarzanie komunikatów.
Windows API Całość
komunikacji między każdą aplikacją dla Windowsa a systemem Windows wykorży stuje interfejs programowania zwany Windows API (ang. Application Programming fnter(ace). Składa się on dosłownie z setek funkcji, dostarczanych standardowo z systemem Windows, które umo żliwiają komunikowanie si ę aplikacji z Windowsem i odwrotnie. Windows API był utworzony, gdy głównym językiem programowania b ył C, na długo przed nadejści em C++, i z tego powodu do przesyłania niektórych rodzajów danych między systemem Windows a aplikacją stosowane są struktury, a nie klasy .
Windows API obejmuje wszystkie aspekty komunikacji między Windowsem i aplikacją. API zawiera tak wiele funkcji , bezpośrednie z nich korzystanie może być bardzo trudne - wyzwaniem jest tu nawet samo ich poznanie. Tutaj z pomocą pro gramiście przychodzi Visual C++ 2005 . W tym środowisku funkcje API są ustrukturyzowane w sposób przypominający obiekty. Dzięki niemu można również w prostszy sposób korzystać z interfejsu w C++ z bardziej rozbudowaną domyślną funkcjonalnością. Przybiera to formę Microsoft Foundation Classes - MFC. Ponadto dla aplikacji przeznaczonych dla CLR dostępna jest funkcja Windows Fonns, gdzie cały kod potrzebny do utworzenia GUl jest tworzony automatycznie . Wówczas pozostaje jedynie napisać kod potrzebny do obsługi zdarzeń w wymagany przez aplikację sposób. Aplikacj ę korzystającą z Windows Fonns utworzysz w dalszej części tego rozdziału , natomiast szczegóły korzystania z Windows Forms poznasz w rozdziale 22 . Ponieważ
Visual C++ dostarcza także kreatory aplikacji (ang. application wizard) do tworzenia podstawowych aplikacji różnego rodzaju, włącznie z aplikacjami MFC i Windows Fonns. Kreator aplik acji może wygenerować kompletną, działającą aplikację , zawierającą szablonowy kod potrzebny dla podstawowej aplikacji dla systemu Windows, pozostawiając programiście jedynie dostosowanie go do własnych potrzeb. Przykład z rozdziału l . pokazał, jak wiele funkcjonalno ści można uzyskać dzięki Visual CH, nie wpisując żadnej linijki kodu. Omówię szczegółowo ten temat, gdy przejdziemy do pisania bardziej praktycznych przykładów za pomocą kreatora MFC Application.
Typy danych wsystemie Windows Windows definiuje sporo typów danych wykorzystywanych do określania typów parametrów funkcji oraz typów zwracanych w Windows API. Te swoiste dla systemu Windows typy rozprzestrzeniają się do funkcji zdefiniowanych w MFC. Każdy z tych typów windowsowskich będzie miał odwzorowanie w którymś z typów C++, jednak ponieważ mapowanie typów Windows i CH może się zmienić, zawsze gdy potrzeba, powinien eś stosować typy Windows. Na przykład w przeszłości windowsowski typ WORD był definiowany w jednej wersji Windowsajako typ unsigned shor t , a w innej wersji jako typ unsi gned i nt. Na maszynach szes-
Rozdział 11.• Załoienia programowania
nastobitowych te typy
jednak na maszyna ch 32-b itowych s ą zdecy dowanie z typów C++ zamiast z typów systemu Wind ow s m ó gł mieć
problemy.
Kompl etn ą li stę
znajduje
s ię
typów danych w Wind owsi e znajdziesz w dokum entacji, natomi ast kilka najc zęś ciej spotykanyc h.
poni żej
BOOLlub BOO LEAN
Zm ienna logiczna przyjmując a wart ości TR UE lub FA LSE . Z a uw a ż , ż e nie j est to ten sam typ co boa l w C++, który m ó gł mi e ć wartość t rue lub fal se.
iJYTE
Bajt
CHAR
Znak
m·JORD
32-bitowa liczba
HAND LE
Uchwyt obiektu, 32-bitowa liczba
HBRUSH
Uchw yt
HCURSOR
Uchwyt kursora .
HOC
667
są r ównow a żn e,
ró żne , wi ęc ktoś korzystaj ący poważn e
dla systemu Windows
oś m iobi towy o ś m i obito wy
cał k owita
pędzla, pęd z e l
Uchwyt kon tekstu na okn ie.
bez znaku , co odp owi ada typowi un signed l ong w C++. c ałko wi ta określ ająca lok ali z ację
obiektu w pa mięci .
wy korzystywany do w ypełnieni a obs zaru ko lorem.
urz ąd z eni a
-
kontekst
urz ądzen ia jest
ob iektem, który pozwala
ry s ować
HIN STAN CE
Uchwyt instancji .
LPA RAM
Para metr komuni katu .
LPCSl R
Wsk a źn ik
do
LP HAND LE
Wska źni k
do uch wytu .
LRESULT
Wa rt o ś ć
vJO RD
16-bitowa liczba
s t ałego ł ańc u ch a
(ze znakie m)
b ęd ąc a
ca ł kow i ta
S-bitowy ch znaków.
wynik iem przetworzenia komuni katu .
bez znaku, co odpo wiad a typo wi uns 1 gned short w C++.
I
Wszelkie inne typy danych Windowsa wy stępujące w przykładach będę omaw iał na bieżąc o . Wszystkie typy wykorzystywane prze z system Windows oraz prototypy funk cji Windows API s ą przechowywan e w pliku nagłówkowym windows .h, więc musisz go dołączyć przy tworzeniu podstawowego programu dla Wind owsa.
Notacja wprogramach dla systemu Windows W wielu program ach dla Windowsa nazwy zm iennych m aj ą prefiks o kreś l ający, jakiego rodzaju warto ś ć przech owuje zmienna i jak jest ona wykor zystywana. Istnieje sporo takich prefiksów i często s ą stosowane ich kombinacje. Na przykł ad prefiks l pf n okre śl a daleki wskaźnik do funkcji (ang. tong pointer to a functiony. Przykładowe prefiksy, z którymi możes z si ę zetkn ąć są wymieni one w tabeli na następnej stronie. Te prefiksy tworzą tak zwaną notację węgierską . Zo stała ona wprow adzona, aby zminimal izować ryzyko błędneg o użyc i a zmiennej przez zinterpr etowanie jej w inny sposób, ni ż była ona zdefiniowana, lub niez godnie z jej przeznac zeniem . Tego typu błąd można b yło łatwo popełni ć w języ ku C, poprzedn iku CH . W C++, dzięki jego lepszej kontroli typów , nie trzeba
668
Visual C++ 2005. Od podstaw
b
zmienna logiczna typu BOOL,ekwiwalent i nt
by
typ unsi gned cher (skrót od angielskiego byte)
c
typ char
dw
typ DWORD, czyli unsi gned l ong
fn
funkcja (ang.fimction )
h
uchwyt (ang. handle), używan y do odwoływania
si ę
do
c zegoś
typ i nt typ l ong lp
daleki wskaźnik (ang. fong p ointer)
n
typ i nt
p
w sk a źnik
(ang. pointer)
łańcuch
(ang. string)
sz
ł ańcu ch
ASCII
w
typ WORD , czyli uns igned short wkładać tyle wysiłku w sposób zapisu, aby uniknąć tego typu błędów . Kompilator zaws ze oznaczy błąd niespójności typów w programie, więc wiele błędów , które dotykały programy nap isane w C, nie może pojawić się w C++.
Z drugiej strony notacja węgierska może pomóc z ro zu m i e ć kod programu, w szczególności gdy mamy do czynienia z dużą liczbą zmiennych różnego typu, które są argumentami funkcji Windows API. Ponieważ programy dla Windowsa są wciąż pisane w języku C i oczywiście dlatego , że parametry dla funkcj i Windows API są wciąż definiowane z użyciem notacji wę gierskiej, ta metoda jest wci ąż szeroko stosowana. Wi ęcej informacji na temat notacji węgier skiej znajd ziesz na stronie: http://web.umr.edu/- cpp/common /hungarian.html. Możesz sam zdecydować o stosowaniu notacji węgi er skiej, ponieważ nie jest ona obowiąz kowa . Możesz w ogóle z niej nie korzystać , jednak gdy s ię z nią dobrze zapoznasz, będzie Ci łatwiej zrozumie ć , czym są argumenty funkcji Windows API. Jest tu jednak m ałe zastrzeżenie. Wraz z rozwojem systemu Windows typy argumentów niektórych funkcji Windows API zostały nieco zmienione, natomiast nazwy zmiennych pozostały takie same. W konsekwencji prefik s może niedokładnie określać typ zmiennej .
Struktura programu dla systemu Windows W celu utworzenia małego programu korzystającego jedynie z Windows API napiszesz dwie funkcje. Będ zie to funkcja WinMain( l, od której zaczyna się wykonywanie programu i przeprowadzana jest jego podstawowa inicjalizacja, oraz funkcja WindowProc( i .wywoływan a przez system Windows w celu przetworzenia komunikatów dla aplikacji. Część Wi ndowProc() programu jest przeważnie większa, ponieważ to tutaj znajduj e s i ę większość kodu specyficznego dla aplikacji , reagującego na komunikaty powodowane działalnością użytkownika.
Rozdział 11.
•
Założenia programowania dla
systemu Windows
669
Pomimo że te dwie funkcje tworzą cały program, nie są one bezpo średnio połączone . Wi nMai n( l nie wywołuje Wi ndowProc ( l . Robi to system Windows. W rzeczywistości Windows wywołuj e takż e Wi nMai n( l. Przedstawia to rysun ek 11.2.
Rysunek 11.2
Windows
Windows API
6 ~----
.8"
fi----
~
---- E I I I I
,
·1[
Finrsh
II
Cancel
opcji w tej kategorii:
Opcja
Opis
Dialog based
Oknem aplikacji jest okno dialogowe, a nie okno ramek.
Mullipie top-level documents
Dokumenty są wyświetlane w oknach potomnych pulpitu, a nie jako okna potomne aplikacji , jak to ma miejsce w aplikacj i MDI.
Document/View architecture
Ta opcja jest domyślnie włączona, więc kod ob s łu g uj ąc y architekturę dokument-widok zostaj e wbudowan y. Gdy opcja ta jest nieaktywna, obsługa tej architektury nie jest dostarczana, więc możesz samodzielnie zadecydować , co zaimplementować.
support
Resource language
Lista rozwijana zawiera wybór języków dla zasobów takich jak menu i łańcuchy tekstowe w aplikacji .
Use Unieode libraries
Obsługa Unieode j est dostarczana przez biblioteki MFC w wersji Unicode . Jeżeli chcesz z nich korzystać, musisz zaznaczyć tę opcję .
Powinieneś wyłączyć opcję
Use Unieode libraries. Jeżeli jest ona aktywna, aplikacja spodziewa w formacie Unicode, a pliki są zachowywane jako znaki Unicode, wskutek czego nie są one możliwe do odczytania w programach spodziewających się tekstu ASCII. się wejścia
708
Visual C++ 2005. Od podstaw Możesz także wybrać między stylem projektu Windows Exp/orer i MFC standard. Pierwszy z nich implementuje okno aplikacji podzielone na dwie części - lewa wyświetla dane w formie drzewa, a prawa wyświetla zwykły tekst. Możesz także wybrać
sposób, w jaki kod bibliotek MFC jest wykorzystywany w programie. jako biblioteki współdzielone DLL (ang. Dynamie Link Library biblioteka dołączalna dynamiczni e), co oznacza, że program łączy się z ich procedurami, gdy są potrzebne. Dzięki temu rozmiar pliku wykonywalnego się zmniejsza, ale wymaga, aby biblioteka MFC DLL znajdowała się na komputerze, na którym program jest uruchamiany. Łączna wielkość obydwu modułów (pliku .exe aplikacji i MFC .dlł) może być większa niż w sytuacji, gdyby biblioteka została połączona statycznie. Jeżeli wybierzesz łączenie statyczne, procedury biblioteki MFC podczas kompilacji zostają włączone do modułu wykonywalnego programu. Aplikacje z łączeniem statycznym działają nieco szybciej niż te z łączeniem dynamicznym, więc należy tu wybrać rozwiązanie kompromisowe między szybkością wykonywania i użyciem pamięci . Jeżeli pozostawisz wybraną domyślną opcję, czyli korzystanie z biblioteki współdzielonej, kilka programów działających jednocześnie i korzystających z tej samej biblioteki będzie mogło współużytkować jedną kopię biblioteki w pamięci. Domyślnie są one używane
W kategorii Doeument Template Strings mamy możliwość wpisania rozszerzenia plików tworzonych przez program. Dobrym wyborem dla naszego przykładu jest rozszerzenie .t xt . W tej części w polu Filter Name można podać nazwę filtru, który zostanie użyty w oknach dialogowych Open (otwórz) i Save as (zapisz jako) do wyświetlania jedynie plików z rozszerzeniem obsługiwanym przez aplikację. Kategoria User Interface Features kreatora aplikacji daje (patrz tabela na następnej stronie).
dostęp
do kolejnych opcji programu
W kategorii Advaneed Features znajduje się kilka opcji, na które należy zwrócić uwagę. Jedną z nich jest wybrana domyślnie Printing and print preview, a inną Context-sensitive he/p, którą należy uaktywnić. Opcja Printing and print preview dodaje do menu File (plik) standardowe elementy Page Setup (ustawienia strony), Print Preview (podgląd wydruku) oraz Print (drukuj), a kreator aplikacji umieszcza kod obsługujący te funkcje . Uaktywnienie opcji Contextsensitive he/p dodaje podstawową obsługę pomocy zależnej od kontekstu. Jeżeli chcesz korzystać z tej funkcji, musisz dodać odpowiednią treść do plików pomocy. Po wybraniu kategorii Generated Classes ukaże się lista klas, które kreator aplikacji wygeneruje w kodzie aplikacji, co obrazuje rysunek 12.7. Możesz wybrać dowolną klasę, klikając ją,
a w części poniżej ukaże się jej nazwa, nazwa pliku nagłówkowego, w którym będzie przechowywana jej definicja, używana klasa bazowa oraz nazwa pliku zawierającego implementację funkcji składowych klasy. Definicja klasy jest zawsze przechowywana w pliku z rozszerzeniem .h, a kod źródłowy funkcji składowych zawsze znajduje się w plikach .cpp. W przypadku klasy CTextEditorOo c możemy zmienić wszystko oprócz klasy bazowej, natomiast gdy wybierzesz klasę CTextEdi torApp,jedyne, co będzie można zmienić, to nazwa klasy. Spróbuj wybrać inne klasy. Dla klasy CMai nFrame można zmienić wszystko poza klasą bazową, a dla CText Ed itorVie w- pokazanej na rysunku 12.7 - można również zmienić klasę bazową, Rozwiń listę innych klas, których można użyć jako klasy bazowej . Lista ta jest przedstawiona na rysunku 12.7. Możliwości w klasie widoku zależą od wybranej klasy bazowej .
Rozdział 12.
Opcja ThickFrame
• Programowanie dla sYStemu Windows zwykorzystaniem MFC
709
Opis Umożliwia zmianę
Opcja tajest
rozmiaru okna poprzez
przeciąganie jego
obramowania.
domyślnie aktywna.
Minimize box
Ta opcja jest również domyślnie aktywna i umieszcza przycisk minimalizacji w prawym górnym rogu okna .
Maximiz e box
Ta opcja jest również domyślnie aktywna i umieszcza przycisk maksymalizacji w prawym górnym rogu okna .
Minimized
Po wybraniu tej opcji aplikacja zostaje uruchomiona ze zminimalizowanym oknem .
Maximized
Po wybraniu tej opcji aplikacja zostaje uruchomiona ze zmaksymalizowanym oknem .
lnitial status bar
Opcja ta włącza pasek stanu umieszczony na dole okna aplikacji. W pasku tym znajdują się wskaźniki aktywności CAPS LOCK, NUM LOCK i SCROLL LOCK, a także wiersz komunikatów, w którym wyświetlane są łańcuchy podpowiedzi dla opcji menu i przycisków paska narzęd zi.
Sp/iI window
Ta opcja umieszcza pasek
Standard docking bar
Ta opcja dodaje do okna aplikacji pasek narzędzi , w którym znajduje s ię standardowy zestaw przycisków stanowiących odpowiedniki standardowych elementów menu. Pasek narzędzi jest umieszczany domyślnie. Pasek dokowalny może być przeciągany do bocznej lub dolnej części okna aplikacji, więc można go umieścić w najbardziej dogodnym miejscu. W rozdziale 13. dowiesz się, jak dodawać przyciski do paska nar zędzi .
Browser style toolbar
Ta opcja dodaje do okna aplikacji pasek Internet Explorer.
Rysunek 12.7
podziału
w każdym z głównych widoków aplikacji .
narzędzi
podobny do tego z programu
MFC AI'plicalion Wiza. d - Textfditor -
-
L1Jf:EJ
Generated Classes
Ov er vłe w
Application Type Compound OOC:lJTlert 5upport Document Template Strings
§enerated dasses: r-
.il CTe xtEditorApp
CTextEditorOoc
,l CMainframe
O_taba,e Support User Interface Features Advanced Features
Generat ed Classes
q~sname :
.h fll!:.,
!CTextEditorView
Bgseclass: J C Vie w
i Te xt Ed ito rVie w .h
....
~-CFormV iew
C Hl m lEdilVie w CHlmlView
CListV iew C RichEditView
C S cro llVle w CTreeview CV iew
Iv
I
. cp~ file ,..,'::-:-.ITe xtEd ilo rVie w .cp p
--,
710
Visnal C++ 2005. Od podstaw
Klasa bazowa CEdit Vi ew
Możliwości klasy widoku U m o ż li w i a prostą wielowierszową ed ycję
tekstu, a
także
funkcj e wyszukiwania,
zamieniania oraz drukowania. CFarmVi ew
Dostarcza widok b ędąc y formularzem. Formularz jest oknem dialogowym, które może zawierać kontrolki dla wyświetlania danych oraz wprowadzania danych przez użytkownika. Jest to w zasadzie taka sama funkcjonalno ść jak w przypadku aplikacji Windows Form s dla CLR, które po znasz w rozdziale 21 .
CHtml Ed itV i ew
Ta klasa rozszerza klasę CHt ml Vi ewi dodaj e
CH t ml Vie w
Dostarcza widok , w którym
CList View
Umożliwia
CRie hEdi t View
Um ożli wia wyświ etl ani e
CSera11Vi ew
Dostarcza widok, który automatycznie dodaje pa ski przewij ani a, je ż eli dane tego wymagają.
CTr eeVi ew
U m ożl i w i a
CView
Dostarcza podstawowych
możliw ość
można przegl ądać
edytowan ia stron HTML.
stron y WWW ora z lokalne pliki HTML.
korzystanie z architektury dokument-widok z kontrolkami list. i edycję tekstu sformatowanego (RTF). wyświetlan e
korzystanie z architektury dokument-widok z kontrolkami drzewa. możl iwości pr z e gl ądania
dokumentu.
Poni eważ nasza aplikacja nosi nazwę Text Editor, czyli edytor tekstu, wybierz CEdit Vi ew, aby automatycznie uzyskać podstawowe możliwości edycji tekstu .
Kliknij przycisk Finish, aby kreator aplikacji MFC utworzył pliki dla w programu bazowego, korzystając z właśnie wybranych opcji .
pełni
funkcjonalnego
Wynik działania MFC Application Wizard Wszystkie pliki utworzone przez kreator aplikacji są przechowywane w katalogu projektu TextEditor, który jest podkatalogiem katalogu rozwiązania o tej samej nazwie. W katalogu res, będącym podkatalogiem katalogu projektu, znajdują się pliki zasobów. W IDE można na kilka sposobów przeglądać informacje zwi ąza n e z projektem (patrz tabela na następnej stronie). Jeżeli
w panelu Solution Explorer klikniesz prawym przyciskiem myszy TextEdi tor, a nawybierzesz Properties z menu kontekstowego, wyświetlone zostanie okno właściwo takie jak na rysunku 12.8.
s tępn i e ści ,
Lewa część teg o okna zawiera grup y właściwości , których zawartość będz ie wyświetlana w prawej czę ści okna. W tej chw ili wy świetlana jest grupa General (ogólne). Wartość wła ściwości można zmieniać w prawej części okna, klik ając ją i wybierając nową wartość z listy rozwijanej znajdującej się po prawej stronie nazwy właściwości lub w niektórych przypadkach samodzielnie wpisując wartości.
Rozdzial12.• Programowanie dla systemu Windows zwykorzystaniem MFC Zakładka
711
Zawartość
lub panel
Solution Explorer
Tu wyświetlane sąp liki wchodzące w sk ład projektu. Są one um ieszczone w wirtualnych katalogach o nazwach Header Fi/es (pliki nagłówkowe) , Resource Fi/es (pliki zasobów) i Sourc e Fi/es (pliki źró d łow e).
Class View
W Class View wyśw iet la ne s ą klasy i ich składowe wc hodzące w skł ad proj ektu. Znaj dują s ię tu ró wn i eż wszys tkie zdefiniowa ne byty globalne . Klasy wyświetla ne są w górnym panelu, a w dolnym wyśw ie tla ne są składowe klasy wybranej w górnym panelu. Klikn i ę ci e prawym przyc iskiem myszy elementu zn aj d uj ące go się w Class View spowoduj e wyśw ietlen ie menu, za pom o cą którego m ożna przej rzeć defi nicję danego elementu lub odwolania do niego.
Resource View
Tu wy świ etl a ne są zaso by, tak ie ja k elemen ty menu i przyciski paska narzę dz i, wykorzystywane w proj ekcie. K liknięcie zasobu prawym przyciskiem myszy spowoduje wyśw i e tleni e menu um ożli wi aj ące go edycj ę istniej ącego zasobu lub dodanie nowego.
Property Pages
Tu w y św i etl a n e są moż liwe do utworzen ia we rsje proj ektu. Wersj a tes towa (ang. Debug ) zawiera dodatkowe udogodnienia uła tw i aj ące debugowanie kodu . Skompilowanie wersji ostatecznej (ang. Release) skutkuje utworzeniem mniej szego pliku wykonywalnego. Wersję tę tworzymy po przeprowadze niu wszystkich testów. Po k liknięciu prawym przyciskiem myszy wersj i (Debug albo Release) zostanie wyśw ietlone menu kontekstowe, za pomocą którego m ożesz dodać arkusz właśc iw ośc i lub wyśw i e tli ć bi eżące właśc i wo ści danej wersji. Arkusz wła ściw ośc i umożliwia ustawieni e opcji kompil atora ikonsolidatora.
Te xtf duur
Plopell y
ConfigU"ation:
Pages
,,1
I
Active(Debuo)
Pletf crm:
!iI COtMlon Properties
@MMOI'
.Gener.1 Oebugging
i±I M an~est T001 ltl ge soc rces @ XML Docereot Generator !ł1. Browse Inform6tion
No Use Unicode Character set No Common Languege Runtime support No Whole Program Opt imization
Character Set Comman Language Runtime SUPP Oł"t Whole Program Optimizetion
D\JtpUt OirecŁory
Specjfiesa r ełative path to tbe ouQ:n,t fle drector y; cen indude environmentvariabIes.
I Rysunek 12.8
,
$(Conf igurationName )
OK
II
Aruluj
= 11 Z.'j5!:~JI
J
712
Visual C++ 2005. Od podstaw
Przeglądanie
IJlików proieklu
w panelu Solution Explorer rozwiniesz listę poprzez kliknięcie znaku + przy nazwie TextEditor, a następnie klikniesz ten znak przy folderach Source Fi/es (pliki źródłowe) , Header Fi/es (pliki nagłówkowe) i Resource Fi/es (pliki zasobów), ujrzysz wszystkie pliki wchodzące w skład projektu. Zostało to przedstawione na rysunku 12.9. Jeżeli
TextEd,tor .h Te xtłidit n rfr o c .h TextEditorYiew .h
Resource Rles
[00
TextEdltor.k o
gJ
TextEd'to r.r c2
Jii TextEdltor.rc
-
~ TexłEditorOoc. ico
tS Toolb_r .bmp
8
t2:4 Source F~es
e
, '::3 MainFrm,cpp stdefx.cpc '::3 TextEditor .cpp o
·
~ TextEdit orDoc.cpp
g 111
TextEditorView ,cpp
ReadMe.t xt
,&!SOIutlon Explorer ~$ CI ess Y'ow
Na rysunku 12.9 panel przedstawiony jest jako pływające okno, aby była widoczna cała lista plików. Wszystkie zwinięte panele można przekształcić w pływające okna poprzez kliknięcie ikony strzałki skierowanej w dół, która znajduje się w górnej części panelu, i wybranie z listy żądanej pozycji. W skład naszego projektu wchodzi 17 plików. Zawartość każdego pliku można wyświetlić , klikając dwukrotnie jego nazwę. Zawartość wybranego pliku jest wyświetlana w oknie edycji. Otwórz w ten sposób plik Readme.txt . Jak się przekonasz, zawiera on krótki opis zawartości wszystkich plików wchodzących w skład projektu. Nie będę powtarzał tych opisów, ponieważ ich podsumowanie w pliku Readm e.txt jest bardzo dobre.
Przeglądanie klas Dostęp
do projektu za pomocą panelu Class View jest często zdecydowanie bardziej wygodny za pomocą Solution Explorer, ponieważ klasy są podstawą organizacji aplikacj i. Gdy chcemy przejrzeć kod, przeważnie chodzi nam o definicję klasy lub implementację funkcji skła dowej. Z panelu Class View mamy bezpośredni dostęp do każdej z nich. Czasem jednak przyniż
Rozdział 12.
daje
się
• Programowanie dla systemu Windows zwykorzystaniem MFC
713
Solution Explorer. Gdy chcemy sprawdzić dyrektywy #i ncl ude w pliku .cpp, za poExplorer możemy bezpośrednio otworzyć interesujący nas plik .
mocąSolution
W panelu Classes możesz rozwinąć element TextEditor, aby zobaczyć klasy zdefiniowane dla aplikacji. Kliknięcie nazwy klasy powoduje wyświetlenie wszystkich jej składowych w dolnej części panelu. W panelu Class View, przedstawionym na rysunku 12.10, została wybrana klasa CTextEdi t orDoc.
Rysunek 12.10
®
(lass Yiew
... l ~ .
u l.
;. . C ~.
<Seerch>
El ~ TeKtEditor ffi = Maps .. Global FuncŁions and VariabJes
. .~ MatrosandConstants [jJ
"l$ CAboutDlg
I±I ~ CMainFrame
$ ~
CTextEditorApp
Iii ~
CTextEditorView
l±l 4tNAi iiild@
..
Finish
)
I
Cancel
Dodatkową klasą jest CChi l dFr ame, która jest wyprowadzana z klasy MFC CMD I Ch i l dWnd. Klasa ta dostarcza okno ramowe dla widoku dokumentu , który pojawia się wewnątrz okna aplikacji utworzonego przez obiekt CMai nFrame. W aplikacji typu sm istnieje jeden dokument z jednym widokiem, więc widok jest wyświetlany w obszarze klienta głównego okna ramowego. W aplikacji typu Mm może zostać otwartych wiele dokumentów, a każdy z nich może mieć wiele widoków. Aby było to możliwe, każdy widok dokumentu w programie posiada wła sne potomne okno ramowe utworzone przez obiekt klasy CCh i l dF rame. Jak widziałeś wcześniej, widok jest wyświetlany w osobnym oknie, które dokładnie wypełnia obszar klienta okna ramowego.
Uruchamianie programu Program możesz utworzyć tak samo jak w poprzednim przykładzie. Gdy go uruchomisz, zostanie wyświetlone okno aplikacji przedstawione na rysunku 12.13. Oprócz głównego okna aplikacji mamy osobne okno dokumentu oznaczone jako Sketcherl. Sketcherl jest domyślną nazwą początkowego dokumentu, a po zapisaniu ma rozszerzenie .ske. Możesz tworzyć dodatkowe widoki dokumentu, wybierając z menu Window/New Windowo Możesz także tworzyć nowe dokumenty poprzez wybranie z menu Fi/e/New. W takiej sytuacji w aplikacji będą dwa aktywne dokumenty. Sytuacja, w której mamy dwa aktywne dokumenty z dwoma widokami każdy, została przedstawiona na rysunku 12.14. Na razie w aplikacji nie można tworzyć żadnych danych, ponieważ nie napisaliśmy jeszcze żadnego kodu, który by to umożliwiał. Natomiast cały kod tworzący dokumenty i widoki został dołączony przez kreator aplikacji.
Rozdzial12.• Programowanie dla syStemu Windows zwykorzystaniem MFC
Rysunek 12.13 Dwa widoki dokumen t u Sketcherl To j est tworzon e jako egzemplarz CMainFrame Dwa w idoki dokume ntu Sketcher2
Menu i pasek narzędz i
są
t worzone przez obiekt CMainFrame, ale są wspó /dzie lone
To jest okno w obs zarze klienta okna CChi ld Frame. Jest ono t wo rzone przez obiekt CSketcherView i bę dz ie zawierało dan e dokume ntu
-T-
Rysunek 12.14
123
724
lisual C++ 2005. Od podstaw
Podsumowanie Ten rozdział dotyczył głównie sposobu działania kreatora aplikacji MFC. Poznałeś podstawowe komponenty programów MFC , generowane przez ten kreator zarówno dla aplikacji typu sm, jak i MDI. Wszystkie nasze przykładowe aplikacje MFC zostały utworzone za pomocą kreatora aplikacji MFC, więc należy pamiętać ogólną strukturę i szerokie powiązania klas. Liczba szczegółów może być nieco przytłaczająca, jednak wraz z rozwijaniem aplikacji w kolejnych rozdziałach wszystko stanie się bardziej zrozumiałe. Poniżej
przedstawiam naj istotniejsze zagadnienia omówione w tym rozdziale:
•
Kreator aplikacji MFC generuje kompletny, działający szkielet aplikacji dla Windowsa, który można następnie dostosowywać do własnych potrzeb.
•
Kreator aplikacji może utworzyć aplikacje z interfejsemjednodokumentowym (Sfrl), które pracują z pojedynczym dokumentem i pojedynczym widokiem, lub może utworzyć aplikacje z interfejsem wielodokumentowym (Mm), które mogą obsłużyć jednocześnie wiele dokumentów w wielu widokach.
•
Cztery najważniejsze klasy w aplikacji sm, które •
klasa aplikacji,
•
klasa okna ramowego,
•
klasa dokumentu,
•
klasa widoku.
są wyprowadzane
z klas MFC, to:
•
Program może posiadać tylko jeden obiekt aplikacji. Jest on definiowany automatycznie w globalnym zakresie przez kreator aplikacji .
•
Obiekt klasy dokumentu przechowuje dane specyficzne dla aplikacji, a obiekt klasy widoku wyświetla zawartość obiektu dokumentu.
•
Obiekt klasy szablonu dokumentu jest używany do połączenia dokumentu, widoku i okna. W aplikacji sm służy do tego klasa CS ingleDocTemplate, a w aplikacji MDI wykorzystywana jest klasa CDocTemp l ate. Obydwie te klasy pochodzą z MFC i przeważnie nie ma potrzeby wyprowadzania klas specyficznych dla aplikacji.
Ćwiczenia utworzenia ćwiczeń związanych z programowaniem, jedynie podstawowe sposoby tworzenia aplikacji MFC. Do poniż szych ćwiczeń nie ma rozwiązań, ponieważ odpowiedź albo pojawi się na ekranie, albo będziesz mógł ją znaleźć w treści tego rozdziału.
W tym rozdziale nie ma
możliwości
ponieważ przedstawiał on
Natomiast pod adresem: http://helion,pl/ksiaz/d/vcpppo.htm znajdziesz kod z tej książki oraz rozwiązania innych ćwiczeń.
kładów
źródłowy
do przy-
Rozdział 12.•
Programowanie dla slslemu Windows z wlkorzyslaniem MfC
725
1. Jaki jest związek miedzy widok iem a dokumentem?
2. Do czego
a.
s łu ży
szablon dokumentu w programie MFC dla Windowsa?
Dlaczego należy być ostrożnym i dokładnie przed skorzystaniem z kreatora aplikacj i?
zaplanować strukturę
lo Utwórz prosty edytor tekstu. Skompiluj zarówno wersję a na stępnie
sprawdź
testową,
pro gramu
jak i o stateczną,
typ i rozmiar utworzonych plików.
I. Kilka razy utwórz edytor tekstowy, za każdym razem zmien iając styl okna w zakł adc e Advanced Options kreatora aplikacji .
726
Visual C++ 2005. Od podstaw
13 Praca zmenu i paskami narzędzi
Z poprzedniego rozdziału dowiedziałe ś się, z czego składa się podstawowa aplikacja wygenerowana przez kreator aplikacji MFC oraz jak te części s ą ze s o bą powiązane. W tym rozdzial e rozpoczniemy rozbudowę naszej podstawowej aplikacji MOI o nazwie Sketcher w celu stworzenia z niej w pełni funkcjonującego programu . Zaczniem y od widoku. Pierw szy krok w tym procesie polega na zrozumieniu sposobu, w jaki definiowane są menu w Visual C++ 2005 oraz jak tworzone są funkcje obsługujące charakterystyczne dla aplikacji elementy menu, które dodamy do programu . Dowi esz się też, jak dodać do aplikacji paski narzędzi. Po przeczytaniu tego rozdziału będziesz wiedz iał : obsługuje
•
W jaki sposób program oparty na MF C
•
O zasobach menu i sposobach ich tworzenia oraz modyfikacji.
•
O
•
Jak utworzyć funkcję obsługującą komunikat wygenerowany po wybraniu elementu menu .
•
Jak
dodać
procedury
•
Jak
dodać
przyciski paska narzędzi i powiąz a ć j e z
właściwościach
komunikaty.
menu i sposobach ich tworzenia oraz modyfikacji.
obsługi aktualizuj ące właściwości menu. istniejąc ymi
elementami menu.
Komunikacja zsystemem Windows Jak wiesz z rozdziału 11., Windows komunikuje s i ę z programem, wysyłaj ąc do niego komunikaty. Większość pracy związanej z obsłu gą komun ikatów wykonuje MFC ; nie musisz zatem w ogóle martwić się o dostarczenie funkcji WndProc( ). Dzięki MFC możesz dostarczyć funkcje obsługujące wybrane komunikaty, a resztę zignorować . Te funkcje nazywaj ą się procedurami ob sługi komunikatów. Ponieważ nasza aplikacja je st oparta na MFC, procedura ob sług i komunikatów jest zawsze funkcją składo wą j ed n ej z klas aplikacji.
728
Visual C++ 2005. Od pOlIslaw P owiązanie międ zy
konkretnym komunikatem a o b s łu g uj ącą go funkcj ą w program ie jest dokonywane przez map ę komunikatów - musi ją mi eć k a żda klas a w pro gram ie, która obsługuje komun ikaty Windowsa . Mapa komunikatów dla klasy jest po prostu tablic ą funkcji s kład o w yc h, które obsłu gują komun ikaty Windowsa. Każdy wpis w mapie komunikatów łączy funkcję z konkretnym komunikatem. Gdy nadejdzie dany komunikat, wywoływana jest odp owiadająca mu funkcja . W mapie komunikat ów dla klasy zn ajdują się jedynie komun ikaty dla niej istotne. Mapa komunikatów dla klasy jest tworzona automatycznie przez kreator aplikacji MFC podczas tworzenia projektu lub przez ClassWi zzard, gdy dodajesz do programu klasę obsłu guj ąc ą komunikaty . Dodawaniem i usuwaniem wpisów z mapy komunikatów przeważnie zajmuje się ClassWi zard, jednak istnieją wyjątki i wtedy musisz samodzielnie zmienić mapę komun ikatów. W kodzie programu początek mapy komunikatów jest określony makrem BEGIN_MESSAGE_ MAP O , ajej koniec makrem END_MESSAGE_MAPO. Przyjrzyjmy się teraz, jak działa mapa komunikatów, na podstawie naszego przykładowego programu Sketcher.
Zrozumieć mapy komunikatów Kreator aplikacj i MF C tworzy mapę komunikatów dla każdej głównej klasy programu. W przypadku programu typu MDI, takiego jak Sketcher, mapa komunikatów jest definiowana dla następujących klas: CSket cherApp, CSket cherDoc, CSket cherVi ew, CMai nFrame i CChi ld Frame. Mapa komunikatów dla klasy znajduje się w pliku .cpp zawierającym implementację danej klasy. Oczywiście funkcje umieszczone w mapie komunikatów muszą być także zadeklarowane w definicji klasy, są one jednak identyfikowane w specy ficzny sposób. Przyjrzyj się poniższej definicji klasy CSk etc herApp:
cl ass CSketcherApp
publ ic CWinApp
{
publ ic: CSketcherApp(): II Przes lonięcia.
publ tc :
vir tual BDDL InitI nst ance(): II Implementacj a.
W klasie CSketc herApp zadeklarowana jest tylko jedna procedura obsługi komunikatów OnAppAbout ( l. Słowo af x_msg, znajdujące s i ę na początku wiersza z deklaracją funkcji OnAppAbout( l, służy jedynie do wyróżn ienia procedury ob sługi komunikatów spo śród innych funkcji składowych klasy. Preprocesor zmienia je na białe znaki, więc nie ma ono znaczenia podczas kompilacji. Makro DECLARE_MESSAGE_MAP( ) wskazuje, że klasa może zawi erać funkcje składowe służące do komunikatów. W rzeczywi sto ści wszystkie klasy wyprowadzane z klasy MFC CCmdTarget mogą potencjalnie zawierać procedury obsługi komunik atów, więc tego typu klasy będą obsługi
Rozdział 13.•
Praca zmenu i paskami narzędzi
729
zawierały to makro w swojej defin icj i. Umie ści j e tam kreator aplikacji MFC lub kreator Add Class, w zależnoś ci od narzędzia, z którego skorzystasz do utworzenia danej klasy. Rysunek 13.1 przedstawia klasy MFC wyprowadzone z klasy CCmdTarget , które zostały użyte w naszych dotych czasowych przykładach.
CCmdTarget
CWinThread
CWnd
CDocument
CFrameWnd
Rysunek 13.1 Klasy , które zostały wykorzystane bezpośrednio lub jako bezpośrednia podstawa naszych własnych klas, są zacieniowane. A zatem klasa CCmdTarget jest p o średnio klasą bazową dla klasy CSketc herApp, wi ęc ta ostatnia będzie zawsze zawierała makro DECLARE_ME SSAGE_MAP ( ). Wszystkie klasy widoku (i inne) wyprowadzane z CWnd również będąje zawierały . Jeżeli samodzielnie dodajesz własne składowe do klasy , najlepiej będzie , jeżeli pozostawisz makro DECLARE_MESSAGE_MAP O jako ostatni wiersz definicji klasy. Jeżeli dodasz składowe za makrem , będziesz musiał umieści ć dla nich specyfikatory do stępu: publ t e, pr ot ecte d lub
pri vat e.
Definicje procedur obslugi komunikatów Jeżeli
definicja klasy zawiera makro DECLARE_MESSAGE_MAP, implementacja klasy musi zawierać makra BEG IN_MESSAGE_MAPO i END_MESSAGE_MAP O . Jeżeli zajrzysz do pliku Sketcher.cpp, znajdziesz tam poniższy kod będący częś cią implementacji CSket cherApp:
BEGI N_MESSAGE_M AP(CSketcherApp, CWi nApp) ON_COM M AND(ID_APP_ABOUT. &CSketche rApp: :OnAppAbout) II Standardowe polecenia do pr acy z plikami.
ON_COM M AND(ID_FILE_NEW, &CW inApp: :OnFileNew) ON_COMM AND( ID_FIL E_OPEN . &CWinApp: :OnFi leOpen) II Standardowe polecenie ustawień drukowania.
ON_COM M AND(ID_F ILE_PRINT_SETUP. &CWinApp: :OnF il ePri nt Setup) ENDME SSAGE M AP( )
730
Visnal C++ 2005. Od podstaw Tak wygląda mapa komunikatów. Makra BEGIN_MESSAGE_MAP( ) i END_MESSAGE_MAP( ) są granicami map y komunikatów, a każda procedura obsługi komunikatów klasy znajduje się między nimi . W tym przypadku kod obsługuj e tylko jedną kategorię komunikatów - rodzaj komunikatu WMJ OMMAND, zwany komunikatem polecenia, który jest generowany, gdy użytkownik wybierze opcję menu lub użyje skrótu klawiaturowego (może się to wydawać nieco niezgrabne, a jest tak dlatego, że - jak przekonasz się w dalszej części rozdziału - istnieje jeszcze jeden rodzaj komunikatu WM_COMMAND, zwany komunikatem powiadamiającym z kontrolki). Mapa komunikatów rozpoznaje naciśnięty klawisz lub wybrany element menu na podstawie identyfikatora przesłanego w komunikacie. W przedstawionym powyżej kodzie znajdują s ię cztery makra ON_COMMAND, po jednym dla każdego obsługiwanego komunikatu poleceń. Pierwszy argument przesyłany do makra stanowi identyfikator pow iązany z konkretnym poleceniem, natomiast makro ON_COMMAN D wiąże nazwę funkcji z poleceniem określonym przez identyfikator. J eżeli zatem zostanie otrzymany komunikat odpowiadający identyfikatorowi l F_APP_ABOUT, zostanie wywołana funkcja OnAppAbout ( l . Podobnie dla komunikatu z identyfikatorem IDJILE_NEWzostanie wywołana funkcja OnFi leNew( l . Ta proc edura, podobnie jak pozostałe dwie, jest definiowana w klasie bazowej. Makro BEG IN_MESSAGE_MAP ( l przyjmuje dwa argumenty. Pierwszy z nich identyfikuje nazwę klasy, dla której definiowana jest mapa komunikatów, a drugi dostarcza połączenie z klasą bazową w celu odszukania procedury obsługi komunikatów. Jeżeli procedura nie zostanie odnaleziona w klasie, dla której definiowana j est mapa komunikatów, przeszukiwana jest mapa komunikatów dla klasy bazowej . Należy pamięta ć , że
identyfikatory poleceń, takie j ak ID_APP_ABOUT, są standardowymi identyfikatorami definiowanymi w MFC. Odpowiadają one komunikatom ze standardowych elementów menu i przycisków pasków narzędzi . Prefiks ID_używany jest do identyfikowania polecenia powiązanego z elementem menu lub przyciskiem paska narzędzi, o czym przekonasz s ię , gdy b ędę omawiał zasoby. Na przykład IDJ ILE_NEWjest identyfikatorem odpowiadającym wybraniu z menu FileINew, a ID_AP P_ABOUT odpowiada wybraniu z menu Help/About. Do identyfikowania standardowych komunikatów Windows wykorzystuje nie tylko symbol WM_COMMAND. Każdy z nich ma prefiks WM_ ozn aczaj ący komunikat Windowsa (ang. Windows Message). Te symbole są definiowane w pliku Winuser.h, który jest włączany w Windows.h. Jeżeli chcesz je poznać , plik Winuser.h znaj duje si ę w podfolderze lnclude folderu VC zawierającego Visual C++ .
Istni eje skrót do przeglądania pliku .h. Jeżeli j ego nazwa pojawia s ię w oknie edycj i, możesz ją po prostu kliknąć prawym przyciskiem myszy i wybrać z menu podręczn ego Open document "Nazwa_pliku.h". Komunikaty Windowsa często mają dodatkowe dane wykorzystywane do dokładnej identyfikacji danego komunikatu określonego identyfikator em. Na przykład komunikat WM_COMMAND jest przesyłany dla szeregu poleceń, włączając w to te pochodzące z wybrania elementu menu lub przycisku paska narzędzi. Podczas samodzielnego dodawania procedur obsługi komunikatów należy pamiętać, aby nie mapować komunikatu (lub - w przypadku komunikatów poleceń - identyfikatora polecenia) do więcej niż jednej procedury obsługi w klasi e. Robiąc w ten sposób, niczego nie zepsujes z,
Rozdzial13. • Praca zmenu i paskami narzędzi
731
jednak druga procedura nigdy nie zostanie wywołana . Procedury obsługi komunikatów dodaje się zwykle za pomocą okna właściwości i w takim przypadku nie ma możliwości mapowania komunikatu do więcej niż jednej procedury. Okno właściwości klasy można wyświetlić, klikając prawym przyciskiem myszy nazwę klasy w panelu Class View i wybierając Prop erties z menu kontekstowego. Procedurę obsługi komunikatów dodaje się poprzez kliknięcie przy cisku Messages w oknie Prop erties (rysunek 13.2). O tym, który przycisk to Messages, dowiesz się, przytrzymując kursor nad każdym z przycisków do momentu pojawienia się wskazówki.
Kliknięcie przycisku Messages spowoduje otwarcie listy identyfikatorów komunikatów. Zanim jednak powiem , co będziemy dalej robić, muszę omówić dokładniej typy komunikatów, które będziemy obsługiwać.
Kategorie komunikatów Istn ieją trzy kategorie komunikatów obsługiwanych przez program , a kategoria, do której komunikat należy , determinuje sposób jego obsługi . Istniejące kategorie komunikatów są pokazane w tabeli na następnej stronie.
Standardowe komunikaty Windowsa należące do pierwszej kategorii są identyfikowane przez definiowany przez system Windows identyfikator poprzedzony prefiksem WM_. W kolejnym rozdziale napiszesz procedury obsługi kilku takich komunikatów. Komunikaty z drugiej kategorii są szczególnym rodzajem komunikatów WM_COMMAN D. Poznasz je w rozdziale 16. podczas pracy z oknami dialogowymi. W tym rozdziale zajmiemy się ostatnią kategorią - komunikatami pochodzącymi z menu i pasków narzędzi. Oprócz identyfikatorów komunikatów
732
Visual C++ 2005. Od podstaw
Kategoria komunikatu
Opis
Komunikat Windowsa
Opr ócz ko munikatów WM_COMMAND, które omówię za mom ent, są to standa rdowe komunikaty Wind owsa roz p o c zy n aj ąc e się prefiksem WM_. Przykł adem może być tu komunikat W M_PAINT wskazujący k oniec zn o ś ć przerysowania obszaru klienta okna i WM_LBUTTONUP ws ka zuj ący na zwolnienie lewego przyci sku myszy.
Komuni katy z kontrolek
powiadamiając e
S ą to komunikaty W M_COMMAND prz e s y ła ne z kontrolek (takich jak lista wyb oru) do okna, które utworzyło k ontrol kę , lub z okna potomnego do macierzystego. Parametry p owiązan e z komunikatem WM_COMMAND p ozwalaj ą w programie odróżni ć komun ikaty pochodzące z kontrolek.
Komun ikaty
poleceń
Są
to komuni katy WMJOMMAND p och od z ąc e z elementów inter fejsu takich jak elementy menu i przyciski pasków narzędzi . MFC defin iuje unikalne identyfikatory dla standardowych komunikatów pol ec eń menu i pasków narzęd zi . u żyt k own ika,
dostarczanych przez MFC dla standardowych menu i pask ów narzędzi, możesz defin iować własne identyfikatory komun ikatów dla menu i pasków n arzędz i , które dodasz do programu. Jeżeli nie podasz identyfikatora dla tych nowych elementów , MFC automatycznie wygeneruje go za Ciebie na podstawie tekstu menu .
Obsluga komunikatów wprogramie Procedury obsługi komunikatów nie można umieścić w dowolnym miejscu . Jej umiejscowienie zależy od rodzaju komunikatu , jaki ma obsługiwać. Pierwsze dwie kategorie komunikatów z przedstawionej powyżej listy, czyli standardowe komunik aty Windowsa i komunikaty powiad amiające z kontrolek, s ą zaw sze obsługiwane przez obiekty klas wyprowadzanych z CWnd. Z CWnd są wyprowadzane na przykład klasy okien ramowych i widoków, więc mogą one zawi erać funkcje składowe do obsługi komunikatów Windowsa i komunikatów powiadamiających . Klasy aplikacji dokumentu czy szablonu dokumentu nie s ą wyprowadzane z CWnd, więc nie mogą one obsługiwać tego typu komunikatów . U życi e
okna właściwości klasy do dodania procedury obsługi komunikatu zwalnia nas od procedur, ponieważ zawiera ono jedynie identyfikatory komunikatów, które dana klasa może obsłużyć. Je żeli na przykład jako klasę wybierzesz CSketcherDoc, w oknie jej wła ściwości nie pojawi si ę żaden komunikat WM_. koniecznośc i pamiętania możliwych um iejscowień
Klasa CWnd dostarcza domyślną obsługę standardowych komunikatów Windowsa . Jeżeli zatem wyprowadzona klasa nie zawiera proc edury ob słu gi standardowego komunikatu Windowsa , jest on przetwarzany przez domyślną procedurę obsługi umieszczon ą w klasie bazowej . Jeżeli w klasie umieścisz własną procedurę obsługi , do poprawnego ob słu żenia komunikatu może być czasem potrzebne odwołanie do procedury znajdującej się w klasie bazowej. Gdy tworzysz własną procedurę obsługi, po wybraniu procedury w oknie właściwości klasy tworzona jest jej ramowa implementacja zawierająca w razie potrzeby odwołanie do procedury bazowej. Obsługa
komunikatów poleceń jest znacznie bardziej elastyczna. Procedury ich obsługi można w klasie aplikacji, klasach dokumentu i szablonu dokumentu , w klasach okna oraz widoku. Co s ię zatem dzieje po przesłaniu komunikatu polecenia do aplikacji, biorąc pod uwagę liczbę miejsc , w których mogą się zn ajdować ich procedury ob sługi? umieszczać
Rozdzial13. • Praca zmenll ipaskami narzędzi
733
Przetwarzanie komunikatów poleceń
Wszystkie komunikaty poleceń są przesyłane do głównego okna ramowego aplikacji. Następ nie główne okno ramowe rozsyła je w określonej kolejności do klas programu w celu ich obsłu żenia. Jeżeli klasa nie może przetworzyć danego komun ikatu, jest on przesyłany do następnej. Oto
kolejność
1
klas, do których
przesyłany jest
komunikat w aplikacji typu SD! :
Obiekt widoku.
2. Obiekt dokumentu.
a.
Obiekt szablonu dokumentu .
.. Obiekt głównego okna ramowego.
I. Obiekt aplikacji. Jako pierwszy możliwość obsłużenia komunikatu otrzymuje obiekt widoku, ajeżeli nie została zdefiniowana procedura obsługi, taką możliwość ma kolejny obiekt klasy. Jeżeli żadna z klas nie ma zdefiniowanej procedury obsługi danego komunikatu, zajmuje się tym domyślna procedura , przeważnie wyrzucając komunikat. W programie typu l'vlDI sytuacja się w niewielkim stopniu komplikuje. Mimo że mamy dostęp nych wiele dokumentów z wieloma widokami każdy, jedynie aktywny widok i powiązany z nim dokument są zaangażowane w przesyłanie komunikatów. Kolejność przesyłania komunikatu polecenia w programie MDI przedstawia się następująco: 1
Aktywny obiekt widoku .
2. Obiekt dokumentu powiązany z aktywnym widokiem.
a.
Obiekt szablonu dokumentu dla aktywnego dokumentu.
.. Obiekt okna ramowego dla aktywnego widoku.
I. Obiekt głównego okna ramowego.
8. Obiekt aplikacji. Kolejno ść przesyłania
gana, że nie
będę
jej
komunikatów można zmienić, jednak jest to sytuacja tak rzadko wymaw niniejszej książce.
omawiał
Rozwijanie programu Sketcher W tej części dodamy do programu Sketcher kod, aby zaimplementować funkcje niezbędne do tworzenia szkiców. Umożliwi on rysowanie różnokolorowych linii, kół, prostokątów i krzywych o różnej grubości linii, a także dodawanie komentarzy do szkiców. Dane szkicu będą przechowywane w dokumencie, zapewnimy także różnorodne widoki tego samego dokumentu w różnych skalach.
734
Visual C++ 2005. Od podstaw Poznanie wszystkich elementów, jakie zamierzamy dodać, zajmie nam kilka rozdziałów, natomiast dobrym punktem wyjścia będzie dodanie elementów menu obsługujących typy rysowanych kształtów i pozwalających wybrać kolor, w którym będziemy rysować . Zarówno wybrany kształt, jak i kolor będą trwałe w programie, co oznacza, że wybór będzie aktualny do momentu wybrania innego kształtu lub koloru . Dod anie menu do programu Sketcher będzie przebiegać w
poniższych
pojawiały się
•
Zdefiniowanie elementów menu, aby oraz w każdym menu .
•
Zdecydowanie, które klasy naszej aplikacji powinny dla poszczególnych elementów menu.
•
Dodanie do klas funkcji
•
Dodanie do klas funkcji on aktywny wybór.
•
Dodanie dla
każdego
obsługujących
w
etapach:
głównym
pasku menu
obsługiwać komunikat
komunikaty menu.
uaktualniających wygląd
elementu przycisku paska
menu tak, aby
zadań
odzwierciedlał
razem z podpowiedziami .
Elementy menu dwoma aspektami pracy z menu w MFC : tworzeniem i modyfikacją menu w aplikacji oraz przetwarzaniem niezbędnym po wybraniu danego elementu menu, czyli definicją procedury obsługi komunikatu . Zacznijmy od tworzenia nowych elementów menu .
Zajmiemy
się
pojawiającego się
Tworzenie iedycja zasobów menu Menu są definiowane poza kodem programu - w pliku zasobów, a specyfikacja menu jest nazywana zasobem . Istnieje jeszcze kilka innych rodzajów zasobów, które można dołączyć do aplikacji. Typowymi przykładami mogą być tu okna dialogowe, paski narzędzi i przycisk i pasków narzędzi. W trakcie dalszego rozwijania programu Sketcher poznasz kolejne z nich. Przechowywanie definicji menu jako zasobu pozwala na zmianę wyglądu menu bez modyfikowania kodu obsługującego zdarzenia menu. Możesz na przykład zmienić elementy menu z angielskich na francuskie lub norweskie bez konieczności modyfikowania i rekompilowania kodu programu. Kod obsługujący komunikat generowany po wybraniu elementu menu nie musi być związany z wyglądem menu, a jedynie z faktem, że element został wybrany. Oczywiście , jeżeli dodasz jakieś elementy do menu, będziesz musiał dla każdego z nich dodać kod, aby te elementy do czegokolwiek słu żyły! Program Sketcher posiada już menu , co oznacza, że istnieje dla niego plik zasobów. Dostęp do zawartości pliku zasobów dla programu Sketcher możemy uzyskać, wybierając panel Resource View lub klikając dwukrotnie Sketcher.rc w panelu Solution Explorer. Przełącza to widok na Resource View (widok zasobów) , w którym wyświetlane są zasoby. Po rozwinię ciu zasobu menu (po kliknięciu symbolu +) okaże się, że mamy zdefiniowane dwa menu
Rozdział 13.•
Praca zmenu i paskami narzędzi
135
oznaczone identyfikatorami IDR_MAI NFRAME oraz IDR_Sket cher TYP E. Pierwsze z nich dotyczy sytua cji, gdy żaden dokument nie jest otwarty, natomiast drugie sytuacj i, gdy otwarty jest jeden lub więcej dokum entów. MFC wykorzystuje prefiks IDR_ do oznaczania zasobów definiujących kompl etne menu okna. Będziemy zm i en ia ć
jedynie menu o identyfikatorze IDR_Sket cher TYPE. Nie musimy zajmoIDR_MA INFRAME, ponieważ dodamy elementy menu mające zastosowanie jedynie w sytuacj i, gdy b ędzie otwarty jaki ś dokumen t. Może sz uruchomić edytor zasobów dla menu poprzez dwukrotne kliknięci e identyfikatora zasobu w Resource View. Jeżeli otworzysz w ten sposób IDR_Sket cher TY PE, pojawi się panel Editor , który został pokazany na rysunku 13.3. wać s ię
.J
• x ł Śkd(h~; . t c(i:'~ ';T-YPf -~·Menu'-·~ ,1 -----;===--~------------"'-------"---~~---'-'-
~d l t
!:iew
!:!ew
Ctrl+N
Qpen.. .
Ctr\+O
~ind ow
ttelp
l_T}p"H..., ..
I
~.Iose ~e
ct rl+S
~rtlt. . .
arl+p
Print Preyiew
ptirt SetIJP•.•
Recent File
Rvsunek 13.3
Dodawanie elementu menu do paska menu Aby dodać nowy element menu, wystarczy kliknąć element menu z etykietą Type Here i wpis a ć nazwę noweg o elementu. Jeżeli w nazwie elementu umieścisz przed literą znak &, litera ta będzie skrótem klawiaturowym danego elementu. Jako nazwę pierwszego elementu wpisz E&l ement. Dzięki temu L będzie skrótem klawiaturowym tego elementu , a do jego wywołania będzie wystarczało wciśnię cie Alt +L. Nie możemy wykorzystać E, ponieważ jest już ono używane dla Edit. Gdy skończysz wpisywać nazwę , kliknij dwukrotnie nowy element, aby wyświetlić jego właściwości (rysunek 13.4).
736
Visual C++ 2005. Od podstaw
Rysunek 13.4
_!,lg
" I Men;;Edilor IMenuEd
~
I ~ 1 ~ 1::1 B Appe arance I
I
Checked
EBd.m.nt F.Is.
E~~i~d
True
G,. ~ed
F. lse
Popup
True
-
--
-
8 Behavlor Break Right Ju~ly RlQht Ordor
Non SelectObj ect C&aPen); II Wy bierz p ędzel.
CBrush* pOldBrush = CCBrush* )pDC ->SelectStockObjectC NULL_BRUSH) ; II Teraz rysuj
Po ustawieniu pióra i pędzla po pro stu przesyłamy cały prostokąt bezpośrednio do funkcji Rectang leC) , aby ta go narysowała . Wszystko, co pozostaje zrobić, to posprzątać i przywrócić stare pióro i pędzel w kontek ście urządz enia .
796
lisual C++ 2005. Od podstaw
Klasa CCircle Interfejs klasy CC i re l e nie różni się od klasy CReeta ng l e. Okrąg definiujemy tylko na podstawie opisującego prostokąta, więc definicja wygląda następująco : II Klasa
definiująca
obiekt
okręgu.
class CCi rcle :
pub lic CElement
pub li c:
-CCircle(vold);
virt ual voi d Oraw(COC* pOC); II Konstrukt or obiektu
II Fu nkcja
rysują ca okrąg
okręgu .
CCi rcleCCPoi nt Start, CPoint End, COLORREF aCo lor); protected: CCirc le(voidJ;
II Domyś lny konstrukt or - nie po winien
być używany.
}; Zdefiniowaliśmy
publiczny konstruktor, który tworzy okrąg na podstawie dwóch punktów i ponownie chroni konstruktor nieprzyjmujący argumentów. Dodaliśmy także do definicji klasy deklarację funkcji rysującej.
Implemenlacia klasy CCircle Jak pisałem wcześn iej , gdy tworzymy koło , punkt, w którym zostanie naciśnięty lewy przycisk myszy, stanowi jego środek, a po przemieszczeniu kursora (z wciśniętym lewym przyciskiem) punkt, w którym zostanie on zwolniony, jest punktem leżącym na obwodzie ostatecznego koła. Zadaniem konstruktora jest przekształcenie tych punktów w postać używaną przez klasę defi niującą okrąg .
Konslruklor klasy CCircle Punkt, w którym zostanie zwolniony lewy przycisk myszy , może znajdować się w dowolnym miejscu na obwodzie , więc współrzędne punktów wyzn aczających prostokąt opisujący muszą zostać obliczone jak na rysunku 14.18.
Rysunek 14.18
II
S k cłl h e rl
_
Odległość
to 2r
~l}:1
Tutaj naciśnięty lewy przycisk ----+-~;, myszy Promień r
-
I
Odległość
to 2r
I
__ ...J
x,.y,
Tutaj zwolniony lewy przycisk myszy
X
Rozdział 14.•
Na rysunku 14.18 widać ,
Rysowanie woknie
797
że możemy obliczyć współrzędne
lewego górnego i prawego dolnego (xl, y1), który jest punktem zapisanym przy naciśnięciu lewego przycisku myszy. Z akładając, że trybem mapow ania jest MM_TEXT, dla lewego górnego wierzchołka musimy jed ynie odjąć promień od obu współrzęd nych środka . Również prawy dolny wierzchołek uzyskamy, dodając promień do w spółrzęd nych x i y środka okręgu. Możemy więc napisać następujący kod konstruktora: wierz ch ołka prostokąta op isujące go wz ględem środka okręgu
II Konstrukt or obiektu
okręgu.
CC i rele : :CCi rele(CPoint St art . CPoint End. COLORREF aColor ) { II Naj p ierw ob licz prom ień .
II Używamy typ u zmiennopozycyjnego. pon ieważ j est to wymaga ne
II przez .fu nkcję biblioteczną (w math.h) d/a obliczenia pi erw iastka.
long Rad ius
~
stati e_east (sqrt (
st atie_east <double>« End.x-Start .x)*(End.x-Sta rt .x)+ (End.y-St art .y)*(End.y-St art .y» » :
II Teraz oblicz prostokąt opis ujący o krąg
II przy zalożeniu trybu mapowania MM_ TEXT.
m_EnelosingReet = CReet (Start .x-Radi us. St art .y-Rad ius .
St art .x+Radi us . Star t .y+Ra di us) ;
mColor = aColor: mPen = l : Aby
użyć
II Ustaw kolor okręgu.
II Ustaw szerokość p ióra na l .
funkcji sqrt O , musimy
dodać
na początku pliku Elements.cpp
następuj ący
wiersz :
#i nc lude <mat h> Możesz
go
umieści ć
pod
dyrektywą #i nc l ude
dla stdafx.h.
Maksymalne wartości współrzędnych są 32-bitowe , a składowe x i y CPo i nt są deklarowane jako l ong, więc obliczenie argumentu dla funkcji sqrt ( ) może być przeprowadzone jako liczba całkowita . Wynik obliczania pierwiastka kwadratowego jest typu doubl e, wię c rzutujemy go na l ong, ponieważ chcemy go wykorzystać jako liczbę całkowitą.
Rvsowanie okręgu Dowiedziałeś się już ,
raz
użyjemy
jak narysować okrąg, korzystając z funkcji Arc( ) klasy COC, więc te funkcji Ell i pse O . Implementacja funkcji Oraw() w klasie CCi rel e wygląda
następująco:
II Rysuj
okrąg.
void CCi rele : :Oraw(COC* pOC) { II Tworzy pi óro d/a tego obiektu i
II inicjalizuj e je z obie ktem kolom i
linią
o gruboset / piksela.
CPen aPen:
i f( !aPen.Create Pen(PS_SOLIO. m_Pen. m_Color )
( II Tworzenie p ióra nie po wiodło s ię.
AfxMessageBox(_T( "Pen ereat ion fai led drawi ng a ci rele "J . MB_OK ): AfxAbort ( );
798
Visual C++ 2005. Oli podstaw CPen* pOldPen = pDC- >SelectObject (&aPen);
II Wybierz pióro
II Wybierz pusty p ędzel.
CBrush* pOldBrush II Rysuj
=
(CBrush*)pDC->SelectSt ockObject( NULL_BRUSH);
okrąg .
pDC->Ell i pse(m_Enc losingRect); pOC->Select Obj ect( pOldPen);
II Przywró ć stare pióro.
II Przywró ć stary p ędzel.
~.poc - >se lectO bject( pOldB rUSh ) ;
Po wybraniu pióra w odpowiednim kolorze i pustego pędzla okrąg jest rysowany poprzez wywołanie funkcji El l i pset ). Jedynym argumentem jest obiekt CRect opi sujący rysowany okrąg. To jest kolejny przykład , którego utworzenie nie wymaga zbyt wiele wysiłku, ponieważ jest bardzo podobny do kodu rysującego prostokąt.
Klasa CCurve Klasa CCur ve różni się od pozostałych klas, ponieważ musi sobie poradzić ze zmienną liczbą punktów definiujących. To wymaga utrzymywania jakiegoś rodzaju listy, a ponieważ na temat list o różnej długości powiemy sobie więcej w następnym rozdziale, dopiero wtedy omówię szczegóły definiowania tej klasy. Na razie możesz zamieścić definicję klasy zawie rającą atrapy funkcji składowych, aby można było skompilować i skonsolidować kod zawiera jący ich wywołania . W pliku Elements.h powiniene ś wpisać:
A w pliku Elem ents.cpp: II Konstruktor obiektu krzyw ej .
CCurve: :CCurve(COLORREF aCo lor) {
m_Color = aColor;
m_EnclosingRect = CRect( O.O.O ,O) ;
m]en = l ;
II Rysuj
krzywą.
votd CCurve: :Oraw(COC* pOC)
{ }
być używany.
Rozdzial14.• Rysowanie woknie
799
Ani konstruktor, ani funkcja Draw( ) nie robiąjeszcze nic użytecznego, nie mamy także danych składowych definiujących krzywą. Konstruktor ustawia jedynie kolor, pusty prostokąt jako m_Enc l osi ngRect i szerokość pióra. W następnym rozdziale rozwiniesz tę funkcję.
UZlJpelnianie lirOCelilIr obslugi mySZY Możemy
do procedury obsługi komunikatu WM_MOUSEMOVE i uzupełnić szcze do niej dostać, wybierając CSket cher Vi ew w panelu Class View i klikaj ąc dwukrotnie nazwę procedury - OnMouseMove() . teraz
powrócić
góły . Możesz się
Ta procedura ma znaczenie tylko podczas rysow ania kolejnych tymczasowych wersji kształtu w trakcie przesuwania myszy, ponieważ ostateczny kształt jest tworzony po zwolnieniu lewego przycisku. Aby zapewnić podgląd rysowania (ang . rubber-bending) , możemy potraktować rysowanie tymczasowych kształtów jako całkowicie lokalne dla tej funkcji, pozostawiając narysowanie ostatecznej wersji utworzonego kształtu funkcji składowej widoku - OnDraw( ). To podejści e sprawi , że podgląd rysowania kształtów będzie wydajny , ponieważ nie będ zie angażował funkcji OnDr aw() odpowiedzialnej za rysowanie całego dokum entu .
Ustawianie trybu rysowania Funkcja SetROP2( ) ustawia tryb rysowania dla wszystkich kolejnych operacji wyjścia w kon z obiektem COC. ,,ROP" w nazwie funkcji jest skrótem angiel skiego Raster OPeration (operacje rastrowe), ponieważ ustawienia trybów rysowania dotyczą elementów rastrowych .
tekście urządzenia, powiązanych
Tryb rysowania określa, jak połączyć kolor pióra użytego do rysowania z kolorem tła, aby ustalić kolor wyświetlanego obiektu . Tryb rysowania określamy jednym argumentem funkcji, który może przyjąć jedną z wartości :
Tryb rysowania
Etekt
R2 BLACK
Wszystko jest rysowane na czarno.
R2 W HITE
Wszystko jest rysowane na
R2 NO P
Operacje rysowania nie mają efektu.
R2 NOT
Rysowanie przebiega zawsze w kolorze przeciwnym do wyświetlanego już na ekranie. To zapewnia, że wyjście będzie zawsze widoczne, ponieważ zapobiega rysowaniu w kolorze tła.
R2 COPY PEN
Rysowan ie w kolorze pióra. To jest domyślny tryb, je żeli go nie ustawisz .
R2 NOTCOPY PEN
Rysowanie w kolorze przeciwnym do koloru pióra.
R2 MERGEPE NN OT
Rysowanie w kolorze powstałym z alternatywy bitowej koloru pióra i koloru przeciwnego do koloru tła .
R2 MASK PENNOT
Rysowanie w kolorze powstałym z koniunkcji bitowej koloru pióra i koloru przec iwnego do koloru tła .
R2_MERGENOTPE N
Rysowanie w kolorze do koloru pióra.
biało.
powstałym
z alternatywy bitowej koloru
tła
i koloru przeciwnego
Wisuał
800
C++ 2005. Od podstaw
Tryb rysowania
Elekt
R2 MASKN OT PEN
Rysowanie w kolorze powstałym z koniunkcji bitowej koloru tła i koloru przeciwnego do koloru tła .
R2 MERGEPEN
Rysowanie w kolor ze
R2 NOTMERGEPEN
Rysowanie w kolorze przeciwnym do koloru R2_MERGE PEN.
R2_MASKPEN
Rysowanie w kolorze pows ta łym z koniunkcji bitowej koloru
R2 MASKPEN
Rysowan ie w kolorze przeciwnym do koloru R2_MASKPEN.
R2 XORPEN
Rysowanie w kolor ze powstałym z bitowej alternatywy wyklu czającej koloru pióra i koloru tła .
R2 NOTXORPE N
Rysowanie w kolorze przeciwn ym do koloru R2_XORPEN.
Każdy
sporo
pow stałym
z alternatywy bitow ej koloru
tł a
tła
i koloru pióra .
i koloru pióra.
z tych symboli jest predefiniowany i odpowiada konkretnemu trybowi rysowania. Jest ale dla nas największe cuda może zdziałać R2_NOTXORPEN.
możliwości,
W trybie R2_NOTXORPEN, gdy za pierwszym razem narysujesz kształt na domyślnym , białym tle, jest on rysowany normalnie, wybranym kolorem pędzla . Jeżeli narysujesz ten sam kształt na narysowanym wcześniej, zniknie on, pon i eważ kolor, w którym jest rysowany nowy kształt, powstał w wyniku bitowej alternatywy wykluczającej samego koloru pióra. Powstałym kolo rem jest biały. Będzie to łatwiej zrozumi e ć na podstawie przykładu. B iały
jest tworzony w równym stopniu przez "maksymalne" ilości czerwonego, niebieskiego i zielonego. Dla uproszczenia możemy to przedstawić jako l , l , l - trzy wartości repre zentujące składniki RGB. W tym schemacie czerwony będzie zapisany jako l, 0, o. Łączy s ię to następująco:
R Tło
G
B
O
O
O
O
białe
-
Pióro
czerwone
XOR czerwonego
O
NOT XOR - powstaje czerwony
A zatem za pierwszym razem, gdy rysujesz czerwoną linię na białym tle, powstaje czerwona, co wskazuje ostatni wiersz tabeli. J eżeli teraz narysujesz taką samą linię na już istniejąc ej , tło , na którym będziesz rysował , będzie czerwone. Kolor rysowania powstaje następująco:
R Tło
-
Pióro -
czerwone czerwone
XOR czerwonego NOT XOR -
powstaje biały
O
G
B
O
O
O
O
O
O
Rozdział 14.•
RysOwanie woknie
Jak wskazuje ostatni wiersz tabeli, powstaje biała linia, a ponieważ reszta linia znika.
801
tła jest również biała,
Musimy tu zadbać o użycie wła ś ciwego koloru tła . Myślę, że zauważyłe ś , iż rysowanie białym piórem na czerwonym tle nie będzie działało zbyt dobrze , p onieważ już przy pierwszym ryso waniu linia będzie czerwona, a w zw i ązku z tym niewidoczna. Za drugim razem będzie biała. Jeżeli będziesz rysował na czarnym tle, kształty będą poj awiały się i znikały jak przy białym tle, ale nie będą rysow ane w wybranym kolorze pióra.
Tworzenie kodu obsługi OnMouseMovelJ Zaczniemy od kodu tworzącego kształt po otrzymaniu komunikatu o ruchu myszy . Poniew aż będziemy rysowali ks ztałt w procedurze ob sługi , potrzebujemy utworzyć obiekt kontekstu urządzenia. Najwygodniejszą w tym celu klasą jest CC l i entDC, która jest wyprowadzana z CO C. Jak wspominałem wcześniej , zaletą korzystani a z tej klasy, a nie z CDCjest fakt, że automatycz nie zajmuje si ę ona tworzeniem kontekstu urządzenia i niszczeniem go po zakończeniu . Two rzony przez nią kontekst urządzenia odpowiada obszarowi klienta okna, a o to właśnie nam chodzi. Dodaj poniższy kod do zary su procedury obsługi, który zdefin iował e ś wc ześniej : voi d CSketcher Vie w: :OnMouseMove(UI NT nFla gs. CPoi nt poi nt ) II Definiuje obiekt kontekstu
urządzen ia
CCl i ent.OC aOC( t hi s) ; aOC Set.ROP 2( R2_NOTXO RPEN ) ; i f (nFl ags & MK ~ B U T TO N)
dla widoku. II Kan/eks/ urządz eni a dla lego widoku. II Ustaw tryb ryso wania.
m_S econdPoi nt. = poi nt ; II Sprawdź. czy istnieje poprzedni
ksz ta ł t
II Zap isuje bieżącą pozycję kursora. tymczasowy.
II Tutaj dochodzimy. jeże li mysz była wcześniej poruszo na, II więc um ieś cimy tu kod usu wają cy stary kszt ałt. )
II Utwórz tymczaso wy element a typ ie i kolorze. II który j est zap isany w obiekcie dokumentu, a n as tępn ie go I1a1YS1!j. m_pTempEl ement = Creat.eEl ement ( ) :11 Utwórz no")' element. m pTempEl ement.->Draw(&a OC) ; II Rysuj e/ement.
Pierwszy nowy wiersz kodu tworzy lokalny obiekt CCl i ent. Wskaźnik thi s, który przesyłamy do konstruktora, identyfikuje bieżący obiekt widoku, więc obiekt CCl i entDC ma kontekst urzą dzenia odpowiadający obsz arowi klienta bieżącego widoku. Oprócz wspomnianych charakte rystyk, obiekt ten ma wszystkie funkcje ry sujące, których potrzebujemy, ponieważ dziedziczy je z klasy COC. Pierwszą funkcją s k ład ową, z której korzystamy, jest SetRO P2(), ustawiająca tryb rysowania na R2_NOTXORPEN. Aby utworzyć nowy element, zapisuj emy bieżącą pozycję kursora w składowej m_SecondPoi nt, a następnie wywołujemy funkcję składową widoku Creat eElement ( ) (tę funkcje zdefiniujemy zaraz po ukończeniu tej procedury). Funkcja ta powinna tworzyć kształt na podstawie dwóch punktów przechowywanych w obiekcie bieżącego widoku oraz typu i koloru przechowywa nych w obiekcie dokumentu , a następnie zwracać adres dokumentu. Zapisz to w m_pTempEl ement.
802
Visual C++ 2005. Od podstaw Używając wska źnika
do nowego elementu, wywołujesz jego funkcję składową Oraw ( ), aby sam. Adres obiektu CCl i entDCjest przesyłany jako argument. Ponieważ zdefiniowali śmy funkcję Oraw( ) jako wirtualną w klasie bazowej CElement , zostanie automa tycznie wybrana funkcja dla typu kształtu wskazyw anego przez m_pTempElement . Nowy k ształt zostanie narysowany normalnie w trybie R2_NOTXORPEN, ponieważ za pierwszym razem rysuje my go na białym tle. obiekt
narysował się
Możemy użyć wskaźnika
sowy. Kod dla tej
części
m_pTempElement do określ enia , czy istnieje poprzedni procedury obsługi wygląda następująco:
kształt
tymcza
void CSket cherView: :OnMouseMove( UINT nF lags. CPo lnt point ) { II Defin iuje obiekt kont ekstu
urządz enia
CCl ientDC aDC (t his ): aDC. Sct ROP2(R2_NOTXOR PEN ) : i f( nFl ags &MK_LBUTTON)
dla widoku.
II Kont ekst urządzen ia dla tego widoku.
II Ustaw tryb rysowania.
{
m_Second Po int
=
point:
II Zap isuj e
b ieżącą pozycję
kursora.
lf (mpTempElement ) { II Przerysuj stary element, aby
II USllli stary element. II Przywróć wskaźn ik na O.
II Utwórz tymczaso wy element o typ ie i kolorze,
II który j est zapi sany w obiekcie dokumentu, a nas tępnie go narysuj.
m_pTempE lement = Creat cE lement () . II Utwórz nowy element.
m_pTempEl ement ->Draw(&aDC); II Rysuj element.
Poprzedni kształt tymczasowy istnieje, jeżeli wskaźnik m_pTempEl ement nie jest zerem . Musi my przerysować kształt, do którego on wskazuje, aby usunąć go z obszaru klienta widoku. Następnie usuwamy kształt i przywracamy wskaźnik na 0, Potem tworzony jest nowy ele ment i rysowany przez poprzednio dodany kod. To połączenie automatycznie tworzy podgląd tworzonego kształtu , więc wydaje si ę on być przyczepionym do kursora. Po utworzeniu osta tecznej wersji k ształtu nie możemy zapomnieć o przywróceniu wskaźnika na O w procedurze obsługi komunikatu WM_LBUTTONUP.
Tworzenie kształtu Powinieneś dod ać funkcję
Creat eElement jako
CSketc herVi ew:
class CSketcherView publ ic CView { II Reszta definicji bez zmian. II Operacje. publ t e :
składową chronioną do części
Operacj e klasy
Rozdział 14.•
Rysowanie woknie
803
prot ecte d:
CElement * Creat eElement (void);
II Reszta defin icj i bez zmian .
l. W tym celu może sz albo b e zpo średnio zm i en ić defini cj ę klasy, dodając zacieniowany wiersz, albo kl iknąć prawym przyciskiem myszy n a zwę klasy CSk et cherVi ew w panelu Class View i wybrać Add/Add Function z menu kontek stowego . Zostanie otwarte okno dialogowe przed stawione na rysunku 14.19. -
-
Rysunek 14.19
-
L~
Add Hember f unctio n Wizard . Sket cher
Wel come lo lhe Arlrll\'1ember Function Wi zard
Return t~8:
I
lCEIe m e nt·
(v
Parame~er t y~. :
I
F~ctlOn
nerner
[ereatt Element
Parameter list:
Parameter neme :
I !lt
H I' -
Ac'ę.s s:
!prote:,:te.:.. :::d
~
D' 2tati' D -J;.Iv-J! D [nline
~Irtual
D
Eu".
[ · '~~ tUe :
eOffil1lent (II not ęnon not recuired) :
---'I
L-"-'--_ _- '
Flnish
II
I
Cancel
Okre ś l funkcj ę jak na rysunku i kliknij przycisk Finish. Deklaracja funkcj i s kła do wej zostanie dod ana do definicj i klasy i zostanie od razu otw arty szki elet funkcj i w Sketcher.cpp . J e żeli samodzielnie dod ał e ś d ekl ara cj ę do defini cji klasy, musisz dodać ca łą defini cję funk cji do pliku .cpp :
II Tworzy element bie żą cego typu.
CElement* CSket cherV1ew: :CreateElement Cvoid) ( II Pobierz
wskaź n ik
do dokumentu dla tego widoku.
CSketcherOoc* pOoc ~ GetOocument C) :
ASSE RT_VALIO CpOoc). II Spra wdź, czy
wskaźnikjes t
dobry .
II Wybierz element. korzystajq c z typu zapisanego w dokumencie.
swi tchCpOoc->Get ElementTypeC» )
{
case RECTAN GL E.
ret urn new CRect angle (m_Flrst Pol nt . case CIRCLE:
SecondPOlnt .
pOoc->GetEl ement ColorC» ;
ITI
804
Visual C++ 2005. Od podstaw ret urn new CCi rcle (m_FirstPoint , mSecondPoint , pDoc->Get Element Color( )) ; case CURVE; return new CCurve(pDoc->Get ElementColor ( )) ; case LI NE ret urn new CLine(m_First Point , m_SecondPo1nt . pOoc->Get Element Color () ); detault : II Coś po szlo nie tak.
AfxMessageBox(_T( "Bad Element code") : MB_OK) ; AfxAbort( ); ret urn NULL ;
Niezacieniowane wiersze zostały automatycznie dodane, j eżeli dodałeś funkcję do klasy, korzy stając z okna dialog owego AddlAdd Function. Pierwsza rzecz, jaka się tu dzieje, to pobranie wskaźnik a do dokumentu poprzez w ywołanie funk cji GetOocument ( l. Dla bezpieczeństwa używamy makra ASSERT_VALID( l, aby upewnić s i ę, że został zwrócony dobry wskaźnik. W wer sji testowej aplikacj i makro to wywołuje składową Asse rtVal i d( l obiektu, który jest okre śl ony jako argument makra . Sprawdza ona poprawność bieżącego obiektu i jeżeli wskaźnik jest NULL lub obiekt jest uszkodzony , wyświetlany jest komunikat o bł ędzie . W wersji końcowej aplikacji makro to nic nie robi . Instrukcj a swi t ch wybiera kształt, który ma zostać utworzony na podstawie typu zwróconego przez funkcj ę w klasie dokumentu - GetE l ementType ( l. Inna funkcja z klasy dokum entu jest używana do uzyskania bieżącego koloru kształtu. Możesz dodać definicje tych dwóch funkcji bezpośrednio do defini cji klasy CSketche rDoc, poniewa ż s ą one bard zo proste :
class CSketc herDoc ; publi c CDocument ( II Reszta defini cji bez zmian, II Op eracj e.
pub11(;
unsigned i nt Get El emen t Type() { ret urn m_E lement; } COLDR REF Get Element Color( ) { ret urn mCo lor ; }
II Pobierz typ elementu. II Pobierz kolor elementu.
II Reszta definicji bez zmian.
}; Każd a z funkcj i zwraca w artość przechowywaną w odp owiedniej skł adowej . Pamiętaj , ż e umies zczenie definicji funkcji składow ej w definicj i klasy j est równ oznaczne z utworzeniem funkcji i nl i ne, w i ę c opró cz tego, że są one proste, powinn y być także szybkie.
Rozdział 14.•
Rvsowanie woknie
805
ObslUga komunikatów WM_LBUTTONUP Komunikat WM_LBunONUP kończy proces tworzenia kształtu. Zadaniem procedury obsługi tego komunikatu jest przesłanie ostatecznej wersji kształtu do obiektu dokumentu, a następnie oczyszczenie danych składowych obiektu widoku. Możesz znaleźć i zmodyfikować kod tej procedury, tak jak robiłeś to poprzednio. Dodaj do funkcji poniższe wiersze:
voi d CS keteherV iew: :OnLBut t onUp(UINT nFlags. CPo i nt pOint) ( II Upe wnij
się. że
element istni ej e.
i f(m_pTempElement ) ( II Wywołaj funkcj ę z klasy elementu. aby zapisa ć element
II wskazyw any przez myTemp Element w obiekcie dokumentu.
del et e m_pTempElement : m_pTempEl ement ~ O:
II Ten kodj est tymczasowy . II Zeruj wskaźnik do elementu.
Instrukcja i f sprawdza, czy m_pTempEl ement nie jest zerem przed przetworzeniem go. Zawsze istnieje prawdopodobieństwo, że użytkownik przyciśnie i zwolni przycisk bez ruszania myszą, a w takim przypadku dokument nie zostałby utworzony. Jeżeli tylko kształt istnieje , wskaźnik do niego jest przesyłany do obiektu dokumentu . Kod dla tej funkcjonalności dodamy w następ nym rozdziale. W międzyczasie usuwamy tylko kształt, aby nie zaśmiecać sterty. Wres zcie, m_pTempElement zostanie ustawiony na Oi przygotowany na następną operację rysowania.
Testowanie szkicownika Zanim
będziesz mógł uruchomić przykład
poprawi ć funkcję
który
z procedurami obsługi komunikatów myszy, musisz OnDraw() w implementacji klasy CSketcherVi ew i pozbyć się starego kodu,
dodawałeś wcześniej.
Aby upewnić się, że funkcja OnDr aw( ) jest czysta, kliknij dwukrotnie jej nazwę w panelu C/ass View w celu otworzenia jej implementacji w pliku SketcherView.cpp. Usuń stary kod, ale pozo staw pierwsze cztery wiersze, które wstawił kreator, aby mieć wskaźnik do obiektu dokumentu. Będziesz go potrzebował później, aby dosta ć się do kształt ów przechowywanych w dokumen cie. Kod tej funkcji powinien wyglądać następująco:
vo id CSketeherView: :OnOraw(COC* pOC) (
CSket eherOoe* pOoe = Get Ooeument( ):
ASSERT_VAL IO(pOoe) :
if ( ! plice )
ret urn:
80&
Visual C++ 2005. 011 podstaw Poni e wa ż
nie mam y jes zcze k s zt ałtó w w dokum entach, nie musisz na razie niczego doda do tej funkcj i. Gdy zaczniemy prze chow yw ać dane w dokumentach (a b ęd zi e to w na s tę p ny m rozdziale), dodasz kod ry sujący ks ztałty w odpowiedzi na komu nikat WM_PA l NT. Bez niego kszta łty po prostu znikają za każdym razem , gdy zmienisz wie lko ś ć widoku , o czy m się przek on asz . wać
Uruchamianie przykładu Po upewnieniu si ę , że zap isałeś wszystkie pliki źród ło we , zbuduj program . Je ż eli podczas wpi sywa nia kodu nie p opełn iłe ś żad nego błędu , przebiegn ie to bez problemów, więc będziesz mógł uruchomi ć program . Możesz ryso wać linie, o kręg i i pro stokąty we wszystkich czterech kolorach obsłu gi wan y ch przez program . P rzykład owe okno przedstawia rysunek 14.20.
\
I
I
~/
Reedv
Rysunek 14.20 Poekspe rymentuj z inte rfejsem u ż ytkownika. Zwróć uwag ę, że m o że s z prze suwać okno, a ks ztałty pozostaną w nim, chyba że przeniesiesz okno tak, że kształ t będ zie poza oknem apli kacji . Jeż el i tak zrobisz, kształty nie pojaw i ą s i ę ponownie, gdy powrócisz z oknem. Dzieje tak dlatego, że i stn iejące elementy nie są nigdy przerysowywane. Gdy obszar klienta jes t zakrywany i odkrywany, Windows wysyła do aplikacj i komunikat WM_PAI NT, który powo duje wywołanie funkcj i składowej widoku OnDraw ( ). Jak wiesz, nasza funkcj a OnDraw( ) w widoku na razie nic nie rob i. Naprawimy to, gdy będ ziemy p rzec howyw ać ks ztałty w dokumentach. Gdy zmieniasz rozmiar okn a, ksz tałty z n i kają natychmiast, natomiast gdy przenosi sz cały wid ok , p o zost ają (dopóki nie w yjd ą poza okn o aplikacji ). Ja k to moż l i we ? Gdy zm ieniasz w ielk oś ć okna , Windows uni eważni a cały obsza r klienta i spodziewa się , że aplikacj a je prze
Rozdzial14. • Rysowanie woknie
807
rysuje w odpowiedzi na komunik at WM_PAI NT. Jeżel i przem ieszczasz widok, Windows zajmuje s i ę przeno szeni em widoku w takiej postaci, w jakiej jest. Mo żes z to zobac zyć, gdy przenie siesz widok tak, że kształt będz ie tylko częściowo zasło n ię ty . Gdy powr ócisz do poprzedniej pozycji, wci ąż b ędzie fragment kształtu, jednak z usuniętym fragmentem, który był zasłoni ęt y. Jeżel i
rysujemy ks ztałt i przeci ągniemy kursorem poza obszar klienta, zaczn ą się dziać dziwne rzeczy. Poza oknem widoku tracimy ś l e dzen ie myszy, co psuje mechanizm pod glądu rysowa nia. Co si ę dzieje?
Przechwytywanie komunikatów myszy Probl em bierze się s tąd , że Windows przesyła komunikaty myszy do okna zn aj dującego s ię pod kursorem. Gdy kurs or opuści obszar kli enta nasz ej aplikacj i, komunikaty WM_MOUSEM OV E są wysyłan e w inne miejs ce. Możemy to naprawić, korzystając z odzi edziczonych składo wych klasy CS ket cher Vi ew. Klasa widoku dziedziczy fun kcję Set Ca pt ur e( ), której m ożemy użyć do poinformowania sys temu Windows, że nasza aplikacja ma otrzymywać wszystkie komunikaty myszy, dopóki nie powiemy, że ma być inaczej, czyli nie wywołamy kolejnej funkcji dziedziczonej przez klasę widoku - Rel easeCaptu r e( ). Możemy przechwytywać komunikaty myszy, gdy naciśni ęty jest lewy przyci sk, je śli zmienimy nieco pro c edurę obsługi komunikatu WM_LBUTTONDOWN.
void CSketcherV iew: :OnLButt onDown(U INT nFlags , CPoi nt poi nt ) { II Do zrob ienia: wpisz tu kod procedury
mFl rstPolnt ~ otnt : SetCapt ure().
o bs ługi
komunikatu i (lub) wywołaj domyślną . ll Za isz oz cję kursora.
II Przec hwy tuj ko lejn e ko munika ty z myszy.
Teraz musimy wywołać funkcję Rel easeCapt ur e() w procedurze ob słu gi WM_LBLlTTONUP. Je żeli tego nie zrobimy, inne programy nie będą otrzymywały żadnych komunikatów myszy w trak cie dział ania naszego programu. Oczywi ści e powinniśm y zwalniać mysz jedynie wtedy, gdy wc ześni ej ją przech wycili śm y . Funkcja Get Capt ur e( ), dz iedziczona przez klas ę widoku , zwraca w skaźnik do okna, które przechwyciło mysz, a to pozwala nam stw i erdz ić, czy komu nikaty s ą przechwytywane, czy też nie . Musimy tylko dodać następujący kod do pro cedury obsługi
WM_LBLlTTONLl P:
void CSketcherVi ew: :OnLButtonUp(U INT nFlags. CPoint point) i f( t his ~= GetCapt ure( )) ReleaseCapture(): II Up ewnij się,
że
II Zakoń cz przech wy tywanie komun ika/ ów z myszy.
element istnieje.
i f (m_pTempEl ement ) { II Wywo laj fu nkcję z klasy elementu. aby zapisać eleme nt II wskazy wany prz ez myTempElement w obiekc ie dok um entu.
delete m_pTempElement : m_pTempElement = O:
II Ten kod j est tymczasowy. II Zeruj ws kaźnik do elementu.
aoa
Visual C++ 2005. Od podslaw Je że li w skaźnik
widok
zwróc ony przez
przechwycił mysz, w ięc ją
funkcj ę
GetCapture () j est równy
wska źnikowi
t hi s, nasz
zwalniamy.
O st atni ą koniec zn ą p op rawką obsługiwa ła jed ynie
jest zmodyfikow anie procedury o bsł ug i WM_MOUSEMOVE, aby komunikaty przechwycone przez widok. Moż esz to zrobić, wprowadzaj ąc
j edną małą zmianę :
void CSketcherView: .OnMouseMoveCUINT nFlags . CPoint po i nt) { II Definiuj e obiekt kontekstu
urządz enia
dla widok u. II Kontekst urządzen ia dla tego widoku. II Usta w tryb rysowa nia.
CCl ientDC aOCCt hl s): aOC.SetROP2 (R2 NOTXORPEN) : i f ccnF lags &MK LBUTTON) && Cthis m_SecondPoint
=
point :
~~
GetCa pt ureC)) ) II Zapisuje bi eżącą pozycję kursora.
ifCm_pTempE l ement ) { II Przerysuj stary elem ent, aby znikn ął z widoku .
m_pTempE lement ->O rawC&aCC) :
delete m_pTempEl ement : m_pTempE lement = O:
II Usuń stary element.
II Przywróć wskażn ik na O.
II Utwórz tymczasowy element o typie i kolorze,
II który jest zapis any w obiekcie dokumentu, a nas tępn ie go narysuj.
m_pTempElement = Creat eElement C) . II Utwórz nowy element.
m_pTempE lement->Oraw C&aOC ): IIRysuj element.
Procedura ob słu gi przetwarza komunikaty tylko wtedy, gdy lewy przycisk jest wciśnięty i pro cedur a o bsługi wciśnięcia lewego przycisku myszy dla widoku została wyw ołana , więc mysz zo stała prz ech....-ycona przez nasze okno widoku . J eżeli
ponownie skompilujesz program Sketcher z tymi dodatkami, przekona sz s ię, że wcze problemy powstające przy przeciągni ęciu kursora poza obs zar klienta ju ż nie wy
śniej s ze stępuj ą.
Podsumowanie Po lekturze tego rozdziału powini eneś mieć wied z ę na tem at twor zenia pro cedur obsłu gi komunikatów dla myszy , a takż e w i edzi eć , jak zorgani zować operacje rysowania w aplikacji dla systemu Windows. Najwa żniejs ze punkty omów ione w tym rozd ziale: •
Domy ślni e
Window s adresuje obszar klient a okna, używ ając układu w sp ółrzędnych, którego poc zątek znajduje s i ę w lewym górn ym rogu obszaru klienta . W arto ś ci x rosn ą od lewej do prawej , a wartości y od góry do dołu.
•
M ożesz ry sować
w obszar ze klienta okna,
korzystaj ąc jedynie
z kontekstu
urząd zenia .
Rozdzial14. • Rvsowanie woknie
809
• Kontekst urządzenia zapewnia szereg logicznych układów współrzędnych, zwanych trybami mapowania, s łużących do adresowania w obszarze klienta okna. •
Domy ślnym początkiem
w trybie mapowania je st lewy górny róg obszaru klienta. trybem mapowania je st MM_TEXT, który obsługuje w spółrzędne wyrażone w pikselach. Wartości x w tym trybie rosną od lewej do prawej , a warto ś ci y od dołu do góry. Domyślnym
• Program powinien zaw sze ry sować stał ą zawarto ść obszaru klienta okn a w odpowiedzi na komunikat WM_PAINT, natomi ast zaw artość tymczasowa mo że b yć rysowana kiedy indziej. Wszystkie operacj e związ ane z rysow aniem w dokumencie aplikacj i powinny być sterowane z funkcji s kł ad ow ej kla sy widoku OnDr aw( ). Ta funkcja jest wywoływan a , gdy aplikacja otrzyma komun ikat WM PAl NT.
•
Możesz okre śli ć częś ć
obszaru klienta, którą chcesz przerysować, wywołując kla sy widoku In val i dateRect(). Obszar prz e syłany jako argum ent jest dodaw any przez system Window s do całego obszaru wym agaj ącego przerysowania po otrzymaniu komunikatu WM_PAI NT. funkcję składową
• Window s wysyła standardowe komun ikaty dla zd arze ń mysz y. Może sz procedury obsługi tych komunikatów za pomocą kreat ora klas . •
Wyw ołując funkcję
SetCapt ur e( ) w klasie widoku , możesz sprawić , że wszystkie
komunikaty myszy
będą kierowane
wyw ołując funkcję
Rel easeCaptu r e( ).
nie będą otrzymywały •
tworzyć
do aplikacji. Mu sisz potem zw o l n ić mys z, J eżeli tego nie zrobisz, inne aplik acje komunikatów myszy.
Możesz zaimplementować pod gląd rysow ał
rysowania (ang. mbber-handing),je żel i będzi esz obiekty w procedurze ob sługi komunikatu o przemie szczeniu myszy .
• SetROP2 ( ), czyli funkcja składow a klasy CDC, um ożliwia ustawienie trybu rysowania.
Wybrani e odpowiedniego trybu rysowania znacznie rysowania.
uł atwia
operacje
podglądu
Ćwiczenia Kod źródłowy oraz ksiazki/vcpp o.htm 1
rozwiązania
do poniższych
ćwiczeń
znajdziesz pod adresem http://helion.pl/
Dodaj element menu oraz przycisk paska narz ędzi dla elipsy, jak w ć wi cze n i u z rozdziału 13. Zdefiniuj klasę obsługującą rysowanie elips wyznaczanych prze z dwa punkty zn ajdujące s i ę w przeciwl egłych wierzchołkach opi sującego prostok ąta.
2. Które funkcje Z mi eń
będą wymagały
program, aby
zmian, aby ob sługiwały rysowanie elip s? rysowanie elips.
możliwe b yło
810
Visual C++ 2005. Od podstaw 8. Które funkcje w przykładzie z poprzedniego ćwiczenia należy zmienić, aby pierwszy punkt określał środek elipsy, a
bieżąca
pozycja kursora definiowała w ten sposób. (Wskazówka: przejrzyj w pomocy składowe klasy CPoi nt) . wierzchołek opisującego prostokąta? Zmień przykład
4. Dodaj nowe menu do IDR (Sk etcherTYPE) dla stylu pióra (Pen Sty le), aby wybranie linii ciągłej, kreskowanej, kropkowanej, oraz z dwóch kropek i kreski .
I. Które części programu kształtów
z
użyciem
8. Zaimplementuj
należy zmienić,
umożliwić
z kropek i kresek
aby możliwe było działanie menu i rysowanie
tych typów linii?
obsługę
z tych typów linii.
składającej się
nowego menu i rysowania
kształtów z użyciem każdego
15
Tworzenie dokumentu
ipoprawianie widoku
W tym rozdziale poznasz możliwości, jakie MFC oferuje do zarządzania kolekcjami danych. Wykorzystasz je do ukończenia definicji i implementacj i klasy kształtu krzywej, której nie dokończyliśmy w poprzednim rozdziale. Rozbudujemy także program Sketcher, aby przecho wyw ał dane w dokumencie, i uelastycznimy trochę widok dokumentu, wprowadzając kilka nowych technik . W tym rozdziale będziemy się uczyć: • O kolekcjach i ich zastosowaniu. • Jak
użyć
kolekcji do przechowywania danych punktu krzywej.
• Jak
użyć
kolekcji do przechowywania danych dokumentu.
• Jak
zaimplementow ać
rysowanie dokumentu.
• Jak
zaimplementować
przewijanie w widoku .
• Jak
utwo rzyć
• Jak
podświetlić
menu kontekstowe przy kursorze. element
znajdujący się najbliżej
użytkownikowi informację zwrotną
• Jak
zaprogramować
kursora, aby dostarczyć podczas przenoszenia i usuwania elementów.
mysz do przemieszczania i usuwania kształtów.
Czym są klasy kolekcji Programując w systemie Windows, często musimy obsługiwać kolekcje danych, nie znając z góry liczby elementów ani nawet ich typu. Dobrze to widać na przykładzie programu Ske tcher. Użytkownik może narysować w wybranej kolejności dowolną liczbę elementów,
812
Visual C++ 2005. Od podstaw o gących być liniami , prostokątami , okręgami i krzywymi. MFC dostarcza grupę klas kolekcji zaprojektowanych do obsługi takiego problemu - kolekcj a j est tutaj zbiorem dowolnej liczby dany ch zorgan izow anych w okre śl ony sposó b.
Typy kolekcii MF C dostarcza sporo klas kolekcj i do zarządzania danymi. W praktyce będziesz wykorzy s ty w a ł tylko kilka z nich , jednak dobrze jest znać także inne dostępne typy kolekcj i. MFC obsługuje trzy rodzaje kolekcji, różniące się sposobem organizacji danych. Sposób organizacji kolek cji nazywany jest j ej k s ztałtem . Istnieją trzy typy organizacji lub ks ztałtu : Kształt
Sposób organizacji informacji
Tabli ca
Ta blica w tym
ko n te kś cie
j est tym samym, z czym spotkałe ś s ię w j ę zy ku C++. Jest to elementów, z którego każdy element może zo stać pobrany z wykorzystaniem indeksu będ ące g o li czb ą c ałko wit ą. T ablica mo że automatyczn ie ro sn ąć , aby b ył a w stanie pomie ś ci ć wię c ej elemen tów, jednak zaleca s ię stosow anie poz o stały ch typów kolekcji, p oniewa ż kolekcje tablicowe mo g ą b yć powol ne w dzi a ła ni u .
upo rządkowany układ
Lista
danych , gdzie z każd ym elementem p owi ąz an e są dwa do popr zedniego i następnego elementu na li ście . W rozdzia le 7., gdy omawi ałem struk tury, pozna łeś li s t ę po wi ązaną. Omawian y tut aj typ listy stanowi l i s tę podwójnie p ow i ąz an ą, p on ie w a ż posiada łąc za wskazujące zarówno w tył , j ak i w przód . M oże być przeszukiwana w obu kierunkach i, podobnie jak tablica, kolek cj a listy w miarę potr zeby powięks za s ię. Lista jest prosta w u ży ciu i m ożna szybko do niej dod awać elementy. Jeż eli je dnak na liście znajd uje s ię wiele elementów , ich wys zukiwanie może być pow olne . Lista je st
kolek cj ą up orządk o w an y ch
wska źniki,
Mapa
które
w s kaz uj ą
Mapa stanowi n i euporządkowan ą ko le kcję danych, gdzie każd e m u elementowi przyporządkow an y j est kluc z, u ż ywan y do odzys kiwania elementu z map y. Klucz jest przeważnie łańcuchem, lecz może być t akże li czbą lub dowolnym typem obiektu. Mapy są szybkie zarówno przy zapisyw aniu dan ych , jak i przy ich wyszukiwa niu, p oniew a ż kluc z daje bezpośredni dostęp do ż ąda neg o elementu . To brzmi , jakby mapy były zawsze najlepszym wyborem, i często tak jest, jednak przy sekwencyjnym d ost ępi e tablice są szybsz e. Pojawia s i ę r ównież problem z wyborem klucza dla obiektu, poni e waż d la ka żde go elementu musi on być unikalny. Kłasy kolekcji MFC umożliwi aj ą zaim plementowanie wszystkich typów kolekcji na dwa różn e sposoby. Jeden z nich bazuje na wykorzystaniu szablonów klas i zapewnia kontrolę typów danych w kolekcj i. Kontrola typów oznacza, że typ danych przesyłanych do funk cji s kła do wej klasy kolekcji jest sprawdzany pod kątem możliwości przetworzenia danych tego typu przez funkcj ę.
Drugi sposób wykorzystuje szereg klas kolekcji (a nie szablonów ), ale nie zapewnia to kontroli danych. Jeżeli chcesz, aby klasy kolekcji posiadały kontro lę typów , musisz im to z ap ewnić samodzielnie. Te drugie klasy były dost ępne w starszyc h wersjach Visual C++ dla Windo wsa, natomiast nie były dostępne szablony klas kolekcji . Skupi ę s ię na wersj ach opartych na szablo nach, ponieważ łatwiej dzięki nim un ikn ąć błęd ów w aplikacj i.
Rozdział 15.•
Tworzenie dokumentu i poprawianie widoku
813
Klasy kolekcji z kontrolą typÓW Klasy kolekcji z kontrol ą typów oparte na szablonach obsługują kolekcj e obiektów dowolnego typu oraz kolekcje wskaźników do obiektów dowolnego typu. Kolekcje obiektów są obsługi wane przez wzorce klas CArray, CLi st i C1~ a p , natomiast kolekcje wskaźników do obiektów s ą obsługiwane przez szablony klas CTypedPtrArray , CTypedPt rL1St i CTypedPtrMap. Nie będ ę omawiał ich wszystkich, a jedynie dwie, z których będziemy korzystali w programie Sketcher. Jedną wykorzystamy do przechowywania obiektów, a drugą do przechowywania wskaźników do obiektów, więc będziesz miał pojęcie o obydwu rodzajach kolekcji .
Kolekcje obiektów Wszystkie klasy szablonów dla definiowania obiektów są wyprowadzane z klasy MFC COb ject . Są definiowane w taki sposób, że dziedziczą właściwości klasy CObj ect , które często się przydają. Należą do nich operacje wejścia i wyjścia dla obiektów, nazywane ogólnie seriali zacją, o której dowi esz się więcej w rozdziale 17. Te klasy szablonów mogą przechowywać i obsługiwać obiekty dowolnego rodzaju , włącznie z podstawowymi typami danych w C++, a także wszelkimi klasami i strukturami, jakie Ty lub ktokolwiek inny mógłby zdefiniować . Ponieważ klasy te przechowują obiekty, za każdym razem , gdy dodasz element do listy, tablicy lub mapy, obiekt klasy szablonu musi utworzyć kopię Twojego obiektu . Również dowolny typ klasy, który chcesz przechować w którejkol wiek z tych kolekcji , musi mieć kopię konstruktora. Kopia konstruktora dla klasy jest używana do tworzenia duplikatu obiektu, który ma zostać przechowany w kolekcji. poszczególnych klas szablonów, które zapewni aj ą bezpieczne To nie jest wyczerpujące omówienie wszystkich dostarc zonych funk cji składowych . Ma to raczej przybliżyć Ci ich sposób d ziałania, abyś mógł podjąć decyzję, czy z nich korzystać . Informacj e o wszys tkich funkcjach składowych znajd ziesz w systemie pomocy. Przyjrzyj
się właściwościom
zarządzanie obiektami.
Klasa szablonu Clrray Tego szablonu możesz użyć do przechowywania dowolnego rodzaju obiektu w tablicy, która będzie automatycznie się powiększała, aby w razie potrzeby pomieścić więcej elementów . Tablica jest przedstawiona na rysunku 15.1. Podobnie jak przy tablicach, które poznałeś w natywnym C++, elementy w kolekcjach tabli cowych są indeksowane od O. Deklaracja kolekcji tablicowej przyjmuje dwa argumenty. Pierw szy argument jest typem obiektu, który ma zo stać przechowany, więc jeżeli tablica ma przechować obiekty typu CPo int , należy jako pierwszy argument podać CPoi nt . Drugi argu mentjest typem używanym w wywołaniach funkcji składowej . Aby zapobiec opóźn ieniom w kopiowaniu obiektów przy przesyłaniu wartości, drugi argument jest przeważnie referencją, więc na przykład deklaracja kolekcji tablicowej przechowującej obiekty CPoi nt wygląda na stępuj ąco:
CArray Point Arr ay:
814
Visual C++ 2005. Od podstaw
Rvsunek 15.1 Kolekcja tabl icy: CArray anArray
~
Typobiektu. który / . ma zosta ć zapisa ny
Typ argumentu, który .ma zosta ć uż yt y
Indeks
~
Zwracany obie kt
GetAt(2) --- Oty mi nd eksie /
o
Objecll
1
Obj e ct 2
2--- 3
Obje ct4
4
Objec t5
S ~ Zwracany indek s
-----@
~n~J e.::t
~
- - - - - Zapisuje obiekt
.
.
Defini uje począt kowy
rozmiar
Obje ct6 L
.
Add (AnObject)
SetSize(5)
Obje c t3
--.! Automat yczne p ow i ększan i e
To definiuje obiekt klasy kolekcji tablicowej - Poi ntArray, która przechowuje obiekty CPoi nt. Gdy wywołujemy funk cj ę składową tego obiektu, argumentem jest referencja, więc aby dod ać obiekt Cpoi nt , napi szem y: .
Point Array. Add(aPoint ) ; Argument aPoi nt zostan ie p rzesłany j ako refere ncja. Po zadeklarowaniu tablicy n ależy wyw oła ć funkcj ę s kładow ą SetS ize() , aby u stal i ć poc ząt kową lic zbę e leme ntów tabl icy przed j ej u życi em . Jeżeli tego nie zrobisz, to i tak b ędzie działać, jednak p oczątkowa alokacja elementów i kolejne powi ęk szenia są niewielkie , co skutkuje niewydajnym działaniem i częs tą r eal oka cj ą pamięci dla tablicy. Początk ow a liczba elementów, jaką p owiniene ś o kre ś l i ć , zależy od przeciętnego rozmi aru tablicy, której będ ziesz potrzebował, i zmiennośc i tego rozmiaru. Je ż eli spodziewasz s i ę , że program będz i e minimal nie wymagał od 400 do 500 elementów, ale czasem będzie potrzeb ował 700 lub 800, odpo wiednim początkowym rozmi arem będzie 600. Aby
p obrać zawartość
zac h o w ać
aPoi nt Klasa
elementu, m ożesz użyć funkcji GetAt( l , co obrazuje rysunek 15.1. Aby trzec i element Poi ntArray w zmiennej aPoi nt, napiszemy:
~
Point Array.Get At (2) ;
prze c i ąża także
używaj ąc
opera tor [] , wi ęc m ógłbyś pobrać trzeci element tablicy Poi nt Array, Poi nt Array[2 ]. Je żeli na przykład zmienna aPoi nt byłaby typu CPoi nt, m ógłbyś
napisa ć :
aPoint = Poi ntArray[2]:
II Przechowaj
kopię
trzeciego elementu.
Dla kolekcji tablicowych niebędących const ta notacja może zos tać u żyta zamiast Set At( l do ustawi enia zawart ośc i i stniejącego elementu. Poni ższe dwie instrukcje są w ięc równ oznaczne:
PointArray.SetAt (3.NewPoint ); Poi nt Array[3] = NewPo int
II Przechowaj Newł'o in t j ako czwarty element. II To samo co powy żej.
Tutaj obiekt NewPoint j est tego samego typu , który został użyty do zadeklarowania tablicy. W obu przy padkach element musi j u ż i stnie ć . Nie możn a w ten sposób powi ększy ć tablicy. Do powi ęk szania tabl icy s łu ży, pokazana na schemacie, funkcja Add ( ), która dodaje nowy element do tablicy. Istnieje także funkcja Append () , która dodaje tablicę element ów na końcu tablic y.
Funkcje pomocnicze razem, gdy wywoływana jest funkcja składo wa SetSi zeO kolekcj i tablicy, wywo funkcja globalna ConstructEl ement s( ) w celu alokacji pami ęci dla początkowej liczby elementów, którą chcemy przechować w tablicy. Domyśln a wersj a tej funkcji ustawia zaw artość pami ęci na zero i nie wywołuje konstruktora klasy obiektu, wię c jeżeli takie dzia łanie tej funkcj i nie jest wystarczające dla Twoich obiektów, b ęd zi esz musiał utworzyć własną wersję tej fu nkcj i pomocniczej. Tak jest w przypadku, gdy przestrzeń dla danych składowych obiektów klasy jest alokowana dynamicznie lub gdy wymagan a je st inna inicj alizacja. Funk cj a ConstructEl ement () jest wywoływana również przez funkcj ę skł adową Inse rtAt() , która wstawi aj eden lub więcej elementów w konkretnym miejscu tablicy. Za
każdym
ływ ana j est też
S kł a d o w e kla sy kol ekcji CArra y, które usuwają elementy , wywołują funkcję pomocniczą uest ructt l ement s O , Domy ślna wersja nie robi nic, więc je żeli konstrukcja obiektu alokuje pamięć
na sterci e, nal eży
przesłoni ć tę funkcj ę,
aby
wła ściwi e z wo l n ić p amięć.
Szablon kolekcji CLi st korzysta z funkcji pomocniczej podczas wyszukiwania konkretnego elementu na li ści e. Omówię to s zczegół owo w na stępn ym podro zdziale. Kolejna funkcja pomocnicza, Seri al i zeElementsO, jest wykorzystywana przez klasy kolekcji tablic, list i map, a opiszę ją podczas omawiania zapisywania dokumentu do pliku .
Klasa szablonu CList Przyjrzyj s i ę dość dokładnie liście, ponieważ zastosujemy ją w programie Sketcher. Parametry dla klasy szablonu Cl,i st są takie same jak dla szablonu CArray:
CLis t aList; Gdy deklarujemy listę , musimy dostarczyć dwa argumenty do szablonu: typ obiektu, który ma przechowany, i sposób, w jaki obiekt ma być określany w argumentach funkcji. W przy kładzie drugi argument jest referencją, ponieważ jest to n ajczęściej używane rozwiązanie. Jednak nie musi tak być - można użyć wskaźnika, a nawet typu obiektu (obiekty byłyby więc przesyłane po wartościach ), jednak d z iałałoby to pow oli . zostać
Możemy użyć listy do obsługi krzywej w programie Sketcher. Li stę do przechowywania punk tów określających obiekt krzywej możemy zadeklarować za pomoc ą poniższej instrukcji:
CL is t Poi nt l i st ; Deklaruje ona listę o nazwie POl ntL i st , przech owując ą obiekty typu epoi nt, które są przesy do funkcji w klasie poprzez referencje. Powrócimy do tego, gdy uzupełnimy jeszcze inne s zcz egóły programu Sketcher.
łane
816
VislJal C++ 2005. Od podstaw
Dodawanie elementów do listy Elementy można dodawać na początku lub końcu listy za pomocą funkcji ad( ) i AddTai l () , co obrazuje rysunek 15.2.
składowych Ad d He ~
Kolekcja listy: CList aList
~
Typobiektu , który / ma zostać zapisany
Typarqurnentu, który ma zostać użyty
Automatyczne powiększanie
Rysunek 15.2 Rysunek 15.2 przedstawia wskaźniki wskazujące w obu kierunkach, które " s klej aj ą" obiekty w listę . Są one wewnętrznymi połączeniami i nie ma do nich bezpośredniego dostępu, ale za pomocą funkcji zapewnionych przez publiczny interfejs klasy możesz w zasadzie zrobić wszystko. Aby
dodać
obiekt aPoint na
Point Li st .AddTail( aPoint J; W
miarę
jak dodawane
końcu
listy Poi ntL i st, napiszemy:
II Dodaje element na
zostają nowe
końcu.
elementy, lista
się
automatycznie
powiększa.
Obydwie funkcje, AddHead( ) i AddTai l O , zwracają wartość typu POSITION, która określa pozy cję wstawionego obiektu na liście . Sposób , w jaki używana jest wartość typu POSITION, przed stawia rysunek 15.3. Wartości
typu POSITIONużywamy do pobrania obiektu z określonej pozycji na liście za pomocą funkcji Get Next() . Zauważ, że na wartościach typu POSITIONnie można przeprowadzać opera cji arytmetycznych - wartość pozycji można zmienić jedynie za pomocą funkcji składowych obiektu listy. Co więcej, nie można ustawi ć wartości pozycji na określoną wartość numeryczną. Zmienne POS ITION mogą być ustawiane jedynie przez funkcje składowe obiektu listy.
Rysunek 15.3 Kolekcja listy: CList aList Typ obiektu. który / ma zostać zapisany
,
Typ argumentu. który
.
ma zostać
użyty
Pozycja konkretnego elementu
Je st określana przez
wartość typu POSITION
: ~ Zwraca Objectl
,.. .L..., , ' IZWI'km .pGet Next (aPos) ->Oralti(pOC);
II Rysuj b ie żq cy element.
Ta implementacja funkcji OnOr aw( l zawsze rysuje wszystkie elementy, które zawiera dokument. Instrukcja w pętli whi l e najpierw pobiera wskaźnik do elementu z dokumentu za pomocą wyrażenia pOoc ->GetNext ( l. Zwrócony wskaźnik jest używany do wywołania funkcji Oraw ( l dla tego elementu. Instrukcja działa w ten sposób bez nawiasów dzięki temu, że operator -> kojarzy od lewej do prawej. Pętla wh i l e kroczy od początku do końca listy. Można to jednak zrobić lepiej i zwiększyć wydajność programu. Często,
gdy komunikat WM_PA INT zostanie przesłany do programu, jedynie część okna musi przerysowana. Gdy Windows wysyła komunikat WM_PAINT do okna, definiuje też obszar w obszarze klienta okna i tylko ten obszar musi zostać przerysowany. Klasa COC dostarcza funkcję składową RecV i s i bl e (l sprawdzającą, czy prostokąt przesłany jej jako argument nakłada się na obszar, który powinien zostać przerysowany. Możemy to wykorzystać , aby dopilnować, by rysowane były jedynie elementy w obszarze, który Windows chce przerysować, dzięki czemu nasza aplikacja będzie bardziej wydajna: zostać
vOld CSket eherV iew: :OnOraw( COC* pOC ) {
CSkete herOoe* pOoe = Get Ooeument () : ASSERT_VALI O(pOoe) ; if ( I pOoe) ret urn: POSITION aPos ~ Ooe ->GetLis t HeadPosit ion() ; CElement * pElement ~ O; II Zapisz dla wskaźnika do elementu. wh i le(aPos) II Pętla, dopó ki al'os niejest pusty. {
pEl ement
=
ouoc->GetNext (aPos ):
II Pobierz wskaźn ik do bi eżąc ego eleme ntu.
II Jeż eli element j est widoczny ...
i f(pDC->ReetV isi ble( pEl ement ->Get BoundReet( ))) pEl ement->Draltl(pDC) : 11 ...narysuj go .
Pobieramy pozycję pierwszego wpisu listy i przechowujemy ją w aPos. Używamy wartości aPos do kontrolowania pętli wh i l e, która pobiera poszczególne wpisy wskaźników, więc pętla kontynuuje działanie, dopóki aPos nie będzie miała wartości NULL . Pobieramy opisujący prostokąt dla każdego elementu za pomocą funkcji składowej obiektu Get BoundRect( l i przesyłamy go do funkcji Rect Vis i bl eO w instrukcji i f . W rezultacie rysowane są tylko elementy nakłada jące się na obszar, który Windows zidentyfikował jako niepoprawny. Rysowanie na ekranie jest stosunkowo czasochłonną operacją, więc sprawdzanie, które elementy mają zostać przerysowane - zamiast rysowania wszystkiego za każdym razem - znacznie poprawia wydajność programu .
836
Visual C++ 2005. Od podstaw
Dodawanie elementu do dokumentu Ostatnie, co musimy zro b ić , aby mieć w pełn i działający dokument w naszym programie, to dopisan ie kodu do procedury obsługi OnLButtonUp() w klas ie CSketcher Vi ew, który będzie d odawał tymczasowy element do dokumentu :
void CSket cherV iew : :OnLButt onUp(U INT nFlags . CPo i nt point ) (
if (t his == Get Ca pture()) ReleaseCapture( );
II Zako ńcz prz echwy tywanie komu nikatów z myszy.
II Jeżeli istniej e element. doda) go do dokumentu.
lf (m_pTempElement ) GetDocument ()->Add ElementC m_pTempElenent ) : Inval idateRect (O): II Przery suj b ieżące okno . m_pTempElement = O: II ZeI1l) wskaźn ik do elementu.
Oc z ywiś cie ,
przed dodaniem elem entu do dokumentu musimy sprawdzi ć , czy fakt ycznie on istni eje. Użytkownik mógł po prostu kliknąć lewym przyciskiem, nie przesu w ając przy tym myszy. Po dodaniu elementu do listy w dokumencie wywołujemy Inval i dat ekect O , aby obszar klienta bieżącego widoku zo s tał przerysowany. Argument o w artości O oznacza cały obszar klienta w widoku jako niepoprawny. W związku ze sposobem , w j aki dział a mechani zm rubber-band, niektóre elementy mogą by ć niepoprawnie w yświetl ane . J eżeli na przykład narysujesz poziom ą linię , a nast ępnie będ z ie s z rysow ał pro stokąt tego samego kolom, to w momen cie, gdy j ego dolna lub górna krawędź n ałoży s ię na linię , ten fragment linii zniknie. Dzieje się tak dlatego, że krawędź j est rysow ana w kolorze przeciwn ym do koloru elementu l eżącego poniż ej , więc ponownie uzyskujemy kolor tł a. Zerujemy także wskaźnik m_pTempEl ement , aby unikn ąć błędów , gdy utworzony zostanie kolejnyelement.
Testowanie dokumentu Po zapisaniu wszystkich zmienionych plików możesz utworzyć nową w ersj ę programu Sketcher i ją uru chomi ć . Będziesz w stanie wykona ć w nim rysunek taki jak ,,Zadowolony programista", przedstawion y na rysunku l 5.8. Działani e programu lepiej teraz odpow iada rzeczywistości . Przechowuj e w skaźnik do każdego elem entu w dokumencie obiektu , wi ęc elementy te są przerysowywane automatycznie, gdy zaj dzie taka potrzeba . Program dokonuje także poprawneg o czys zczenia danych dokum entu, gdy zostanie usunięty. Mo żn a jednak w ciąż wskaza ć
•
M ożna otworzyć
Windowo Ta i s t n i ej ąc e g o
szereg
ogran i c zeń
programu. Na przykład :
kolejne okno widoku, wybierając z menu programu WindowiNew jest wbudowana w ap l i k acj ę MOl i otwi era nowy widok dokumentu, a nie nowy dokument. Jednak w trakcie rysowania
możliwość
836
Visual C++ 2005. Od podstaw
Dodawanie elementu do dokumentlJ Ostatnie, co musimy zrobić, aby mieć w pełni działający dokument w naszym programie, to dopisanie kodu do pro cedury obsług i OnLButtonUpO w klasie CSketcher View, który będzi e d od awał tym czasowy element do dokum entu :
void CSketc herView: :OnLBut t onUp(UINT nFl ags . CPoi nt point) {
lf(t his == GetCa pture C)) Re leaseCa pt ureC );
II Zakoń cz prze chwytywa nie komunika /ów z myszy .
II Jeżeli istnieje element, dodaj go do dokumentu. l
f Cm_pTempEl ement l Get Oocument () ->AddElement Cm_pTempElement ); II Przerysuj bie ź qce okno. Inval idat eRect CO) ; m_pTem pE lement = O: II Zeruj wskaźn ik do elementu.
O czywi ście ,
przed dod aniem elementu do dokumentu mu sim y sprawd zi ć , czy faktycz nie on istnieje. Użytkownik m ógł po prostu kliknąć lewym przyciskiem, nie przesuw aj ąc przy tym my szy. Po dod aniu eleme ntu do listy w dokumencie wyw o ł uj e my Inval i dateRecU), aby obszar klienta bie żąceg o widoku z ostał przerysowany. Argument o w artośc i O oznacza cały obszar klienta w widoku jako niepoprawny. W związku ze sposobem, w jaki działa mechani zm rubber-band, niektóre elementy mogą być niepoprawnie wyśw ietl ane . Jeżeli na przykład narysujesz poziomą lini ę, a nast ępnie będ z ie sz rysow ał prostokąt tego samego koloru, to w momenci e, gdy jego dolna lub górna krawęd ź nałoży się na lini ę, ten fragment linii zniknie. Dzieje s i ę tak dlatego , że krawędź j est rysowana w kolorze przeci wn ym do koloru elementu l eżąceg o poni żej, więc ponownie uzyskujemy kolor tła . Zerujemy także wskaźnik m_pTernpE l ement , aby uniknąć błędów, gdy utworzony zostanie kolejny element.
Testowanie dokumentu Po zapisaniu wszystkich zmienionych plików możes z utworzyć nową wersję programu Sketcher i ją uruchomi ć . Będziesz w stanie wykonać w nim rysunek taki jak ,,Zadowolony programista", prz edstawiony na rysunku 15.8. Dział an i e
programu lepiej teraz odpowiada rzeczywi stości. Przechowuj e w skaźnik do każdego elementu w dokumencie obiektu, wię c elementy te są przerysowywane automatycznie, gdy zajdzie taka potrz eba. Program dokonuje także poprawnego czyszczenia danych dokum entu, gdy zostanie usunięty. Możnajednak wc iąż wskazać
•
Można otworzyć
szereg
o g ran i cze ń
programu. Na
przykład:
kolejne okno widoku, wybierając z menu programu Windo w/N ew Window oTa możliwo ś ć jest wbud owana w ap l i k ację MDl i otwiera nowy widok i stniejąceg o dokumentu, a nie no wy dokument. Jednak w trakcie rysowania
Rozdział 15.•
Tworzenie dokumentu i poprawianie widoku
837
Rysunek 15.8 w jednym oknie elem enty nie są rysowane w drugim oknie. Elementy nigdy nie pojawi ają się w oknie innym niż tym , w którym zostały utworzone , chyba że zajmowany przez nie obszar musi zost a ć zj akie go ś powodu przerysowany. •
Możes z rysować
tylko w wido cznym obszarze klienta . Byłoby miło, gdyby wid oku i rysowania na wi ęks zym obszarze.
istniała
mo żliwość przewinięcia
•
W żaden spos ób nie musisz albo to jako ś
To są całkiem
można usunąć z n i e ś ć,
albo
elementu, więc je żeli p opełnis z b ł ąd , od nowa , tworząc nowy dokument.
ro zp ocząć
poważne
mało użyteczny.
braki, które zebrane razem sprawiają, że program w takiej postaci jest Na prawimy j e wszystkie jeszcze w tym rozdziale.
Poprawianie widoku Pierwszym problemem, z ja kim si ę zmierzymy, będzie uaktualnianie wszystkich okien dokumentu podczas rys owania elementu. Problem powstaje, ponieważ jedynie widok, w którym element je st rysow any , wie o nowym elem encie . Każdy widok d ziała niezależnie od inny ch i nie ma między nimi komunikacji. Musimy sprawić , aby k ażdy widok, który doda element do dokumentu, informował o tym p ozostałe widoki , tak aby i one mogły podjąć odpowiednie d z iałan i a.
Uaktualnianie wielokrotnych widoków Klasa dokumentu zawiera funkcję Upda t eA 11Vi ews O, co stanowi nieocenioną pomoc pr zy rozwiązywaniu tego problemu. D z ięki tej funkc ji dokument m o że przesł a ć komunikat do wszystki ch swoi ch widoków. Mus imy jąjedynie wywołać z funkcji OnLBut to nUp() w klasie CSketc her Vi ew po każdym dodaniu nowe go elementu do dokumentu :
838
Visual C++ 2005. Od podstaw void CSket cherView: :OnLButt onUp(UINT nFlags. CPoint poi nt ) {
if( this == GetCapture() ) Re leaseCapture() ;
II Przerwij przechwytywanie komunikatów myszy.
II Jeżeli istnieje element, dodaj go do dokumentu.
if (m_pTempElement ) {
GetOocument( )->AddE lement (m Tem El ement ); GetOocument ( )->Upd at eA11 Vi ews (O .O.mpTempE lement) ; II Powiadom wszystkie widoki . m_pTempEl eme nt = O; II Zeruj wskaźn ik do elementu.
Jeżeli
m_pTempEl ement nie jest NULL, działanie funkcji jest rozszerzane, aby wywoływana była funkcja składowa klasy dokumentu IJpdat eA11 Views(). Funkcja ta komunikuje się z widokami, powodując wywołanie funkcji składowej OnUpdat e( ) każdego widoku . Trzy argumenty funkcji Upd ateA11 Views ( ) zostały opisane na rysunku 15.9.
Ten argum ent j est do bieżącego widoku . Pow strzymuje on wywołanie funkcji składowej OnUpd ateOwido ku. wskażnikiem
LPARAMjest 32-bitowym typem w systemie Windows i może zostać użyty do przesIania informacji o obszarze, który ma zo stać uaktualni ony w obszarze klient a
void UpdateAlIView( CView* pSender, LPARAM IHint
Ten argum ent jest w s kaźnikiem
do ob iektu mogącego do starczyć informacji o fragm encie obszaru, który ma zostać zaktualizowany w obszarze klienta
=OL, CObject* pHint =NULL) ;
\"
j
Wartości tych dwóch argumentów są przesyłane do funkcji OnUpdateO
w widokach
Rysunek 15.9 Pierwszym argumentem funkcj i UpdateA11 Vi ews() jest często wskaźnik th i s dla bieżącego widoku. Zapobiega to wywołaniu funkcji OnUpdateC) dla bieżącego widoku i jest przydatne , gdy bieżący widok jest już aktualny . W przypadku programu Sketcher, ponieważ korzystamy z metody rubber-banding, chcemy, aby bieżący widok również został przerysowany. Podając jako pierwszy argument 0, sprawiamy, że funkcja OnUpdate() zostanie wywołana dla wszystkich widoków, włącznie z bieżącym. To sprawia, że wywołanie Inva1 id ateRectC ) staje się niepotrzebne.
Nie używamy tu drugiego argumentu Updat eA11Vi ews (l , jednak przesyłamy wskaźnik do nowego elementu poprzez trzeci argument. Przesłanie wskaźnika do nowego elementu pozwala widokom określi ć, który fragment ich obszaru klienta wymaga przerysowania. Aby przechwyci ć informacje przesłane do funkcji UpdateA11Views( l, dodamy funkcję składową On Update ( l do klasy widoku. Możesz to zrobić za pomocą kreatora klas, korzystając z właści wości klas CSketc herView. Jestem pewien, że pamiętasz , i ż właściwości klasy można wyświe tlić , klikając prawym przyciskiem myszy nazwę klasy i wybierając Properties z menu kontekstowego. Jeżeli klikn iesz przycisk Overrides w oknie Properties, będziesz mógł wyszukać OnUpdate na liście funkcji możliwych do nadpisania. Kliknij nazwę funkcji, a następnie wybierz z menu rozwijanego w s ąsi edniej kolumnie OnUpdate. Gdy zamkniesz okno Prop er-
i II Jeżeli laki istnieje , unieważn ij obszar odpo wiadają cy wskazywanemu ob iektowi. II w p rzeciwnym razie unieważn ij cały obszar klienta . i HpHi nt)
InvalidateRect ( (CElement*) pHint)->Get BoundRect () ) ; el se Inval idat eRect (O ) ; Zauważ, że
musisz usunąć komentarze przy nazwach parametrów w wygenerowanej wersji funkcji. W przeciwnym przypadku nie skompiluje się ona z dodatkowym kodem. Trzy argumenty przesyłane do funkcji OnUpdat e( l w klasie widoku odpowiadają argumentom przesyła nym w wywołan iu funkcji UpdateAll Views( ). A zatem pHin t zawiera adres nowego elementu. Nie możemy jedn ak zakładać , że tak jest zawsze. Funkcja OnUpdate ( l jest wywoływana także przy pierwszym utworzeniu widoku, jednak ze wskaźnikiem NULL jako trzecim argumentem. A więc funk cja sprawdza, czy wskaźnik pHi nt nie ma wartości NULL i tylko wtedy pobiera opisujący prostokąt dla elementu przesłanego jako trzeci argument. Unieważnia ten obszar w obszarze klienta widoku poprzez przesłanie pro stokąta do funkcji I nval i dateRect () . Ten obszar jest przerysowywany przez funkcję OnDraw w tym widoku, gdy zostanie do tego widoku przesłany kol ejny komunikat WM_PAI NT. Jeżeli wskaźnik pHi nt ma wartość NULL, cały obszar klienta jest unieważniany. Przerysowywanie nowego elementu przez funkcję OnUpdat e( l może być kuszące, jednak nie jest to dobry pomysł. W odpowiedzi na komunikat WM_PAI NT powinny powstawać jed ynie trwałe rysunki. Oznacza to, że funkcja OnDraw( l w widoku powinna być jedynie miejscem rozpoczynającym jaki ekolwiek operacje rysowania dla danych dokumentu. To zapewnia, że widok zostanie narysowany poprawnie za każdym razem, gdy Windows uzna, że trzeba rysować. Jeżeli przebudujesz i uruchomisz teraz program Sketcher z nowymi modyfikacjami, wszystkie widoki pow inny zostać uaktualnione.
840
Visual C++ 2005. Od podstaw
Przewijanie widoków Na pierwszy rzut oka dodawanie przewijania do widoków wygląda prosto. C h o c i aż woda jest w rzeczywi stości głę bsza i bardziej mętna, niżby s ię to wydawało , to jednak do niej wskoczymy. Pierwszym krokiem je st zmiana klasy bazowej eSket eherView z Cvt ew na eSera1lView. Ta nowa klasa bazowa posiada wbudowan ą funkcję przewijania, więc możemy zmieni ć definicję klasy na:
class CSket cherView
pub ll C CSc rol lV iew
II Reszta defi nicji bez zmi an.
Musimy także zmie n i ć dwa wiersze kodu na początku pliku SketcherView.cpp, które odwołują się do klasy bazowej eSketeherView. Jako klasę bazową musimy podać eSe ra 11Vi ew zamiast eView:
IMPLEMENT_DYNCREATE(CSket cherView , CScrol lView) BEGINMESSAGE MAP (CSket cherVi ew, CSc rollView) Jednak to wciąż nie wystarczy. Nowa wersja kla sy widoku musi coś wiedzieć o obszarze, w którym rysujemy, na przykład znać jego rozmiar i to, jak daleko widok został przewin ięty za pomocą paska przewijania . Te informacje muszą zostać dostarczone przed pierwszym narysowaniem widoku . Możemy wstawić odpowiedni kod do funkcji On l nitia 1UpdateC) w klasie widoku. Wymagane informacje możemy dostarczyć , korzystając z funkcji SetSerallSizesC) dziedziczonej z klasy eSerall Vi ew. Argumenty tej funkcj i s ą objaśnione na rysunku 15.10. Przewinięcie o jeden wiersz następuje po kliknięciu strzałki w górę lub w dół na pasku przewijania. Przew in ięcie o jedną stronę ma miejsc e po kliknięciu samego paska przew ijania. Mamy tu możliwo ść zmiany trybu mapowania. MM_LOENGLI SH będzie dobrym wyborem dla aplikacji Sketch er, jednak najpierw uruchomimy przewijanie dla trybu mapowania MM_TEXT, pon ieważ wciąż czekają nas pewne problemy.
Aby dodać kod wywołujący funkcję SetScr a11Sizes( ), musimy nadpisać domyślną wersję funkcji Onlniti alupdat et ) w widoku. Możemy to zrobić w ten sam sposób jak przy nadpisywaniu funkcji OnUpda t e() - za pomocą okna Properties klasy eS kete her Vi ew. Po dodaniu nadpi sania po prostu dopisz kod do funkcji w miejscu oznaczonym komentarzem:
void CSket cherV iew : :Onlnit ialUpdate( ) {
CScrol lView : .Onl ntt ta l tlpdat et ): II Definiuj e rozmiar dokum entu.
CS ize OocSize(20000.20000): II Ustawia tryb mapowania i rozmi ar dokum entu.
To d efiniuje odległo ść w poziomie (ex) i w pio nie (cy), o j ak ą przewinąć stronę . Może być zdefi niowane jako : CSize Page(cx. cy); Do m yś l n ą wart o ścią jest 1/10 całego ob szaru
841
To definiuje odległ o ść w poziomie (ex) i w pionie (cy), o j a k ą przew inąć l in ię .
M oże być zdefiniow ane jako: CSize Line(cx, cy); Domyś Iną wa rtośc i ą j est 1/10 calego obszaru
void SetScrollSizes( int MapMode, SilE Total, const SllE& Page = sizeDefault, const SllE& line = sizeDefault );
M oże być jedn ą z p on iż szych : MM_TWIPS MM_Text MM_HEINGLlSH MM_LOENGLlSH MM_HIMETRIC MM _LOMETRIC
To jes t całkow ity obszar ry sowan ia i m oż e być zdefi niow ane jako : CSize Total(cx,cy); gdzie ex jest wym iarem w pozi omie , a cy w pionie wyrażonym w jednostkach log icznyc h
Rysunek 15.10 To pozostawia tryb mapowania MM_TEXT i umożl iw ia rysowanie na obszarze 20 000 na 20 000 pikseli . Te zm iany s ą wystarczające do zadziałania mechanizmu przewijania. Przebuduj program i uruchom go. Narysuj kilka elementów, a następnie przewiń widok. Mimo że okno przewija się poprawnie, gdy spróbujesz nary sować więcej elementów w przewin iętym obszarze, nie wszystko będzie dobrze działało. Elementy pojawiają się w innym miejscu, ni ż były rysowane, i nie są wyświetlane poprawnie. Co się dzieje?
Współrzędne logiczne i współrzędne klienta Problem jest związany z używanymi układami współrzędnych - a liczba mnoga jest tu zastosowana celowo. Do tej pory we wszystkich przykładach używaliśmy dwóch układów współ rzędnych, choć fakt ten mógł być n iezauważony . Jak widziałeś w poprzednim rozdziale, przy wywołaniu funkcji takiej jak Li neTo( ) zakłada ona, że prze słane argumenty s ą współrzędnymi logicznymi. Funkcja ta jest składową klasy COC, która definiuje kontekst urządzenia, a kontekst urządzenia ma własny układ współrzędnych logicznych. Tryb mapowania, będący właściwością kontekstu urządzenia , określa jednostki układu współrzędnych używane podczas rysowania. Z drugiej strony współrzędne otrzymywane wraz z komunikatami myszy nie mają nic wspólnego z kontekstem urządzenia lub obiektem CDC, a poza kontekstem urządzenia współrzędne
842
Visual C++ 2005. Od podslaw do OnLButtonDown () i OnMouseMove( ) mają zawsze w jednostkach urządzenia, czyli pikselach, i są mierzone względem lewego górnego rogu obszaru klienta. Są one nazywane współrzędnymi klienta. Podobnie, gdy wywołujes z I nva l i dat eRect( ) , zakłada się , że prostokąt jest zdefiniowany z użyciem współrzędnych klienta. logiczne nie
mają zastosowania. Punkty przesyłane
współrzędne wyrażone
W trybie MM_TE XT w s p ó ł rzę d ne klienta i współrzędn e logiczne w kontekście urządzenia są w pikselach, więc po zostają te same, dopóki nie przewiniesz okna. We wszystkich poprzednich przykładach nie przewijaliśmy okna, więc wszystko działało bezproblemowo. W ostatniej wersji programu Sketcher wszystko działa poprawnie do momentu przewini ęc ia widoku, kiedy to logiczne współrzędne początku układu współrzędnych (punkt O, O) zostają przeniesione przez mechanizm przewij ania, w związku z czym nie pokrywają si ę już one ze współrzędnymi początku układu współrzędnych klienta. Jednostki dla współrzędnych logicznych i współrzędnych klienta są takie same, ale początki dwóch układów współrzędnych już nie. Ta sytuacja została przedstawiona na rysunku 15.11. wyrażo n e
Rysowanie w nieprzewiniętym oknie W jednostkach logicznych
We współrzędnych klienta linia rysowana jest tutaj
We współrzędnych logicznych linia pojawia się tutaj
Rysowanie w przewiniętym oknie W jednostkach logicznych
0,0 x 4
Widok zastal przewinięty, więc początek układu znajduje się teraz tutaj _____ ~ SuttlWl" N aciśnięcie
lewego przycisku
~
Zwolnienie lewegoprzycisku
.' JJ We współrzędnych klienta linia rysowana jest tutaj
l_~L:tEJ
----..: ..~
We współrzędnych logicznych linia pojawia się tutaj
Rysunek 15.11 Lewa strona pokazuje pozycj ę w obszarze klienta , w której rysujesz, i punkty będące pozycjami myszy podczas definiowania linii. Są one zapisywane z użyciem współrzędnych klienta. Prawa strona pokazuje, gdzie linia jest naprawdę rysowana . Rysowanie jest dokonywane we współrzędn ych logicznych, ale z użyciem wartości współrzędnych klienta. W przypadku
Oznacza to, że używamy złych wartości do definiowania elementów w programie Sketcher, a gdy unieważniamy fragmenty obszaru klienta, aby je przerysować , przesyłane prostokąty także są nieprawidłowe - stąd bierze się dziwne zachowanie programu . Przy innych trybach mapowania jest jeszcze gorzej, ponieważ nie tylko jednostki dla współrzędnych są różne , ale też osie y mają przeciwne kierunki!
Jak poradzić sobie ze wspólrzędnymi klienta Zastanówmy się , co musimy musimy się zająć :
zrobić,
aby
rozwiązać
ten problem.
Są
dwie sprawy, którymi
•
Musimy przekształcić współrzędne klienta otrzymane z komunikatami myszy na logiczne współrzędne, zanim użyjemy ich do tworzenia elementów.
•
Musimy
przekształcić opisujący prostokąt utworzony
współrzędnych z wywołania
powrotem na współrzędne klienta, Inval i dat eReet( ).
z użyciem logicznych chcemy go użyć do
jeżeli
to do dopilnowania, aby korzystając z funkcji kontekstu urządzenia, zawsze logiczne, a dla innej komunikacji z oknem używać współrzędnych klienta. Funkcje, dzięki którym uzyskamy przekształcenia, są powiązane z kontekstem urzą dzenia, więc musimy uzyskać kontekst urządzenia za każdym razem , gdy będziemy chcieli przekształcać logiczne współrzędne na współrzędne klienta lub odwrotnie . Możemy do tego użyć funkcji konwersji współrzędnych klasy CDC, które są dziedziczone przez CCl ientDC. Sprowadza
się
stosować współrzędne
Nowa wer sja procedury OnLButtonDown()
będzie wyglądała następująco:
vO ld CSket cherV iew: :OnLBut tonOown(UINT nF lags. CPoi nt poi nt ) I CC li entOC aOC (t his ) : II Tworzy kontekst urządzenia .
On Prepa reOC( &aOC): II Popra wia początek układu wsp ółrzędnych.
aOC.OPtoLP (&point) ; II Konw ertuje punki na układ logiczny. m_Fl rst Po lnt ~ pOlnt : II Zapisz pozycję kursora. II Prz echwytuj kolejne komunikaty z mys zy. SetCa pt ure() ; }
Poprzez utworzenie obiektu CCl i entDC i przesłanie wskaźnika thi s do konstruktora uzyskujemy kontekst urządzenia dla bieżącego widoku . Zaletą CCl i entOCjest fakt, że automatycznie zwalnia kontekst urządzenia, jeżeli obiekt wyjdzie poza zakres. Ważne jest, aby nie przetrzymywać kontekstów urządzeń, ponieważ w systemie Windows jest ich ograniczona liczba i może ich po prostu zabraknąć . Jeżeli korzystasz z CCl i entDC, nie ma takiego zagrożenia. Ponieważ wykorzystujemy CSero11 Vi ew, funkcja składowa OnPr epareOCO dziedziczona z tej klasy musi zostać wywołana do ustawienia początku logicznego układu współrzędnych w kontekście urządzenia zgodnie z przewiniętą pozycją. Po ustawieniu początku układu korzystamy z funkcji OPtoLP( ), która zamienia punkty urządzenia na punkty logiczne (ang. Device Points to Logical Points), aby przekształcić wartość point przesyłaną do procedury na współrzędne logiczne. Następnie zachowujemy przekształcony punkt jako gotowy do tworzenia elementu w procedurze OnMouseMove().
844
Visual C++ 2005. Od podstaw Nowy kod dla procedury OnMou seMove ()
wyg ląd a n astęp ująco :
void CSketcherView: :OnMouseMove( Ul NT nFlags. CPoi nt point) ( II Definiuje obiekt kontekstu urządzenia dla widoku . CCl ientOC aDC(this): II Kontekst urządz e nia dla te o widoku. OnP repareDC( &aDC): II Popraw początek układu współrzędnych.
II Konwer tuj pu nkt na układ log iczny . II Zapisz bieżącą pozycję kursora.
II Reszta funkcji bez zmian...
Kod dla konwersji w artości punktu przesyła nego do procedury jest taki sam jak w poprzedniej procedurze i w tej chwili tyle nam wystarczy . Os tatn i ą funk cj ą, którą musimy zm i e n i ć , a co łatwo przeoczyć , jest funkcja On Update( ) w klasie widoku. Musi by ć ona zmieniona w nast ę puj ący sposó b:
void CSket cherView : :OnUpdat e(CView* pSender . LPARAMlHi nt . CObject* pHint) ( II Jeżeli taki istnieje. un ieważnij obszar odpo wiadający wskazywa nemu obiektowi. II w prz eciwnym razie unieważn ij cały obszar klien ta.
l f( oHint ) CCl lent DC aDC(t hi s): OnPrepa reDC( &aDC) ;
II Utwórz kontekst urządzenia . II Popraw początek układu współrzędnych.
II Pob ierz pros toką t opis ujący i konwertuj go na
Wprow adzona tu zmiana tworzy obiekt CC l i entOC i u żywa funkcji składowej LPt oO P( ) do konwersji pro st okąta obszaru, który ma zos tać przerysowany na współrzędn e klienta. Je żeli
teraz skompilujesz oraz uruchomisz program Sketcher z wprowadzonymi modyfikacjami i udało Ci s ię nie p op ełni ć żadnych literówek, będzi e on dział ał poprawnie nieza leż nie od przewini ę c i a widoku.
Korzys1anie ZIrybu mapowania MM_LDENGLl8H Zastanówmy s ię teraz, co musimy zrobić, by zacząć korzystać z trybu mapowania MM_L OENGLI SH. Umożliwia on rysowanie w jednostkach logiczn ych 0,01 cala, a także zapewnia, że rozmiar rysowania jest spójny na wyśw ietlaczach o różnych rozdzi elczości ach . Z punktu widzenia użyt kownikajest to bardzo użyte czne .
Rozdział 15.•
Tworzenie dokumentu ipoprawianie widoku
845
Tryb mapowania możemy ustawić w wywołaniu SetScro l l Si zes ( l znajdującym się w funkcji Onl nitia lUpdat e () w klasie widoku. Musimy także określić całkow i ty rozmiar obszaru rysowania, wi ęc j eżeli okre ślimy go jako 3000 na 3000, będz i emy m ie li do dy spozycji obszar o rozm iarze 30 na 30 cali, co wydaje się odpowiednie. Domyślne od l egłośc i przewijania dla wiersza i strony są zadowalające, więc nie będzi emy ich dodatkow o określać. Możemy użyć panelu Class View, aby przejść do funkcji On l nit i al Updat e ( l , a n astępn ie zmienić ją w nastę pujący sposób:
void CSket eherVi ew : :OnInit 1alUpdat e() (
CSero11 Vlew: :OnInit ialUpdat e() : II Zdefiniuj rozmiar doku mentu jako 30 na 30 cali w trybie MM_LOENGLlSH.
CS ize OoeSize(3000.3000 ): II Ustaw tryb mapowania i rozmi ar dokumentu.
SetSerol lSizes(MMLOENGLISH. OoeS ize): Zmieniamy jedynie argumenty w wywołaniu SetScrollS i zes() na żądany tryb mapowania i rozmi ar dokumentu . To wszystko, co nale ży zrobić , aby um o żliwić pracę w trybie MM_LOENG LISH, j edna k wciąż musimy poprawić sposób obsługi prostokątów . Zauważ, że
wyboru trybu mapowania nie ustala się raz na zawsze . M oże sz w dowolnym momencie zmienić tryb mapowania w kontekście urządzenia i rys ować różne czę ści wyświe tlanego obrazu, korzyst ając z różnych trybów mapowania. Służy do tego funkcja Set MapMode ( l , jednak ni e będę jej tu sz erzej o m aw i ał. Nasza aplikacja może d zi ałać , korzystając jedynie z trybu MM_LOENGLI SH. Za każdym razem, gdy tworzony jest obie k t CC l ientOC dla widoku i wywoływana jest funk cj a OnPrepareOC() , kontekst urządzenia, który go posiada, ma tryb mapowania ustawi ony przez funkcję Onl rr i t i al i zeUpdate ( ). Problem, który mamy z prostokątami , polega na tym, że klasy elementów zakładają, iż trybem mapowania jest WM_TEXT, a w MM_LOENGLI SH prostokąty są odwró cone , ponieważ odwrócona jest również oś y. Gdy dla prostokąta zostanie zastosowana LPt oOP( l , zakł ad a s ię , że jest on poprawnie położony, z uwzględnieniem os i trybu MM_LOENGLI SH. A jako że tak nie jest, funkcja odwraca pro stokąty w pionie. Stwarza to problem, gdy w celu uni eważni eni a obszaru widoku zostaje wywołana funkcja Inva l i dateRect O, ponieważ odwrócony prostokąt we współrzęd nych urządzeni a nie jest rozpoznawany przez system Windows jako leżący wewnątrz widocznego obszaru klienta. Możemy
to
rozwiązać
na dwa sposoby: poprzez zmodyfikowanie klasy elementów , aby prood razu wyrażane w MM_LOENGLISH, lub poprz ez re n o rm a lizacj ę prostokąta, który zamierzamy prze słać do funkcj i Inval i dateRect ( l . Drugi sposób jest najłatwiej szy , ponieważ należy tylko zmienić jedną składową klasy widoku - Onupdate t l: stokąty opisujące były
void CSk eteherVi ew: :OnUpdat e(CView* pSender . LPARAMlHint. CObJeet * pHint ) { II Jeż eli taki istn iej e, un ie ważn ij obszar odpowiadający wskazywanemu obiektowi, II w p rzeciwnym razie un ieważnij cały obszar klienta.
if (pHintl (
CC l ientOC aOC (t hi s): OnPrepareOC(&a DC ) :
II Utwórz kontekst urzqdzeni a. II Popraw początek układu współrzędnych.
846
Visual C++ 2005. 0(1 podstaw
II Pobierz prostokąt
op isujący
i kon wertuj go na współrzędne klienta.
CRect a Rect~ « C E l emen t *) pH in t ) - >G et Bo u n d Re c t( ) : aDC.LPt oDP(aRect) : aRect .No rmal izeRect () : II Przerysuj obszar. inval idat eRect (aRect ): }
else invalid at eRect (O );
II
Unieważnij
obszar klienta .
Takie rozwiązanie wystarczy dla naszego programu. Jeżeli przebudujesz pro gram Sketcher, przewijanie będzie działało dla wielokrotnych widok ów. Musisz pamiętać, aby renonnalizować wszystkie prostokąty konwertowane do współrzędnych urządzeni a dla użytku z funkcją Inva l i dateRect ( ). Ma to również wpływ na wszystkie konwersje w drugą stronę .
Usuwanie iprzesuwanie kształtów Możliwość
usuwania kształtów jest jedną z podstawowych cech programu do rysowania. Wi ą z tym pytani e, w jaki sposób będziemy wybi era ć element do u sunię cia . Oczywi ście , gdy zdecydujemy się na sposób wyb ierania elementu, będzie on również doty czył przesuwania elementów, wię c usuwanie i przesuwanie elementów możemy uznać za tematy powi ą zane . Najpierw j ednak zastanówmy się , w jaki sposó b dodamy operacje przesuwania i usuwani a elementów. że się
Zgrabn ym sposobem udostępnienia funkcji przemieszczania i usuwania byłoby dodan ie menu kontekstow ego wyśw i etlającego się przy kursorze po kliknięciu prawym przyciskiem myszy . Możemy wówczas do niego dodać Move (przenieś) i De/ele (u suń) . Takie menu kontek stowe jest bardzo przydatnym udog odnien iem i mo żesz je w ykorzystać w wielu różnych innych sytu acjach. Jak powinn o być u żywan e menu kontekstowe? Standardowo użytkownik przesuwa kursor nad wybrany obiekt i klika go prawym przyciskiem myszy. Obiekt zostaje zaznac zony i zostaj e wyświ etlone menu zaw i erające li st ę działań do stępnych dla tego obiektu . Oznacza to, że ró ż ne ob iekty mo gą mi eć różne menu . Może s z z ob aczyć to r ozwiązan i e w Developer Studio. Gdy kliknie sz prawym przyci skiem myszy ikon ę klasy w panelu C/ass View, pojawi s i ę menu inne od tego , które pojawi s i ę po klikni ęciu prawym przyciskiem ikony dla funkcj i składowej . Pojaw i aj ąc e s i ę menu j est zależne od kontekstu kurs ora i stąd właśnie wziął się termin "menu kontekstowe". W program ie Sketcher mamy dwa możli we menu kontekstowe. Mo żna kl ikn ąć prawym przyci skiem albo wtedy , gdy kursor znajduje s ię nad elementem, albo kiedy s ię tam nie znajduj e. w i ęc sposób można za i m p le me n to wać tak i zestaw funkcj i w pro gr amie Sketcher? to zro b ić , two rz ąc po prostu dwa menu : jedno dla sytuacji, gdy pod kur sorem znajduje się element, i drugie w przeciwnym przypadku. Gdy użytkow n ik wc iś n i e prawy przycisk myszy, m ożem y sp raw dzać , czy pod kursorem znajduje s i ę element. J e żeli tak, możemy podśw ie t l ić element, aby użytkownik dokładnie wiedz i ał , do którego elementu odnosi się menu kont ekstowe.
W j aki
Można
Rozdział 15.•
TWll'Zellie 110kllmenlll i poprawianie widoku
847
poznaj sposób, w jaki można utworzyć menu kontekstowe dla kursora, a gdy ono działało , powrócimy, aby zaimplementować szczegóły operacji przemieszczania i usuwania elementów.
Na
początek
będzie
Implementacja menu kontekstowego Pierwszym etapem jest utworzenie menu zawierającego dwie karty - jednej z elementami Move i Delete, a drugiej stanowiącej połączenie elementów z menu Element i Color . Przejdź do Resource View i rozwiń listę zasobów. Kliknij prawym przyciskiem folder Menu, aby otworzyć menu kontekstowe - to kolejny przykład tego , co staramy się utworzyć dla programu Sketcher. Wybierz Jnsert Menu, aby utworzyć nowe menu. Ma ono przypisany domyślny identyfikator IDR_MENU1, jednak możesz go zmienić. Wybierz nazwę nowego menu w panelu Resource View i wyświetl jego okno właściwości - w tym celu naciśnij Alt+Enter (to skrót do elementu menu View/Other Windows/Properties). Możesz teraz zmienić identyfikator zasobu w oknie Prop erties. Zmień go na bardziej odpowiedni, np. IDR_CURSOR_MENU. Zauważ, że nazwa zasobu menu musi rozpoczynać się od IDR. Naciśnij klawisz Enter, aby zapisać nową nazwę .
Utwórz teraz dwa nowe elementy paska menu w panelu Editor. Mogą one mieć dowolne stare podpisy, ponieważ i tak będą niewidoczne dla użytkownika. Reprezentują one dwa menu kontekstowe, które będą dostępne w programie Sketcher, więc nazwij je el ement i no el ement, zgodnie z sytuacją, w której będą używane. Teraz dodaj elementy Move i Delete do karty el ement. Domyślne identyfikatory, lO_ELEMENT~OVE i lO_ELEMENT_DELETE, są odpowiednie, lecz jeżeli chccszje zmienić, możesz to zrobić w oknie właściwości każdego z nich. Rysunek 15.12 obrazuje, jak wygląda nowe menu .
Rysunek 15.12
Sketcher.rc (.:.~MENU ~Meiluj·tL. no element ~ h';::'
element
L,!..,.,
J
~_~~_ .~
~~~· .2L
Move Delete
Drugie menu zawiera listę dostępnych typów elementów i kolorów, czyli elementów menu Element i Color z głównego paska menu, ale oddzielonych separatorem. Identyfikatory użyte dla tych elementów muszą być takie same jak w menu IDR_Sketch erTY PE. Jest tak dlatego, ponieważ procedura obsługi menu jest powiązana z identyfikatorem menu . Elementy menu z tym samym identyfikatorem używają tej samej procedury, więc ta sama procedura będzie użyta dla elementu Line, niezależnie od tego, czy zostanie wywołana z menu głównego, czy też z menu kontekstowego. Dostępny
jest skrót, dzięki któremu nie musimy po kolei tworzyć tych elementów menu. menu IDR_SketcherTYPE i rozwiń Element, a następnie wybierz wszystkie elementy menu poprzez kliknięcie pierwszego elementu, a następnie ostatniego, trzymając wciśnięty klawisz Shift . Potem kliknij zaznaczenie prawym przyciskiem myszy i wybierz Capy z menu Wyświetl
848
Vlsual C++ 2005. Od podstaw kontekstowego lub po prostu n a ci śnij Ctrl-sC. Przejdź do !DRJUR SOR_ME NU i kliknij prawym przyciskiem myszy pierwszy element w menu no element. Jeżeli wy bierzesz Paste z menu kontekstowego lub naciśniesz Ctr/ + V, zost ani e wstaw iona c ał a za w a rt ość menu Element. Skopiowane elementy menu b ędą miały takie same identyfikatory jak oryginały. Aby wstawić separator, kliknij prawym przyciskiem pusty element menu i wyb ierz Insert Separator z menu kontekstowego. Powtórz ten proces dla elementów menu Color . Umieszczenie razem elementów menu Element i Color spowodowało konflikt, ponieważ elementy Rectangl e i Red mają ten sam skr ót. Zmiana &Red na Re&d ro zw iąże ten problem, a dla spójności dobrze byłoby to zrobi ć również w menu IORSketcher TYPE. W tym celu należy zmienić właś ciwo ś ć Cap tion elementu menu . Ukończone menu powinno wyg l ąd ać j ak na rysunku 15.13 .
Rysunek 15.13
IV5ketch~r :rc (:, ::",~,~;.:;u=--~M:::=~ enU).·te,..........-.='--'-"_ _-"-'-'-'--"-'_-'--'.........~'-='-"- =- I element
C CIRCLE~~E lemen t Ty pe? M F _CH E C K E O : MF_UN C HE C KE D ) I M F_BYCOM M A NO) :
menu .CheckMenuIt emC IO_ELEMENT_CURVE. CCURVE==Element Type?MF_CHECKEO: MF_UNCHECKEO) IMF_BYCOMMAND): }
CMenu* pPopup = menu. GetSubMenuC m_pSelected == o ? l : O); ASSERTC pPopup ł= NULL) ; pPopup- >TrackPopupMenuCTPM_LEF TALIGN I TPM_RIGHT8UTTON. po i nt .x. point. y. t hlS) . Dzięki
tej zmianie opcj e menu kont ekstow ego powinny przebudujesz i uruchomi sz program Sketcher.
by ć
poprawn ie zaznaczone, gdy
Podświellanie elementów Najlep iej był oby, gdyby użytko wn i k w i edz iał , który element znajduje si ę pod kursorem, zanim kliknie prawym przyciskiem w celu wy świetlenia menu kontekstowego. Gdy usuwasz element, chcesz wiedz i eć, z którym elementem pracujesz. Podobnie, jeżeli chcesz użyć drugieg o menu kontekstowego - na przykład w celu zmiany koloru - musisz mi eć pewno ść , że pod kursorem nie ma ż a dnego elementu. Aby do kładni e pokazać , który element znaj duje s i ę pod kursorem, musisz go w jakiś sposób w yróżn i ć przed klik nię ci em prawym przyci skiem myszy. Mo żemy
to zro bić w funkcji s kłado wej Draw( ) dla elementu . Musimy jedynie przesła ć argument do funkcji Draw(), aby w skazać, kiedy element powinien zo stać p odś w i etlony. Jeżeli prześlemy do funkcji Oraw( ) adres aktualn ie wybranego elementu , który jest przechowywany w składowej m_pSel ect ed widoku, będz iemy mogli poró wn a ć go ze w skaźniki em t hi s, aby sp rawdz ić, czy je st to bi e ż ący element. Wszystkie podświetlenia działają w ten sam sposób , więc pr zedstawię je na przykładzi e skła dowej CLi ne. Będz ie sz mógł d odać podobn y kod do wszystkich klas dla innych typów elementów. Zanim zaczniesz zmieniać CLi ne, musisz najpierw z m i e n i ć definicję klasy bazowej CEle ment :
II Kolor elementu. II Prost o kąt opisujacy element. II Szerokos ć p ióra.
publi c: virt ua l -CElement () :
II Wirtualny destruktor.
II Wirtualna o eracia rysowa nia.
vi rt ua l vOid Oraw(COC* pOC.CElement* pElement =O) {} virtual voto Move(CSize&aS ize){} II Przenieś element. CRect Get8oundRect() ; prot ected: CElenent O: };
II Pob ierz prostoką t ograniczający element.
II Umieszczamy tu/aj , aby zapo biec wywa laniu .
856
VisualC++ 2005. 0[1 podstaw Zmiana polega na dodaniu drugiego parametru do funkcji wirtualnej Draw() . Jest to wskaźnik do elementu. Drugi argument inicjalizujemy z warto ścią O, pon ieważ chcem y p ozwolić na uży cie tej funkcji tylko z jednym argumentem; jako drugi argument domyślnie będzie podane O. W ten sam sposób musimy zmien i ć deklaracj ę funkcji Draw() w każd ej klasie wyprowadzanej z CEl ement. Na przykład definicję klas y CL i ne nale ży zmieni ć w następujący sposób:
cl ass Cli ne : publ ic CE lement publ ic:
-u.tner voi d) II Funkcj a
:
wyświetlająca lin ię .
vi rtual void Oraw(COC* pOC , CElement* pElement=O ) ; II Konstru ktor obiektu linii.
Cl lne(CPoint Start, CPoint End, COlORREF aColor): II Funk cja przen o sząca element.
vir tual vo id Move (CS ize&'a Size); protected: CPo int m_St artPoint ; CPoint m_End Point :
II Początek linii. II Koniec linii.
Cl ine(vo id):
II Domyślny konstruktor - nie powinien
być używany.
}:
Implementacje wszystkich funkcji Draw () dla klas wyprowadzanych z CE l ement rozbudowane w ten sam sposób. Funkcja dla klasy CLi ne wy gl ąda następująco :
void Cl ine: :Oraw( COC* pOC . CElement* pE lement) { II Tworzy pi óro dla tego elementu i II inicj alizuje je z obiektem koloru i
linią
o gru b oś ci l p iksela.
CPen aPen; COlORREF aColor = m_Color : II l nicj alizuj z kolorem elementu. if (t hi s == pElement) II Czy ten element jest wybrany? II Ustaw kolor wyróż nienia.
aColor = SElECT_COlOR: i f( faPen.CreatePen(PS SOLID. m Pen. aCo lor)) II Tworz enie pióra nie po wiodlo się.
zako ńcz
program.
AfxMessageBox( _T(" Pen creat ion fail ed drawing a l ine"). MB_OK): AfxAbo rt( ): CPen* pO ldPen II Teraz narysuj
~
oOC- >SelectObject(&aPen) :
II Wy bierz pi óro.
linię.
pOC ->MoveTo( m_St art Pol nt) ; pOC ->L ineTo(m_EndPoint) : pOC->Select Object (pOl dPen);
II Przywró ć star e p ióro.
mu szą być
Rozdział 15.
• Tworzenie dokumentu i poprawianie widoku
857
Jest to bardzo prosta zmiana. Ustawiamy nową zm ienną lokalną aCo l or na bie ż ący kolor przechowywany w m_Co lo r , natomiast instrukcja if usta wia ponownie warto ś ć aCo lor na SELECT_COLOR, gdy pEl ement jest równy th i s, czyli w przyp adku, gdy bi eżący element i wybran y element są tym samym . Musi sz także dodać definicj ę SELECT_COLORdo pliku Our tlonstants. h: // Definicje stalych.
#pragma ance // Defin icje typów elementów. // Wa rtość każ dego typu musi być
canst canst canst canst
unsi gned unsigned un signed unsigned
i nt i nt int int
różna .
LINE = lOlU ; RECTANGLE = l02U; CIRCLE = l 03U; CURVE ~ l04U ;
/////////////////////////////////// //
War tośc i
canst canst canst canst canst
kolorów rysow ania.
COLORREF BLACK ~ RGB (O .O.O ); COLORREF RED = RGB(255,O. O); COLORREF GREEN = RGB( O.255.0) ; COLORREF BLUE = RGB(O .O,255) ; COLORREF SELECT COLOR = RGB(255.0,180) ;
///////////////////////////////////
Musimy także dodać dyrektywę #i ncl ude dla pliku Ourtlon stants.h do pliku CE l ements .cpp, aby ud ostępnić definicję SELECT_COLOR. Prawie zaimplementowaliśmy podświetl anie . Klasy wyprowadz ane z klasy CEl ement mogą się teraz same rysować , gdy zostaną wybrane - potrzebujemy jedynie mechanizmu po wodującego wybran ie elementu. Gdzie powinniśmy go umieści ć? W procedurze OnMouseMove ( l klasy CSketch erVi ew okre ślamy, który i czy w ogóle ja kiś element znajduje się pod kursorem, jest to w i ę c z pewnością odpowiednie miejsce dla mech anizmu pod świetleni a . Do procedury Onr1ouseMove ()
należy wprowad zić następując e
zmiany;
vaid CSket cherView; ;OnMauseMave(UI NT nFlags . CPai nt paint ) ( // Definiuje ob iekt kontekst u
urządzen ia
CCl ient DC aDC( th i s); OnPrepareDC(&aDC) ;
d la widoku. // Kont ekst urządz enia dla tego widoku. // Ustaw tryb rysowania .
aOC.SetROP2 (R2_NOTXORPEN ); // Ustaw tryb rysowa nia. i f«nFlags&MK_LBUTTON) &&(t his==Get Capt ure())) (
aDC.OPt oLP(&point ); m_SeCandPal nt = paint ;
// Konwertuj punkt na uklad logi czny. // Zapisz bieżącą pozycję kursora .
i f(rn_pTempElement l (
if( CURVE {
~=
GetOocument ( )->Get El ementType() )
// Ry sujemy krzywą, // więc dodaj odcinek do
istn iejącej
// Czy j est to krzywa?
krzy wej.
st at lc_cast (m_pTempE lement )->AddSegment (m_SecandPo int ) ; m_pTempEl ement- >Oravl(&aOC) . // Terazją narysuj. retur n; // Skończyliśmy.
858
Visnal C++ 2005. Od podstaw aOC.SetROP2( R2_NOTXORPEN ) : II Przerysuj stary elem ent, aby
II Ustaw tryb rysowania. widoku . II Us uń stary element. II Zeruj wskaźnik.
II Utwórz tymczasowy element o typie i kolorze II zapisanymi w dokumencie, a n astępnie go narysuj. m_pTempEl ement = Cr eate El ement ( ) : II Utwórz nowy element . m_pTempEl ement- >Oraw(&aOC) : II Rysuj element .
else { II Nie rysuj emy elementu. więc podświetlamy. .. CSket cher Ooc* pOoc=GetDocument ( ) : II Pobierz wskaźnik do dokumentu. CElement * pE l ement = O: II Zap isz wskaźnik do elementu. CRect aRect(O ,O. O. O) : II Zap isz prostokąt. POSITlON aPos = pOoc->GetL i st HeadPosi t i on() : II Pobierz pozycję pierwszego elementu. CEl ement * pOl dSe l ect io n ~ m_pSel ecte d : II Zapis z stary wybrany element.
m_pSelec t ed = O: II Itera cja przez whi l e(aPos ) { pEle ment = pOoc->Get Next (aPos): aRect ~ pEl ement ->GetBoundRect ( ) : aOC.LPt oOP (aRect ) : aRect .Normal i zeRect ( ) :
listę.
II Wybierz element znajdujący s ię p od kurs orem.
if(aRect .Pt l nRect (poi nt ) ) { m_pSel ect ed = pElement : break:
} i f (m_pSel ect ed == pOldSel ect i on) r etu r n:
II Jeżeli nowo wybrany element j est tym samym co stary. II s ko ńczyliśmy.
II Zakoncz podświetlanie elementu.
if(pOl dSel ecti on != O) II Sprawdź, czy jakiś element jest podświetlony.
( aRect = pOl dSele ct i on->GetBoundRect ( ) : II Konw ertuj na współrzędne urządzenia.
aOC. LPt oOP (aRectl : aRect .Nor ma l i zeRec t ( ) : II Norma lizuj. I nvalidat eRect (aRect. FALSE) : II Unieważnij obszar.
II Jeżeli został wybrany no,,)' element, podświ etl go.
if(m_pSel ect ed != O) II Spra wdź , czy zosta ł wybrany. ( aRect ~ m_pSel ect ed->GetBoundRect( ) : aOC. LPtoOP(aRect ) : II Ko nwertuj na wspólrzędn e urządzenia.
aRect .Norma l i zeRect ( ) : II Normalizuj. I nval i dat eRect ( aRect, FALSE ) : II Unieważnij obszar .
Rozdział 15.
• Tworzenie dokumentu i poprawianie widoku
859
Podświetlanie
elementów ma być dokonywane jedynie wtedy, gdy nie jest tworzony nowy element. Cały kod dla podświetlenia dodany jest w nowej klauzuli el se w głównej instrukcji i f. To wiąże się z przesunięciem kodu określającego element pod kursorem, napisanego poprzednio do nowej klauzuli el se. Musimy śledzić poprzednio wybrane elementy, ponieważ jeżeli podświetlany jest kolejny element, podświetlenie poprzedniego musi zostać usunięte . W tym celu zapisujemy wartość m_pSelected do pOldSelecti on. Następnie sprawdzamy, czy pod kursorem znajduje się element, a j e że l i tak, zapisujemy jego adres do m_pSel ected. Jeżeli pOl dSel ected i m_pSel ected są równe, wtedy albo obydwie zmienne zawierają adres tego samego elementu, albo obydwie mają wartość zero. Jeżeli ich wartości są te same i niezerowe , element już podświetlony musi pozostać podświetlony, więc nie trzeba nic robić. Jeżeli obydwie wartości wynoszą zero, nic nie zostało podświetlone i nic nie musi zostać podświetlone, więc również w tej sytuacji nic nie trzeba robić. W obu przypadkach po prostu powracamy z funkcji. Jeżeli wartości te są różne, należy coś zmienić.
Jeżeli pOl dSel ect i on jest niepuste, należy usunąć podświetlenie starego elementu . Mechanizm jest taki sam jak poprzednio (pobierz prostokąt opisujący we współrzędnych urządzenia i prześlij go do funkcji Inval i dateRect () kontekstu urządzenia). Następnie sprawdzamy zmienną m_pSel ected i jeżeli jest ona niepusta , musimy podświetlić element, którego adres zawiera. To ponownie wiąże się z pobraniem prostokąta opisującego we współrzędnych urządzenia i przesłaniem go do funkcji lnva l i dateRect() .
Rysowanie podświetlonych elementów Wciąż
musimy sprawić, aby podświetlony element faktycznie został narysowany jako podW pewnym momencie wskaźnik m_pSel ected musi zostać przesłany do funkcj i rysującej element. Jedynym odpowiednim miejscem jest funkcja OnDraw() w widoku: świetlony .
void CSkete herVlew: :OnOraw(COC* pOC) {
CSketeherOoe* pOoe = GetOoeument(): ASSERT_VALIO (pOoe): if ( I pOoe) ret urn: POSIT ION aPos = pOoe- >GetListHeadPosition (): II Zapisz dla wskaźnika do elementu. CEl ement * pElement = O: while(aPos) II Pętla, dopóki aPos nie je st puste. (
pElement
=
pOoe->GetNext( aPos):
II Pobierz
wskaźnik do bieżącego
elementu.
II Jeżeli element j est widoczny. ..
i f ( OC- >ReetV isi ble( El ement ->Get BoundReet())) pEl ement->Oraw(pOC. mpSeleeted):l1...narysuj go.
Należy zmienić
tylko jeden wiersz kodu. Do funkcji Draw() elementu dodany ment przekazujący adres elementu, który ma zostać podświetlony .
został
drugi argu-
860
Visual C++ 2005. 011 podstaw
Testowanie podświetleń To wszystko, co by ło wym agane do d z iałania p od świetleń . Nie było to proste, jednak nie był o też bardzo trudne. Możesz przebudow ać i uru ch omić program Sketcher, aby go przetestować . Gdy pod kursorem znaj duje s ię jakiś element, zostanie on nary sowany na fioletowo. Dzięki temu jest oczyw iste, z którym elementem będzie działało menu kontek stow e, zanim zostanie ono wyświetlone , a także od razu wiadomo, które menu się pojawi.
ObslUga komunikatów menu Kolejn ym etapem jest uzupełni eni e kodu w c iała c h procedur obsługi elementów menu Move i Delete, które dod a li śmy w cześniej . Dodamy najpierw kod dla Delete, p onieważ j est on prostszy.
Usuwanie elementu Kod potrzebny w procedurze OnE1ementDe 1et e() w klasie C Ske tc h e rV i e~1 służący do aktualnie wybranego elementu jest całk i em prosty:
u suni ęcia
void CSketcherView : :OnElement Delet e( ) (
if (m_pSe lect ed) (
CS ket cherDoc* pDoc = GetDoc ument (): pDoc->De let eEl ement (m_pSelect ed); pDoc->Up dat eAl lViews( O); m_pS elected = o;
II Pob ierz wskaź n i k do doku men tu. II Usuń element. II Prz erysuj wszys tkie widoki. II Zeruj wskaźnik do wy bran ego elementu.
Kod usuw ający element jest wykonywany jedynie wtedy, gdy m_pSel ect ed zaw iera poprawny adres, w skazuj ący , że istnieje element do usuni ęci a . Pobieramy wskaźn ik do dokumentu i wyw ołuj emy funkcję De1eteE1ementC ) dla obiektu dokumentu. Za chwilę dodam y tę składową do kla sy CSk et cherDoc. Gdy element zostan ie usunięty z dokumentu, wywołujemy Update A1l Vi ews () , aby przerysow ać wszystkie widoki bez u sunięte g o elementu. W koń cu ustawi amy m_pSel ected na zero , aby w skazać , że żaden element nie j est wybrany. Dodaj teraz
d eklaracj ę
Del et eE1ement ( ) jako
class CSket cherDoc ; public CDocument {
protected: II Do tworzenia tylko z serializacji. CSket cherDoc( ): DECLARE_DYNCREATE( CSket cherDoc) II Atrybuty.
publ i c: II Operacje.
pub l ic:
public zną składową klasy
CSket cherDoc:
Rozdział 15.•
Tworzenie dokumentu ipoprawianie widoku
vOld Delet eElement( CElement * pElement l: unsigned i nt GetElementType() { ret urn m_Element : }
861
II Usuwa element. II Pobierz typ elementu.
II Reszta klasy bez zmian.
}:
Przyjmuje ona jako argument wskaźnik do elementu, który ma zostać w pliku Sketch erDoc.cpp jako:
usunięty,
i nic nie zwraca .
Możeszjązaimplementować
vOl d CSket cherDoc: :De lete Element (CElement* pEl ement l (
i HpEl ement) ( II Jeżeli wskażnik do elementujes t pop rawny , II z najdź wskaźnik na liś cie i go usuń .
POS JTJ ON aPosit ion = m_ElementLlst .Fi nd(pE lement l: m_Element Li st RemoveAt( aPoslti onl : delet e pElement : II Usu ń element ze sterty .
Zrozumienie, jak to działa, nie powinno sprawi ać kłopotu. Po upewnieniu się , że wskaźnik jest niepusty, wyszukujemy wartość POSITIO Ndla wskaźnika na liście , korzystając ze składowej Fi nd() obiektu listy. Używamy jej ze skład ową RemoveAt ( ), aby usunąć wskaźnik z listy, a następnie usuwamy element wskazywany przez parametr pEl ement ze sterty. To wszystko, co je st nam potrzebne do usuwania elementów. W programie Sketcher można teraz rysować w wielu przewijalnych widokach i u suwać wszelkie e lementy z ry sunku w dowolnym z w idoków.
Przenoszenie elementu Przenoszenie zaznaczone go elementu jest nieco trudniejsze. Ponieważ element musi s ię przerazem z kursorem myszy , trzeba dodać kod do metody OnMouseMove ( ). Ponieważ funkcja ta jest używana również do rysowania elementów, potrzebujemy mechanizmu, który oznaczał a by , że aktywny jest tryb "przenoszenia" . Najprościej można to zrobić, używając flagi w klasie widoku, którą możemy nazwać m_MoveMode. Jeżeli będzie ona typu BOOL, u żyjemy warto ści TRUE, gdy tryb przenoszenia będzie włączony, a FAL SE, gdy tryb ten będzie nieaktywny. Oczywiście moglibyśmy ją także zdefiniować jako typ podstawowy - boal , a wtedy warto ści ami byłyby t r ue i fa l s e. suwać
kursor w trakcie przenoszenia , więc w tym celu potrzebujemy kolejnej w widoku. Nazwiemy j ą mJ ursor Pos i będzie ona typu CPo'jnt . Oprócz tego musimy umożliwić zrezygnowanie z przenie sienia elementu . W tym celu należy zapamiętać pozycję kursora w momencie rozpoczęcia operacji, aby w razie potrzeby można było przenieść element z powrotem. Będzie to kolejna składowa typu CPoi nt , a nazwi emy ją mJ i r st Pos. Dodaj trzy nowe składowe do części protected klasy widoku:
Musimy
także śled zić
składowej
class CSketc herView pub l ic CScrol lView { II Reszta klasy bez zmian.
862
Visual C++ 2005. Od podstaw prot ect ed: CPoint m_First Point: CPolnt m_Second Po int: CElement* m_pTemp Eleme nt : CElement* mpSelect ed: BOOL m_M ov eMode: CPoint m_CursorPos : CPo lnt m Fi rst Pos :
II Pie rwszy punkt zapisany dla elementu. II Drugi punkt zapisany dla elementu. II Wskaźnik do tymczasowego elementu. II Aktu alnie wybran y element. II Flag a przenosz enia eleme ntu. II Pozycja kur sora . II Pierwotna p ozycja przy przenoszeniu .
II Reszta klasy bez zm ian.
}; Mu szą one równ ież zostać
go w
następujący
zainicjalizowane w konstruktorze klasy CSketcherVi ew,
więc zmień
sposób:
CSketcherV iew : :CSket cherView( ) m_First Po int( O.O) mSecondPoint(O .O) m_pTempE lement (NULL ) mpSelected(NU LU m_MoveMode(FAL SE ) m_CursorPos(CPo int(O,O)) mFirst Pos(CPoi nt (O.O)) II Do zrob ienia: tutaj wpisz kod konstrukcji.
Proces przenoszenia elementu rozpoczyna się, gdy zostanie wybrana opcja Move z menu kontekstowego. Teraz dodaj kod procedury obsłu gi elementu menu Move i ustaw warunki niezbędne dla operacji :
vO ld CSketcherView: :OnEl ementMove( ) f CClient OC aDC( t his) : OnPrepa reDC(&aDC): II Usta w kont ekst urządzenia .
GetCursorPos(&m_CursorPos) : II Pobierz pozycję kur sora we wspólrzędnych ekranu . Sc reenToCl lent (&m_CursorPos): II Kon wertuj na wsp ć łrzę dne klient a. aDC.DPtoLP(&m_CursorPos ): II Kon wertuj na układ logi czny . m_Fi rst Pos = m_CursorPos: II Zapamiętaj pierwszą pozycję.
mMoveMode = TRUE: II Rozpocznij tryb przenoszenia. Powyższa procedura
l
wykonuje cztery
działania :
Pobiera współrzędne bieżącej pozycji kursora, ponieważ operacja przenoszenia rozpoczyna się z tym punktem odniesienia.
2. Konwertuje pozycję kursora na współrzędn e logiczne, definiowane z użyciem współrzędnych logicznych.
a.
Zapamiętuje początkow ą pozycję później anulować
ponieważ
kursora na wypadek, gdyby
elementy
są
użytkownik chciał
przenoszenie.
.. Ustawia flagę trybu przenoszenia w celu poinformowania procedury OnMouseMove( ).
Funkcja Get Cursor Pos() jest funkcj ą Windows API, któr a przechowuj e bieżącą pozycj ę kursora w m_CursorPo s . Zauw aż, że do tej funkcji przesyłamy wskaźnik . Pozycj a kursora jest wyrażona we współrzędnych ekranu (czyli współrzędnych w zg l ęd e m lewe go górnego rogu ekranu). Wszyst kie opera cj e z kursorem są wyrażone we współrzędnych ekranu . Potrzebujemy pozycji wyrażonej we współrzędnych logicznych, więc mus imy wykona ć dwuetap ową konwersję . Funk cja ScreentoCl i ent ( ), która jest dziedziczoną składową klasy widoku, zamie nia ze w s pó łrzę d nyc h ekranu na wsp ółrzędne klienta, a n ast ępnie przesyła wyn ik do funk cji skł ad ow ej obiektu aOC PtoLP() , aby ot rzy mać współrzędne logiczne . Po zapisaniu po czątkowej pozycji kursora w mJ i rstPo s ustawiamy flagę m_MoveMode na TRUE, aby procedura OnMouseMove() m ogł a prz enieś ć element. Gdy ustawili śm y flagę trybu przenoszenia, czas myszy , by mogła przenosić elementy.
zaktualizować procedurę obsług i
przesuwania
Wprowadzanie zmian do procedury obslugi WM_MOUSEMOVE Przenoszenie elementu ma miej sce jedynie w trybie przenoszen ia przy przesuwaniu kursora. Z tego względu w procedurze OnMouseMove() mu simy jedynie doda ć kod obsłu gujący przenoszeni e elementu. Należy go umie ści ć w bloku wykonywanym wtedy, gdy m_MoveMode ma warto ś ć TRUE:
vO ld CSketcherView : :OnMouseMove (UINT nFlags . CPo int po i nt ) ( II Definiuj e obiekt kontekstu
urządzen ia
CCl lentOC aDCCt hi s) : OnPrepareDC C&aDC) ;
dla widoku. II Kontekst urządzen ia dla tego widoku. II Ustaw tryb rysowa nia.
1/ Jeżelij est eśmy w trybie prze noszenia, prz eni eś element i po wróć .
i Hm_MoveMode) {
aDC .DPtoLPC &poi nt ) ; MoveElement CaDC. point ); return; II Reszta procedury bez zmia n.
Dodany kod nie wymaga wyjaśnień, nieprawdaż? Instrukcja i f sprawdza, czy tryb przenoszenia jest aktywny, a następnie wywołuje funkcję MoveElement ( ), która wykonuje wszystko, co jest niezbędne dla przeniesienia. Pozostaje nam tylko z aimplementować t ę funkcję . Dodaj
deklara cj ę
czając poniższy
funkcji MoveEl ement jako składową chronioną klasy CSk etc her Vi ew, umie szkod w odp owiednim miejscu definicji klasy :
void MoveElementC CC l ientDC&aDC. CPoint&point) : Jak zw ykł e możesz w tym celu klikn ąć prawym przyciskiem myszy nazwę klasy w panelu Class View. Funk cj a potrzebuje dostępu do obiektu hermetyzującego kont ekst urz ądzenia dla wid oku (aOC) i bie żącą pozy cję kursora (poi nt), więc o bydwa te elementy s ą parametrami referencyjnymi . Implementacja tej funkcji w pliku Sketcher View.cpp wygląda następująco:
Rozdział 15.
• Tworzenie dokumentu i poprawianie widoku
863
Funkcja GetCursorPos() jest funkcją Windows API , która przechowuje bieżącą pozycję kur sora w m_Cu rsorP os. Z auważ , ż e do tej funkcji przesyłamy wskaźnik. Pozycja kursora jest wyrażona we współrzędnych ekranu (czy li w spółrz ędnych względem lewe go gó rne go rogu ekranu). Wszystki e operacj e z kursorem są wyrażo ne we wsp ółrzędnych ekranu. Potrzebuj emy pozycji wyrażonej we wsp ółrzędnych logicznych, więc mu simy wykon ać dwuetapową konwersję. Funkcja ScreentoC l i ent ( ), która je st dzi edzi czoną składową klasy wid oku , zami enia ze wspó łrzęd nych ekranu na w sp ółrzędne klienta, a następni e przesył a wynik do funk cji s kład o w ej obiektu aDC- Pto LP( ), aby otrz ymać w sp ółrzędne logiczne. Po zapisaniu początkowej pozycj i kursora w mJi r st Pos ustawi am y aby pro cedura OnMou seMove ( ) m ogł a przen ieść element. Gdy ustawili śmy flagę trybu przenoszenia, czas myszy , by m o gła przeno si ć elementy.
flagę
m_MoveMode na TRUE,
zaktualizować procedurę ob sług i
przesuwania
Wprowadzanie zmian do procedury obslUgi WM_MOUSEMOVE Przeno szenie elem entu ma miejsce j edynie w trybi e przenoszenia przy przesuwaniu kur sora. Z tego względu w pro cedurze OnMous eMove() musimy jedynie dodać kod obsługuj ący przenoszenie elementu. N a l eży go um ie ścić w bloku wykonywanym wtedy, gdy m_MoveMode ma w arto ść TRUE:
void CSketcherV iew: :OnMouseMove(UINT nF lags, CPo int pOi nt ) ( II Definiuj e obiekt kan /eks/u
urządzenia
CCl ientOC aOC (t his) ; OnP repa reOC( &a OC ) ;
dla widoku . II Kontekst u rz ądz en ia dla lego widoku. II Usta w tryb rys owania.
II Jeżeli jes/eśmy w trybie przeno szenia, przenieś element i powróć.
i f (m_MoveMode) (
aOC.OPtoLP (&point): MoveElement (aOC. point ); return; II Reszta procedury bez zmian.
Dodany kod nie wymaga wyja śnień , nieprawdaż? Instrukcj a i f sprawdza, czy tryb przenoszenia jest aktyw ny, a następni e wywołuje funkcję MoveEl ementO , która wykonuj e wszy stko, co jest ni ezbędne dla przeniesienia. Po zostaje nam tylko zaimplementowaćtę funkcję. Dodaj
dekl arację
czaj ąc p on i ższy
funkcji MoveElement jako składową chro n i o ną klas y CSket cher Vi ew, umie szkod w odpowiednim miejscu definicji kla sy :
void MoveE lement (CC l ientOC& aOC. CPolnt &po int) ; w tym celu
kliknąć
C/ass View. Funkcja potrzebuje
dostępu
prawym przyciskiem my szy nazwę kla sy w panelu do obiektu h ermetyzującego kontekst urządzenia dla widoku (aDC) i bieżącą pozycj ę kursora (poi nt ), więc obydwa te elementy są par ametrami referencyjnymi . Implementacja tej funk cji w pliku Sketcher View.cpp wygląda następująco :
Jak zwykle
możesz
864
Visual C++ 2005. Od podstaw void CSketcherView: :MoveElement (CCl ient OC&aOC. CPo int &point) (
CSi ze Oist ance = po int m_CursorPos = poi nt :
111_ CursorPos
:
II Pobierz odleglość p rzenoszen ia. II Ustaw bi eżą cy punkt jako pierwszy
następnym
razem .
II Jeżeli jakiś element j est wybr any , prz en ieś go .
II Teraz p rzenies element. II Rysuj p rzeniesiony elemen t.
}
Odległość, o jaką należy przenie ść wybrany element, jest przechowywana lokalnie w obiekcie CSi ze Di st ance. Klasa CSi ze została zaprojektowana właśnie do reprezentowania względnych współrzędnych pozycji i posiada dwie publiczne składowe - ex i cy, które odpowiadają przyrostowi x i y. Są one obliczane jako różnica mi ędzy bieżącą pozycją kursora przechowywaną w poi nt a poprzednią pozycją kursora prze chowywaną w m_Cursor Pos. Używany jest tu operator, który jest przeciążony w klasie CPo i nt. Wersja, której tu używamy , zwraca obiekt CSi ze, jednak istnieje też wersja zwracająca obiekt CPoint . Przeważnie można operować na połączo nych obiektach CS iz e i CPo i nt. Zapisujemy bieżącą pozycję kursora w m_Cur sor, aby ją wykorzystać przy następnym wywołaniu funkcji, które będzie miało miejsce, gdy podczas bi eżącej operacji przenoszenia pojawi się kolejny komunikat o przemie szczeniu myszy.
Zaimplementujemy przenoszenie elementu w widoku, korzystając z trybu rysowania R2_NOT XORPEN, ponieważ jest to szybkie i proste. Jest to dokładnie to samo, co robiliśmy podczas tworzenia obiektu. Przerysowujemy wybrany element w j ego aktualnym kolorze (wybranym kolorze), aby nadać mu kolor tła , a następnie wywołuj emy funkcję Move( ), aby przenieść element o odległość określoną przez Di stance. Za chwilę dodamy tę funkcję do klas elementów. Gdy element zostanie przeniesiony, ponownie użyjemy funkcji Draw(), aby w nowym położeniu był wyświetlany jako pod świ etlony . Normalny kolor elementu zostanie przywrócony, gdy operacja przenoszenia zostanie zakończona, pon ieważ procedura OnLButtonUp() przerysuje wszystkie okna poprzez wyw oła n i e funkcji UpdateA11Vie ws ( ).
Przenoszenie elementów Dodaj funkcję Move( ) jako wirtualną s kład o wą klasy bazowej CElement. w na stępujący sposób: class CE lement : publ ic CObject (
II Przenosi element. II Pobiera pros tokąt ogran iczający elemenl.
protected; CE lement ().
II Umieszczamy lulaj, aby zapobiec
wywo łaniu.
}
Jak wspom i nałem wcze śn i ej , przy okazji omawiania funk cji Dr' aw() , mimo że implementacja funkcji Move( ) w tym miej scu nie ma znaczenia, nie możemy z niej zrobi ć funkcj i czysto wirtualnej , a to ze wzg lęd u na wymagania stawiane przez s er i al izację . Dodaj teraz d ekl ara cj ę funkcj i Move( ) jako p ubliczną s kładową wszystkich klas wyprowadzanych z klasy CEl ement. Jes t ona dla wszystkich taka sama : II Funkcja przenoszaca element.
vi rt ual void Move(CSize&aSize); N astępn ie
zaimplementuj
void CLin e:
~l o v eCCSi ze&
funkcję
Move( ) w klasie CLi ne:
aS lze)
(
m_St artPoint +~ aSi ze : m_EndPoint += aSize: m_Enc losi ngRect += aSi ze;
II Punkt początkowy p rzenoszenia II i punkt docelowy II Przen ieś pros tokąt op is ujący.
Jest to proste d zi ęki przeciążo ny m opera to rom += w klasach CPoi nt i CRect . Wszystkie one działaj ą z obiektami CS i ze, w ięc po prostu dodajemy wzg lęd ną odl egłość okreś loną przez aSi ze do p o cz ątk o w e g o i końcowego punktu linii oraz do pro sto kąta o pi suj ącego. Przenoszenie obiek tu CRect angl e jes t jeszcze prostsze:
void CRectangle: :MoveC CS i ze&aSize) (
m_Enc losi ngRect += aSize; Poni e w aż pros to kąt jest
II Przenieś prosto kąt .
defi niowany przez
s kł adową
m_Encl osi ngRect, wys tarczy
przen ie ś ć
tylk o ją. Skła dowa
Move( ) dla klasy CCi r e i e j est ide ntyczna ;
vo id CCi rcle; ;Move(CSize&aSize) (
.
m_E n c l o s l n g R ect +~
aSl ze;
II Przenieś pros toką t .
Przeniesienie obiektu CC urve jes t nieco bardziej skomplikowane, p on ieważ jest on definiowa ny przez ni eznan ą z góry liczb ę punktów . M oże s z zai mplemen tow ać tę fu nkcję w następuj ący spo sób ;
void CCurve: :MoveCCSi ze&aS ize) {
m EnclosingRect
+~
aSi ze:
II Prz enieś prostoką t .
866
Visual C++ 2005. Od podstaw II Pobierz pozycję p ierwszego elementu.
_ _~=-J
POSITION aPosi t ion = m_Point Lls t .GetHeadPoslti on(); wh i 1e(aPos i t ion) m_Poi nt l1st .Get Next (aPos it ion ) , ~ aS i ze;
'---'-- - - - - - -- - - - -
II Przen ieś
każdy p unkt
z listy.
Nie jest tego dużo . Najpierw przenosimy opisujący pro stokąt przechowywany w mJnc l osi ngRect, korzystając z przeciążonego operatora += dla obiektów eRect . Następnie iterujemy przez wszystkie punkty definiujące krzywą, przeno sząc po kolei każdy z nich za pomocą operatora += przeciążonego w epoi nt .
Upuszczanie elementu Pozo stało nam jedyni e dodać możliwoś ć upuszczenia elementu w wybranej pozycji po zakoń czeniu przenoszenia lub zrezygnowaniu z przenoszenia. Aby upuścić element w nowym miejscu, użytkownik klika lewy przycisk myszy, więc możemy o bs łużyć tę operację w procedurze OnLButtonDown ( ). Aby przerw ać operację, użytkownik klika prawy przycisk myszy, więc możemy dodać procedurę obsługi OnRButton Down( ), w której to obsłużymy .
Zajmijm y się na początek lewym przyciskiem myszy. Musimy potraktować to jako specjalne działanie przy aktywnym trybie przenoszenia. Konieczne zmiany zostały zacieniowane poniżej:
void CSket cherView: :OnLButt on Down (UINT nF1ags. CPolnt pOlnt ) {
CC11entD C aDC(thi s); OnP repa reDC( &aDC) ; aDC. DPt oLP(&poi nt ):
II Utwórz kontekst urządzenia. II Popraw początek uktadu wspó łrzędnyc h. II Kon wertuj punkt na u k ład logiczny.
i Hm_MoveMode) { II Tryb przenoszenia jest akty wny. więc
upuść
m_MoveMode = FALSE: m_pSel ected = O: Get Document ()->Updat eA11 Vi e\'JS(O);
element. II Zakończ try b p rzenoszenia. II Żaden element nie j est j uż wyb rany. II Prz erysuj wszystkie widoki.
}
e1se (
m_Fi rst Point ~ point : Set Capture() :
II Zapisz pozycję kursora . II Przechwy tuj kolejne komunikaty z my szy.
Ten kod jest całkiem pro sty. Naj pierw upewniamy się, że tryb prz eno szenia je st aktywny. J eżeli tak, ustawiamy flagę trybu przenoszenia z powrotem na FALSE, a następnie usuw amy wybór elementu. To wszy stko , czego potrzebujemy, ponieważ ś l e dz il iś my element my s zą, więc jest on ju ż na swoim miejscu. W końcu , aby uporządkow ać wszystkie widoki dokumentu, wyw ołujem y funkcję UpdateAll Vi ews( ) dokumentu , co powoduje przerysowanie wszystkich widok ów. Dodaj pro cedurę obsłu gi komunikatu !tJM_RBUTTONDOWN do eSk etche r Vle w, Properti es klasy . Jej implementacja musi wykonywać dw ie czynn ości :
korzyst aj ąc przeno sić
z okna element
Rozdział15.
z powrotem na j ego
- Tworzenie dokumentu i poprawianie widoku
początk ow ą pozycję
i
wyłączać
867
tryb przenoszenia. Odpowiedni kod
wygl ąda następująco:
void CSket cherView: :OnRButtonDownWINT nFlags. CPoi nt point) {
i f( m_MoveMode) ( II Tryb przenoszenia j est aktyw ny,
II Przenieś element do p ierwo tnej pozycji. II Zakoń cz tryb p rzenoszenia. II Żaden element nie jest już wybrany. II Przerysuj wszy stki e widok i. II Koniec.
Najpierw twor zymy obiekt CCl i entDC, aby go użyć w funkcji MoveElement () . Na stępnie MoveEl ement ( ), aby prz enieść aktualnie wybrany element o odl e głość od bie żącej pozycji kursora do pierwotnej pozycji kursora, którą zach owali śmy w mJ i r stPos. Po zmianie umiejscowienia elementu po prostu wyłączamy tryb przenoszenia, anulujemy wybór elementu i przerysowujemy wszystkie widoki . wywołujemy funkcję
Testowanie aplikacjj Ws zystko j est gotowe do pracy menu kontekstowych. W programie Sket cher można teraz wybierać typ i kolor elementu z jednego menu kontekstowego lub - gdy kursor znajduje s ię nad elementem - można przenieść lub usun ąć ten element, korzystaj ąc z drugiego menu kontekstowego.
Rozwiązywanie problemu
nakładających się
elementów
W ci ąż
istnieje ograniczenie, któreg o chcieliby śmy się pozbyć. Jeżeli element, który chcemy lub usun ąć , znajduje się wewn ątrz pro stok ąta innego elementu, narysowanego po interesuj ącym nas elemencie , nie b ę dzi emy mogli go podświetli ć , ponieważ Sketcher zawsze najpierw znaj duje element zewnętrzny. Zewnętrzny element cał kow i c ie przesłan i a element, który zawiera. Dzieje s i ę tak ze względu na kolejnoś ć elementów na liście. Możemy to naprawi ć poprzez dodanie opcji Send to back (prze ślij na tył ) do menu kontekstowego. Będzie ona przenosi ć element na początek listy . przenieś ć
Dodaj separator i element menu do menu element w zaso bie IDRJ URSOR_MENU, j ak na rysunku 15.15.
868
Visual C++ 2005. Od podstaw
Rysunek 15.15
. śk etćiier .r~ ·(..":::M iiiu: 1'1"0';)+ 1",--__~ ~~~_=_ element no element T ~...,:I
LC~::':: ' :- :Jj
T r e'.
Delete
_ .~
.. j
I
2end to beck T
'10, '"
---:..1
Dodaj procedurę obs ług i tego elementu do klasy wid oku, korzy stając z okna Properties klasy CSketcherVi ew. Naj lepiej obsłużyć go w widoku , poniewa ż to tu zapi suj emy wybrany element. Kliknij przycisk Events na pasku narzędzi okn a Prop erties i dwukrotnie kliknij identyfikator komunikatu ID_ELEMENT_SENDTOBACK. Nas tęp n ie wybierz opcj ę COMMAND znaj duj ącą się w prawej kolumn ie poniżej opcj i OnElementSendtoback. Zaimplementuj procedurę w ten spos ób:
void CSketc herVi ew: :OnEl ement Sendt oback() {'---
- - -- -- --
-
-
-
-
GetDocumentC )->SendToBackCm pSelecte d):
-
-
-
-
-
-
-
-
- --
-
-
-,
C ałą pracę wykona dokument, gdy do publicznej funkcji SendToBackO , którą zaimplementuj emy w klasie CSketc herDoc, prześlemy wskaźn ik do aktualnie wybranego elem entu . Dodaj ją do definicj i klasy, okreś l aj ąc void jako typ zwracany, zaś typ parametru jako CElement *. Oto implementacja tej funkcji :
voi d CSketcherDoc: :SendToBackCCEl ement* pEl ement) {
if CpEl ement) { II Jeżeli wskaź nik do ele mentu jest p op rawny , II wyszukaj element na liśc ie i usuń go.
POSI TIONaPosit ion = m_ElementL ist .Fi nd (pE lement ): m_E lement Li st.RemoveAt Ca Positi on); m_ElemenU i st. AddTai l (pE l ement);
II
Umieść go
na koncu listy.
'---"-----------~
Gdy znamy wartość POSITION odpowiadającą elementowi, usuwamy element z listy, wywołu RemoveAt ( ). Oczyw i ście nie usuwa to elem entu z pamięci, a jedynie usuw a z listy w skaźnik do nieg o. Następni e na końcu listy z powrotem dod ajemy w skaźnik do e lementu za pomocą funkcji AddTai l () .
jąc
Gdy element zostanie przeniesiony na koniec listy, nie będzie przesłaniał innych, ponieważ lista jest przeszukiwana od początku. Zawsze najpierw znajdz iesz jeden z pozostałych elementów, jeżel i odp owiedni prostokąt opisujący zawiera bi eżącą pozycję kursor a. Dzi ęki opcji Send to back można rozwiązać każdy problem zw i ązany z nakł ad aniem się elem entów w widoku .
BBB
Visual C++ 2005. Od podstaw =-Men;;j-
Rysunek 15.15
Sket·ćhe'.' ć-( ....:.Mi NU element
• x
no element ~"-'-'-'-'-'
Move
Delete :i. nd to back
r
1::~~; " ~
._
L
----:
D odaj procedurę obsługi tego elementu do klasy widoku, korzy stając z okn a Properties klasy CSketc her Vi ew. Naj lepiej obsłuży ć go w widoku, ponieważ to tu zapisujemy wybrany element. Kl iknij przyci sk Events na pasku narzędzi okna Properties i dwukrotnie kliknij identyfikator komunikatu ID_ELEME NT_SENOTOBACK. Następnie wybierz opcj ę COMMANDznajdującą s i ę w prawej kolumnie poni żej opcji OnElementSendtoback. Zaimplementuj procedurę w ten sposób:
void CSketcherVi ew: :OnElementSendtobackC) {L.
I
_
Get Doc umentC) ->SendToBackCm pSelecte d) :
I
C ałą pracę wyk ona dokument, gdy do publi cznej funkcji SendToBack( l, którą zaimplementujemy w klasie CSket cher Ooc, prześlemy w skaźnik do aktualnie wybranego elementu. Dodaj ją do definicji klasy , okre ślając voi d jako typ zwracany, zaś typ parametru jako CEl ement*. Oto implementacja tej funkcji :
vOld CSket cherDoc: :SendToBack(CElement* pElement J { if CpE l eme n Ł)
{ II Jeżeli ws kaźn ik do elementu j est pop rawny, II wyszukaj element na liś cie i usuń go.
POSITION aPosi t ion = m_E lement Lis t ,FindCpElement J: m_Element Lls t .RemoveAtCaPosit ionJ: m_El ementLi st .AddTai l CpE lement J;
II Um ieść go na koń c u listy.
Gd y znamy wartość POSITION odp ow iad aj ąc ą elementowi , usuwamy element z listy, wywołu RemoveAt ( l . Oczywi ście nie usuwa to elem entu z pami ęci , a jedynie usuw a z listy wskaźnik do nie go. Następnie na końcu listy z powrotem dodaj emy wskaźnik do elementu za p omocą funkcji AddTai l (l .
jąc
Gdy element zostanie przenie siony na koniec listy, nie będzie przesł ani ał innych , ponieważ lista jest przeszukiwana od poc zątku . Zawsze najp ierw znajdziesz jeden z po zostałych elementów, je żeli odpowiedni prostokąt opisujący zawiera bieżącą pozycj ę ku rsora. Dzięki opcj i Send to back można rozwiązać każdy problem związany z nakładaniem się elementów w wid oku .
Rozdział 15.•
Tworzenie dokumentu i poprawianie widoku
869
Podsumowanie jak stosować klasy kolekcji MFC do problemów wymaobiektami i wskaźnikami do obiektów. Kolekcje są bardzo pomocne w programowaniu dla systemu Windows, ponieważ dane aplikacj i, które przechowujemy w dokumencie, często powstają w sposób nieustrukturyzowany i nieprzewidywalny. a musimy mieć możliwo ść przez nie przebrnąć za każdym razem, gdy widok musi zos tać zaktualizowany.
W tym rozdziale
dowiedziałeś się ,
gających zarządzania
Widziałeś też,
w
kontekście
jak tworzyć dane dokumentu i zarządzać nimi na liście wskaźników oraz aplikacji Sketcher - jak widoki i dokumenty komunikują się ze sobą.
Pod wieloma względam i poprawiłeś możliwo ści widoku w programie Sketcher. Dodałeś przewijanie widoku, korzystając z klasy MFC CScroll View, a także menu kontekstowe służące do przemieszczania i usuwani a elementów . Zaimplementowałeś też podświetlanie elementów, aby zapewnić użytkownikowi i n fo rm acj ę zwrotną przy przemieszczaniu i usuwaniu elementów. W tym rozdziale
zostało
poruszonych wiele
ważnych zagadnień ,
a oto
najw ażniejsze
z nich:
•
Jeżel i
potrzebujesz klasy kolekcji do zarządzania obiektami lub w skaźnikami,
najlepszym wyborem będzie jedna z klas kolekcj i opartych na szablonach, ponieważ w większości przypadków zapewniają one bezpieczne operacje (ang. typ e-safe ).
•
Gdy rysujesz w kontekście urządzenia, współrzędne s ą w jednostkach logicznych, które zależą od ustawionego trybu mapowania. Punkty w oknie , które s ą dostarczane z komunikatami myszy, wyrażone są we współrzędnych klienta. Te dwa układy współrzędnych przeważnie są różne.
•
Współrzędne okre ślające pozycję
•
pikselach, i określane
Funkcje
służące
dostępne są w
kursora są wyrażone we współrzędnych ekranu lewego górnego rogu ekranu.
względem
do konwersji klasi e CDC.
współrzędnych klienta
i współrzędny ch logicznych
•
Windows żąda przerysowania widoku, wysyłając komunikat WM_PAINT do aplikacji . Powoduje to wywołanie funk cji składowej OnDraw() danego widoku.
•
Wszelkie trwałe rysowanie w dokumencie powinieneś zawsze przeprowadzać w składowej Draw ( ) klasy widoku . Daje to pewność , że okno zostanie poprawnie narysowane, gdy zaż ąd a tego Windows .
•
Możesz zwiększyć wydajność
RectVi s i bl e () klasy CDC, aby przerysowane. •
implementacji OnD raw( ) poprzez wywołanie składowej sprawdzi ć, które elementy powinny zostać
W celu uaktualnienia wielu widoków przy zmianie zawartości dokumentu możesz dokumentu Upd at eA11Vi ews ( ). Powoduje to wywołanie s kład o wej OnUpdat e ( ) wszystkich widoków.
wywołać składową obiektu
870
Visnal C++ 2005. Od podstaw •
Możesz przesłać do funkcj i Update A11Vi ews( ) informacje wskazujące, które obszary w widoku powinny zostać przerysowane. To przyspiesza przerysowywan ie widoków .
•
Po kliknięciu prawym przyciskiem myszy może być przy kursorze. Tworzy się je tak jak zwykłe menu.
wyświetlane
menu kontekstowe
Ćwiczenia Kod źródłowy oraz ksiazki/vcppo.htm
rozwiązania
1. Zaimplementuj
listy .
2. Zaimplementuj
klasę
krzywej
a.
poniższych ćwiczeń
klasę CCurve,
końcu
a nie na
do
tak aby
znajdziesz pod adresem http.r/helion.pl/
wsk aźniki były
dodawane na
początku,
CCur ve w programie Sketcher, wykorzystując do reprezentacji
listę wskaźników
z typami zamiast listy obiektów.
Wyszukaj w systemie pomocy informacje o klasie szablonu kolekcji CAr r ay i użyj jej do przechowywania punktów w klasie CCu r ve w programie Sketchcr.
16 Praca zoknami dialogowymi ikontrolkami Okna dialogowe i kontrolki są podstawowymi narzędziami komunikacji z użytkownikiem w środowisku Windows. Z tego rozdziału nauczysz się, jak implementować okna dialogowe i kontrolki poprzez zastosowanie ich w programie Sketcher. Dzięki temu dowiesz się o: •
Oknach dialogowych i tworzeniu ich zasobów.
•
Kontrolkach i dodawaniu ich do okien dialogowych.
•
Podstawowych rodzajach kontrolek.
•
Tworzeniu klasy okna dialogowego do
•
Programowaniu tworzenia okna dialogowego i sposobach uzyskiwania informacji zjego kontrolek.
•
Modalnych i niemodalnych oknach dialogowych.
•
Implementowaniu i używaniu za pomocą kontrolek.
•
Implementowaniu skalowania widoku.
•
Dodawaniu paska stanu do aplikacji.
zarządzania
bezpośredniej
dialogi em.
wymiany danych oraz o walidacji
Poznaj okna dialogowe Oczywi ście, okna dialogowe nie są dla Ciebie niczym nowym . Wi ększość programów dla systemu Windows używa okien dialogowych do obsługi części danych wejściowych. Wybieramy opcję z menu i wyskakuje okno dialogowe z różnymi kontrolkami, w których podajemy informacje. Niemal wszystko, co pojawia się w oknie dialogowym, jest kontrolką. Okno dialogowe
872
Visual C++ 2005. Od podstaw oczyw iście
jes t oknem, ale równi e ż ka żda ze specjalizowanym oknem. Przyzwyczaj się, że nie w sys temie Windows to okna .
znajd ującyc h s i ę większość
w nim kontrolek także je st wyelementów pojaw iających s ię na ekra-
Mimo że kontrolki są szczegó lnie związane z oknami dialogowymi, m o żn a j e także tw orzy ć i u ż yw ać w innych oknach. Typ owe okno dialogowe je st przedstawione na rysunku 16.1. Lista kombi nowana
RYSunek 16.1 Przyci ski
Przycisk i
v i
otwór. Anuluj
·1 I
To jest okno dialogowe p oj aw i aj ąc e s ię po wy braniu z menu File/Op en/File w Visual C++ 2005 . Opisy p o kazuj ą róż noro d n e kontrolki tworzące intuicyjny interfejs do wybierania pliku, który ma zos tać otwarty. Sprawia to, ż e okno dialogowe j est proste w u życiu , mimo że jest w nim sporo mo ż l iwości wyboru. Do utworzenia i wyśw ie t le n ia okna dialogowego w progra mie MFC potrzebne są dwa elementy: fizyczny wyg ląd okna dialogowego, który jest defi niowa ny w pliku zaso bów, oraz obiekt klasy dialogu używany do o bsług i operacji okna dialogowego i je go kontro lek. MFC dostarcza kl as ę CDi al og, którą mo żem y u żyć po zdefi niowa niu zaso bu dialogu.
Poznaj kontrolki W system ie Windows dostępnych jest wiele róż nyc h kontrolek, a w wię kszośc i przypadków ich wy g ląd i sposób działa n ia są elastyczne. Większość z nich n al e ży do je dnej z sześci u kategorii przedstawionych poniżej :
Rozdział 16.
• Praca zoknami lIialogowymi i kontrolkami
873
Typ kontrolki
Co robią?
Kontr olki statyczne
S ą u żywan e
Przycis ki
Prz yciski d o starczaj ą mechanizm wpr owadzan ia dan ych jednym k likn i ęcie m . I s tni eją trzy rodzaje przycisków: zwy kł e przyciski, przyciski opcji , gdzie tylko j edn a m oże b y ć wybrana w danym mo menc ie, i pola wyboru , gdz ie w danym mome ncie może być kilka zaznac zonych .
Paski przewijani a
Paski przewijania są zwykle używane do przewijania tekstu lub obrazów zarówno w pionie, jak i w pozi omie wewn ątrz innej kontrolki.
Pola list
Zawi e rają one l i stę d o stępn y ch m o żli w ości więcej
do dostarc zania
tytuł ó w
lub inform acj i opisowych.
wybo ru. M o żna
wy b rać je d ną
pozwal aj ą wprowadzać
edytow ać wy świetlany
Kontrolki edycj i
Kontrolki edycji
Listy kombin owane
Lis ty kombinowane prz edstawi aj ą l istę dos tępn ych opcji, które m o żn a oraz dają możli wość samo dzielnego wprowadzenia tekstu .
Rysunek 16.2 przedstawia
lub
opcji .
przykł ady ró żn y ch
lub
tekst. w ybrać,
typów kontrolek. Pole listy zaw iera predefiniow aną elementów, któ re można w yb ie rać. Pasek prz ewijania może nie być potrzebny w przypadku krótk iej listy. Lista może zawiera ć wiele kolum n i może by ć przewijalna w pozio mie . Dostępna jest równi e ż wersja mogąca za w ie rać zarów no ikony, jak i tekst list ę
Kontrolki statyczn e dosta r c zaj ą statycznych informacji, takich jak tytuł czy instrukcje, lub po prostu sta n o w i ą dekorację ok na dialogowego w postaci ikony bądź pokolorowanego pro stokąta Przyciski opcji są przeważnie
zgrupowane, tak że
może zos tać zaznaczony tylko j ede n z ni ch Pola wyboru są zaznaczane pojedynczo i w danym momencie może być
ich zaznaczonych kilka Przyciski mogą - takjak tutaj - m i e ć etykiety, ale mogą
również zaw i erać ikony Widz iał eś już paski przewijan ia doczepione do obszaru klienta okna w programie Sketcher. Paski przewija nia mogą równi eż występowa ć samodz ielni e
Listy komb inowane łączą w sobie m ożliwo ś c i listy rozwijanej, z której można wybierać elementy z m ożl iwo ścią samodzielnego wprowadzania dany ch. Okno dialogowe Zapisz jako ... u żywa listy kom binowanej, za pomo cą której można wprowadzić na zwę pliku
Rysunek 16.2 Kontrolka może , ale nie musi by ć powi ązana z obiektem klasy. Kontrolki statyczne nie robi ą nic bezpo średnio , w ięc powi ąza ny obiekt klasy może wydawać s i ę niepotrzebn y. Istnieje jednak klasa MFC - CSt ati c, dostarczająca funkcje umożliwiaj ąc e zmian ę wyglądu kontrolek statycznych . Takż e przyciski w wielu przypadkach mogą b yć obsłużon e przez obiekt okna
874
Visual C++ 2005. Od pOIlstaw dialogowego , ale pon ownie MF C dostarcza kla sę CButton na wypa dek potrzeb y pos iadan ia obi ektu klasy do zarządzania kontrolką. MFC dostarcza także cały zestaw klas do obsługi innych kontrolek. Poni eważ kontrolka j est oknem, wszystkie są wyprowadzan e z CWnd.
Wspólne kontrolki Zestaw standardowych kontrol ek, które s ą obsługi wane przez MFC i edytor zasobów, nazywany jest kontrolkami wspólnymi (ang. comm on controls). Wsp ólnymi kontrolkami są wszystkie powyżej omówione, a także inne bardziej złożone kontrolki, takie jak na przykład kontrolka animacji, która ma możliwo ś ć odtwarzania plików A VI (ang. Audio Video Interleaved), i struktura drzewiasta umożliwi ająca przedstawienie hierarchii elementów w postaci drzewa. Kol ejną przydatną kontrolką w zestawie kontrolek wspóln ych jest pokrętło . M o żna go używać do zw i ęk sza n i a i zmn iej szani a warto ś c i z n aj duj ąc y c h się w powiązanej kontrolce edycj i. Omówienie wszystkich dostępnych kontrolek wykracza poza zakres niniejs zej książki , więc przed stawię tylko kilka przykład ów (łączn ie z uży ciem pokrętła) i zai m p l eme n tuj ę je w programie Sketcher.
Tworzenie zasobu okna dialogowego Prz ejdźm y
do konkretnego przykładu. Możemy dodać okno dialogowe do programu Sketcher, wybór szero koś ci pióra użytego do rysowani a elementów. To wiąże s i ę z koniecznością zmiany bieżącej szero ko ści pióra w dokumencie, a także w klasie CE l ement oraz dodaniem lub zmod yfikowaniem funkcji obsługujących szerokość pióra. Zajm iemy się tym po utworzen iu ok na dial ogowego.
które
umożliw i
Wyświetl panel Resource View, rozwiń drzewo zasobów programu Sketcher, kliknij prawym przyciskiem folder Dialog, a n a stępnie wybierz Insert Dialog z menu kont ekstowego, aby doda ć nowe okno dialogowe do zasobów programu Sketche r. Uruchomi s i ę edytor Dialog Resource i wy świetli okno dialogowe w panelu edycji wraz z panelem Toolbox zawierającym li stę dostępnych kontrolek. W oknie dialogowym zn aj d uj ą s i ę przyciski OK i Cancel. Dodawanie nowych kontrolek do okna dialogowego jest bardzo proste - wystar czy przeciągnąć żądaną kontrolkę z palety na wybrane miejsce w oknie dialogowym. Ewentua lnie można kliknąć kontrolkę z listy, aby j ą wybrać, a następnie k l iknąć miejsce w oknie dialogowym, w którym chcemy ją umieścić. Po umiejscowieniu kontrolki wciąż mo żna ją przenosi ć oraz zmieniać jej rozmiar poprzez przeciąganie jej krawęd zi. Dom yślnie oknu dialo gowemu przypisany jest identyfikator IDO_D IALOGI, j ednak lepiej mi eć bardziej opi sową nazwę. Identyfikator można zmienić popr zez kliknięcie prawym przyci skiem myszy nazwy okna dialogowego w panelu Resource View i wybranie Properties z menu kontekstowego. Właściwo ści okna dialogowego można też wyświetlić, klikając prawym przyciskiem myszy w panelu Dialog Editor i wybierając j e z menu kontek stowego. Zmi eń identyfikator na coś bardziej zwi ązanego z przeznaczeniem okna dialogowe go, np. IDR_PENWIDTH_CLG . Pr zy okazji możesz zmienić też wartość właściwości Caption na Set Pen Wi dth.
Rozdział 16.
• Praca zoknami dialogowymi ikontrolkami
875
Dodawanie kontrolek do okna dialogowego Aby zapewnić mechanizm wybierania szerokości pióra, dodaj kontrolki do podstawowego okna dialogowego, tak aby wyglądało jak na rysunku 16.3.
Rysunek 16.3 przedstawia siatkę, której mo żna uży ć do umiejscowienia kontrolek. Jeżeli siatka nie jest wyświetlana, użyj odpowiedniego przycisku paska narzędzi ; przycisk paska narzędzi włącza lub wyłącza wyświetlanie siatki. Można również wy świetlić linijki po bokach i u góry okna dialogowego, za pomocą których można utworzyć prow adnice (rysunek 16.4).
odpowiedniej linijki możesz utworzyć prowadnicę poziomą. Aby ją umiejjej s trza łkę wzdłu ż linijki. Do umiejscowienia kontrolki możes z posłuży ć
się więcej niż jedną prowadnicą.
Okno dialogowe posiada sześć przycisków opcji umożliwiających wybór szerokości pióra. Są one umiejscowione w polu grupy z nagłówkiem Pen Widths . Pole grupy otacza przyciski opcji i sprawia, że stanowią one grupę , z której tylko jeden może być w danym momencie wybrany . Każdy przycisk opcji ma odpowiednią etykietę identyfikującą szerokość pióra. Dostępne są także domy ślne przyciski OK i Cancel zamykające okno dialogowe. Każda kontrolka w oknie dialogowym posiada własny zestaw właściwości. Można się do nich dostać i je zmieniać w taki sam sposób jak w przypadku właściwości okna dialogowego. Kolejnym krokiem je st dodanie pola grup . Jak w spominałem , pol e grupy słu ży do powią zania przycisków opcji w grupę oraz dostarcza nagłówek i obramowanie grupy przycisków. Gdy potrzebny jest więcej niż jeden zestaw przycisków opcji, do ich poprawnego działania
876
VisualC++ 2005. Od podstaw niezbędne jest grupowanie. Kliknij przycisk odpowiadający polu grupy w palecie wspólnych kontrolek, a następne kliknij przybliżoną pozycję środka grupy przycisków. W oknie dialogowym zostanie umieszczone pole grupy o domyślnym rozmiarze . Następnie możesz przeciągać obramowanie pola grupy, aby pomi eściły się wszystkie przyciski. Aby ustawić nagłówek pola grupy, wpisz go we właściwo ści Caption (w tym przypadku wpisz Pen Wi dths ).
Ostatnim krokiem jest dodanie przycisków opcji . Wybierz kontrolkę przycisków opcji, a następnie kliknij miejsce wewnątrz pola grupy w oknie dialogowym, w którym chcesz ją umieścić. Wykonaj to samo dla pozostałych sześciu przycisków opcji. Następnie zmień podpis ka żdego przycisku opcji . J eżeli zachodzi taka potrzeba, zmień rozmiar przycisku poprzez przeciągnięcie jego obramowania. Aby wyświetlić okno właściwości kontrolki, zaznacz ją, a następnie wybierz Properties z menu kontekstowego. W oknie właściwości zmień identyfikator każdego przycisku, aby lepiej odzwierciedlał je go przeznaczenie: IDC_P ENWIDTHO dla pióra o szerokości] piksela, ID_PENWIDTHl dla pióra o szerokości 0,01 cala, IDC_PENW IDTH2 dla pióra o szerokości 0,02 cala itd. Rozmieś ć
poszczególne kontrolki, przeciągając je za pomocą myszy . Możesz także wybra ć kontrolek, klikając je po kolei z na ci śniętym przyciskiem Shift lub przeciągając nad nimi kursorem z wciśniętym lewym przyciskiem myszy . Aby wyrównać grupę kontrolek, kliknij odpowiedni przycisk z paska narzędzi Dialog Editor, który został przedstawiony na rysunku 16.5. grupę
Rysunek 16.5
Pasek narzędzi jest przedstawiony w stanie niezadokowanym , a więc zos tał przeciągnięty z obszaru pasków n arzędzi u góry okna. Jeżeli pasek narzędzi nie jest widoczny, możesz go wyświetlić, klikając prawym przyciskiem myszy w obszarze pasków narzędzi i wybierając go z wyświetlonej listy. Możesz także wyrównać kontrolki w oknie dialogowym, wybierając odpowiednią opcję z menu Format .
Tes10wanie okna dialogowego Zasób okna dialogowego jest już ukończony. Możesz go przetestować, klikając przycisk zn aj duj ąc y się po lewej stronie paska narzędzi przedstawionego na rysunku 16.5 lub używając kombinacj i klawiszy Ctrl+T. Zostanie wyświetlone okno dialogowe, w którym dostępne będą podstawowe operacje kontrolek, więc możesz klikać przyciski opcji. Gdy utworzona jest grupa przycisków opcji , wybrany może być tylko jeden. Po wybraniu jednego poprzednio wybrany przestaje być aktywny. Kliknij przycisk OK lub Cancel albo nawet ikonę Zamknij w pasku tytułu okna dialogowego, aby zakończyć testowanie. Po zapisaniu zasobu okna dialogowego jesteśmy gotowi do dodania kodu do jego obsługi.
Rozdział 16.
• Praca zoknami dialogowymi ikontrolkami
877
Programowanie okna dialogowego Z programowaniem okna dialogowego związa ne są dwie kwestie : jego wyświetlenie i obsługa d z i ał an i a jego kontro lek. Zanim b ę d z i em y mog li wyświet lać okno dialogowe o dpowiadające właśnie utworzonemu zasobowi, musimy utworzyć dla niego kl as ę okna dialogowego . Pomoże nam w tym krea tor klas.
Dodawanie klasy dialogu Kliknij prawym przyciskiem myszy okno dialogo we, które właś n i e utw orzyłeś w panelu Resource Editor, a następn i e wybierz Add Class z menu podrę cznego, aby uruc homić kreator klas. Zdefmiujemy now ą k lasę dialogu, wyprowadzan ą z klasy MFC CDi al og, wi ę c wybierz tę nazwę klasy z listy rozwijanej Base c/ass. W polu teks towym C/ass nam e wpisz CPenDi al og jako nazwę klasy. Okno kreatora klas powinno wyg lądać jak na rysunku 16.6.
Rysunek 16.6 Welco me t o the MFCClass Wizard
N03 mes
qassname;
Docum ent 'ternplete 5trin gs
! c p enD ialogl !łase
dass:
0~one
.h file:
O f!utomation
!penDialog,h
.c'lPfile:
iPenO~log , CllP
O Active acc0SetRa nge(l . 8); ret urn TRUE ; II
WYJĄTEK:
II Zwraca TRUE, chyba że usta wisz fo kus dla kontrolki. str ony właś ciwoś ci OCX powinny zwracać FALSE.
Należy dodać jedynie trzy wiersze kodu wraz z czterema wierszami komentarzy. Pierwszy wiersz kodu tworzy wskaźnik do obiektu klasy MFC CSpi nButtonCt r l. Ta klasa przeznaczona jest do zarządzania pokrętłem i jest inicjalizowana w następnej instrukcji, aby wskazywała do kontrolki w naszym oknie dialogowym . Funkcja GetDlgl tem( ) jest dziedziczona z CWnd przez CDi alog i pobiera adres kontrolki, której identyfikator zostanie przesłany jako argument. Jak widziałeś wcześniej , kontrolka jest jedynie wyspecjalizowanym oknem, więc zwracany wskaźn ik jest typu CWnd*; z tego względu należy go rzutować do typu odpowiedniego dla konkretnej kontrolki, w tym przypadku CSpi nButtonCt r l*. Trzecia instrukcja ustawia górną i dolną granicę pokrętła poprzez wywołanie funkcji składowej Set Range( ) obiektu pokrętła. Pomimo że ustawiliśmy zakres dla kontrolki edycji, nie ma to bezpośredniego wpływu na
Rozdziai 16.• Praca zoknami dialogowymi ikontrolkami
895
pokrętło . Jeżeli nie ograniczymy tutaj wartości dla pokrętła, zezwolimy mu na wstawianie do kontrolki edycji w artości wykraczających poza limit , co spowoduje komunikat o błędzie z kontrolki edycj i. Możesz s ię o tym przekon a ć , gdy dodasz komentar z do instrukcj i wywołującej Set Range() i przetestujesz bez niej program Sketcher. Jeżeli
chce sz u stawić kontrolkę powiązaną w kodzie, zamiast u aktywniać właśc iwo ść Auto Buddy pokrętł a , w klasie CSpi nButtonCt r l dostępna jest umożliwiająca to funkcja składowa. Musiszjedynie d odać poniższą instrukcję w miej scu oznaczonym dwoma komentarzami:
pSpin->Set Buddy(Get Dlgltem(IDC SCALE» ;
WYświellanie pokrętła Okno dialog owe ma zostać wy świetlone po wybraniu opcji Scale z menu (lub odpowiadajej przycisku w pasku n arzędzi ), więc za pomocą okna Properties klasy musimy doda ć do klasy CSketcherVi ew pro cedur ę obsługi komunikatu COMMAND odpowiadającego komunikatowi ID_VI EW_SCALE. Dodaj pon i żs zy kod;
j ącego
void CSket cherV iew: :OnV iewScale( ) I CScaleD ialog aDlg : aDl g.m_Sca le = m_Scale; if (aD lg .DoModa l ( ) ~ ~ IDOK)
II Utwórz obiekt okna dialogowego. II Prz esyła s ka lę widoku do okna dialogowego.
(
m_Scale = aDl g.m_Scale ; Inva li date Rect( O) ;
II Pobiera nowy wspolczynnik skali. II Unieważ n ia cale okno.
Modalne okno dialogowe tworzymy w ten sam sposób jak w przypadku okna dialogowego umożliwiającego wybór szerokości pióra. Zanim okno dialogowe zostanie wyświetlone przez wywołanie funkcj i DoModal ( ), zapisujemy warto ść skali dostarczoną przez m_Sca l e - skła dową CS ket cher Vi ew - w składowej okna dialogowego o tej samej nazwie. Dz ięki temu kontrolka będzie wyświetlała bi eżącą wartość skali po w y świetleniu okna dialogowego. Jeżeli okno dialogowe zostanie zamknięte za pomocą przycisku OK, zapisujemy nową s kal ę ze składowej m_Scal e obiektu okna dialogowego w składowej widoku o tej samej nazwie. Ponieważ zmieniliśmy skal ę widoku, będziemy musieli przerysować widok w nowej skali. Zajmie się tym wywołan ie I nval idateRect ( ) . Oczywiście wię c
dodaj
nie możemy zapomnieć o dodaniu s kładowej m_Scal e do definicji CSket cher Vi ew, wiersz na końcu pozo stałych danych składowych w definicji klasy :
p oniższy
i nt mScale :
II Aktu alna skala widoku .
Musimy zmodyfikować konstruktor CSket cher Vi ew, aby inicjalizował m_Sca l e z wartością l. Dzięki temu widok zawsze będzie najpierw wy świetlany w skali jeden do jednego. Uwaga: jeżeli o tym zapomnisz, j est mało prawdopodobne, że aplikacja będzie działała poprawn ie. Ponieważ
odnosimy się do klasy CScal eDi al og w implementacji klasy CSket cher View, musimy #in cl ude dla pliku ScaleDialog.h na początku pliku Sketch erView.cpp. To
dodać dyrektywę
896
Visual C++ 2005. Od podstaw wszystko, co musimy zro b ić, aby okno dialogowe skalowania i pokrętło działały. Skompiluj i uruchom program Sketcher, aby go wypróbować , zanim dodasz kod wykorzystuj ący współ czynnik skali w procesie rysowania.
Korzystanie ze wsPólczynnika skali Skalowanie w systemie Windows zwykle wi ąże się z wykorzystaniem jednego ze skalowalnych trybów mapowania: MM_I SOTROPI C lub MM_AN I SOTROPle. Dz i ęki u życiu jednego z tych trybów mapowania Windows wykona za nas więk szość pracy. Niestety nie polega to na zwykłej zmianie trybu mapowania, p onieważ żaden z tych trybów nie jest o bsług iwany przez eSera1lView. J e żeli jednak uda s ię nam to obejść, b ędziemy w domu. Z powodów, które obj aś nię za chwi l ę , wykorzystamy tryb mapowania MM_ANI SOTROPI C, wi ęc najpierw poznajmy go bl i żej .
Skalowalne tryby mapowania dwa tryby mapowania umożliwi aj ąc e mapow anie mi ędzy logiczn ymi a współrzędnym i urząd zenia i są to tryby MM_I SOTROPl C oraz MM_ANISOTROPI C. Tryb IVIM_I SOTROPl e cechuje s ię tym, że Windows wymusza, aby ws pó łczyn niki skali zarówno dla osi x, jak i y były równe, co ma tę zaletę , że koła zawsze po zo stan ą koł am i . Wad ą takiego ro zw i ązania jes t fakt, że nie m o żem y m ap ow ać dokumentu tak, aby p asował do pros tokąta innego kształtu . Z drugiej strony tryb MM_AN I SOTRO Ple umożliwi a niezal eżn e skalowanie osi x i y. P on iew aż jest on bardziej elastyczny, wykorzystamy go do przeprowadzania skalowania w programie Sketcher. Jak
w s p omin ałem , do stępne s ą
w sp ółrz ędnymi
Sposób, w jaki logiczne współrzędne są zamieniane na współrzędne parametrów:
urządzen ia ,
jest zal eżny od
poniższych
Paramelr
Opis
Windm'/ Or igin (poc zątek układu współrzęd ny ch okna)
Logiczne w spółrzędne lewego górnego rogu okna. Jest on ustawiany przez w ywołanie funkcji COC : :Set WindowOrgO.
Window Ent ent (zakres okna)
Rozmiar okna określony we współrzędnych logicznych, Jest on określan y przez wywo ł an i e funkcji COC: : SetW i ndowExt O.
Viewport On gln (po cząt ek układu wziernika)
lewego górnego rogu okna we ws pó ł rzę d nyc h (pikselach). Jest on ustawiany przez wywołanie funkcji CDC : :Set Viel'/portOrgO.
V1ewport Entent (zakres wziernika)
R ozmiar okna we wsp ółrzędny ch urządzenia (pikselach). Jest on ustawiany przez wywołanie funkcji COc : : SetVi e\'/portExt ().
wsp ółrzę d nych
W spółrzędne urząd zeni a
Wziernik, o którym tu mowa, nie ma ża d nego fizycznego znaczenia. S łuży on tylko jak o parametr dla okre śl ania sposobu, w jaki w sp ółrzędne są zamieniane z logicznych w spółrzęd nych na w spółrzędn e urz ądzenia.
Rozdział 16.•
Praca zoknami dialogowymi i kontrolkami
897
Zapam i ę taj , że :
•
Współrzędn e łogiczn e (zwa ne także współrzędn ymi strony) są okreś l ane przez tryb mapowania. Na przykł ad w trybie mapowania MM_LO ENG LIS H j e d no s tk ą współrzę dnych jest 0,01 cala , początek u kła d u znajduje się w lewym gó rnym rogu obszaru klien ta, a w artoś ci y rosn ą z dołu do góry. Są one u żywane przez funkcje rysowania w kontekś c i e urządze ni a.
•
W spółr zędne ur ządzenia
(zwane także współrz ędn ymi kłi en t a w ok nie) są w pikse lach w przypadku okna z p o czątk i e m u kł a du w lewym gó rnym rog u obszaru klienta i wartościami y rosnący mi z góry do dołu . Są one u żyw an e poza kon te ks tem urz ąd zen i a , na prz ykład do definiowani a pozycji kurso ra w procedurach obsługi komu nikatów my szy .
wyrażone
•
Współ rz ę d ne ekranu są wy rażo ne w pi kselach i mają początek uk ład u w lewym górnym rogu ekranu, a wartości y ros ną z góry do dołu . Są one uży wane do pobierania lub ustawian ia pozycj i kurso ra.
Wzory
używane
przez system Windows do konwersji ze
współrzę d nyc h
logicznych na w sp ół
rzędn e urz ąd zen i a wyg lądają n a s t ępuj ąc o :
. ( . . ) x ViewportExt . xlrevice == xl.ogical - x WmdowOrg * . + x Viewportćlrg xWmdowExt ' l - y Wm ' d ow Org )* yViewportExt ' Org · == (L y Devtce y ogica . + y uv lewport y Wm do wEx t
W przypadku systemów wspó łrzęd nych inn ych ni ż te dostarczane pr zez tryb y map owani a MM_ ISOTROPIC i MM_ANISOTROPI C, zakres okna oraz zakres wz iern ika są ustalone przez try b mapowania i nie można ich zmie n ić. Wyw ołani e funkcji Set Wl ndowExt ( ) lub SetVi ewpo rt Ext ( ) w obiekcie COC w celu ich zm iany nie daje rezult atu, c hoć można prz enie ść pozycję (O, O) w logicznym u kładzi e odniesie nia poprzez wywołanie funkcj i Set WindowOrg( ) lub SetViewportOrg(). J e dnakże dla danego rozmiaru dok umentu wyrażonego przez zakres okna w j edno stkach współrzę dnych logicznych m o żn a dos tosować ska lę , w jakiej wyświe t lane są eleme nty, jedynie poprzez odpow iednie ustawienie zakres u wziernika . D zi ęki użyc iu i ustawieniu zakresów wz iernika i okna skalowanie może b yć wykonywane automatycznie.
Ustawianie rozmiaru dokumentu Musi my
ut rzym ać
rozmiar dokumentu w jednostkach logicznych w obi ekcie dok ume nt u. m_DocSi ze, do definicji klasy CSket cherDoc. Będzie ona roz mia r do kumentu:
Może my d odać chro nio ną składową, przec ho w yw ała
(Si ze mDoeSize; B ędz i emy także
II Rozmi ar dokum entu.
chcie li u zyskać dost ęp do da nych s kładowych z klasy widoku, wi ęc dodamy do defini cj i klasy CSket cherDoc:
p u b liczną funkcję
(Size GetOocS ize( ) { ret urn m_OocSi ze:
II Pobierz rozmiar dokumentu .
898
Visual C++ 2005. Od podstaw W konstruktorze dokumentu musimy zain icj al izować m en tacj ę CS ket cher Ooc( ) w n ast ępujący sposób:
U żyj emy zapisu współrzędnych MM_LOENGLI SH, więc po prostuj traktuj jednostki logiczne jako 0,01 cala, a ustawiana wartość skutkuje obszarem do rysowani a o rozmiarach 30 na 30 cali.
Ustawianie trybu mapowania Tryb mapowania MM_AN I SOTROPI C ustawim y w przesłonię ciu dziedzi czonej funkcji OnPr epa r eOC() w klasie CS ketc her View. Ta funkcja wywoływana jest dla każdego komunikatu WM_MESSAGE wted y, gdy rysujem y tymc zasowe obiekty w pro cedurach obsługi komunikatów myszy. Musi ona jednak zrobić wię cej niż tylko zmienić tryb mapow ania. Musimy prze słon ić funkcję w klasie CSket cher Vie w, zanim dodamy kod. Otwórz okno Properties dla klasy CS ket cher vt ew i kliknij przycisk Overrides w pasku narzędzi . N astępnie dodaj przesłoni ęci e poprzez wybranie OnPrepar eDC z listy i kliknięcie OnPrepar eOC w sąsiedniej kolumnie . Będziesz mógł wpisać kod bezpo średni o w panelu edycji. Implementacj a OnPr epar eOC wyg lą da na stępuj ąc o :
void CSkete herV iew : :OnPrepareDC CCDC* pOc. CPri ntl nfo* plnfo) ( II Do zrob ienia : wp isz lu
własny,
niestandardowy kod i (lub)
wywo łaj klasę bazo wą.
CSero11 View: :OnPrepa reDC CpDC. pInfo: ; CSketcherOoc* pOoc = Get Document ( ): pDC->Set MapMOdeCMM_AN ISOTROP IC): II Ustawia tryb map owania. CS ize DocS i ze ~ pDoc->GetDocS i zeC): II Pobiera rozmiar doku mentu. II Wymiar y musi być ujem ny. p onieważ chcemy u ży ć try bu MM~ OENGLISH.
OocSi ze .cy = - OocSlze.cy: II Zm ień znak y . pOC->SeUJl ndowExt (DocSize) : II Ustaw wym iar okna. II Pobierz
licz bę
p ikseli na cal w wymiarach x i y .
int xLog Plxel s = pOC ->GetOeviceCa psCLOGP IXE LSX ); i nt yLog Pixels = pOC ->Get DevlceCa psC LOGPIXELSY ); II Oblicza rozm iary x i y wz iern ika.
long xExtent long yExt ent
= =
stat lc_cast COocSlze.cx)*m_Scale*xLogPixels / lO OL; stat ic_cast (OocSize.cy)*m_Sca le*yLogP ixel s/ l OOL:
pDC ->SetV iewportExtCst at ic_cast (xExt ent ). stat i C cast C-yExtent )):
II Ustaw ia obszar wziernika.
Rozdzial16. • Praca zoknami dialogowymi ikontrolkami
899
Prz es łon ięc ie
funkcj i kla sy bazowej jest tutaj d o ś ć nietypowe, p oni eważ pozostawili śmy CSc ra l lV i ew :OnPr epareDC( ), a modyfikacje wprowadziliśmy dopiero za tym wywołaniem funkcji z klasy bazowej, a nie w miejscu wskazanym przez domyślne komentarze. Gdyby klasa był a wyprowadzana z CV iew, zastąpilibyśmy wywołan ie funkcji klasy bazowej, ponieważ nic by ono nie robiło , jednak w przypadku CSer a11 Vi ew tak nie jest. Potrzebujemy funkcji z klasy bazowej do ustawienia niektórych atrybutów przed ustaleniem trybu mapowania. Nie p opełnij błędu i nie umieszczaj w ywołan i a funkcji klasy bazowej na końcu przesłoni ęci a , ponieważ gdy tak zrobisz, skalowanie nie będzie d zi ałać . wywołanie
Po ustawieniu trybu mapowania i uzyskaniu zakresu dokumentu ustawi amy zakres okna za pomocą ujemne go zakresu y. Robimy tak w celu utrzymania spójno ści z poprzednio u żywa nym trybem MM_LOENGLI SH, gdzie jak sobie przypominasz , początek układu znajduje si ę u góry, wi ęc wart ości y w obszarze klienta s ą ujemne . składowa CDC GetDevi ceCa ps() dostarcza informacje o urz ąd zeniu, z którym pojest kontekst urządzenia. Możemy uzyskać różnego rodzaju informacje o urządz e niu, w zale żno ś ci od przesłan y ch argumentów. W tym przypadku argumenty LOGPIXELSX i LOGPIXELSY zwrac aj ą l iczbę pikseli na logiczn y cal w kierunkach x i y. Te wartości odpowi ad ają 100 jednostkom w naszych wsp ółrzędnych logicznych.
Funkcja
wiązany
Użyjemy tych wartości do obliczenia wartości x i y dla zakresu wziernika i przechowamy je w zmiennych lokalnych xExtent oraz yExt ent . Zakres dokumentu wzdłuż osi wyrażony w jednostkach logicznych podzielony przez 100 da zakres dokumentu wyrażony w calach . J eżeli podzielimy to przez liczb ę logiczn ych pikseli na cal w urządzeniu , uzyskam y odpowiedn ią liczbę pikseli dla zakresu. Jeżeli wtedy użyjemy tej wartości jako zakresu wziernika, uzyskamy dokument wyświetlony w skali l do l. Jeżeli uproś cimy wzory na konwersję między w sp ół rzędnymi urządzen ia a logicznymi poprzez założenie , że początek układu dla okna i wziernika to (O, O), b ęd ą one miały następuj ącą postać :
x ViewportExt xD evice = xl.og ical *--~-
x Window Ext y Viewportlixt yDevic e = yLogical *"----- --'----- yWindowExt Jeżeli pomnożymy wartości
zakresu wziernika przez skalę (przechowywana w m_Sca l e) elementy będą rysowane według w arto ści m_Sca l e. Ta logika jest dokładnie odzwierciedlona w wyrażeniach dla zakresu wz iernika względem osi x i y w kodzi e. Uproszczone równania zawierające skalę wyglądają następująco :
L ' 1* x Viewpo rtEx t * m Scale · x D evtce = x og tca x WindowExt ·
L'
y Dev tce = y ogtca Powinieneś zauważyć, że
1* Y ViewportExt * m Scale y WindowExt
pary współrzędnych urządzenia różnią się proporcjonalnie do wartoskali. Współrzędne w skali równej 3 są trzykrotno ścią współrzędnych dla skali wynosząc ej 1. Oczywiście , oprócz powiększania elementów, zwiększanie skali odsuwa je także od począt ku układu wsp ółrzędnych. ści
900
Visual C++ 2005. Od podstaw To wszystko, czego potrzebujemy do skalowani a widoku. Niestety w tej chwili przewijanie nie będzie dział a ć ze skalow aniem, w ięc musimy zo b aczyć, jak temu zara d z ić .
Implementowanie przewijania ze skalowaniem CSer oll Vi ew po prostu nie dzi ała z trybem mapow ania MM_ANI SOTROP I C, w i ęc musim y użyć innego trybu mapowania do ustawienia pasków przewijania. Najprościej można to z ro b ić , wykorzys tuj ąc tryb MM_TEXT, poni eważ w tym przypadku w sp ółrzędne logiczne s ą takie jak w spółrzędne klienta - wy rażo n e w pikselach. Musimy więc jedynie poznać liczbę pikseli od p o w i adającą logicznemu zakresowi dokumentu dla skali, w której rysujemy, co jest prostsze, niż myślisz. Możemy dodać fu nkcj ę do CSk et eherVi ew, która zajmie się paskami przewijania, i za imp lementować tam wszystko. Kliknij prawym przyciskiem myszy n azwę klasy eSket eher Vi ew w panelu Class View i dodaj publiczną funkcj ę ResetSer ol l Si zes( l , która zwraca typ voi d i nie przyjmuje parametrów. Implementacja tej funk cji wygląda na stępująco :
void CSket cherView: :Reset Scrol lSizes(void) {
CCl ient OC aOC (t hi s); On Prepa reOCC&aOC ); CS ize OocSize ~ Get Oocument ()->Get OocSl ze( ); aOC.LPt oOP(&OocSize) ; Set Scrol lSi zes(MMTEXT. OocSize) ;
II Ustawia kontekst urządzenia . II Pob iera rozmia r doku mentu. II Pobiera rozmiar w p iksela ch. II Ustawia pas ki przewija nia.
Po utworzeniu lokalnego obiektu eC l i entDe dla widoku wywołuj emy OnP r epa reDC() w celu ustawienia trybu mapowania 1~1~_AN I S DTR D P l e . Ponieważ brane tu je st pod uwagę skalowanie, składowa LPt oDP( l obiektu aDe przek ształca rozmiar dokumentu przechowywan y w zm iennej lokalnej DoeS i ze na poprawną li czbę pikseli dla b ieżącego rozmiaru i skali dokumentu. C ałko wity rozm iar dokumentu w pikselach okre śla, j ak du ży musi być pasek przewijania w trybie MM_TEXT - pamiętaj, że logiczne współrzędne w tym trybie są wyrażone w pikselach . N as tęp nie na tej podstawie składowa SetScr o11 Si zes(l klasy eSe r ol lVi ew ustaw ia nam paski przewijania , okre ślaj ąc MM_TEXT jako tryb mapowania. M oże się wydaw a ć
dziwne , że zmieniamy tryb mapowania w ten sposó b, ale należy pam iętać , tryb mapowania jest niczym więcej jak tylko definicją sposobu, w jaki logiczne w spółrzęd ne s ą zamieniane na współrzędne urządzenia. Którykolwiek tryb wybierzesz (a tym samym algorytm konwersji), ma on wpływ na wszystkie kolejne funkcje kontekstu urządzenia, dopóki go nie zmien isz, co możesz zrobi ć kiedykolwiek zechcesz. Gdy ustawisz nowy tryb, kolejne funkcje kontekstu urządzenia używ aj ą algorytmu konwersji określ onego przez ten nowy tryb. Sprawdzamy rozmiar dokumentu w pikselach w trybie MM_AN I SOTROPIC, ponieważ jest to jedyny sposób na uwzględnienie skali, a na stępnie przełączamy na tryb MM_TEXT, aby u stawić paski przewijania, pon i eważ do ich poprawnego dział ania potrzebujemy pikseli jako jednostek. Jest to naprawdę proste, jeżeli się wie, jak to zrobić. że
UstaWianie pasków przewijania Na początek musimy klasy eSket eher Vi ew.
ustawi ć
paski przewijania dla widoku w skład owej Onl nitia lUpdat eO tej funk cji na poniżs zą:
Zmień poprzednią implement ację
Rozdział 16.•
Praca zoknami dialouowrmi i kontrolkami
901
vet d CSket cherVi ew : :OnInit ial Updat e( )
Reset Sero11Si zes(): CSero11Vi ew: :Onlniti al Updat e():
II Ustawia pa ski prz ewijani a.
Wywołujemy jedynie funkcję ResetSero11Si zes ( ), którą dopiero co dodaliśmy do widoku . Zajmuje s ię ona wszystkim - no, prawie wszystkim. Do poprawnego działania obiekt eSero1lV iew wymaga, aby dla OnPrepareOoe był ustaw iony początkowy zakres, więc musimy dodać jedną in strukcję do konstruktora eSket eherVi ew:
CSk et eherView: 'CSket eherView() m_Fir st Poi nt (O. O) m_SeeondPoint (O .O ) m_pTempE1ement (NULL) m_pSeleet ed(NULL) m_Mo veMode(FALSE) m_CursorPos(CPoi nt (O.O)) m_Fi rst Pos(CPoint(O ,O )) .m_Seale(l ) l
SetSe ro11Sizes(MMTEXT, CSizeCO ,O )) :
II Ustawia podany rozmiar pas ków p rzewijania .
Dodatkowa instrukcj a wywołuje tylko Set Sero11 Si zes () z dow olnym zakre sem , aby zainicjalizować paski przewijania przed nary sowaniem widoku. Gdy w idok zostaj e po raz pierwszy narysowany, wywołanie funkcji Reset Sero11 Sizes () w On lniti alUpdate () ustawi a popr awnie paski przewij ania. Oczywi ście , przy każdej zmianie skali musimy zaktualizować paski przewijania przed przerysowaniem widoku. Może s ię tym zająć procedura obsługi OnVi ewSeal e( ) w klasie eSket eherVi ew:
voi d CSket eherView: :OnVi ewSeale() CSea1eDi alog aDlg: aD1g .m_Seale = m_Sea1e: if (aDlg. DoModal C) ~= IDOK)
II Utwórz ob iekt okna dialogow eg o. II Przesyła skalę widoku do okna dialogowego .
(
mSea 1e = aD lg.m Seale: ResetSero11Sizes() ; Inva1idate Reet CQ ):
II Pobiera no wą ska lę.
II Dostosowuje pas ki przewijan ia do nowej ska li. II Un ieważn ia cale okno.
Użyci e
funkcji Re setSero11Si zes ( ), zajmującej s i ę paskami pr zewijania, ni e jest trudne . Wszy stkim zajmuje s i ę jedna dodatkowa linijka kodu .
Teraz możesz przekompil ować projekt i uru chom i ć aplikację . Przekonasz się, że pask i przewijan ia dział aj ą tak, jak powinny. Zauważ, ż e każdy wid ok obsługuje swój własny współczyn nik skalowania, n iezależny od pozostałych widoków.
902
Visual C++ 2005. Od podstaw
Praca zpaskami stanu Teraz, gdy każdy widok jest niezależnie skalowalny, zachodzi konieczność posiadania jakiejś informacji o tym , jaka jest skala danego widoku. Wygodnym sposobem będzie wyświetlanie skali w pasku stanu, który zo s tał domyślnie utworzony w aplikacji Sketcher. Domyślnie pasek stanu pojawia się na dole okna aplikacji, pod poziomym paskiem przewijania, choć można też wyświetlać go u góry obszaru klienta . Pasek stanu jest podzielony na segmenty zwane polami, których w pasku stanu aplikacji Sketcher są cztery. W polu po lewej stronie znajduje się napis Ready, pozostałe trzy znajdujące się po prawej stronie są używane do oznaczania, gdy aktywne są CAPS LOCK, NUM LOCK i SCROLL LOCK. W pasku stanu dostarczonym domyślnie przez kreator aplikacji można pisać, jednak należy do składowej m_wndSta tus Bar obiektu CMainFrame dla aplikacji, ponieważ ona go reprezentuje. Ponieważ jest to chroniona składowa klasy, należy dodać publiczną funkcję składową w celu zmodyfikowania paska stanu spoza klasy. Mógłbyś w tym celu dodać poniż szą publiczną funkcję składową do klasy CM ai nFrame : uzyskać dostęp
void CMai nFrame : :Set PaneText (int Pa ne. LPCTSTR Text ) {
m_wndStat usBar .SetPaneText (Pane. Text) ; Implementacja znajdzie się w pliku .cpp i musisz dodać deklarację funkcji do definicji klasy. Funkcja Set Pa neTextO ustawia tekst określony przez drug i parametr (Text ) w polu wskazanym przez pierwszy parametr (Pa ne) w obiekcie paska stanu reprezentowanym przez m_wndSt at usBar. Pola paska stanu są indeksowane od lewej strony, rozpoczynając od O. Teraz za pomocą tej funkcji możemy pisać do paska stanu spoza klasy. Na przykład :
CMa i nF rame* pFrame = (CMai nF rame*)AfxGetApp() ->m_pMa lnW nd : pFrame->Set Pa neText (O. oGoodbye cruel worldo): Ten fragment kodu pobiera wskaźnik do głównego okna aplikacji i wypisuje łańcuch tekstu do pola znajdującego się najbardziej po lewej stronie paska stanu. To całkiem niezłe rozwiązanie, jednak główne okno aplikacji nie jest miejscem dla skali widoku . Równie dobrze możemy mieć kilka widoków w programie Sketcher, więc naprawdę zależy nam na powią zaniu wyświetlanej skali z k ażdym widokiem . Lepszym podejściem byłoby dodanie do każdego okna potomnego osobnego paska stanu . Składowa m _wndSt atusBa r w CMai nFrame jest egzemplarzem klasy CSt at usBar. Możemy użyć tej samej klasy do zaimplementowania wła snych pasków stanu.
Dodawanie paska stanu do ramki Klasa CSt atu sBa r definiuje pasek sterowania z wieloma polami , w których można wyświe tlać informacje. Obiekty typu CSt atus Bar dostarczają ten sam zestaw funkcji co wspólne paski stanu Windows poprzez funkcję składową Get StatusBarCtrl O. Dla każdej ze wspólnych kontrolek systemu Windows istnieje klasa MfC, która ją kapsułkuje - ta dla wspólnej kontrolki paska stanu to CSt atusBa rCt rl. Jednak bezpośrednie jej użycie wiąże się ze sporym
Rozdział 16.•
Praca zoknami ItialogowJmi ikontrolkami
903
nakładem pracy w celu zintegrowania jej z innymi klasami MFC, ponieważ surowe kontrolki Windows a nie łączą si ę z MFC . Użycie CStatus Bar w programie Sketcher je st prostsze i bezpieczniej sze. Funk cja GetStatusBarCtr 10 zwraca odnie sieni e do obiektu CStat usBarCt r l , który dostarcza cały zestaw funkcji wspólnej kontrolki, a obiekt CStat usBa r zaj muje s i ę komunikacj ąz resztą MF C.
Pierwszym krokiem do wykorzystania tej klasy jest dodanie składowej dla paska stanu do definicji klas CChil dFrame, która jest oknem ramowym widoku, więc dodaj poniższą dekl aracj ę do publicznej czę ści klasy:
CStat usBar mSt at usBar; W tym miej scu może być kon ieczne p ewne wyjaśnienie. Paski stanu p owinny być częścią ramki. a nie widoku. Nie chcemy umożliwiać przewijania p asków stanu lub ryso wan ia na nich. Powinny one po prostu pozostawać na dole okna. Gdybyśmy dodali pasek stanu do widoku, pojawi/by s ię on wewnątrz pasków p rzewijania i by lby przewijany p rzy każdym przewijaniu widoku. Jakiekol wiek ryso wanie na cz ęści widoku zawie rającej pasek stanu powodowałoby jego przerysowanie , co skutkowałoby denerwującym migotaniem . Umieszczenie paska stanu j ako częś ci ramki p ozwala uniknąć tych p roblem ów. Skład ow a m _StatusBar powinna zostać zainicjalizowana tu ż przed wy świetleniem widocznego okna widoku . A zatem używając okna Properties dla klasy CChi ldF rame, dodaj do klasy funkcję, która będzie wywoływana w odpowiedzi na komunikat WM_C REATE przesyłany do aplikacji, gdy okno ma z osta ć utworzone. Dodaj poniżs zy kod do procedury o bsł ugi OnC reate ( ):
int CChi ldF rame:: OnCreate( LPCREATESTRUCT l pCreat eSt ruct ) {
DIChi ldWnd : :OnCreat e(l pCreateSt ruct ) == -l) i f (CM ret urn -1: II Tworzy pasek stanu.
m_Stat usBar .Create (t hl s ): II Oblicza
szerokość
tekstu. który chcemy
wyświet lić.
CRect t ext Rect : CCl ientDC aOC( &m_St at usBar ); aDC.SelectObject (m_Stat usBa r .GetFont ()) : aDC. DrawText (_n "View Scale:99 "i . -l. t ext Rect . OT_SI NGLELI NE lOT_CALCRECT) : II Przygot owuj e obszar mogący pom ieścić tekst.
i nt width = t ext Rect .Width( ): mStat usBa r .Get Stat usBarCtr l ( ).Set Part s(l . &wi dt h): /r Inicjal izuje tekst pa ska stanu.
mStat usBa r .GetSt at usBa rCtrl O .SetText ( n"Vi ew Scale :1"l. O. O) : ret urn O: Wygenerowany kod niejest zacieniowany. Mamy tu wywoł ani e fu nkcji OnCreat eO z klasy bazowej, która zajmuje si ę utworzeniem definicji okna widoku. Ważne jest, aby nie usuwać tej funkcji, pon iew aż okno nie zostanie utworzone.
904
Visual C++ 2005. Od podstaw Funkcja Create() w obiekcie CSt atus Bar tworzy pasek stanu. Wskaźnik th i s dla bieżącego obiektu CCh i l dFrame jest przesyłany do funkcji Create (), ustanawiając połączenie między paskiem stanu a posiadającym go oknem. Sprawdź, co dzieje się w kodzie, który dodałeś do funkcji OnCreate() .
Definiowanie części paska stanu Obiekt CStatusBar posiada powiązany obiekt CStatu sBarCtr l z jedną c zę ś c i ą lub większą ich liczbą. Części i pola w kontekście pasków stanu są synonimami - CSta t usBar odnosi się do pól, a CStat usBarCt rl do części. W każdej części można wyświetlać osobne informacje. Możesz zdefiniować liczbę części i ich szerokości poprzez wywołanie składowej SetParts () obiektu CSt atusBarCtr l . Ta funkcja wymaga dwóch argumentów . Pierwszym argumentem jest liczba części w pasku stanu, a drugim jest tablica określająca prawą krawędź każdej części wyrażoną we współrzędnych klienta. Jeżeli pominiesz wywołanie SetPart s (), pasek stanu będzie domyślnie zawierał jedną część zajmującą cały pasek stanu . Moglibyśmy tak zrobić, jednak nie jest to zgrabne rozwiązanie. Lepszym pomysłem jest ustawienie rozmiaru części w taki sposób, aby wyświetlany tekst ładnie się w niej mieścił . Tak właśnie zrobimy w programie Sketcher. Pierwszą rzeczą, jaką musimy zrobić w funkcji OnCreat e( ), jest utworzenie tymczasowego obiektu CRect, w którym zapiszemy prostokąt otaczający dla wyświetlanego tekstu. Następnie utworzymy obiekt CCl ientOC, który zawiera kontekst urządzenia z tym samym zakresem co pasek stanu. Jest to możliwe, ponieważ pasek stanu, podobnie jak inne kontrolki, jest po prostu oknem . Następnie
czcionka używana w pasku stanu (ustawiana jako część właściwości pulpitu) jest wybierana do kontekstu urządzenia poprzez wywołanie funkcji Sel ect Object ( ). Składowa GetFant ( ) m_Sta t usBar zwraca wskaźnik do obiektu CFant reprezentującego bieżącą czcionkę. Oczywiście konkretna czcionka określa ilość miejsca potrzebną do wyświetlenia tekstu. W celu obliczenia cję składową
•
prostokąta otaczającego
tekst, który chcemy
wyświetlić, wywołujemy
funk-
OrawText () obiektu CCl i ent Oc. Funkcja ta przyjmuje cztery argumenty:
Łańcuch
tekstowy, który ma zostać narysowany. Przesyłamy łańcuch zawierający znaków , którą będziemy chcieli wyświetlić : Vi ew Scal e : 99.
maksymalną liczbę
•
Liczbę
znaków w
łańcuchu. Podaliśmy
zakończony pustym
tu -I, co oznacza, że nasz łańcuch jest znakiem. W takim przypadku funkcja sama określa liczbę
znaków. •
Nasz prostokąt - t extRect. we współrzędnych klienta .
•
Jedna lub więcej flag
Prostokąt otaczający
sterujących działaniem
tekst jest tu zapisywany
funkcji.
Podaliśmy kombinacje dwóch flag . OT_S INGL ELI NE określa, że tekst ma być umieszczony w jednym wierszu . Z kolei OT_CALCRECT wskazuje, że chcemy , aby funkcja obliczyła rozmiar prostokąta potrzebnego do wyświetlenia łańcucha i zapisała go w prostokącie wskazywanym przez trzeci argument. Funkcja OrawText () normalnie jest używana do rysowania łańcucha.
RozlJział 16.
• Praca zoknami dialogowymi ikontrolkami
Istnieje szereg innych flag, których można użyć z tą funkcją; temat znajdziesz w systemie pomocy dla tej funkcji. Kolejna instrukcja ustawia
c zęś c i
szcz egół o we
905
informacje na ich
paska stanu :
m_St at usBar ,Get StatusBarCt rl( ).Set Pa rts( l . &widt h) : Wyrażenie
m_St at usBar Get St at usBarCt r l() zwraca odniesienie do obiektu CStatusBarCtr l do m_Stat usBar. Zwrócone odniesieni e jest używane do wywołania funkcji Set Par t s ( ) dla obiektu. Pierwszy argument funkcji Set Pa rts ( ) okre śla liczbę częś c i pask a stanu, czyli w tym przypadku l. Drugi argument je st przeważnie adresem tablicy typu i nt zawierają cej współrzędne x prawej krawędzi każdej c zęści, wyrażone we współrzędnych klienta. Tablica zawiera jeden element dla k ażd ej części paska stanu. P onieważ mamy tylko jedną część, przesyłamy adres jednej zmiennej wi dt h, która zawiera szerok o ś ć prostokąta zapi sanego w t ext Rect. Jest ona wyrażon a we współrzędn ych klienta, ponieważ kontekst urządzenia domyślnie korzy sta z trybu MM_TEXT. nal eżącego
W
końcu
ustawiamy
początkowy tekst
paska stanu,
CSta t usBarCtr l. Pierws zym jej argumentem jest a drugim - indeks z poni ższych :
c zęści,
która ma
wywołując funkcję składową
SetText ( )
łańcuch
zawierać łańcuch .
tekstu, który ma zostać napisan y, Trze cim argumentem może być jeden
Koli stylu
Wygląll
o
Tekst posiada rarnk ę , która je st
SET NOBO RDERS
Tekst je st pisany bez obramowania.
SET OW NERDRAW
Tekst je st rysowany przez okno macierzyste.
SET POPOUT
Tekst ma obramowanie, które wygl ąd a , jakby w ystawało z paska
zag łę b i o n a
w pasku
zadań .
W naszym kodzie określiliśmy tekst z rarnką wyglądającą, jakby M ożesz przetestować wygląd innych opcj i.
zad ań.
była zagłębiona
w pasku stanu.
Uaktualnianie paska stanu Gdyby ś teraz s k o mp i l ow ał i u ru chomił kod, paski stanu pojawiałyby s i ę , ale wyświ etlałyby jedynie współczynnik skali równy l , niezal eżni e od faktycznie używanego współczynnika , co je st niezbyt przydatne. Musimy gdzieś dodać kod zm ieniający tekst przy każdej zmiani e skali . To oznacza konieczność zmodyftkowania procedury obsług i OnVi ewSca l e() w CSket cherVi ew, aby zm i en i ała ona pasek stanu dla rarnki. Potrzebne sąjedynie cztery dod atkowe linijki kodu :
void CSketcherView: :OnViewScale() {
CScaleD ialog aDlg: II Utwórz obiekt okna dialogoweg o. aOl g.m_Scal e ; m_Scale; II Przesyla skalę widoku do okna dialogowego. if (aDlg.DoModal () ~ ~ IDOK) {
mScale
>
aDlg.m_Scale :
II Pobiera
nową ska lę.
II Pobiera okno ramki dla tego widoku.
CChl ldFrame* viewFrame ; st at ic cast ( Get ParentFrame() ):
906
Visual C++ 2005. Od podstaw II Buduje
łańcuch
komunikatu.
CString St at usMsgC"View Scal e: "l : Stat usMsg += stat ic_cast C 'o '
+
m_Sca1el :
II Wyp isuje tekst na pasku stanu.
viewFrame ->mStat usBar .Get St at usBa rCt r1C l.SetTextC St atusMsg, O. O) : Reset Scra11 Sizes(l : II Dostosowuj e paski przewijania do nowej skali. Inval idat eRect CO, ; II Unieważnia całe okno.
Ponieważ
początku
odnosimy się tu do obiektu CChi l dF rame, musimy dodać dyrektywę #i ncl ude na pliku SketcherView. cpp za i stniejącymi dyrektywami #i ncl ude.
Pierwsza linijka wywołuje Get Pa rent Frame( l, czyli składową klasy CSket cherVi ew, która jest dziedziczona z klasy CScro ll Vi ew. Zwraca ona wskaźnik do obiektu CFrameWnd, aby odpowiadał oknu ramowemu. A zatem aby był on nam przydatny, musi być rzutowany do CCh il d-
Frame*. Kolejne dwie linijki tworzą komunik at wyświetlany w pasku stanu. Użyta jest po prostu klasa CSt r i ng, ponieważ jest bardziej elastyczn a niż tablica char. Obiekty CStr i ng omówię szczegółowo nieco później, gdy będziemy dodawać do programu Sketcher elementy nowego typu. Znak dla wartości skali uzyskujemy przez dodanie wartości m_Scal e (która będzie z zakresu od l do 8) do znaku O. To generuje znak od l do 8. W końcu wykorzystujemy wskaźnik do potomnego okna ramowego, aby dosta ć się do skła dowej m_St at usBar, którą dodaliśmy wcześniej . Wówczas możemy dostać jej kontrolkę paska stanu i użyć składowej SetText ( ) kontrolki do zmiany wy świetlanego tekstu . Reszta funkcji OnVi ewScal e() pozostaje niezmieniona. To wszystko, co było nam potrzebne dla pasków stanu. Je żeli ponownie skompilujesz program Sketcher, dostępnych będzie wiele przewijalnych okien z różnymi skalami, a w pasku stanu każdego widoku wyświetlana będzie aktualna skala.
Używanie pól lisi Oczyw i ście
do ustawiania skali nie musimy korzystać z pokrętła. Możemy na przykład u żyć pola list. Zasady obsługi współczynnika skali będą dokładnie takie same, a zmienią s i ę jedynie okno dialogowe i kod pobierający z niego współczynnik skali. Jeżeli chciałbyś to wypróbować, nie zakłócając przy tym rozwoju programu Sketcher, skopiuj cały folder zawierający projekt Sketcher do innego folderu i wprowadzaj modyfikacje do kopii. Usuwanie części programu zarządzanego przez kreator klas może wywołać pewien bałagan, więc będzie to przyd atne doświ adcz eni e w sytuacji, gdy faktycznie będziesz potrzebował to zrobić .
Rozdział 16.•
Praca zoknami dialogowymi i kontrolkami
907
Usuwanie okna dialogowego Scale Najpierw mu sim y u sunąć z pr ojektu Sketcher defini cję i i m p l em e n ta cj ę CSketc her Oi a l ag, a także zasób dla okna dialogowego umo żliwiając eg o zmi an ę ska li. W tym ce lu przejdź do pan elu Solution Explorer, zaznacz ScaleD ialogcpp i n aci śnij klawi sz D elete, a na stępnie zaznac z Sca lelrialog.h i ponownie n a ci śnij klawisz Delete, aby usunąć je z projektu. W obu przypadkach zostanie wyświetlone okno dialogowe pozwalające usunąć pliki tylko z projektu lub usun ąć je również fizycznie; kliknij przy cisk Delete w oknie dialogowym, chyba że chcesz zachować kod . N as tę p n i e p rzejd ź do pan elu Resource View, rozwiń folder Di alog , kliknij IDO_SCAL E_OLG i naci śnij klawisz Delete, aby usunąć zasób okna dialog owego. Us u ń dyrektywę #i ncl ude dla ScaleDialog h z pliku SketcherView. cpp, N a tym etapie wszystkie odniesienia do pierwotnej klasy okna dialogowego zostały usunięte z projektu. Czy już s kończy liśm y? Prawie. Iden tyfik atory zas obów powinny zostać usunięte za nas. Aby to sprawdzi ć, kliknij prawym przyci skiem myszy Sketcher.rc w panelu Resource View i wybierz Resource Symbols z menu podręczn e go . Jak wid ać , IOC_SCALE i IOC_SPIN_SCALE zniknęły z listy. Oczywi ście procedura obsługi OnVie wSca leO w klasie CSket che rVi ew w c i ąż odnosi s i ę do eScal eDialog , więc projekt Sketcher jeszcz e się nie skompiluj e. Naprawimy to, gdy dodamy kontrolkę pola list. Wybierz Build/Clean Solution z menu głównego, aby u sunąć wszystkie pliki pośrednie , które mogłyby zawierać referencj e do CSc a l eOi al ag. Gdy to zrobisz, będz i esz m ógł przystąpić do ponownego tworzenia zasobu okna dialogowego umożliwiającego wprowadzanie współczyn nika skalowania.
Tworzenie kontrolki pola list Kliknij prawym przyci skiem myszy Dialog w panelu Resource View i dodaj nowe okno dialogowe z odpowiednim identyfikatorem i podpisem. Użyj tego samego identyfikatora co poprzednio - IIJf) - SCALE- f) LG. Kliknij przycisk pola list w l i ście kontrol ek i kliknij miejsce, w którym chcesz umieści ć kontrolkę w oknie dialogowym. Możesz powiększyć pole list i do sto sować po zycję kontrolki , odpowiednio ją przeciągając . Kliknij prawym przyciskiem myszy i wybierz Properties z menu podręcznego. Ustaw odpowi edni identyfikator, np. IOC_SCALELIST, j ak na rysunku 16.18. Właś ciwoś ć
Sort będzie domy ślnie ustawi ona na True, więc prze staw ją na False . Dz ięki temu które dodamy do listy, nie będą automaty cznie sortowan e. Zamiast tego będą umieszczane na końcu listy i wyświetlane w takiej kolejno ś ci, w jakiej je wpiszemy. Ponieważ do okreś l eni a skali będziemy używać pozycj i na li ś c i e, ważne j est, aby sekwencj a nie była zmieniana. D om y ślnie pole list jest wyposażone w pionowy pasek przewij an ia dla wpisów listy i może sz pozostawić domyślne ustawienia dla pozost ałych właściwości . Jeżeli jednak chciałbyś poznać ich działanie , może sz kliknąć po kolei każdą z nich. Na dole okna właściwości zostanie wyświ e tlony tekst objaśniający zastosowanie danej wła ściwości. łańcuchy,
908
Visual C++ 2005. Od podstaw
Rysunek 16.18
Propertfes
....
r,.
X
Appeara nce
Border ClientEdge Disable No SerolI Horizontal Seroli Left SerolIbar Modal Frame No Integral Height No Redrew Notify Right AlignText Right ToLeft ReadingOrder Static Edge Transparent Vertical ScrolIbar
True False
False False False False True False True
False
F~lse False False True
Behevtcr Accept Files Disabled Has SŁring s Help ID Multicolumn No Data OwnerDraw Selection
(Name) Group
False
ID Tabstop
IDC_SCALELlST True
Tworzenie klasy okna dialogowego Kliknij prawym przyciskiem myszy okno dialogowe i wybierz Add Class z menu podręcz nego. Ponownie zostanie wyświetlone okno dialogowe umożliwiające utworzenie nowej klasy. Nadaj klasie odpowiednią nazwę, jak na przykład tę używaną poprzednio, CScal eDi al og, i wybierz CDi al og jako klasę bazową. Jeżeli po kliknięciu przycisku Finish pojawi się komunikat, że ScaleDialog.cpp już istnieje, oznacza to, że zapomniałeś usunąć pliki .h i .cpp . Wróć i zrób to teraz. Wtedy wszystko powinno działać , jak należy. Po ukończeniu tych czynności pozostaje nam dodanie do klasy publicznej zmiennej kontrolki m_Scal e, odpowiadającej identyfikatorowi pola list IOC_SCALELI ST. Powinna ona być typu int i mieścić się w zakresie od Odo 7. Nie zapomnij ustawić Value w Category, ponieważ nie będziesz mógł wprowadzić zakresu . Dla składowej m_Scal e zostanie zaimplementowany DDX, ponieważ utworzyliśmy jąjako zmienną kontrolki, i wykorzystamy zmienną do przechowywania indeksu jednego z ośmiu elementów listy .
Rozdzial16.• Praca zoknami dialogowymi ikontrolkami
909
Musimy za i nicj a l i zo wać pole list w procedurze o bsług i Onln i tOia log w klasie CScaleO i alog, wię c dodaj przesłoni ę cie tej funkcji za pomocą okna Properties klasy. Dodaj poniższy kod: BOOL CScaleDi alog : :Onl ni tDi al og( ) { CDia l og: :Onlnit Di al og() ; CLis t Box* pList Box ~ sta t ic _cast(Get Dlgl tem( IDC_SCALEL IST) ): pLi stBox->AddSt r i ngCT( "Scal e I" ) ) ; pL i st Box ->AddSt r i ng(_T( "Scal e 2" ) ); pLi st Box->AddStri ngC T( "Scal e 3" ) ) ; pLi st Box ->AddStr i ng(_TC'Sca le 4") ) ; pl,i stBox->AddSt r i ngC T( "Scal e 5") ) : pLi stBox->AddSt n ng(_T( "Scal e 6") ) : pLi stBox->AddSt n ng(_T< "Scal e 7" ) ) ; pLi stBox->AddStr i ng(_TSetCurSel(m Scal e-l); retu r n TRUE ; II Zwraca TRUE. chyba że usta wisz fo kus dla kontrolki. II WY.JĄ TEK: strony właśc iwości OCX pow inny zwra ca ć FAU iE.
Pierwszy dodany wiersz pobiera wskaźnik do kontrolki pola list poprzez wywołanie s kładowej Get Dl gIt em () klasy okna dialogowego. Jest ona dziedziczona z klasy MFC CWnd. Zwra ca wskaźnik typu Cwnd*, więc rzutujemy go do typu CL i stBox*, który jest w ska źnikiem do klasy MFC definiującej pole list. K orzystając ze wskaźnika do obiektu okna dialogowego CLi st Box, używamy kilkakrotnie skł a dowej AddString () w celu dodania wierszy definiujących listę współczynników skali. Pojawi ają się one w polu list w kolejności , w jakiej je wpisaliśmy, więc okno dialogowe będzi e wygl ądał o jak na rysunku 16.19.
II Dostosowuje paski prz ewijania do nowej skali. II Un ieważnia c ałe okno.
Ponieważ warto śc i ind eksów dla wpisów wyb ieran ych z listy rozpo czyn ają si ę od zera, musimy do nich dodawać l , aby w widoku była za pisy wana wła śc iwa wartość ska li. Kod w yświ etl aj ący tę warto ść w pasku stanu wid oku j est dokładni e tak i jak prz edtem. Reszt a kodu ob słu guj ącego w sp ółczynni k i skali jest ju ż uk o ń c zona i nie wymaga zmian. Po ponownym umi eszczen iu dyrektywy #i ncl ude dla ScaleDialog.h możesz s komp i l ować i uruchomi ć tę w e rsję programu Ske tche r, aby zo b ac zyć dzi ałan i e pola list.
Korzystanie zkontrolki pola tekstowego Mo żem y u żyć kontrolki pola tekstowego do dodaw ani a opisów w programie Sketcher. Bę dzi em y potrzebowali nowego typu elementu - CText , odpowiadającego łańcuch owi tekstowemu, oraz będziemy mus ieli d odać dodatkowy element menu ustaw i aj ący tryb TEXT dla two rzenia elementów. Ponieważ eleme nt tekstowy w ym aga tylko jednego punktu odniesien ia, będzie on tw orz ony przez procedurę ob sług i OnLBut ton Down ( ) w klasie wido ku. B ęd z i emy także potrzeb ow a ć nowego eleme ntu w menu Element, u stawi aj ąc eg o tryb TEXT. Będzi emy doda w a ć te m ożli wo ści do pro gramu Sketc her w n a stępującej k olejno ś ci :
1
Utworzy my zas ób okna dialogowego i powiązaną klasę .
2. Dod am y nowy element menu . 8. Dodam y ko d otwi erający okno dial ogowe dla tworzenia elementu. 4. Dod amy o bsługę klasy CText .
Rozdzial16. • Praca zoknami dialogowymi ikontrolkami
911
Tworzenie zasobu pola tekstowego Utwórz now y zasób okna dialogowego w panelu Resource View poprzez klikn ięcie prawym przyciskiem myszy folderu Dialog i wybranie lnsert Dialog z menu podręcznego. Zmień identyfikator nowego okna dialogowego na IDD_TEXT_DLG, zaś tekst podpisu na Enter Text . W celu dodania pola tekstowego kliknij ikonę pola tekstowego w liście kontrolek, a następnie kliknij miejsce w oknie dialogowym, w którym chcesz j e umieścić. Możesz dostosować rozmiar pola tekstowego poprzez przeciąganie jego krawędzi, a także zmieni ć jego poło żenie poprzez przeciąganie całego elementu. Wyświetl właś ciwości pola tekstowego, klikając je prawym przyciskiem myszy i wybierając Properties z menu podręcznego . Zmień jego identyfikator na IDC_EDI TT EXT,jak na rysunku 16.20.
Rysunek 16.20
r:~_~ IT T EXT " " Control) IEdBoxEditor
1"" ;L 1 JjI I
Y
J
ID':_EDITTEXT (Ed, Contra Ą
(Nerne) Ą c ce p t
.
Files
Palse
AliQn Text Auto H5croll
Left
:lsKlndOf( RUNTIME_CLASS( CText)) ) { II Znajdują cy s ię tutaj kod zostanie wykonany tylko wtedy, gdy zostanie wybrany elem ent klasy CText.
}
Instrukcja ta wykorzystuje makro RUNTIMEJ LASS w celu uzyskania wskaźnika do obiektu typu CRu nt i meClass , a n astępni e przesyła ten wskaźnik do funkcji Is Ki ndOf () skład owej m_pSel ected. Ta zwraca wynik niezerowy, jeżeli m_pSe l ected jest klasy CText , a w przec iwnym razie zwraca zero. Jedynym warunkiem je st, aby klasa, dla której sprawdzamy, była zadeklarowana z u ży ciem makr DECLARE_DYNCREATE lub DECLARE_SERIAL, dlatego też zamieściłem tę poprawkę dopiero tutaj . Istnieje jeszcze jeden sposób okre śl ania typu klasy, wykorzystujący możliw ości wbudowane w ISO/ANSI C++. Operator type i d() zwraca odniesienie do obiektu typu typ e_i nt o, kap suł kuj ącego wskaźnik do nazwy typu obiektu lub wyrażenia w trakcie wykonywania, którą to na zw ę umieszczamy w nawiasach . Ponieważ obiekty t ype_i nfo możemy porównywać za pomocą operatora == (lub !=), możemy sprawdzić, czy m_pSel ect ed jest typu CText w nastę pujący spos ób :
938
Visual C++ 2005. Od podstaw i f (typeid(m_pSelect ed) == typid(CText )) { II Znajdujący s ię tutaj kod zostan ie wykonany tylko wtedy , gdy zostanie wybrany elem ent klasy CText.
} Jeżeli chciałbyś użyć
operatora typeid ( ), musisz dodać dyrektywę #incl ude dla nagłówka ISOIANSI C++ . Oczywiści e typy nie muszą być deklarowane za pomocą makr MFC przedstawionych w poprzedniej metodzie , jednak musisz użyć opcji IGR kompilatora, umożliwiającej korzystanie z informacji typu czasu uruchom ieniowego . Ostateczny kod funkcji MoveElement O
CSize Di stance ~ point - m_CursorPos : II Pobierz odleg lość przenos zenia. m_CursorPos = poi nt: II Usta w bieżący punkt jako pierwszy n astępnym razem . II Jeżeli jakiś element j est wybrany, przenieś go .
if(mpSel ect ed) l II Jeż eli element j est tekstem .
użyj
tej metody...
if (mpSelect ed->IsKindOf(RUNT IME CLASS( CText) )) (
II Przenieś element. II Pobierz nowy prostokąt ograniczający.
II Polq cz prostokąty ograniczające.
II Konw ertuj do wspćlrzednych klienta. II Normalizuj połączony obszar. II Un ieważnij po lączony obszar. II Natychmias t przerysuj. II Ry suj wyróżn ione
} II ... w przeciwnym razie
użyj
tej metody.
aDC.SetROP2(R2_NOTXORPE N); m_pSelected->Draw (&aDC.m_pSe1ect ed): II Rysuj elem ent, aby go wymaza ć.
m_pSe1ect ed->Move(Di st ance): II Teraz przen ieś elem ent. m_pSelected->Draw(&aDC,m_pSelected): II Rysuj przeni esiony element.
Jak widać, kod un i eważniający prostokąty, którego musimy użyć do przenoszenia tekstu, jest znacznie mniej elegancki od kodu ROP, stosowanego dla p ozostałych elem entów. Jednak działa on, o czym wkrótce się przekonasz, gdy wprowadzisz tę poprawkę, a następnie skompilujesz i uruchomisz aplikację. Jeżeli chciałbyś zastosować do sprawdzania typu mechanizm t ypei d( ), zmień po prostu warunek w instrukcji if i dodaj dyrektywę #i ncl ude dla do pliku SketcherView.cpp .
Rozdział 17.•
Przechowywanie idrukowanie dokumentów
939
Drukowanie dokumentu Zajmijmy się teraz drukowaniem dokumentu. Dzięki kreatorowi aplikacji oraz platformie mamy zaimplementowane podstawowe możliwości drukowania. Elementy menu File/Print, File/Print Setup oraz File/Print Preview dział ają. Wybranie FilelPrint Preview powoduje wyświetlenie okna zawierającego podgląd bieżącego dokumentu programu Sketcher na stronie, co przedstawia rysunek 17.4.
Rysunek 17.4
Cokolwiek znajduje się w bieżącym dokumencie, zostaje zamieszczone na stronie w bieżącej skali widoku. Jeżeli rozmiar dokumentu jest większy niż rozmiar papieru, niemieszcząca się część dokumentu nie zostanie wydrukowana . Po wybraniu przycisku Drukuj strona ta zostanie przesłana do drukarki . Robi to wrażenie, biorąc pod uwagę, że są to podstawowe możliwości, które otrzymujemy "gratis". Jednak w wi ększości przypadków możliwości te są niewystarczające. Typowy dokument w naszym programie może zdecydowanie się nie mie ści ć na stronie, więc albo będziemy go musieli skalować, by się na niej zmieścił, albo, co bardziej wygodne , będziemy chcieli wydrukować cały dokument na tylu stronach, ile będzie potrzeba. Aby rozszerzyć możliwości dostarczone przez platformę, możesz dodać własny kod dla drukowania, jednak najpierw musisz dowiedzieć się, jak drukowanie zostało zaimplementowane w MFC.
Proces drukowania Drukowanie dokumentu jest sterowane przez bieżący widok . Proces jest nieco zabałaganiony i może wymagać zaimplementowania własnych wersji sporej liczby funkcji dziedziczonych w klasie widoku. Rysunek 17.5 przedstawia
logikę
procesu i biorące w nim
udział
funkcje.
Rysunek 17.5 obrazuje, jak kolejność zdarzeń jest sterowana przez platformę i jak w drukowaniu elementów bierze udział pięć dziedziczonych składowych klasy widoku , które być może będzie trzeba przesłonić . Funkcje składowe COC umieszczone po lewej stronie schematu
940
VisIJal C++ 2005. Od podstaw
-
CDC::StartDocO
Pęt la,dopóki są _
_
w
-
Składowe
widoku
~
OnPreparePrintingO
~
OnBeginPrintingO
• Alokuje zasoby GOI
~
jeszcze jakieś strony
_________________ •
---
'"E
•Zmienia po czątek układu
--et;
OnPrepareDCO
współrzędnych wziernika
• Ustawiaatrybuty kontekstu urządzenia
'"
ii: CDC::StartPageO
• Oblicza liczb ę stron • Wywołuje OoPreparePrintingO
~
CDC::EndPageO
~
CDC::EndDocO
--s-
~
OnPr intO
f---9--
OnEndPrintingO
• Drukuje nagłówk i i stopki • Drukuje bieżącą stronę
• Zwalniazasoby GOI
Rysunek 17.5 komunikują się
przez
ze sterownikiem
urządzenia drukującego i są
automatycznie
wywoływane
platformę .
Obok nazwy funkcji znajduje się krótki opis roli, jaką pełni ona w procesie drukowania. Kolejność, w jakiej funkcje są wywoływane, jest wskazywana przez liczby na strzałkach . W praktyce nie musisz implementować wszystkich tych funkcji, a jedynie te, które będą potrzebne . Przeważnie będziesz chciał zaimplementować przynajmniej własne wersje OnPreparePri nt i ng() , OnPrepareOC() i OnPr i nt O . Nieco później w tym rozdziale, zobaczysz, jak te funkcje mogą być zaimplementowane na przykładzie programu Sketcher. Wysyłanie danych
do drukarki odbywa się w ten sam sposób co wysyłanie danych do urządze nia wyświetlającego - przez kontekst urządzenia . Wywołania GDI, których używamy do wysłania tekstu lub grafiki, są niezależne od urządzenia, więc działają one zarówno dla drukarki, jak i wyświetlacza. Jedyną różnicę stanowi urządzenie, którego dotyczy obiekt COC. Funkcje COC na rysunku 17.5 komunikują się ze sterownikiem drukarki. Jeżeli wydrukowanie dokumentu wymaga więcej niż jednej strony , proces zapętla się i wywołuje ponownie OnPre pareOC( ) dla każdej kolejnej nowej strony, określanej przez funkcję EndPage () .
Rozdział 17.•
Przechowywanie i drukowanie dokumentów
941
Jako argument wszystkich funkcji klasy widoku biorących udział w procesie drukowania przesyłany jest wskaźnik do obiektu typu CPri ntI nfo. Ten obiekt zapewnia łącze między wszystkimi funkcjami obsługującymi proces drukowania, więc przyjrzyjmy się szczegółowo klasie CPrin tI nfo.
Klasa CPrintlnlo Obiekt CPr i ntIn fo odgrywa podstawową rolę w procesie drukowania, ponieważ przechowuje on informacje o wykonywanym zadaniu drukowania oraz jego stanie w dowolnym momencie . Dostarcza on także funkcje umożliwiające dostęp i manipulację tymi danymi. W trakcie drukowania za pośrednictwem tego obiektu przesyłane są informacje z jednej funkcji widoku do kolejnej oraz między platformą a funkcjami widoku . Obiekt klasy CPr i ntIn fo jest tworzony zawsze po wybraniu opcji menu FilelPrint lub FilelPrint Preview. Po użyciu przez każdą funkcję widoku biorącą udział w procesie drukowania jest automatycznie usuwany po zakończeniu drukowania. Wszystkie dane
składowe
CPr i ntInfo
są publiczne.
Oto one :
Składowa
Opis
m_pPD
Wskaźnik
m bDirect
Jest ustawiana na TRUE przez plat formę, jeżeli operacja dmkowania ma pominąć okno dialogowe drukowania, W przeciwnym razie ma wartość FALSE.
m bP re Vl ew
Składowa typu BOOL, której w artoś ć to TRUE, jeżeli Pre view; w przeciwnym razie ma warto ść FALSE.
m_bConti nuePri nt in g
Składow a typu BOO L. Jeżeli jest ustawiona na TRUE, platforma kontynuuje pętlę drukowania pr zedstawioną na schemacie. Jeżeli jest ustawiona na FALS E, pętla jest kończona. Tę zmienną należy ustawiać, jeżeli nie przesyłamy licznika stron dla operacji drukowania do obiektu CP r i nt! nf o (za pomocą funkcji składowej SetMaxPage( l). W takim przypadku należy samemu zaznaczyć zakończenie drukowania poprzez ustawienie tej zmiennej na FALS E.
do obiektu CPri ntOi al ag, który wyświetla okno dialogowe drukowania.
Wartość typu UINT przechowuj ąca numer zazwyczaj zaczyna się od l. lic zbę
zostało
bieżącej
wybrane FilelPrint
strony. Numeracja stron
m_nNumPreviewPa ges
Wartość typu UINT okre ślająca wydruku. Może to być I lub 2.
mJ pU serData
Jest ona typu LPVOID i przechowuje wskaźnik do obiektu, który tworzysz. To pozwala nam utworzyć obiekt przechowujący dodatkowe informacje o operacji drukowania i powiązać go z obiektem CPrintInfo.
m rectDr aw
Obiekt CRec t
definiujący użyte czny
stron
wyświetlanych
obszar strony we
w oknie
współrzędnych
Obiekt CSt rl ng zawierający łańcuch formatujący używan y przez wyświ etlenia numerów stron w podglądzie wydruku.
Obiekt CPr i ntI nfo posiada następujące publiczne funkcje
składowe:
podglądu
logicznych .
pl atformę
do
942
Visual C++ 2005. Od podstaw
Funkcja SetM i nPage (UINT nMi nPage)
Opis Argument
określa
numer pierwszej strony dokumentu. Ni e jest zw racana
żadna w arto ś ć.
Set MaxPage (U INT nMaxPage)
Argument
określa
numer ostatniej strony dokumentu. Nie jest zwracana żadna
wartość.
GetMi nPage ( ) const
Zwraca numer pierwszej strony dokum entu jako typ UINT.
Get MaxPage () canst
Zwraca numer ostatniej strony dokumentu jako typ UI NT.
Get FromPage() canst
Zwraca numer pierw szej strony dokumentu , która ma zos ta ć wydrukowana (typ UI NT). Wartoś ć tajest ustawi ana przez okno dialogowe drukowani a.
GetTaPage ( ) canst
Zwraca numer ostatniej strony dokumentu, która ma z os ta ć wydrukowana (typ UlNT). Wartość ta jest ustawiana przez okno dialogowe drukowania.
'------- -
Jeżeli
drukowany jest dokument zawieraj ący kilka stron, musimy dowiedzieć się, ile zajmuje on stron, i przechować tę informację w obiekcie CPri ntIn fo, aby była ona dostępna dla platformy . Możemy to zrobić we własnej wersji składowej OnPreparePr i nt ing( ) bieżącego widoku. Aby ustawić numer pierwszej strony dokumentu, musimy wywołać funkcję Set Mi nPage() w obiekcie CPri ntI nfo, która przyjmuje numer strony jako typ UINT. Żadna wartość nie jest zwracana. Do ustawienia ostatniej strony w dokumencie wywołuj emy funkcję Set MaxPage ( ), która także przyjmuje numer strony jako argument typu UINT i nie zwraca żadnej wartości. Jeżeli c h c i ałbyś później pobrać te wartości , użyj funkcji GetM i nPage () i Get M axPageO obiektu
CPr i ntI nfo. Dostarczone przez nas numery stron s ą przechowywane w obiekcie CPr i ntDi a l og wskazywanym przez składową m_pPD obiektu CPr i ntI nfo i wyświetlane w oknie dialogowym pojawiającym się po wybraniu z menu File/Print. Użytkownik ma wtedy możliwoś ć określenia pierwszej i ostatniej strony, które mają zo stać wydrukowane. Wartości te pobieramy poprzez wywołanie składowych GetFromPage () i GetToPage O obiektu CP r i ntIn f o. W obu przypadkach zwracane są wartości typu UINT. Okno dialogowe automatycznie sprawdza, czy numery stron, które mają zostać wydrukowane, mieszczą się w dostarczonym zakresie, określając najmniejszy i największy numer strony w dokumencie. Wiesz już, jakie funkcje możesz zaimplementować w klasie widoku w celu samodzielnego zarządzania drukowaniem. Wiesz też, jakie informacje są dostępne z obiektu CPr i ntI nfo przesyłanego do funkcji związanych z drukowaniem. Mechanizm drukowania stanie się dla Ciebie bardziej zrozumiały, gdy zaimplementujesz podstawową możliwość wielo stronicowego wydmku dokumentów programu Sketcher.
Implementacja wieloslronicowych wydruków W programie Sketcher używamy trybu MM _LOENGLISHdo ustawiania, a następnie przechodzimy do trybu MM_ANI SOTROPI C. Oznacza to, że kształty i rozmiar widoku są mierzone w setnych c zęściach cala. Oczywiści e , mając j ednostkę miary o stałym rozmiarze fizycznym, chcielibyśmy drukowa ć obiekty w ich aktualnym rozmiarze.
Rozdział 17.•
PrzechowJwanie i drukowanie dokumentów
943
Ponieważ określiliśmy
rozmiar dokumentu jako 3000 na 3000 jednostek, możemy tworzyć dokumenty w kształcie kwadratu o boku 30 cali, co gdyby wykorzystać pełny obszar, zajęłoby dobrych kilka kartek papieru. Obliczeni e liczby stron potrzebnych do wydrukowania szkicu wymaga nieco więcej pracy niż w przypadku zwykłego dokumentu tekstowego, ponieważ przeważnie do wydrukowania całego dokumentu szkicu będziemy potrzebowali dwuwymiarowej tablicy stron. Aby nadmiernie nie komplikować problemu, załóżmy, że dnikujemy zwykłą kartkę papieru (rozmiaru A4 lub 8 1/ 2 na II cali) w orientacji pionowej (co oznacza, że dłuższa krawędź jest pionowa). W obydwu przypadkach będziemy drukowali dokument o wymiarach 6 na 9 cali w centralnej części kartki. Z takimi założeniami nie musimy się przejmować faktycznym rozmiarem papieru. Wystarczy, że "pokroimy" dokument na fragmenty o rozmiarach 600 na 900 jednostek. Przy dokumentach większych niż jedna strona podzielimy dokument w sposób przedstawiony na rysunku 17.6.
Rvsunek 17.6 Liczba stron na szeroko ść : 4
Ił
i
Strona 2
Strona'
Strona 3
~
Strona 4
N
~ o
"" s >.
:.i
?:
'" "'2"
t;;
.n '"
!
.. l v o-
N V
:::;
Strona S
Strona 6
Strona 7
Strona 8 +-6cali-+
Jak widać, będziemy numerować strony wierszami, więc w tym przypadku strony od l do 4 znajdą się w pierwszym wierszu, a strony od 5 do 8 w drugim.
Uzyskiwanie calkowitego rozmiaru dokumentu Aby dowiedzieć się, ile stron zajmuje konkretny dokument , musimy wiedzieć , jak duży jest szkic, a do tego będzie nam potrzebny prostokąt otaczający wszystkie elementy dokumentu. Możemy to prosto zrobić poprzez dodanie funkcji Get Doc Ext ent() do klasy dokumentu CSketc herDoc. Dodaj poniższą deklarację do interfejsu publ i c klasy CSket cher Doc: ~c t GetDocExt ent ( ): II Pobiera prostokąt ograniczający ca ły dokum ent,
Implementacja nie stanowi
większego
problemu . Kod
II Pobierz prostokąt opisujący cały dokument,
CRect CSketc herDoc: :Get DocExtent()
wygląda następująco:
944
Visual C++ 2005. Od podstaw
CRect OocE xtent (O. o.1.1); II Początkowe wymiary dokum entu. CRe'ct ElementBound(O,O.O.O) ; /r Odstęp dla prostokąta ogran iczającego ,
POSITION aPosit ion ~ m_Element List. GetHead Pos it ion(); whi 1e(aPas 1 t ion) II Pętla p rzez wszystkie elem enty na liscie. ( II Pobierz prostokąt
ograniczający
element
E lemen tBound~ (m_E lement L is t. G et N ext (a Pos i t ion) )->GetBoundRe c t ( ) ;
II
Współrzędn e
rozmiaru dokumentu
mają ograniczać powyższe ,
OocE xtent .UnionRect(OocExtent, ElementBound), }
OocExtent .Normall zeRect( ); ret urn DocE xtent; Mo żesz dodać tę definicję
do pliku Sket cherDo c.cpp lub po prostu dodać kod , j eżel i skorzyz opcj i menu kontekstowego Add/Add C/ass w panelu C/ass View. Pętla przechodzi przez wszystki e elementy w dokumencie, używając zmiennej aPos ition do przem ieszc zania s ię po li ście i pobierania otaczających prostokątów poszczególnych elementów. Składowa Un i onRect ( ) klasy CRect oblicza najmniejszy prostokąt zawierający dw a prostokąty prze słane j ako argumenty i umieszcza wartość w obiekcie CRect, dla którego funkcja została wywołana. A zatem rozmiar DocExt ent zwiększa się , dopóki nie znajdą s ię w nim wszystkie elementy dokumentu. Zwróć uwagę , że inicjalizujemy Doc Extent z (O . O. 1. 1), pon i eważ funkcja Uni onRect( ) nie dz i ała poprawnie z prostokątami o zerowej wysokości lub szeroko ś ci. stałeś
Przechowywanie danych drukowania Funkcja OnPreparePri nti ngO w klasie widoku jest wywoływana przez platformę aplikacji w celu umożliwi enia nam zapoczątkowania procesu drukowania dokumentu . Pod stawową wymaganą in icjali zacjąje st dostarczenie informacji o liczbie stron w dokumencie do wy świe tlaneg o okna dialo gowego drukowania. Musimy przechować informację o stronach wymaganych przez dokument, więc możemy wykorzystać ją później w innych funkcjach widoku biorących udzi ał w procesie drukowania. Zapoczątkujemy to w składowej OnPreparePri nt ingO klasy widoku , przechowamy w obiekcie własnej klasy, którą zdefiniujemy w tym celu , i przechowamy wskaźnik do obiektu w obiekcie CPri ntInfo udostępnionym przez platformę, Obrałem taki sposób, aby zademonstrować działanie tego mechanizmu; w większości przypadków znacznie prościej będzie przechowywać dane w obiekcie widoku, co znacznie upra szcza zapis odniesień do danych. Będziemy musieli zapi sać liczbę
stron składających się na szerokość dokumentu - m_nWi dths, stron wzdłuż długości dokumentu - m_nLengths. Będziemy musieli także przechować lewy górny róg prostokąta zawierającego dane dokumentu j ako obiekt CPoi nt - m_DocRefPoi nt, ponieważ będziemy go używać do wyliczenia pozycji strony do wydrukowania na podstawie jej numeru strony. Przechowamy też nazwę pliku dokumentu w obiekcie CSt r i ng - m_DocTitl e, abyśmy mogli ją dodać jako tytuł każdej strony. Definicja klasy zawierającej to wszystko wygląda następująco:
a
także liczbę rzędów
#pragma ance class CPrint Oata { publ ic :
Rozdzial17.• Przechowywanie idrukowanie ddmmentów
945
lI INT mnWidt hs : II Licznik stron dla szerokości dokumentu. LI INT m=nLengt hs : II Licznik stron dla d/ugości dokumentu. CPo int m_OocRefPoint : II Lewy górny róg zawa r tości dokumentu. CString m_DocTit le: II Nazwa dokumentu. };
Dodaj do projektu nowy plik nagłówkowy o nazwie PrintData.h. W tym celu kliknij prawym przyciskiem myszy folder Header Files w panelu Solution Explorer, a następnie wybierz AddlNew Item z menu kontekstowego. Wpisz teraz definicję klasy w nowym pliku. Nie potrzebujemy pliku z implementacją dla tej klasy. Domyślny konstruktor (który jest generowany automatycznie) jest tu odpowiedni . Ponieważ obiekt tej klasy będzie wykorzystywany tylko tymczasowo, nie potrzebujemy stosować klasy CObject jako klasy bazowej ani wprowadzać dodatkowych komplikacji. Proces drukowania rozpoczyna się wywołaniem funkcji składowej klasy widoku OnPreparePri nt i ng( ), więc sprawdźmy, jak ją zaimplementować.
Przygolowania do wydruku Na samym początku kreator aplikacji dodał do klasy CSketcherVie w funkcje On PreparePrin t i ng( ), OnBegi nPri nt i ng( ) i OnEndPri nt i ng ( ). Jak widać, podstawowy kod dostarczony dla funkcji OnP reparePrinti ng( ) wywołuje funkcję DoPreparePrint ing () w instrukcji ret urn:
BOOL CSketc herView : :OnPreparePri nt i ngC CPri nt lnfo* plnfo) ( II Domyśln e przygotowan ia.
ret urn OoPreparePr int i ngC plnfo): Funkcja DoP reparePri nti ng( ) wyświetla okno dialogowe Print, używając informacji o liczbie stron do wydrukowania, zdefiniowanych w obiekcie CPri nt I nfo. Jeżeli tylko jest to możliwe , powinieneś obliczyć liczbę stron do wydrukowania i zapisać ją w obiekcie CPri nt lnfo przed tym wywołaniem . Oczywiście , zanim to zrobisz, często będziesz potrzebował informacji z kontekstu urządzenia - na przykład w sytuacji, gdy na liczbę potrzebnych stron będzie miała wpływ wielkość użytej czcionki - a w takim przypadku uzyskanie liczby stron nie będzie możliwe przed wywołaniem OnPrepar ePrinti ng( ). W takiej sytuacji możemy obliczyć liczbę stron w składowej OnBegi nPri nt ing O , która przyjmuje jako argument wskaźnik do kontekstu urządzenia . Platforma wywołuje tę funkcję po wywołaniu OnPreparePr-i nt ing O, więc dostępne są informacje umieszczone w oknie dialogowym drukowania. Oznacza to, że możemy również uwzględnić wybrany przez użytkownika rozmiar papieru. Przyjmijmy, że rozmiar papieru jest wystarczający, by pomieścić obszar rysunku o wymiarach 6 na 9 cali. W takim przypadku możemy obliczyć liczbę stron w On Prepare Pri nt i ng( ). Kod wygląda następująco:
BOOL CSketcherVi ew: :OnPreparePri nt ing(CPrint lnfo* plnfo) {
plnfo->m_lpUserOata = new CPrintDat a: CSket eherOoe* pOoe = GetDoeument(); II Pobi erz
ca ły
rozmiar dokum entu.
II Utwórz obiekt z danym i druko wania. II Pobierz wskaźnik do dokumentu.
946
Visual C++ 2005. Od podstaw CRect OocExtent
=
pDoc->GetDocExtent();
II Zapisz punkt odniesienia dla
całego
dokumentu.
((CPrintData*)(plnfo- >m_lpU serOata»->m_DocRefPoint CPoint (DocExtent.left. DocE xtent .bottom); II Pobierz
nazwę
=
pliku dokum entu i zapisz ją.
((CPrintOata*)(plnfo->m_l pUserOat a» ->m_OocTit le
=
pOoc ->Get Tit le() ;
II Oblicz wymaganą liczbę stron wydruku szerokości 600 jednostek II do pomieszczenia szerokości dokumentu.
«(C Print Oata*)( plnfo->m_l pUserData »->m_nWidt hs = st at ic_cast (cei l ((st at ic_cast <double>(OocExt ent .Widt h(» )/600 O» ; II Oblicz wymaganą liczbę stron wydroku o długości 1000 j ednostek II do pomieszczenia długości dokum entu.
((CPrintDat a*)(plnfo->m_l pUserOata» ->m_nLengths = st at ic_cast (ceil(( sta t ic_cast <double>(OocExtent .Height( » )/900.0»; II Ustaw liczb ę dla pi erwsz ej strony j ako l II i ustaw numer ostatniej strony jako całkowitą liczbę stron.
plnfo->SetMinPage(}); plnfo->SetMaxPage((st ati c_cast (plnfo->m_l pUserOata»- >m_nWi dths * (static_cast (plnfo->m l pUserData» ->m nLengt hs); return DoPreparePrlnt l ng(plnfo); Najpierw tworzymy obiekt CPr i ntDat a na stercie i przechowujemy jego adres we wskaźniku m_l pUse r Dat a w obiekcie CPri ntI nfo przesyłan ym do funkcji poprzez wskaźnik pln f o. Po uzyskaniu wskaźnika do dokumentu pobieramy prostokąt zawierający wszystkie elementy dokumentu poprzez wywołanie funkcji GetDocExtent ( ), którą dodaliśmy wcześniej do klasy dokumentu . Następnie przechowujemy róg tego prostokąta w składowej m_DocRefPoi nt obiektu CPri nt Oata i umieszczamy nazwę pliku zawierającego dokument w m_OocTit l e. Referencja do obiektu CPrintData przez wskaźnik w obiekcie CPr i ntInfo jest raczej zawikłana. Do wskaźnika dostajemy się za pomocą instrukcji pl nfo->mJ pUserData, ale ponieważ wskaź nikjest typu void , musimy dodać rzutowanie na typ CPri nt Oata*, aby dostać się do składowej m_DocRefPoi nt obiektu. Pełna instrukcja pozwalająca uzyskać dostęp do punktu odniesienia w dokumencie wygląda następująco:
(st atic_ cast (plnfo->m_l pUserData » ->m_DocRefPoint Musimy użyć tego podejścia dla wszystkich odniesień do składowych obiektu CP r i ntDat a, więc wszystkie zawierające je instrukcje będą ozdobione takim zapisem. Gdy umieścimy dane w klasie widoku, będziemy musieli jedynie używać nazwy składowej . Nie zapomnij dodać do pliku SketcherView.cpp dyrektywy #i ncl ude dla PrintData.h. Kolejne dwa wiersze kodu obliczają liczbę stron w poprzek dokumentu oraz liczbę stron potrzebnych do pokrycia długości. Liczba stron pokrywających szerokość jest obliczana poprzez podzielenie szerokości dokumentu przez szerokość drukowalnego obszaru strony o wymiarze 600 jednostek (6 cali) i zaokrąglenie wyniku do najbliższej największej liczby całkowitej za pomocą funkcji bibliotecznej cei l ( ) zdefmiowanej w nagłówku <mat h. h>. Do pliku SketcherView.cpp należy dodać dyrektywę #i ncl ude również dla tego pliku . Na przykład wywołanie cei 1(2 . l) zwróci 3,0, cei 1(2 .9) również zwróci 3,0, a ce il (-2 .l) zwróci - 2,0. W podobny sposób obliczana jest liczba stron niezbędnych do pokrycia długości. Iloczyn tych dwóch wartości stanowi całkowitą liczbę stron do wydrukowania i tę wartość przesyłamy jako maksymalną liczbę stron.
obiekt CPr i ntData utworzyliśmy na stercie, musimy dopilnować , że zostanie on usugdy już nie będzie nam potrzebny. W tym celu dodamy kod do funkcji OnEndPri nt i ng( ):
voi d CSketcherView: :OnEndPri nting(CDC* /*pDC*/ . CP rintlnfo* plnfo) II Usuń obiekt z danymi drukowan ia.
delete static cast( plnfo->m l pUserDat a) : To wszystko , co będzie nam potrzebne w tej funkcji w programie Sketcher , jednak w niektórych przypadkach nie będzie to wystarczające. W tym miejscu powinno być dokonywane całe porządkowanie. Upewnij si ę, że usunąłeś znaczniki komentarza (/* *1) z nazwy drugiego komentarza, w przeciwnym razie funkcja się nie skompiluje. Domyślna implementacja umieszcza nazwy parametrów w komentarzu, ponieważ w Twoim kodzie odwoływanie się do nich może nie być potrzebne. Ponieważ użyliśmy parametru plnfo, musimy usunąć z niego komentarz, w przeciwnym razie kompilator zaraportuje, że jest on niezdefiniowany. Nie musimy dodawać nic do funkcji OnBegi nP ri nt i ng( ) w programie Sketcher, ale musimy dodać kod alokuj ący wszelkie zasoby GOI, takie jak pióra, które mogłyby być potrzebne w procesie drukowania. Następnie będziemy musieli je usunąć podczas porządkowania w funkcji OnEndP rinting().
Przygotowywanie kontekstu urządzenia Na tym etapie program Sketcher wywołuje OnPrepareDC( ), która ustawia tryb mapowania na MM_ANI SOTROPIC, aby uwzględniony być współczynnik skali. Musimy wprowadzić dodatkowe zmiany, aby kontekst urządzenia był odpowiednio przygotowany do drukowania: void CSketc herView : :OnPrepareDC(CDC* pDC. CPri nt lnfo* plnfo) (
int Scal e = m_Scale: II Przechowaj ska lę lokalnie. if(pDC- >IsPrinting ()) Scale = l : II Jeżeli drukujemy, ustaw ska lę na l . CSc roll Vlew: :OnPrepareDC(pDC. plnfo) : CSketcherDoc* pOoc = GetDocument(): pOC- >SetMapMode(MM_ANISOTROPI C) : II Ustawia tryb mapowania. CSize OocSize = pOoc->Get DocSize() : II Pobiera rozmiar dokum entu. II Wymia r y musi
być
ujemny, ponieważ chcemy
użyć
trybu MM_LOENGLISll.
DocSize .cy = -Do c 'ś t ze.cy: II Zm ień zn ak y. pDC- >SetWindowExt WocSize); II Ustaw wymiar okna. II Pobierz
long xExtent = st at ic_cast (DocSize.cx)*Scale*x Log Pixels/ lOO L: long yExte nt = st at ic cast (DocSize.cy)*Scale*yLogPixel s/lOOL; pOC->SetViewportExt (static_cast (xExtent ). stat ic_cast (-ytxt ent i): II Ustawia obszar wziernika.
948
Visual C++ 2005. Od podstaw Funkcja ta wywoływana je st przez platformę w celu przesłania wyjścia do drukarki oraz na ekran. Dopilnuj, aby użyt a była skala wynosząca I, musimy bowiem w trak cie drukowania przestawić współrzędne logiczne na współrzędne urządzen ia. Gdybyśmy pozostawili wszystko bez zmian, wyjście byłoby przesłane w skali bieżącego widoku, jednak w trakcie obliczania wymaganej liczby stron oraz ustawiania po czątku układu odniesienia dla każdej strony musimy brać pod uwagę współczynnik skali. Możemy spraw dzi ć, czy mamy wskaźnik do kontekstu urządzenia, poprzez wywołanie skła dowej Is Pr i nt i ng( ) bieżącego obiektu CDC, która zwraca TRUE, jeżeli trwa drukowanie. Gdy mamy wskaźnik do kontekstu urządzenia , musimy jedynie ustawić skalę na I . Oczywiście, musimy zmieni ć instrukcje znajdujące się pon iżej, które korzystają z wartośc i skali, aby uży wały lokalnej zmiennej Sca le zamiast składowej widoku m_Scal e. Wartości zwracane przez wywołanie Get Dev i ceCaps O z argumentami LOGPIXELSXi LOGPI XELSY s tanowi ą liczbę logicznych punktów na cal w kierunkach x i y w drukarce (w przypadku drukowania) lub ekranu (jeżeli na nim rysujesz). Dzięki temu do urządzenia, do którego przesyłasz dane wyj ściowe , automatycznie dosto sowany jest rozmiar wziern ika.
Drukowanie dokumentu Możesz zapis ać
dane do kontekstu urządzenia drukarki w funkcji OnPri ntO . Jest ona wyworaz dla każdej strony, która ma zostać wydrukowana. Musimy dodać przesłonięcie tej funkcji w klasie CSket cher Vi ew, korzystając z okna Prop erti es tej klas y. Wybierz OnPrint z listy Overrides i kliknij OnPrint w prawej kolumnie.
ływana
Możesz uzyskać
numer bieżącej strony ze s kładow ej m_nCurPage obiektu CP r intI nfo i użyć tej do wyliczenia współrzędnych pozycji w dokumencie, odpowiadających lewemu górnemu rogowi bieżącej strony. Najlepiej to zrozumieć na podstawie przykładu , więc wyobraź sob ie, że drukujemy siódmą z ośmiu stron dokumentu, jak na rysunku 17.7. wartości
Możesz obliczyć indeks pozycji w poziomie poprzez odj ęcie l od numeru strony i pobranie reszty z dzielenia przez liczbę szerokości stron, na których zmieści się cała szerokość obszaru dokumentu do wydrukowania. Pomnożenie tej warto ści przez 600 da nam współrzęd ną x lewego górnego rogu strony względem lewego górnego rogu prostokąta otaczającego elementy w dokumencie. W podobny spo sób możemy obliczyć pionową pozycję w dokumencie poprzez podzielenie bieżącego numeru strony pomniej szonego o l przez liczbę szeroko ści stron potrzebnych dla szerokości całego dokumentu. Pomnożenie reszty przez 900 da nam względną współrzędną y lewego górnego rogu strony. Możemy to wyrazić w poniższych dwó ch instrukcjach:
i nt xDrg = (st at ic_cast ( pInfo->m_l pUserDat a» ->m_DocRefPoi nt .x + 600*((pInfo->m_nCurPage - 1)% ((st ati c cast (p lnfo->m l pUserDat a» )->mnWi dt hs )) ; i nt yD rg-~ (st at ic_cast (plnfo->m_l pUserDat a))->m_DocRefPoint .y 900*((pInfo->m_nCurPage - 1)/ ((statl c cast ( pInfo->m l pUserDat a)- >mnWidths» ; Instrukcje te wyglądają na złożone, głównie jednak dlatego że uzyskujemy dostęp do informacji przechowywanych w obiekcie CPri ntDat a za pośrednictwem wskaźnika w obiekcie CPr i ntl nfo.
Rozdział 17"
• Przechowywanie i drukowanie dokumentów
949
_ - - - - m_nWldths: Liczba stron na szerokość: 4 - - - _
dobrze, gdyby na górze każdej strony była drukowana nazwa pliku, jednak chcemy nie wydrukujemy dan ych dokumentu na nazwie pliku. Chcemy także wyśrodkować drukowany obszar strony. Możemy to zrobi ć poprzez przesunięcie początku układu współrzędnych w k ontekś cie urządzenia drukarki po wydrukowaniu nazwy pliku. Przedstawia to ry sunek 17.8: mi eć pewność, że
Rysunek 17.8 przed stawia zależność między drukowanym obszarem strony w kontekście urządzenia a stroną do wydrukowania w ramach odniesień danych dokumentu. Jak sobie przypominasz, są one w jednostkach logicznych - odpowiedniku ~II~_LOENGLI SH w programie Sketcher - więc y jest malejące z góry do dołu . Rysunek przedstawia też spo sób wyliczenia przesunięcia od początku strony dla obszaru o wymiarach 600 na 900 jednostek, w którym będziemy drukowali stronę. Będziemy chcieli umieścić informacj e z dokumentu w zakreskowanym obszarze pokazanym na stronie, więc mu simy odwzorować punkt (xOrg . yO rg) z dokumentu do pokazanego na drukowanej stronie miejsca, które jest przemieszczone względem początku strony o warto ści przesunięcia xOffs et i yOffset. Domyślnie początek układu współrzędnych używany do definiowania elementów w dokumencie jest odwzorowywany do początku układu w kontekś cie urządzenia, można to jednak zmienić . Obiekt COC dostarcza w tym celu funkcję SetW indowOrgO. Umożl iwia ona zdefiniowanie punktu w logicznym układzie współrzędnych dokumentu, który ma odpowiadać począt kowi w kontekście urządzenia. Ważne jest, aby zapisać stary początek, zwrócony z funkcji Set WindowOrg ( ), j ako obiekt CPo i nt . Po zakończeniu drukowania bieżącej strony należy przywrócić stary początek, w przeciwnym razie składowa mJ ectOraw obiektu CPri nt l nfo nie będzie poprawnie ustawiona przy drukowaniu kol ejnej strony.
Punkt w dokumencie, który chcemy odwzorować do początku strony, ma współrzędne (xO rgMoże to być trudne do wyobraż enia sobie, pamiętaj jednak, że poprzez ustawienie początku okna definiuj emy punkt mapujący do początku wziernika. Jeżeli
xOf f set. yO rg-yOffs etL
850
Visual C++ 2005. Od podstaw Początek układu współrzędnych
m_rectDraw.rlght
. strony
o(
•
-- -1 Nazwa pli ku
E
s
'O
J:l
~
e
Ta odległo ść to yOffset obliczona - (m _rect Draw.bottom - 600)/2
I I I
'"
I
~ __~J~
o
ti
~I E
następująco:
Drukowana strona
Począte k układu współrzędnych
Ta odległość to xOffset
w dokumencie jest odwzorowywany tutaj
(m~~~~~::':i:~~~j~~)/2/'- ,,
Początek układu
współrzędnych
dokumentu DocRefPoint
--.---------o
-
-
-
-
-
-
-
---To
,
,,,
: ' : : :
: : : :
-
-
-
-
-
-
-
-
-
.
-
Strona 7
Dokument
__ ___ _ _ __ __ _ ' - -
-J
0
o
_
lISIIIlIk 17.8 chw ilę s ię zastanowisz, dojdziesz do wniosku, że punkt (xOrg, YOrg ) w dokumencie jest miejscem, w którym chcemy go umie śc ić na stronie. Pełny
kod
drukuj ący stronę
II Drukuje
s tronę
dokumentu
wygląda następuj ąco:
doku mentu.
void CSketc herVi ew : :OnPri nt (CDC* pDC . CPrint lnfo* plnfo) { II Wypisuje nazwę pliku doku mentu .
pDC ->Set TextAl ign(TA_CENTER): II Center the fol lowing text pDC ->TextOut(pl nfo->m_rectDraw. right/2. -20. (stat ic_cast(plnfo->m_lpUserData ))->m_DocTit le ): pDC->SetTextAlign (TA_LEFT) ; II Left j ust lfy text II Oblicza początek układu
współrzędnych
dla
bieżącej str ony .
i nt xOrg = (static_cast(plnfo->m_lpUserData ))->m_DocRefPoi nt .x + 600*«plnfo->m_nCurPage - 1)% « static_cast (plnfo->m_l pUserData )) ·>m_nWidt hs) ): i nt yOrg = (st atic_cast ( plnfo->m_l pUserData ))->m_DocRefPoi nt .y 900* «p lnfo->m_nCurPage - 1)1 «st at ic_cast( plnfo->m_l pUserDat a))->m_n Wi dt hs)):
Rozdział 17.• II Oblicza przes unięcia II (wa rtośc i dodatnie).
II Zmienia początek układu II stary po cz. układu .
CPoint Ol dOrg
do
Przechowywanie idrukowanie dokumentów
=
współrzędnych
okna na
odpowiadające bieżącej
stronie oraz zapisuje
pOC ->Set W i ndowOrgCxOrg-xOff set. yOrg+yOff set) ;
II Definiuj e wycinek o rozmiarze drukowanego obszaru .
pDC->Intersect Cl ipRect CxO rg,YOrg.xOrg+60 0.yOrg -900): OnOraw(pOC); II Rysuje ca ły dokum ent. pOC ->Select Cl i pRgnCNULL ); II Usuwa stary wycinek. pOC->SetWi ndowOrg(O ldOrg) ; II Przywra ca stary początek układu współrzędnych okna. Pierwszym krokiem jest wypisanie nazwy pliku przechowanej w obiekcie CPrintInfo. Funkcja SetText Al ign() obiektu COC umożliwia określenie wyrównania kolejnego tekstu względem punktu odniesienia, który zostanie dostarczony dla łańcucha tekstowego w funkcji TextOut(). Wyrównanie jest określane przez stałą przesyłaną jako argument funkcji. Wyrównanie tekstu możemy określić na trzy sposoby: Stała
Wyrównanie
TA LEFT
Punkt znajduje się po lewej stroni c prostokąta otaczającego tekst, po prawej stronie podanego punktu . Jest to domyślne wyrównanie.
TA RIGHT
Punkt znajduje się po prawej stronie po lewej stronie podanego punktu.
TA CENTER
Punkt znajduje
s ię
w
prostokąta otaczającego
ś ro dku prostokąta otaczającego
Określimy współrzędną x nazwy
pliku na stronie jako
więc
tekst znajduje się
tekst, więc tekst znajduje
się
tekst.
połowę szerokości
strony, a współrzędną
y jako 20 jednostek, czyli 0,2 cala od góry strony. Po przesłaniu nazwy pliku jako wyśrodko
wanego tekstu przywracamy w dokumencie.
domyślne
wyrównywanie tekstu (TĄJE X T) dla
pozostałego
tekstu
Funkcja SetTextAl i gn( pozwala też na zmianę pozycji tekstu w pionie poprzez połączenie operatorem OR kolejnej flagi z flagą wyrównania. Drugą flaga może być jedna z poniższych: Stała
Wyrównanie
TA TOP
Wyrównuje górną krawędź prostokąta otaczającego tekst z punktem tekstu. Jest to domyślne ustawienie.
TA BOnOM
Wyrównuje dolną krawędź prostokąta otaczającego tekst z punktem określającym po zycję tekstu .
TA BASELINE
Wyrównuje tekstu.
Następnie
linię
odniesienia czcionki użytej dla tekstu z punktem
określającym pozycję
określającym pozycję
w funkcji OnPrint() wykorzystywany jest omawiany wcześniej sposób odwzorowywania obszaru dokumentu w bieżącej stronie. Dokument zostaje narysowany na stronie poprzez wywołanie funkcji OnOraw( l, używanej do wyświetlania dokumentu w widoku. Funkcja
952
Visual C++ 2005. Od podstaw dokument, jednak możesz ograniczyć to, co pojawi się na stronie, poprzez okrewycinania. Wycinek otacza prostokątny obszar w kontekście urządzenia, do którego następuje wyj ście. Dane wyjściowe poza wycinkiem są pomijane . Można także okreś l i ć nieregulame kształty wycinków, które nazywamy wtedy obszarami.
ta rysuje
cały
ś l en i e prostokąta
Początkowo domyślnym
obszarem wycinania w kontekście urządzenia jest granica strony. My wycinek odpowiadający wyśrodkowanemu na stronie obszarowi o wymiarach 600 na 900 jednostek. Dzięki temu będziemy rysować ten obszar i nie nadpiszemy nazwy pliku. określamy
Po narysowaniu bieżącej strony zostaje wywołana funkcja Set Cl i pRgn() z argumentem NULL w celu usunięcia wycinka. Jeżeli tego nie zrobimy, tytuł dokumentu zostanie pominięty na wszystkich kolejnych stronach, pon ieważ leży on poza wycinkiem, który byłby wówczas aktywny w procesie drukowania do czasu kolejnego wywołania I nt ers ectCl i pRect ( ). Na koniec wywołujem y ponownie Set Wi ndowOrg() w celu przywrócenia pierwotnego położenia, co zo stało omówione wcześniej w tym rozdziale.
początku
okna do
Drukowanie dokumentu Aby wydrukować swój pierwszy dokument z programu Sketcher, musisz skompilować projekt i uruchomić program (po poprawieniu wszystkich literówek). Po wybraniu FilelPrint Preview widok okna powinien przypominać ten z rysunku 17.9.
RYSIInek 17.9
fJ
~
" 'I
~ ""
"'
Funkcję podglądu wydruku otrzymujemy gratis . Platforma do utworzenia obrazów stron w oknie podglądu wydruku wykorzystuje kod, który wpisal iśmy dla zwykłego drukowania wielostronicowego. To, co pojawia się w oknie podglądu wydruku, powinno być zgodne z tym, co pojawia się na wydrukowanych stronach.
Rozdział 17.•
Przechowywanie idrukowanie dokumentów
953
Podsumowanie Z tego rozdziału dowiedziałeś się , jak zap i sać dokument na dysku w formie pozwalającej na ponowny jego odczyt i zrekonstruowanie c zęśc i składowych obiektów, korzystając z procesu serializacji obsługiwanego przez MFC. W celu zaimplementowania serializacji dla klas defin iujących dane dokumentu musisz:
l
Wyprowadzić własną klasę bezpośrednio
lub pośredn io z CObj ect.
2.
Umie ści ć
makro DECLARE_SERIAL O w definicji klasy.
a.
Um i eśc ić
makro IMPLEMENT_SERI AL O w implementacji klasy .
..
Zaimplementowaćdomyślny
I.
Zadeklarować funkcj ę
8.
Zaimplementować funkcję
konstruktor w klasie.
Seri al i ze() w klasie. Seri al i ze( ) w klasie do serializacj i wszystkich danych
składowych.
Proces serializacji wykorzystuje obiekt CArchi ve do przeprowadzania operacji wej ścia-wyjścia. Używamy obiektu CArchi ve przesyłanego do funkcji Seri al i ze() w celu serializacji danych składowych klasy. Dowiedziałeś się też, jak MFC obsługuje wypisywanie do drukarki. Aby rozszerzyć podstawowe możliwości dostarczone domyślnie, możesz zaimplementować własne wersje funkcji klasy widoku , biorące udział w drukowaniu dokumentu. Funkcje te pełnią następujące role:
Funkcja OnPreparePrinting()
Rola Okr e śl a liczbę
stron w dokumenc ie i wywołuje
fun k cj ę s kł adową
OoP repareP rinting() widoku. OnBeginPrint ing()
W kontekście urządzenia alokuje zasoby wymagane podczas drukowania i określa lic zbę stron w dokumencie, j eżeli zależy ona od informacji z kontekstu urządzenia .
OnPrepareOC( )
Ustawia
OnPrint O
Drukuje dokument.
OnEndPri nt ingO
Usuwa wszelkie zasob y GOI utworz one przez OnBegi nPri nt ing() i wykonuje wszelkie dalsze porządki .
właściwo ści
kontekstu
urządzenia.
Informacje związane z procesem drukowania są przechowywane w obiekcie typu CPr i nt Info, który jest tworzony przez platformę. Możesz przechowywać dodatkowe informacje w widoku lub innym własnym obiekcie. Jeżeli użyjesz obiektu własnej klasy, możesz go śledzić , umieszczając do niego wskaźnik w obiekc ie CPri nt Info.
954
Visual C++ 2005. Od podstaw
Ćwiczenia Kod źródłowy oraz rozwiązania do ksiazki/vcppo.htm
l
pon iższych ćwiczeń
znajdziesz pod adresem http://helion.pl/
Dodaj do funkcj i On Pri nt ( ) kod drukujący na dole strony jej numer w postaci "Strona n". Jeżel i wykorzystasz możliwo ś ci klasy CStri ng, zajmie Ci to jedynie trzy wiersze! zmi e ń implementacj ę tak, aby skalowanie (Wskazówka - wyszukaj funkcję Creat ePoi ntFont () w systemie
2. Jako kolejne rozszerzenie klasy CText działało poprawnie.
pomocy ).
18 Tworzenie własnych plików DLL W rozdziale 9. omówiłem, w jaki sposób biblioteki klas C++/CLI są przechowywane w pliku .dl!. Biblioteki dołączalne dynamicznie są również intensywnie wykorzystywane w aplikacjach tworzonych w natywnym C++. Pełne omówienie tego tematu wykracza poza zakres książki dla początkujących, jednak jest to zagadnienie na tyle ważne, aby poświęci ć mu chociaż jeden rozdział. Z tego rozdziału dowiesz się: działaniu.
•
O bibliotekach DLL i ich
•
Kiedy
•
Jakie
•
Jak
można rozszerzyć możliwości
•
Jak
zdefiniować,
•
Jak
uzyskać dostęp
powinieneś rozważyć
są rodzaje
zaimplementowanie DLL.
DLL i do czego
co jest
służą.
MFC , korzystając z DLL.
dostępne w
DLL.
do zawartości DLL z programu.
Poznaj OLL Niemal wszystkie języki programowania obsługują biblioteki z modułami standardowego kodu dla często wykorzystywanych funkcji. W natywnym C++ korzystałeś z wielu funkcji przechowywanych w bibliotekach, jak np. z funkcji cei l O, której użyłeś w poprzednim rozdziale , a która jest zadeklarowana w nagłówku <mat h>. Kod tej funkcj i jest przechowywany w pliku biblioteki z rozszerzeniem .lib i podczas tworzenia modułu wykonywalnego programu Sketcher konsolidator pobrał z biblioteki kod tej standardowej funkcj i i z integrował jej kopię z plikiem .ex e programu Sketcher. Gdybyśmy napisali kolejny program, korzystający z tej samej funkcji, miałby on również własną kopię funkcji cei l (). Funkcja cei l () jest statycznie powiązana z każdą aplikacją i jest integralną częścią modułu wykonywalnego, co obrazuje rysunek 18.1.
956
Visual C++ 2005. Od podstaw
Biblioteka statyczna
Kopia dodawana do
każdego
programu w trakcie konsolidacji
Funkcja biblioteczna
ProgramA.exe
ProgramB.exe
4
L--.
Programc.exe
Funkcja biblioteczna
Funkcja biblioteczna
,
L--.
Funkcja biblioteczna
Rysunek 18.1 Mimo że jest to bardzo wygodny sposób wykorzystywania standardowej funkcji, wymagający najmniejszego nakładu pracy ze strony programisty, ma on swoje wady w sytuacji, gdy kilka jednocześnie uruchomionych programów wykorzystuje tę samą funkcję w środowisku Windows. Standardowa funkcja dowiązana statycznie, gdy jest wykorzystywana przez kilka programów , jest powielana w pamięci przez każdy program z niej korzystający. W przypadku funkcji cei l ( ) może s i ę to wydawać bez znaczenia, jednak niektóre funkcje - np. wejścia -wyj ścia - wykorzystywane często w programach, zajmują znaczne fragmenty pamięci . Ich statyczne dowiązanie byłoby szalenie nieefektywne. W kolejnym przypadku standardowa funkcja ze statycznej bibliotek i może być wiązana z setkami programów w systemie , więc jej kod umieszczany w pliku .exe każdego programu niepotrzebnie zajmowałby miejsce na dysku. Z tego względu Windows obsługuje jeszcze jeden sposób korzystania ze standardowych funkcji . Są to biblioteki dołączalne dynamicznie (ang. Dynamie Link Library - DLL). Pozwalają one na współużytkowanie standardowej funkcj i przez kilka jednocześnie uruchomionych programów i nie wymagają umieszczania kopii kodu z funkcj i bibliotecznej w wykonywalnym module programu.
Rozdział18 .•
Tworzenie własnych plików DLL
957
Jak działaią DLL Biblioteka dołączaLna dynamicznie jest plikiem zawierającym zbiór modułów, które mogą być wykorzystywane przez dowolną liczbę różnych programów. Plik ten ma przeważnie rozszerzenie .d!!, jednak nie jest to wymóg. Nadając nazwę plikowi DLL, można wybrać dowolne rozszerzenie, jednak może to mieć wpływ na sposób obsługi tych plików przez system Windows. Windows automatycznie ładuje biblioteki dołączalne dynamicznie, mające rozszerzenie .dl!. Jeżeli mają one inne rozszerzenie, należy je jawnie ładować, dodając w tym celu kod do programu. Sam Windows używa rozszerzenia .exe dla nicktórych plików DLL. Spotkałeś się też pewnie z rozszerzeniami . vbx i .ocx, które są nadawane plikom DLL zawierającym specyficzne rodzaje kontrolek. Możesz
sobie wyobrażać, że masz wybór, czy w swoim programie korzystać z bibliotek dołą czalnych dynamicznie, ale niestety go nie masz. Win32 API używany przez wszystkie programy dla systemu Windows oraz API są zaimplementowane w zestawie plików DLL. Biblioteki dołą czalne dynamicznie są podstawą programowania w Windowsie.
Dołączanie funkcji z DLL do programu odbywa się inaczej niż przy bibliotekach dołącza nych statycznie, gdy kod był umieszczany raz na zawsze podczas konsolidacji programu przy tworzeniu modułu wykonywalnego. Funkcja z DLL jest dołączana do korzystającego z niej programu w sytuacji, gdy aplikacja jest uruchomiona, a dzieje się to jedynie wtedy, gdy program jest wykonywany, co obrazuje rysunek 18.2.
Rysunek 18.2 pokazuje sekwencję zdarzeń, gdy po kolei uruchamiane są trzy programy korzystające z funkcji znajdującej się w DLL, a następnie są jednocześnie wykonywane. Żaden fragment kodu z DLL nie jest zawarty w pliku wykonywalnym któregokolwiek z programów. Gdy jeden z programów jest wykonywany, jest on ładowany do pamięci i jeżeli DLL, z której on korzysta, nie jest jeszcze obecna, jest ona ładowana osobno. Następnie ustanawiane są odpowiednie dowiązania między programem i DLL. Jeżeli program zostaje załadowany, a biblioteka już znajduje się w pamięci, musi jedynie zostać utworzone dowiązanie programu i wymaganej funkcji z DLL. Zauważ, że
gdy program wywołuje funkcję z DLL, Windows automatycznie ładuje DLL do kolejny program ładowany do pamięci, korzystający z tej samej DLL, może wykorzystywać możliwości dostarczane przez tę samą kopię DLL, ponieważ Windows rozpoznaje, że biblioteka znajduje się już w pamięci, i po prostu ustanawia dowiązanie między nią a programem . Windows ś l edzi liczbę programów korzystających z poszczególnych DLL znajdujących się w pamięci, więc biblioteka pozostaje w pamięci, dopóki korzysta z niej choć jeden program . Gdy biblioteka przestaje być wykorzystywana przez wykonywane programy, Windows automatycznie usuwa ją z pamięci . pamięci. Każdy
MFC są dostarczane w postaci szeregu bibliotek DLL, które mogą być dołączane dynamicznie do programu, a także biblioteki dołączanej statycznie. Domyślnie kreator aplikacji generuje program dołączający dynamicznie biblioteki DLL MFC. Umieszczenie funkcji w DLL umożliwia zmianę funkcji bez konieczności wprowadzania zmian do programów z niej korzystających . Dopóki interfejs funkcji w DLL pozostaje ten sam, programy mogą bezproblemowo korzystać z nowej wersji funkcji, bez potrzeby ponownej kompilacji czy też konsolidacji. Niestety ma to również pewną wadę : łatwo można zacząć
Programowanie mechanizmu transferu danych międ zy b azą danych a oknem dialogowym, którego właści cielem jest obiekt CProduct Vi ew, zos ta ło przedstawione na rysunku 19.19. Kl asa zestawu rekordów i klasa widoku rekordu współp ra c ują ze sobą w celu umożliwienia transferu danych między bazą danych a kontrolkami w okn ie dialogowym. Klasa CProductSet obsługuj e transfer między bazą danych i własnymi składowymi, zaś klasa CProductView zajmuje s ię transferem mi ęd zy składowymi CP roductSet a kontrolkami w okni e dialogowym.
Testowanie przykładu M o żesz
mi nie w i e rzyć , ale można już uruchomić przykład. Po prostu skompiluj go w zwy sposób , a następnie uruchom. Aplikacja powinna wyświetlić okno podobne do tego z ry sunku 19.20. kły
Rozdział 19.
Rysunek 19.19
~ączenie się ze źródłami
•
\;~~~ I
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Tabela Productsjako dane w DBSample
RFX wywołuje DoFieldExchangeO
Q) 01
CProductSet
1+ m_ProductlD m_ProductName
c:
'"
s: u
x
UJ
s
o'" o o m st rSort ~ "[Cat egoryID] . [Produet ID]" ; II Ustawia kryteria sortowania.
CReco rdView: :Onlnitia l Updat e ( ) :
Po prostu ustawiamy m_st rSor t w zestawie rekordów na łańcuch zawierający nazwę pola z identyfikatorem kategorii i nazwę pola Produet ID. Nawiasy kwadratowe są przydatne, nawet jeżeli w nazwach nie występują spacje, ponieważ pozwalają one odróżnić łańcuchy zawierające te nazwy od innych łańcuchów, dzięki czemu można od razu odnaleźć nazwy pól. Oczywiście, jeżeli w nazwach pól nie ma spacji , nawiasy nie są wymagane.
Zmienianie podpisu okna Jest jeszcze jedna rzecz, którą warto byłoby w tym momencie dodać do tej funkcji - tytuł okna byłby bardziej użyteczny, gdyby zawierał nazwę wyświetlanej tabeli . Można to uczynić poprzez dodanie kodu ustawiającego tytuł obiektu dokumentu : void CP roduct Vi ew: :Onl nit i alUpdat e( ) ( m_pSet = &Get Document( )->m_DBSample Set :
II Ustawia n azwę tabelijako tytul dokumentu. i f (m_pSet ->IsOpen( ») II Sprawdza, czy zestaw rekordówje st otwarty.
(
CSt r i ng st rTit l e ~ _T("Ta ble Name" ) ; IIUsta wia p odsta wu wy la ńcuch tytulu . CStri ng st rTable = m~ p S e t- > G e tT a b l eN a m e ( ) ; if(l strTabl e . IsEmpt y(» II Sprawdza, czy mamy nazwę tabeli, st rTitle + = _T(" ; ") + st rTable : Il i dodaje ją do lań cucha z tyt ulem . GetDoc ument () ->SetTit l e(strT it l e) : II Ustawia tytul dukumentu.
Po upewnieniu s i ę , że zestaw rekordów jest faktycznie otwarty, zostaje zainicjalizowany lokal ny obiekt CStr i ng z podstawowym łańcu chem tytułu . Następnie zostaje pobrana nazwa tabeli z obiektu zestawu rekordów poprzez wywołanie funkcji s kładowej GetTabl eName ( ). N ależy s p rawdzić, czy łańcuch faktyc znie został zwrócony z funkcj i GetTableNameO . Istnieje wiele sytuacji, które uniemo żliwiają ustawienie nazwy tabeli - na przykład w skł a d zestawu rekor dów może wch odzić więcej ni ż jedn a tabela. Po dołączeniu dwukropka i pobranej nazwy tabeli do podstawowego tytułu w strTit l e ustawiamy wynik jako tytuł dokum entu poprzez wywoła nie s kł a dowej dokumentu SetTit l eO . Gdy ponownie przebudujesz aplikację, będzie ona d ziałała jak popr zednio, z tą ró żn icą, że okno b ędzie m i ało nowy tytuł, j ak na rysunku 19.21. Produkty są up orządkowane ro snąc o według swoich identyfikatorów w ewnątrz kategorii również okre ślanych przez identyfikatory kategorii.
Rysunek 19.21
Produet ID
Product Nam13
Catec;ory ID
E=:J
Ichai
UnitPrice
Unit. In Setek
Ready
Units On Order
~
NUM
Używanie drugiego obiektu zestawu rekordów wszystkie produkty z bazy danych, sens ownie byłoby dodać wszystki ch zamówi eń na konkretny produkt. W tym calu dodamy kolejną kl a s ę zestawu rekordów, która będzie obsł u giwał a informacje o zamówieniach z bazy danych, oraz komplementarn ą klas ę widoku, wyświ e t lającą niektóre pola z zestawu rekordów. Gdy
mo żem y ju ż p rzegląd ać
możliwoś ć wyświetlenia
100& Visual C++ 2005. Od podstaw Dodamy także przycisk do okna dialogowego Prod ucts , który będzie umożliwiał przejście do okna dialogowego Orders (zamówienia), gdy użytkownik będzie chciał zobaczyć zamówienia na bieżący produkt. Umożliwi to działanie w sposób przedstawiony na rysunku 19.22. To okno dialogowe Product ID
I Edit
Product Name
I Edit
Unit Price
I Edit
Units In Stock
I Edit
CategorylD
I_E_d_it_ _
Units On Order
I Sample ed I
umożliwia przeglądanie
wszystkich dostępnych produktów
I Show Orders I Naciśnięcie przycisku Show Orders spowoduje otwarcie okna dialogowego Orders dla bieżącego produktu Naciśnięcie przycisku
Show Products spowoduje powrót
do okna dialogowego Products
To okno dialogowe umożliwi przejrzenie
wszystkich zamówień na konkretny produkt
Order ID
Edit
Product ID
Edit
Quantity
I_E _d_it_ .....J
Customer ID
Show Products
Ryslmek 19.22 Okno dialogowe Products jest punktem wyjściowym, z którego można przeglądać w obu kierunkach wszystkie dostępne produkty. Kliknięcie przycisku Show Orders spowoduje przejście do okna dialogowego, w którym będzie można przeglądać wszystkie zamówienia na bieżący produkt. Po kliknięciu przycisku Show Products można powrócić do okna dialo gowego Products.
Dodawanie klasy zestawu rekordów Zaczniemy od dodania klasy zestawu rekordów dla zamówień. Kliknij prawym przyciskiem myszy DBSample w panelu Class View i wybierz Add/Class z menu podręcznego. Wybierz MFC z zestawu kategorii Visual C++ i MFC ODBC Consumer jako szablon. Po kliknięciu przycisku Add w oknie dialogowym Add Class pojawi się okno MFC ODBC Consumer Wizard (rysunek 19.23).
Rozdział 19.
• lączenie się ze źródłami danych 1007
Rysunek 19.23 Welcome to the MFCODBCConsumer Wizard
Dat.asource:
I
Dato ~OU'ce .. .
o.;~
'-
----', 1 ....
Tvpe:
o
O O:t.noset
----' ~i1d all columns
(i:)Saapsnct
Flnish
II
( oneel
Wybierz typ konsumenta jako Snap shot poprzez kliknięcie odpowiedniego przycisku opcji, a następnie kliknij przycisk Dala Sourc e, aby przejść do okna dialogowego Wybierz ź ródło danych, służącego do określania źródła danych; powinno ono zn aj d ować si ę w zakładce Kom puterowe ź ró dła danych. Po wybraniu w ten sam sposób jak poprzednio bazy Northwind jako źródła danych zostanie wyświetlone okno dialogowe Select Database Object, przedstawione na rysunku 19.24.
Rysunek 19.24
X
setect Database object
s . ll!f Lebles l j_..g C~t egories ! i--m1 Custorners
OK e "" cel
j··..łiil E mpIo~ ee ' ! Il!l Orae; Delali :..··!lil Order.
i Lm Prooucts . "'!liI Shippers 1 .. 1lll Supplier s
. aVie...
Tym razem będziemy chcieli powiązać dwie tabele z zestawem rekordów, więc przytrzymaj wciśnięty klawisz Ctrl i wybierz tabele Orders i Order Detai 1s. Następnie klikn ij przycisk OK, aby zakończyć wybieranie. Powrócisz tym samym do okna dialogowego MFC ODBC
1008 ViSUal C++ 2005. Od podstaw Consumer, w którym znajdują się już nazwa klasy i nazwy plików. Zmień nazwę klasy na COrderSet , a następnie w odpowiadający nowej nazwie sposób zmień nazwy plików (rysu nek 19.25) .
Rysunek 19.25 Welcome to the MIT: DDBC Cnnsurner Wizard
Data source:
I
Oota~ee ...
·1
et"'.:
IQ
.ellP file: e-d;~-----"] Q
o !lird al!colurms
Finish
Kliknij przycisk Finish, aby
wygenerować klasę
licance!
COrderSet.
Jak widziałeś przy okazji klasy CProductSet, która została utworzona w początkowej fazie projektu, implementacja funkcji GetDefaul t Connect() w klasie COr derSet jest poprz edzona dyrektywą #error , która uniemożliwia kompilację, więc umieść ją w komentarzu. W klasie COrderSet zostały utworzone dane składowe dla każdego pola z obu tabel. Gdy wybierzesz dwie lub więcej tabel dla danego zestawu rekordów, zawsze istnieje możliwość , a nawet całkiem duże prawdopodobieństwo wystąpienia tych samych nazw pól. Na przykład pole Order 10 występuje w obydwu tabelach. Aby zapewnić zróżnicowanie nazw pól dla da nych składowych, nazwy pól są zawsze poprzedzane nazwą tabeli. Jeżeli niepotrzebne są Ci wszystkie pola, możesz je usunąć lub umieścić w komentarzu, ale jak wcześniej wspomina łem , musisz uważać, aby nie usunąć żadnych zmiennych będących kluczami głównymi. Gdy usuniesz składową dla pola tabeli, musisz także usunąć jej inicjalizację w konstruktorze klasy oraz wywołanie RFX_() dla tej zmiennej z funkcji składowej DoFi el dExchange(). Musisz także zmieni ć początkową wartoś ć składowej m_nF i el ds w konstruktorze klasy COr der Set, aby odzwierciedlała liczbę pól pozostałych w klasie. W tym przykładzie musisz zachowa ć nastę pujące dane składowe: m_Or der sOr der ID, m_Orderuet.ai l sOrder I D, m_OrderDeta i l sProduct I D, m_OrderDetai l sQuant i ty i m_OrdersC us tomerID. Jeżel i chcesz zachować tylko te, zmień war tość przypisywaną m_nFi el ds na 5. Zmień typy danych składowych CStri ngW na CSt r i ng. Aby umieścić nowy zestaw rekordów w dokumencie , musimy dodać nową składową do defi nicji klasy CDBSamp l eDoc, więc kliknij jej nazwę w panelu C/ass View i wybierz Add Variable z menu podręcznego. Jako typ podaj COrder Set, aj ako nazwę m_OrderSet. Pozostaw jąjako publiczną składową klasy. Kliknij przycisk OK, aby zakończyć dodawanie składowej do
Rozdział 19.
• ~ączenie się ze źródłami danych 1009
dokumentu. Kompilator musi wiedzieć, że COrderSet jes t klasą, zanim rozpocznie kompilowa nie klasy CDBSamp l eDoc. Jeże li zajrzys z do pliku nagłówkowego DBSampleDoc.h, zobaczysz, że na jego początku została dodana dyre ktywa #i ncl ude.
#pragma ance #inc l ude "DBSampleSet .h" #incl ude "arderset .h" class CDBSamp leDac publ ic CDacument { II Reszta definicji klasy.
}
Dodawanie klasy widoku dla zestawu rekordów W tym momencie możesz spodziewać s ię, że dodanie klasy wyprowadzanej z CRecor dVi ew odbywa s ię poprzez wybranie Add/Class z men u kontekstowego pane lu Class View w celu wyświetle nia danych z obiektu COrderSet. Było to możliwe we wcześniejszych wersjach Visual C++, ale niestety Visual C++ 2005 nie zapew nia tej możliwości . Okno dialogowe służące do dodawania nowych klas nie um ożli w i a wybra nia CRecordV i ewjako klasy bazowej, więc zawsze trzeba samodzielnie t w orzy ć klasy, któ rych klasą bazow ą jest CRecor dVi ew. Zanim dodamy klasę widoku, musimy utworzyć kolej ny zasób dialog u, abyś my mieli dostępny identyfikato r zaso bu, którego u żyj e my w defi nicji klasy widoku .
Tworzenie zasobu dialogu Przejdź do widoku zaso bów , klikn ij prawym przyciskiem myszy folde r Dialog i wy bierz Jnsert Dialog z menu kontekstowego . U suń z okna dialogowego obydwa d o m y śl n i e dodane przyciski. Następ nie musimy zmieni ć identyfikator i sty le okna dialogowego, w ięc kliknij je prawym przyciskiem myszy i wybierz Propert ies z menu kontekstowego, aby wy świet l ić właściwości okna dialogowego. Zm i eń identyfikator na IDD_ORDERSJORM. Z m i eń tak że wła ś ciw ość Style na Child i Border na None.
Teraz możemy umi eśc ić w oknie dialogowym kontrolki dla pól z tabel Or ders i Or der Detai l s, które chcemy wyświetlić . Je ż el i podczas pracy nad oknem dialogowym przejd ziesz do panelu Class View i wyb ierzesz k lasę CO r derSet, b ędziesz mógł zobaczyć nazwy zmie nnych. Dodaj i rozmieść kontrolk i w oknie dialogowym w spo sób pokazany na rysunku 19.26.
Rysunek 19.26
f
Order ID
[ s"mple od
PioductID
Is" mple ed I
Q u."t ~y
I
s" mple ed '
( ustome; ID
I
Sampleed
I
~how Products I
Mamy tutaj cztery kontrolki edycji dla pól Or der ID, Cust omerID, Pr oductI D i Quantity z tabel z k lasą COrderSet , a t a kże kontrol ki statyczne, które je opis uj ą. J eżeli nie usu n ął e ś składowyc h klasy, możesz do dać kon trolki dla innyc h pól. Nie zapomnij o zm ianie
p ow i ąza nych
1010
Visual C++ 2005. Od podstaw identyfik atorów kontrolek edycji na takie, które będą okre ślał y przeznaczeni e kont rolki. nazw pól tabeli poprzedzonych nazwami tabel, aby zac ho wać zgodno ść z na zwami danych s kł ad owych . Na koni ec musisz o zn aczyć kontrolki j ako tylko do odczytu poprzez ustawi eni e ich wła ś ciw o ś ci Read Only na True. Ewe ntualnie m oże sz za jednym razem ustawi ć tę właśc i w ość dla wszys tkich kontrolek , je żeli wybierzesz je wszystkie, trzy mając wci śni ęt y kla wisz Ctrl, a nast ępn i e zmienisz w ł aśc iw ość Read Only na True.
Mo żesz u ży ć
Przy cisk za ty tu łow any Show Produets b ędz i e używany do powracania do widoku tabeli Pr o duet s, w i ęc zmień identyfikator tego przy cisku na IOCPROOUCTS. Gdy uło żysz już wszy stko według włas n ych upodobań, zapis z zasób okna dialogowego.
Tworzenie klasy widoku rekordu Utwórz plik Orderl/iew.h, który będzi e zawi erał definicję klasy CO rder View. W tym celu klik nij prawy m przyc iskiem myszy DBSample w panelu Solution Expl orer i wybierz Add/New Ilem z menu kontek stow ego. Wybierz szabl on dla tworzenia pliku .h i wp isz Order Viewjako nazwę pliku . Po utw orzeniu pliku dodaj następujący kod definicji klasy :
#pragma once cl ass COrderSet : Cl ass CDBSampleDoc ;
Ten kod opiera s ię na kodzie wygenerowanej klasy CPr oductvi ew. Makro DECLARE_DYNCREATE umożliwia tworzenie przez plat form ę MFC obiektów typu tej klasy w momencie uruchamia nia. Z reguły klasy MfC dokumentu, widoku i ramek z aw ieraj ą to makro. Nieco późni ej do damy komplementarne makro IMPLEMENT_DYNCREATE do pliku .cpp . Pominąłem wersj ę testową funkcji GetDocument() , ponieważ klasa CProductVie w zawiera wersję funkcji , która sprawdza poprawn ość obiektu dokumentu. Wplatana wersja definicji klasy COrder Vi ew po prostu zakłada, że rzutowanie na CDBSamp l eDoc* będzie w p orządku . U m ieśc iłem deklaracje As ser tVal i dO i Dumpi ), które s ą kompil owan e jedyn ie w trybie testowym , wi ęc definicje mu szą być zawarte w pliku .cpp dla klasy. Wylicze nie określ a identyfikator dialogu i wykorzystamy go w defin icji konstruktora. Składow a m_pSet będzi e przechowywała adres obiektu zestawu rekordów, do st arcz aj ącego dane wy św i etl an e w tym widoku. Implementacja klasy COrderVi ew będzie znaj d o wać si ę w pliku Orderl/iew.cpp, więc utwórz ten plik w projekcie w ten sam sposób jak przy okazji pliku .h. Dodaj po czątkowe dyrektywy #i ncl ude dla klas, do których będz iemy potrz ebowali odniesień :
#inc l ude #i nc lude #incl ude #i nel ude #incl ude
Mam y tu wyw ołan i e kon struktora klasy bazowej z argume ntem będąc ym identyfikatorem dialogu, który z o s tał o kreś lo n y w wyliczeniu w klasie. Okre śla on okno dialogowe p owi ązane z widokiem. Teraz dodamy definicje dwóch funkcji, które mogą zo stać testowym. II Diag nostyko COrderView.
#ifdef _OEBUG void COrderView: :Assert Val id( ) const (
CRecordView- :Assert Val idC);
}
void COrderVi ew: :Oump(COumpCont ext &de) eonst
{
użyte
podczas uruchamiania w trybie
1012
Visual C++ 2005. Od podstaw CRecordView: :Dump(dcl :
}
#endi f
II DEB UG.
Funkcja OoOata Exchange () łączy kontrolki okna dialogowego z polami w zestawie rekordów . Definicja tej funkcji wygląda n a st ępuj ąco :
vOl d COrderView: :DoDataE xcha nge (CDat aExc ha nge* pDX l (
CRecordView: :DoData Exchange (pDXl : DDX_Fie ldText (pDX. IDC_ORDERDETAILS_ORDERID. m_pSet->m_OrderDeta i lsOrderID. m_pSet l : DDX_F ieldText (pDX. IDC_ORDERS_CUSTOMERID. m_pSet ->m_OrdersCustomer ID . m_pSetl: DDX_Flel dText (pDX. IDC_ORDERDETAI LS_PRODUCTI D. m_pSet ->m_OrderDet ai l sProduct ID . m_pSet l: DDX_FieldText (pDx. IDC_ORDERDETAILS_OUANTITY. m_pSet ->m_Orde rDet ai lsOuantlty. m_pSet l ; Składowa
s łuży do uzyskania dostępu do pól w obiekcie COrderSet, które maj ą zostać Drugi arg ument ka żdej funk cji OOXJi el dTextC ) okre śla kontrolk ę dla pol a wskazywanego przez trzeci argument. Jak w idz iałeś przy okazji klasy CProduct View, pierwszy argument określa, czy dane maj ą być przesyłane z, czy do kontrolki. Ostatni argument po prostu określ a zestaw rekordów zaangażow any w cały proces.
m_pSet
wy świet l o n e.
N al eży również zdefiniować dwie funkcje bi orące udzi ał w pobieraniu zestawu rekordów. W celu uzyskania wskaźnika do obiektu COrderSet kapsułkującego zestaw rekordów wywołu jemy funkcję Get RecordSet C) . Możemy ją zaimplementow a ć w następujący sposób:
COrderSet* COrderView : :Get Recordset Cl (
ASSERT(m_pSet 1= NU LLl : ret urn m_pSet : Składowa m_pSet zawiera wskaźnik do zestawu rekordów. Zn ajdujące si ę tu makro MFC ASSERT przerywa działanie programu z komunikatem, jeżeli wyrażenie między nawiasami wyniesie O. A więc po prostu sprawdza, czy wskaźnik COrderSet nie jest pusty. Makro ASSE RT ma tę zal etę , że dzi ała jedynie w wersji testow ej . W wersj i ostatecznej nic nie robi.
Funkcja OnGet Reco r dSetC) jest czysto wirtualn ą funkcją w klasie bazowej , więc musimy ją tu jako:
zdefini ować . Moż emy ją zaimplementować
CRecordset* COrderView : :OnGet Recordset ()
{
ret urn m_pSet :
}
W tym przypadku zwraca ona po prostu adres w m_pSet. Oczywi ści e w sytuacji , gdyby nale żało ponownie utw orzy ć zest aw rekordów, kod b yłb y znaczn ie bardziej skomplikowany. Nie
skoń czyliśmy
określ eniu ,
co ma
j eszcze z kla s ą widoku . Kolejny etap będzie zestaw rekordów dla zamów i eń .
zawierać
polegał
na
dokł adni ejszym
Rozdział 19.
• ~ączenie się ze źródłami danych
1013
Dostosowywanie zestawu rekordów W obecnej postaci polecenie SQL SELECT dla obiektu COr derSet utworzy tabelę zawierającą wszystkie kombinacje rekordów z obu tabel. Może to być spora liczba rekordów, więc musimy dodać do zapytania odpowiednik klauzuli WH ERE, aby ograniczyć pobierane rekordy jedynie do tych, które mająjaki ś sens . Ale jest je szcze jeden problem - gdy przechodzimy z przeglądania tabeli Product s, nie chcemy przeglądać po prostu wszystkich poprzednich zamówień. Chodzi nam dokładnie o zamówienia na produkt z identyfikatorem, takim jak przed chwilą oglądany, czyli wybranie jedynie tych zamówień, dla których identyfikator produktu jest taki sam j ak ten zawarty w bieżącym rekordzie CProductSet. Można to zrobi ć również za pomocą klauzuli WHERE . W kontekście MFC klauzula WH EREw poleceniu SELECT dla zestawu rekordów nazywana jest filtrem.
Dodawanie filtru do zestawu rekordów Filtr do zapytania dodaje się poprzez przypisanie łańcucha do składowej m_st r Fi l t er obiektu zestawu rekordów. Ta składowa jest dziedziczona z klasy bazowej CRecor dSet. Podobnie jak było w przypadku klauzuli ORDER BY, którą dodawaliśmy poprzez przypisanie wartości skła dowej m_st rSo rt zestawu rekordów , miejscem implementacji jest tu składowa Onlm t i a l Update( ) klasy widoku zestawu rekordów , tu ż przed wywołaniem funkcji klasy bazowej. Chcemy ustawić dwa warunki filtracji . Pierwszy będzie ograniczał rekordy tworzące zestaw rekordów do tych, których pole Orde rID w tabeli Orde rs będzie równe polu o tej samej nazwie w tabeli Or der Det a11 s . Możemy zapisać ten warunek jako: [Orders] .[OrderID] = [O raer Det ai l s]. [OrderI D] Drugi warunek, który chcemy zastosować dla rekordów spełniających warunek pierwszy, b ędzie wybierał jedynie te rekordy, dla których pole Product ID będ zie równe polu Product ID bieżącego rekordu w obiekcie zestawu rekordów wyświetlającym tabelę Product s . Oznacza to, że musimy porównać pole Product ID z COrderSet z wartością zmiennej . Zmienna w tej oper acji nazywa się parametrem, a warunek dla filtra jest zapisywany w specjalny sposób: ProductI D =
7
Znak zapytania reprezentuje warto ść parametru dla filtra i wybierane są te rekordy, dla których pole Product ID jest równe wartości parametru. Wartość zastępująca znak zapytania jest ustawiana w funkcji skł adowej OoFiel dExchange O zestawu rekordów. Zaimplementujemy to za moment, a najpierw ukończymy określanie filtra. Zdefiniujemy łańcuch dla zmiennej filtra, która
będzie zawierała
obydwa warunki w poleceniu:
II Jako filtr ustawiamy pole Productll) z równymi identyfikatorami zamówień (OrderlD) .
m_pSet ->m_st rFi l te r = "[Produet ID] = ? AND [Orders] .[ Order ID] = [Order Detai ls ] .[Order ID] " . Um i e ści m y
to w
składowej
zakończymy ustawianie
Onln it i al Updat e ( ) klasy COrde r View, ale zanim to uczynimy, parametru dla filtra.
1014
Visual C++ 2005. Od podstaw
Definiowanie parametru filtra Do klasy COrderSet dodamy składową przechowującą bieżącą wartość pola Product IDz obiektu CProductSe t. Ta składowa będzie s łużyła również jako parametr zastępujący znak zapytania w filtrz e dla obiektu CO rderSet. A w ięc kliknij prawym przyciskiem myszy nazwę klasy COrderSet w panelu C/au View i wybierz Add/Add Variable z menu kontekstowego . Typ zmiennej musi b yć taki sam jak s kładowej m_ProductID klasy CProduct Set , czyli lo ng, a jako nazwę podaj m_Produet IDpa ram. Pozostaw jąjako publiczną składową. Musimy z ainicjalizować tę zmienną w kon struktorze, a takż e ustawi ć -li c znik param etrów. Platforma ap likacj i wymaga, aby licznik parametrów w zestawie rek~rdów odzwierciedlał liczbę używanych parametrów, w przeciwnym razie nie będzie działa ć poprawnie. Dodaj zacieniowan y poniżej kod do definicji konstruktora klasy COrde rSet :
mOrderDeta i lsOrderID ~ o: m_OrderDetai l sProd uct ID = o; m_OrderDetai l sOuantity = o; m_OrdersOrderID = o: m_OrdersCust omer ID = L"": mnFields = 5: m_Produet IDparam = OL ; II Ustawia początkową wartość pa rametru. mnPa rams = 1: II Ustawia liczb ę parametrów. m_nDefault Type = snapshot : Cały
niezacieni owan y kod
został
utworzony przez kreator klas w celu inicjalizacji danych polom w zestawie rekordów i do określenia typu jako snapshot. Usuń inicjalizację dla innych pól w zestawie rekordów. Nowy kod inicjalizuje parametr jako zero i ustawia licznik parametrów na 1. Zmienna m_nParams jest dziedziczona z klasy bazow ej CRecordSet . P onieważ istnieje licznik parametrów, możesz się domyślać, że można używać więcej niż jednego parametru w filtrze zestawu rekordów. składowych odpowiadających
W klasie COrderSet u suń albo umi e ść w komentarzu składowe przechowujące niepotrzebne pola z zestawu rekordów. Usuń lub umieść w komentarzu pola z definicji klasy, pozostawiając jedynie poniższe:
long m_OrderDeta i lsOrder ID; long m_OrderDetai ls ProductID: lnt m_OrderDet ail sOuant ity: long m_OrdersOrderID; CStri ng m_OrdersCust omer ID : long m ProductI Dparam :
II Taka sama jak Order ID w tabeli Orders. II Taka sama jak Produet ID w tabeli Products. II Unikalna liczba zamówien ia. II Taki sam wpis jak w tabeli Customers.
Aby określić w klasie zmienną m_ProductID param jako parametr filtra w obiekcie COrde rSet, musisz dodać kod do składow ej DoFi el dExchange() :
pFX- >Set Fi eldType(CFi eldExcha nge : :outputCol umn):
Rozdział 19.• ~ączenie się ze źróllłami
danych
1015
RF X_Lon g(pFX, T( " [Order Detai l s]. [Order ID]" ) . m_Or der Det ai l sOrder ID) : RFX_Long(pFX. _T(" [ Or der Deta il s] [Product ID] " ) . m_Or der Oet ai l sProductlD ) : RFX_I nt( pFX . _T(" [ Order Det ail s]. [ Quant ity]" ) . m_Or derDet ail sQuanti ty ) : RFX_Long(pFX. _T(" [ Or ders ] . [ Order ID]" ). m_OrdersOr der ID) : RFX_Text (pFX. _T( " [Order s] . [ Cust omer ID]") . m_OrdersCust ome r ID) : II Ustaw ia typ pola jako param etr. pFX->Set Fie l dType(CF i el dExchange: :param): RFX Long(pFX. T( "Pr oductlD Par am"). m_ProductlDparam ) ;
Kreator klas dostarczył kod przesyłający dane między bazą danych a zmiennymi pól, które dodał do klasy. Dla każdej danej składowej zestawu rekordów dostępne jest jedno wywolanie funkcji RFX_( ). Usuń te, które nie są potrzebne w tej aplikacji, pozostawiając jedynie te wystę puj ące w poprzednim kodzie . Pierwszy nowy wiersz kodu wywołuje składową Set Fie l dType( ) obiektu pFX w celu ustawienia trybu pa ramdla kolejnych wywołań RFX_( ). W rezultacie trzeci argument wszystkich kolejnych wywołań RF X_() będzie interpretowany jako parametr zastępujący ? w filtrze zestawu rekordów . Jeżeli używasz więcej niż jednego parametru, parametry zastępują znaki zapytania w łańcuchu m_strFi lter w kolejnoś ci od lewej do prawej, więc należy dopilnować, by wywołania RFX_() były we właściwej kolejności , jeżeli jest ich kilka. W trybie par am drugi argument w wywołaniu RFX _() jest ignorowany, więc jeżeli chcesz, możesz tu umieś cić NULL lub jakiś inny łańcuch .
Inicjalizowanie widoku rekordu możemy zaimplementować przesłonięcie funkcji On lniti al UpdateO w klasie COrder Vi ew. Ta funkcja wywoływana jest przez platformę MFC, zanim widok zostanie wyświetlony,
Teraz
więc możemy
padku
w tej funkcji umieścić ca ły kod dla jednorazowych inicjalizacji . W tym przytu filtr zestawu rekordów. Oto definicja tej funkcji :
określimy
void COrderView: :Onl ni t i al Updat e( )
{ Begi nWaltCur sor ( ): CDBSamp le Doc* pDoc ~ st ati c_cast (GetDocument (» ) : m_pSet = &pDoc ->m_O rder Set : II Pobiera wskaźnik do zestawu rekordów. II Użyj bazy danych, dla której j est otwarty zestaw rekordów z produktami. m_pSet ->m_pDatabase = pDoc ->m_DBSamp l eSet .m_pData base: II Jako parametr ustaw b ieźący identyji kator produktu. m_pSet ->m_Product IDpa r am = pDoc->m_DBS ampl eSet .m_Product ID; II Jako /ay teriumjiltrowania usta w pole identyjikatora produktu. m_pSet- >m_strFi l ter = "[ Pr oduct IDJ =? AND [Orders]. [OrderID ] = [Order Detai ls ] .[Order ID]" :
CRecordView: :Onl ni t i al Updat e( ): EndWai t Cur sor ( ) :
1016
Visual C++ 2005. Od podstaw Dodaj tę definicję funkcji do pliku OrderView.cpp. Wersja klasy COrderS et wygenerowana przez kreator klas nie prz esłania składowej GetOocument ( ), ponieważ nie jest początkowo powiązana z kJasą dokumentu. W wyniku tego trzeba rzutować wskaźnik ze składowej GetOocume nt ( ) klasy bazowej do obiektu COBSa mpl eOoc. Ewentualnie możemy dodać przesłoniętą wersję GetOocument( ) do COrde rSet w celu wykonania rzutowania. Tak czy siak, potrzebujemy wskaźnika do obiektu dokumentu, ponieważ musimy uzyskać dostęp do składowych obiektu. Wywołanie
Begi nWa i tCurso r ( ) na początku funkcj i OnInit i al Updat e ( )w czasie wykonywania kursor w kształcie klepsydry. Dzieje się tak dlatego, że jej wykonanie może zająć trochę czasu, szczególnie gdy mamy do czynienia z wieloma tabelami . Odbywa się tu bowiem przetwarzanie zapytania i transfer danych do zestawu rekordów. Znajdujące się na końcu funkcji wywołanie EndWaitCursor ( ) przywraca zwykły kursor.
tej funkcji
wyświetla
Pierwszą rzeczą, jaką robi ten kod, jest ustawienie składowej m_pOat abase obiektu COrderSet w takim sam sposób jak w przypadku obiektu CProductSet. Jeżeli tego nie zrobimy, platforma ponownie będzie otwierała połączenie z bazą danych, gdy zostanie otwarty zestaw rekordów dla zamówień. Ponieważ połączenie z baząjuż zostało otwarte dla zestawu rekordów dla produktów, byłoby to marnotrawstwem czasu . Następnie
zmiennej parametru m_Product IOparam na bieżącą wartość przem_ProductID zestawu rekordów dla produktów. Wartość ta zastępuje znak zapytania w filtrze, gdy otwierany jest zestaw rekordów z zamówieniami, więc wybierz, jakie chcesz rekordy, a następnie ustaw filtr dla zestawu rekordów na łańcuch , który widziałeś ustawiamy
wartość
chowywaną w składowej
wcześniej .
Dostęp do wielu widoków tablic Ponieważ zaimplementowaliśmy program z interfejsemjednodokumentowym, aplikacja ma jeden dokument i jeden widok. Dostępność tylko jednego widoku może wydawać się problemem, ale w praktyce nim nie jest. Możemy sprawić, że obiekt okna ramowego w aplikacji będzie tworzył egzemplarz klasy COrder Vi ew i przełączał na niego bieżący widok, gdy będzie miał zostać wyświetlony zestaw rekordów z zamówieniami. Będziemy
musieli śledzić, jakie okno jest bieżące, co można zrobić poprzez przypisanie unikalnego identyfikatora każdemu oknu z widokiem rekordów w aplikacji. W tej chwili mamy dwa widoki: widok produktów i widok zamówień. Aby zdefiniować dla nich identyfikatory, utwórz nowy plik o nazwie OurConstants .h i umieść w nim poniższy kod: II Definicj e s ta łych.
#pragma ance II Wybrane przez nas s ta łe
identyfikujące
widoki rekordów.
const uns 1 gned int PROOUCT_VIEW ~ 1; canst unsigned int ORDER VIEW ~ 2; Teraz możemy używać tych stałych do identyfikowania każdego widoku i zapisywać identyfikator bieżącego widoku w obiekcie okna ramowego. Aby zapisać identyfikator bieżącego widoku, dodaj publiczną daną składową typu unsigned i nt do klasy CMa i nFra me i nadaj jej
Aplikacja rozpoczyna działanie z widokiem produktów, więc zgodnie z tym inicjalizujemy nl_CurrentViewID. Teraz dodaj dyrektywę #include dla OurConstants.h na początku pliku MainFrm.cpp, aby definicja PROD UCT_VI EW była dostępna w pliku źródłowym.
Przełączanie widoków widoków, dodamy do klasy CMai nFrame publiczną funkcję skła o nazwie Sel ect View() z parametrem określającym identyfikator widoku . Ta funkcja będzie przełączała z bieżącego widoku na ten, którego identyfikator zostanie przesłany jako argument. Aby
umożliwić przełączanie
dową
Kliknij prawym przyciskiem myszy CMainFrame i wybierz Add/Add Function z menu kontekstowego, aby dodać do klasy nową publiczną składową. Jak typ zwracany podaj voi d, a jako nazwę funkcji SelectView. Jako nazwę parametru wpisz Vi ewID o typie unsi gned int. Zaimplementuj tę funkcję w następujący sposób:
void CMai nF rame : :Select View(unsigned int ViewID ) I
CVi ew* pOldAct i veV iew = GetAct i veViewO;
II Pobiera
bieżący
widok.
II Pobiera wskaźnik do nowego widoku, jeżeli ten istnieje, IIjeżeli nie, wskaźnik będzie pusty. CVi ew* pNewActiveView = static_cast(GetDlgltem(ViewID )); II Jeżeli jest to pierwsze użycie nowego widoku, II nie będzie on jeszcze istniał, więc musimy go utworzyć.
i f (pNewAct i veVi ew == NULL) {
switch (Vi ewID ) {
case ORD ER_VI EW; II Tworzy widok zamówień.
pNewAct i veView = new COrderView; break: default ; AfxMessageBox(L"Invalid View ID"); retu rn; II Przełączanie widoków . II Uzyskuje kontekst bieżącego widoku, aby
VisIlai C++ 2005. Od podstaw Set Act i veVi ew(pNewAct i veV i ew) : II Aktywuje nowy widok. pOl dAct i veVi ew->ShowWi ndow(SW_HIDE) : II Ukrywa stary widok. pNewAct i veView->ShowWi ndow(SW_SHOW ) : II Pokazuje nolry widok. pOl dAct l veView->Set DlgCtr l ID(m_Cur r entV i ewID); II Ustawia identyfikator starego widoku. pNewAct iveVi ew->SetOlg Ctrl I D(AFX_I DW_PANE_FI RST ): II Zapisuje identyfi kator nowego widoku . m_Cur re ntV i ewID = Vi ewID ; Recal cLayout ( );
Działanie
l
tej funkcji
można podzielić
Pobranie wskaźników do
na trzy
bieżącego
części:
i nowego widoku .
2. Utworzenie nowego widoku, jeżeli jeszcze nie istnieje. 3.
Zastąpienie bieżącego
widoku nowym.
Adres aktywnego widoku jest dostarczany przez składową GetActi veV iew() obiektu CMai nFr ame. Aby uzyskać wskaźnik do nowego widoku, wywołujemy składową GetDlgl t em( ) obiektu okna ramowego. Jeżeli widok z identyfikatorem podanym w argumencie funkcji już istnieje, zwraca ona adres widoku, a w przeciwnym razie zwraca NULL i wtedy musimy utworzyć nowy widok. Po utworzeniu obiektu widoku definiujemy obiekt CCr eateContex t - contex t. Obiekt CCre ate Context jest niezbędny jedynie wtedy, gdy tworzymy okno dla widoku, który ma być podłączony do dokumentu. Obiekt CCreateContext zawiera dane składowe, które mogą połą czyć
dokument, okno ramowe i widok , a w przypadku aplikacji MOI także szablon dokumentu. Gdy będziemy przełączać widoki , będziemy tworzyć nowe okno dla nowego widoku, który ma zostać wyświetlony. Za każdym razem, gdy będziemy tworzyć nowe okno, będzie my używać obiektu CCr eat eCont ext do ustanowienia połączenia między widokiem a obiektem dokumentu . Musimy przechować wskaźnik do obiektu dokumentu tylko w składowej m_pCurrentDoc w context . Ogólnie mówiąc, zanim będziesz tworzył widok, być może będziesz musiał przechować dodatkowe dane w obiekcie CCreat eContext; zależy to od okoliczności i rodzaju tworzonego okna . Przy
wywołaniu składowej
Create( ) obiektu widoku , która tworzy okno dla nowego widoku, obiekt conte xt jako argument. Ustanawia to właściwą relację z dokumentem i sprawdza wskaźnik do dokumentu. Argument hi s w wywołaniu Cr eat e() określa bieżące okno ramowe jako okno macierzyste, a argument View ID określa identyfikator okna. Ten identyftkator umożliwia uzyskanie adresu okna przez kolejne wywołanie składowej GetDl gl t em( ) okna macierzystego.
przesyłamy
Aby nowy widok był widokiem aktywnym, wywołujemy skład ow ą Set Act iveView () klasy CMai nFrame. Wówczas nowy widok zastępuje aktywny widok. Aby usunąć stare okno widoku, wywołujemy składową ShowW i ndow() widoku z argumentem SW_HIOE używającym wskaźnika do starego widoku . Aby wyświetlić nowe okno widoku, wywołujemy mentem SW_SHOWkorzystającym ze wskaźnika do nowego widoku.
tę samą funkcję
Set Act i veView (pNewAct i veView); II Aktywuje nowy widok. pOl dAct i veV i ew->ShowW i ndow(SW_HIDE); II Ukrywa stary widok. pNewAct i veVi ew->Sh owW i ndow(SW_SHOW) : II Pokazuje no wy widok. pOldActi veVi ew->SetD l gCt r l ID(mJ ur rentV i ewID): II Ustawia identyfikator starego widoku . pNewAct iveView->Set Dl gCt r l ID(AFX_IDW_PANE_FI RST); II Zapisuj e identyfikator nowego widoku. m_Cur r entVi ewID = Vi ewID ;
z argu-
Rozdział 19.
•
lączenie się ze źródłami
danych
1019
Przywracamy identyfikator starego aktywnego widoku do wartości identyfikatora, kt órą dla niego zd efiniowali śmy w dodanej wcz eś n i ej składowej m_CurrentVi ewID klasy CMai nFrame. Ustawiamy także identyfikator nowego widoku na AFX_IDW_PANEJ IRST, aby określić go jako pierwsze okno aplikacji. Jest to ni ezbędne , ponieważ aplikacja ma tylko jeden widok , więc pierwszy widok jest zarazem jedynym widokiem . W końcu zapisujemy identyfikator nowego okna w składowej mCurrentv i ewID, aby był dostępny przy kolejnym przełączen iu widoku. Wywołan ie Recal culateLayout () powoduje przery sowani e widoku , gdy wybrany zostanie nowy widok. Musimy dod ać dyrektywę #i nclude dla pliku OrderView.h na początku pliku MainFrm .cpp, aby udostępnić tu definicję klasy COrderView. Po zapisaniu pliku MainFrm.cpp możemy przejść do dodawania przycisków do okna dialogowego Pr oduct s w celu połączenia z oknem dialogowym Orders. Następnie dodamy procedury obsługi tego przycisku i jego odpowiednika w oknie dialogowym Or der s, aby wywoływały s kład ową Sel ectVi ew() klasy CMai nFr ame.
Umożliwianie przełączania widoków Aby zaimplementować mechanizm przełączania widoków, wróć do panelu Resource View i otwórz okno dialogowe IDD_DBSAMPLEJORM. Musimy dodać do okna dialogowego kontrolkę przycisku, jak na rysunku 19.27.
Rysunek 19.21 ProduetID
i 5~~I~_~_1
Cat.gory ID
Product Nam.
Unit:Prfee
Units In 50tck
Unlts On o-der
Zgodnie z nazewnictwem
pozostałych
I.Show Orders I
kontrolek w oknie dialogowym nadaj mu identyfikator
IDC ORDERS .
Po zapisaniu zasobu możemy utworzyć procedurę obsługi przycisku poprzez kliknięcie go prawym przyciskiem myszy i wybranie Add Event Handler z menu kontekstowego . Użyj okna dialogowego Event Handler Wizard do dodania funkcji OnOrders( ) do klasy CPr oduct Vi ew dla komunikatów typu BN_CLICKED; ta procedura będzie wywoływana, gdy przycisk zostanie kliknięty . Aby ukończyć procedurę obsługi , musisz dodać tylko jedną linijkę kodu:
GetPar entF rame ( ) obiektu widoku jest dziedziczona z CWnd, która jest niebezpośred nF r ame. Ta funkcja zwraca wskaźnik do macierzystego okna ramowego i wykorzystuje go do wywołania funkcji Sel ect View() , którą właśnie dodaliśmy do klasy CMai nFr ame. Wartość argumentu ORDER_V IEWpowoduje, że okno ramowe przełącza się na okno
nią klasą bazową CMa i
dialogowe Orders . Jeżeli ta sytuacja dzieje się po raz pierwszy, zostanie utworzony obiekt widoku i okna. Za drugim i kolejnymi przełączeniami do widoku zam ów i eń zostanie ponownie użyty istniejący widok za mó w ie ń. Na
początku
pliku ProductView.cpp musimy
umieścić następujące dyrektywy
#inc l ude:
#inelude "OurCon sta nts .h" #inelude "Mai nF rm .h" Kolejnym zadaniem jest utworzenie procedury obsługi dla przycisku, który wcześniej umieści liśmy w oknie dialogowym IDD_ORDERSJORM. W oknie edycji tego okna dialogowego w ten sam sposób dodaj procedurę obsługi OnP r oduct s() do klasy COrderVi ew. Następnie dodaj jedną linijkę kodu do jej implementacji :
void COrderVlew. :OnProduct s( ) sta t lc east (Get Parent Frame())->SeleetVi ew(PRODUCT VIEW ) ; Ta procedura
działa
w ten sam sposób co poprzednia. Ponownie musimy
dodać
dyrektywy
#i ncl ude dla plików OurConstants.h i MainFrm.h na początku pliku Orderl/iew.cpp, a następ
nie go
zapisać.
ObslUga aktywacji widokll do istniejącego widoku , musimy dopilnować, aby zestaw rekora okno dialogowe ponownie zainicjalizowane w celu wyświetlenia właściwych informacji . Gdy istniejący widok jest aktywowany lub dezaktywowany, platforma wywołuje składową OnAc t i vateVi ew( ), tak więc jest ona dobrym miejscem dla od świeża nia zestawu rekordów i okna dialogowego. Możemy przesłonić tę funkcję w każdej klasie widoku . Zrób to poprzez kliknięcie przycisku Overrides w oknie Properties klasy widoku i wybranie z listy OnAct ivateView(). Dopilnuj, aby dodać przesłonięcia do obu klas widoku . Gdy dów
następuje przełączenie został odświeżony ,
Dodaj
poniższy
kod, aby ukończyć
implement ację przesłoniętej
funkcji w klasie COrderVi ew:
void COrderView : :OnAeti vat eVi ew(BOOL bAct ivate. CVi ew* pAct i vateView . CView* pDeactiveView) (
lf(bAet i vat e) { II Pobiera
wskaźn ik
do dokumentu.
CDBSampleOoc* pOoe = GetOocument() ; II Pobiera
wskaź nik
do okna ramowego .
CMainFrame* pMF rame = st at ie_east(Get Parent Frame()); II Jeżeli ostatnim widokiem był widok produktów. musimy ponowni e przesłać zapy tanie II do zestawu rekordów z identyfi katorem pr oduktu z zestawu rekordów produkt ów.
Rozdział 19.• lączenie się ze źróllłami
danych
1021
if (pMFrame->m CurrentViewI O;;PROOUCT VIEW) { i f ( Im_pSet ->IsOpen()) II Upewnij s ię, że zestaw rekordów jest otwarty. return; II Jako parametr usta w bieżący identyfikator produktu.
m_pSet ->m_ProductIOparam; pOoc->m_OBSampleSet.m_ProductIO; m_pSet ->Requery() ; II Pobierz dane z bazy danych. /1 Jeżelijesteśmy poza EOF, nie ma
wię cej
rekordów.
i f Cm_pSet ->lsEOFC)) AfxMessageBox(L "No orders for t he current produet lO" ); II Ustawia tytul okna.
CStr ing strTi t le ~ _TC "Table Name : ") : CSt ring strTable ~ m_pSet->Get TableNameC) ; if C1st rTable .l sEmptyC) ) strTi t le +; strTable ; el se strTi tle += _TC "Orders - Mul ti ple Tables") ; pOoc->SetTitle Cst rTit le ) ; CRecordView : :On lnit ial updat et ) : II Uaktualnia wartośc i w oknie dialogowym . CRecordView : :OnAct i vat eViewC bAct ivat e. pAct lvat eV iew. pOeacti veV iew ) ; Ten kod jest wykonywany jedynie podczas aktywacji widoku, kiedy to argument bActi vate ma wartość TRUE. Po pobraniu wskaźników do dokumentu i macierzystego okna ramowego kontrolujemy, czy poprzednim widokiem był Produet View, zanim ponownie sprawdzimy zestaw zamówień. To sprawdzenie w tej chwili nie jest niezbędne, ponieważ poprzednim widokiem zawsze będzie Produet View, ale gdy dodamy do aplikacji kolejny widok, twierdzenie to nie zawsze będzie prawdziwe, więc umieść ten kod już teraz . Aby ponownie przesłać zapytanie do bazy danych , ustawiamy parametr m_Product lDparam w klasie COrderset na bieżącą wartość parametru m_ProductID w zestawie rekordów produktów. Powoduje to wybranie zamówień na bieżący produkt. Nie musimy tu ustawiać składowej m_strFi l t er zestawu rekordów, ponieważ została ona ustawiona w funkcji Onln it i alUpdat e ( l, przy pierwszym tworzeniu obiektu CReco rdVie w. Funkcja składowa IsEOF( ) obiektu COrderSet jest dziedziczona z CRecordSet i zwraca TRUE, jeżeli po ponownym zapytaniu zestaw rekordów jest pusty. Dodaj
poniższy
kod dla funkcji OnActi vateView() w klasie CP roduct Vie w:
vO ld CProduc tVlew ; ;OnAct i vateVlewCBOOL bActivate. CView* pActi vateVlew. CView* pOeact iveView) {
if CbActi vate) ( II Uaktualnia tytul okna.
CStri ng st rTi t le ; _TC"Table Name") ; CStr i ng strTable ; m_pSet ->GetTa bleNameC); st rTit le +; _TC": ") + strTable; GetOocument ()->SetTit le( strTit le);
1022 Visual C++ 2005. Od podstaw CRecardView : :OnAct i vat eVi ew( bAct ivat e. pAct ivateView. pDeactiveView l: W tym przypadku musimy jedynie uaktualnić tytuł okna, jeżeli widok został aktywowany. Ponieważ Produet View jest napędem całej aplikacji, zawsze chcemy przywrócić widok do stanu sprzed dezaktywacji. Jeżeli nie robimy nic poza zmianą tytułu okna, widok jest wyświetlany w poprzednim stanie.
Przeglądanie zamówień na produkt Możesz
teraz skompilować
moduł
wykonywalny nowej wersji programu. Gdy go uruchomisz, na wybrany produkt po kliknięciu przycisku Orders w oknie dialogowym produktów. Typowy widok zamówienia jest przedstawiony na rysunku 19.28. powinieneś mieć możliwość przeglądania zamówień
Rysunek 19.28
, ;~ Tabłe
Uame: Order s -
Hułtlple Tebtes - DBSdll1pte
_
eJ.? SelectView(ORDER VI EW): Podobn ą lin ijkę
dodamy
też
w kodzi e funkcj i OnProduct s ():
void CCust omerView: :OnProducts() sta tic cast (Get Parent Frame() )->Select View( PRODUCT VIEW): Teraz musimy
dodać
kod
określający
filtr dla zestawu rekordów z informacjami o kliencie, aby informacje o kliencie z polem identyfikatora klienta znajdującym s i ę zamówieniu w obiekcie COrderSet.
wyświetlane były jedynie
w
bieżącym
Dodawanie filtra Zdefiniujemy filtr w składowej Onlniti alUpdat e{) klasy CCust omerV iew. Poni eważ spodziewamy s i ę, że zostanie zwrócony tylko jeden rekord odpowiadający poszczególnym identyfikatorom klientów, nie musimy się przejmować sortowaniem. Kod tej funkcji wygląda nastę pująco :
void CCust omerVl ew : :OnInit ial Updat e( ) {
Begi nW ai t Cursor() ; CDBSampleDoc* pDoc = st at ic_cast (Get Document()): m_pSet = &pDoc->m_Cust omerSet ; II Inicj alizacj a wskaźnika do zestawu rekordów. II Ustawia b azę danych dla zestawu rekordów z klientami.
m_pSet ->m_pDa t aba se = pDoc ->m_DBSamp leSet .m_pData ba se; II Jako
wartość parametru filtra
ustawia bieżący identyfikator klienta.
m_pSet ->m_Customer IDparam = pDoc->m_OrderSet .m_OrdersCustomer ID: m_pSet ->m_st rFi lter ="CustomerIO = ?" : II Filtruje względem p ola Customerll). CRecordVl ew: :Onl nit i al Updat e( ); if (m_pSet->IsOpen()) {
CSt ri ng st rTi tl e = m_pSet ->m_pDat abase->Get DatabaseName(); CString strTable = m_pSet- >GetTableName( ): i f( lst rTable. IsEmpty() ) strTit le += _T(" :") + st rTabl e; GetDocument ( )->SetTitl e(st rTitl e): }
EndW ai t Cursor( );
Rozdzial19.•
~ączenie się ze źrólllami
danych 1021
Po uzyskaniu wskaźnika do dokumentu zapisujemy adres obiektu CCu sto lTIe rSet składowego dokumentu w składowej lTI_pSet widoku. Wiemy, że baza danych jest już otwarta, więc możemy ustawi ć wskaźn i k do bazy danych w zestawie rekordów z informacjami o kliencie na ten przechowywany w obiekcie CP roductSet. Parametr dla filtra będzie zdefiniowany w składowej ITI_Custo me rIDParam klasy CCustomer Set . Tę s kład ową dodamy za chwilę . Parametr ten jest ustawiany na składową mJ ust omer ID obiektu COrderSet, którego właści cielem jest dokument. Zdefiniujemy filtr w taki sposób , aby zestaw rekordów z informacjami o kliencie zawierał jedynie rekord z tym samym identyfikatorem klienta jak ten w bieżącym zamówieniu . Funkcja OnAct i vat eView( ) obsługuje aktywację widoku informacji o kliencie i zaimplementować w pliku CustomerView.cpp w następujący sposób:
możemy ją
void CCust omerV iew : :OnAct ivat eView(BOOL bAct i vat e. CView* pAct ivateVi ew. CV iew* pOeact iveView) if( bAetiva t e) (
lf(l m_pSet ->IsOpen()) retu rn: CDBSampleDoe* pOoe = st at ie_east (GetDoeument()) ; II Ustawia
b ieżący
identyfik ator klienta jako parametr.
m_pSet- >m_CustomerI Dpa ram ~ pDoc- >m_O rderSet .m_OrdersCustomer ID; m_pSet ->Requery(): II Pobiera dane z bazy danych. CRecordV iew: :Onlnit ialUpdat e(): II Przerysowuj e okno dialogowe. II Spraw dza. czy zestaw rekordów nie j est pusty.
i f (m_pSet ->I sEOF()) AfxMessageBox(L"No cust omer det ail s for the curre nt cust ome r ID" ); CSt ring st rTitl e = _T( "Table Name: "): CSt ring st rTable = m_pSet ->GetTab leName() ; if ( !st rTable.I sEmpty( )) strTit le += st rTable : else st rTit le += _T( "Mult i ple Tables"); pDoe->SetTitle (st rTitle) : }
CRecordV iew: :OnActivat eV iew (bAeti vate , pAct ivat eV lew, pOeaet iveVi ew); Jeżeli funkcja ta zostanie wywołana, ponieważ widok został aktywowany (a nie dezaktywowany), bActi vat e ma wartość TRU E. W tym przypadku ustawiamy parametr filtra z zestawu rekordów zamówi eń i ponownie przesyłamy zapytanie do bazy danych. Składowa mCustorer Ip obiektu zestawu rekordów CCust omerSet powiązane go z tym obiektem widoku jest ustawiana na identyfikator klienta z obiektu zestawu rekordów zamówi eń , który jest przechowywany w dokumencie. Będzie to identyfikator klienta dla bieżącego zamówienia. Wywołanie funkcji Requery ( ) dla obiektu CCustomer Set pobiera rekordy z bazy danych, korzys taj ąc z ustawionego filtra. W rezultacie informacje o kliencie dla bieżącego zamówienia zapisywane są w obiekcie CCusto merSet , a następnie przesyłane do obiektu CCust omer Vi ew celem ich wyświetlenia w oknie dialogowym .
Pierwsze dwie są potrzebne ze względu na klasy, które zostały użyte w definicji klasy dokumentu . DBSampleDoc.h potrzebujemy z powodu referencji do klasy COBSampl eDoc w funkcj i OnIni t i al Update ( ), a pozostałe dwa pliki .h zawi eraj ą definicje, do których odniesienia znajdują s i ę w procedurach obsługi przycisków w klasie CCustomerV iew.
Implementacia parametru filtra Dodaj do klasy CCust ome r Set publiczną zmienną typu CSt r i nq, odpowiadającemu typowi m_Cust omerID zestawu rekordów, i nadaj jej nazwę m_Cu st ome rIDpa ra m. Jeżeli korzy stałeś z menu kontekstowego panelu Class View, nowa zmienna będzie od razu inicjalizowana w konstruktorze. W przeciwnym razie dodaj inicjali zację zgodnie z poniższym kodem . Ustaw w następujący sposób licznik parametrów w konstruktorze klasy CCust ome rSet: składowej
pFX->SetFieldType(CF ieldExchange: :out put Col umn): RFX_Text (pFX. _T( "[Cust omer ID]"), m_Cust omer ID ): RFX_Text (pFX. _T ("[CompanyName J" i . m_CompanyName ): RFX_Text (pFX . _T("[ContactName]" ). m_Conta ctName ):
a licznik parametrów
instrukcj ę
do
s kład o
Rozdział 19.
•
lączenie się ze źródłami
danych 1029
RFX_Text (pFX , _Te " [ Cont actTit l el" ) : m_ContactT it l e) ; RFX_Text (pFX , _Te " [AddressJ") . m_Address) ; RFX_Text (pFX, _T( "[ City ]") . m_C ity) ; RFX_Text (pFX , _TC "[Regi onJ" ), m_Regi on) ; RFX_Text(pFX , _TC " [PostalCode] "), m_Posta lCode) ; RFX_Text (pFX, _TC " [ Count ry ] " ) , m_Country) ; RFX_Text (pFX, _TC "[ Phone] ") . m_Phone); RFX Text( FX . T( " [ FaxJ" ) , m Fax) ; pFX ->Set Fi el dType(CF i el dExchange; ; param) ; RFX Text( pFX , TC "Cust ome r lDParam" ) , m Cu stomerlDparam);
Pominąłem wiersze z komentarzami z początku tej funkcji w celu zaoszczędzenia miejsca . Po ustawieniu trybu par amprzez wywołanie składowej SetFiel dType( ) obiektu pFX wywołujemy funkcję RFX_Text () w celu przesłania wartości parametru do podmiany w filtrze . Używamy RFX_Text(), ponieważ zmienna parametru jest typu CStri ng. Istnieje wiele funkcji RFX_ () obsługujących ró żne typy parametrów.
~ączenie okna dialOgowego Order zoknem dialogowym Cuslomer Aby umożliwić przełączanie do okna dialogowego z informacjami o kliencie , potrzebujemy kontrolki przycisku w oknie dialogowym IDD_ORD ERSJ ORM, wię c otwórz je w Resource View i dodaj dodatkowy przycisk, jak na rysunku 19.30.
nieco układ kontrolek - możesz je ułożyć według własnego upodobania. Zdefiniuj identyfik ator nowej kontrolki przycisku jako IDC_CUSTOMER. Po zapisaniu okna dialogowego dodaj procedurę obsługi przycisku poprzez kliknięcie go prawym przyciskiem myszy i wybranie Add Event Handler z menu podręcznego. Procedura ta wymaga dodania tylko jednej linijki l kodu : void COr der Vi ew; ;OnCust omer ( ) st at i c cast (GetParent Frame() ) ->Select Vie w(CUSTOMER VI EW) ;
Pobiera ona adres okna ramowego i wykorzystuje go do wywołania składowej Sel ectVi ew( ) klasy CMa i nFr ame w celu przełączenia do' widoku informacj i o kliencie. Przedostatnim krokiem do ukończenia tego programu jest dodanie do funkcji Select View() kodu zajmującego się przesłaną do niej w artością CUSTOMER_V I EW. Wymaga to jedynie trzech poni ższych linijek kodu: voi d CMa inFrame : :Sel ect Vi ew(unsi gned int Vi ewIO)
{ CV ie w* pOld Act iveView = GetAct iveView();
// Pobiera bieżący widok.
1030 Visual C++ 2005. Od podstaw II Pobiera wskaźn ik do nowego widoku, jeże li ten istnieje II jeżeli nie, wskaźn ik będz ie p usty.
CView* pNewAct iveView
~
stat ic_cast CGetD lglt emCView ID)) ;
II Jeże li jestt o p ierwsze użycie nowego widoku, II nie będz ie on jeszcze istniał, więc musimy go utworzyć.
i f CpNewActi veView == NULL) (
switch(View ID ) (
case ORDER_VIEW: II Tworzy widok zamó wień.
pNewActive View = new COrderView ; break; case CUSTOMER_VI EW: II Tworzy widok klientów. pNewActi veVi ew = new CCust omerVi ew; break; default : AfxMessageBox CL"lnval id View ID" ); return; II Przełączanie widoków. II Uzyskuje kontekst bieżącego widoku, aby zas toso wać g o do nowego widoku.
Set Act i veViewCpNewAct iveView); II Aktywuj e nowy widok. pOl dAct i veView- >ShowW indowC SW _HIDE ) ; II Ukrywa stary widok. pNewAct i veVi ew->ShowWi ndow CSW_SHOW ) ; II Pokazuje nowy widok. pOl dAct l veView->SetDlgCt rl IDC m_Current Vi ewlD) ; II Ustawia identyfikator starego widoku. pNewAct i veVi ew ->SetDl gCtrl IDCAFX_IDW_PANE_FIRST ); m_Current ViewID = ViewID ; II Zapisz identyfi kator nowego widoku. RecalcLayout C) ; Jedyną konieczną zm i an ą
jest dodanie instrukcji case w bloku SWl tch w celu utwor zenia obiektu CCu st omerVi ew, jeżeli jeszcze nie istnieje. Na stępn ym razem ka żdy obiekt widoku będzie ponownie u żyty, więc s ą one tworzone tylko jeden raz. Kod przełączaj ący m iędzy widokami działa z dowolną liczbą widoków, więc jeżeli chcesz, aby ta funkcja obsługiwała ich więcej , musisz po prostu dla każdego nowego widoku dodać kolejną instrukcję case do bloku swi tch . Mimo że obiekty widoku tworzymy tutaj dynamicznie, nie musimy się martwić ich usuwaniem. A to dlatego, że są one powiązane z obiektem dokumentu i są usuwane przez platformę przy zamykaniu aplikacji. Ze
w zględu
dyrektywę
na odniesienia do klasy CCust omer Vi ew w funkcji Sel ect View( ) musimy #i ncl ude dla CustomerView.h na po czątku pliku MainFrm .cpp.
Aby ukończyć aplikację, musimy dodać do pliku CustomerView.cpp OoOat aExc hange () dla klasy CCusto mer Vi ew:
CRecordVi ew : :DoData Exchange(pDX) ; DDX_Fi eldText (pDX. IDC_ADDRESS. m_pSet->m_Address . m_pSet ) ; DDX_FieldText(pDX, IDC_C ITY . m_pSet->m_C ity . m_pSet ); DDX_FieldText (pDX. IDC_COMPAN YNAME. m_pSet- >m_CompanyName. m_pSet ) ; DDX_FieldText(pDX . IDC_PHONE. m_pSet ->m_Phone. m_pSet ); DDX_FieldText (pDX. IDC_CUSTOMERI D. m_pSet- >m_CustomerI D. m_pSet) ; Tak jak poprzednio wykorzystuje ona funkcję OOX_ do transferu danych z kontrolek edycji do składowych klasy CCustomerVi ew. Musimy dodać dyrektywę #i ncl ude dla pliku nagłówkowe go CustomerSet.h, aby program poprawnie się skompilował.
Teslowanie przeglądarki bazy danych W tej chwili program jest ukończony. Możesz skompilować aplikację i ją uruchomić. Tak jak poprzednio głównym widokiem bazy danych jest widok produktów, a kliknięcie przycisku Orders zabiera nas do widoku zamówień. Drugi przycisk w tym okn ie powinien być teraz aktywny, a kliknięcie go spowoduje wyświetlenie szczegółowych informacji o kliencie, który złożył bieżące zamówienie, co obrazuje rysunek 19.31.
Rysunek 19.31
l:"Tebte l ł dme : ( ClJ\l om enl
D8Sample
_
U X
Fle Edit Record Vlew He\>
I
Customer ID Comp.nyNeme
~ ' - - - --
- -
QU -I-CK-'S-top-"-
.- .-
'I
____ __ _ _ _ =:J -
-
- - -.- ••----- - --.
--'-
-
- - - - ,, .....J
Adaess aty
I
Cunewelde
1037Z-Q3S1BB
l?ho';! Order. I
Dwa przyciski
powodują powrót
odpowiednio do widoku
1S''low Produ'! . I
zamówień
i widoku produktów.
1032 Visual C++ 2005. Od pOdstaw
Podsumowanie Powin iene ś mie ć korzystając
teraz z ODBC.
podstawową wiedzę
Najistotniejsze informa cje z tego
na temat tego, jak MFC
łączy się
z bazą danych ,
rozdziału :
•
MFC zapewnia obsług ę OLE DB i ODBC w celu uzyskiwania dostępu do baz danych.
•
Aby
•
Połączenie
•
Obiekt zestawu rekordów reprezentuje polecenie SQL SELECT zasto sowane dla określonego zestawu tabel. Jeżeli jest to niezbędne, przy tworzeniu obiektu zestawu rekordów platforma automatycznie tworzy obiekt bazy danych reprezentujący połączenie z bazą danych .
•
Klauzula WHERE może
korzystać
z bazy danych z użyc i em ODBC, musi
być
ona zarejestrowana.
do bazy danych jest reprezentowane przez obiekt CDa tab ase lub CDaoDa ta base.
zo stać
dodana do obiektu zestawu rekordów poprzez jego
składową m _st rFi lte r.
•
Klauzula ORDER BY może
zostać określona
dla zestawu rekordów w jego
składowej
mstrSort . •
Obiekt widoku zestawu rekordów rekordów.
służy
do wyświetlania zawartości obiektu zestawu
Ćwiczenia Kod źródłowy oraz rozwiązania do poniższych ćwiczeń znajdziesz pod adresem http://helion.pl /ksiazki/vcppo.htm
l
Korzystając ponownie z tabeli Products, dodaj do aplikacji okno dialogowe ,,kontroli zapasów". Powinno ono być osiągalne za pomocą przycisku w oknie dialogowym produktu, a samo powinno zawierać przycisk umożliwiający powrót do okna dialogowego produktów . Ma ono zaw ierać pola z identyfikatorem produktu, nazwą produktu, poziomem, dla którego należy wysłać zamówienie (Reorder leve/), i ilością produktów w magazynie. Nie przejmuj się na razie filtrowaniem ani sortowaniem, tylko utwórz taki mechanizm. powyższy projekt tak, aby okno "kontroli zapasów" przed kliknięciem przycisku automatycznie wyświetlało informacje o produkcie w oknie dialogowym produktów.
2. Dopracuj
a.
Zaimplementuj system, w którym użytkownik będzie ostrzegany w oknie dialogowym "kontroli zapasów" o tym, że aktualny stan zapas ów zbliża s ię do poziomu, dla którego należy wysłać zamówienie. Musiałeś już zauważyć , że niektóre poziomy ponownego zamówienia mają wartość zero - w takich przypadkach ostrzeżen ie nie powinno być wyświetlane.
20 Aktualizacja źródeł danych w tym rozdziale na podstawie wiadomości , które zdobyłeś w poprzednim rozdziale o uzyskiwaniu do stępu do baz danych poprzez ODBC, będziemy uaktualn iać baz ę danych Northwind Traders, korzy stając z tego samego mechanizmu. Z tego
rozdzi ału
dowiesz
się :
•
O transakcjach bazodanowych.
•
Jak uaktu alnić bazę danych, korzystając z obiektu zestawu rekordów.
•
W jaki sposób podczas aktualizacj i dane są transferowane z ze stawu rekordów do bazy danych.
•
Jak uaktualnić istniejący wiersz tabeli.
•
Jak dodać nowy wiersz do tabeli .
Operacje aktualizacji Gdy piszemy kod służący jedynie do przeglądania bazy danych , jedynym problemem j est uzy skanie dostępu do bazy danych (autoryzacja). J eżeli baza danych ma poprawn ą ochronę dostępu , dane w bazie danych są bezpieczne. Sprawa przedstawia się zupełnie inaczej , gdy tworzymy program aktualizujący bazę danych. Ponieważ zmieniamy zawartość bazy danych, tego typu modyfikacje mogą zniszczyć integralność bazy dan ych i uczynić zawartoś ć tabeli bezsensowną, a nawet sprawić, że nie będzie można korzystać z tabeli. Zawsze należy dokładnie przetestować kod, korzystając z jakiej ś testowej bazy danych, zanim zastosuje się go w poważn ych rozwi ązan iach . Uaktualnianie bazy danych przeważnie wiąże się z modyfikacją jednego lub większej liczby pól w wierszu istniejącej tabeli , na przykład modyfikacją ilości zamówionego towaru, czy z dodaniem nowego wiers za, na przykład w przypadku bazy Northwind nowego zam ówienia. Utworzy my przykłady dla obu tych sytuacji, ale najpierw zajmijmy się ich implikacjami.
1034 Visual C++ 2005. 011 podstaw Większość trudności , jak ie mogą powstać w związku z uaktualni aniem bazy danych, staje się widoczna w kontekście wieloużytkownikowych baz danych . Bez właściwej kontroli procesu aktualizacji jednoczesny dostęp kilku użytkowników może spowodować dwa rodzaje problemów. Pierwszy powstaje, gdy jedna osoba może pobrać rekord, podczas gdy jest on aktualizowany. Osoba pobierająca dane może otrzymać stare dane sprzed aktualizacji albo nawet mieszankę starych i nowych. Drugi problem powstaje podczas jednoczesnego uaktualniania - gdy jedn a osoba rozpoczyna uaktualnianie rekordu, podczas gdy inna osoba już ten rekord uaktualnia. Gdy dzieje się to na rekordzie z jednej tabeli, może dojść do utraty całej aktualizacji . Gdy operacje są przeprowadzane na rekordach z kilku tabel, dane w bazie danych mogą być niespójne. Zanim omówię, jak uniknąć tych problemów, zajmiemy się pod stawowymi operacjami uaktualniania zestawu rekordów .
Operacje aktualizacji CRecord8et W poprzednim rozdziale widziałeś, jak wywołania funkcji RFX_O w składowej DoFi el dE xchange() obiektu zestawu rekordów pobierają dane z wybranych pól tabeli lub tabel bazy danych i transferująje do danych składowych obiektu zestawu rekordów . Te same funkcje są używane do uaktualniania pól w tabeli bazy danych i do dodawania zupełnie nowych wierszy . W klasie CRecordSet Ed it( )
dostępnych jest pięć
funkcji
obsługujących
operacje aktualizacji :
Wywołanie
tej funkcj i rozpoczyna uaktualnian ie istniejącego rekordu. Jeżeli tabela nie zaktualizowana, funkcja zgłasza wyjątek CDBExcept i on, a w warunkach braku zgła sza wyjątek CMemory Exce pt l on.
może zo stać
pamięci
AddNew ()
Wywołanie
tej funkcji rozpoc zyna dodawanie zupełnie nowego rekordu. Zgłasza ona i on, jeżeli nowy rekord nie może zostać dołączony do tabeli.
wyjątek COBExce pt
Update( )
Wywołanie
Zgłasza
lub De l ete ( )
tej funkcji dokańcza uaktualni an ie i stniejącego lub dodawanie nowego rekordu . ona wyjątek CD BE xce pt i on, j e ż e l i pojedynczy rekord nie został uaktualn iony
wystąpił błąd .
rekord poprzez utw orzenie i wykonanie polecenia SQL DEL ETE. Zgła s za CDBExcept i on, jeżeli wystąpi błąd - na przykład jeżeli baza danych jest tylko do odczytu. Po operacj i De l et e ( ) wszystkie dane składowe zestawu rekordów będą miały wartość NULL, czyli tak jakby nie miały ustawionej wartości. Przed wykonaniem jakiejkolwiek nowej operacj i na obiekcie zestawu rekordów należy przejść do nowego rekordu.
U suwa
bieżący
wyjątek
CancelUpdate()
Powoduje zaniechanie lub dodających nowy.
oc z ekujących
operacji
modyfikujących istniejący rekord
Żadn a z tych funkcji nie przyjmuje parametrów. Pierwsze cztery funkcje zgłaszają wyjątki, więc
ich wywołanie należy umieszczać w bloku t ry i dodać blok catch , j eże l i nie chcemy , aby nasz program gwałtownie kończył pracę w przypadku wystąpienia błędu.
Aby usunąć bieżący rekord z obiektu zestawu rekordów, należy po prostu wywołać skła d ow ą Del ete r i. Następnie trzeba przewinąć zestaw rekordów do nowej pozycji, zanim użyje się której kolwiek z powyższych funkcji, poniewa ż po wywołaniu De l ete ( ) wartości składowych obiektu zestawu rekordów będą niepoprawne.
Rozdział20 .•
Rysunek 20.1 obrazuje pod stawow ą dodawaniu nowego rekordu .
kolej n ość zd arzeń
Uaktualnianie istniejącego rekordu 1.
Edit O dla ob iektu zestawu rekordów: - Zapi suj e do bufora b ie żące wartości z danych składowych pól zestawu rekordów warto ści
danym
składowym
przy uaktualnianiu
i stniej ącego
lub
Uaktualnianie nowego rekordu
Wywołaj
2. Przypi sz nowe
Aktualizacja źródeł danych 1035
l.
pól
3. Wywołaj UpdateO dla obiektu zestawu rekordów: - Wyszukuje zmodyfi kowane pola poprzez por ównan ie z zapisanymi wa rtościami - Tworzy i uruchamia polecenie SQL UPDATE, aby uaktualn i ć bazę danych o zmieni one pol a - Kasuj e bufor zawierający stare. zapisane warto ści pól
Wywołaj
AddNew O dla obiektu zestawu rekordów: - Zapisuje do bufora bi e żące warto ści z danych s k ł a d owy c h pól zestawu rekord ów - Przypisuje danym składowym pól zestawu rekord ów warto ść PSEUDO_NULL
2. Przypisz nowe 3.
wartości
danym
s kła d owy m
pól
Wywołaj
UpdateOdl a obiektu zestawu rekordów - Wyszukuje niepuste pola - Tworzy i uruch amia polecenie SQL INSERT. aby u aktualn ić b azę danych o niepu ste pola - Przywraca z bufora stare, zapisane wa rto ści
Rysunek 20.1 Po wywołaniu AddNew ( ) dla zestawu rekordów w celu dodania nowego rekordu do tabeli funkcja zapisuje do bufora bieżąc e warto ści wszystkich danych skład owych obiektu zestawu rekordów, które odp owiad ają polom, a następni e przypisuje im wartość PSEUDO_NULL. Nie jest to zero ani nu11,j ak w przypadku wskaźnika. Jest to warto ść wskazująca, że składowa nie został a ustawiona. Po wyw ołaniu Updat e( ) w celu ukończenia dodaw ania rekordu przywracane są pierwotne (sprzed wywołani a AddNew ( )) wartości danych skł adowyc h zestawu rekordów . J eżeli chcemy, aby zestaw rekordów zawierał wartoś ci nowego rekordu, musimy wywołać składową Requery ( ) obiektu zestawu rekordów . Ta funkcja zwra ca TRUE (w a rt ość typu BODL w MFC), j e żeli operacja s i ę pow iedzie . Funkcję Requery( ) wywołujemy takż e, jeżeli chcem y uzyskać inny widok danych , w którym będziemy pobierać reko rdy z u życiem innego polec enia SQL lub innego filtra rekordów. Transfer danych między danymi składowymi zestawu rekordów a bazą danych zawsze odbywa się z ud ziałem funkcji DoFi el dExchangeO s kł adowej obiektu zestawu rekordów, więc funkcje RFX_( ) zapewniają dwojakiego rodz aju m ożliw ości - zapisywanie do bazy danych lub odczytywanie z niej.
Sprawdzanie, CZy operacje są dozwolone Zawsze warto s p rawdz i ć, czy operacje, które zamierzamy przeprowadzić , są dozwolone dla obiektu zestawu rekordów. Bard zo prosto można skończy ć z zestawem rekordów tylko do odczytu - wystarc zy na przykład jedynie zapomnie ć o usunięciu atrybutu tylko do odczytu z pliku North wind.mdb . Jeżeli spróbujemy uaktualnić tabele tylko do odczytu , zostanie zgło szony wyjątek , czego można uniknąć, jeżeli po prostu upewnimy się, że opera cjaj est możl iwa do przeprowadzenia . Użyc ie wyj ątków do wyłapywania błędów , które nie s ą niespodziewane, jest niewydajn e i raczej unikane . Je żeli, jak w tym przypadku, jest to m ożliwe, należy najpierw s p rawd z ić, czy operacja jest możliwa do wykonania. Dzięki temu kod ob sługujący wyj ątki będzi e się zaj m ow ał naprawdę wyjątkowymi sytuacjami.
1036 Visual C++ 2005. Od podstaw Składowa CanUpdat e () klasy CRecordSet zwraca TRUE, jeżeli m o żna zmod yfik ow ać rekordy tabeli, które są reprezentowane w obiekcie zestawu rekord ów. Gdy chcemy d odać nowy rekord, możemy w celu sprawdzenia wywołać CanAppend( ). Zwraca ona TRUE, jeżeli dodawanie nowych rekordów do tabeli jest dozwolone .
Zabezpieczanie rekordu Zabezpieczanie rekordu uniemożliwia innym użytkownikom uzyskanie do stępu do zabezpieczonego rekordu podczas aktualizacji wiersza tabeli. Zakres zabezpieczenia rekordu podczas aktualizacji jest określony przez tryb zabezpieczenia ustawiony w obiekcie zestawu rekordów. W obiekcie zestawu rekordów określone są dwa tryby zabezpieczeń - optymi styczny (opt i mist ic) i pesymi styczny (pess i mi sti c). Ckecord set : :opt i mist i c
W optymistycznym trybie zabezpiecze ń rekord jest zabezpieczany jedynie podczas wykonywania funkc ji IJpdat e( ). Minimalizuj e to cz as, w którym rekord jest niedostępny dla innych użytkowników bazy danych. Jeżeli operacja edycj i może zająć du żo czasu, pesymistyc zne zabezpieczanie cz ę sto nie j est praktycznym rozwi ązaniem , pon ieważ inni użytkowni c y m ogą potrzebow a ć d o stępu do bazy danych. Standardowym ro związan iemj est wtedy wykorzystanie trybu optymistycznego i wprowadzeni e j ak i ego ś rodzaju mech anizm u ro związywani a konfliktów.
CRecor d5et : :pess i mi st i c
W pesymi stycznym trybie zabezpieczania rekord jest zabezpieczany już przy wywołaniu funkcj i Ed it( ) i pozostaje zabezpieczony, a tym samym niedo stępny dla innych u żytkowników a ż do ukończeni a wykonywania funkcji IJpdat e( ) lub do czasu anulowania operacji aktualizacji. Może to mi eć szczególnie silny wpływ na w yd ajno ś ć, gdy aktualizacj e są przy gotowywane interaktywnie; jedn ak ten tryb w wielu sytuacj ach jest ni e zbędny dla utrzymania integralnośc i danych .
Domy ślnym
trybem dla zestawu rekordów jest opt i mi st i c, w ięc tryb nal eży ustawiać, jedynie chcemy korzystać z trybu pesymistycznego. Aby u stawić ten tryb, należy wywołać skła dową Set Lock i ngM odeO obiektu zestawu rekordów z argumentem CRecordset : : pess i mi st i c. Oczywi ści e można również przywrócić go do poprzedniego stanu, wywołując tę fu n kcj ę z argumentem CRecord set: .opt tmt st tc ,
jeżeli
Transakcie W kontekście baz danych transakcja umożliwi a bezpieczne cofuięcie operacji, jeżeli zajdzie taka potrzeba. Transakcja umieszcza dobrze zdefiniowaną se r i ę modyfikacji bazy danych w jednej operacji , więc w przypadku wystąpi enia błędu do momentu zakończenia transakcji wszystkie operacje mogą zostać odwrócone (ang. roll back). Mówiąc jaśniej , jeżeli uaktualnienie nie zostanie ukończone (np. z powodów sprzętowych) , może to mieć katastrofalne skutki dla integralności bazy danych . Transakcja nie jest tylko uaktualnieniem jednej tabeli. Może ona zawierać złożone operacje na bazie danych , włącznie z serią modyfika cji wielu tabel, a do jej ukończenia może być potrzebne sporo czasu. W takich sytu acjach ob sługa transakcji jest po prostu n iezbędna dla zapewnienia integralności bazy danych.
Rozdział 20.
• Aktualizacia źródeł danych 1037
Przy operacjach opartych na transakcjach system bazy danych zarządza przetwarzaniem transakcji i zapi saniem wszelkich informacji niezbędn ych do odtworzenia, aby wszystko, co jest wykonywane przez transakcję na danych, mogło zostać co fni ę te , je że li po drodze wystąpią jakieś problem y. Poprzez oparc ie operacji na bazach danych na transakcjach możemy zabezpieczyć bazę danych przed błędami , które mogłyby wystąp i ć podczas przetwarzania. Przeważ nie przetwarzanie transakcji w razie potrzeby blokuje (zabezpiecza) rekord i zapewnia, że inni użytkownicy bazy danych natychmiast będą wid zieli dane zmi enion e przez transakcj ę . Transakcje są ob sług iwane przez większość dużych kom ercyjnych systemów baz danych dla komputerów typu mainframe, ale nie zawsze dotyczą systemów baz danych przeznaczonych dla komputerów klasy Pc. Mimo to klasa CDa ta base w MfC obsługuje transakcje, a tak się też skład a, że robi to również M icrosoft ODBC dla baz danych Access, więc jeżeli chcesz, może sz wypróbować przetw arzanie transakcji dla bazy danych Sample Data.
Transakcie W CDalabase Transakcje są zarządzan e przez składowe obiektu klasy CDa taba se, który zapewnia połączenia do bazy danych . Aby określić , czy tran sak cje s ą obsług iwan e prz ez konkretne połączenie, wywołujemy składową CanTransact() obiektu CDataba se. Zwraca ona TRUE, jeżeli transakcje są ob sługiwane. Tak się też składa, że istnieje również składowa CanUpdate ( ) klasy CDa tabase, która zwraca FALSE, jeżeli źródło danych jest tylko do odczytu. W klasie CData base
dostępne są
trzy funkcje
biorące udział
w przetwarzaniu transakcji:
z bazą danych. Wszystkie kolejne operacje na zestawie rekordów transa kcji do czasu wywo łan ia Comn : tT rans() lub Roll Back(). Funk cja ta zwraca TRUE,jeż eli rozpoczęcie transakcji s i ę powiedzie.
Begi nTrans ()
Rozpoczyna
tran sakcję
s taj ą się części ą
Comm tTrans ( )
Wyk onuj e tran sa kcję , wi ęc wszystki e operacje na zestawie rekordów będące częścią transakcji s ą prze sył ane . Funk cja ta zwraca FAL SE ,j e żeli wy s tąp i błąd, w którym to przypadku stan źr ódła danych jest niezdefiniowany.
Rol l bac x( )
C ofa wszystkie operacje zes tawu rekord ów wykonane od wywołania Begi nTran s ( ) i przywraca źródło danych do stanu sprzed wywołania Begi nTra ns() .
Kolejno ść zdarzeń
w transakcji jest bardzo prosta:
•
Wywołanie
BeginTrans () w celu
rozpoczęcia
•
Wywołanie
Edit ( ), Updat e ( ), Add New() dla zestawu rekordów.
•
Wywołanie
CommitT rans ( ) w celu
ukończenia
transakcji .
transakcji .
operacje Ed it () lub Add New() na zestawie rekordów są wykonywane po Updat e ( ). Wewnątrz transakcji nie są one wykonywane do czasu wywołani a Commi tTra ns ( ) dla obiektu CDa tabase. Jeżeli zachodzi potrzeba przerwania transakcji w dowolnym momencie po wywołaniu Begi nTran s O , nal eży po prostu wywołać Ro11bad ( ).
Poz a
transakcją,
wywołaniu
Działanie Commi tT ra ns() i Roll back( ) może powodować problemy - na przykład pozycja, na której działamy w zestawie rekordów, może zostać utracona, więc należy podjąć pewne kroki w programie, aby przywrócić wskaźnik do rekordu po ukończeniu lub przerwaniu transakcj i.
1038 VislJal C++ 2005. Od podstaw CData base. Po wywołaniu Commi tT r an s( ) należy wywoDat abase, a po wywołaniu Ro11 ba ck należy wywołać GetCurs or Ro11Ba ckBehavi ort ). Obydwie te funkcje zwracająjednąz trzech wartości typu i nt, wskazujących, co powinniśmy zrobić : Pomocne w tym
będą
dwie
składowe
łać składową GetCursorCommit Behavior ( )
Połączenie
rekordu z b a zą danych jest nie naru szone pr z ez lub a nu lowania tra nsakcji , więc nic nie rób.
SQL_C B_CLOS E
Należy wywołać
po zycję
oper ację
wykonania
Requery ( ) dla obiektu zestawu rekordów , aby przywrócić w zestawie rekordów.
bie żącą
Należy zamknąć zestaw
a
n astępnie
rekordów przez wywolanie składow ej C1os e ( ) obiektu, w razie potrzeby ponownie otworzyć zestaw rekordów.
W praktyce występująjeszcze dalsze komplikacje przy korzystaniu z transakcji, ponieważ konkretne sterowniki mogą mieć wpływ na to, kiedy należy otworzyć zestaw rekordów. Przy niektórych sterownikach trzeba otworzyć zestaw rekordów przed wywołaniem BeginTr ans( ), a przy innych, jak w przypadku sterownika Microsoft Access ODBC, Rol l back( ) nie zadziała, j eżeli zestaw rekordów nie zostanie otwarty po wywołaniu BeginTrans() . Należy dokładnie poznać konkretny sterownik przed przystąpieniem do implementowania transakcji w aplikacji .
Prosty przykład uaktualnienia Czas nabrać nieco doświadczenia z operacjami uaktualniania, rozpoczynając od bardzo prostego przykładu. Nie będzie on zawierał większości zagadnień omawianych na początku tego rozdziału, ale utworzymy go, abyś poznał zastosowanie choćby części z tych operacji. Dzięki kreatorowi aplikacji możemy minimalnym nakładem pracy utworzyć aplikację uaktualniającą tabelę bazy danych. Utworzymy program umożliwiający uaktualnienie określonych pól w tabeli Order Det ai l s. Utwórz projekt o nazwie DBSi mpl eUpdate, używając szablonu MFC App1i cat i on. W kategorii
Database wybierz Database view without file support bez obsługi plików i ODBC jako opcję Client Type, tak jak w poprzednim rozdziale. Wciąż będziemy korzysta ć z bazy danych Northwind przez ODBC, jednak tym razem wybierz typ dynamiczny rekordu. W środowisku wieleu żytkownikowym dynamiczny zestaw jest automatycznie uaktualniany, jeżeli ktoś wprowadzi zmiany do rekordu, podczas gdy korzysta z niego nasz program. Dzięki temu dane dostępne dla naszego programu są zawsze aktualne. Dla operacji modyfikującychistniejący rekord lub dodających nowy powinieneś wybrać dynamiczny typ zestawu rekordów. Ponieważ
zamierzamy uaktualniać bazę danych, musimy mapować zestaw rekordów na pojebazy danych. Klasy baz danych w MFC nie obsługują uaktualniania zestawów rekordów złożonych z połączenia dwóch lub większej liczby tabel. Jako domyślną tabelę zestawu rekordu wybierz Or der Det ai l s, jak na rysunku 20 .2. dynczą tabelę
Jeżeli
wybierzesz tu kilka tabel, uaktualnianie zestawu rekordów nie będzie mo żliwe , poniezostanie on automatycznie oznaczony jako tylko do odczytu . Klasy baz danych obsługują j edynie dostęp w trybie tylko do odczytu do połączeń wielu tabel , a nie ich uaktualnianie. waż
Rozdział
Rysunek 20.2
20.• Aktualizacja źródeł danych 1039 :&.'
Selec t pat anas e o bje ct
E> 'l ebles
~ i
!.
!
OK
i m CalegoIle'
ii:.···.Im
Cancel
Eustorrers Employee,
; 11m O~De uli i
, i llll i liil
Dtoers
'· ll!l Illl ii L..
Shoces
I ;
'I
1-
Proouct s
S u ppłers
.1i1 views
Zmień
nazwy klas widoku i zestawu rekordów oraz p ow i ązanych z nimi plików na zgodne z nazwą tabeli , którą się zajmujemy, jak na rysunku 20.3.
Rysunek 20.3
?
Hre Appllc.lion Wiz.rd ..DBSimpleUpd.le -
-
~
-
Generated Class es
lieneroted desses:
ov erve w Applica ~on
Type
Compound Document Support
Document Templete Strin9' Dat;lbase Support UserInterf.ece Features Advanced Features GeneratedClasses
. h fi l~:
[c o rdero eteilss et 'l!.,;e
IOrde rOeta il, s e t. h ,C Plłf ie :
oS>;
I... j o rde rOeta ilsS et :cpp
C Recor dset
< PreviOus
Pozostaje jeszcze naszych potrzeb.
kliknąć
przycisk Finish i
rozpo cząć
I
r'lr
~~
Finish
II
Cancel
dostosowyw anie okna dialogowego do
1040 Visnal C++ 2005. Od podstaw
Dostosowywanie aplikacji Tabela Or der Detail s zawiera pięć kolumn - Order ID, Pr oduet ID, Unit Pr i ce, Qua nt i t y i Discount. Jeżeli w panelu Class View wy świetlisz składowe klasy COrder Deta i l sSet , ujrzysz dane składowe odpowiadające tym kolumnom. Dla każdej z nich potrzebujemy w oknie dialo gowym statycznej kontrolki i kontrolki edycji. Ułożyłem je jak na rysunku 20.4, ale możesz to zrobić na swój własny sposób.
Rysunek 20.4 Order ID :
r~ ~ c _ _-'
ProduetID:
~~pi~~1
UrOt Prlce:
!5ampI-; od j
Quanlly :
ISamplo odl
DlsCOLr1l:
I
Sampleed
I
Przypisz identyfikatory kontrolkom edycji, aby zgadzały s ię z nazwami pól, jak to zrobiłe ś w poprzednim rozdziale - na przykład ostatnim z nich jest IOC_DISCOUNT. Domy ślny styl ustawiany dla kontrolki edycji pozwala na wprow adzanie danych z klawiatury, ale zakładając, że chcemy ograniczyć liczbę zmienianych pól zestawu rekordów, ustawimy trzy pierwsze kontrolki jako tylko do odczytu, korzystając z zakładki stylów w oknie Properties. Wartość wyświetlana w kontrolce tylko do odczytu może być ustawiana przez program, ale nie można do tej kontrolki wpisać nic z klawiatury . Możesz za jednym razem ustawić właściwość tylko do odczytu dla wszystkich trzech kontrolek, je żeli klikniesz każdą z nich, trzymając wciśnięty klawisz Ctrl , a następnie klikniesz prawym przyciskiem myszy i wybierzesz Properties . Cokolwiek teraz ustawisz w oknie Properties będzie miało zastosowanie dla wszystkich trzech kontrolek. Przy ułożeniu elementów okna dialogowego przedstawionym na rysunku 20.4 będzie można wpisywać dane tylko dla Quant i ty i Discount . Pozostaje jedynie powiązać kontrolki edycji z odpowiadającymi im danymi s kł a d owy m i zestawu rekordów, czyli jak widziałeś w poprzednim rozdziale, musimy dla każdej danej pola w zestawie rekordów dodać wywołania funkcji DDX_( ) do funkcji DoDataExchange( ) w klasie widoku zestawu rekordów COr derDetai l sV i ew. Kod będ z i e wyglądał następująco :
void COrderDetai ls View: .DoDataExchange(CDataExchange* pDX) {
~ Uaktualnianie bazy danych Jeżeli tylko ustawiłe ś poprawnie kontrolki i pami ętałe ś o umieszczeniu w komentarzu dyrek tywy #er ror, znajdującej się przed definicj ą funkcji GetOefaultConnect () w klasie COrder Oet ai l sSet, program powinien skomp i l ować się bezproblemowo. Dyrektywa #erro r j est umi eszcz ana , abyś zw róc i ł uwa gę na kwestie b ezp i eczeństwa pod cza s łąc zenia się z bazą danych. Gdy program jest uruchomiony, można przemie szcza ć się między wier szam i tabeli, korzystając z przy cisków paska narzędzi. Jeśl i wpiszesz dane do kontrolki edycj i dla pola Quant ity lub Oiscount , zostanie ono uaktualn ione, gdy przejdziesz do kolejnego lub poprzed niego rekordu w zestawie. Okno aplikacji zostało przedstawione na rysunku 20.5 .
Rrsunek 20.5
~ Untitled - DDSllnp leUpdałe F~e
Edit
Record
iii.",
order ID:
l l~Z~ .J
i · ····- - - l
~
UnitPrice:
~;;-.~
osccont:
U ~
, - -_.---.
Produet ID:
Quantlty;
.1
_
HeIp
i
999
1
E:::J
I
R. ady
Jak widać, zmieniłem warto ści Quanti t y i Oi scount dla produktu z identyfikatorem 11 w zamó wieniu o identyfikatorze 10248 na jak ie ś mało prawdopodobne wartośc i .
Jak lo llziała Gdy klikniesz jeden z przycisków paska narzędzi , by prz ejść do innego rekordu , wywoływana jest procedura obsługi OnMove( ) dostarczona przez domyślną kla sę b azową CRecordVi ew. Ta funkcja wypisuje wszystkie zmiany dokonane w zestawie rekordów, zanim przejdzie do inne go rekordu w zestawie poprzez wywołanie składowej Move( ) klasy CRecordset, która to skła dowa je st dziedziczona w klasie CO rderOeta i l sSet . Pamiętaj , że wymiana dany ch zachodzi tu na dwóch poziomach . Funkcje RFX_O wywoływane w składowej DoDa t aExchangeO klasy COrderOetai l sSet przesyłaj ą dane między wierszem w zestawie rekordów z bazy danych a da nymi składowymi klasy. Funkcje OOX_( ) wywoływane w składowej DoDat aExchange( ) klasy CO rderOeta i l Vi ew przesyłają dane między kontrolkami edycji a danymi składowymi COrder Oet ai l Set . Gdy zmieniamy wartość w kontrolce edycji, nowe dane s ą przekazywane do danych skł adowych obiektu zestawu rekordów . Gdy prze chodzimy do kolejnego rekordu poprzez kliknięcie przycisku z paska narzędzi , nowe dane zostają zapisane w bazie danych przez funk cję
OoFi el dExchange( ).
1042 VisIlai C++ 2005. Od podstaw Ten przykład nie jest zły, ale zapisywanie danych do bazy bez jakiegoś konkretnego działania ze strony użytkownika j est nieco kłopotliwe . Powinniśmy mieć trochę więcej kontroli nad tym , co się dzi eje . Napiszemy przykład, w którym użytkownik będzie mu siał coś zrobić przed wykonaniem uaktualnienia.
Zarządzanie procesem aktualizacji
Zdecydowanie chcemy, aby użytkownik podjął jaki eś działanie w celu umożliwienia aktuali zacji , a nie żeby było to domyślnym zachowaniem programu . Możemy zacząć od ustaw ienia wszystkich kontrol ek jako tylko do odczytu, wię c domyślnie nie będzie możliwe wpisywanie danych z klawiatury w żadn ej kontrolce. Następnie możemy dodać do okna dialogowego przy cisk Edit Order (edytuj zamówienie), który będzie uaktywn iał odpowiednie kontrolki edycji w celu umożliwienia wpisania danych z klawiatury. Obrazuje to rysunek 20 .6
Rysunek 20.& Order ID:
[sample odl
ProdJ et ID:
ISa~e .d I
UnitPrice:
~. od l
Qu. ntity:
!~ od l
Dlscount :
[:.mplo::§j
I Edi:Order I
Zaimplementujemy dwa tryby działania programu: tryb tylko do odczytu, gdy uaktu alni anie nie będzi e możliwe, ponieważ kontrolki będą mi ały atrybut tylko do odczytu, oraz tryb edycji, w którym wpisywanie danych z klawiatury będzie możliwe w wybranych kontrolkach, więc możliwe będzie uaktualnienie zestawu rekordów. Zał o że n i e jest takie, że gdy użytkownik kliknie przycisk Edit Order, kontrolki edycji pól, których zmianę chcemy umożliwić, pozwolą na wprowadzenie danych z klawiatury, a program przejdzie w tryb edycji. W oknie dialo gowym aplikacji DBS impl eUpdate dodaj przycisk. Ustaw jego identyfikator na IDC_EDITORDER. Następnie do klasy COrderDet ailsView dodaj procedurę obsługi przycisku poprzez kliknięcie go prawym przyciskiem myszy i wybranie Add Event Hand/er z menu kontek stowego. Skróć nazwę procedury do OnEdi to r der ( ). Idealnie
byłoby, gdybyśmy
w tryb ie aktualizacji zabronili korzystania z przycisków paska i elementów menu Record, służących do przechodzenia między rekordami, ponieważ chcemy, aby użytkownik musiał kliknąć przycisk w celu zakończenia operacji aktualizacj i, nie zmieniając bieżącej pozycji w zestawie rekordów.
narzędzi
kliknię ciu
przycisku Edit Order tryb tylko do odczytu dla kontrolek Quanti ty i Discount przycisk, po kl iknięciu którego odbędzie się aktualizacja. Aby pomieścić to wszystko, okno dialogowe aplikacji po kliknięciu przycisku Edit Order powinno wyglądać jak na rysunku 20.7. Po
Kontrolki edycji dla pól Quant i ty i Oi scount pozwalają teraz na wpisywanie danych, przycisk Edit Order ma zaś nowy podpis (Update), dostępny jest również nowy przycisk - Cancel, który umożliwia użytkownikowi przerwanie operacji. Ponadto przyciski paska narzędzi słu żące do przemieszczania bieżącego rekordu powinny zostać dezaktywowane po kliknięc iu przycisku Edit Order. To samo dotyczy elementów w menu Record. Program działa teraz w trybie edycji. Dodaj przycisk Cancel do okna dialogowego, ale ponieważ nie chcemy, aby był on początkowo wyświetlany, ustaw jego właściwość Visible na False. Będziemy także potrzebowali procedury obsługi dla przycisku Cancel, więc dodaj ją teraz z nazwą OnCa nce l ( ) do klasy COrder Oeta 11sVi ew, w ten sam sposób jak w przypadku przycisku Edit Order. Później uzupełnimy kod od tej funkcji .
W procesie uaktualn iania użytkown ik wpisuje dane do aktywnych kontrolek edycji w oknie dialogowym i klika przycisk Update w celu zakończenia aktualizacji. Okno dialogowe powra ca wtedy do pierwotnego trybu - tylko do odczytu, a wszystkie kontrolki edycji mają atrybut tylko do odczytu. Jeżeli użytkownik nie będzie chciał kontynuować uaktualniania będzie musiał kliknąć przycisk Cancel zamiast Update. Aby
otrzymać
taki mechanizm i efektywnie zarządzać procesem aktualizacji, program kilka operacji po kliknięciu przycisku Edit Order:
będzie
musiał przeprowadzić
•
Zmienić
tekst przycisku Edit Order na Update, ponieważ teraz operacji uaktualniania.
będzie
to przycisk
umożliwiający zakończenie
•
Wyświetlić
•
Zapi sać
w oknie dialogowym przycisk Cancel.
w klasie, że nastąpiło przejście do trybu edycji. Jest to niezbędne, ponieważ wykorzystywali ten sam przycisk w dwóch celach , zmieniając etykietę z Edit Order na Update i z powrotem. będziemy
•
Umożliw ić
które
wpisywanie danych z klawiatury do kontrolek edycji zawierających pola, zmieniane.
mają być
Teraz napiszemy kod, który
będzie się
tym wszystkim
zajmował.
1044 Visual C++ 2005. Od podstaw
Implementacja trybu uaktualniania Zacznijmy od rejestrowania, czy aplikacja jest w trybie uaktualnia. Możemy to zrobić, dodając deklaracj ę enumdo klasy COrderDet ai l sVi ew wraz ze zm ienną typu enum, która będzie odzwier ciedlała b ieżący tryb. Dodaj poniższe dwie linijki kodu do publicznej części klasy:
enum Mode {REAO_ONL't . LI PDATE} : II Tryby dzialania ap likacj i,
Mode m_Mode , II Zap isuj e bieżący tryb,
Aplikacja początkow o będzie w trybie READ_ONLY, zmienną m_Mode w konstruktorze:
w ięc będziem y
odpowiednio
inicjalizow ać
COrderOetai l sView: :COrderOetai l sV iew()
: CRecordView(COrderOeta il sView : :100 )
,m Mode (REAOONLY)
m_pSet = NULL ; II DO ZROBIENIA : wpisz tutaj kod konstrukcji.
W procedurze obsługi On Ed i torde r () , którą dodaliśmy do klasy widoku, możemy umie ści ć tekstu klawisza oraz trybu programu. Początkowa wersja tej funkcji musi imple mentować tę możliwo ś ć w następujący sposób:
przełączanie
voi d COrderOeta i lsView: :OnEdit order() {
i f (m_Mode == UPDATE )
{
II Jeżeli przycisk zastal klikn ięty, jesteśmy w trybie uaktualniania.
II Wylącz wprowadzanie danych do kontrolek edycji.
II Zmień tekst przy cisku Update na Edit Order.
II Ukryj przycisk Cancel.
II Aktywuj elementy menu Record oraz przyciski pa ska narzędzi.
II Dokończ uaktualnianie,
m_Mode = RE AD_ONLY: II Przejdź do trybu tylko do odczy tu.
}
else
{
II Jeże li przycisk zas tal klikn ięty, jesteśmy w trybi e tylko do odczytu .
II Zezwól na wprowadzanie dany ch do kontrolek edycji,
II Zmień tekst przycisku Edit Order na Update.
II Wyś wietl p rzycisk Cancel.
II Wylącz elementy menu Record i przyciski paska narzędzi.
II Rozpocznij uaktualnianie.
m_Mode = UP OATE: II Przejdź w tryb uaktualniania.
}
Kod
przełączający już s ię
tu znajduje. W tej chwili funkcja ta jedynie przełącza zmi enną zarejestrować bieżący tryb. Resztę funkcjonalności , opisano w komentarzach. Teraz po kolei zajmiemy się każdą linijką
m_Mode m iędzy READ_ONL Ya UPDATE, aby którą musimy komentarzy.
dostarczyć,
Rozdzial20. • Aktualizacia źródel danych 1045 Włączanie i wyłączanie kontrolek
edycii
Aby zmienić właściwości kontrolki , musimy wywołać pewnego rodzaju funkcję powiązaną z kontrolką. Oznacza to, że musimy mieć dostęp do obiektu reprezentującego kontrolkę. Można w bardzo łatwy sposób dodać zm ien n ą kontrolki do klasy widoku ; po prostu kliknij prawym przyciskiem myszy kontrolkę w widoku zasobów i wybierz Add Variable z menu podręcznego . Rysunek 20.8 przedstawia okno dialogowe dla kontrolk i edycj i zawierającej w artoś ć
Di scount .
Rysunek 20.8
?
Add Hember v e r tee r ę Wi zard . OOSlIl1pleUpdate
x,
Welcome to the Add Member Variable Wizard
Act...:
IpubrlC
ltariab1e type:
Controi [D:
\ CEdit
Variablell'lme:
Controlt~ :
I§l:T-
-'
r 'I:1 Ie\~i' :
Comment (/I notatlon
I
not reQUired):
Icortrol vClrlable fa discount ecIit contral
FInish
II
Canceł
Nazwałem zmienną m_Di scountCtr l ,
a ponieważ jest ona związan a z kontrolką edycji, jej typ to CEd it. Kliknij przycisk Finish, aby dodać tę zm ienną do widoku . Powtórz tę procedurę dla kontrolki edycji wy świetl aj ącej Quant i t y, nadając zmiennej nazwę m_Quant ityCt r l .
Po dodaniu dwóch zmiennych kontrolk i do klasy widoku , mamy dostęp do kontrolek, a tym samym możliwość zmiany ich stylu, więc zmień funkcję On Edi t or der ( ) w następujący sposób:
void COrderDeta i l sView: :OnEdit order( )
{
if(m Mode == UPDATE)
{
II Jeżeli przycisk zostal
klikn ięty, jesteśmy w
trybie uaktualniania.
cI
II Zmień tekst przycisku Upda te na Edit Order. II Ukryj przycisk Cancel. II Aktywuj elementy menu Record oraz przyciski paska n arzędzi. II Dokończ uaktualniani e. m_Mode ~ READ_ONLy ; II Przejdź do trybu tylko do odczytu.
104& ViSIlaI C++ 2005. Od podstaw el se { II Jeżeli przycisk zastal kliknięty, jesteśmy w trybie tylko do odczytu. d .
II Zmień tekst przycisku Edit Order na Update. II Wyświetl przycisk Cancel. II Wylq cz elementy menu Record i p rzyciski paska n arzędzi.
II Rozpocznij uaktualnianie. m_Mode = UPDATE; II Przejdź w tryb uaktualniania.
}
Składowa
Set ReadOnly( l klasy CEdit przyjmuje argument typu BOOL, którego domyślną wartoTRUE. A więc wywołanie tej funkcji bez argumentów skutkuje podaniem domy ślnej wartości argumentu i nadaniem kontrolce właściwości tylko do odczytu. Przesłanie do funkcji wartości FALSE usuwa właściwoś ć tylko do odczytu. Tak się składa, że możemy skrócić kod funkcji poprzez usunięcie wywołań funkcji SetReadOnly ( l w instrukcjach i f -el se i dodanie dwóch instrukcji po instrukcji i f : ściąjest
m_Ouanti tyCtrl .Set ReadOnly(m_Mode == UPDATE); mDi scountCtrl .SetReadOnly(m Mode == UPDATE ); Wyrażenie w argumencie mJ10de == UPOATE ma wartość T RU E , j eże l i m_Mode ma wartość UPOATE . W przeciwnym razie ma wartość FALSE, więc te dwa wywołania funkcji Set ReadOnly( l wykonują pracę poprzednich czterech. Wadąjestjedynie fakt, że kod stał się mniej czytelny.
Warto przypomn ieć, że wiele funkcji MFC przyjmuje argumenty typu BOOL, które mogą mieć TRUE albo FALSE, ponieważ zostały one napisane, zanim dostępny był typ bool w C++. Zawsze można używać wartości typu boal jako argumentów parametrów typu BOOL, jednak ja wolę używać wartości argumentów TRUE i FAL SE dla typu BOOL, a wartości t rue i f al se dla zmiennych typu boal . wartości
Zmienianie tekstu przycisku Możemy otrzymać obiekt odpowiadający przyciskowi Edit Order, dodając składową kontrolki m_EditDrderCtrl do klasy widoku dokładnie w ten sam sposób jak w przypadku kontrolek edycji. Zmienna będzie typu CButton, która jest klasą MFC definiującą przycisk. Wykorzystamy tę zmienną do ustawiania tekstu przycisku w składowej OnEditorder ( l poprzez wywołanie składowej SetWi ndowText ( l, dziedziczonej przez klasę CButton z klasy CW nd:
void COrderDetai ls View: ;OnEdit order () {
if (m Mode -- UPDATE) { II Jeżeli przycisk zastal kliknięty, jes teśmy w trybie uaktualni ania. II Wylącz wprowadzanie danych do kontrolek edycji.
m EditOrderCt rl .Set WindowText ( T("Edit Order")) : II Ukryj przycisk Cance/. II Aktywuj elementy menu Record oraz przyciski paska narzędzi.
II Dokończ uaktualnianie. m_Mode ~ READ_ONLY: II Przejdź do trybu tylko do odczytu .
)
else { II Jeżeli przycisk został kliknięty, jesteśmy w trybie tylko do odczy tu. II Zezw ól na wprowadzanie danych do kontrolek edycji .
m_QuantitYCtrl .SetReadOnly(FALSE): m_DiscountCt rl .SetReadOn ly( FALSE): II Zmie ń tekst r cis/a, Edit Order na Undate.
mEditOrderCtr l .SetWindowText(
T(
"Updat e")):
II Wyświetl przycisk Cance/. II Wylącz elementy menu Record i przyciski paska narzędzi.
II Rozpocznij uaktualnianie. mj10de ~ UPDATE : II Przejdź w tryb uaktualniania.
Każde wywołanie funkcji Set W i ndowText ( ) ustawia tekst wyświetlany w przyc isku na łańcuch podany jako argument funkcji . Parametr jest typu LPCTSTR, dla którego możemy użyć argumentu CSt r ing lub łańcucha stałych typu _T.
Sterowanie widocznością przycisku Cancel Aby
uczynić
przycisk Cancel widocznym lub niewidocznym, potrzebujemy zmiennej kontrolki temu przyciskowi, dodaj zatem zmienną kontrolki o nazwie ITI_Cancel Edi tCt r l w ten sam sposób jak w przypadku przycisku Edit Order . Ponieważ klasa CBut to n jest wyprowadzana z CWnd, możemy wywołać dziedziczoną składową ShowW i ndow() obiektu CButton, aby ustawić widoczność lub niewidoczność przycisku: odpowiadającej
void COrderDet ail sVi ew: :OnEdit order () {
if( m_Mode
~=
UPDATE)
{ II Jeż eli przycisk zos ta ł kliknięty, jesteśmy w trybie uaktualniania. II Wylącz wprowadzanie danych do kantrolek edycji.
m_QuantitYCtrl .Set ReadOnly( ): m_Dis countCt rl .Set ReadOnly(): II Zmień tekst przy cislai Update na Edit Order.
m_Ed itOrderCtr1.SetW i ndowText '-T( " Ed it Oroeri ) : II
cisk Cance/.
mCancel Ed itCtr l .ShowWi ndow (SW HIDE): II Aktywuj elementy menu Record oraz p rzyciski paska narzędzi.
II Dokończ uaktualnianie, m_Mode ~ REA D_ON LY : II Prz ejdź do trybu tylko do odczytu.
}
else { II Jeżeli przycisk został kliknięty, jes teśmy w trybie tylko do odczytu.
1048 Visual C++ 2005. Od podstaw II Zezwól na wprowadzanie dany ch do kontrolek edycji.
m_OuantityCt rl. Set ReadOnly( FALSE) : m_Oi scountCtrl .Set ReadOnly(FALSE): II Zm ień tekst przy cisku Edit Order na Update.
m_EditOrderCtrl .SetWindowText CT("Update")) :
II Wyłą cz elementy menu Record i przyciski paska n arzędzi.
II Rozpocznij uaktualnianie. m_Mode = UPOATE: II Przejdź w tryb uaktualniania .
Funkcja SholWJi ndow( ), dziedziczona przez klasę CButton po CWnd, wymaga argumentu typu i nt, który musi być z ustalonego zakresu (w dokumentacji znajdziesz cały zestaw) . Używamy wartości argumentu SW _HI DE, aby przycisk zniknął, gdy m_Mode ma wartość UPDATE . Z kolei wartość SW _SHOWjest stosowana, gdy aplikacja przechodzi w tryb edycji, aby wyświetlić i aktywować przycisk. Wyłączanie menu Record
Chcemy wyłączyć elementy menu w menu Record, gdy składowa m_Mode widoku ma wartość UPDATE. Jednak nie będziemy tego robić w procedurze obsługi OnEditor der () , ponieważ jest prostszy i lepszy sposób , który za chwilę poznasz. Usuń zatem dwie linijki komentarza w instrukcji if w procedurze OnEd i to rder ( ), Możemy zarządzać stanem elementów menu i przycisków paska narzędzi poprzez dodanie dla nich procedur aktualizacji do klasy widoku. Wyświetl zasób menu aplikacji w widoku zasobów. Jest to zasób menu w pliku DBSimpleUpdate.rc z identyfikatorem IOR_MAI NFRAME. Dodaj procedurę obsługi dla komunikatu UPOATEJOMMAND_U I dla każdego elementu menu Record, rozpoczynając od elementu First Record. Rozwiń menu Record, klikając je, a następnie kliknij prawym przyciskiem myszy element menu First Record w widoku zasobów i wybierz Add Event Handler z menu podręcznego . Rysunek 20.9 przedstawia okno dialogowe Event Handler Wizard.
Jak widać, wybrałem typ komunikatu UPDATEJOMMAND_UI, a jako klasę, w której ma zostać dodana procedura, wybrałem COrderDetai l sVi ew. Opis na dole okna dialogowego określa główne zastosowanie procedury obsługi UPDATE_COMMAND_U I - dokładnie to, czego potrzebujemy w tym przypadku. Kliknij przycisk Add and Edit, aby dodać procedurę obsługi, i powtórz te czynności dla trzech pozostałych elementów menu Re cord. Do procedury obsługi UPOATE_COMMAND_UI przesyłany jest argument typu CCmdUI, a klasa CCmdUI zawiera funkcję składową En abl e( ), za pomocą której można włączyć lub wyłączyć dany element menu . Wartości argumentu TRUE włącza element, natomiast wartość FALSE go wyłącza. Domyślną wartościąjest TRUE, więc w celu włączenia elementu można wywołać tę funkcję bez argumentów. Chcemy wyłączać elementy menu i przyciski paska narzędzi, gdy m_Mode ma
Rozdział 20.•
Aktualizacja źródeł danych 1049
RJslmek 20.9 Welcome to the Event Handler Wizard
Mes..getl!Pe:
C,",s list :
haGet Dat abaseName(); CSt ri ng strTa ble = m_pSet ->GetTab leName(). H( I strTable. IsEmpty ()) strTit le +~ _Te ": " ) + st rTabl e: GetDocument()->SetTl tleCstrTi t le); }
EndWaitCursor() : Definicja klasy CProductVi ew j est mniej więcej taka sama jak klasy CCust omer View, główna różnica leży w powiązanym z nią oknie dialogowym: II Product t/iew.h: plik nagłówko wy.
#pragma once class CProduct Set: cl ass CProduct Vi ew : publi c CRecordView {
publ ic: CProductView (): virt ua l -CProductView( ); publ t e: enum { IDD ~ IDD]RODUCTJORM }: CProductSet* m_pSet :
II Dane formularza.
II Ope racje .
pub l ic: CProductSet* Get Recordset( ): II Impl ementacj a.
Teraz możemy dodać funkcje przesłaniające OnGet RecordSetO, OoOat aExchangeO i On lniti alUpdateO, korzystając z tego samego sposobu co przy poprzednich klasach, oraz dodać poniż sze dyrektywy na początku pliku ProductView.cpp:
#include #include #include #incl ude
"st dafx.h" "DBSimpleUpdate.h" "ProductV iew .h" "Product Set .h"
Rozdzial20.• Aktualizacja źródel danych 1059 Konstrukt or powini en i n i cj al i zow ać s kładową m~p S e t , a destruktor powinien usuwać obiekt wskazywany przez obiekt m_pSet , p onieważ tworzymy go na stosie. Defini cje kon struktora i destruktora wyglądaj ą następująco :
Powrócimy do funkcji, które mają b yć zaimplementowane w klasie Cpr oduct Vi ew, nieco póź niej w tym rozdziale, a w międzyczasie zapełnimy okna dialogowe potrzebnymi kontrolk ami.
1060 VisualC++ 2005. Od podstaw
Dodawanie kontrolek do zasobów dialogu Mimo że powiązaliśmy okno dialogowe IDO_CUSTOMERJ ORM tylko z tabelą Custo me rs, musimy w nim zamieści ć wszystkie informacje potrzebne do utworzenia nowego rekordu w tabeli Orders. Źródła danych dla poszczególnych pól nowego rekordu Order s zostały przedstawione na rysunku 20.14. Wygenerowane przez max(OrderID)+1
1 I
OrderlD
I
CustomerlD
I
EmployeelD
Dane z okna dialogowego
data
I
Ord erDate
Ustawione jako
I RequiredDate I ShippedDate
Przyjmijmy 3
I
ShipVia
Tabela Customers
I
I
Bieżąca
Przyjmijmy 9
Freight
I
ShipName
I
ShipAddress
I
ShipCity
I
I
I
ShipRegion
I Ship Post alCode I
ShipCountry
I
Przyjmijmy 9.95
RYSlmek 20.14 Połowa pól jest pobierana z wybranego przez użytkownika rekordu tabeli Customer s. Ponieważ tworzymy nowe zamówienie, będziemy musieli utworzyć nowy, unikalny identyfikator zmówienia. W tym celu możemy wyszukać największy w danej chwili identyfikator w tabeli Orders i dodać do niego l.
Aby wybrać klienta, użytkownik przegląda zestaw rekordów, aż zostanie wyświetlony żądany klient. Wtedy możemy pobrać z zestawu rekordów dane potrzebne do utworzenia nowego rekordu w tabeli Or ders. Jako datę zamówienia możemy wyświetlić w oknie dialogowym bieżącą datę, a do wybrania daty dostawy dostarczymy odpowiednią kontrolkę. Aby nadmiernie nie komplikować przykładu , pozostałym polom przypiszemy jakąś dowolnie wybraną wartość. Oczywiście
w oknie dialogowym nie musimy wyświetlać wszystkich informacji z tabeli wystarczy tylko nazwa. Jednak wciąż będziemy potrzebowali danych w zestawie rekordów. Kontrolki w oknie dialogowym możesz rozmieścić w sposób przedstawiony na rysunku 20.15.
Cust omers -
Z rysunku wynika, jak będą wykorzystywane kontrolki, a takż e widoczne są ich identyfikatory. Kontrolka wyboru daty i czasu pozwala na wybranie lub wpisanie czasu lub daty. Czy zostanie wybrana data, czy też czas, zależy od właściwoś ci Format. Tutaj kontrolki mają ustawiony format Short Date. Data jest wybierana przez kliknięcie strzałki skierowanej w dół i wybranie
Rozdział20.
IDC- COMPANYNAMEz tabelą Customers
Rysunek 20.15
Łączy
Unikalny identyfikator utworzony przez max(OrderID)+1 IDC_NEWORDERID
• Aktualizacja źródeł danych
IDC CANCELORDER Anuluje tworzenie nowego zamówienia IDC_REQUIREDDATE Aktualna data IDCORDERDATE
~ S ełec t Prom_Prod uct Name, m_p5et): DDX_Text (pDX. IDC_NEW ORDERID. m_OrderID): DDX_Text (pDX. IDC_CU5TOMERNAME, m_Cust omerName J;
1064 Visual C++ 2005. Od podstaw DDX_Text (pDX. IDC_ORDERQUANTITY. m_Qua nt i ty) : DDX_Text (pDX. IDC_ORDERDI SCOUNT . m_Di scount) ; Mając
zdefmiowane okna dialogowe,
możemy zaimplementować
mechanizm ich
przełączania .
ImplementaCja przełączania okien dialogowych Podstawową logikę prz eł ączania okien dialogowych w tym rozdziale widzi ał eś na rysunku 20.12 . Mechanizmem przełączającym z jednego okna dialogowego do innego jest kliknięcie przycisku, więc procedury obsługi przycisków będą zawierały kod powodujący przełączeni e . Najpierw zdefiniujemy identyfikatory poszczególnych okien dialogowych, w ięc dodaj do projektu plik nagłówkowy ViewConstants.h. Tym razem wykorzystamy deklarację enumdo identyfikacji widoków, wię c plik pow inien zawierać następujący kod :
II Definicja s ta łych
identyfikujących
widoki rekordów.
#pragma once enum Vi ewID{ ORDER DETAILS . NEWORDER. SELECT_P RODUCT}; W klasie CMain Frame będziemy potrzeb owali zmiennej typu ViewID do zapisania identyfikatora bieżącego okna , więc dod aj m_Cu rrentViewID poprzez kliknięcie prawym przyciskiem myszy CMai nFrame w panelu Class View i wy branie Add Variable z menu podręcznego. Musimy j ą za i n icj a l i zo wać, więc zmień w pon i żs zy spos ób konstruktor klasy CM a i nFra me :
CM ai nFrame : ;CMai nF rame()
mCurrentViewID (ORDER_DETAILS)
II DO ZROBIEN IA: dodaj kod inicjalizacj i s kłado wej.
Powyższy kod określa widok, z którym aplikacja rozpoczyna działanie. Dodaj dyrektywę #i ncl ude dla pliku ViewConstants.h do pliku .cpp , aby defini cje identyfikatorów widoku były tu dostępne.
Dodamy teraz do klasy CMai nFrame funkcję skład ow ą S e l e c t V i ew O , która będzie przełączała okna dialogowe. Zwracanym typem jest void, ajedyny parametr jest typu Vi ewID, ponieważ argumentem będzie jeden z identyfikatorów widoku zde finiowanych w enum. Implementacja Se l ect Vi ew() ma następującą postać : II
Umożliwia przełączan ie
widoków. Argument określa no"" widok.
void CMainFrame : :SelectV iew(ViewIDviewID ) (
CView* pOldActiv eView
~
GetAct iveVi ew() :
// Pobiera
b ie ż acy
widok:
II Pobiera wskaźnik do nowego widoku. jeżeli ten istni ej e, IIjeżeli nie. wskaźnik będzie pusty.
CView* pNewAct iveVi ew = st at ic_cast( GetDlg ltem( viewID » ; II Jeże li jest to p ierwsze użycie nowego widoku, nowy widok II nie istnieje. wię c musimy go utworzyć .
II Widok Order De tailsjest zawsze tworzony jako pierwszy, więc nie musimy II w tym celu nic rob ić.
Rozdział20.
• aktualizacja źródeł danych 1065
if (pNewAct i veView == NULL ) (
switch(vi ewID ) {
case NEW_ORD ER : II Utwórz widok umożliwiają cy dodanie nowego zamówienia. pNewAct iveView = new CCust omerView; break; case SELECT_PRODUCT: II Utwórz widok umożli wiający dodanie produktu do zamówienia. pNewAct iveVi ew = new CProduct Vi ew; break: default : AfxMessageBox(_T ("Inval id View ID"» : return: II Przelączan ie widoków. II Uzyskaj kontekst b ieżącego widoku, aby
zastoso wać
go do now ego widoku.
CCreateContext context ; cont ext .m_pCurrentDoc = pOldAct i veView->GetDocument () : pNewAct i veView->Create(NULL . NULL. OL. CFrameW nd : :rect Default. t his . viewI D. Scontext l: pNewAct i veView- >Onlnit ial Update(): }
SetAct i veV iew( pNewAct i veview ): II Aktywuj nolry widok. pO ldAct i veView->ShowWi ndow(SWHIDE) : II Ukryj stary widok. pNewAct iveV iew- >ShowWi ndow(S( SHOW): II Wyświetl nolry widok. pOl dAct iveV iew ->Set DlgCt rl ID (mJ urrentV lewID): II Ustaw identyfika tor starego pNewAct iveView- >Set DlgCtrlID(AFX_IDW _PANE_FIRST) : m_CurrentV iew ID = view ID: II Zapisz identyfi kator nowego widoku. RecalcLayout () : Ten kod
widoku.
odwołuje s i ę
zbędne są dyrektywy
do klas CCust omer Vi ew i CProduct View, więc w pliku źródłowym nie#i ncl ude dla plików Customer View.h i Produ ctVi ew.h.
Przełączani e
z okna dialogowego ze szczegółami zamówieni a do okna dialogowego rozpoczy tworzenie nowego zamówienia wykonywane jest w procedurze obsługi OnNewor der( ) w klasie COrder Det ai l sVi ew: nającego
void COrderDet ai l sView: :OnNewo rder( ) st at ic cast (Get Parent Frame(» ->Se lect View(NEWORDER ): Pobierany je st wskaźn ik do macierzystego okna ramowego widoku - obiektu CMai nF r ame aplikacji - a następnie jest on wykorzystywany w wywoł aniu Sel ect Vi ew() w celu wybrania okna dialogowego dla tworzenia nowego zamówienia. Dyrektywa #i nl ude dla pliku ViewConstants.h jest niezbędna także w tym pliku, pon ieważ odnosimy się tu do NEW_ORDER. Ponadto musimy dodać dyrektyw ę #i ncl ude dla pliku MainFrm.h, aby uzyskać definicję CMai nFrame. Procedura ob sługi przycisku Select Products w klasie CCusto mer Vi ew przełącza okno dialogowe na CPr oducts Vi ew:
void CCustomerVlew : :OnSelect product s( ) {
1066 Visual C++ 2005. Od podstaw stat i c cast (Get ParentFrame( ))->Sel ect View(SELECT PRODUCT ) : Procedura o bs łu g i przycisku Cancel w tej samej klasie po prostu poprzedniego okna:
przełącza
z powrotem do
void CCust omerView: :OnCancelorder () {
stat ic_cast (Get Parent Frame())->Se1ectV iewWRDER_DETAILS): }
Nie zapomnij dod ać do pliku CustomerView. cpp dyrektyw #incl ude dla plików ViewConstants.h i MainFrm.h. Ostatnią operacją przełączania do zaimplementowania jest procedura OnDone( ) w klasie CPr oduct Vi ew:
void CProduct View: :OnDone() I
stat ic_cast (Get ParentFrame ( ))->Select View(ORDER DETAILS): Przełącza
ona z powrotem do pierwotnego okna apl ikacji , umo żliw iającego przeglądanie i edycję s zczegółów zamówienia. Oczywi śc i e m oglibyśmy na przykład przełączać z powrotem do okna dialogowego CCust omer Vi ew, aby potwi erdzi ć dokonan ie zapisania zamówienia. Ponownie nie zapomnij o dyrektywach #i ncl ude dla plików ViewConstants.h i MainFrm.h. Przełączan i e między początkowym
oknem dialogowym umożliwiającym przeglądanie szczedo okna dialogowego umożliwiającego edycję zamówienia musi teraz sterować też w idoczno ścią przycisku New Order; w przeciwnym przypadku przycisk Cancel będzie schowany za przyciskiem New Order w oknie dialogowym edycji. Dodaj do klasy COrderDeta11sVi ew zmienną kontrolki - m_NewOrder Ct rl , odpowiadającą kontrolce IDC_NEWORDER. Nas tęp n i e zmi eń pro c edurę On Edi to rder( ) w następujący sposób: gółów zamówień
vo id COrderDetai l sV iew: :OnEdit order () (
i f( m_pSet ->CanUpdat e() ) {
t ry (
i f (m_Mode
~~
UPDATE)
( II Jeżeli przycisk zastal kliknięty, jes teś my w trybie uaktualniania. II Wylącz wp rowadzanie dany ch do kontr olek edycj i.
m_Quant ityCtrl .Set Rea dOnly( ) : m_Di scount Ct rl .Set Rea dOnly( ). II Zmień tekst przycisku Update na Edit Order.
m_EditOrderCtrl.SetWindowText(_T("Edit Order" )) : II Ukryj przycisk Can cel.
m_Cancel Edit Ct rl .ShowWlndow(SW_HIDE) : II
Wyświetl przy cisk
nowego zamówienia.
mNewOrderCt rl .ShowWindow(SWSHOW ): II Dokoń cz uaktualnianie.
m_pSet- >Update () : m_Mode ~ READ_ONLY:
II Przejdź do trybu tylko do odczy tu.
Rozdział 20.
• Aktualizacja źrólleł danych 1061
} el se ( II Jeżeli przy cisk zastal kliknięty, jesteśmy w trybie tylko do odczy tu. II Zezwól na wpro wadzanie danych do kontr olek edycji.
m_Ouant i ty Ctrl .SetReadOnly ( FALSE) ; m_Discount Ct r l.SetReadOnl y(FALSE) ; II Zmień tekst przycisku Edit Order na Update.
m_Edi t OrderCtr l .SetWi ndowText (_T(" Updat e" ) ) ; II
Wyś wietl przycis k
Cancel.
m_Cancel Ed i tCt r l .ShowWi ndow(S W_SHOW) ; II Ukryj przycisk nowego zamówienia.
m NewOr derCtrl .ShowWin dow(SWHIDE) ; II Rozpo cznij uaktualnianie.
m_pSet ->Ed i t () ; m_Mode = UPDATE; II Przejdż w tryb uaktualniania.
} cat ch(CException* pEx) { pEx->Repor t Err or ( ); II
Wyś wietl
kom unikat o blę dzie.
} el se AfxMessageBox(_T("Recordset t s not updatable . ") ) ;
Teraz , w za l eżn oś c i od tego, czy zmienna m_Updat eMode ma wartość READ_ONLY, czy UPDATE, przycisk New Order jest ukrywany bąd ź pokazywany. Także w procedurze ob sługi On Ca nce l ( ) musimy sprawić , by przycisk był widoczny : voi d COrder Det ai lsVie w: :OnCa ncel () ( m_pSet ->Cancel Update t ) : II Anuluj op e rację uaktualniania . m_Edi to rder Ctrl .SetWi ndowText(_T( "Ed i t " )) :11 Zm ień tekst p rzyc isku. m Cancel EditCt r l. ShowW i ndow(SWHIDE); II Ukr . rz ciskCancel. m NewOr der Ct r l .ShowWi ndow(SWSHOW ) ; II Wyświ etl przycisk nowego zamówienia. m_Ouant l t yCt r l .Set ReadOnl y (TRUE); II Ustaw stan kontr olki edycj i Quantity. m_Di scountCtr l .Set ReadOn l y (TRUE): II Ustaw stan kontr olki edy cj i raba tu. m_Mode = READ_ONLY: II Przelącz tryb.
Do tej pory zaimplementow aliśmy podstawowy mechanizm przełączania widoków . Musimy jednak wrócić i d odać kod uaktualni ający b azę danych . Mimo to jest to dobry moment na skompilowanie i uruchomienie programu, aby pozbyć si ę wszelkich literówek i innych błędów . Jeżeli program działa , można przegl ądać kłientów i produkty. Sprawdź wszystkie możliwe przełączenia widoków.
1068 Visual C++ 2005. Od podstaw
Tworzenie identyfikatora zamówienia Do utworzenia identyfikatora nowego zam ówienia będziemy potrzebowali zestawu rekordów dla tabeli Order s. Kliknij prawym przyciskiem myszy DBS i rnp l eUpdate w panelu Class View, a następnie wybierz Add/Class z menu p odręcznego. Wybierz MFC ODBC jako szablon i kliknij przycisk Add; następnie wybierz Northwind jako bazę danych i Order s jako tabelę dla zestawu rekordów . Jako typ wybierz Dynaset, ponieważ będziemy ponownie uży wać tego zestawu rekordów , gdy będziemy chcieli dodać nowe zamówienie. Jako nazwę klasy wpisz Cor derSet, a odpowiednie pliki nazwij OrderSet.h i OrderSet.cpp. Kliknij przycisk Finish , aby utworzyć klasę . Zmień sk ład o we typu CSt r i ngW w nowej klasie na typ CStr i ng i umieść w komentarzu dyrektywę #er r or w pliku OrderSet.cpp.
Przechowywanie identyfikatora nowego zamówienia W tej części bardziej dogłębnie omówię operacje na zestawach rekordów. Za każdym razem , gdy będziemy tworzyć nowe zamówienie w klasie CCust omer View, będziemy musieli utworzyć unikalny identyfikator zamówienia, więc musimy się zastanowić , gdzie to najlepiej zrobić i jak ten proces powinien wyglądać. Za utworzenie nowego identyfikatora naprawdę powinien być odpowiedzialny obiekt CO r derSet , mimo ze identyfikator jest wyświetlany przez jedną z kontrolek w widoku reprezentowanym przez obiekt CCusto me r Vi ew, a to dlatego że identyfikator jest zależny od danych w tym zestawie rekordów. Dobrym podejściem byłoby dodanie zmiennej do klasy CCus tomer Vi ew, która ustawiałaby wartość identyfikatora w kontrolce edycj i. Zmienna ta mogłaby być ustawiana przez funkcję należącą do obiektu CO r derSet . Przejdź do okna dialogowego IDD_CUSTOMERJORMw widoku zasobów i kliknij prawym przyciskiem mys zy kontrolkę edycji zawierającą identyfikator zamówienia (z identyfikatorem IDC_NEWORDERID). WybierzAdd Variable z menu kontekstowego, a następnie wpisz nazwę oraz wybierz typ i kategorię w sposób przedstawiony na rysunku 20.18. Domyślnym typem jest CSt r i ng, więc zmień go na Ionq, Funkcje DDX_Text( l, które przesyłają dane z i do kontrolki edycji, są do stępne w wielu wariantach, aby obsłużyć różne typy danych pokazywane na liście rozwijanej w oknie dialogowym.
Tworzellie identyfikatora nowego zamówienia Obiekt COrderSet nal eży do obiektu dokumentu, więc do klasy CDBSi mp1eDoc dodaj publiczną składową o nazwie m_OrderSet (aby współgrała z m_DBSimp1eUpda t eSet , która została utworzona przez kreator aplikacji). Zrób to jak zazwyczaj - kliknij prawym przyciskiem myszy nazwę klasy w panelu Class View i wybierz Add/Add Variable z menu podręcznego . Obiekt COrderSet będzie tworzony automatycznie podczas tworzenia obiektu dokumentu . Ponieważ zestaw zamów ie ń znajduje się w dokumencie, jest on dostępny dla wszystkich klas widoku.
Rozdział 20.•
Rysunek 20.18
Aktualizacja źródeł danych 1069
Add Hember Varlabte Wiz_rd . DBSlIllpleUpd_te
-
-
--
~
----
-
? -
~
- --
Welcome to the Add Member Variable Wizard
aceess:
f?UbC I'..,i abletyp. :
Calegury:
EJ
I Ion9
Ivalue i7J c ,
Variableaame:
~Ne w Or der. :.:: ID
--,
Minv~ly'e ;
'r== ---
=====11
-
L
-
-
-.1
ie
(omment(II notation not required):
'---_ _,..---,..---.....JI
-'I ! ·
1-\
Finish
II
Cancel
Dodaj do klasy COr derS et nową funkcję składową, generującą nowy, unikalny identyfikator zamówienia. Za pomocą panelu Class View dodaj funkcję Crea te NewOrder I D( ), zwracającą typ long i nieprzyjmującą parametrów. Funkcja CreateNewO rder ID () musi na
początku sprawdzić,
czy zestaw rekordów jest otwarty:
long COrderSet: :Creat eNewOrder ID(void) I if ( II sOpen()) Open(CRecordset. :dynaset ); II Reszta implementacji funkcji...
Wywoływana
w instrukcji if funkcja I sOpen( ) zwraca TRUE, jeżeli zestaw rekordów jest otwarty, a w przeciwnym razie zwraca FALSE. W celu otwarcia zestawu rekordów wywołu jemy składową Open t ), dziedziczoną z klasy CRecor dSet . Przesyła ona zapytanie do bazy danych z typem zestawu rekordów określonym przez pierwszy argument. Jako ten argument przesyłamy CRecordset: :dynase t , co jak możesz się spodziewać, otwiera dynamiczny zestaw rekordów. Tak się składa, że jest to niepotrzebne, ponieważ jeżeli argument zostanie pominięty, będzie zastosowana domyślna wartość, którą określiliśmy, tworząc klasę, czyli dynaset. Dzięki temu mogę jednak wspomnieć o innych możliwych argumentach (patrz tabela na następnej stronie). Funkcja Open() przyjmuje jeszcze dwa parametry, dla których zaakceptowaliśmy domyślne Drugim parametrem jest wskaźnik do łańcucha, który może być nazwą tabeli, poleceniem SELECT języka SQL , wywołaniem prędefiniowanego zapytania lub może być pusty, co jest ustawieniem domyślnym. Jeżeli łańcuch ten jest pusty, używany jest łańcuch zwracany przez GetOe fau l t SQL ( ). Trzecim argumentem jest maska bitowa, za pomocą której można ustawić bardzo wiele opcji połączenia, włączając w to ustanowienie trybu tylko do odczytu, co wartości.
1070 Visual C++ 2005. Od podstaw CRecordset: :snapshot
Otwierany jest zestaw rekordów typu utrwalonego - omawi ałem dynamiczne i utrwalone zestawy rekordów w poprzednim rozdziale.
CRecordset : :forwa rdonly
Zestaw rekordówjest otwieranyw trybietylko do odczytui może być przewijany jedynie do przodu. (Po otwarciu zestawu rekordów automatycznie wybierana jest pierwsza pozycja).
CRecordset : :dynami c
Zestaw rekordów jest otwierany z możliwością przewijania w obydwu kierunkach, a zmiany dokonywane przez innych użytkowników mają odzwierciedlenie w polach zestawu rekordów.
AFX_DB_USE_DEFAULT_TYPE
Zestaw rekordów je st otwierany jako domyślny typ zestawu rekordów, przech owywany w dziedziczonej składowej m_nDef aultType, która inicjalizowana jest w konstruktorze.
oznacza, że w ogóle nie można zapisywać , lub ustanowienie trybu przyłączania, w którym nie jest możliwe edytowanie bądź usuwanie rekordów. Więcej szczegółów znajdziesz w dokumentacji tej funkcji. Mając
otwarty zestaw rekordów, chcemy wyszukać to zrobić, używając poniższego kodu :
największą wartość
w polu Or der ID.
Możemy
long COrderSet. :Creat eNewOrder ID(vo id ) { if( ! IsOpen( ) )
Open(C Recordset : :dynaset) : II Sp rawdź. czy zesta w rekordów nie j est pusty.
long newOrder ID ~ O: if ( I( IsBOF( ) & & IsEOF( ) )) { II Mamy rekordy, MoveFi r- st r ) : II więc przejdź do p ierwszego. whi l e(! IsEOFO) II Porównaj z pozo stalymi.
{ II Zapisz
ide n tyfika to r.fe żeli jest większy.
if (newOrderI D < m_Order ID) newOrder ID = m_Order ID: MoveNext ( ): /1 Przejdź do następnego rekordu. }
ret urn ++newOrderID;
Składowe
I sBOF () i I sEOF( ) zestawu rekordów zwra c aj ą true, jeże l i znajdujemy się poza lub końcem (odpowiednio) zestawu rekordów , w którym to przypadku żaden
po czątki em
rekord nie jest aktywny, więc powinniśmy użyć pól. Jeżeli zestaw rekordów jest pusty, obie funkcje zwracają TRUE. Jeżeli w zestawie znajdują s ię jakieś rekordy , przechodzimy do pierwszego z nich, wywołując funkcję składową MoveFirst O . Do stępna jest także składowa Movel.a st O , przechodząca do ostatniego rekordu w zestawie. początkową wartością O, która na końcu będzie identyfikatora zamówienia w tabeli . Pętla whi l e przechodzi przez rekordy w zestawie, wykorzy stując do tego funkcję MoveNext (), sprawdzając, czy obecna j est warto ść większa niż m_newOrderID. Przed wywołan iem którejkolwiek składowej
Tworzymy
zmienną lokalną
newOrder ID z
przechowywała m aksymalną wartość
Rozllział 20.
• Aktualizacja źródeł danych
1071
przechodzenia przez zestaw rekordów należy - w zależności od obranego kierunku I sEOFO lub I sBOF (). Jeżeli funkcja przechodząca zostanie wywołana poza początkiem lub końcem zestawu rekordów, funkcja zgłosi wyjątek typu CBExcept i on. wywołać funkcję
Oprócz użytych przez nas funkcji trzy inne:
MoveLast()
kroczących,
obiekt zestawu rekordów dostarcza jeszcze
Przechodzi do ostatniego rekordu w zestawie. Nie należy używać tej funkcji (ani M oveF i rst( l), mam y do czynienia z zestawem rekordów typu forwardonly, ponieważ zostanie zgłoszony wyjątek typu CDB Except ion .
je żeli
MovePrev()
Przechod zi do rekordu p opr zed zaj ącego bie żący rekord w zestawie. Jeżeli taki nie istnieje, przechodzi jedną pozycję poza pierwszy rekord. W takim przypadku pola zestawu rekordów nie są poprawne i IsBDF() zwraca true.
Move( )
Funkcja ta słu ży do przechodzenia o jeden lub w ię cej rekordów w zestawie . Pierwszy argument, typu l onę , określa liczbę wierszy, o kt órą chcemy przemieści ć si ę w zestawie. Drugi argument, typu WORD, określa rodzaj przemieszczenia. Cztery wartości dla drugiego argumentu mogą uczynić z tej funkcji funkcję to ż samą z omawianymi wcześniej funkcjami przemieszczania . Wi ęcej szczegół ów na ten temat znajdziesz w dokumentacji Visual C++.
Ostaniem etapem jest przesłanie wartości do kontrolki , aby pojawiła się w oknie dialogowym IDO_CUSTOMERJORM. Służy do tego wywołanie składowej UpdateData ( l obiektu widoku z argumentem FALSE. Ta funkcja jest dziedziczona przez klasę widoku rekordów z klasy CWnd. Argument FALSE powoduje przesłanie danych z danych składowych klasy widoku do kontrolek w oknie dialogowym. Wartość TRUE powoduje pobranie danych z kontrolek i zapisanie ich w danych składowych . W obu przypadkach platforma wywołuje składową widoku DoDat aExchange( l .
ROzpoczynanie tworzenia identyfikatora Przy wyświetlaniu widok klientów wymaga, aby identyfikator nowego zamówienia był dostęp ny. Dodaj publiczną funkcję składową Set NewOrder ID O do klasy CCustomerVi ew i zaimplementuj ją w następujący sposób :
void CCustamerVlew: :Set NewOrderID(voi d) I
II Pobiera identyfikator nowego zamówien ia z obiektu COrderSet w dokumencie.
m_NewOrder ID ~ statl c_cast (Get Document ())->m_OrderSet .Creat eNewOrderID( ); UpdateData( FALSE) ; II Przesyła dane do kontrolek. Wskaźnik
zwracany przez dziedziczoną funkcję GetDocument( l jest typu CDocument. Chcemy aby uzyskać dostęp do s kł ad owej m_Order Set wyprowadzonej klasy, więc musimy rzutować wskaźnik na typ COBS i mpl eUpdateOoc* . Następn ie wywołujemy funkcję składową składowej dokumentu 1ll_0rderSet. Funkcja ta zwraca identyfikator nowego zamówienia i przechowuje wynik w składowej mJ~ewOrder ID klasy CCustonlerVi ew. Wywołanie odziedziczonej składowej UpdateOata(l widoku przesyła dane z danych składowych widoku do kontrolek. Musimy dodać dyrektywę #i ncl ude dla pliku DBSimpleUpdateDoc.h do pliku źródłowego, ponieważ odnosimy się do nazwy klasy CDBS i mp l eUpd ateOoc. go
użyć,
1072 Wisual C++ 2005. Od podslaw P o nieważ
zawsze tworzymy tylko j eden obiekt CCustomerVi ew i w razie potrzeby wykorzystujemy go ponowni e, chcemy, aby nowy identyfikator był dostępny za każdym razem , gdy przełączamy się do tego widoku. Składowa Select View() obiektu CMainFrame obsługu j e przełączan ie okien dialogowych; tutaj takż e tworzone są obiekty CCust omerVi ew. Jest to dobre miejsce do rozpoczęcia procesu tworzenia nowego identyfikatora zamówienia. Musimy jedynie dodać trochę kodu wywołującego składową SetNewOrder ( ), w przypadku gdy widokiem jest CCusto mer Vi ew:
void CMainFrame : :Select View(Vi ewIDviewID ) (
CView* pOl dAct i veView ~ Get Act iveView( ):
II Pobiera
b ieżący
widok.
II Pobiera wskaźni k do nowego widoku, jeże li ten istniej e. IIjeże li nie, wskaźn ik będz ie pusty.
CView* pNewActiveView = static_cast (Get Dlgl tem(viewID)) : II Jeżeli jestto pi erwsze użycie noweg o widoku, nowy widok II nie istnieje, więc musimy go u tworzyć.
II Widok Order Details j est zawsze tworzony jako pierwszy, wię c nie musimy II w tym celu nic ro b ić.
l f (pNewActiveV iew ~= NULL ) {
switch (viewID) (
case NEW_ORDER: II Utwórz widok umożliwiają cy dodanie nowego zamówienia. pNewAct iveVi ew = new CCust omerVi ew : break : case SELECT_PRODUCT: II Utwórz widok umożliwiają cy dodanie produktu do zamówienia. pNewAct lveView = new CProduct View : break : default : AfxMessageBoxCT(" Inva l id View ID" )): return; II Prz ełq czanie widoków. II Uzyskaj kontekst bieżą cego widoku. aby
zastoso wać
go do nowego widoku.
CCreat eContext conte xt : cont ext .m_pCurrent Doc ~ pOldActi veView->GetDocument () : pNewAct iveVi ew- >Create (NULL. NULL . OL. CFrameWnd : :rectDefault . thi s . vlewID. &cont ext ): pNewAct iveView->Onl nitia l Updat e( ): widok. Set Act iveView( NewAct i veView): II Ak i f (viewID==NEW _ORDER) st at ic cast (pNewAct iveView) ->SetNewOrderID () :
pOldActiveView ->ShowWi ndow( SW _HID EJ : II Ukryj stary widok. pNewAct i veV iew- >ShowWi ndow(SW _SHOW): II Wyświetl n0"'Y widok. pOldAct i veV iew->SetDlgCt r l ID (m_CurrentVi ewID ): II Ustaw identyfikator starego widoku. pNewActi veView->Set DlgCtrlID(AFX_IDW_PANE_FIRST) : m_Current View ID = viewID: II Zap isz identyfikat or nowego widoku. RecalcLayout ():
Rozdział 20.•
Aktualizacja źródeł danych 1073
Sprawdzamy jedynie wartość viewID. Jeżeli jest nią NEW_ORDER, wywołuj emy skł ad ową Set NewOrde r ID() obiektu nowego widoku . Poni eważ pNewAct i veVi ew je st typu CVi ew, musimy rzutowa ć j ą na właś c iwy
typ widoku, aby
wywoła ć funkcję skład o wą.
Przechowywanie Ilanych zamówienia Dopóki nie będzie dostępny pierwszy rekord Produet Detai l s dla zamówienia, nie chcemy tworzy ć nowego wpisu w tabeli Or ders , w ięc musimy zn a l eźć sposób na przesłanie danych zgromadzonych w obiekcie CCustomerV iew do obiektu CPr oduct Vi ew. Prostym rozwiązaniem jest tu utworzenie nowej klasy reprezentując ej zamówienie. Musi ona jedyn ie m ieć dane skła dowe dla wszy stkich wartoś ci , które chcemy przesł ać. Poza polem daty wysyłki , której umieszczenie w nowym zamówieniu b yłob y nierozs ądne, dane składowe są takie same ja k dane składowe odp owiadające polom w klasie COrder Set. Utw órz w projekcie nowy plik nagłówkowy - Order. h i dodaj do nieg o poniższy kod: II Zapis uje dane nowego zamó wienia.
#pragma once cl ass COrder
{ publ i c: II Dane składowe są takie sam e j ak p ola w Carderset.
l ong m_Order ID: CSt ring m_Cu stome r I D: l ong m_Emp l oyeeI D. CTi me m_Order Date ; CTime m_Requi redDat e; l ong m_Shi pVia; double m_Freight ; CStr i ng m_Ship Name; CStr i ng m_ShipAddr ess: CStri ng m_Sh l pCi t y ; CStr ing m_ShipRegion : CSt ri ng m_ShipPosta lCode. CSt ring m_Ship Count ry :y II Domyślny konstruktor.
COrder() ; m_OrderI D(O). II Będzie ustawiony p rzez obiekt Ctlustomerl/iew. m_Empl oyeel fn l ) . II Przypisany dowo lny pracownik. m_Shi pVi aC3). II Przypis ana dowolna fi rma transportowa. m_Cu stomerIDCTC "" )) . mJ reig htCO .O) . m_Shi pNameC_TC " " )). m_Shi pAddress(_ TC" " )) . m_Shi pC i t y( _TC" ")). m_ShipRegion C_TC " ")) . m Shi pP osta lCodeC TC " " ) ) . m Ship Count ry( TC " " ) ) { SYSTEMTIME Now: GetL ocalTi me( &Now); II Pobiera bieżący czas. m_Order Date ~ m_Requi r edDat e = CTime CNow ) ; II Jako czas ustaw ia dz is iejsz ą datę.
}
l:
1074 Visual C++ 2005. Od podstaw Umieszczanie, jak tutaj, wszystkich danych składowych w części publ ic nie jest dobrą praktyką, ale ponieważ wszy stk ie klasy zestawu rekordów generowane przez kreator klas mają publ iczne składowe , niewiele byśmy zyskali , umieszczając je w części pr i vate. Gdy dodamy s kład ow ą m_Order typu COrder do klasy dokumentu , będziemy mogli jej użyć do przesyłan ia danych zamówienia do obiektu CProduct View. Musimy jedynie sprawić, aby obiekt CCustomer Vi ew ładował dane składowe po kliknięciu przycisku Select Produ cts - muszą być bowiem gotowe do przejęcia przez obiekt CProduct Vi ew. Zaimplementuj procedurę obsługi przycisku w następujący sposób:
void CCustomerVi ew : :OnSelect product s() { II Pobiera
wskaźn ik
do dokumentu.
CDBSimpleUpdat eDoe* pDoe = st at ie_east (Get Doeument ()) : II Ustawia
wartoś ci pól
zamówienia na podstawie obiektu CCustomerSet.
pDoe->m Order .m Cu st omer ID = m pSet ->m Customer ID: pDoe->m=Order.m=ShipAddress = m_pSet- >m_Address : pDoe-> m_Order .m_Shi pCity = m_pSet->m_Ci t y: pDoe-> mOrder.m ShipCount ry = m pSet->m Count ry; pDoe -> m-Order.m- Sh l pName = mpSet ->m CompanyName : pDoe-> m=Order .m=ShipPost al Code = m_p5et ->m_Post alCode; pDoe-> m_Order .m_ShipRegi on = m_pSet->m_Regi on: II Ustawia wartości pól zamówienia na p odstawie dany ch wprowadzonych w oknie dialogowym II CCustomerView. pDoe -> m_Order .m_OrderID ~ m_NewOrder ID : li Nowy wygenerowany identyfikator. pDoe-> m_Order .m_OrderDat e = m_OrderDate: II Z kantrolki z datą zamówienia.
pDoe-> mOrder .mRequir edDate
=
mRequi redDat e:
IIZ kon tro lki zdatą wymaga lnośc i.
st at ie_east(Get Parent Frame( )) ->SeleetView(SE LECT_PRODUCT); Jest to zupełnie prosty kod . Po prostu kopiujemy wartości z zestawu rekordów i obiektów widoku rekordów do obiektu Order, który jest skł ad o w ą obiektu dokumentu.
Ustawianie dat Z kontrolkami wyboru daty w oknie dialogowym CCusto merVi ew mamy pewien problemw tej chwili odpowiadające im zmienne, m_OrderDat e i m_RequireDat e, nie są inicjalizowane, więc kontrolki nie wyświetlaj ą na początku sensownych wartości. Chcemy, aby wyświetlały one na początku bieżącą datę, więc dodaj poniższy kod na końcu składowej Onlniti alUpdate ( ) wywoływanej podczas tworzenia obiektu widoku :
void CCust omerVi ew : :Onlnit ialUpdate( ) {
BeginW altCursor (): Get Recordset () : CReeordView: :Onlniti alUpdat e(): i f (m pSet ->IsOpen()
{
-
CString strTit le = m_pSet ->m_pData base->Get Data baseName( ): CSt ring st rTa ble = m_pSet ->GetTableName( ) : i f (!st rTable.I sEmpty( »)
Rozdział 20.•
Aktualizacja źródeł danych 1075
st rTit le += T(" :") + strTable: Get Document() ->Set Tit le(st rTi t le) : }
EndWaitCursor( ); II Inicj alizacj a
wart ości
cza su.
SYSTEMTIME Now: GetLocalTime(&Now): II Pobiera b ieżący czas. mDrderDate = mRequi redDat e = CTime(Now ):
II Jak o czas ustawia dzisiejszą datę.
Ustawiamy tutaj obydwie zmienne CTime na bieżący czas, takjak w konstruktorze klasy COrder. Obiekt CCust omerVi ew jest już ukończony . Wyświetla poprawną datę i zapisuje wszystkie wartości pól wiersza tabeli Orders , a zatem możemy się zająć procesem wybierania produktu.
Wybieranie produktów dla zamówienia Gdy widok wybierania produktu będzie wyświetlany, chcemy, aby zmienne kontrolek wyświe identyfikator zamówienia i nazwę klienta miały już poprawne wartości . Uzyskamy te wartości ze składowej Order obiektu dokumentu . W tym celu do klasy CProduct Vi ewdodamy funkcję o nazwie Initial i zeView() i zwracanym typie void. Będziemy wywoływać tę funkcję ze składowej Sele ct Vi ewO obiektu CMainF rame aplikacji. Dzięki temu będziemy mieć pewność, że kontrolki będą zawsze zainicjalizowane przed wyświetleniem okna dialogowego. tlających
Zanim zaimplementujesz funkcję Init i al i zeVi ewO , rozważ jeszcze jedną kwestię. Nowy rekord w tabeli Orde rs będzie dodawany jedynie po kliknięciu przycisku Select Produet w celu dodania pierwszego produktu do zamówienia. Kolejne kliknięcia przycisku powinny po prostu dodawać kolejny produkt do zamówienia, więc musimy znaleźć sposób określania, czy po kliknięciu przycisku do tabeli Orders został dołączony rekord , czy też nie. Możemy to zrobić poprzez dodanie do klasy CProduct Vi ew zmiennej m_OrderAdd typu bool z początko wą wartością fa l se i ustawienie jej na true w procedurze obsługi przycisku. Dodaj więc tę zmienną do klasy. Będziemy ją inicjalizować w składowej Ini tial i zeView O, którą zaimplementuj w ten sposób:
void CProductView : : Init iali zeVi ewO (
II Pobiera
wskaźn ik
do dokumentu.
CDBSimpleUpdat eDoc* pDoc = stat ic_cast ( GetDocument() ): m_OrderID = pDoc->m_Order .m_OrderID : m_CustomerName = pDoc->m_Order .m_ShipName : m_Quant ity = 1: II Należy zamówić przynajmniej l sztukę.
m_Dl scount ~ O: II Brak domyślnego rab atu. m_O rderAdded ~ false: II Po czątkowo zamówienie nie j est dodane. UpdateData(FALSE ): II Przesyła dan e do kontrolek. Ten kod inicjalizuje składowe klasy widoku dla kontrolek identyfikatora zamówienia i nazwy klienta poprzez skopiowanie wartości z odpowiednich składowych składowej Order dokumentu . W tej funkcji możemy również zapewnić , by kontrolki ilości zamawianego towaru i rabatu miały odpowiednią początkową wartość. Musimy zamówić przynajmniej l sztukę
176 Visual C++ 2005. Od podstaw towaru, a domyślną w arto ś cią rabatu b ędzie O. Wywołanie dziedziczonej składowej Update Dat aO z argumentem FAL SE powoduje przesłanie danych ze zm iennych klasy do kontrolek. Na początku pliku źródłowego musimy dodać dyrektywę #i nc l ude dla pliku DBSimpleUpdateDoc.h, aby udostępni ć d efinicję klasy dokumentu. Aby to działało , musimy wywoływać funkcję I ni t i al i zeVi ew() przy każdym przełączeniu do widoku okna dialogowego umożliw iającego wybór produktu . Oczywistym miejscem jest więc składow a Select View() klasy CMa inFrame:
II Pobiera ws kaźn ik do nowego widoku, jeż eli ten istnieje, IIjeżeli nie, wskaźn ik będz ie pusty. CV iew* pNewAct i veView = stat ic_cast (GetD lgltem(vlewID )); II Jeże lijes t to p ierwsze użyc ie noweg o widoku. nowy widok, /1 nie istn iej e, więc musimy go utwo rzyć.
/1 Wido k Order Details jest zawsze tworzony jako pierwszy , więc nie musimy II w tym celu nic rob ić .
if (pNewAct iveV iew == NLILL ) {
switch(viewID ) {
case NE W_ORDER : II Utwórz widok umoż liwiający dodanie nowego zamówi enia. pNewAct iveView = new CCust omerView; break: case SE LECT_PRODUCT: II Utwórz widok umożliwiają cy dodani e produktu do zamówienia. pNewActi veVi ew = new CProductView; break: default : AfxMes sageBox (_T("lnval id View ID")) ; return : II Przełączan ie widoków. II Uzyskaj kontekst bieżącego widoku, aby
zastoso wać
go do no wego widoku.
CC reat eContext conte xt : context .m_pCurrentOoc = pOldAct i veV iew ->GetOocument( ); pNewAct i veView->Create( NULL. NULL , OL. CFrameWnd ' :rect Default . t hi s . viewlD. &cont ext ) : pNewAct i veView->Onlni t i alUpdat e(l : }
SetAct i veview(pNewAct i veView ) : II Aktywuj nowy widok. if (viewIO==NEW _OROER) st at ic cast ( NewAct iveViewl ->Set NewOrderI O( ) ; else if(viewl D== SELECT_PRODUCT ) st at ic cast (pNewAct i veView )->Init lal izeView() : _HIDE) : II Ukryj stary widok. pOl dActi veView->ShowW indow(SW pNewAct i veVi ew->ShowW"jndow (SW _SHOW): II Wyśw i etl nowy widok. pOldAct i veVi ew->SetOlgCtrl ID (m_Cu rrentV lewIDl ; II Ustaw identyfikat or starego widoku. pNewAct iveView->Set DlgCt rl ID(AFX- lDWPA NE- FIRSTl : m_Curr ent ViewID= vi e« ID ; II Zap isz identyfikator nowego widoku. RecalcLayout ();
Rozdzial20.• Aktualizacja źródeł danych 1077 Zmienne klasy CProductVi ew dla kontrolek identyfikatora zamówienia i nazwy klienta, a także zmienna typu boa l , sterująca tworzeniem noweg o rekordu w tabeli Or ders, zos taną zainicjali zowane, je żeli parametr vi ew lO będzie miał wartość SELECT_PRODUCT.
Dodawanie nowego zamówienia Do pełnego działani a programu brakuje nam jedynie kodu dodającego nowe zamówienie. Dodaw anie zamówienia będzie zawsze wykonywane przez skł adową OnSe l ectp roducts ( ) CProductVi ew. Efekt klikn ięcia przycisku Select Products zale ży od warto ści składowej m_OrderAdded. Jeżel i ma ona w artość f al se, funkcja powinna dodać nowy rekord do tabeli Orders , a także nowy rekord do tabeli Order Detai l s. Jeżel i m_Or derAdded ma warto ś ć t r ue, nowy rekord powinien zostać dodany tylk o do tabeli Order Det ai l s, pon iewa ż dodajemy kolejny produkt do tego samego-zamówienia. Wszystkie wartości potrzebne dla nowego rekordu Order s są przechowywane w s kładowej m_Order dokumentu . Musimy je tylko skop iow ać do składowych obiektu COrderSet, który takż e jest składową dokum entu. Obiekt dokumentu j est odpowiednim miejscem dla tej operacji , więc dodaj do CDBSimpleUpda te Doc funkcję składową AddOrder () zwracającą typ boal . Zaimplementuj ją w następujący sposób :
bool CDBSimpl eUpdat eDoc ' :AddOrder(void) I
try {
i f ( !m_OrderSet . IsOpen( » II Jeżeli zes taw rekordów nie jest otwarty. m_OrderSet .Open() ; II otwórz go . i f(mOrderSet .CanAppend(» II Jeżeli możemy dodać reko rd. { II dodaj go.
m_OrderSet .AddNew() : II Rozpocznij dodawanie nowego rekordu. m_OrderSet.m_Customerl D ~ m_Order .m_Custome rlD: mOrderSet .m EmployeelD ~ mOrder .m EmployeelD; m=OrderSet .m=Freight ~ m_Order .m_F reight ; m_OrderSet .m_OrderDat e ~ m_Order .m_OrderDat e; m_OrderSet .m_Order ID = m_Order .m_OrderI D; m_OrderSet. m_Requi redDate = m_Order .m_Requ i redDate ; m_OrderSet .m_ShipAdd ress = m_Order.m_ShipAddress ; m_OrderSet .m_ShipName = m_Orde r .m_ShipName ; m_OrderSet .m_Sh i pPosta lCode = m_Order .m_Shi pPostalCode; m_OrderSet .m_ShipRegi on = m_Order .m_ShipReg ion; m_OrderSet .m_Sh lpVia = m_Order .m_ShipVia : II Brak
wartości
dla p ola Shippe d Date.
m_OrderSet .SetFieldNu ll( &m_OrderSet m_ShippedDate); m_OrderSet .Upda te(); II Dokoń cz dodawa nie nowego rekordu. ret urn tru e: II Działanie zakoń czo ne po wodze niem. }
el se AfxMessageBox (_T( "Cannot append t o Orders t able }
cat ch(CExcept ion* pEx)
l }
pEx->ReportError() :
II
Wyłapuj
wszystkie
wyją tki .
II Wyswietl komunikat o
b łędzie .
U»
;
1078 Visnal C++ 2005. Od podstaw return false:
II Tutaj mamy niepowodzenie.
Wcześniej
w tym rozdziale dowiedziałeś się, że w obiekcie zestawu rekordów funkcje dodai edytujące rekordu mogą zgłaszać wyjątki . A zatem umieszczamy kod w bloku try i wyłapujemy je wszystkie, aby zapobiec przerwaniu działania aplikacji, jeżeli zdarzy się wyjątek.
jące
Po upewnieniu się, że zestaw rekordów COrderSet jest otwarty, sprawdzamy, czy można do niego dodawać rekordy, poprzez wywołanie składowej CanAppen d(). Dodawanie nowego rekordu przebiega w trzech etapach:
l
Wywołujemy składową AddNew ( )
zestawu rekordów. To rozpoczyna proces i zapisuje danych składowych zestawu rekordów, ponieważ będziemy je zmieniać, i ustawia wartości danych składowych na nu11 . Nie ma to nic wspólnego z wartością nu11 dla wskaźników i nie jest to zero - nu11 oznacza tu, że zmiennej nie przypisano żadnej wartości. bieżące wartości
składowym dla wartości pól w zestawie rekordów potrzebne w rekordzie. Jest to całkiem łatwe . Po prostu kopiujemy wartości przechowywane w obiekcie m_Order do składowych obiektu zestawu rekordów . Składowa m_ShippedDate pozostaje, ponieważ nie przypisujemy jej tu żadnej wartości .
2. Przypisujemy wszystkim danym wartości
8.
Wywołujemy Update( ), aby zapisać rekord, a także przywrócić pierwotne wartości w obiekcie zestawu rekordów. Nie dotyczy to akurat tej sytuacji, ale gdybyśmy wyświetlali zestaw rekordów , do którego dodawaliśmy nowe rekordy, musielibyśmy wywołać składową Requery() obiektu zestawu rekordów , aby nowe wartości rekordu zostały wyświetlone.
Teraz do klasy CProductV i ew możemy wstawić podstawową logikę procedury obsługi OnSel ectproduct ( ). Musimy wywołać Updat eData( ) dla widoku, aby dane wpisane do kontrolek edycji zostały przesłane do danych składowych obiektu widoku. Oto zarys kodu tej funkcji :
vo id CProduetView ; :OnSeleetproduet() {
UpdateOata(TRUD : II Pobi era
wskaźnik
II Prześlij dane z kontr olek.
do dokumentu.
COBS impleUpdateOoe* pOoe = st at ie_east (Get Oocument ()); i f ( I m_OrderAdded) II Jeżeli zam ówienie nie zostało dodane. m_OrderAdded = pOoe->AddOrder ( J: II spróbuj j e dodać.
i f(m OrderAdded)
II Kod dodający nołry rekord Order Details...
Po wywołaniu Updat eDat aO dla obiektu CProduct Vi ew pobieramy wskaźnik do obiektu dokumentu. Potrzebujemy go do wywołania składowej AddOrder ( ) dokumentu. Następnie sprawdzamy składową m_OrderAdded. Chcemy dodać rekord do Orders tylko wtedy, jeżeli składowa ta ma wartość fa l se. Składowa AddOrde r ( ) obiektu dokumentu zwraca tru e, j eże l i dodawanie zamówienia zakończyło się powodzeniem, a w przeciwnym razie zwraca fa l se . Użyjemy tej wartości do ustawienia składowej m_OrderAdded CProduct Vi ew i jako wskaźnik, czy możemy kontynuować dodawanie szczegółów zamówienia. W przypadku niepowodzenia nie musimy wyświetlać żadnego komunikatu. Robi to już funkcja AddOrder ( ).
Rozdział20.
• Aklualizacia źródeł danJch 1079
Najlepszym miejscem dla kodu dodającego rekord do tabeli Order Detai l s jest również obiekt dokumentu, ale funkcja składowa klasy dokumentu, która będzie się tym zajmowała , musi mieć dostęp do czterech wartości ze składowych klas CProduct View i CP roductSet - dla identyfikatora produktu , wielkoś ci zamówienia, ceny za jednostkę i rabatu. Identyfikator zamówienia jest dostępny w klasie dokumentu dzięki jego składowej m_Order, więc nie musimy si ę nim martwić . Dodaj do klasy COBS i mp1eUpdateDoc funkcję AddOrder Deta il s(), która będzie dodawała rekord do tabeli Order Detai ls . Powinna ona zwracać typ voi d i przyjmować cztery parametry: I Dtypu l ong, pr ice typu doub1 e, quant i ty typu in t i di scount typu f1oat. Zaimplementuj
tę funkcję
w
następujący
sposób :
vOld COBSimp leUpdateDoc : :AddOrderDet ai ls (long ID. double price . i nt-quant ttv. fl oat discount ) I
try ( Im _DBSi mpleUpdateSet . IsOpen( )) II Jeżeli zes taw rekordów nie j est otwarty, m_DBSimpleUpdateSet . Opem ) : II otwórz go.
if(
m_DBSimpleUpdateSet.AddNew(): II Ustaw
wartośc i
II Rozpocznij dodawan ie noweg o rekordu .
danych skladowyc h zestawu rekordów Produ et Details.
mDBSimpleUpdateSet.m Order ID = mOrde r .mOrder iD : m) BSimpleUpdateSet .m=Ouant ity = quantity: m_OBSimpleUpdateSet.m_Ois count = discount : m_OBSimpleUpdateSet.m_ProductIO = ID ; m_OBSimpleUpdateSet .m_Unit Price = price; m_OBSimpl eUpdateSet .Updat eO : II Dokończ dodawanie nowego rekordu . }
catch(CExcept ion* pEx )
II
Wyłapuj
wszystki e wyjątki .
{
pEx->ReportError( ):
II
Wyświetl komun ikat
o błę dzie.
}
Przypisuje ona wartości składowym m_DBSi mp1 eUpdateSet, a następnie uaktualnia tabelę dokładnie w ten sam sposób, jak to było przy okazj i tabeli Orders. Ponownie musimy umieścić kod uaktualniający w bloku t ry, aby wyłapać wyjątki, które mogłyby być zgłoszone przez funkcje AddNew( ) i Update ( ). Chcemy, aby ta funkcja była wywoływana zawsze, gdy wywoływana jest procedura obsługi przycisku Select Produ et w klasie CProduct Vi ew, więc zmodyfikujemy w tym celu tę funkcję:
void CProduct View: :OnSelectpr oduct () (
UpdateDat a(TRUE): II Pobiera
II Prześ lij dan e z kontr olek.
wskaźn ik do
dokumentu.
COBSimpleUpdat eDoc* pDoc
=
st at ic_cast(Get Oocument());
i f( !m_OrderAdded) II Jeżeli zamó wienie nie zostalo dodan e, m_OrderAdded = pOOC->AddOrder ( ): II sp róbuj je dodać.
if (m_OrderAdded)
1080 Visnal C++ 2005. Od podstaw
pDoc->AddDrderDetai lsCm_pSet ->m_ProductI D. m_pSet ->m_Unit Price. m_Quantity . m_Di scount ): II Przywró ć domyślne
W celu uaktualnienia tabeli Order Det al l s wykorzystujemy obiekt m_DBSimpl eUpdat eSet. Był on używany przez pierwotny widok aplikacji i jest przechowywany w obiekcie dokumentu. Pobieramy wartości dla wielkości zamówienia i rabatu z danych składowych obiektu widoku odpowiadających kontrolkom edycji umożliwiającym wpisanie tych wartości. Wartość identyfikatora zamówienia została ustawiona, gdy okno dialogowe zostało wyświetlone, więc będzie ona wyświetlana jedynie w celach informacyjnych. Identyfikator produktu i cena jednostkowa są pobierane z obiektu CProductSet powiązanego z tym widokiem. Po wywołaniu Updat e( ) w celu zapisania rekordu przywracamy początkowe wartości domyślne dla wielkości zamówienia i rabatu.
~ Dodawanie nowych zamówień Po dodaniu szeregu innych zamówień, o czym możesz się przekonać , spoglądając na identyfikator zamówienia, dołączyłem zamówienie pokazane na rysunku 20.19.
Ryslmek 20.19
;ł G:\Docum enh and Se« mgs\Soft \Ho je d ok umenły\db 1 :(Custome r'§) . DBSUllp...
SeroH to select a produet end enter the quantityand discQunt
Order ID :
Customer:
Quantity:
Di.count:
ISeIect productl
~
Done
NUM
Ready
Kliknięcie
przycisku Select Produet dodaje bieżący produkt do zamówienia dla klienta, a napozwala na wybranie kolejnego produktu. Każde kliknięcie przycisku Select Produet dodaje nowy rekord do tabeli Order Oetai l s dla bieżącego identyfikatora zamówienia. Gdy zamówienie zostanie ukończone , kliknij przycisk Done. stępnie
Po dodaniu zamówienia można przekonać się, czy zostało ono poprawnie dodane, przechodząc do ostatniego zamówienia w widoku przeglądania szczegółów zamówienia, jak na rysunku 20.21 .
Rysunek 20.21
~ G :\Docume n t ł ;j')
III (Ó.t:;Bi,;,,;,g; )
- -
TOQ
B Add
II
Remove
'- l
De5ign
lotl01 ab
GenerM~oM~embe -:--r--~T'ue '
ot
II
caocol
Korzystanie zkontrolki GroupBox Kontrolka GroupBox słu ży do tworzenia grupy z innych kontrolek. Kontrolka Gr oupBox wyty cza także grani cę grupy, oddzi elaj ąc ją lin i ą, a także um ożliwia zatytułow anie grupy. W lote rii Lotto obstawiamy sześ ć różnych liczb z przedziału od l do 49. Dodaj kontrolkę GroupBox
Rozdział 21.
do
zakła dk i
• Aplikacie wYkorzystuiące Windows Forms 1095
Lotto poprzez kl ikni ęc i e kontrolki w oknie Toolbox i kliknięcie w zakład c e Lotto. Text, (Name) i Ooek w sposób przedstawi ony na rysun
Następni e zm i eń wartości właś ciwości
ku 2 1.10.
RYSunek 21.10 ts
Font
Microsoft Sans 5erifi 6}2Spt • ControlText
ForeColor RightT oLeft
Text
No Valu es Ilu 49
U s~alt Cursor
eelse
B Behavior ContextMenuStrip
(none)
Enabled ImeMode Teblnde x
True NoControl
UseCompatlbleTextRendering
O False
vrsble
True
,13 D_l_ iS (Applicotionsel bngs)
is
(OataBindings)
l
Tag
[3 Design
(Name)
lottovelues
GenerateMember
True False
Iocked fY10diflers
tEJ
Focus
~
C~usesV"li~tion
Pri~ ate
True
'13 layaul Anchor , AutoSize Auł:o Si z eMo de
im Lecatlen ~ I±J ManJln
!m~ximumSiz e
lmMinimumSize I±l Padd,ng ~ [±J
Size
v
Docl< Oefine:s which borders ci tbe
Właściw o ść
contr oł ere
bound to the container.
zakres możliwych do wytypowania liczb w bieżącej zakładce. Ooek sprawi a, że kontrolka GroupBox wypełn ia pojemnik, którym jest zakł adka Lotto w kontrolce Tab. Wartość właś c iwo ści (Name ) określ a nazwę zmiennej w klasie Forml, która identyfikuje tę kontrolkę. Text
określa
Wartość właści wośc i
dla loterii Euromillions potrzebujemy dwóch grup wartości - jednej z 5 różnymi od l do 50 i drugiej z dwoma różnymi "głównym i" wartoś ciami od l do 9. Numery w pierwszej grupie mogą być taki e same jak "główn e" wartości. Użyjemy więc kolejnego pojemnika w zakładce Euromillions, kliknij zatem Spl itCont ai ne r w grupie Contai ne rs w oknie Toolbox , a następnie kliknij w zakładce Euromillions, aby umieś cić w niej ten pojem nik. Aby wybrać zakładkę, najpierw kliknij tytuł zakładki u góry kontrolki zakładki - zostanie wybrana kontrolka zakładki z pod świetloną zakładką; następnie klikn ij wewnątrz obszaru zakładki , aby wybrać podświetloną zakładkę w kontrolce. Domyślną wartością właściwości
W
zakładce
wartościami
1096 Visual C++ 2005. Olf pOIfstaw Ooek kontrolki Sp1itConta iner jest Fi ll , więc kontrolka pow inna wypełniać trolka ma dwa panele, z których każdy może zawie rać kolejne kontrolki . Wyświetl właściwości kontrolki i ustaw wiony na rysunku 21 .11.
Panele powinny być tera z oddzi elon e poziomym separatorem, wi ęc przeciągnij go tak, aby dolny panel miał wielkość połowy górnego. Następnie ustaw właściwoś ć IsSp1itterFixe d na wartość True,jak na rysunku 21.11. Separator zostanie wtedy zablokowany w pozycji, w której go ustawisz. Jeżel i pozostawisz Fal se jako wartość właściwo ści IsSpl i tterFi xed, użytkownik będzie mógł dowolnie przeciągać separator. Możesz z grupować zawartoś ć każdego panelu w kontrolce Spl i tConta i ne r, korzystając z kontrolki GroupBox, w i ęc dodaj tę k ontrolkę do każdego panelu. Jako właściwoś ci Text , (Name ) i Ooek kontrolki GroupBox w górnym panelu podaj odpowiednio Va 1ues l t o 50, euro Va l ues i Fi 11 , a w kontrolce GroupBox w dolnym panelu podaj odpowiednio Val ues l t o 9, euroSta rs i Fi 11. Okno Editor powinno wyglądać jak na rysunku 21.12.
Rozdzial21. • Aplikacje wYkorzystujące Windows Forms 1091 Ryslmek 21.12
I:lay
~imits
tjelp
J,~.':~~~J __~~_~~~~_~~_.~~.= ,,_,,""~"_~ ,","_"""""""""'"~""" '"p1ayMenultem->C1ick +~ gcnew Syst em: :EventHand1er(t hi s . &Forml : :p1ayMenultem_C1ick) ;
Ta instrukcja dodaje nazwę funkcji do delegata w obiekcie p1ay l~ e n u ltem klasy Form l. Delegatom i obsłudze zdarzeń poświęcony był rozdział 9.
będącym składową
Dodaj procedury obsługi dla elementów menu Limits/Upper, Limits/Lower i Help/About, klikając dwukrotnie każdy z nich. Zrób to samo dla elementu Choose w menu kontekstowym.
Dodawanie składowych do klasy Forml Zanim zaczniemy implementować procedury ob sług i elementów menu, będziemy potrzebowali pól do przechowania danych powiązanych z ograniczeniami zakresu typowanych liczb. Wiesz już , jak dodać nowe pole do klasy Form l ; d okładnie w ten sam sposób, w jaki dodawałeś skład owe do klasy . Przejdź do panelu Class View, kliknij prawym przy ciskiem kl as ę Forml i wybierz Add/Add Variable z menu kontekstowe go. Ewentualnie dodaj kod samodzielnie, ale postaraj się nie naruszyć obszaru definicji klasy zarezerwowanego dla operacji Form Design. Musim y dodać następujące zmienne jako składowe pryw atne:
private: i nt lottoVal uescount . i nt euroVal uesCount :
II Liczba wartośc i obstaw ionych w Lotto. II Liczba wartośc i obstaw ionych w loterii Euromillions.
1104
VislJal C++ 2005. Od podstaw lnt int lnt lnt lnt
euroStarsCount: lot t oLowerLimit : lot t oUpperL imit : lot t oUserM inimum; lot t oUserMaximum;
int int int int int int int
euroLowerLimi t : II Minimalna war tość dozwolona w Eur omillions. euroUppe rL imit: II Maksy malna wart oś ć dozw olona w Euromillions. euroStarsLowerLimlt : II Minimaln a wartos ć g lówna dozw olona w Euromillions. euroStarsUpperLimi t : II Maksymalna wartoś ć dozwol ona w Eur omillions. euroUserMinimum : II Dolny limit zakresu w Eurom illio ns p odany przez użytkownika. euroUserMaximum: II Górny limit zakresu w Eur omillions p odany przez użytkownika. euroSt a rsuserm ni mum: II Dolny limit glównych liczb w Eur omillions p odany
II Li czba glównych wart oś ci obs tawiony ch w Eur omil/ions. II Minimalna wartość dozwolona w Lal/o. II Maksymalna wartos ć dozwolona w Lal/ o. II Dolny limit za kres u w Lott o podany p rz ez użytkownika . II Górny limit zakresu w Lott o podany przez użytkownika.
int euroStarsUserMaxl mum : Do klasy Forml musimy
II przez użytkownika. II Górny limit głównych liczb w Euromillions p odany II przez użytkownika.
także dodać
Random.. . random:
prywatne pole typu Random:
II Generuje pseudoloso we liczby .
Wszystkie te pola muszą zostać zainicjalizowane w konstruktorze klasy. Dopilnuj, aby definicja konstruktora klasy Forrnl przypominała poniższą:
publiC ref class Formi : pub l iC System: :Wlndows: :Forms : :Form {
publ ic: Forml( void) lot t oVal uesCount( 6) , euroVa l uesCount(S J, euroSta rsCount (2J. l ot t oLowerLimi t (I J, lot t oUpperLi mit (49l , lott oUserMi nimum( lotto LowerL imit J, lot t oUserMaximum(lottoUpperLimit J. euroLowerLimit( l ). euroUpperLi mit(SOJ . euroSta rsLowerLi mi t( l ). euroStarsUpperLimit( 9J. euroUserMinimum(euroLowerLi mit ). euroUserMaximum( euroUpperLi mit J. euroStarsUserMinimum(euroSta rsLowerLi mi tl , euroSt arsUserMa ximum( euroSt arsUpperLimit J initi ali zeComponent (J: II
random
~
gcnew Random:
II
} Są
to na razie wszystkie nowe dane do obsługi zdarzeń.
składowe
klasy, których
będziemy
potrzebowali,
więc
powróćmy
ObshJga zdarzenia menu Play Procedura obsługi pl ayMenuI tern_Cl i ck( ) powinna tworzyć nowy zestaw wartości na przyciskach w aktualnie widocznej zakładce. Jak sobie przypominasz, wcześniej ustawiłeś wartość właściwości (Name ) tych zakładek w kontrolce TabCont rol jako l ottoTab i euroTa b. Jeżeli spojrzysz do całkiem już sporej definicji klasy Fo rml, znajdziesz w niej dwie zmienne typu TabPage.. . o tych nazwach. Obiekty typu Tab Page mają właściwo ść Vi si bl e typu bool, która
Rozdział 21.
• Aplikacie wYkorzystlliące Windows Forms
1105
ma warto ść tru e, jeżeli zakładk a jest widoczna, a fa l se, jeżeli tak nie jest. Właśnie tego potrzebujemy do zai mplementowania proc edury obsługi elementu menu Play. Ogólny sposób w ten sposób:
działani a
tej procedury
może wykorzystywać właściwość
Vi S i bl e
zakładek
prl vate: Syst em: :Void playMenult em_Cl ickCSyst em: :Obj ect A sender. Syst em: :EventArg sA e) (
i f (lott oTab ->Visible ) { II Gen eruje i ustawia
wartości
dla Lotto.
}
else ifC euroTab->Visible) { II Generuje i usta wia
wartości
dla Euromillions.
}
Jeżeli właściwość
Vi si ble dla zakładki lottoTab ma wartość true, tworzymy nowy "kupon" dla loterii Lotto, a jeżeli właściwość Vi si ble dla zakładki euroTa b ma warto ś ć true, tworzymy "kupon" dla loterii Euromillions. Mimo że zakładki te nie mogą by ć wyświetlone jednocześnie, dobrze jest przetestować prawdziwość dla obu zakładek , ponieważ użytkownik może kliknąć Play, gdy wido czna je st zakładka Web Page. Proces generowania zestawu wartości dla obu loterii ma pewne cechy wspólne . W przypadku Lotto musimy wygenerować sześć losowo wybranych liczb całkowitych z określonego zakresu. W przypadku loterii Euromillions musimy wylosować pięć różnych liczb całkowitych z zakresu, a następnie wygenerować dwie różne liczby całkowite z innego zakresu . Przydałaby się funkcja pomocnicza losująca wybraną liczbę liczb całkowitych z określonego zakresu. Możemy zdefiniować taką funkcję w taki sposób: void ( II
GetVa l ue s (ar ray< i n t >Ą Wypełn ij tab licę różnymi
values. i nt mi n. int max)
loso wymi liczbami ca łkowitymi z zakresu min do max ...
l Właściwość Lengt h tablicy wartości powie nam, jak wiele wartości ma zostać wygenerowanych. Funkcja wywołująca musi tylko utworzyć tablicę o odpow iednim rozmiarze i przesła ć jąjako pierwszy argument do funkcji GetVa l ues O . Drugi i trzeci argument określają zakres wartości, spośród których nastąpi losowanie.
Dodaj funkcję Get Va l ues () jako w poniższy sposób :
prywatną składową klasy
Forml i
voi d GetVa l uesCa rrayA values . i nt mln. int ma x) (
val ues[O]
~
random->Next(ml n. ma x):
II Wyl osuj pozostałe
for(int i
~
II Wylosuj pierwszą
wartość.
wa rtości.
l . i Length ; i++ )
(
for C. ; ) {
II Pętla, dopóki nie zostanie odnaleziona pop rawna
wartoś ć .
uzupełnij
jej
definicj ę
1106
Visual C++ 2005. Od podstaw II Wyl osuj
liczbę ca łko w itą
z zakr esu min do max .
values[i ] = random->Nextt min , max) ;
II Sprawdź, czy jest
róż na
if( IsVa l i drva l ues[ i] .
break.
od p oprzednich wart oset. va lues . i )) II Porównaj z po przednimi warto ś ciami .
II ...jest różna , w ięc zako ń cz pętlę .
Dowolna wartość z zakres u m o że być p rzyj ęta jako pierwsza liczba. Kolejn e wartości mu s z ą b y ć porównane z poprzedn io wylosowanymi i tym zajmuj e s ię funkcja I sVa l i d( l. Oto, jak możemy za i m p l em e nto w a ć tę funkcję:
II Sprawdza, czy liczba j est różna od wa rtośc i elementów tablicy II znajdujących się na pozycj ach z indeksem mniejszym n iż indexl.imit.
boal IsVa l i dCi nt number , array" values . i nt i ndexLimit) (
for( int l = O i< i ndexLimit ; i++) {
ifCnumber ~~ val ues[i Jl return fal se: }
ret urn true; Dodaj tę funkcj ę jako p rywatną s kład o w ą klasy Fo rml. Jej d zi ałan i e jest pros te: porównuj e pierwszy argument z elementam i tablicy określo nej przez drug i argument, które maj ą indeks mniej szy n i ż trzeci argument, i zwrac a fa l se, jeżeli pierwszy argument jest równy jak iemuko lwiek element ow i tabl icy. W przec iwny m razie zw raca t rue, co wskazuje, że pierwszy arg ument jes t poprawny.
for w funkcji GetVal ues() jest wykonywana i losuje nowe wartośc i do mo mentu, gdy funkcja I s Va l i d() zwróci wartoś ć t rue, kiedy to w ewnę trzna pę tla kończy dz i ałani e i wykonywany j est kolejny przebieg zewn ętrzn ej pęt li w celu odnalezienia n astępnej
Nies końc zo na p ętl a
n i ep o wt arzaj ącej się warto ś ci.
Teraz m o żemy wykorzystać imp lemen tację funkcji Get Val ues() w procedurze nia Cl i ck dla eleme ntu menu Play:
Syst em ; .Vold playMenu ltem_C llckC System; ;Obj ect
A
o bs ługi
sende r, System : ;Event Args
A
I
array" va l ues; II Zmienna przec howująca uch wyt i f (lot t oTab->Vi si blel
to tablicy liczb ca łkowitych.
( II Generuje i ustawi a
wa rtości
dla Lotto.
values = gcnew arrayClottoVal uesCount) ; Get ValuesC values . lot toUserMinlmum . lottoUserMax lmum ); Set Val ues Cva l ues . lottoVal ues l ; }
el se l fCe uroTab ->Vi si ble) ( II Generuj e i usta wia wartoset dla Eu rom illions.
va lues = gcnew arrayCeuroVal uesCount); GetVal ues(va l ues. euroUserM inimum, euroUSerMaxlmum); Set Val uesCva l ues. euroValues) ;
II Utwó rz tablicę . II Losuj wartośc i .
e)
zdarze-
Rozdział 21.
• Aplikacje wYkorzystuiące Windows Forms
1107
val ues = gcnew array(euroStarsCount) : GetVal ues( val ues . euroStarsUserMinimum. euroSt ars UserMa ximum); Set Values (val ues . euroSt ars );
Tworzenie "kuponu" dla Lotto przebiega w trzech etapach:
l
Tworzenie tablicy
przechowującej wartości.
2. Wylosowanie wartości przez
wywołanie
funkcji GetVal ues ( l.
3. Ustawienie wartości jako tekstu w przyciskach poprzez Set Va l ues( l. Te etapy wartości,
wywołanie funkcji
są
dwukrotnie powtórzone w przypadku loterii Euromillions: raz dla zestawu a drugi raz dla zestawu dwóch liczb "głównych".
pięciu
Implementując funkcję Set Va lues (), skorzystamy z faktu, że przyciski zn aj d uj ą się w kontrolce GroupBox. Właściwość Controls obiektu GroupBox zwraca zestaw wszystkich kontrolek dodanych do obiektu . Sam zestaw zwracany przez wła ściwość Control S kontrolki GroupBox ma domyślnie indeksowan ą właściwość dającą dostęp do kontrolek w zestawie. Zestaw jest uporządkowanywedług zasady pierwszy na wejściu - ostatni na wyjściu, czyli tak jak stos, a więc wartości indeksów właściwości dają dostęp do kontrolek w odwrotnej kol ejności niż ta, w której dodawaliśmy je do kontrolki GroupBox. Zaimplementuj funkcję SetVa lues ( l jako prywatną składową klasy Fo rm l w nast ępujący sposób:
II Ustawia wartose t j ako tekst przyci sków w kontrolce GroupBo x.
vo id Set Va lues(arr ay< i nt>A values, GroupBoxA groupBox ) {
Ar ray: :Sort (val ues ) ; II Sortuj e war/oś ci w kolejności rosnącej. i nt count = val ues->Lengt h - l ; for (l nt i ~ O : iCont rols ->Count ; 1++ ) safe_cast<But tonA>(groupBox ->Controls [ i ] )->Text = va l ues[count -i ] .ToString( ) ; Po posortowaniu tabli cy wartości zmiennej count przypisujemy wartość indeksu ostatniego elementu w tablicy. Następnie pętl a zapisuje w odwrotn ej kolejności wartości w postaci łań cucha do właściwości Text wszystkich kontrolek Button . Wyrażenie groupBox->Cont rols [ i ] daje uchwyt typu Cont rol A, odno s zący się do kontrolki , której indeksem w zestawie jest i. Rzutujemy go do typu But t on" przed uzyskaniem dostępu do właściwoś ci Text w celu ustawieniajej wartości. Nie musimy implementować możliwości funkcji Set Values ( l, korzystając z obiektu GroupBox. Wszystkie przyciski są jawnymi składowymi klasy Forml , więc możemy to zrobi ć "na piechotę" i bezpośrednio użyć każdej z nich do przypisania wartości właściwości Text. Będzie my potrzebowali przynajmniej dwóch różnych funkcji - jednej dla ,,kuponu" Lotto, a drugiej dla "kuponu" Euromillions, choć tę ostatnią wygodnie byłoby podzielić na dwie funkcje. Oto, jak mogłaby wyglądać taka funkcja dla "kuponu" Lotto : voi d Set NewValues(arr ayA values) (
Array ; .So rt t velues r : lott oVa lue1->Text = values[O] .ToSt ri ng ( ) :
1108
Visual C++ 2005. Od podstaw lot t oValue2->Text ~ values[lJ .ToSt ri ng( ); lot t oVal ue3->Text = va l ues[2J.ToSt ring( ); lot toVa l ue4->Text = val ues[3J.ToSt ring( ); lottoVal ue5->Text = va lues[4J.ToStr ing(); lot t oVal ue6->Text = values[5J.ToSt ri ng( ); Ta funkcja wykorzystuje uchwyt każdego przyci sku do ustawienia je go wła ściwości Text . Funkcje ustawiaj ące wła ściwoś ć Text dla kuponu Eurom illions mogłyby zo sta ć zaimplementowan e dokładni e w ten sam spo sób. Następn ie mu siałby ś zmienić procedurę pl ayMen u1te m_el i ck( ), aby w ywoływała te funkcje w celu przypisania wartości. Możesz
teraz ponownie skompilować Cw21_01 i sprawdzi ć , czy wszystko działa. Jeżeli nie literówek w kodzie, program powinien gene rowa ć ,,kupon" dla Lotto jak na rysunku 21.18.
pop ełniłeś żadnych
Rysunek 21.18
fil
A Winnon g App~c .tl on
Play LolIO
Liml1s
_
LI
~
Help
L(UlOlTUlionsl WebE.ge]
Veo es 1 lo 49
::
I
Liczby na przyciskach pojawiają s ię w kolejności rosnąc ej, więc wygląda to schludnie i ładnie . nie pojawiają s i ę w takiej kol ejności, może być tak dlatego, że nie dodawałeś przycisków do kontrolki Gr oupBox w uporządkowan y sposób. Jeżeli
Program tworzy
również
"kupon" dla loterii Euromillions, jak na rysunku 21.19 .
Gdy mamy już podstawowy zestaw funkcji , czas
rozwinąć
dalej
naszą aplikację .
Obsluga zdarzeń dla menu Umils W menu Lim its znajdują s i ę trzy elementy i będziemy dla każdego z nich potrzebowali procezdarzenia Cl i ck. Kliknij dwukrotnie każdy z elementów menu , aby utworzyć i zarejestrować procedury obsług i zdarzenia Cl i ck.
durę ob słu gi
Rozdzial21. • Aplikacje wykorzystujące Windows Forms RJsunek 21.19
~E A WinOIng App llcatlon
_
1109
u } Sel ecteolt e!Tl I ~ null pt r ) uoper ~ safe_cast (lottoUoperllst ->Select edltem): 1/ Jeżeli j est wybrany dolny limit. zap isz go.
if( lotto LowerL i st ->Select edlt em I ~ nul l ptr ) lower = safe_cast (lot toLower Ll st ->Select edlt em) : if (upoer - lower < 5)
r ::Oia logRes ult resul t = MessageBox: :ShowW'Upper l t mt t : " + uoper + L" Lower l i mit : " + lower L"\nUpper l imit must be at least 5 greate r that the lowe r l imit ." + L" \nTry Agai n ". L"Li mi t s Inval id". MessageBoxBut t on s . :OKCancel . Mes sageBoxlcon: :Error ): if(result == : :DialogResu1t : :OK) OialogRe sult = : Dia logResult : :None: else DlalogResult = : :DlalogResult : :Ca ncel:
+
Rozdział 21. III
Aplikacje wykorzystujące Windows Forms
1119
}
else {
upperL iml t lowe rLimit
~
~
upper ; lowe r:
trzecim arg umentem funkcj i Show() jest Mess ageBoxButtons: :Cancel , okno komuni katu ma teraz dwa przyciski, jak na rysunku 21.23 .
Ponieważ
Rysunek 21.23
111 A Win Oln c App tic ah on Play
Urr;t.
1:1 X
_
Help ~
Lotto
EwomirtoM: Set l lluits tor Lott o
vet ues
Valuas 1 to 49
~ ?.:. .~I
Selectlower limit value:
l.lpper Iirrit 29 Lower limit 35 Upper limit must be at leest 5 gledler that the lower ~mi t.
TryAgain
.
OK
II
OK
Anuluj w ncel
W procedurze obsługi zdarzenia ( l ick dla przyc isku OK w oknie dialo gow ym umoż liwi aj ą cym z mianę limitów prze chowujemy w zmiennej resu1t wartość zwr óco n ą przez funkcję Show( ). Typ zmiennej resul t musi zostać okreś lony z użyciem operatora zakresu. W przeciwnym razie zostanie zinterpretowany przez kom pilator jako właściwość Di a10gResult obiektu l ot t ot. imi ts Di al og i kod nie zos tan ie skompilowany. Jeżeli re sult zawi era wartość : : Di alogRes ult : OK, ustawiamy wartość właściwości Di al ogRes ult obiektu l otto l l mi t sDi al og na : :Di al ogRes ul t: ; None, co zapobiega zamknięciu okna dialogowe go i u m ożl i w i a zmianę limitu. W przec iwnym przypadk u ustawiamy wartość właściwoś ci Dia 10gRes ult okna dialogowego na : Di alog : ; Cance l , co ma taki sam efekt jak kl iknięcie przycisku Cance1 w okn ie dialogowym, czyli je zamy ka .
Obshloa zdarzenia elementu menu Heset Może my zaimp le me n towa ć p roce durę obsł ugi
l f(l ott oTab->Vi si ble) { II Przywraca domyś lne limity dla Lal/o.
lot toUs erMaximum = lott oUpperLi mit : lottoUserMi nimum = lottoLowerL imit ;
A
n astęp ujący
sender. System: :EventArgs
A
e)
spos ób :
1120
VislJal C++ 2005. Od podstaw l ott oLimits Dia log->UpperLimi t l ott oLimi ts Dialog->LowerLi mi t
~ ~
l ottoUppe rLi mit ; lott oLowerL imit ;
}
el se lf( euroTab ->Vis iblel { II Przywraca domyśln e limity dla Eurom illions.
euroUserMaximum = euroUpperLimit ; euroUserMinimum = euroLowerLimi t ; euroStarsUserMaximum = euroStarsUpperLimit : euroStarsUserMinimum = euroSt arsLowerLimit ; II Kod akt ua lizujący okno dialogowe dla loterii Euromillions.
Ta funkcja po prostu przywraca domyślne wartości limitów w polach obiektu Forml, a następnie odpowiednio aktualizuje właściwości obiektu okna dialogowego. Wciąż musimy dodać do tej funkcji kod, który będzie przywracał domyślne warto ści w niedodanym jeszcze do aplikacji oknie dialogowym obsługuj ącym wprowadzanie limitów dla loterii Euromillions. Możesz teraz ponownie skompilować program i spróbować zmieni ć limity dla loterii Lotto . Typowe okno aplikacji zostało przedstawione na rysunku 21.24.
Rysunek 21.24
:~ Ą WinnIn g Appli c.hon
PI"y
Lollo
Limit,
_
Ll X
Help
UlomiIlom Set Llluits for lotto Values
Value s 1 lo 49
Selecl lowel Irrot value:
Selecl upper jmil vehe:
ar. I
li Jak widać , automatycznie otrzymujemy pasek przewijania umożliwiający przewijanie listy elementów w polu listy. Zwróć uwagę, że przewijanie do danego elementu nie wybiera go. Aby wybrać element, musisz go kliknąć, zanim klikniesz przycisk OK. Wybranie Limits/Reset przywraca domyślne wartości limitów.
Dodawanie drugiego okna dialogOwego Tworzenie drugiego okna dialogowego, które będzie umożliwiało ustawienie limitów dla loterii Euromillions, będzie proste ; jest to ten sam proces co przy okazji pierwszego okna . Utwórz nowy formularz w projekcie poprzez naciśnięcie Ctrl+Shijt+A, aby wyświetlić okno
Rozdział 21.
• Aplikacje wYkorzystujące Windows Forms
1121
dialogowe Add New ltem. Nastę p nie wybierz kategorię Ul i szablon Wi ndows Form; jako nazwę podaj Eur oLi mi t sDial og. Ustaw wartości właściwości tego okna dialogowego podobnie jak poprzedniego: Właściwość formularza
Wartość do ustawienia
FormBorderStyle
FixedDia log
Cont rol Box
Fa lse
Min imizeBox
Fal se
MaximizeBox
False
Text
Set Euromi l li ons Limi t s Następni e
dodaj do okna dialogowego przyciski OK i Cance l. Ustaw właściwości Text przycisków na OK i Cance l , a ich właśc iwości (Name) na euroOK i euroCancel . Ustaw też wartośc i właściwości Dial ogResul t na OK i Cancel. Mając zdefiniowa ne przyciski, powróć do właści wo śc i okna dialogowego i ustaw wartości Accept Button i Ca ncel Button odpowiednio na euroOK i euroCancel . W celu nabrania dośw iadcze n ia w pracy z jak naj więks zą lic zbą kontrolek, zaniedbamy spójno ść aplikacji i nie użyjemy kontrolek Li stBox do obsługi wprowadzania danych, jak to było w pierwszym oknie dialogowym . W tym oknie dialogowym musimy umoż l i w i ć wprowadzanie górnego i dolnego limitu dla zestawu p ię c iu warto ści , a także dla zestawu dwóch liczb głów nych. Nie b ędzie to zbyt p i ękn a implementacja, ale abyś p oprac o wał z jak n aj w i ę k s zą l i czbą kontrolek, dla tych pierwszych użyjemy kontro lek Numeri cUpDown, a dla drugich kontrolek ComboBox. Dodaj te kontro lki oraz odpowiedni e kontrolki Labe l do okna dialogowego i u m i eść je w kontrolce Gr oupBox, j ak na rysunku 21.25 . Oczyw iś cie najp ierw musisz do dać kontro lki GroupBox, a następ n i e um ie s zc zać w nich pozostałe kontrolki . • x
Rysunek 21.25 Set EuronullionsLinłits
Set V. lues Limits Lewe Limit
Set StersLimits Lower Limit:
OK
lli3l
rn
Upper Limil
Uppef Limit:
149 [.: 31
rn
~ncel
1122
Visual C++ 2005. Od pOIlstaw Aby łatwo o kreśl i ć przeznaczenie kontrolek w poszczególnych grupach, wartość Text dla górnej grupy zos tał a ustawiona na Set Va l ues Li mit s, a dolnego pola grupy na Set Sta r s Li mi t s. Nie będ ziemy korzystać z obiektów GroupBox w kodzie, więc warto ść wł aściwości (Name) jest nieistotna. Ustaw wartości wł aściwoś ci Text poszczególnych kontrolek Label w sposób przedstawiony na rysunku 2 1.25. Warto ści właściwości (N ame) kontrolek Numer i cUpOown w górnej grupie powinny być ustawione na l owerVa l uesLi mits i upperVal uesLi mit s. Mo żesz ustawi ć wartości wyświetlane w tych kontrolkach , odpowiednio ustawiając wartoś ci właściwości Mini mumi Maxi mum. Te wartości dla kontrolki l ower Va l uesLi mi ts powinny wynosić odpo wiednio l i 44, warto ś ci właściwości Maxi mum i Mi nimumdla prawej kontrolki powinny wyn os i ć odpowiednio 49 i 6. Ustaw wartość właściwości Val ue kontrolki upper Val uesLi mi t s na 49; jest to warto ś ć początkowo wyświetlana w kontrolce. Ustaw też wartość True dla właściwości ReadOnl y obu kontrolek Numer i cUpDown, co uniemożliwi wprowadzanie warto ści z klawiatury. Kontrolkę Numer i cUpDown wykorzystujemy tu w bardzo prosty sposób. Można z mi e n i ać inkrementację i dekrementacj ę poprzez ustawienie wartoś ci wł aściwości I ncr ement . Wła ściwość I ncr ement jest typu Oecima l , więc można tu ust aw i ć także liczby niecałkowite .
Ustaw
wartości wł aściwości
(Name) dla kontrolek ComboBox w dolnej grupie na l owerSt ar sLi w kontrolce ComboBox można wprowadzić bardzo łatwo . Kliknij m ałą strzałkę zn aj d uj ącą s i ę w górnej prawej c zęści lewej kontrolki Com boBox, aby wyświetli ć menu przedstawione na rysunku 2 1.26.
Wybierz element menu Edit Jtems, aby wy ś w i e tlić okno dialogowe edytora String Collection Editor, przed stawion e na rysunku 21.27 . Rysunek 21.27 przedstawia wartości umieszczone w lewej kontrolce ComboBox. Dla kontrolki znajdującej s i ę po prawej wpisz warto ś ci od 2 do 9 włąc znie .
ComboBox
Rozdzial21. - Aplikacie wYkorzystuiące Windows Forms Rysunek 21.27
Sl rul g C oU e cłlon [ (Jitor
?
Enter the strlngs in the coJ1ection (on. per Ino):
------"----I 2
--'---"-----'------ ---
1123
X
- - ---,
3 1
5 6
7
BI
O\(
II
Canceł ~ I
Kontrolka ComboBox nie jest najlepszym rozwiązaniem w tej aplikacji, ponieważ oprócz wybierania z listy umo żliwi a również wpisywanie tekstu; chcemy, aby wartość limitu była wybierana jedynie z listy. Nazwa kontrolki, ComboBox, wzięła się stąd , że kontrolka ta łączy działanie kontrolki ListBox, umożliwiającej wybór z listy, z działaniem kontrolki TextBox, umożliwiającej wprowadzanie tekstu .
Pobieranie danych zkontrolek okna dialogowego Wartości
limitów z kontrolek będziemy pobierali w ten sam sposób jak w oknie dialogowym limitów dla Lotto. Dodaj nowe składowe do k1asy EuroL imit sOi al og, które będą przechowywały limity wprowadzone przez użytkownika:
umożliwiającym określenie
private : int lowerVa l uesLimi t : int upperVal uesL imit; i nt lowerStarsL imit : lnt upperSt arsL imi t : Bezpiecznie
będzie inicjalizować
te
składowe
w konstruktorze klasy:
EuroLimi ts Dialoq(void) :lowerVal uesLimi t Cl ) ,upperVal uesL imi t (50) ,lowerSt arsL imitCl) .upperStarsLimi t (9) Init ia l lzeComponent C) : II II D O ZR OBIENIA : wpisz tu kod konstruktora. II
} Będziemy także potrzebowali publicznych właściwości zdefiniowanych w k1asie okna dialogowego , aby limity były dostępne z obiektu okna aplikacji:
pub l ic: property i nt LowerValuesLi mi t
1124
Visual C++ 2005. Od podstaw
i nt get ( ) { retu rn l owerVal uesLimit: void set( int l i mi t ) {
lowerVal uesLimit ~ l imit : lowerValuesLimit s->Val ue ~ l imi t :
II Ustaw
według
wybo ru w Num eric UpDown.
II Ustaw
według
wyb oru w NumericUp Down.
} }
propert y int UpperVal uesLimlt {
i nt get( ) { ret urn upperValuesLimit : void set (int l imit) {
upperVa l uesLimit ~ l imit, upperVa l uesLimit s- >Val ue ~ limit: }
property int LowerStarsLimit (
i nt get() ( return lowerStarsLimi t: void set( i nt limit) (
l owerStarsLimi t = li mi t: lowe rStars Limit s->Selectedltem ~ l imit: II Ustaw według wybo ru w ComboBox. lowerStarsLimits ->Se lecte dlndex = II Ustaw indeks dla wybra nego elementu. l owerSt arsLimi t s- >F i ndStri ng(li mi t .ToString(» : }
property i nt UpperStars Limi t (
int get () ( ret urn upperSt ars Limit: void set( i nt l imi t) (
upperSt arsLimi t = l imit : upperStarsLimits ->Selectedlt em ~ l imit : II Ustaw według wyb oru w Comboliox. upperSt arsL imt t s->SelectedIndex = II Usta w indeks dla wybra nego elementu. upperSta rsLimits ->FindStri ng(li mit. ToSt r ing( » :
Funkcja get O w każdej właściwości zwraca wartość odpowiadającą prywatnej składowej klasy okna dialogowego. Funkcja set ( ) ustawia wartość składowej oraz aktualizuje kontrolkę w oknie dialogowym, aby wartość ta była wartością wybraną. Wartość właściwości Se l ecte dlnde xjest indeksem wybranego elementu. Ustawiamy ją za pomocą funkcji FindString O kontrolki ComboBox, która zwraca wartość indeksu dla pierwszego wystąpienia argumentu w zestawie e1cmentów kontrolki . Wartość z tej pozycji jest początkowo wyświetlana w kontrolce. Dodaj do klasy EuroLi mi tsDi al og procedurę obsługi zdarzenia C1 i ck dla przycisku OK poprzez dwukrotne kliknięcie przycisku w oknie Design. Nie musimy implementować procedury obsłu gi dla przycisku Cancel . Zaimplementuj procedurę obsługi przycisku OK w następujący sposób:
Syste m: :Voi d euroOK_C li ck(System : :Object A sender . System: :EventArgsA e) {
: :DialogRes ult result ;
Rozdzial21. • Aplikacje WYkorzystujące Windows Forms II Pobierz limity dla
1125
wartoś ci .
i nt va luesLower = Oec ima l : :Tolnt 32(lowerVal ues Li mi ts ->Va l ue); i nt va luesUpper = Oecima l: .Tolnt 32(upperVal uesLi mit s->Val ue); if(valuesUpper - va l ues Lower < 4) II Sprawdź zakres. {
result = MessageBox: :Show( t his . II Za kres s ię nie zga dza , wię c "Upper val ues l imit: "+val uesUpper + II wyświetl okno komunikatu . " Lower val ues limi t: "+ val uesl.ower"\nUppe r values l imit must be at least 4 greater t hat t he lower l imit. "+ "\nTry Agai n.". "L imi t s Invali d". MessageBoxButtons: :OKCancel. MessageBoxlcon: :Error ); if( result == : : Oi alogResu lt: :OK ) II W okni e dialogowym zosta ł kliknięty przy cisk Oi alogResult = :: Oi alogResult : :None; II więc zapo biegn ij zamkn ięciu
OK.
II okna dialogowego.
else II W okn ie komunikatu został kliknięty przycisk Cance l, OialogResult = : :OialogResult: :Cancel; II wię c zamkn ij okno ret urn;
dialogowe .
II Pobi erz limi ty dla liczb glównych.
inl slars Lower = lowerStarsL imits->Select edlt em == nu llptr lowerSta rsLimit : Int32: :Pa rse( lowerSta rsLimits ->Select edlte m->ToStri ng()); i nt sta rsUpper = upperSt arsLimit s->Select edltem == null ptr ? upperStarsL imit : Int32: :Parse(upperSt arsLimi t s- >Select edlt em- >ToSt ring()); if (st arsUpper - sta rsLower < 1) II Sprawdź zakr es. {
result = Mes sageBox: :Show( t his . II Zakres s ię nie zga dza, więc "Upper st ars l imit : "+starsUpper + II wyś wietl okno ko muni katu. " Lower sta rs l imit: "+ sta rsLower+ "\nUpper sta rs l tmi t must be at least l great er that the lower l imit ."+ "\nTry Aga in .". "Limits Inval id". MessageBoxButtons: :OKCancel. MessageBoxlcon: :Er ror); if (result == : : OialogResult: :OK) II W okni e dialog owym zosta ł kliknięty przy cisk OK, Oi alog Res ult = :. OialogResult : :None: II więc zapo bieg nij zamknięciu II okna dialogowego.
el se II W oknie komunikatu został kliknięty przycisk Cancel, Dialog Res ult = : . Oi alogResu lt : :Cancel ; II więc zamknij okn o dialogowe. } II Zap isz no we limity.
Val ue kontrolki NumericUpOown zwraca wartość typu Decima l. Aby przekonwertotypu Int32, przesyłamy jąjako argument do funkcji statycznej Tol nt32 () w klasie Deci ma l . Wartość zwrócona przez tę funkcję jest automatycznie pozbawiana właściwości obiektowych, więc może być zapisana jako zmienna typu i nt. wać ją do
1126
VisIlai C++ 2005. Od podstaw Wartość
zwracana przez właściwość Sel ected Item kontrolki ComboBox jest typu Object A, więc sprawdzamy, czy nie jest pusta . Jeżeli jest, przypisujemy lokalnej zmiennej bieżącą wartość zapisaną w obiekcie okna dialogowego; jeżeli nie jest pusta, przechowujemy wartość reprezentowaną przez właściwość Selectedltem. Nie możemy bezpośrednio zapisać tej wartości, ale wywołanie funkcji ToStri ng() obiektu tworzy łańcuchową reprezentację obiektu, którą można wtedy przekonwertować do typu i nt za pomocą statycznej funkcji Parse() klasy Int32. dla
bezpieczeństwa
Będziemy potrzebowali prywatnej nowego okna dialogowego:
składowej
klasy Forml,która
private : EuroL imi tsOialogA euroLimi tsOialog; II Okno dialogowe
będzie przechowywała
umożliwiające
uchwyt
ustawienie limitów
II dla loterii Euromillions.
Dodaj poniższy kod na końcu konstruktora klasy Formi, aby utworzyć obiekt okna dialogowego i zaktualizować właściwości dla wartości limitów dla liczb głównych:
euroLimitsDialog = gcnew EuroLimitsDialog; euroL lmitsDialog->LowerStarsL imit = euroStarsLowerLimit; euroLimitsDialog->UpperStarsLimit = euroStarsUpperLimit; Dzięki ustawieniu właściwości LowerStarsL i mit i Uppe rStarsL imit mamy pewność, że w kontrolkach ComboBox wyświetlane są te wartości, gdy okno dialogowe zostaje wyświetlone za pierwszym razem. Jeżeli w kontrolce ComboBox nie jest wybrany żaden element, początkowo nic nie jest wyświetlane.
Nie zapomnij
dodać
do Forml .h dyrektywy #i ncl ude dla definicji klasy Eurol imitsDi al oq:
#include "EuroLimitsOialog .h"
Wyłączanie kontrolek wejścia Gdy zostanie kliknięty element menu Limits/Upper, chcemy zapobiec wprowadzaniu dolnego limitu, a gdy zostanie kliknięty element menu Limits/Lower, chcemy zapobiec wprowadzaniu górnego limitu . Dodaj w tym celu kilka funkcji składowych do klasy EuroL i mi tsD i alog:
lowerVal uesLi mi t s->Enabled = fal se: lowerStarsL imi t s->Ena bled = fal se: W artość właściwości
En abl ed kontrolki określa, czy dana kontrolka jest włączon a. Wartość true włącza kontrolkę, a wartoś ć f al se ją wyłącza, więc użytkownik nie może z nią pracować. Funkcja Set LowerEnabl ed( ) wyłącza kontrolki używane do wprowadzania górnych limitów i włącza te umożliwiaj ące wprowadzanie dolnych limitów. Dzi ałanie funkcji SetUpperEnab1 ed( ) jest odwrotne.
Aktualizacja procedur obs~Jgi elementów menu Limils Ostatnim krokiem potrzebnym do ukończenia obsługi wprowadzania limitów dla loterii Euromillions j est aktualizacja w klasie Forml procedur o bsługi zda rze ń Cl ick dla elementów menu Limits. Procedura ob sługi dla menu Vpper powinna zostać zmieniona w następujący sposób:
System: :Vo id upperMenult em_Cl lck(System. :Object
A
sender . Sys tem : :EventArg s
A
e)
{
: :Dial ogResul t result : if (lottoTab ->Vi si ble ) (
l ott oLlmi ts Dlalog->SetUpperEnabled(): resul t = lott oL lmi t sDlal og->ShowDial og(t hi s) ; if( result == : : DialogResult : :OK) { II Uaktualnij limity
użytkownika
z
właściwości
okna dialogowego.
l ott oUserMaxi mum = lot t oLimi t sDialog->UpperLimi t; l ot t oUserMinimum = lot t oLimi t sDialog->LowerLimi t ; } )
el se if (euroTab ->Vi si ble) (
euroLimltsDia log ->SetUpperEnabled() ; result = euroLi mit sDial og ->ShowDia log(t his) : if( result ~~ : :DialogResult : :OK) (
Zmienna lokalna res ul t j est używana w obu instrukcjach i f , więc teraz jest deklarowana na początku funkcji. Po włączeniu odpowiednich kontrolek w oknie dialogowym poprzez wywołanie funkcji Set Upper Enab1ed() obiektu okna dialogowego wyświetlamy modaln e okno dialogowe. Jeżeli użytk ownik zamkn ie okno kliknięciem przycisku OK, zapisujemy wyniki dostępne dzięki właściwościom obiekt u okna dial ogowego. Zmi any, które należy bardzo podobne:
wprowadzi ć
do procedury
obsługi
zdarzenia elementu menu Lower,
są
1128
VislJal C++ 2005. Od podstaw Syst em: :Void lowerMenult emCl ickCSyst em: :Object ( : :OialogResult resul t : i f Clott oTab->Visi ble)
A
sender. System: :Event Args
A
e)
(
lottoLi mit sDia log->SetLowerEnabled(); result = lotto LlmitsDialog->ShowDi alog(t his) ; ifC result == : :Di alogResult : :OK) ( II Uaktualnij limity użyt ko wnika z wiasciwosci okna dialogo wego.
lottoUserMaximum= lot t oLimit sDialog->UpperL imit : lott oUserMi nimum = lottoLimit sDialog->LowerLimit ; else if CeuroTab->Visl ble) (
euroLlmits Dl alog->SetLowerEna bled( ); result = euroLimits Di alog ->ShowDialog(thi s) ; if (result == : DialogResult :OK) {
Zasad a dzi ałania je st tu taka sama jak w poprzedniej procedurze
o bsług i.
Implementacja elementu menu Help/Abuut Gdy poznałeś już kl as ę MessageBox, czeka nas proste zadanie. Wystarczy, że wyśw ie tl i my okno komunikatu, gdy zostanie kliknięty element menu Help/About . Gdy zostanie klikn ięty element menu, procedura wione na rysunku 21.28.
ob sługi wyświetli
okno komunik atu przedsta-
ObskJga kliknięcia przycisku Kli kn i ęcie
w przyci sku inną losowo ta musi być różn a od p o zo st ałych w arto śc i , a także ró żn a od w cześ niej wy świetlanej wartości. Dobrym rozwiąz aniem byłoby przedstawieni e całego zestawu w sposó b posortowany; może to s powodować, że nowa wartość będzie si ę ju ż znaj dowała na innym przycisku, ale je st to lepsze ni ż sytuacja, w której nie mielibyśmy ułożo nych wartości . przycisku pow inno
zas t ę p ować warto ś ć wy świetl an ą
wybran ą wartością. Oczyw i śc i e warto ś ć
Proces ob sług i kliknięc ia przycisku będzie ten sam dla wszystkich przycisków, więc b ędziemy mogli zaos zczędzić sobie pisania kodu poprzez utworzenie zgeneralizowanej funkcji zajmuj ą cej się tym zadaniem. Zdefiniuj prywatn ą funkcję składową klasy FormI, która będzie losowała now ą warto ś ć dla okreś l oneg o obiektu Button z tablicy przycisków:
Rozdział
Rysunek 21.28
21. • Aplikacje wwkorzwstuiące Windows Forms
~ A Win nm g Application Lim~ ,
Play
_
1129
Ll X
Help
Ab out A Winnmg Appli c4t lOn
~
:&:
le) COPY'ighllvor Hetten
OK
II Losuj e no wą
war/ość
dla przycisku.
różną
od aktualny ch
war/ oś ci p rzy cisku.
void S et New V a lue( B ut t on button, a rray <But to nĄ > Ą buttons . i nt l ower Li mi t . int upperLi mi t ) Ą
i nt l ndex = O; II Indeks przycisk u II w tablicy p rzycisk ów przechowującej war/ości przycisk ów . ar ray< i n t > Ą
II Pob ierz
val ues = gcnew array(but tons ->Lengt h) ;
war/ośc i
z przycisków i
znajdź
indeks przycisku .
for ( i nt i = O : l < values->Length ; i ++) { val ues [ i J Int32; ;Parse (buttons[ i] · >Text ) ; II Jeżeli
bieżący
lichwy/ jest taki sam jak przy cisk, zap isz
II Pobierz bieżącą indeks u.
wartos ć
p rzycisku .
wartoś ć
i f (button == but t ons[ i J ) i ndex = i ; } i nt newVal ue
= O; II Zapisz nową war/ość przycisku. II Sprawdź, czy jest różn a od war/oś ci pozos tałych p rzy cisków. for ( ; : ) II Pętla, aż znajdzie s ię odp owiednia war/oś ć.
(
newValue = random- >Next (l owerLimi t . upperLimi t ) ; /r Losuj wa rtos ć .
i f( IsVal id(newVal ue . values . val ues- >Length ) ) I/Jeżeli jes / dobra ... break; 11...zakończ pętlę.
} val ues [i ndex] = newVal ue ; II Przechowaj nową war /ość w indeks ie. Ar ray : :Sort (val ues ) : II Posortuj war /ośc i
for ( i nt i = O : i < val ues ->Lengt h ; i ++ ) II i ustaw war/ośc i
but t ons[i ] ->Text = val ueS [l ] .ToStr ing() : /r jako tekst przy ciskow.
Pierwszymi dwoma parametrami funkcji są przycisk, dla którego losujemy nową li czbę, i tablica przyc isków w grupie, do której należy ten pierwszy. Kolejne dwa parametry określają górny i dolny limit wartości. Bieżące warto ści przyci sków w tablicy są przechowywane w tablicy wartości w pierwszej pętli . Ta pętla wyszukuje również wartość indeksu w tablicy buttons dla uchwytu But t on", który j est pierwszym argumentem. Potrzebuj emy go, aby wiedzieć , który elem ent tablicy val ues ma zostać z a stąp iony .
1130
Visual C++ 2005. Od podstaw jest tworzona w n ieskończonej pętli f or . Jest to ten sam mechanizm, jakiego do tworzeni a warto ś ci dla przycisku za pierw szym razem . Po odn alezieniu odpowiedniej w arto ści zapisujemy j ą w tablicy val ues. Następnie sortujemy elementy tej tabli cy przed zapisaniem ich jako w alioś c i właśc i wośc i Text przycisków w tablicy buttons. Będziemy mogli u żyć tej funkcji do obsłu gi zd arze ń C1i ck dla wszy stkich przycisków.
Nowa
wartość
u żyl i śmy
J eżeli
jeszcze tego nie z ro b i łeś, kliknij dwukrotnie pierws zy przycisk w zakładce Lotto , aby dla niego procedurę obsług i zdarzenia C1 i ck. Gdy n ac i śnie s z Enter, w kodzie zostanie wprowadzona nowa nazwa. Zmieniłem wartoś ć na Io t tovalueCl t ck. utworzyć
Możesz zmienić procedurę obsługi
zdarzenia C1i ck, aby wyw oływała funkcję Set NewVal ue( J, do klasy For ml: Sys t em: .Void lott oVa1ue C1ick(System : :Obj ect A sender, System: :Event ArgsA e)
którą
przed
chwilą dodali śmy
-
(
ButtonAbutt on II Utwórz
tab licę
=
safe_cast<But t onA>( sender) ;
uch wyt ów przycisków.
array<ButtonA>A buttons = {lott oVa1uel. lot t oVa1ue2. lott oVa1ue3. lottoVa1ue4. lot toVa1ue5. lot t oVa1ue6}; II Zas tąp
wartość
przycisku.
SetNewVa 1ue(but ton. but t ons . lott oUserMinimum. lot toUse rMaximum) , Dostępno ść
funkcji Set NewVal ue( ) sprawia, że ta procedura jest bardzo prosta. Pierws za instrukcja zapisuj e uchwyt klikniętego przyci sku. Pierwszym parametrem procedury obsłu gi zdarzenia jest uchwyt obiektu, który spowodował zdarzenie, więc wystarczy go tylko rzutować do odpowiedniego typu. Następnie układamy z uchwytów przycisków tabli cę i wywołu jem y kol ejn ą fu n kcję - to wszystko! W ciąż
musimy poradzi ć sobie ze zdarzeniami C1 t ek dla innych przycisków w zakład ce Lotto, lecz nie wymaga to ju ż pisania żad n e go kodu . Otwórz okno Properties drugiego przycisku i kliknij przycisk Events. Gdy klikniesz wartość zdarzenia C1i ck, pojawi się lista istniejących procedur obsłu g i (rysunek 2 1.29). Jeżeli wybierzesz z listy l ottoVal ue_C1i ck, procedura obsług i zdarzenia dla pierwsz ego przyci sku będ zi e zarejestrowana również jako procedura drugiego przycisku. Powtórz ten proces dla pozostałych czterech przycisków w zakładce Lotto, aby jedna procedura obsługi była wywoływana w odpowiedzi na zdarzenie C1i ck dla wszystkich przycisków w tej zakładce.
Utworzenie procedury obsługi zdarzeń C1i ck dla przycisków zakładki Euromilli ons będzie równie ż bardzo łatwe . Kliknij dwukrotni e pierwszy z pię ciu przycisków w grupie Val ues , aby utworzyć procedurę ob sługi. Otwórz okno właściwoś ci przycisku i zmień warto ś ć dla zdarzenia C1 i ck na euroVal ue_Cli ck. Następni e zmodyfi kuj kod procedury w następujący sposób: Syst em: :Void euroVa 1ue_C1i ck(Syst em : :Object A sender. Syste m: :EventArgsA ej (
But tonA button = safe cast <ButtonA>(sender ); array<But t onA>Abut t ons = (euroVa1uel. euroVa1ue2. euroVa1ue3. euroVa1ue4. euroVa1ue5 ) ; Set NewValue(button. buttons. euroUserMinimum. euroUserMaximum);
Funkcja ta d ziała dokładnie w ten sam sposób jak w przypadku przycisków Lotto. Tablica zawiera uchwyty dla pięciu przycisków w grupie wartości , a funkcja SetNewVal ues() zajmuje się resztą. Gdy otworzysz okno właściwości dla pozostałych przycisków, będziesz mógł wybrać tę funkcję, aby odpowiadała na zdarzenia C1i ck dla tych przycisków . Upewnij się , że wybrałeś euroVal ue_Cl ic k, a nie lottoV alu e_Cl i ck! W ten sam sposób postępuj z przyciskami w w następujący sposób:
Syst em: :Void euroSta r Cl ickC System: -
{
B u tt on Ą
but t on ~
zakładce
: Ob j ect
Ą
Euromillions. Zaimplementuj
sender . Syste m: : Ev entArgs
Ą
procedurę
e)
s a f e_ca s t< B u tt o n Ą>(s e n d e r ):
but tons = { euroSt arl . euroStar2 }: Set NewVal ueCbut t on. butto ns . euroSt arsUserMinimum . euroSt arsUserMaximum) : a rray Ą
Ustaw euroStar_el i ck jako procedurę obsługi zdarzenia C1i ck dla drugiego przycisku i po wszystkim . Jeżeli teraz ponownie skompilujesz przykład, będziesz mógł losować wartość dla dowolnego przycisku, po prostu go klikając . Pozostało nam jeszcze umożliwić użytkownikowi samodzielne wpisywanie wartości dla przycisku.
Reagowanie na menu kontekstowe Kliknięcie prawym przyciskiem myszy przycisku będzie wyświetlało menu kontekstowe z jednym elementem - Choose. Gdy użytkownik kliknie ten element, program będzie wyświetlał okno dialogowe, umożliwiające wprowadzenie odpowiedniej wartości. Kliknij nazwę w menu kontekstowym w zakładce Design dla Forml, a następnie kliknij dwukrotnie element menu, aby utworzyć dla niego procedurę obsługi zdarzenia Cl : ck.
1132
Visual C++ 2005. Od podstaw Pierwszą kwestią jest ustalen ie, w której grupie znajduje s i ę kl iknię ty przycisk. Każda grupa przycisków znajduje si ę w oddzielnej kontrolce GroupBox, a klasa Gro upBox ma właściwość Control s, zwracaj ąc ą referen cj ę do obiektu typu Cent ro l : :Cont ralCo11 ect i on, reprezentują cego zestaw kontrolek w polu grupy. Klasa Contro 1: :Cont ro 1Co11ect io n definiuje funkcję Cont a i ns ( ), która zwraca tr ue, j eżel i kontrolka prz esłana jako argument tej funkcji znajduje się w zestawie, a w przeciwnym razie zwraca f al se. Dzięk i temu m ożemy określ i ć , do której grupy przycisków nal eży przycisk powodujący zdarzenie C1i ck. Zarys implementacji procedury obsługi ma następującą po stać: System : :Void chooseVa lue Cl ick(Syst em: :Object A sender . System : :Event ArgsA e)
f
-
II Pobierz przycisk klikn ięty w celu
wyświe tlenia
menu kontekstowego.
if( lottoVal ues->Contro l s->Cont ains (theButton)) { II Przycisk znaj duj e s ię w grup ie W ito...
}
else i f (euroVal ues->Cont rol s->Cont ai ns(t heBut ton)) { II Przycisk znajduje s ię w g rup ie Values...
}
el se l f (euroSt ars->Cont rol s->Cont ains (t heBut t on)) { II Przycisk znajduje się w g rupie Stars ..,
}
To rozwiązuje nam problem zw iązan y z określeniem grupy , do której należy przycisk, przynajmniej w zał ożen i ach . Ale wciąż mamy problem - jak dowiedzieć s i ę , który przycisk został klikn ięty prawym przyci skiem myszy w celu wyświetleni a menu kontekstowego? Gdy zostanie klikn ięty element menu Choose, wywoływana jest procedura obsługi chooseVa l ue_el i ck, więc parametr sender tej procedury określ a element menu , a nie przycisk. Potrzebujemy procedurę , która będzie reagowała na samo kl ikn ięci e przycisku, i możemy j ą utworzyć poprzez dwukrotne kliknięcie buttonCont ext Menu w panelu Design dla FormI. Uzupełnij kod tej funkcji w nast ępujący sposób: Syst em : :Void buttonContextMenu_Opening(System: :Obj ect A sender , Syst em: :Component Model : 'Cancel Event ArgsA e)
context But t on = safe cast <But t onA>(buttonCont extMenu->SourceCont rol ) ; To rzutuje uchwyt sender na typ Butt on'" i przechowuje go w s kładowej cont ext Button klasy FormI. Poniew aż w tym przypadku zdarzenie wystąp iło dla menu kontekstowego, parametr sender określa komponent, który został kliknięty w celu wyśw ietlenia menu. Oczywiście musimy jeszcze dodać zmienną contextButton j ako prywatną składową klasy Forml:
privat e: II Przycisk. który zos ta ł kliknięty prawym przy ciskiem w celu wyś wietlen ia menu kontekstowego .
But t onA cont ext But t on; Pozostaj e nam
zast anowić się,
co
będziemy
dalej
robi ć .
Rozdzial21. • Apl.ikacje wYkorzystujące Windows Forms
1133
Sposób obslUgi elementu menu Choose Proces reagowania na k liknięcie elementu menu Choose grupac h przyc isków i mo że m i eć następujący przebieg: 1
Wyświetlenie
okna dialogowego
2. Sprawdzenie p oprawnośc i od po zos tałyc h .
może być
ten sam we wszystkich
umożliwiającego wprowadzenie wartości.
wartości
- czy li czy znąjduje
się
w zakresie i jest różna
8.
Wyświ etlen i e okna komunikatu, jeżel i wartość nie jest poprawna, i um ożl iwi eni e ponownego wprowadzenia wartości lub za mkn i ę c i e okna dialogowego.
..
Jeżeli wartość jest
poprawna, aktualizacj a n o wą wartośc ią przycisku prawym przyciskiem myszy .
k likniętego
Zacznie my od utworzenia nowego formularza okna dialogowego.
Tworzenie formularza okna dialogowego N aciśn ij
Ctrl+Shift+A, aby wyśw ie t lić okno dialogowe Add New Ite m, a nas tę pnie wybierz Ul i szablon Windows Form. Jako n azw ę formularza podaj User Val ueOi al ag i kliknij przycisk Add. Otwórz okno właściwości nowego formularza poprzez n aci śnięci e klaw isza F4 i ustaw właściwośc i okna tak, aby by ło oknem dialogowym. Ustaw wartości właściwośc i Con tra l Box, Mi nimi zeBox i Maxi mi zeBox na Fal se, ajako właściwość Text wpisz User Val ue I nput. kategori ę
Dodaj przyciski OK i Cancel, a takż e kontrolki Label i TextBox, jak na rysunku 21.30.
Rysunek 2130
:; x~ 1 U. erValue Olalog.h [De.ign] + ~;;.;:;::;"" EuroU·mi l ;;:;IaI sD""~ Q9 . h __-'-'-"-'~--"'=--':"""' ~ User Value tnput Dlaloc
D
.
. Enle~ ~ u r value between 1 and 50 .
OK
Cancel
Ustaw wartości właśc i wo ś ci Text i (Name) przycisku OK na OK, a wartość właściwości Oial ogResult na OK. Wa rtością w łaściwośc i Text , (Neme) i Oi al ogResult dla przycisku Cancel powinno być Cancel. Ustaw wartość właściwo ści (Name ) kontrolki TextBox na t extBox, a warto ś ć jej właściwości Text Al i gn na Cente r . Nazwą kontrolki Label może być la bel , a jej wła śc iwość Text m o że być dowolna, p oni eważ i tak bę dz ie my j ą zm i en iać w kodzie.
1134
Visual C++ 2005. Od podstaw Wyświet l jeszcze okno właściwości formularza i ustaw odpowiednio na OKi Ca nce l .
wartości
AcceptButton i Cancel Button
Rozwijanie klasy okna dialogowego Wartość chować,
wpisana do kontrolki Text Box musi być dostępna dla obiektu FormI, dodaj do klasy UserVal ueOi al ag właściwość :
więc
aby ją prze-
pub l i c: property int Val ue: Jest to przykład najzwykJejszej właściwości skalarnej, więc funkcje get( ) i set ( ) są dostarczane domyślnie.
Obiekt okna dialogowego powinien znać ustawione limity, ponieważ procedura obsługi przycisku OK w klasie okna dialogowego będzie sprawdzać, czy wprowadzona wartość jest poprawna . Z tego samego powodu obiekt okna dialogowego musi wiedzieć, jakie są aktualne wartości w przyciskach, aby dopilnować , żeby się nie powtarzały. Dodamy więc kolejne trzy właściwości do klasy Use rV al ueOi al ag, aby przechować dane:
publ ic: property int LowerLimit : property int UpperLi mi t : property array " Va1ues :
II Aktualne
war/oś ci przycisków.
Obiekt FormI musi mieć możliwość zmiany właściwości Text kontrolki l abel, w zależności od limitów mających zastosowanie dla danego przycisku, gdy wyświetlone zostaje okno dialogowe; dodamy w tym celu publiczną składową do User Va l i dOi al ag:
publ t e : void SetLabelText (i nt lowe r . int upper ) {
label ->Text
=
L"Ent er your va l ue between " + lower +L" and" + upper :
} Mógłbyś również pob ierać
ściwości musiałyby być
pozbywamy
się
limity z właściwości obiektu okna dialogowego, ale wtedy wła zawsze ustawiane jako pierwsze . Używając parametrów dla limitów,
zal eżności .
tej
Możemy utworzyć prywatną składową
obiekt okna dialogowego w konstruktorze klasy Form, ale musimy tej klasy, która będzie przechowywała uchwyt:
dodać
private : UserValueDialog userValueDialog: A
Będziemy także nagłówkowym
Dodaj
potrzebowali Forml .h.
poniższą linijkę
userVal ueD ialog
=
dyrektywę
#i ncl ude dla pliku UserValueDialog.h w pliku
kodu do konstruktora, aby
gcnew UserVal ueDi alog:
utworzyć
obiekt okna dialogowego:
Rozdział 21.
• Aplikacje wrkorzrsluiące Windows Forms
1135
KJiknij dwukrotni e przycisk OK w formularzu User Va l ueDi al og, aby utworzyć pro cedurę obsł u gi zdarzenia Cl i ck dla przycisku. Funkcja b ędzie pobierała w artość wpisaną do kontrolki Text Box i sprawdzała , czy m ieś ci się ona w limitach i jest różna od bieżącego zestawu wartośc i. Jeżel i z jakiegoś powodu warto ś ć nie b ędzie poprawna, funkcja będz i e wyśw ie tlała okno komunikatu. Oto implementacja tej funkcji :
System : :Voi d OK_C l ick(System : :Obj ect
A
sender . System. :EventArgs
A
e)
(
: :DialogResult result : tf (St r i ng: :IsNu11 OrEmpty(t extBox->Text i ) {
result = MessageBox: :Show(t his . L" No input - ente r a val ue." , L" I nput Error ", MessageBoxButt ons: :RetryCancel , MessageBoxlcon: :Error): if(result = = : :DialogResult: :Ret ry) Di alogResult = . :Dl alogResult : :None: e l se II ...w prz eciwnym razie... Dial ogResult = : :Di alogRes ult : :Cancel; ret urn:
II Zapisz wartość zwróconą przez Showi). II Sprawdź, czy łań cuch nie jest pusty lub null.
II Jeżeli zostal klikn ięty przycisk Retry , II ...nie zamy kaj okna dialogowego ... II ...zamknij okno dialogowe.
}
lnt va l ue = I nt 32: :Parse(t extBox->Text ) ; II Pobierz wartoś ć z pola tekstowego. boa l va l id = true : II Okreś la, czy wartoś ć j est pop rawna. for each(int n in Va l ues) II Porównaj wprowadzo ne inf ormacje z pozostałymi wartościami. if(val ue ~= n) II Jeżeli są te same... {
vali d = fal se: break:
II ...j est niepoprawna. II Zakończ p ętlę.
} II Sprawdza limit i wynik poprzedniego testu.
if ( I va l id I I val ue
UpperLi mit)
{
result ~ MessageBox: :Show(t his. L"lnput not va l ld. " + L"Val ue must be from ': + Lowerll mit + L" t o " + UpperLimit + L"\ na nd must be different from existi ng val ues .", L"lnput Error". MessageBoxBut t ons: :Ret ryCancel . MessageBoxlcon: :Error) ; if( result == : :Dia logResult · :Retry) DialogResult = :DialogResult : :None ; else DialogResult :Di alogResult: :Cancel: }
els e Val ue
~
val ue;
II Zapisuje wprowadzoną wartość do w łaś c iwośc i .
Okno komunikatu jest wy świ etl ane , jeżeli właściwo ść Text pola tekstowego jest pust a lub wynosi nu11. Okno komunikatu zawi era komunikat o błędzie i ma przyciski Retry i Cance l zamiast OK i Cancel. Jeżeli zostanie kliknięty przycisk Retry, oznacza to, że użytkownik chce ponowi ć próbę wprowadzenia wartośc i , w ięc zapobiegamy zamknięciu okna poprzez ustawienie wartośc i właściwości DialogResu 1t na : .Dt al oqkesul t : :None. U żytkownik może jeszcze
1136
Visual C++ 2005. Od podstaw kliknąć
tylko przycisk Cancel w oknie komunikatu i w tym przypadku ustawiamy
właściwość
Di alogResul t obiektu okna dialogowego na : : Di alogResul t : :Cancel, co ma ten sam efekt jak kliknięcie
przycisku Cancel w oknie dialogowym.
Właściwość
Text kontrolki Text Box zwraca uchwyt typu St ri nq". Konwertujemy go na liczbę poprzez przesłanie uchwytu do statycznej funkcji Parse() w klasie Int32. Porównujemy wartość z pola tekstowego z elementami tablicy val ues, która reprezentuje aktualny zestaw wartości przycisków. Nowa wartość powinna być różna od nich wszystkich, więc jeżeli okaże się, że jest taka sama, przypisujemy zmiennej val i d wartość fa l se i kończymy pętlę. całkowitą
Warunek instrukcji i f znajdującej się za pętlą for each porównuje wartość z limitami i bieżącą zmiennej val i d, łącząc te warunki operatorem OR. Jeżeli jedno z tych trzech wyrażeń nie jest prawdą, cały warunek ma wartość fa l se i wyświetlamy okno komunikatu. Działa ono w ten sam sposób co poprzednie okno komunikatu i zawiera komunikat o błęd zie i przyciski Retry oraz Cancel. Jeżeli wartość jest poprawna, zapisujemy ją we właściwości Va l ue obiektu okna dialogowego, dzięki czemu może zostać pobrana przez procedurę obsługi zdarzenia w obiekcie Form, który rozpoczął cały proces . wartością
ObshJga zdarzenia Click dla menu Choose Ukończymy
teraz szkielet procedury obsługi chooseVal ue_Cl i ck() dzięki możliwościom, które do klasy UserVa l ueDi alog. Uchwyt przycisku klikniętego prawym przyciskiem myszy jest już zapisany w składowej contextButton, ponieważ procedura buttonConte xt Menu_Openi ng() została wykonana wcześniej . Syst em : ' Void chooseVal ue_CllckCSystem: :ObJ ect A sender . Syst em : .EventArgsA e) dodaliśmy
(
arrayA va lues: array<ButtonA>A theButt ons. II Sprawdź, czy przycisk
należy
II Tablica przechowująca aktualn e II Uchwyt tablicy przycisków.
wartości przycisków.
do grupy Lotto.
if ClottoVal ues->Controls ->Conta l nsCcontextButton)) { II Przycisk należy do grupy Lotto ...
array<ButtonA>A buttons = {lottoVal uel. lottoVal ue2 . lottoValue3 . lottoValue4. lottoVal ue5. lottoVal ue6}; II Zapisz uchwyt tablicy w zewnętrznym zakresie. t heButtons = buttons: va l ue s ~ GetButtonVa l ues Cbuttons) : II Pobierz tabli cę wartości przycisków. II Przygotuj okno dialogowe do
wyświetlenia.
userValueO ial og ->Va lues = values = GetButtonVa luesCbuttons) ; userValueO ial og->LowerLimit ~ lott oUse rM inimum. userValueDialog->UpperL imit ~ lott oUse rMaximum ; userValueD ialog->SetLabelTextClottoUserM inimum. lottoUserMaximum) : } II Sprawdź, czy przy cisk
należy
do grupy euroValues.
else ifCeuroVal ues ->Controls ->Contai ns(contextButton)) { II Przycisk należy do grupy euroValues...
array<ButtonA>A but t ons ~ {euroVal uel . euroValue2. euroVal ue3 . euroValue4. euroValue5}; theButtons = buttons: II Zapisz uchwyt tablicy w zewnętrznym zakresie. va lues = GetButtonVa l uesCbut t ons ): II Pobierz tablicę wartości przycisków. II Przygotuj okno dialogowe do wyswietlenia .
Rozdzial21. • Aplikacje wYkorzystujące Windows Forms
el se if(euroStars->Control s->Cont ains(cont extBut t on )) { II Przycisk należy do g rupy euro Values...
arr ay<But t onA>Abuttons = { euroStarl . euroSt ar2 }; t heButtons ~ butt ons : II Zapisz uchwyt tablicy w zewn ętrz nym zakres ie. va l ues = GetBut tonVa l ues(buttons) : II Pobierz tablicę wartoset przycisków. II Przyg otuj okno dialogowe do
for(int i = O : iLengt h if (contextButton == theButtons[ i] )
i++)
{
val ues[i ] break:
~
userVal ueOialog->Val ue:
}
Array; :Sort( val ues): II Ustaw
wartoś ci
II Sortuj wartości .
wszystkich przycisków.
for (int i = O : iLength : i++) theButtons[ i]- >Text = val ues[ i ] .ToSt ring( ):
definiujemy dwie zmienne tablicowe, jedną przechowującą przyciski, a drugą ich Musimy je tu najp ierw zadeklarować , pon ieważ te tablice są tworzone w j ednej lub drugiej instrukcji i f, a chcemy mieć do nich dostęp spoza bloków i f .
Na
początku
wartośc i .
Pierwsze trzy instrukcje i f sprawdzają, które pole grupy zawiera przycisk kliknięty prawym przyciskiem myszy. Działanie trzech bloków i f jest dokładnie takie samo, ale tworzone tablice będą różne. Tworzona jest tablica przycisków ze zmiennych przechowujących uchwyty któregokolwiek zestawu przycisków, do którego należy contextButton. Tablica przycisków jest następnie zapisywana do t heButt ons, aby była dostępna w zewnętrznym zakresie. Następnie wywołujemy funkcję (jeszcze jej nie dodaliśmy) GetB uttonValue s O , która zwraca tablicę zawierającąliczby całkowite będące wartościami przycisków. W końcu w bloku i f ustawiamy trzy właściwości obiektu okna dialogowego i wywołuj emy funkcję Set LabelText () , aby ustawić tekst podpisu zgodne z odpowiednimi limitami. Przyci sk context Button musi nal eżeć do jednego z trzech pól grupy, ponieważ s ą to jedyne przyciski, dla których jest dostępne menu kontekstowe. Gdy zostanie wykonana jedna z instrukcj i i f , wyświetlamy okno dialogowe poprzez wywoła niejego funkcji ShowDi al og() w warunku czwartej instrukcji if. Jeżeli funkcja ShowD i alo g() zwróci : : Di al ogRes ult : :OK, wykonujemy kod bloku if. Pierwszy określa przycisk, którego
1138
Wisnal C++ 2005. Od podstaw warto ść
mamy zastąpić poprzez porównanie uchwytu contextButton z uchwytami w tablic y t heButtons. Gdy trafimy na odpowiednią wartość, zastępujemy odpowiedni element w tablicy val ues nową wartością i kończymy pętlę . Po posortowaniu wartości aktualizujemy właśc iwość Text wszystkich przycisków w tablicy th eButt ons. Implementacja funkcji Get ButtonVa l ues() w klasie Forml ma następującą postać : II Tworzy
tablicę wa rtości przycisków z
tablicy przy cisków.
array ~ G etB utton Val ue s Carray <Bu t ton ~> ~
buttons)
{
va l ues = gcnew arrayCbuttons->Length) : forCint i = O iLengt h: i ++ ) val ues[i] = Int32: :ParseCbuttons[ i]- >Text) ; return va lues :
a rray< i nt >~
Tutaj tworzym y tabl i cę liczb całkow itych o tej samej długości co przesłana jako argument tablica uchwytów przycisków. Następnie umieszczamy w tabli cy wartości odpowiedniki i nt łańcucha zwróconego przez właściwości Text przycisków i zwracamy uchwyt do tablicy wartości.
Po ponownym skompilowaniu projektu otrzymasz w pełni działaj ącą aplikację . Możn a losować kupony dla kilku loterii z różnymi ograniczeniami . Możemy także losować wartości dla konkretnych elementów lub wybrać własne. Ta aplikacja zawsze działała , ale jeszcze nic dzi ęki niej nie wygrałem.
Podsumowanie W tym rozdziale utworzyłeś aplikację Windows Form , wykorzystującą kontrolki, których najprawdopodobniej będziesz potrzebował w większości programów. Powinno być jasne, że GUl programów Windows Form jest tworzony za pomocą okna Design . Cały kod dla klasy je st umieszczany w definicji klasy , więc przy bardzo złożonej klasie skutkuje to wieloma liniami kodu. Przy komercyjnych aplikacjach kod zawiera sporą liczbę dużych klas, które są raczej nieuporządkowane i trudne w modyfikacji oraz zarządzaniu z poziomu kodu . Z tego powodu zawsze powinieneś korzystać z okien Design i Properties w celu wprowadzenia zmian, a gdy potrzebujesz zmienić coś w kodzie, używaj panelu Class View, aby trafić w odpowiednie miejsce.
Z tego
rozdziału powinieneś zapamiętać, że :
klasę
•
Okno aplikacji j est formularzem, a formularz jest definiowany przez wyprowadzaną z klasy System : : Form.
•
Okno dialogowe jest formularzem, którego na FixedDialog.
•
Okno dialogowe może być utworzone jako modalne poprzez wywołanie jego funkcji ShowDial og() lub jako okno niemodalne poprzez wywołanie jego funkcj i Showi ).
wartość
FormBorderStyl e jest ustawiona
Rozdzial21. • Aplikacie wvkorzvsluiące Windows Forms •
Możemy zde cyd ować, właściwości
• •
1139
czy okno dialogowe zostanie zamknięte, poprze z ustawieni e Di al ogResul t obiektu okn a dialogowego.
Kontrolka ComboBox łączy możliw o ści kontrolek Li st Box i Text Box oraz wybranie elementu z listy lub wpisanie nowego elementu.
umożliw ia
Kontrolka Numeri cU pDown pozwala na wprowadzanie danych liczbowych poprzez kolejnych warto śc i z zakresu. Można też ustawić inkrementację.
wyśw i etl ani e
•
Procedurę ob sług i kl iknię cie
•
•
zdarzenia C1i ck kontrolki można kontrolki w zakładce Form Design .
dodać
poprzez dwukrotne
Można okre ślić i stniejącą funkcję jako procedurę obsługi zdarzenia
dla konkretnego zdarzenia dla kontrolki za pomo cą okna Properties. Klikni ęcie przycisku Events w tym oknie wyświetla listę wszy stkich z darzeń dla kontrolki. P owiniene ś zmieniać
tylko za
nazwy automatycznie wygenerowanych Properties, a nic w edytorze kodu .
skł adowych
klas
pomoc ą okna
Ćwiczenia Kod źródłowy oraz rozwiązania do ksiazki/vcppo.htm
poniższych ćwiczeń
znajdziesz pod adresem http://he/ion.p//
l
Zmień przykład Cw21_01, tak aby wyświetl ał okno dialogowe, które utworzyłeś jako forrnularz dialogu, gdy zost anie kliknięty element menu Help/Ab out.
2.
Zm ień
Cw21_01 , tak aby okno dialogowe wyświetlane dla elementu Choose menu kontekstowego korzystało z kontrolki Li st Box, a nie z pola tekstowego, i wy świetlało pełną listę poprawnych wartości , które możn a wybrać.
3. Pozn aj właściwo ści i funkcje aby
dostępne dla kontrolki W ebBrowser i zmień Cw21_01, wpi sywan ie adresu URL za po średnictwem kontrolki TextBox. tej zmianie kontrolka WebBr owser powinna wyświetlać stronę o podanym URL.
umo żliwić
Dzięki
1140
Visual C++ 2005. Od podstaw
22 Dostęp
do źródeł danych
waplikacjach Windows Forms
Z tego rozdziału dowiesz si ę , jak tworzyć aplikacje oparte na formularzach, które będ ą wyświ e tl ały dane pochodzące z różnych źród eł , a w szczeg ólno ści z istniejąc ej bazy danych. Z teg o
rozdzi ału
dowiesz
się
• Jakie klasy biorą udział w hermetyzacj i źródła danych. • Jak u żyć kontrolki Da t aGr id Vi ewdo
wyśw i etlan i a
dan ych .
• Jak dostosow ać wygląd kontrolki Dat aGri dV i ew. • Jaka jest funk cja komponentu Bindi ngSo urc e i w j aki spos ób mo żna go wykorzystać z kontrolką DataGr i dVi ew. • Jak
użyć
kontrolki Bi ndi ngNa vi gator do nawigowania po danych ze przez kontrolkę Bi ndi ngSource.
źró d ła
zarząd zane go
• Jak wysłać uaktualnienie bazy danych za i komponentu Bin di ngSourc e.
pomoc ą
kontrolki Bindi ngNa viga t or
Visual C+ + 2005 pozwala w dużym stopniu zautoma tyzo wać tworzenie aplikacj i opartych na f ormularzach, które korzystają ze źródeł danych, jednak na po czątku pominiemy te możliwości. abyś dowiedział się. jak programować te komponenty. Dzięki takiemu podej ściu nie tylko dowiesz się. jak to wszystko działa. ale także docenisz, ile pracy wykonuje za Ciebie automatyzacja.
1142
Visual C++ 2005. Od podstaw
Praca ze źródłami danych
Zródło danych jest dowolnym źródłem danych dla aplikacji; relacyjne bazy danych, u sług i
internetowe dające dostęp do danych, obiekty - wszystko m oże b yć źródłem danych. W trak cie prac y nad aplikacją, któr a będzie k or zystała z istniejąc e go ź ró d ł a dan ych, prze ważnie będz iesz musiał w jakiś sposób okre śli ć to źródł o w projekcie. Służy do tego okno Data Sour ces wyświ etlane po wybraniu elementu menu Data/Show Data Sources lub naciśnięciu klawi szy Sh ift +Alt+D. Zródło danych jest reprezentowane przez obiekt klasy, wi ęc dodanie źródła danych do pro jektu nierozerwalnie łąc zy si ę z dod aniem definicji kilku klas. Oto krótkie ich omówienie: Źród ł o danych
Źródło danych j est definiowane przez klas ę wyprowad z an ą z klasy Dat aSet, zdefiniowanej w przestrzeni nazw System: :Data . Ta kla sa hermet yzuje pamięć podręczną wszystkich danych z bazy danych dostępnej dla projektu .
Tabele bazy danych
Każda tabel a bazy dany ch j est definiowana przez k lasę z agni eż dżo n ą w klas ie Da t aSet, reprezentuj ącej b azę danych. Klasa d efiniując a tab elę jest wyprowadzana z klasy Syst em : : Data : : Da ta Tabl e. Klas y repr e zentuj ąc e tabe le tak że definiują zdarze nia, sy gnali zując e zm iany w dan ych oraz w łaści wośc i , dzięki którym d ostępna je st warto ść każdego rekordu b ie żąc ej bazy danych .
Kolumny tabeli
K a żda
kolum na w o kreś lo nej tabeli bazy danych je st identyfik owana przez składową klas y DataTabl e, defin iującej tabelę . Składowe reprez entuj ąc e kolumny są typu Syst em: . Data ' : Dat aCol um n i de fi n i uj ą charakterystyki kolumny, takie jak nazwa kolumny i typ danych w kolumnie. Te charakterystyki zbiorczo o kr e ś l am y mianem schematu kolumny.
Wiersze tabeli
Wiersz w tabeli j est rep rezento wany pr zez obiekt typu System: :Dat a : :Dat aRow. Obiekt Dat aRow zaw iera dane z wiersza i ma tyl e e leme ntów danych , ile jest kolumn w obi ekci e Dat aTabl e.
Mówiąc
prost o - je żeli baza danych zawiera kilka tabel , z których każda ma pewną liczbę kolumn , do reprezentowania źród ła danych zostanie wygenerowane du żo kodu; oczywiście dziesiątki tysięcy wierszy kodu nie s ą czym ś niespotykanym w praktyce.
Klasy, które om ówił em w powyżs zej tabeli , służąjedynie do hermetyz acji dany ch ze źródła danych; nie dostarczają one mechanizmu połączenia ze źródłem danych, takim jak baza danych, i uzyskaniem dostępu do danych. Ta możliwo ść jest zapewniona przez kla sę komponentu nazywaną adapterem tabeli, który zostanie wygenerowany automatyc znie. Adapter tabeli usta nawia połączenie z bazą danych i wykonuje polecenia lub instrukcje SQL operujące na bazie danych. Dla każdej składowej DataTa bl e w obiekcie Dat aSet istn ieje jedna klasa adaptera tabeli, więc j eże l i aplikacja będzie prac owała z trzema tabelami bazy danych, zostaną zdefi niowane trzy klasy adaptera. Obiekt adaptera tabeli wypełnia danymi składową DataTa ble obiektu DataSet i może w razie potrzeby aktualizować tabelę w bazie danych.
Rozdział 22.• Dostęp do źródeł danych waplikacjach Windows Forms
Dostęp
1143
do danych i ich wyświetlanie
W przestrzeni nazw System: :Wi ndows : : Forms zdefmiowane są trzy komponenty współpracujące ze sobą w celu uzyskania dostępu do danych w aplikacji Windows Forms i wyświetlenia ich :
Komponent
Opis
Da t aGridVi ew
Kontrolka, która mo że wyświetla ć praktycznie k ażdy rodzaj danych w pro stokątn ej siatce. Tej kontrolki można u żywać niezale żnie od po zostałych dwóch.
Bi ndi ngSource
Ten komponentjest używany w celu opakowania danych ze źródła danych. Komponent ten może zaj ąć s ię uzyskaniem dostępu do źró d ła danych oraz uaktualnieniem ich i może b yć użyty jako nośnik dla danych wy świetlanych w kontrolce Dat aGr idView.
Bi ndi ngNavigat or
Ta kontrolka dostarcza pasek narzędzi służący do p rzeglądania i manipulowania danymi ze źródł a danych, przeważni e źró dła danych opakowanego w kontrolce Bi ndi ngSource.
Komponent Bindi ngSource nie je st kontrolką, ponieważ nie posi ada graficznej reprezent acji, z któ rą mógłb y pracować użytkownik , a le jest zaprojektowany do u zupełnienia działania i pracy z kontrolkami Data Gri dVi ew i Bindi ngNa vi gator w aplikacj ach bazodanowych. Kom ponent Bi ndi ngSource zapew nia komunikację ze źródłem danych, niezbędną do wykonania zapytań i poleceń aktualizacji . Kontrolki DataG r id View dostarczają interfejs użytkownika umożliwiający przeglądanie i wprowadzanie danych, natomiast kontrolka Bindi ngNa viga to r dostarcza pasek narzędzi, ułatwiający przeglądanie danych. Użycie kontrolki Bi ndi ngNavi ga t or j est opcjonalne. Jeżeli chc es z, może sz samodzielnie zmieniać rekordy. Mimo że te kontrolki zostały zaprojektowane do współpracy ze sobą, kontrolka DataGri dView jest sama w sobi e szczególnie przydatnym narzędziem, ponieważ można ją wykorzystywać niezależnie od pozostałych dwóch. Zapewnia ono niesamowitą l iczbę możliwości zmieniania wyglądu siatki wyświ etlającej dane. Poznasz niektóre sposo by dostosowywania kontrolki DataGri dV i ew, zanim omówię, jak używać jej w połączeniu z komponentami Bi ndi ngSource i Bi ndi ngN avi gator. Zauważ, że
do uzyskania dostępu do źródła danych można użyć również kontrolek SqlCon nect ion, SqlOat aAdapter i Da taSet. Jeżeli chciałbyś wykorzystać te kontrolki, być może b ędziesz musiał je samodzielnie dodać do pan elu Toolbox. Możesz to zro b ić poprzez wybranie z menu głównego Tools/Choose Toolbox Items i wskazanie na liście kontrolek, tych, które chcesz dodać do okna Toolbox.
Używanie kontrolki
oataGridView
Kontrolka DataGridV i ew umożliwia wyświetlan ie i modyfikację prostokątnej tabli cy z danymi pochodzącymi z szerokiego zakre su źródeł dan ych . Możesz także użyć kontrolki Dat aGri dVi ew w celu wyświetl enia niemal każdego rodzaju danych pochodzących bezpośrednio z programu. Za kulisami jest to bardzo złożona kontrolka zapewniająca olbrzymią elastyczność . Możemy
1144
Visual C++ 2005. Od podstaw wykorzystać mnóstwo jej możliwości, używając wielu jej właściwości, funkcji i zdarzeń . Po nadto kontrolka DataGri dView może być zaskakująco prosta w użyciu . Możemy nie wgłębiać się w jej skomplikowane procesy wewnętrzne i korzystać z niej w oknie Form Design, które za nas zajmie się szczegółami. W dalszej części rozdziału utworzysz w pełni działający program pracujący z bazą danych Northwind, w ogóle nie pisząc kodu; cały program zostanie wyge nerowany przez możliwości Form Design i ustawianie odpowiednich właściwości komponen tów zastosowanych w projekcie.
Dane w kontrolce Da taGri dView są wyświetlane w prostokątnej tablicy, którą możesz sobie zbiór kolumn i wierszy. Każda kolumna komórek ma u góry komórkę nagłów kową, która przeważnie zawiera tekst identyfikujący kolumnę, a każdy wiersz ma na początku komórkę nagłówkową wiersza, co obrazuje rysunek 22.1 .
wyobrazić jako
DataGridView Jl gr idCntrl ~ gcnew DataGridView; II Tworzy
gridCntrl->Columns[3] odnos i się do pojedynczej kolumny gridCntrl ->ColumnCount jest liczbą kolumn
To są nagłówki wierszy
I
~ ColumnO gridCntrl->RowCount jest lic zbą wierszy
-+
kontrolkę
Columnl
Column2
To są nagłówki kolu mn
~
~
Column3
Column4
RowO Rowl
gridCntrl->Rows[2] --+ odnosi się do jednego wiersza -+
-,
Row2 Row3
gridCntrl->Rows odnosi się do kolekcji wierszy
-, I'..
gridCntrl->Columns odnosi się do kolekcji kolumn
-,
-.
gridCntrl->Rows[2]->Cells[3] odnosi się do czwartej komórki w trzecim wierszu
Rysunek 22.1 Do wierszy i kolumn komórek odwołujemy się za pośrednictwem właściwości obiektu kon trolki Dat aGri dVi ew. Właściwość Rows zwraca wartość typu Dat aGri dRowColl ect i on, która jest zbiorem wszystkich wierszy, a do konkretnego wiersza odwołujemy się za pomocą indeksu, co pokazuje rysunek 22 .1. Podobnie właściwość Co l umn s kontrolki zwraca wartość typu Data Gri dViewCo1umnCollection, z której także można wydobyć konkretną kolumnę za pomocą indeksu. Wiersze i kolumny są indeksowane od zera. Właściwość Ce11 s obiektu DataGri dRow Co11 ect i on reprezentuje kolekcję zawierającą komórki w wierszu i możemy za jej pomocą dostać się do określonej komórki w wierszu. Rysunek 22.1 przedstawia przykład, jak odnieść się do czwartej komórki w trzecim wierszu. Liczba komórek jest dostępna jako wartość właściwości RowCount kontrolki, a właściwość Co l umnCount zwraca liczbę kolumn. Początkowo, gdy kontrolka nie jest związana ze źródłem danych, nie będzie miała ani wierszy, ani kolumn. Możesz ustalić liczbę kolumn i (lub) liczbę wierszy, ustawiając odpowiednie właściwości kontrolki, ale gdy używasz kontrolki do wy świetlenia danych ze źródła danych, robione jest to automatycznie.
Rozdział 22.• Dostęp do źródeł danych waplikacjach
Kontrolki Dat aG r i dV i ew
możn a używ ać
Tryb n i e zwi ąza n y (ang. unboundy
w trzech
różnych
Windows Forms
U45
tryb ach:
W trybie ni ezwiązanym samodzielnie prze s yłam y dane do kontrolki, za p omoc ą funkcj i Add() wła ściwoś ci Rows kontrolki. Jest to tryb u żywan y do wy świetl ania raczej małej i lo ś ci danych.
p rzeważnie
Tryb związan y (ang. bound)
W tym trybie okreś l a my ź ró d ł o danych, które maj ą być wyświ etl o n e poprzez ustawienie wa rtośc i wł aś c iw o śc i DataSource kontrolki.
Tryb wirtualny
W trybie wirtualnym ł ąc zymy kontrolk ę z pami ęci ą p od rę c zn ą danych, wy p e ł n ia n ą danymi z oddzielnego źródł a danych. Tym trybem po słu g uj emy się w przypadku, gdy chcemy wyś wie t l ić dane ze źródła , w którym chcemy zarządz a ć d ostępem do danych w celu optymalizacji wy d aj nośc i .
W tryb ie n ie związanym możemy u żyć kontrolki DataGri dVi ew do wyświetlenia w aplikacji dowolnych danych, które m o gą zostać wyśw ietl one w siatce . Zyskuj emy dzięki temu bardzo użyteczne n arzędzie do wyświetlania dany ch w wielu różny ch aplikacjach . W kolejnym pod rozdziale o m ów i ę dokładn iej używani e kontrolki w trybie ni ezwiązanym.
Używanie kontrolki DataGridView
wtrybie niezwiązanym Dan e w kontrolce DataGr i dVi ew są zapisywane w postaci prostokąta określan ego właś ciwo ś ciam i Rows i Co l umns kontrolki. W trybie niezwiązanym dodajemy dane do kontrolki za pom ocą funkc ji Add ( ) wła śc iwości Rows O, ale zanim będziemy mogli d odawać wiersze do kontrolki , muszą zo s tać zdefiniowane kolumny, chociażby po to, aby ustalić , ile elementów b ędzie w w ierszu. Ustawienie w kodzie właściwości Col um nCount kontrolki zmienia liczbę kolumn i określa, ż e kontrolka ma działać w trybi e ni ezwi ązanym . Poni żs ze instrukcje tw orzą kontrolkę , do której odnosimy si ę za pomocą uchwytu data Gri dV i ew, a następnie ustawiaj ą liczbę kolumn na 3. Dat aGr ldVi ewA dat aG ridView = gcnew DataGr idVl ew; dataGridView->Co l urnnCount = 3. II Ustaw liczbę kolumn .
Opcjonalnie
możemy zasto sować
id entyfikuj ących
dane w
każdej
etykiety kolumn w kontrolce poprzez określenie nagłówków kolumnie. Służy do tego właściwość Name kolumny. Oto, jak
można to zrobić :
dataGridView- >Colurnns[O ] ->Name dat aG r idView ->Col umn s[ l] ->Name dat aGri dVi ew- >Col umns[ 2]- >Name
=
= =
L"Name" ; L"Phone Number ": L"Address" ;
Właściwość Co 1um ns kontrolki j est wła ściwością indeks owaną, w ięc uzyskujemy dostęp do poszcze gólny ch kolumn za pom ocą wartości indeksu, które rozpoczynają się od O. A zatem instrukcje te etykietują trzy kolumny w kontrolce dataGr idView. Jak przekonasz s ię w kolejnym przykładzi e , nagłówki kolumn m ożna też ustawiać za pomocą okna Propert ies kontrolki .
1146
Visual C++ 2005. Oli podstaw W arto ś ć zwracana przez właśc iwość Rows jest kolekcją typu Dat aGri dV i ewRowCo11 ect i on, a ten typ je st definiowany w przestrzeni nazw Syst em: :Wi ndows : :Forms. Właściwość Co unt tej kolekcji zwraca liczbę wierszy. Dostępna jest także domyślnie indeksowana właściwość zwra c ająca wiersz z danym indeksem. Kolekcja wierszy posiada sporą liczbę funkcji; nie omówię ich wszystkich, a tylko najbardziej przydatne, służące do dodawania i usuwania wierszy.
Ad d( )
Dodaje jeden lub więc ej wierszy do kolekcji .
l nsert()
Wstawia jeden lub wi ęcej wierszy do kolekcji.
Cl ear ( )
Usuwa wszystkie wiersze z kolekcji .
Ad dCopy( )
Dodaje k o pię wiersza
Insert Copy( ) Remove( )
Wstawia kop ię wiersza okre ślonego przez pierwszy argument na p o zycję drugi argument. Usuwa wiersz okre ślony przez argument typu Oat aGr i dV i el-lROI-I A.
RemoveAt ()
Usuwa wiersz określony
o kreś lo n ego
wartością
przez argument. określoną przez
indeksu podanąjak o pierwszy argument.
Funkcja Add( ) dla wartości zwróconej przez właściwość Rows występuj e w czterech prze cią żonych wersjach, które umożliwiają dodanie wiersza danych do kontrolki na różne sposoby. Add()
Dodaje jeden nowy wiersz do kolekcji.
Add(i nt rowCount )
Dodaje rowCount nowych wierszy do kolekcji. Je żeli argument ma warto ś ć zero lub jest liczbą ujemną, zgłaszany jest wyjątek typu System: :ArgumentOutOf RangeExcepti on,
Add( Oa t a G r i d V ie~
row)
Dodaje wiersz określony przez argument. Obiekt OataGridVi ewRow zawiera komórek, a także parametry określające wygląd komórek w wierszu.
kolekcję
Add( , .. Obj eetA obje et )
Dodaje nowy wiersz i wypełnia komórki w wierszu obiektami przez argumenty.
okre ś lo n y m i
Wszystk ie wersje funk cji Add() zwracają wartość typu i nt, która jest indeks em ostatni ego dodanego wiersza do kolekcji. Jeżeli właściwość Dat aSour'ce kontrolki OataGrid Vi ew nie jest pusta lub kontrolka nie ma kolumn, wszystkie wersje funkcji Add() zwracają wyjątek typu Sys t em : : I nva1 idOperati onException. Możemy dodać poniższych
nowe wiersze do kontrolki dataGri dVi ew instrukcji :
mającej
trzy kolumny za
pomocą
dat aGridVi ew->Rows ->Add(L"Fred Abl e" , L"914 696 1200". L"1235 Fir st St reet. AnyTown") : dat aGridVi ew->Rows- >Add(L"May Ea st ". L"914 696 1399" , L"1246 Fi rst Street . AnyTown") ; Każda
z tych instrukcji dodaje nowy wiersz do kolekcji , a trzy argumenty funkcji Add( ) odpo trzem kolumnom w kontrolce. Kontrolka musi posiadać wystarczającą liczbę kolumn, aby pomieścić liczbę elementów, którą chcemy dodać w wierszu. Jeżeli spróbujesz dodać do wiersza więcej elementów niż dostępna liczba kolumn w kontrolce , nadmiarowe dane zo staną wiadają
pominięte.
Rozdzial22.•
Dostęp do źródeł danych waplikacjach Windows
Forms
Na początek wypróbujemy tryb niezwiązany w przykładzie, w którym ustawimy Da t aGri dVi ew za pomocąjej wła ściw oś ci w zakładce Form D esign .
1147
kontrolkę
~ Kontrolka DataGridView wtrybie niezwiązanym Ten przykład wyśw i et l a listę książek określ onych przez ISBN, tytuł , autora i wydawcę. Utwórz nowy projekt Window s Forms o nazwie Cw22_01. Dodaj do formularz a kontrolkę Dat aGr i d View i kliknij s trzał kę zn ajdującą s ię w prawej górnej części kontrolki, aby wyświetlić menu przedstawione na rysunku 22.2 . • x
Rysunek 22.2 ~ 1II1 forml
_
1:]
X
OataGridYlew Tasks Choos e Data Source
~~.~==;:;~
Edi t Co!umns... Add Colomn, •• ~ Eneble Addlng ~ En.ble Editing ~ En.bl e Deleting
D
Eneble Colcnn Reordering
Dock in parent container
Je żeli
klikniesz doln y element menu, Dock in parent container, kontrolka wyp ełni obszar klienta formularza. Górny element menu służy do wyb oru źródła danych, ale tym razem nie będ ziemy go określać . Gdy klikniesz element menu Add Column, zostanie wyświ etlon e okno dialogowe przedstawione na rysunku 22.3, służące do wprowadzenia kolumn wyświetlanych w kolumnie. Przycisk opcji Unbound column jest zaznaczony, poniewa ż dla kontrolki nie zostało określ on e źró d ł o danych - i tak powinno być w tym przykład zie. Wpi s w polu Name jest wartością właściwości Name kontrolki, a wpis w polu Header Text je st wartością właściwości Header Text, która odpowiad a tekstowi wyświ etlanemu w kontrolc e jako nagłówek tabeli. Po rozwi n ięc iu pola listy służącego do wyboru wartości Type ukaże s i ę szeroki wybór typów kolumn y. Pozo staw tutaj dom yślny wybó r - TextBox, ponieważ będz i emy dodawać łańcuchy jako wyświetlane dane. Pozostałe typy kolumn dostarczają różne kontrolki w kolumnach przedsta wi ających dane :
1148
Visual C++ 2005. Od podstaw
Rysunek 22.3
? X
Add Columo
r
.t
@ Unbound colLllln Name:
r:cI ISB=-N : --- - - - - - - - ---.
Type:
IDataGri:l'v'iewTextBoxCoIumn
Heeder text:
IL... I S~ BN
(...1
I
,----J
o Vr.lble
ORead Only
Add
II
D
Froz_n
Caocel
DataGr i dV i ewButtonColumn
Ten typ jest używany do kolumny .
Oat aGr idViewCheckBoxCol umn
Ten typ jest używany, gdy chcemy przechować w komórkach
kolumny wartości boal (obiekty Syst em: :Boo l ean) lub obiekty
Syst em : :Windows : :Forms : :CheckStates jako pola wyboru.
Data Gr i dV i ewComboBoxCo l umn
Ten typ jest używany, gdy chcemy wyświetlić komórce kolumny.
DataGr i dV i ewJmageCol umn
Ten jest obraz.
OataGr ldVlewLi nkCol umn
Ten typ jest używany, gdy w każdej komórce kolumny ma zostać
używany,
wy świetlania
przycisku w ka żdej komórce
listę ro zwijan ą
gdy w każdej komórce kolumny ma być
w każdej
wyświetlany
wyświetlone łącze.
Aby dodać kolumnę do kontrolki, wpisz wartości dla Name i Header Text, następnie wybierz typ z listy, jeżeli chcesz zmienić typ domyślny, i kliknij przycisk Add. W tym przykładzie dodaj kolumny z nazwami ISBN, Ti t l e, Autor i Pub11sher . W polu Header Text wpisz te same warto ści. Po dodaniu kolumn kliknij przycisk Close, aby zamknąć okno dialogowe (przycisk ten jest widoczny po dodaniu ostatniej kolumny) . W dowolnym momencie możesz edytować kolumnę poprzez kliknięcie Edit w menu nym ; zostanie wtedy wyświetlone okno dialogowe przedstawione na rysunku 22.4.
podręcz
Okno dialogowe Edit Columns umożliwia zmianę kolejności kolumn, dodanie nowych lub usunięcie już istniejących ; można w nim także edytować wszystkie właściwości kolumny. Powróć
do zakładki Design i zmień właściwość Text formularza na My Book Li st . Gdy wy kod formularza, będziesz mógł zmienić jego konstruktor, aby dodać dane do kon trolki Dat aG r i dView, korzystając z funkcji Add() właściwości Rows(): świetlisz
Form1(void)
{
Jnit i al i zeComponent ( ) ;
II
II DO ZROBIENIA : dodaj kod konstrukt ora,
Rozdział22.• Dostęp do źródeł
danych waplikac;ach Windows Forms
1149
Rysunek 22.4 5elected Colunns:
~~ ~ T itle
Con te xtM~USl: rip _ _(none)
~ Author ~ Publisher
Maxlnp utLenQth
32767
Re. dOnly Reslzable
False Tr~ - -
SortMode
s
: AutomatiC
Data DataPropert yName
'S Design IN""",) Colt..mnType
(none)
ISBN Dat·~G;idViewT ex t6o~Column
8 Layoul
Add..,
II
I (N,;mel rrdc eres the nameusedincodeto identify the obiect. Remoye
- - - r=---'==--=:" , --=--, Ol " book2 - {L"D -D9-977 17D-5". L"The Emperor ' s New Mind". L"Roger Penrose", L" Vintage"}: array<Stri ngA>A book3 - {L"D-14-Dl7996-8",L"Met amaglcal Themas" , L" Douglas R, Hofstadter ", L"Penguin"}: array<Stri ngA>Abook4 - {L"D- 2Dl -36D8D- 2". L"The Meaning Of It Al l ". L"Richard P. Feynman" , L"Addison-W esley"}: arr ay<St ringA>Abook 5 - {L"O-593-D3449-X", L"The W al pole Orange", L"F rank Mui r" , L" Bant am Press"}: array-St r t nq" >" book6 - {L "D-439-99358-X", L"The Amber Spyglass", l.'P tn l ip Pull man", L" Scholast ic Chi ldrens Book s"}: ar ra y- St r-tnq">" book7 ~ {L"D -552-l3461-9", L"Pyramids", l.'Terry Pratchett ", L"Corgi Books"} : array-St r t nq" >" book8 - {L"D- 7493-9739-X" , L"Made In Amerlca ". L"Bi l l Bryson" . L"Minerva"}: II Utwórz tablicę książek.
arrayA>A books - {bookl , book2 , book3. book4. book5. book6, book7, book8} : II Dodaj wszystkie ks iążk i do kontrolki .
for eachCar ray<St ringA>A book in book s
dat aGridViewl ->Rows->AddCbook):
II
Dla każdej k s iążki tworzona jest tablica typu ar ray-St r t nq> , a uchwyt do każdej tablicy jest zapisywany w zmiennej typu ar ray<array<Str ingA>A>A, Każda tablica łańcuchów składa się z czterech elementów, które zawi erają dane odpowiadające kolumnom w kontrolce Dat aGri dV iew, wię c każda tablica definiuje książkę. Dla wygody gromadzimy uchwyty tablic z łańcu chami w tablicy books. Typ tablicy books jest nieco nieczyteiny z powodu powtórzeń znaku A, ale jest to po prostu uchwyt do tablicy ele mentów, gdzie każdy element jest typu ar ray<Stri ngA>A. Tablica books umożl iwia nam usta wienie wszystkich danych w kontrolce w jednej pętli for each. Właściwoś ć Rows obiektu
1150
Visual C++ 2005. Od podstaw DalaGr i dVi ew zwraca uchwyt typu DataGri dViewRoli:o11ect i on odnoszący się do kolekcji wierszy w kontrolce . Wywołani e funkcji AddC) dla obiektu zwróconego przez właściwo ś ć Rows dod aje pełny wiersz do kolekcji. Każdy element tablicy przesłanej j ako argument odpowiada kolumnie w kontrolce, a każdy typ elementu dan ych musi być zgodny z typem wybranym dla kolumny. Tutaj wszystkie kolumny mają ten sam typ, więc wszystkie komórki wiersza mogą zostać prze słane w tablicy. J eżeli kolU11U1Y byłyby różnego typu, moglibyśmy okre ś l i ć element dla każdej kolumny jako oddzielny argument funk cji Add C) lub uży ć tabli cy elementów typu Ooje ct" . J eżeli
skompilujesz i uruchomisz przykład , n aciskając klawisze Ctrl+F5, powinn o okno aplikacji przedstaw ione na rysunku 22.5.
zo s tać
wy świetlone
Rysunek 22.5
fil f orml
1:] :8:
-
ISBN
TiUe
Aulhol
Pubnsl
0-ll9-174271-4
Wondeltul Lito
SlophonJ.y Gould ' Hulchir
0-09·977170·5
Tho Emporo" N... Roger Peroro' e
Q.14-017996·B
Melam.QicalThe... OOLIQIa, R. HoI, t .. , PengUII
.Vinlage
0·201·360BO·2
The Me.ningOl 1... RichOld P. Fey"...
!Addiso
Q.593-o3449·X
The Walpole Ola__. Flank MUl
' Banlam
0·439-9935B·X
The Ambel Spyg!. ._ Phlip Pullman
10-552-13461-9-
iPl'lamid,
Schola
Terry Pretcheu
: COIgiB
I0-7453-9739-X J~de I~Am elica_ eon
i Minelv
I~ Paski przewijania wyśw iet lan e są automatycznie, ponieważ kontrolka DataGri dVi ew nie mieści si ę w obszarze klienta formularza. Jeżeli zmienisz rozmiar okna, paski przewijania znikną. gdyby kolumny były wystarczająco szerokie, aby pomieścić mak symalną tekstu, który mają zawierać. Poprawi to zmiana warto ści właściwości AutoSi zeCo 1umnsMode w grupi e Layout. Otwórz okno właściwości kontrolki Da ta Gr idView z poziomu zakładki Form] [Design] w panelu Editor i zmień warto ść właściwo ści AutoSizeCo l umnsMode na A11 Ce 11s. Jeżeli teraz ponownie skompiluj esz i uruchomisz program, okno aplikacji będ zie wygl ąd ało jak na rysunku 22 .6. Dobrze
byłoby ,
dłu g o ść
Rysunek 22.6
F.:l! f orml
-
Ijtle
ISBN Q.lJ9.174271·4
Won~ ~I Lif e
ISlephen J. y Gould
0·09·977170·5 1he Empe'o" New MI"d IRogel Penro' e 0·14·017996·B MelamaQical Thema, 0-201-36080·2 The Me.ning OfIlAII
Publ Hutc Vinla
Oougla, R. Ho/,I. d1.. Peng R,ch. ldP.F eynman
IAddis
0-593-03449·X The Walpole Orange
I FI. nk Mu;r
Sant
0-439-99358·>( TheAmberSpyglass
IPhil;p Pullm.n
Schol
0-552-13461·9 Pyr.mid,
, l.
R
I:]
Aulhcir
Terry Plalcholl ..B ~ Brysen
1-- -
0·7493·9739·>( I M. de InAmerice
COIgi ~"'
I
ROZllział
22. •
Dostęp do źródeł
danych waplikacjach Windows Forms
1151
Szerokość ka żdej kolumny jest wystarczaj ąca do przechowania najdłuższego łańcucha zawar tego w kolumnie. Powstałe okno aplikacji nie jest złe, ale z poziomu kodu można zrob ić znacz nie więcej w celu poprawy je go wyglądu .
Dostosowywanie kontrolki DataGridView
Jak w spominałem w cześn iej w tym rozdzial e, wygląd kontrolki Oa t aG ri dVi ew mo że by ć zmieniany na wiele sposobów. Poznasz niektóre z nich w trybie niezwiązanym , ale wszystko , czego si ę tu nauczysz, ma również zastosowanie w kontrolce działającej w trybie zwi ązanym . Wygląd każd ej komórki w kontrolce Oat aGridVi ewjest okreś l any przez obiekt typu Oa t aGri dViewCe ll Sty le , który ma następujące właś c i w ośc i: Właściwość
Opis
BackCo lor
Warto ść je st
ForeCol or
W ar t o ścią j est
obiektem typu Sys t em: .Drawi ng: :Col or, który okreś l a kolor tł a kontrolki. Kl asa Co l or defin iuje ze staw standardowych kolorów j ako statyczne składowe . Domyślną w artośc ią jest Colo r: : Empty. obiekt Col or , który o kr e ś ła kolor pierwszego planu kontrol ki. Colo r : :Empt y.
Dom yśln ą w arto ś c iąj e st
Sel ect i onBackCol or
Wart ościąjest w art oś ciąj est
Select i onForeCol or
Wartością je st
wybr aniu. Font
obiekt Col or, który Colo r: : Empt y.
określa
obiekt Col or, który
o kre ś l a
D omyśln ą wart o ś c iąjest
Wartość jest
kolor tła po wybran iu kontrolki. Domyślną
kolor pierw szego planu komó rki po jej Colo r : :Empty .
obiektem typu Sys t em: :Drawi ng: :Font, który o kr e śl a c zc i onkę tekstu w ko ntrolce. Domy śl n ą wa rt o ściąj est nu11.
w yświetlane g o
Al i gnment
Wartość okre śla poło żenie z awarto ści kom órki. W a rt o śc i s ą zde finiowane przez wy liczenie Dat aGr i dViewA l i gnment , więc może to być j edn a z po niższyc h stał ych:
Bot tomCenter, Bot to mleft, Bot t om Rlght , Mid dl eCente r, Mi ddleleft, Ml ddl eRi ght , TopCent er, Topl e f t , TopRig ht , Not Set. D om yślną warto ś ci ąjest
Wr apMode
Not Set.
W artość okre śla, czy tek st w komórce ma zo s t ać za w i nię t y , gd y j es t zbyt dłu gi , aby zm i eś ci ć si ę w komórce. Warto ś cią jest jedna ze stałych definiowanych przez wy liczenie Dat aGr i dViewT r i St at e:
True, Fa l se, Not Set . D omyślną wartoś ci ą jest
Padding
Not Set.
Wartośćjest
obiektem typu Syst em: :Windows : : For ms : .Padd: ng, który o kre ś l a o dstęp komórki a kr awędzią ko mó rki. Kon struktor klasy Paddi ng wymaga argumentu typu i nt , kt óry jest o d s t ę p e m w yr a ż o nym w piksel ach . D o myśl n a w art ość odpowiada brakowi od st ępu w komórce. między za wa rtośc ią
Format
Wartość
j est ł a ńc uchem formatuj ącym o kreś l ający m, j ak z a w a rt o ś ć łańcu cha sform atowana. Jest to fo rmatow anie tego samego rod zaju, z j akiego korzysta łeś w funkcj i Console : :W r i tel lne( ). Domyślną warto ściąjest pusty łańcuch .
ma
z o s t ać
1152
Visual C++ 2005. Od podstaw Nie jest to pełna lista właściwości obiektu DataGri dVi ewCe 11 Styl e, a jedynie tych z wyglądem komórek .
związanych
Sposób, w jaki określany jest wygląd konkretnej komórki, jest dosyć skomplikowany, ponie dysponujemy wieloma różnymi właściwościami określającymi, jak będzie wyświetlana konkretna komórka lub grupa komórek, które możemy ustawić dla kontrolki Dat aGr i dVi ew, ale tylko niektóre z nich mogą mieć zastosowanie w danym momencie . Na przykład możemy ustawić wartości właściwości określających wygląd rzędu komórek lub kolumny komórek, lub wszystkich komórek w kontrolce, a wszystkie z nich mogą działać jednocześnie . Mówiąc jaśniej, ponieważ wiersze i komórki w kontrolce zawsze się przecinają, wszystkie te trzy możliwości będą stosowane do wszystkich komórek, więc mamy tu jawny konflikt. waż
Każda
komórka w kontrolce DataGri dVi ew jest reprezentowana przez obiekt System: :Wi ndows : : Forms :DataG n dVi ewCe 11, a wygląd każdej kontrolki, włącznie z komórkami nagłówkowymi, jest określany przez jej właściwość InheritedSty l e. Wartość właściwości Inheri t edStyl e komórki jest dobierana po przejrzeniu wszystkich możliwych właściwości zwracających war tość będącą obiektem DataGri dVi ewCe 11 Style, mających zastosowanie do komórki, a następnie rozważeniu ich wedle priorytetów. Zastosowanie ma pierwsza odnaleziona właściwość, która ma zostać ustawiona. Określenie wartości właściwości dla komórek nagłówkowych jest obsłu giwane w inny sposób niż właściwości Inher itedSty l e pozostałych komórek, więc omówię je oddzielnie, rozpoczynając od komórek nagłówkowych.
Dostosowywanie komórek nagłówkowych Wartość właściwości
przez uznanie
Inher it edSt yle każdej komórki nagłówkowej w kontrolce jest kolejnych właściwości w następującej kolejności:
określana
wartości
•
Właściwość
•
Właściwość Co l umnHeadersDefau ltCe 11 Sty l e lub RowHeadersDefaultCellStyle obiektu kontrolki.
•
Właściwość
Styl e dla obiektu DataGridVi ewCe 11
reprezentującego komórkę.
właściwość
Defau ltCe 11 Sty l e obiektu kontrolki.
A więc jeżeli została ustawiona wartość właściwości Style dla obiektu komórki, właściwość Inheri tedSty l e tej komórki przyjmuje tę wartość i określa wygląd komórki. Jeżeli nie, zasto sowanie ma kolejny kandydat. Jeżeli druga możliwość nie została ustawiona , stosowana jest właściwość Defau ltCe ll St yl e kontrolki. Nie zapomnij, że wartość właściwości Inheri tedStyl e jest obiektem typu Dat aGr i dVi ewCe 11 Sty l e, który sarn posiada właściwości określające różne aspekty wyglądu komórki . Proces przechodzenia przez hierarchię ważności ma zastosowanie do wszystkich właściwości obiektu DataGridVi ewCe l l Styl e, więc na końcu wpływ może mieć więcej niż jedna z właściwości z kolejności priorytetów.
Rozdział 22.• Dostęp do źródeł danvch
waplikacjach Windows Forms
1153
Dostosowywanie pozostałych komórek Wartość wła śc i woś ci rające
• •
Inherit edSt yl e po zostałych komórek w kontrolce (s ą to kontrolki zawie nast ępuj ąc ych właśc i wości obiektu Dat aG r i dVi ew w kolejno ści :
dane) je st okreś la n a z Wła ściw o ść
St yl e dla obiektu Dat aGr i dVi ewCe11
reprezentującego komórk ę .
Właściwo ść
De faul tCe11Styl e obiektu Da t aG ri dVi ewRow reprezentuj ącego wiersz do obiektu Dat aG r i dViewRow popr zez indeksowanie wła ściw o śc i Rows w obiek cie kontr olki .
zaw i erający kom órkę. Przeważni e b ędzie sz s i ę odn osił
•
Właściwo ść Al t er nat i ngRowsDef aultCe11Styl e obiektu kontrolki. Dotyczy to jedynie komórek w wierszach o niepar zystym indeksie.
•
Właściwo ść
•
RowsDef aul t Ce 11St yl e obiektu kont rolki.
Właśc iwo ść
Def aultCel l Sty l e obiektu DataGri dVi ewCol umn, który zawiera komórkę . do obiektu Da t aGr i dVi ewCo l umn poprzez indeks owanie właściwości Co l umns w obiekcie kontrolki.
Przew ażnie b ęd zi e sz si ę o d nos i ł
•
Właściwo ś ć
DefaultCe11St yl e obiektu kontrolki.
Potencjalnie można m i eć różne obiekty DataGr i dV i ewCe11 St yl e dla wszystkich kontrolek , jed nak ze wz g lęd u na wyd ajno ść n al eży s toso wać jak najmn iej taki ch obiektów. W kolejnym podrozdziale "Spróbuj sam" poznasz niektóre z tych obiekt DataGri dVi e«.
możliwości ,
samodzielnie
ustaw i aj ąc
RmIElJI Ustawianie wyglądu kontrolki Utw órz nowy projekt CLR o nazwie Cw22_02, korzystający z szablonu Windows Form s. W za kła d c e Design dodaj kontrolkę DataGri dVi ew i zm i eń jej właściwość (Name) na data Gr i dVi ew. Jest to nazwa uchwytu w klasie bazowej , o d woł ująca s ię do obiektu kontrolki. Zmi eń także właściwo ść Text formularza na M y Other Book Li st. Resztę przykładu b ędziemy tworzyć bezp o średni o w kodz ie konstruktora.
"
Dane, które będ zi emy wy ś w i e t lać , są bardzo podobne do tych z poprzedniego przykładu, ale rozszerzymy nieco m ożliwoś ci. Na p oczątku wiersza o kreś l aj ącego k si ążkę dodamy datę , więc komórki w pierws zej kolumnie b ęd ą zaw ierały o d woł an ia do obiektów typu Syst em: : DateTime, a p o zo stałe kolumny będą zawierały łańcuchy. Klasa DateTime definiuje czas, który przew ażn ie okreś l amy jako datę oraz porę dnia. W przykładzie będz i e nas interesowała jedynie data, więc zastosujemy konstru ktor przyjmując y tylko trzy argumenty: rok, m iesiąc i dz ień.
Ustawianie danych Pierws zym etapem j est utworzenie danych, które będą w yś w i etla n e . Dodaj konstruktora Forml , za wywołaniem funkcji Ini t i al i zeComponent () :
poni żs zy
kod do
1154
Visual C++ 2005. Od podstaw II Utwórz dane ks iążek, j edna
książka
w tablicy .
arrayA book s ~ {bookI. book2, book3. book4 .
book5. book6. book 7. bookS} ;
Podstawowy sposób działania jest tu taki sam jak w poprzednim przykładzie. Różnica polega na tym , że każdy opis książki ma teraz dodatkowy element typu DateTime, więc tablica elemen tów je st typu Ooj ect ". Jak sobie przypominasz, klasa Object jest klasą bazową wszystkich klas C++/CLI, więc w elemencie typu Objeet A możemy przechować uchwyt do obiektu dowolnej klasy. Następn ie
dodaj do konstruktora poniższą instrukcj ę:
arr ay<Str l ngA>A headers
=
{L"Dat e", L"ISBN" , L" Ti tl e". L" Author", L"Publ is her "};
Tworzy ona t ablicę zawierając ą tekst na gł ówków kolumn w kontrolce. Aby gł ówki w kontrolce, dodaj do konstruktora poni ższy kod :
umie ści ć
te na
dataGri dVi ew->Col umnCount = headers ->Length : II Usta w liczb ę kolumn.
for (int i ~ O ; iLength : i++)
dataGri dV iew->Co l umns[i ] ->Name = headers[i ]:
Pierwsza instrukcja określa liczbę kolumn w kontrolce poprzez ustawienie wartości właśc iwości Co l umnCount ; ustawia też niezw iązany tryb kontrolki. Pętla ustawia właściwość Name każd e go obiektu kolumny na odpowiedni łańcuch z tablicy headers. Właściwość Co l umn s kontrolki zwraca odniesienie do kolekcji kolumn, więc po prostu indeksujemy te odniesienia do poszcze gólnych kolumn. W kolejnej
pętli
dodamy wiersze do kontrolki:
for each(arr ayA book in book s)
dataGridVi ew->Rows ->Add( book):
Pętla
f or each wybiera po kolei ka żdy element z tablicy ks iążek i przes yła go do metody Add( ) referencji zwr óconej przez właściwość Rows kontrolki. Każdy element w tablicy książek jest tablicą łańcuchów, w której znajduje się tyle łańcuchów , ile j est kolumn w kontrolce,
Rozdział 22.• Dostęp do źródeł
danych waplikaciach Windows Forms
Dane zostały załadowane do kontrolk i, wi ęc liczba wierszy i kolumn jest ustalona, a kolumn okreś lo ne . Możemy teraz zająć si ę wyglądem kontr olki .
1155
nagłówki
Ustawianie kontrolki Chcemy, aby kontrolka była zadokowana w obszarze klienta formularza , a poprzez ustawienie właściwości Oock:
możemy
to
zrobić
dataGr idVi ew->Dock = DockStyl e: :Fi l l : W arto ścią właściwości
Ooc k musi być jedna ze stałych zdefiniowanych w wyliczeniu OockStyl e; dopuszczalnymi warto ściami są: Top, Bottom, Left, Right i None, okre śl ając e , po której stronie formular za będzie zadokowana kontrolka. pozostałymi
Można również określi ć po zycję
kontrolki w obszarze klienta formularza poprzez ustawienie Anchor kontrolki. Wartość właściwości Anchor określa krawędzie kontrolki, które mają być zakotwiczone do obszaru klienta formularza. Wartość jest bitową kombinacją sta łych zdefiniowanych w wyliczeniu AnchorSty l es i może być dowolną liczbą wymienionych: Top, Bot t om, Left i Ri ght . Aby na przykład zakotwiczy ć g órną i l ewą s tron ę kontrolki, okre ś li li byś my wartość jako AnchorStyl es: :Top & AnchorSt yl es: :Left. Ustawienie właśc i w oś c i Anchor trwale ustala pozycję kontrolki i jej pasków przewij ania w pojemniku o okreś l o n y m rozmiarze, więc po zmianie wielkośc i okna aplikacji rozmiar kontrolki i jej pasków przewija nia nie zmieni się . Jeżeli - jak w poprzedniej instrukcji - ustawimy właściwość Oock, zmia na wielkości okna aplikacji odsłania większy lub mniejszy obszar kontrolki i pasków przewi jania, w ięc w naszym przypadku je st to leps ze ro związanie . wła ściwości
Chcemy, aby szerokość kolumn był a dostosowywana do danych w komórkach , co poprzez wywołanie funkcji AutoResi zeCol umns( ):
możemy
osi ągnąć
dataGrl dView->Aut oResi zeCol urnns() :
Ta instrukcja dostosowuje s ze roko ść wszystkich kolumn, aby pomieściły b ieżącą zawarto ść , włącznie z komórkami nagłówkowymi . Należy pamięta ć , że dz iała ona jedynie w momencie wywołania , więc zawartość musi znajdować się w tabeli podczas wywołania . Jeżeli zawart o ść jest kolejno zmieniana, szerokość kolumn nie jest modyfikowana. J eżeli chcesz, aby s zeroko ść kolumn była dostosowywana przy każdej zmianie zawartości komórki, musisz również ustawić właściwość AutoSi zeCo l umnsMode kontrolki w następujący sposób : dat aGri dVi ew-'}AutoSi zeCol urnnsMode
~
Dat aG r i dV i ewAuto Si zeCal urnnsMode : :A11 Ce11 s :
Warto ś cią musi być jedna ze stałych okre ślonych w wyliczeniu OataGri dVi ewAutoSi zeCol umns Mode; pozos tałe możliwe wartości to: Col umnHeader, A11Ce11 sExceptHea der, Oi spl ayedCe 11s, Oi spl ayedCe 11 sE xceptHeader, Fi 11 i None. O czywi ście są to również wartości wyświetlane w li ście wartości tej właściwości w oknie Properties kontrolki .
Może s ię zdarzyć, że będziesz chciał , aby w przypadku zmiany zawartości automatycznie była dostosowywana kolumna tylko niektórych kolumn. W takim przypadku należy ustawić warto ść właściwości AutoSi zeMode dla obiektu kolumny.
1156
Visual C++ 2005. Od podstaw Istnieją także dwie przeciążone wersje funkcji AutoRes i zeCol umns. Jedna przyjmuje argument typu OataGri dVi ewAutoS i zeCo l umnsMode, a komórki, na które ma wpływ, są określane przez wartość argumentu . Druga wersja jest chroniona, a wi ęc przeznaczona do użytku w wyprowa dzan ej klasie. Przyjmuje ona dodatkowy argument typu bool , kt óry określa, czy wysokość komórki ma być uwzględniana przy obliczaniu nowej szerokości . Domy ślny
kolor tła wszystkich komórek
możemy ustawić w następujący
da taG ridView->DefaultCel lStyle ->BackCo lor
~
sposób:
Co lor : :Pink:
Ta instrukcja określa kolor tła jako standardowy kolor Pi nk, który jest definiowany jako statycz na składowa klasy Col a r . Właściwość Oef aultCe11St y l e obiektu kontrolki jest stosowana jedynie dla komórki , na którą nie oddziałuje styl o wyższym priorytecie. Możemy również określić domyślny kolor tła
dat aGrid View->Defau ltCe l lStyl e->ForeCo lor
~
pierwszego planu wszystkich komórek:
Co lor : :DarkBl ue;
Aby oznaczyć, które komórki zostały wybrane, możemy ustalić kolory tła i pierwszego planu dla zaznaczonych komórek. Oto, jak można zdefiniować kolor tła zaznaczonej komórki:
dat aGr idView->Defau ltCell Style->Se lect ionBackColor = Color: :Green: Oczywiście celem ustawiania wartości właściwości w kodzie jest fakt , że są one ustawiane w momencie wykonania, więc możemy ustawiać te wartości w zależności od warunków i war tości danych występujących podczas wykonywania aplikacji. Wartości właściwości ustawio nych 7.2 pomocą okna Properties w IDE są ustawiane raz na zawsze - chyba że mamy kod, kt óry je później zmienia.
Na razie kolumn.
kończymy pracę
z
kontrolką.
Teraz zajmiemy się dostosowywaniem •
nagłówków
Ustawianie naglówków kolumn Jeż eli
chcesz samodzielnie zmieniać wygląd nagłówków kolumn, mus isz EnableHeade r sV isualStyle na f a lse:
zmienić wartość wła
ściwości
dat aGr idView->EnableHeadersVis ualStyles = false: Kontrolki w aplikacji Windows Forrns są zwykle rysowane zgodnie z aktywną kompozycją stylów wizualnych . Je żeli uruchamiamy aplikację w Windows XP, kontrolki są rysowane zgodnie z kompozycją Windows XP. Gdy właściwość Ena bleHeadersVisualStyles ma war tość t rue, style wizualne nagłówków kolumn będą ustawione zgodnie z kompozycją stylów wizualnych mającą zastosowanie dla aplikacji, a ustawione przez nas style zostaną pominięte . Ustawimy kilka właściwości wyglądu nagłówków, a w prosty sposób można to zrobić , tworząc obiekt DataGri dV i ewCe11St y l e, dla którego ustawimy żądane właściwości, a następnie sprawi my, by ten obiekt określał style nagłówków. Możemy utworzyć obiekt Oat aGridV i ewCell Styl e w następujący sposób:
DataGr idViewCel lStyl e headerStyle A
=
gcnew Da t aG ridViewCe l lStyle :
Rozdział 22. B yłoby miło ,
gdyby tekst
zm ien i ając właściwo ś ć
•
Dostęp
do źródeł danych waplikacjach Windows Forms
nagłówk a był
napisany
więks z ą c zc ionką,
co
1157
m o żemy os iąg n ąć,
Font :
headerStyle->Font ~ gcnew Sys tem : :Drawing. :FontBackColor = Col or : :Al iceBl ue: headerStyle->ForeColor ~ Color: :BurlyWood : Tekst zostanie teraz narysowany w kolorze BurlyWood na tle Al i ceBl ue. Je żeli wol ałb yś j akiś inny zestaw, klasa Co lo r oferuje bardzo szerok i wybór kolorów, a Intellisense wyś w i e tl i ich l istę , gdy skoń czy sz wprowadzać operator zakre su. Aby ustawić wygląd komórek nagłówko wych zgodnie z właściwoś c i ami , które dla obiektu headerSt yl e, musimy dodać n a st ępującą in strukcj ę :
dataGridView->Col umnHeadersDefault Cel lStyl e Instrukcja ta przypisuje
wła ś ciwośc i
aderSt yl e. Dzięki temu podm ienia
=
u st awili śmy
headerStyle:
Col umnHeadersOef aultCe11 Styl e kontrolki uchwyt he obiekt Oat aGr i dV i ewCe 11 Styl e, który o kre ś lał
istniejący
styl n agłówków . Jest jeszcze jedna rzecz, kt órą powinni śmy zrobić z nagłówkami kolumn . Wi ęk sz a czcionka powoduje, że należy zwróci ć uwagę na dost osow anie w y sok o ś ci kom órek do w i elko ści nagł ówków . Wywołanie funkcji AutoResi zeCo l umnHeader sHei ght () kontrolki dostosowuje wyso kość kom órek n agłówkowych tak, aby pomieściły bieżącą zawartość: Wysokość
wszystkich komórek nagłówkowych jest ustawiana tak, aby pomieścić zawartoś ć kom órki. Jeżeli chciałbyś, aby dostosowywana była wys o koś ć tylko konkr etnej komórki, możesz użyć przeci ążonej wersji funkcji, która przyjmuje argument okre śl ający indeks kolumny , która ma zo stać dostosowana. n ajwi ęk s zej
Je żeli
chcesz, aby
nagłówki
wierszy lub kolumn
były
niewidoczne, ustaw
wartość właśc iwości
RowHeader sV i si ble i (lub) Co lumnHeader sVis i bl e kontrolki na fa l se.
Formalowanie kolumny Pierwsza kolumna zawiera uchwyt obiektu OateTime. W tej chwili aplikacja po prostu wywo ToSt r i ng() obiektu, aby m ieć co wyśw i etl i ć, ale my chcemy czegoś więcej. Usta wimy właściwoś ć Format wła ściwo ści Oefaul tCell Sty l e kolumn y i ten format zostanie zasto sowany do wy świetlenia z w a rto ś c i kom órek:
łuj e funkcj ę
dataGr idVi ew ->Col umns[O] ->DefaultCel lStyle->Format = L"y":
1158
Visual C++ 2005. 011 podstaw Właściwo ś ć Format zostanie ustawiona na łańcuch zawierający specyfikację formatu y obiektu Oat eTi rne, przedstawiającą obiekt jako skróconą datę - miesiąc i rok. Obiekty Da t eTime mają
jeszcze kilka innych specyfikacji formatu. Na f i F wyświetlają czas oraz datę.
przykład
D wyświetla
dzień, miesiąc
i rok,
Jeżeli dodałeś cały
kod do konstruktora klasy Forml , czas wypróbować przykład. Jeżeli skom pilujesz i uruchomisz program, zostanie wyświetlone okno aplikacji podobne do tego z ry sunku 22.7 .
Zmieniłem szerokość okna na rysunku 22.7, aby zmie ściło się więcej kolumn. Niestety, w książce okno aplikacji jest przedstawione w odcieniach szarości, jednak na ekranie mieni się ono różnymi kolorami .
Gdy klikniesz jeden z sunku 22.8.
nagłówków
wierszy w kontrolce, wiersz zostanie
wyróżniony
jak na ry
Rysunek 22.8
I
uth Ol
5tephenJ.y Gould
O·09·97n70·5 The EmperOl·. New Mind Rag!!! Peroose 0·201 ·J6080.2 The Meaning Of 1\ Ali
Richard P. Feynm.n
0-593-03449·X The W.1po1eOrange
Frank Mu.
0·439·99J58.X The Ambel 5pygiess
Ph ~p
0·552·13461·9 P)'amid.
TeuyPralchell
O·749J.9739·X M.de In Americ.
Bil8 ry. on
Pullman
Tło
wybranych komórek jest w kolorze, który ustaliliśmy jako właściwość Select ionBackCal ar Default Styl e kontrolki. Można również wybierać poszczególne komórki, klikając je, i kolor tła takiej komórki zmieni się na zielony. właściwości
Możliwość
sortowania wierszy według kolumny jest wbudowana w kontrolkę Oa taGridVi ew. kolumny, a wiersze zostaną posortowane według tej kolumny. Gdy
Możesz kliknąć nagłówek
Rozdział 22.• Dostęp do źródeł danych waplikacjach
Windows Forms
1159
wiersze zostaną uporządkow ane w odwrotnej kolej do każdej kolumn y, wskazującą na mo żliwo ść sortowania. do kon struktora klasy Forml :
jeszcze raz klikniesz ten sam
n agłówek,
nośc i . Możemy dodać wskazówkę
Dodaj tu pętlę
for eachCDataGr idVi ewCal umn cal umn i n dataGr idView->Ca lumnsl
cal umn->TaalTi pText = L"Click t a\ nsort raws" ;
A
Wartość właściwości Co l ums je st kolekcją kolumn , gdzie każda kolumna jest obiektem typu Oat aGr i dVi ewCo l umn. P ętla przechodzi przez wszystkie kolumny i ustawia wartość właściwości Too lT i pText. Rysunek 22 .9 przedstawia wskazówkę dla jednego z nagłówk ó w tabel i.
Rysunek 22.9
~ My Olh er Oook lisi
_
I ISBl'l'
I Ti
e
I
2004
paździ er nik
BillBrysan
0-14·017996-8 Melama9'
Dougla s R. HoIsladle.
0-552-1 3461-9 Pyrarrids
Teny Pratchett
0-439-9935B-X TheAmber Spyglass
Pl-lp Pullman
2001 0-09-9771 70-5 The Ernpe< Made ln d ick to
scrt rows
~piec
Ll
I A nth oi
lstopod1995
•• • •
0-201-36080·2 The Meaning Ol ltAli . 0-593-03449->< TheWa1po1e O/ange 0'09-1742n..\ WonderlulLile
Hicherd P. Feynrnan Frank MJr
Slephen Jay Gould
pojawi ają się tylko wtedy , gdy kursor znajduje s ię nad komórką nagłówk ową. wskazówki dla wszystkich komór ek wyśw i et laj ąc ych dane, ustaw iając właśc i wość Too lTipText obiektu komórki.
Te wskazówki Można dodać
Dostosowywanie naprzemiennych wierszy Gdy wy świ etl anych je st wiele podobnie wyglądaj ących wierszy, można się łatwo pogub ić. Aby przezwyciężyć ten problem, możemy różnie kolorować naprzemienne wiersze, ustawiając różne kolory jako właściwoś ć BackCol or właściwośc i Alternati ngRowsOefaultSty l e obiektu kontrolki :
dat aGridVi ew ->Al t ernatin gRawsDefault Cel lStyle ->Ba ckCalar
=
Cola r : :Bl ue;
Prawdopodobnie będziesz chc i ał także zmienić właściwość ForeCo lo r właściwości Al t erna t i ngRowsOefaul t St yl e, aby uzy skać rozsądny kontra st między tłem a pierwszym planem :
dataGridV iew->Alte rnat i ngRawsDefaultCel lStyle->ForeCa la r
=
Colar : :Whit e:
Teraz wyświetlane s ą naprzemi an różow e i niebieskie wiersze, jak na rysunku 22.10 (oczywi śc i e w odcieniach szarości, ponieważ ta książka nie jest kolorowa). Teraz je st łatwo odróżnić poszczególne wiersze , a biały tekst na niebieskim tle jest czyteln y. Po zaznaczeniu całego wiersza popr zez kliknięcie jego komórki nagłówkow ej zostani e on zaznaczony na zielono. Kliknięcie komórki znajdującej się po lewej stronie nagłówków kolumn zaznacza wszystkie wiersze.
Dvnamiczne ustawianie stvlów komórki Mamy różne możl iwo ści zmieniania wyglądu komórek poprzez obsługę zd arzeń dla kontrolki Dat aG ri dVi ew. Zdarzenie Ce 11 Forma tt i ng dla kontrolki DataGri dView występuj e , gdy zawarto ść kontrolki musi zosta ć sformatowana przed wyświetleniem, więc dodając procedurę obs ług i tego zdarzenia , możemy dostosować wygląd dowolnej komórki w zależności od jej za w arto ś c i . Na podstawie przykładu Cw22_0f dowiesz s ię , j ak to zrobić . Przypuśćmy na przykład, że chcemy, aby w przykł adzie Cw22_02 tło kom órek kolumny Date było czerwone , gdy zawiera lata sprzed 2000 roku . Jak sobie przypominasz z omó wienia zdarze ń w rozdziale 9., aby zarej estrować procedurę obsługi zdarzenia , należy dodać egzemplarz delegata do zdarzenia. Delegat dla zdarzenia Ce11Formatt i ng j est typu DataGr i d Vi ewCe11Formatt i ngEventHand1 er i spodziewa się dwóch parametrów : pierwszy jest typu Object i ident yfikuj e źródło zdarzenia, a drugi je st uchw ytem obiektu typu DataG r i dVi ew Ce11Format t ing EventArgs. A
Drugi argument przesyłany do procedury obsługi zdarzenia Ce11Formatt i ng dostarcza dodat kowych inform acji o zdarzeniu dz i ęki następującym właśc iwościom: Właściwość
Opis
Valu e
Warto ś ć
Des iredType
Wartość jest uchwytem obiektu typu Type, który for matow anej ko mórk i.
Ce 11 St yl e
Pobiera lub ustawia styl komórki, która jest powiązana ze zdarzeniem form atowania, w ię c wartoś ć jest uchwytem obiektu typu DataGrid Vi ewCe11St yl e.
Col umnl ndex
Wartość
jest indeksem kolumny, w której znaj d uj e
się
formatowana komórka.
Rowl ndex
Wartoś ć
jest indeksem wiersza , w którym znajduje
się
form atowana kom órka.
Format ti ngAppl i ed
W artość zostało
jest uchwytem
zawartośc i
- t ru e lub fa l se zastosowane.
form ato wanej komórki . określa
typ
wsk azuj e, czy formatowanie
za wa rtoś c i
zawartości
komórki
Rozdział 22.• Dostęp do źródeł danych waplikacjach Windows Forms
1161
Te właściwo ś ci umożliw iają uzyskanie wszelk ich potrzebnych informacji o formatowanej ko mórce, czyli wiersza i kolumny, w których znajduje się formatowan a komórka, bieżącego stylu i zawartości komórki. Pierwszym etapem obsługi zdarzenia Ce11 Formatti ng jest zdefiniowanie jego procedury obsług i . Kod procedury powinien być tak krótki i wydajny, jak tylko to możliwe, ponieważ funkcja jest wywoływan a dla każdej komórki w kontrolc e, gdy tylko komórka musi zostać sformatowana. Dodaj poni ższy kod proc edury obsługi zdar zenia Cel l Formatti ng do klasy Forml:
privat e:
void OnCell Format tl ng(Object A sender. DataGridVi ewCel lFormatti ngEventArgsAe)
( II Sprawdź, czy jes t lo komórka z
datą .
if( da t aGridVi ew ->Columns[e->Co lumnlndex]->Name - = L"Dat e") ( II Jeżeli za wartoś ć komórki 10 nie null i rok jest mniej szy II ustaw czerwony kolor tla.
n iż
2000.
i f (e->Val ue != nu l lpt r && safe_cast (e->Val ue)->Year < 2000) (
e->Cel lStyle ->BackCo lor - Co lor: :Red; e->Formatt ingAppl ied = false: II Nie sjo rm ato waliś my daty.
Najp ierw sprawdzamy, czy komórka znaj duje si ę w kolumnie Date, pon iew aż chcemy zmie niać tylko komórki z tej kolumny. Dla kom órek z kolumny Dat e sprawdzamy, czy komórka w ogóle istnieje i czy znajduje si ę w niej obiekt DateTime, w którym rok je st mniejszy niż 2000. Jeżel i tak, ustawiamy właściwość Back Co l or zwróconą przez wła ściwo ś ć Cell Sty l e na Colar : :Red. Ustawiamy właściwość Formatt ingAppl i ed na f al se, aby zaznaczy ć , że nie for matowaliśmy zawartośc i . Nie jest to niezbędne, ponieważ początkową wartością j est f a l se. Ustawilib yśm y ją na t rue , gdybyśmy w procedurze zajmowali si ę również formatowaniem zawartośc i , co z apobiegłoby formatowaniu na podstawie wart o ści właści wości Forma t . Aby
zareje strować tę funk cję
szą in struk cję
na
końcu
jako procedurę obsługi zdarzenia Ce11 Formatt i ng, dodaj konstruktora klasy FormI:
poniż
dataGridView ->Cel lFormatting +~
gcnew Da t aGr idVi ewCell FormattingEventHandle r( t his . &Forml : :OnCe llFormat t l ng):
Pierwszy argument delegata jest uchwytem obiektu zawi erającego procedurę obsługi i jest bie żącym obiektem FormL Drug im argumentem je st adre s funk cji , która jest n ową procedurą obsługi zdarzenia. Jeżeli pon ownie skompi luj esz i uruchomisz program, wszystki e komórki z kolumny Data zawierające rok sprzed roku 2000 będą miały czerwone tło . Kontrolka DataGri dVi ewdefiniuje zdarzenia Ce11MouseEnter i Ce11 MouseLeave, które wy stę puj ą, gdy kursor myszy znajdzie się w obrębie komórki lub ją op uśc i . Możemy zaimplemen tować procedury tych zd arzeń, aby wyróżnić komórkę, nad którą znajduje się kursor, poprzez zmi an ę jej koloru . Możemy ustawia ć nowe kolory tła i pierwszego planu w procedurze obsługi zdarzenia Ce11 MouseEnt e r i przywracać pierwotne kolory w procedurze ob sługi zdarzenia Ce11 Mou seLeave. Wiąże się z tym kilka trudności , więc warto się temu osobno przyjrze ć.
1162
Visual C++ 2005. Od podstaw
~ Wyróżnianie komórki pod kursorem myszy Będzie to rozszerzenie programu Cw22_02 zawierającego ostatnio dodaną procedurę obsługi zdarzenia Ce11Formatt i ng, a nie całkowicie nowy przykład. Mu simy gd zie ś przechow ać stare kolory tła i pierwszego planu, więc dodaj następujące prywatne składowe do klasy Forml:
II Przechowują stare kolory komórek w procedurze obs ługi wprowadzania kursora,
II aby móc je przywró c ić w pro cedurze obs ługi opuszczenia prz ez kursor.
prlvate : Color oldCel lBackColor: private: Color oldCel lForeColor : Zainicjalizuj je w konstruktorze klasy Forml z
wartością
Colo r : : Empt y.
Pierwszy parametr delegata zarówno dla zdarzenia Ce 11 MouseEnt e r, jak i Ce 11 Mouse Leave jest uchwytem obiektu będącego kontrolką Dat aGri dV i ew, z któreg o pochodzi zdarzenie. Drugim parametrem obu delegatów jest uchwyt obiektu typu Da t aG ridVi ewCell Event sArg, który dostarcza dodatkowych informacji o zdarzeniu. Ten obi ekt posiada właściwości RowIndex i Col umnl ndex, a ich warto ści umożliwiają odnalezienie komórki, w obręb której wchod zi lub którą opuszcza kur sor. Pierw szej z nich możemy uży ć do zindeksowania właściwoś ci Rows w celu wybrania wiersza zawierającego komórkę , a drugiej właściwości do zindeksowania właściwości Ce 11s, aby wybrać komórkę w wierszu. N ależy jednak zwrócić uwagę na pewną sprawę wartoś ć właś c i w ości Rowlndex wyno si -1 , gdy kursor znajduje się w wierszu nagłówków kolumn, a właściwo ść Co l umnlnd ex ma wartość - I, gdy kursor znajduje się nad nagłówkiem wi ersza. N ale ży sprawdzić te możliwo ści , ponieważ próba użycia ujemnego indeksu z właściwo ś ci ą Rows spowoduje zgłoszenie wyjątku, co dotyczy również właściwoś ci Ce11s dla wiersza, jeżeli spróbujemy użyć ujemn ej wart o ści. Możemy teraz zdefiniowa ć w klasie Form1:
następującą prywatną procedurę obsługi
zdarzeni a Ce 11 MouseEnt er
void OnCel lMouseEnt er(Object sender , DataGridVi ewCe l l Event Args ej A
A
{
iH e->Co lumnlndex >= O && e->Rowlndex >= OJ II Sprawdza, czy indeksy
nie są ujemn e.
{ II Identyfi kuj e komórkę. w której znajduje s ię kurso r.
DataGr idViewCe ll cel l =
dataGri dView->RowsCe ->Rowl ndex]- >Ce l ls[e->Co lumnlndex]:
A
II Zapisuje stare kolory.
oldCellBackCo lor = cell ->Style ->BackColor : oldCellForeColor = cel l ->Style->ForeCo lor : II Ustawia kolory
wyróż n ienia,
cel l ->Style ->Back Co lor - Colar: :White:
cel l ->Style->ForeColor - Colar : :Bl ack:
}
Po stwierdzeniu, że wartości obydwu indeks ów nie są ujemne, uzyskujemy uchwyt komórki, nad którą znajduje się kursor. W tym celu najpierw wybieramy wiersz poprzez indeksowanie warto ści właściwości Rows kontrolki z właściwością RowInde x dla parametru e. N as t ęp n i e indeksuj emy właściwość Ce115, używając wła ściwości Col umn Index e, aby wybrać komórkę w wierszu.
Rozdział 22.• Dostęp do źródeł danych
waplikacjach Windows Forms
1163
Gdy mam y ju ż uch wyt kom órki , m o ż em y łatwo za p i sać warto śc i właściw oś ci BackCo l or i ForeCol or ze stylu Styl e komórki i u stawi ć nowe kolory wyświetlające czarny tekst na bia łym tle . Wł aś ciwoś ć Sty l e może nie być ustawiona i w tak im przypadku próba do stępu do wartości wł aś ciwo ś c i tworzy nowy obiekt DataGr i dVi ewCe 11Styl e, dla którego warto ś ci Back Co l or i ForeCo l or mają wartoś ć Co lor : :Empty. Przywracane są wi ęc pierw otne kolory . Jeżeli właściwość St yle komórki została ustawiona, wraz zj ej właś ciwo ś ciami ForeColo r i BackColo r uzyskuj emy obiekt zawierający wszelkie ustawione dla niego wartości. Procedura
obsługi
zdarzenia Cell MouseLeave ma przywra cać poprzednie kolory . w n a stępujący spo sób :
Możemy
zaimplem ent o wa ć tę funkcję
void OnCel lMouseLeave (Object sender . OataGridV iewCel l Event Args e) A
A
(
i f (e->Col umnlndex
>~ O
&& e->Rowl ndex
>~ O )
{ II Określa
komórkę, którą
opuszcza kursor.
Oa taGridViewCell cell =
dat aG ridView- >Rows[e->Rowlndex]->Cel ls[e->Column lndexJ;
A
II Przywraca zap isane war toś ci kolorów.
cel l- >Style ->BackCo lor cell ->Style->ForeColor II Przywrac a zapisane
oldCel l ForeCo lor
~
= =
oldCe l lBackColor; oldCe l lForeColor :
wa rtości
na brak koloru .
oldCe llBackColor
~
Co lor: ;Empty;
Pono wn ie zanim przejdziemy dalej, sprawdzamy , czy inde ksy nie są ujemne. Po o kre ś l e n i u komórki w taki sam sposób j ak w poprzedniej proc edur ze przywracam y zapisane kolo ry do właś ciwo ś ci St yl e komórki. Kolory tła i pierwszego planu są okre ś l an e zgodnie z hierarch i ą ważności przed stawioną wcześniej . Jeż e li właściwo ś ć St yl e nie zo stała ustawi ona dla komór ki, przywracane s ą wartości Co lor : :Empt y, które są ignorowane, gdy wyb ierane są kolory dla komórki. Wtedy mają zasto sowani e pierwotne kolory. J e ż eli właściwość Sty le została zdefi niowana i jej właściwo ści ForeColo r i BackCol or ustawi one , są one przywracanymi warto ściami i o k reś l aj ą formatowan ie kolorów komórki . Aby zarej es tro w ać procedury Forml:
gcnew OataGri dViewCel l Event Handl er (t hi s . &Forml; ;OnCel lMouseLeave);
Ten sam delegat ma zastosowanie do wszystki ch zdarzeń w kom órka ch, więc rej estruj emy obyd wie procedury, korzystając z dele gata Dat aGri dV i ewCe 11 EventHandl er. Jeżeli
teraz ponownie skompilujesz i uruchomisz program, obrazuje rysunek 22.11. Wszystko św iet n ie działa niane, wię c co jest gran e?
przeważni e .
wyróżnianie b ędzie działało,
Czerwone komórk i w kolumnie Oate nie
co
s ą wyróż
1164
Visual C++ 2005. Od podstaw
Rvsunek 22.11
Sercem problemu jest kolejność zdarzeń. Zdarzenie Ce11Formatt i ng ma miejsce po zdarzeniu Ce 11MouseEnt er, więc ostatnie słowo należy do procedury obsługi ustawiającej kolor tła na czerwony, która przesłania działanie procedury dla zdarzenia Ce11MouseEnt er . Chcemy, aby procedura obsługi zdarzenia rozpoznawała, że komórka jest wyróżniona , i w takim przypadku nie robiła nic. Możemy to osiągnąć poprzez dodanie kolejnej s kł adow ej do klasy For ml , która będ zie przechowywała uchwyt aktualnie wyróżnionej komórki :
private: OataGri dVi ewCel1 high1ight edCe11; A
II Aktualnie wyróżn iona komórka ,
Tę nową składową należy również zainicjalizować
w konstruktorze klasy.
Teraz zm ienimy procedurę obsługi zdarzenia Ce11MouseEnter, aby dowej uchwyt wyróżnionej komórki :
high l ightedcel l- >Style->BackColor = oldCe ll BackColor: hlghlight edcel l ->Style->ForeColor = oldCel l Fo reCo lor: II
Opróżn iamy
zmienne przecho wujące stare kolory.
ol dCel l ForeColor = ol dCel lBackColor = Color : :Empty: high l lght edCel l
=
nul l pt r :
II Zeruje uchwyt
wyróżnionej kom órki.
Nie potrzebujemy już określać uchwytu komórki, ponieważ jest on dostępny w składowej hi ghl i ghtedCel l klasy Forml. Nie musimy nawet go sprawdzać , ponieważ zdarzenie Cel l Mou seLeave zawsze musi być poprzedzone zdarzeniem Ce 11 MouseEnter. Ustawiamy uchwyt wyróżnionej komórki, ponieważ jest to dobra praktyka. Teraz zmienimy procedurę obsługi zdarzenia Ce 11 Fo rmat t i ng: vOi d OnCel lFormat t i ngCObJect A sender . OataGrldViewCell Formatt ingEvent ArgsA el { II Sp rawdź. czy kom orka jest
wyróżn iona.
l f(dataGridVlew->Rows [e->Rowlndex] ->Cel ls[e ->Col umnlndex] == high l ightedCel l l ret urn: II Sprawdź, czy jest to kom órka z da ta.
if Cdat aGn (
dV iew->Col umns[e->Cel umnlndex]->Name == L"Dat e"l
/r Jeż eli za wartos ć kom órki lo ni e null i rok jest mniejszy n iż 2000, II ust aw czerwony ko lor tla.
i fC e->Val ue != null ptr && safe_cast (e->Val uel ->Year < 2000) {
e->Cel lStyl e->BackCo lor = Coler : :Red: e->Formatt i ngAppl ied ~ tal se: /r N ie sfo rmato walismy daty.
Jeżeli
ponownie skompilujesz i uruchomisz przykład, wszystkie komórki
będą wyróżniane.
Powinieneś teraz mieć jasne pojęcie o implementowaniu zdarzeń dla kontrolki OataGri dVi ew. Istnieje jeszcze mnóstwo innych zdarzeń, więc w miarę potrzeb aplikacji można dynamicznie dostosowywać wiele innych elem entów.
Używanie 1rybu związanego W trybie związanym źródło danych wyświetlanych w kontrolce OataGri dVi ewjest określane przez właściwo ść OataSource, która przeważnie może być dowolnym obiektem typu ref cla ss, mającym jeden z poniż szych interfejsów :
1166
VisIlai C++ 2005. Od podstaw
Interleis
Opis
System: .Coll ect ions : : ILi st
Klasa, która implementuje ten interfejs, reprezentuje kolekcję obiektów dostępnych za pomocą pojedynczego indeksu. Wszystkie jednowymiarowe tablice w C++/CLI implementują ten interfejs, więc obiekt DataGri dVi ewmoże użyć dowolnej jednowymiarowej tablicy jako źródła wyświetlanych danych. Interfejs ILi st dziedziczy składowe klas interfejsu IColl ect i on i IEnume rabl e, które są zdefiniowane w przestrzeni nazw Syst em: :Coll ecti ons.
Syst em: :ComponentModel. : ILi st Source
W klasie implementującej ten interfejs dane są dostępne jako lista, która jest obiektem ILi st. Lista mo że za w ierać obiekty także implementujące interfejs ILi st .
Syste m: :Component Model : : IBi ndi ngLi st
Ten interfejs rozszerza kla sę interfejsu Il i st , umożliwiając bardziej złożone wiązanie danych.
Syst em: :Component Model : : IBi ndi ngL i st View
Dodaje możliwość sortowania i filtrowania do interfejsu IBi ndi ngLi st . KlasaBindi ngSource, którą poznaszw dalszej części rozdziału, definiuje kontrolkę implementując ą ten interfejs.
Oczywiście możesz zaprojektować własne
klasy, które będą implementować jeden lub większą tych interfejsów i będziesz mógł wtedy wykorzystywać je jako źródło danych dla kon trolki Data Gr i dVi ew w trybie związanym. Jednak przeważnie będziesz korzystał z istniejącego źródła danych, nie troszcząc się o tworzenie własnych klas w celu uzyskania dostępu do danych. l tu przychodzi nam z pomocą komponent Bi nd i ngSource. liczbę
Komponent BindingSource Komponent B1ndi ngSource jest pośrednikiem między kontrolkami w formularzu a tabelą w źródle danych . Można związać komponent z kontrolką Da ta Gr i dVi ew, która wyśw ietli całą zawartość tabeli , lub można ustawić pojedyncze kontrolki i wtedy każda z nich będzie wy świetlała jedną kolumnę tabeli . Można również samodzielnie dodawać dane do komponentu Bi ndingSource i w takim przypadku działa on jako źródło danych i zachowuje się dokładnie jak lista. Na początek zobaczymy, jak użyć komponentu Bi ndi ngSou rce jako źródła danych dla kon trolki DataGridView. W trakcie tworzenia programu wykorzystującego komponent Bi nding Sourc e można w dużym stopniu wyręczyć się automatyzacją wielu czynności, jednak na początek samodzielnie złożysz komponent, aby zobaczyć, jak łączy się on z kontrolką. Aby uczynić komponent Bi ndi ng Source źródłem danych dla kontrolki DataGr i dVi ew, ustawia my wartość właściwości Da ta Source kontrolki na uchwyt odnoszący się do komponentu BindingSource.
Rozdział22.• Dostęp do źródeł
danych waplikaciach Windows Forms
1167
Rm!IIjI Korzystanie zkomponentu BindingSource Utworzymy teraz prosty program umożliwiający przeglądanie tabeli bazy danych . Opiszę pro ces, zakładając, że korzystasz z bazy danych Northwind, której używałeś wcześniej podczas pracy z MFC, ale możesz użyć dowolnej bazy danych dostępnej w systemie. Utwórz nowy projekt Windows Forms o nazwie Cw22_03 i zmień warto ść właściwości Text na jakąś bardziej przydatną, jak na przykład "Using a Binding Source Component". Umieść kontrolkę OataGri dVi ew w obszarze klienta formularza i zmień jej nazwę na dat aGridView. Jako wartość właściwoś ci Ooek ustaw Fi 11 . W tej chwili kontrolka OataGr i dVie wjest niezwią zana, wię c musimy dodać do projektu źródło danych, które będziemy mogli powiązać z kon trolką. W wyniku tego zostanie utworzony również komponent Bi ndi ngSouree. Kliknij małą strzałkę znaj d uj ącą się w prawej górnej części kontrolki, aby wyświetlić menu, a następnie kliknij strzałkę w dół przy liście rozwijanej dla elementu menu Choose Data Source oraz łącze Add Project Data Source. Zostanie wyświetlone okno dialogowe przedstawione na rysunku 22.12.
RYSunek 22.12
?
Dala Source ConfiguratJon Wizard
:&
Choose a Data Source Type
wbere will the &pplicetion get data from?
, l ets ; 00 comect to łJ datClbaSB andchoose the detabas:e cbjects foryour apphcation ThIS: option creetes a
I
dęteset.
Canceł
I
To samo okno dialogowe zostanie wyświetlone, jeżeli z głównego menu wybierzesz Data/Add New Data Source, ale wtedy źródło danych zostanie dodane tylko do projektu, a nie zostanie powiązane z kontrolką. Jak wida ć, źródła danych możemy wybrać z dwóch miejsc, ale wybie rzemy opcję Database. Druga opcja umożliwia określenie obiektu dostarczającego źródło da nych zdefiniowane albo w tym projekcie, albo w jakimś innym. Po wybraniu opcji Database kliknij przycisk Next, a ukaże na rysunku 22.13 . Istniejące połączenia znajdują się
się
okno dialogowe przedstawione
na liście rozwijanej i możesz wybra ć jedno z nich. Możesz przycisk New Connection, aby dodać nowe połączenie do źródła danych. Zostanie wtedy wyświetlone okno dialogowe przedstawione na rysunku 22.14. również kliknąć
1168
VislJal C++ 2005. Od pOllstaw
Rysunek 22.13
?
Dali Source Configuration Wizard
~
Choose Vour Data Connection
Which data connectlon should }laur application use to connect to the detebesev
ODBC.ACCESS.G:\Documents ondSettings\Soft\Mo)e doI-,......------~ I Adv.n.:ed... I I Te. t Com ecbon Caf" el I Password:
Tutaj możesz podać jako n azwę specyfikację źródła danych lub - jeżeli klikniesz drugi przycisk opcji - będzie sz mógł wpisać łańcuch określający połączenie. Po wprowadzaniu informacji potrzebnych do zalogowania mo żesz sprawdzić, czy wszystko działa, poprzez kliknięcie przycisku Test Connection . W moim systemie źródło ODBC jest wybierane do myślnie, natomiast można to zmienić, klikając przycisk Change. Zostanie wówczas wyświe tlone okno dialogowe przedstawione na rysunku 22.15 . W oknie dialogowym wyświetlane są dostępne typy źródeł danych. Możesz wybrać ten, z któ rym chcesz pracować, i kliknąć przycisk OK. Wtedy ponownie zostanie wyświetlone okno
Rozdzial22. • Dostęp do źródel danych waplikacjach Windows Forms Rysunek 22.15
1169
? X
Choo se Dał a Source
Data source:
Description
•
rl1icrosoft SQL Serv et
MicrosoftSQLServer Database File Microsoft SQL Servet Mobile Edition Orede Oeteb ese
~e this selecnon to spedłv an ODSC userarsystemdatasourcenameto connect to en ODBC driverthrough
tbe .NET Framework Data Provider f",OOBC .
Data provlder:
I,NEJFramework~ta Provider for OEl O Always usethisselection
Cootlnue
II
Cance!
dialogowe przedstawione na rysunku 22.14, gdzie możesz zakończyć identyfikacj ę i weryfika cję źródła danych przed kliknięciem przycisku OK w celu powrócenia do okna dialogowego przedstawionego na rysunku 22.13 . Po kliknięciu Next prawdopodobnie zostaniesz zapytany, czy chcesz dodać źródło danych do projektu. Po kliknięciu przycisku Yes zostanie wyświetlone okno dialogowe przedstawione na rysunku 22.16 , w którym można wybrać obiekty bazy da nych , z którymi chcemy pracować.
Rysunek 22.16
?
Data Source Configurallon Wiz.ard
~
Choose Vour Database llbjects
Which d~tabas e objects doyouwant in your dataset? Ęl OO t51 abIeS --------~---------,
i w-Drm C.tegorie ' ! i '~ Cusłorre rs
,
W..
o ·, Employees
. @.D ffi1
Order Det eils
i
o rcers
rJr O [!I
~-D iTI'l
Prodocts
$ .0[] Shippers .. ·0 ·"
Suppliers
JiJ - D~ vi.ws
;-··,0
Stored Procede-es
L. O ., Func tions Dat.aSet name:
(eustom."
-~------------------------,
e Previous
które obiekty bazy danych mają być dodane do projektu. W tym celu drzewo , aby wybrać poszczególne tabele, a nawet konkretne pola tabel. W tym przykładzie, aby nadmiernie go nie komplikować , wybierzemy tylko tabelę Customers. Zmień także nazwę zestawu danych na Custome rs. Po kliknię ciu przycisku Finish kreator utworzy kod ni ezbędny do uzyskania dostępu do bazy danych .
Tutaj
możemy wybrać,
należy rozwinąć
Baza danych jest hermetyzowana w klasie wyprowadzanej z klasy Dat aSet . Każda wybrana tabela definiujejako wewnętrzną klasę klasy Dat aSet klasę wyprowadzaną z klasy Data Tabl e. Dla każdej Data Table w klasie Dat aSet istnieje także adapter tabeli , którego zadaniem jest
1170
Visual C++ 2005. Od podstaw ustanowienie połączenia z bazą danych i załadowanie danych z tabeli bazy danych do odpo wiedniej składowej DataTable obiektu DataSet . W wyniku tego powstaje sporo kodu. Do pro jektu zostaje również dodany komponent Bi ndingSou rce, który stanowi interfejs między tabelą Custo mers w bazie danych Northwind a kontrolką Dat aGr idVi ew. Zakładka Design w oknie edytora powinna wyglądać jak na rysunku 22.17 . .x
RYSunek 22.17
~Customers
Jak widać, w wyniku dodania
t;~ customersBindingSource
"/;a CustomersT.bleAdapter
danych do projektu zostały dołączone trzy obiekty: obiekt danych, obiekt Cust omersTableAdapter dający dostęp do danych w tabeli Customers bazy danych i obiekt CustomersBi ndi ngSource, który zarządza komun ikacją między bazą danych a kontrolką DataGri dVi ew.
NorthWi ndDataSet
źródła
hermetyzujący bazę
Aplikacja jest już ukończona, c ho ć jak dotąd nie napi sałeś nawet j ednej linijki kodu . Jest to godne zauważenia, biorąc pod uwagę liczbę funkcji dostępnych w programie. Jednak po uruchomieniu programu nagłówki kolumn były nieco ciasne. Wolę , aby szerokość kolumn wystarczała do pomieszczenia tekstu w wierszach, więc nie mogłem nie dodać poniższych dwóch linijek kodu do konstruktora klas y Forml:
dataGr idV iew->AutoSlzeColumnsMode = DataGri dVi ewAutoSizeCol umn sMode: :Al lCel l s: dat aGri dVi ew- >AutoResizeColumnHeadersHeight(); Okno aplikacji
wygląd a
teraz jak na rysunku 22.18.
Do przegl ądania danych możesz użyć pasków przewijania , a kółko myszy powinno przewijać wiersze . Kontrolka Bi ndi ngNav i gato r poprawi przeglądanie danych , więc teraz ją omówię .
Rozdział
Rysunek 22.18
22.•
Dostęp do źródeł
danych waplikacjach Windows Forms
~ Uj ing a 8 10dlng Source Compon ent Cuslomerl D
~
_
Compan}·Name
1171
Lj ~
Cont.aclName
~ Mleds Ful l e l k i' le
_
. . - - lMa"a Ande"
"
l Ano T'UIIUO Emporedado,. y helados ~~na T'."iillo
Korzystanie zkontrolki BindingNavigator Kontrolka Bi ndi ngNavi gat or z o sta ła zaprojektowana specjalnie do pr acy z komponentem Bind ingSourc e. Wykorzystanie kontrolki Bindi ngNavi gat or do przegląd ania danych ze źródła danych nie mo że b yć prostsze - n al eży jedynie dodać kontrolkę do formularza i ustawi ć w arto ś ć właściwo ści Bi ndi ngSourc e tej kontrolki na zm ienną opakowującą komponent Bi n di ngSource . Kolejny fragm ent "Spróbuj sam" rozwij a program Cw22 _ 03 o tę m ożliwo ś ć .
Rm!!Im Użycie kontrolki BimlingNavigator Kliknij kontrolkę Bi ndi ngNavi gato r w oknie Toolbox , a następnie klikn ij w obszarze klienta formul arza , aby dodać j ą do projektu . Otwórz okno właściwo ści kontrolki i ustaw wartość wła śc iwo ś ci Bindi ngSource na customer sBi ndin gSource , co je st nazwą uchwytu kontrolki Bin di ngSource opakowuj ącej tabelę Cust omers. To wszystko. Je żeli teraz ponownie skompilujesz i uruchomi sz program, zostanie wyświetlone okno aplikacji zawieraj ące pasek narzędzi wspo magaj ący przegląd anie danych, jak na rysunku 22 .19. Przyciski ze strzałkami służą do przemiesz czania się w prz ód i w t ył rekordów, dostępn e są również przycisk i um o żliwiaj ące szybkie przej ści e do pierwszeg o lub ostatniego rekordu . W polu tekstowym w pasku narzęd zi można wpisać numer rekordu i po naci śnię ciu klawi sza Enter przejdziemy be zpo średnio do tego reko rdu. Mo żliw ość nawigowania po rekordach działa przy dowolnym ich ułożeniu . Można posortować rekordy według krajów poprzez klik nięcie nagłówka kolumny Count ry, aby nawigować po rekorda ch w edłu g tej kolejno ści. Kon trolka Bindi ngNavi gato r do sta rcza równie ż przyciski umożliwiające dodawanie i usuwanie rekordów . K ażd y
z przycisków dostarczan ych przez obi ektu Bindi ngSour ce :
przegl ądanego
kontrolkę
Bindi ngNavi gator
łączy s i ę
ze
s kłado wą
1172
Visual C++ 2005. Od podstaw
Rysunek 22.19
~ Unn g a Om dmg Sourc e Compo ncnt
~ I~
~
~I
I
_
f
CACTU
: Cectus Comidas parallever
CENTC
ICentfo comefcialMode zuma
B',
B ev~r~e~
I Vic lmi::~w~
COMMI
Comerce Mineiro
CO NSH
Consolidated Holdings
Francisco CharJg
' Pedro Afonso
IE liz~Oelh Blo,,"
iSven Olllieb - -
DRACD
Drachenblul Delikatessen
DUMON
Du monde enłier
EASTC
Eestem Connedion
jAnn Devon
EANSH
EIn,1Handel
i A oI~nd Mendel
FAMlA
Familia Alquibaldo
i Al i~ Cruz
F1SSA
FISSA Fablica Inter. SaichicM' SA i Diego A oeł
I~
fnliP.~ 1'Y"I•• mAnrip.s:
~
J
Kontrolka paska narzędzi
Działanie
Move Fi rs t ( p r z ej dź do pierwszego rekordu)
pierwszy rekord
Wywołuje funkcję
E
Perrico Simpson
,YangWang
Chop-sueyCh,ne'e
1=- -!-J.J n l Jfi
Ll :lSJ
X
BSBEV
CHOPS
~
~
of 91
22
:J anine Labrune
- - - .-'----.-- - 1łoArl . tit'lP Rrlnr:p,
fi;
~~ I ~r
MoveFi rst ( ) obiektu kontrolki Bindi ngSource, co ustawia danych jako bi eżący rekord.
źró d ła
Move Prev i ous (p rzej dź do poprzedniego rekordu)
Wyw ołuje funk cję
Cur rent Posi t io n (bież ąca pozycja)
Odpow iada wartości wła ściwości Curre nt ob iektu Bindi ngSour ce,
która j est bieżąc ym rekordem w źró d l e danych .
(c ałk owita
Tota l Numbe r of It ems liczba elementów)
O dpowiada wartości wła ś ciwo ś ci Count obie ktu Bi ndi ngSource,
która ozn acza li c zbę reko rdów w źró d l e danych .
Move Next (przej dź do następneg o rekordu )
Wywołuj e funkcj ę M oveNext() obiektu kontrolki Bi ndingSource, co ustawia kolejny (j eże li istnieje) rekord źr ó dła danych jako bież ący rekord .
Move Last (przejdź do ostatniego rekordu )
os tatni rekord
Add New(dod aj nowy)
Wywołuje funk cj ę
Del ete
(usuń)
MovePrevious( ) obiekt u kontrolki Bindi ngSource, co ustawia poprzedni (je żeli istni eje) rek ord ź ró d ła danych jako b ieżący rekord .
Wywołuj e funkcję
AddNew( ) obiektu Bi ndi ngSource. W yw o łuj e ona EndEdi t ( ) w celu wykonani a wszelki ch o cz ekuj ących operacji edycji źró d ła dan yc h i tw orzy nowy rek ord w li ś c i e utrzymywanej przez obiekt B1nd: ngSource. Ni e uaktualni a ona źródła dany ch . Wywołuj e funkcję bi eżącego
RemoveCurrent ( ) obiektu Bi ndi ngSource w celu rekordu z listy. N ie modyfikuje to źródła dany ch.
u sunię ci a
przycisku w pasku naw igacji rozpoczyn a działan ie w obiekcie Bindi ngSou rce danych, ale żadn a z domy ślnych operacji nie zmienia zawartości bazy danych powiązanej z komponentem Bi ndi ngSource. W tym celu nal eży napisać trochę kodu .
A
więc kliknię ci e
MoveLast ( ) obiektu kontrol ki Bi ndi ngSource, co ustawia danych j ako b i e żący rekord .
źró d ła
zarządzaj ącym źródłem
Rozdział 22.
• Dostęp do źródeł danych waplikaciach Windows Forms
1173
RI:JmI!mI Uaktualnianie bazy danych Gdy użytkownik kliknie przycisk w kontrolce BindingNav ig ato r w celu dodania lub usunięcia rekordu , musimy zro b i ć coś jes zcze. W tym celu musimy zaimplementować proc edurę obsługi zdarzenia C1ick dla przycisków. Jak już wiesz, procedurę obsługi zdarzenia C1i ck można dodać poprzez dwukrotne kliknięcie przyci sku w z akład ce Design , więc dodaj pro cedury dla przycisków Add New i De/ele. Gdy kliknies z przyci ski, w komponencie BindingSource wszystko zostanie ustaw ione, aby umożli wi ć aktualizację bazy danych. Pozostaje tylko wywołać funkcj ę Updat e ( ) obiektu adaptera tabeli, która ma zostać zaktualizowana. Ta funkcja zgłasza wyjątek , je żeli co ś pójdzie nie tak, wię c musimy umieścić jej wywołani e w bloku t ry i wyłapać wszelkie zgłaszane wyjątki. Oto, jak można zaimplementować procedurę obsługi zdarzeń dla przycisku Add New :
Argumentem funk cji Update C) musi być nazwa tablicy dany ch zawi erającej wartości , któr e mają zost ać zapi san e do bazy danych , więc w tym przypadku jest to s kład o w a _Cust omers obiektu Cus tomer s w klasie Forml. Jeżel i coś nie zadziała tak , j ak si ę tego sp o dz i e wa liś m y, zostanie wyświetlone okno komunikatu z wyjaśnieniem problemu. Okno komunikatu będzie zawierało tekst zgło s zonego obiektu Except i on, który wyj aśni a przyczynę problemu. Implementacja pro cedury
obsługi
przycisku De/ele jest niem al identyczna:
pr ivate: System : :Vo id bi nding Navigat orDelet elt em_Cl ick(Syst em: :Obj ect Syst em::EventArgs e) I try
Visual C++ 2005. Od podstaw Jedyną ró żnicąjest mo żliwo ś ć
7
tekst w oknie komunikatu. Maj ąc te dwie procedury dodawani a i usuwania rekordów.
ob słu g i , bę d z i es z miał
-.
ązanie Zpojedynczymi kontrolkami Mo żem y także utwor zyć aplikację
Windows Fonns, która wiąże poszczególn e kolumny z ta beli bazy danych z oddzieln ymi kontrolkami; co wi ę cej , będ zie to pro stsze i zajmie mni ej czasu n i ż poprzedni przykład . Rozp oczniem y od utworzenia nowego projektu CLR o nazwie Cw22_04 korzystając e go z szablonu Windows Fo nns Appli cati on. Kolejnym krok iem j est dodanie do projektu źród ła danych i ponownie b ędziemy kor zystali tylko z j ednej tabeli . Naci śnij Shift+Alt+D, aby otwor zy ć okn o DauzSources i kliknij Add New Data Sourc e. Może s z u żyć bazy danych No rthwind lub dowolnej innej , ale pami ętaj , że dla k ażd ej wybra nej kolumn y będziem y miel i o s o b n ą kontroLkę w formularzu. Aby utrzymać roz sądną l iczb ę kontrolek, w przypadku bazy Northwind sugeruj ę, ż e b yś wybrał tab e lę Orde r Detai l s , jak na rysunku 22.20, ponieważ zawiera ona jedynie p i ę ć kolumn.
Rysunek 22.20
?
Data Source Configurahon Wizard
~
Cho ose Vour Oatabase Objects
DataSet name:
ID_t' Set!
I < PreVKlUS I
Finish
)
I
Cancel
Gdy kliknie sz przycisk Finish, okno Dala Sources b ęd zie zaw i erało baz ę danych Northwind jako źródło danych z do stępn ą tylko tabelą Order Deta -Il s. Kliknij n azw ę tabeli Order Det ai l s, a następnie znajdującą s ię po prawej stronie s trzałkę, aby wy świetli ć menu przed stawione na rysunku 22.21. Trzy górn e elementy menu um ożliw iają wyb ór kontrolek używany ch z tabel ą, gdy doda sz ź ródło danych do formularza. Jeżeli klikni esz element menu DalaGridView , wybierzesz tę k ontrolk ę j ako wy św i et lającą dane z tabeli , natomiast kliknię cie elementu Details oznacza, że chces z u ży ć osobnej kontrolki dla każdej kolumny . KLiknięcie elementu [NoneJ oznacza, że nie chcesz, aby po dodaniu ź ró dła danych do formularza były tworzone kontrolki . Ostatni
Rozdzial22. •
Dostęp 110 źróllel danych waplikacjach
Windows Forms
1175
Rysunek 22.21
C us torrśze .••
element menu otwiera okno dialogowe umożl iwiające zmianę domyślnej kontrolk i dla tabeli z domy ślnej OataGri dView oraz dostosowanie wyboru kontrolek. W tym przykładzie kliknij Details, ponieważ chcemy użyć osobnych kontrolek dla poszczególnych kolumn tabeli. Musimy teraz zadecydować, jakich kontrolek będziemy używać dla poszczególnych kolumn. Rozwiń drzewo zawierające nazwy kolumn w tabeli Order Oetai l s poprzez kliknięcie znaku + znajdującego się po lewej stronie nazwy tabeli. Gdy zaznaczysz p ierwszą kolumnę, okaże się , że dla niej także można wyświetlić menu poprzez kliknięcie strzałki w dół . Menu jest przedstawione na rysunku 22.22. Możesz wybrać dowolną kontrolkę
z menu, ale radzę, abyś wybrał Text Box, ponieważ jest ona najbardziej odpowiednia dla kolumny Or der ID. Musisz powtórzyć ten proces dla wszystkich kolumn tabeli ; wybierz kontrolkę Numer icUpOown dla kolumny Qua nt i t y, a dla pozostałych wybierz Text Box.
Zauważ , że kliknięcie elementu menu Customize w panelu Data Sources , przedstawionym na rysunku 22.22, otwiera okno dialogowe Options, w którym może sz zmienić wybór dostępnych kontrolek do wyświetlania danych. Możesz także zmienić domyślnie wybieraną kontrolkę poprzez przeciągnięc ie elementu z panelu Data Sources do formularza. Rysunek 22.23 przed stawia okno dialogowe Options .
Rysunek 22.23 przedstawia kontrolki powiązane z wyświetlaniem listy, natomiast dla każdego typu danych dostępna jest lista powiązanych kontrolek. Oprócz domyślnej kontrolki Oat aGri dVi ew zaznaczyłem jeszcze kontrolki ComboBox i Li st Box, więc te trzy kontrolki są opcjami dla wy świetlania listy danych. Możesz wybrać inne typy danych, dla których chcesz zmienić dostępne kontrolki na liście rozwijanej znajdującej s i ę u góry okna dialogowego.
1176
Visual C++ 2005. Od podstaw
Rysunek 22.22
Customize ,. ,
Rysunek 22.23 ~. Env;rormert
[vi
, .Projects ~nd Solutions
: Source Centro! . Text Editor
Ltt·Data bas. Tools , .Oebuooino
~ ·Oe.. . ce 10015
l:fJ" HTML Oesioner S" WindowsForr'l'K Deskjner
Associated controls:
10
uetesndvew (d"faL.lt)
,0 I,d
ComboBox
Set Delau~
IDChert
elear oefault
Imag"
OLln"
. ..Gene..1
D List
1.... Data Ul Customization
0llSt8ox
D Matrlx D Rectanole D SUbreport D Table D Textbox learnhawto add(ustomcontrolslt! Ol(
II
Canc"1
Ostatnim etapem tworzenia programu jest przeciągnięcie tabeli Order Oetai l s z okna Data Sources do obszaru klienta okna formularza. Zakładka Design panelu Editor powinna wyglą dać podobnie do rysunku 22 .24 . Zmieniłem właściwości
Text formularza, aby
tytuł okna był bardziej odpowiedni. Zmodyfi kontrolek, aby były lepiej umieszczone w obszarze klienta. Zmie niłem też położenie elementów w szarym obszarze, aby wszystkie były widoczne w panelu Design . Jak widać, zostało dodanych pięć kontrolek odpowiadających pięciu kolumnom tabeli. kowałem też
nieco
pozycję
Rozdział 22.
•
Dostęp do źródeł danvch waplikacjach Windows Forms