Al Williams MFC Czarna ksiega


Al Williams




Czarna Księga MFC


Wstęp


Jesteś programistą używającym MFC? To dobrze. Są dwa typy programistów korzystających z MFC. Ciekawe, do którego z nich należysz? Pierwszym typem są “dobrzy" programiści, którzy używają tej biblioteki w taki sposób w jaki chcieliby jej twórcy. Drugim rodzajem są “dzicy anarchiści", którzy wszystko muszą robić swoimi własnymi sposobami. Prawdę mówiąc, ja zaliczam się do tej drugiej grupy. Jeśli płyniesz na tej samej łódce co ja (lub jeśli przynajmniej tego chcesz), to książka ta będzie dla Ciebie doskonała.

Ta książka nie nauczy Cię MFC; a przynajmniej nie w tradycyjnym znaczeniu tego słowa. Powinieneś zabierać się do niej z dobrą znajomością podstaw użytkowania MFC i z przemożną chęcią na robienie wszystkiego w inny sposób. To nie jest przykładowy program Scribble (chociaż w pierwszym rozdziale przypomnę podstawowe wiadomości o sposobach używania biblioteki). W tej książce nauczysz się wykorzystywać w swoich programach wszystkie, nawet najdrobniejsze i najbardziej ukryte, możliwości MFC. Poznasz sposoby używania, omijania oraz wypaczania poprawnego sposobu użycia architektury dokument/widok. Jeśli zawsze chciałeś stworzyć własne archiwa, to wreszcie zobaczysz jak to można zrobić.


Dlaczego ta książka?

Musisz mieć pozwolenie na prowadzenie praktyki medycznej. Czyż nie? Czytywałem artykuły w tych dniach gdy sugerowano, że niektórzy pracownicy HMO i agencji ubezpieczeniowych praktykowali usługi medyczne bez specjalnego wykształcenia i pozwoleń. Oto na jakiej zasadzie to działa: Twój lekarz sugeruje Ci kosztowną kurację, jednakże Twoja agencja ubezpieczeniowa nie zgadza się na refundację kosztów leczenia, gdyż (według nich) nie jest to konieczne. No i nie masz leczenia. A agencja ubezpieczeniowa twierdzi, że nie zajmują się medycyną. Oczywiście nic nie stoi na przeszkodzie, abyś samemu zapłacił cały koszt leczenia; jednakże, wziąwszy pod uwagę koszty z tym związane, odmowa refundacji kosztów leczenia przez agencję ubezpieczeniową, jest równoznaczna z uniemożliwieniem Ci tego leczenia.

Programowanie w systemie Windows ma kilka interesujących podobieństw do powyższej sytuacji. Technologia OLE jest bardzo trudna do użycia, a więc używasz MFC. To bardzo wiele upraszcza. Oczywiście możesz samemu spróbować zaimplementować obsługę OLE (zabierze Ci to około roku ciężkiej pracy). Jeśli jednak będziesz chciał to zrobić w trzy dni, to najprawdopodobniej będziesz musiał skorzystać z MFC.

Ogólnie rzecz biorąc MFC jest bardzo dobrą biblioteką. Jeśli jednak chcesz użyć jakichkolwiek jej możliwości, to automatycznie musisz zaakceptować wszystkie roz­wiązania, które narzuca. Nie możesz użyć tylko fragmentu MFC obsługującego OLE (tak jak nie możesz używać samego podglądu wydruku lub okien dzielonych). Jeśli chcesz używać MFC, to musisz się zgodzić na to, iż wszystko będziesz musiał robić w sposób narzucony przez MFC.

Wewnątrz MFC można znaleźć kilka interesujących fenomenów. Czy wiedziałeś, że można używać dynamicznej wymiany danych (DDX) z każdym kontrolowanym oknem potomnym? Jednakże Class Wizard pomaga Ci w obsłudze okien jedynie kilku określonych typów (na przykład — okien dialogowych). Ze względu na to, że używanie DDX bez kreatora Class Wizard jest mało udokumentowane, prowadzi to w efekcie do ograniczenia Twoich możliwości używania DDX.

Jedną ze wspaniałych rzeczy wynikających z bycia programistą, jest to, że musisz tworzyć. Czasami myślę, że dobrze wykonane oprogramowanie jest jedną z najczystszych form kreacji jakie są dostępne dla człowieka. Pomyśl o tym. We śnie przychodzi Ci do głowy pomysł na nowy program; stukasz kilka godzin w klawiaturę, no i proszę - stworzone zostało nowe dzieło. Jeśli piszesz dostatecznie dobrze i szybko, to dzieje się to tak, jak gdyby program “wlewał" się z Twojego mózgu do komputera. Narzędzia takie jak MFC powinny pomagać Ci w tworzeniu i realizowaniu Twoich pomysłów - nie przeszkadzać im.


Dawno temu...

Pierwszymi komputerami na jakich tworzyłem swoje pierwsze poważne programy były projektowane przeze mnie mikroprocesory. Wtedy pomysłowość była najważniejsza. Napisać mogłem wszystko co mi się przyśniło (oczywiście w granicach możliwości pamięci operacyjnej o wielkości 4KB). Problem polegał na tym, że musiałeś wymyślić każdy, nawet najdrobniejszy szczegół oprogramowania. Za tamtych dni spędzałem długie godziny tworząc kod pobierający i wysyłający bajty przez obsługiwany programowo port RS232. Musiałem liczyć przerwania, aby wiedzieć jaka jest pora dnia. Cóż, jak widać konieczność zbyt dużej twórczości nie jest rzeczą dobrą.

Dziś nie musisz już zawracać sobie głowy tymi drobnymi szczegółami. Jeśli chcesz coś wysłać RS-em, to po prostu “otwierasz" go i wpisujesz do niego poszczególne bajty. Może być jeszcze lepiej -jeśli chcesz obsługiwać mysz, modem lub drukarkę, to istnieją procedury wyższego poziomu umożliwiające bezpośredni dostęp do tych urządzeń z pominięciem portu szeregowego, przy pomocy którego urządzenia te są podłączone do Twojego komputera. Jednakże to co jest zyskiem w szybkości tworzenia oprogra­mowania, jest jednocześnie stratą na kreatywności. Podczas gdy kiedyś mogłeś kontrolować każdy szczegół, teraz musisz się zadowalać możliwościami udostępnianymi Ci przez system operacyjny.

Szczerze mówiąc, nie musi to od razu być złą rzeczą. Kto przy zdrowych zmysłach chciałby pisać swoje własne procedury obsługujące odczyt i zapis na twardym dysku? Czy procedury takie działałyby ze wszystkimi dyskami dostępnymi na rynku? Czy na­prawdę chcesz pisać procedury obsługujące wszystkie dostępne na rynku drukarki i karty graficzne? Zazwyczaj odpowiedź brzmi: nie. Jednakże, to właśnie są szczegóły niskiego poziomu. A co ze szczegółami wysokiego poziomu, takimi jak styl interfejsu użytkownika? System Windows narzuca na Ciebie ograniczenia nawet na tym pozio­mie. Ale właśnie w tym tkwi sedno sprawy - im wyższego poziomu jest narzędzie, tym większe narzuca ono ograniczenia.

Wspaniałym przykładem jest język programowania Visual Basic. VB jest językiem doskonałym do tworzenia pewnego typu programów. Jeśli Twój program robi rzeczy, które można napisać w Visual Basicu, to byłbyś szalony gdybyś nie użył tego właśnie języka. Wskazujesz myszką, klikasz - i gotowe. Ale co się dzieje w momencie, gdy Twój program ma robić rzeczy, do których Visual Basic nie jest przystosowany? Teraz sprawa się komplikuje. Czasami możesz nagiąć Visual Basic i zmusić go do wykonania rzeczy, do których nie jest on przystosowany; jednakże to wymaga wiele pracy. Czasa­mi może to wymagać ogromnej pracy. Może dojść do tego, że znacznie lepiej będzie użyć innego - bardziej elastycznego języka programowania. Visual Basic, podobnie jak wszelkie inne narzędzia narzuca pewne ograniczenia na sposób tworzenia oprogramowania.


Ograniczenia narzucane przez MFC

Jakie ograniczenia narzuca na Ciebie MFC? Jest ich wiele, nawet jeśli nie zdajesz sobie z tego sprawy. MFC ogranicza Cię na kilka sposobów:

MFC ma ściśle określoną architekturę, od której zależy poprawne działanie poszczególnych elementów biblioteki.

Gdy MFC udostępnia komponent lub klasę, to czasami znacznie łatwiej jest użyć tej udostępnianej klasy (niezależnie do jej możliwości) niż samemu tworzyć podobną klasę lub komponent.

Narzędzia udostępniane przez MFC (App Wizard oraz Class Wizard) działają tylko i wyłącznie z określonymi typami programów. Tworzenie programów innego typu jest znacznie trudniejsze, gdyż narzędzi tych nie możesz użyć.

Cała ta książka poświęcona jest sposobom przezwyciężania ograniczeń narzucanych na Ciebie przez MFC. Czasami sprowadza się to do dyskusji o trudnych i pracochłonnych rozwiązaniach. Czasami rozwiązania te będą całkiem proste, zwłaszcza jeśli dysponu­jesz odpowiednimi wiadomościami. Za każdym razem jednak będziesz osiągał swoje cele wykorzystując do tego MFC. Nie masz czasu na sztuczki, które mogą nie działać z kolejnymi wersjami biblioteki.

Oczywiście bycie odmiennym dla samej idei nie jest dobre. Dla przykładu, umieszczenie menu Plik w samym środku paska menu łamie słynną Zasadę Minimalizacji Szoku (ang.: Shock Minimization Principle - SMP). Zasada ta jest jednym z aksjomatów projektowania oprogramowania, głosi ona iż: Oprogramowanie powinno działać w taki sposób, aby minimalizować szok wywierany na użytkownikach.

Z drugiej jednak strony, istnieje wiele przypadków, w których chciałbyś zrobić coś troszkę inaczej. Na pewno chciałbyś wypróbować swoją kreatywność i stworzyć coś, czego nikt inny jeszcze nie widział, nieprawdaż? Taki właśnie jest cel niniejszej książki: pomóc Ci w realizacji Twoich wizji oprogramowania.


Jak używać tej książki

Aby w pełni wykorzystać informacje dostarczane przez tę książkę powinieneś dysponować Visual C++ 5.0 lub wersją późniejszą. Najprawdopodobniej będziesz mógł wykorzystywać większość prezentowanych technik używając innych wersji kompilatorów C++ stworzonych przez firmę Microsoft jak i inne firmy; jednakże wszystkie przykłady umieszczone w tekście książki oraz na CD ROMie używają VC++ 5.0.

Każdy rozdział poświęcony jest innym zagadnieniom. Pierwsza część rozdziału zawiera szczegółowe informacje dotyczące omawianego zagadnienia. Część druga, przedstawia konkretne problemy oraz gotowe “przepisy" na ich rozwiązanie. Mogą się tam także pojawić odwołania do artykułów technicznych dotyczących MFC, stron WWW oraz artykułów prasowych.

Każdy rozdział może być traktowany jako odrębna część, niezależna od pozostałych rozdziałów. Jeśli chcesz rozwiązać konkretny problem, to najlepiej zrobisz przeglądając praktyczny przewodnik umieszczony na końcu każdego z rozdziałów. Oczywiści, umieszczone tam przykłady służą jedynie wskazaniu Ci możliwych sposobów rozwią­zywania problemów, W końcu chcesz sprawdzić swoje możliwości twórcze.

Może masz swoją własną wizję oprogramowania, które chcesz tworzyć. Może Twoi klienci, Twój szef lub konkurencja wymusza na Tobie użycie konkretnych sposobów tworzenia oprogramowania. W każdym z tych przypadków przeczytaj odpowiedni rozdział i poznaj w jaki sposób możesz rozwiązać stojący przed Tobą problem innymi metodami.


Rozdział 1 Architektura

Zanim rozpoczniesz zgłębianie zaawansowanych technik programistycznych z wyko­rzystaniem MFC, będziesz musiał posiąść bardzo solidną znajomość (i zrozumienie) podstawowej architektury MFC - w tym architektury dokument/widok, map komunikatów, analizy linii poleceń, wyprowadzania klas potomnych oraz używania kolekcji.

Starożytni Rzymianie nie byli dobrymi z matematykami. Oczywiście nie pomógł im także fakt, iż nie zadali sobie trudu odkrycia liczby zero. W świecie starożytnym największymi matematykami byli arabowie. Swoją wiedzę zdobywali od uczonych ze starożytnych Indii, którzy zaczęli interesować się matematyką około 200 roku p.n.e. Z drugiej strony atlantyckiej sadzawki Majowie także znali liczbę zero i byli bardzo zaawansowanymi i wyrafinowanymi matematykami. Ich kalendarz był znacznie doskonały od tego, którego my używamy dzisiaj.

Wyobraź sobie co by się stało, gdyby starożytni mędrcy zaczęli liczyć w systemie dwójkowym, a nie w dziesiętnym - moglibyśmy policzyć na palcach do 1024. To w cale nie jest tak abstrakcyjne, jak mógł byś pomyśleć. Germanie oraz Celtowie posługiwali się systemem dwunastko wy m. Oto dlaczego cały czas używamy tuzinów, a na zegarkach jest dwanaście godzin (nie wspominając już o dwunastu calach na stopę).

Ale chodzi o to, aby zdać sobie sprawę z tego, że narzędzia jakich używamy mogą mieć znaczny wpływ na sposób rozwiązania problemu. Weźmy na przykład języki piktograficzne używane w Chinach i Japonii. Języki te ciężko zastosować w komputerach. Fakt ten był głównym problemem wprowadzania komputeryzacji na Dalekim Wschodzie. Dla przykładu, użytkownik piszący na laptopie mógł zapisać słowo fonetycznie używając do tego alfabetu Kanji. Po wpisaniu słowa program mógłby wyświetlić kilka piktogra­mów (o tych samych znaczeniach), a użytkownik mógłby wybrać ten, którego chce użyć. Rozwiązanie takie nie byłoby jednak zbyt efektywne. Z drugiej jednak strony, języki piktograficzne są znacznie prostsze do pisania ręcznego. Wiele japońskich komputerów typu palmtop pozwala na ręczne wpisywanie znaków na monitorze. Takimi samymi możliwościami dysponują jedynie nieliczne komputery tej samej klasy produkowane w krajach anglojęzycznych, a jeśli już, możliwości te nie są zbytnio zaawansowane i dobre. Nasze pisane litery nie są dostatecznie charakterystyczne, aby komputery były w sianie je bez problemów rozpoznać.

Każda cecha używanego środowiska programistycznego ma wpływ na Twoje programy. Fakt, iż piszesz programy, które mają działać w systemie Windows, ma kolosalny wpływ na sposób w jaki będziesz tworzył programy. MFC, C++ oraz inne narzędzia, których będziesz używał przy tworzeniu kodu, także będą miały wpływ na tworzone przez Ciebie oprogramowanie.

Jednym z zadań tej książki jest nauczenie Cię rozwiązywania problemów w niestandardowy sposób; zanim jednak będziesz mógł zabrać się do pracy, musisz zrozumieć zasady działania używanych przez Ciebie narzędzi (w naszym wypadku VC++ oraz MFC). Ist­nieją po temu dwa główne powody. Po pierwsze, musisz wiedzieć jak działa MFC, aby zmusić je do zrobienia tego, czego chcesz. Po drugie, nie jesteś w stanie dowiedzieć się jak niektóre rzeczy są robione w MFC, gdyż do ich zrobienia wykorzystujesz narzędzia (np.: Class Wizard lub App Wizard), które, de facto, wykonują całą robotę za Ciebie. Kreatory podobne są do narzędzi VCR wykorzystujących kody VCR Plus. Wszystko jest w porządku, do momentu gdy dysponujesz odpowiednimi kodami. Jeśli jednak chcesz napisać, coś co nie posiada odpowiadającego sobie kodu VCR Plus, masz duży problem. Nie tylko będziesz musiał stworzyć własne oprogramowanie VCR, lecz, co gorsza, zapewne nawet nie będziesz wiedział jak się do tego zabrać.

Pierwszym celem tego rozdziału jest omówienie wszystkich podstawowych składników MFC oraz ich związku z ogólną architekturą biblioteki. Drugim celem jest rozebranie na części programu wygenerowanego przez App Wizarda i poznanie tajemnic jego za­chowania.

Jeśli już dużo programowałeś przy użyciu MFC, możesz myśleć, że przeczytanie tego rozdziału nic Ci nie da. Jeśli nie chcesz, nie musisz go czytać. Ale spróbuj odpowiedzieć na zadane poniżej pytania; jeśli będziesz potrafił podać odpowiedzi, pomiń ten rozdział. Zawsze jednak warto przeczytać praktyczny przewodnik umieszczony na końcu tego rozdziału. Poniżej podane zostały pytania:

Jaka jest jedyna klasa, którą musi posiadać program wykorzystujący MFC?

W jaki sposób możesz ręcznie stworzyć mapę komunikatów?

W jaki sposób dołączyć do dokumentu dodatkowe widoki?

W jaki sposób można stworzyć nowe typy dokumentów?

Kiedy dokument nie obsługuje poleceń wejścia/wyjścia?

Kiedy widok nie realizuje wizualizacji danych zawartych w dokumencie?

Czy można umieścić uchwyt do menu w klasie dokumentu?

Dlaczego klasa CRect nie może być klasą potomną klasy CObject?

Czy szablony dokumentów muszą być przechowywane w obiekcie aplikacji?

Dlaczego trudno jest stworzyć dwuwymiarową tablicę za pomocą szablonów i klasy CArray?


Warcaby

W skład biblioteki MFC wchodzi wiele klas. Jednak tylko kilka z nich należy do pod­stawowych komponentów, które mają wpływ na architekturę tworzonych programów. Pozostałe z nich nie mają większego wpływu na sposób, w jaki piszesz programy.

Tymi podstawowymi klasami są: CWinApp, CView, CDocument, CFrameWnd oraz CDocTemplate. Chociaż klasy te tworzą fundament większości programów pisanych przy użyciu MFC, to jednak nie musisz z nich korzystać za każdym razem. De facto, jedyną klasą absolutnie konieczną do stworzenie programu MFC jest klasa CWinApp. Aby móc jednak w pełni wykorzystać możliwości dostarczane przez MFC, będziesz musiał użyć także pozostałych czterech podstawowych klas (lub ich klas potomnych).

Potęgą MFC jest technika zwana czasami “projektowaniem poprzez różnice". Pomysł ten polega na tym, iż MFC dostarcza klas, za pomocą których można stworzyć modelowy program działający w systemie Windows. Jednakże ten modelowy program nie robi niczego ciekawego. Twoim zadaniem, jako programisty wykorzystującego MFC, jest stworzenie kodu, który spowoduje, iż Twój program będzie się różnił do wszystkich innych pro­gramów. MFC zadba o zapewnienie standardowego zachowania programu, natomiast Ty będziesz mógł wybiórczo zmodyfikować sposób jego działania.

Klasa CWinApp reprezentuje Twój program działający w pamięci operacyjnej komputera. Użytkownik nigdy bezpośrednio nie widzi niczego, co jest związane z tą klasą. Jest to kluczowe miejsce, w którym można uzyskać dane dotyczące aplikacji (na przykład, parametry wywołania programu, zasoby, uchwyt instancji, itd.).

Podstawowe metody klasy CWinApp przedstawione zostały w tabeli 1.1. Najważniejsze z nich są dwie metody, które użytkownik może przesłaniać w swoich aplikacjach - Initlnstance oraz Exitlnstance. Zazwyczaj w metodzie InitInstance kreator App Wizard umieszcza kod tworzący główne okno aplikacji oraz wykonujący inne czynności inicjalizacyjne. Można sobie wyobrazić sytuację, w której wszystkie operacje programu byłyby wykonywane właśnie w tej metodzie. Po ich wykonaniu metoda InitInstance zwróciłaby wartość FALSE kończąc działanie programu. Kreator App Wizard postąpi w opisany powyżej sposób w momencie, gdy poprosisz go o stworzenie aplikacji, której oknem głównym jest okno dialogowe (patrz Listing 1.1). W takim przypadku program powoduje wyświetlenie okna dialogowego; jednakże jeśli chcesz, może on zrobić cokolwiek innego (o ile nie jest do tego konieczna pętla przetwarzająca komunikaty).



Tabela 1.1 Podstawowe metody i dane klasy CWinApp

Nazwa Opis

m_pszAppName Nazwa aplikacji.

m_lpCmdLine Linia poleceń użyta do uruchomienia aplikacji.

m_hInstance Uchwyt aplikacji.

m_hPrevInstance Poprzedni uchwyt aplikacji.

m_pMainWnd Główne okno aplikacji.

m_bHelpMode TRUE jeśli aplikacja korzysta z pomocy kontekstowej.

m_pszExeName Nazwa pliku EXE.

m__pszProf ileName Nazwa pliku INI.

m_pszRegisteryKey Nazwa klucza rejestru systemowego który może być używany zamiast pliku INI.

LoadCursor Ładuje kursor aplikacji.

LoadStandardCursor Ładuje kursor systemowy (IDC_ *).

LoadOemCursor Ładuje kursor OEM (OCR_ *).

Loadlcon Ładuje ikonę aplikacji.

LoadStandardlcon Ładuje ikonę systemową (IDI_ *).

LoadOemlcon Ładuje ikonę OEM (OIC_ *).

ParseCommandLine Powoduje inicjalizację obiektu klasy CCommandLinelnfo na podstawie danych umieszczonych w linii wywołania programu.

ProcessShellCommand Obsługuje polecenia systemowe umieszczone w obiekcie CCommandLinelnfo.

GetProfileInt Pobiera liczbę całkowitą z pliku INI.

WriteProfileInt Zapisuje liczbę całkowitą do pliku INI.

GetProfileString Pobiera łańcuch znaków z pliku INI.

WriteProfileString Zapisuje łańcuch znaków do pliku INI.

AddDocTemplate Dodaje szablon dokumentu do aplikacji.

GetFirstDocTemplatePosition Określa pozycję pierwszego szablonu dokumenty na liście.

GetNextDocTemplate Pobiera następny szablon dokumentu z listy.

OpenDocumentFile Otwiera plik dokumentu o podanej nazwie.

AddToRecentFileList Dodaje nazwę do listy ostatnio otworzonych plików (w menu).

CreatePrinterDC Pobiera kontekst (DC) aktualnej drukarki.

GetPrinterDeviceDefaults Pobiera domyślne ustawienia drukarki, uzyskując je z pliku WIN.INI lub ostatniej przeprowadzonej konfiguracji drukarki.

InitInstance Inicjalizuje aplikację.

Run Pętla obsługi komunikatów systemowych.

Onldle Obsługa czasu, w którym aplikacja nic nie robi.

ExitInstance Obsługa zakończenia programu.

PreTranslateMessage Filtrowanie komunikatów.

SaveAllModified Prośba o zapisanie wszystkich zmodyfikowanych dokumentów.

DoMessageBox Metoda, którą można przesłonić w celu zmodyfikowania działania procedury AfsMessageBox.

ProcessWndProcException Domyślna procedura obsługująca wyjątki.

ProcessMessageFilter Obsługuje komunikaty trafiające do filtra,

DoWaitCursor Włącza i wyłącza kursor o postaci klepsydry.

WinHelp Otwiera plik pomocy.

LoadStdProfileSettings Ładuje domyślne ustawienia z pliku INI oraz zawartość listy ostatnio otworzonych plików.

SetDialogBkColor Określa kolor tła okien dialogowych.

SetRegistryKey Informuje MFC, aby do przechowywanie informacji używa­ny był rejestr systemowy, a nie plik INI.

EnableShellOpen Umożliwia otwieranie dokumentów metodą przeciagnij-i-upuść.

RegisterShellFileTypes Rejestruje typy obsługiwanych dokumentów.

OnFileNew Domyślna procedura obsługi menu.

OnFileOpen Domyślna procedura obsługi menu.

OnFilePrintSetup Domyślna procedura obsługi menu.

OnContextHelp Domyślna procedura obsługi menu.

OnHelp Domyślna procedura obsługi menu.

OnHelpIndex Domyślna procedura obsługi menu.

OnHelpFinder Domyślna procedura obsługi menu.

OnHelpUsing Domyślna procedura obsługi menu.


Pierwszorzędnym przykładem projektowania poprzez różnice jest przesłonięcie metod Initlnstance oraz Exitlnstance Domyślna implementacja metody Initlnstance nie robi niczego. Z dużą szansą na wygraną mogę się założyć, że Twój program będzie musiał stworzyć główne okno oraz wykonać inne czynności inicjalizacyjne. Dlatego też prawie zawsze będziesz musiał przesłonić domyślną definicję metody Initlnstance. Swoją drogą, wcale nie jest powiedziane, że będziesz musiał cokolwiek robić podczas kończenia programu. Jeśli tak właśnie jest, nie będziesz musiał pisać własnej metody Exitlnstance. Oczywiście, jeśli musisz stworzyć kod obsługujący kończenie aplikacji, nic nie stoi na przeszkodzie, abyś tak zrobił.

Największe korzyści z używania MFC oraz potęga tej biblioteki (ujawniająca się w takich narzędziach jak podgląd listingu czy dzielone okna) dostępne są jedynie wtedy, gdy korzystasz z architektury dokument/widok. Jest to podstawowa cecha znakomitej większości programów używających MFC.

Listing 1.1. Fragment z aplikacji bazującej na oknie dialogowym stworzonej przez kreatora App Wizard.

BOOL CDlgdemoApp::Initlnstance()

{

AfxEnableControlContaiher();

// Standard initialization

// If you arę not using these features and wish to reduce the size

// of your finał executable, you should remove from the

// following the specific initialization routines you do not need

.#ifdef _AFXDLL

Enable3dControls(); // Call this when using MFC in a shared dll

#else

Enable3dControlsStatic(); // Call this when linking to MFC statically

#endif

CT4Dlg dlg;

m_pMainWnd = &dlg;

int nResponse = dlg. DoModal ( ) ;

if (nResponse == IDOK)

{

// TODO: Place code here to handle when the dialog is dismissed with OK }

else if (nResponse == IDCANCEL) {

// TODO: Place code here to handle when the dialog is dismissed with Cancel

// Sińce the dialog has been closed, return FALSE so that we exit the application, rather than start the application' s

// message pump.

return FALSE;

}

Aby w pełni zrozumieć architekturę dokument/widok, rozpatrzmy przypadek, w którym z niej nie korzystamy. Załóżmy, że napisałem prosty program arkusza kalkulacyjnego, którego działanie polega na wyświetlaniu danych odczytywanych z czujników atmosfe­rycznych. Program nie jest zbyt wyrafinowany - prosta tablica komórek, w których można umieszczać liczby oraz formuły. Zadanie było zbyt proste, aby używać architektury dokument/widok. W programie rysowanie siatki było, po prostu, integralną częścią przetwarzania danych. W kilka miesięcy później Ty odziedziczyłeś mój arkusz kal­kulacyjny, a szef kazał Ci dorobić do niego opcję umożliwiającą rysowanie wykresów słupkowych.

Okazuje się, że problem już nie jest taki prosty. Będziesz bowiem musiał przerobić cały mój kod służący do rysowania arkusza, tak aby analizował on, czy rysowany jest wykres, czy normalna siatka z danymi. A co zrobić jeśli będziesz chciał jednocześnie wyświetlić te same dane na oba sposoby? Albo jeszcze lepiej - co zrobić jeśli będziesz chciał wyświetlić dwa, różne komplety danych, każdy w odrębny sposób? Cóż - wpadłeś w poważne problemy.

W takim wypadku zadanie będzie znacznie prostsze, jeśli skorzystasz z architektury dokument/widok. W pierwszej kolejności definiujesz obiekt dokumentu (wyprowadzony z klasy CDocument). Obiekt ten odpowiedzialny jest za obsługę danych programu (w naszym przypadku są to, liczby, formuły oraz dane z czujników). Jego zadaniem jest także zapisywanie danych na dysku, odczytywanie ich oraz ewentualne przeliczenie wszystkich formuł. Jednakże obiekt ten nie jest w żaden sposób odpowiedzialny za rysowanie czegokolwiek na ekranie. Obsługa interfejsu użytkownika to nie jego zmartwienie.

Rysowanie oraz obsługa interfejsu użytkownika jest zadaniem klasy widoku (wyprowadzo­nej z klasy CView). Każdy widok odwołuje się do jednego dokumentu. Zadania klasy widoku są następujące:

Rysowanie graficznej reprezentacji danych umieszczonych w dokumencie skojarzonym z widokiem.

Pobieranie i obsługa czynność wykonywanych przez użytkownika na przedsta­wionych danych (np. kliknięć myszką) i odpowiednie modyfikowanie dokumentu.

Przedstawiona powyżej architektura ma wiele zalet. Przypomnij sobie przedstawiony przed chwilą przykład arkusza kalkulacyjnego. Tym razem wyobraź sobie, że na samym początku zastosowałem model dokument/widok. W takim wypadku dodanie wykresu słupkowego jest trywialnie proste. Proste jest także modyfikowanie dokumentu, na przykład, odczytanie wskazań innego czujnika.

Proste jest także stworzenie jednego dokumentu, który w tym samym czasie steruje wieloma widokami. Przykład takiego rozwiązania będziesz mógł zobaczyć w przewodniku praktycznym, znajdującym się pod koniec tego rozdziału. Użytkownicy nigdy nie operują bezpośrednio na dokumencie; dane prezentowane są jedynie w widoku i za jego pomocą mogą być modyfikowane.


Ale mój program nie działa w taki sposób!

Często programiści uważają, że ich program nie pasuje do architektury dokument/widok. Czasami mają rację, jednak w większości wypadków ich programy dadzą się dopasować do tego modelu- zwłaszcza po chwili zastanowienia. W zasadzie można obwinie firmę Microsoft o powstanie tego problemu, gdyż jest to jedynie kwestia nazwy.

Kluczem jest tutaj słowo “dokument". Zazwyczaj myślisz o dokumencie jako o pliku zapisanym na dysku. Faktycznie, obiekt dokumentu często (ale nie zawsze!) reprezentuje taki plik. Jeśli będziesz pisał arkusz kalkulacyjny, Twój dokument najprawdopodobniej faktycznie będzie reprezentował plik. Ale co będzie reprezentował dokument w programie służącym do wyświetlania “na bieżąco" danych odczytywanych z czujników atmosfe­rycznych? W takim wypadku dokument będzie reprezentował dane odczytywane z czujników. Jeśli będziesz pisał program monitorujący obciążenie sieci, Twój doku­ment będzie zawierał dane dotyczące jej obciążenia i efektywności pracy.

Ten sam schemat używany jest w okienkowych aplikacjach tworzonych w języku Smalltalk. Schemat ten nazywany jest architekturą “Sterownika Widoku Modelowego"(widok w rozumieniu MFC jest połączeniem widoku ze Smalltalka oraz sterownika). Model używany w Smalltalku jest odpowiednikiem dokumentu w MFC. Wolę używać słowa “model", gdyż dzięki temu łatwiej będzie Ci zrozumieć, że wcale nie musi to być plik.

Czym jest dokument?

Dokument jest jedynie abstrakcyjną reprezentacją danych używanych w programie. Nie ma żadnego znaczenia skąd dane te pochodzą.

Czasami trzeba się trochę zastanowić, aby zdecydować, co będzie zapisywane w doku­mencie (lub, w tym wypadku, pliku). Jakiś czas temu napisałem program, którego działanie imitowało terminal (program ten pojawił się w magazynie PC Techniques 12/1 1996 teraz magazyn ten nosi nazwę Visual Developer Magazin). Był to klasyczny przykład sytuacji, w której może się zdawać, że schemat dokument/widok nie pasuje (zwłaszcza, że jako klasy widoku użyłem CeditView(). Co można zapisać w pliku, w programie takiego typu? Okazuje się, że są to dane konfiguracyjne. Kiedy ładujesz plik do takiego programu, nie jesteś zainteresowany zobaczeniem ostatnich 200 linii sesji prowadzonej z innym komputerem. Oczekujesz natomiast, aby program pamiętał, że pracowałeś na porcie COM 2, z prędkością transmisji 9600 bodów i brakiem parzystości.

Zapisywanie danych dokumentu

Nic nie zabrania przechowywania w dokumencie danych, które nie są stałe - nie wymagają zapisywania. Zapisywać możesz jedynie te infor­macje, które będziesz chciał później odtworzyć.

Praktycznie każdy program, opierający swoje działanie na wymianie informacji z użyt­kownikiem, może zostać podzielony na dwie części: dane (zapisane w dokumencie) oraz graficzną reprezentację tych danych, na której użytkownik może operować (czyli widok). Twoim zadaniem jest wymyślenie sposobu dokonania podziału w konkretnym przypadku Twojego programu.


Test przydatności oficerów armii

Czy kiedykolwiek poddałeś się testowi przydatności oficerów armii? Trwa on tylko kilka minut. Oto cały test: Jesteś sierżantem. Do dyspozycji masz kaprala, dwóch szeregowców, dwa drążki o długości 9 cali, jeden drążek o długości 11 cali, trzy kawałki linki o długości 6 cali każdy, łopatę oraz flagę USA. W 30 sekund lub mniej powiedz mi, w jaki sposób zbudować maszt flagowy o wysokości 14 stóp i 5 cali?

Poprawna odpowiedź brzmi: “Kapralu, flaga w górę!"

Wbrew pozorom, przykład ten ma wiele wspólnego z MFC. Może zauważyłeś, że w przedstawionych wcześniej rozważaniach nie wspominałem o tym, co robią dokument i widok. Zamiast tego mówiłem jedynie o tym, za co każdy z tych elementów jest od­powiedzialny. Chociaż MFC upiera się przy tym, aby każdy z obiektów był odpowiedzialny za wykonywanie określonych czynności, wcale nie jest powiedziane, że dany obiekt musi te czynności wykonywać.

W większości wypadków obiekt będzie wykonywał czynności, za które jest odpowiedzial­ny. Jednak istnieje możliwość, żeby obiekt przekazał wykonanie określonej czynności innemu obiektowi. Dla przykładu, możesz dysponować obiektami nie związanymi z MFC, które będą doskonale wiedziały, w jaki sposób należy graficznie przedstawić Twoje dane. Nie ma żadnego problemu - użyj tych obiektów wewnątrz obiektu widoku.

Innym przykładem jest klasa CEditView (klasa widoku udostępniana przez MFC i po­zwalająca na edycję tekstu). Klasa ta potrafi odczytywać i zapisywać pliki tekstowe. Cały czas jednak za odczytywanie i zapisywanie danych odpowiedzialny jest dokument. Jeśli dokument zdecyduje, aby odnaleźć obiekt widoku i zlecić mu zapisanie lub odczytanie pliku, biblioteka MFC nie będzie miała nic przeciwko temu.

Klasa CEditView jest doskonałym przykładem dlaczego zlecać wykonywanie niektórych czynności innym obiektom. Załóżmy, że tworzysz klasę, która obsługuje pliki GIF. Bę­dziesz chciał udostępnić swoją klasę innym programistom tworzącym wiele innych programów. Oczywiście, mógłbyś stworzyć klasy CGIFView oraz CGIFDoc, jednakże nie dadzą one wielkich możliwości wielokrotnego używania. Niektórzy programiści mogą chcieć umieścić obsługę plików GIF w swoim programie wraz z wieloma innymi możliwościami. Oczywiście, mogą oni spróbować stworzyć nowe klasy potomne, lecz co zrobić w sytuacji, gdy będą chcieli użyć klas CPCXView i CPCXDoc (aby obsługiwać pliki zapisane w formacie PCX)? To może spowodować problemy, gdyż MFC nie obsługuje wielokrotnego dziedziczenia.

Dzięki użyciu zlecania można znaleźć dwa sposoby rozwiązania tego problemu. Po pierwsze, możesz stworzyć klasę CGIFDoc, która potrafiłaby odczytywać i zapisywać pliki GIF oraz wiedziała jak je rysować. Dzięki temu widok byłby odpowiedzialny jedynie za wywołanie metody rysującej dostępnej w obiekcie dokumentu. Jeśli program wymagałby użycia wielu dokumentów, mógłbyś skonstruować jeden dokument nadrzędny, który obsługiwałby wszystkie dokumenty wykorzystywane w programie.

Innym rozwiązaniem mogłoby być stworzenie niezależnej klasy (powiedzmy CGIF), której metody mogłyby być wywoływane przez obiekty dokumentu i widoku w celu wykonania odpowiednich czynności. Klasa ta nie musiałby by być w jakikolwiek sposób związana z innymi klasami MFC (chociaż mogłaby ona być klasą potomną klasy CObject; o tej klasie dowiesz się czegoś więcej w dalszej części rozdziału).

Korzystanie z klas MFC

Nie musisz umieszczać całego swojego kodu w klasach MFC. Nic nie ogranicza Twoich możliwości tworzenia nowych klas, spełniających wymagania Twojego programu. Dzięki temu możesz wielokrotnie wykorzystywać kod lub używać właściwości, których klasy MFC nie posiadają (jak na przykład wielokrotne dziedziczenie).


Okna ramek

Widok nie może tak po prostu pojawić się na środku ekranu. Użytkownik zazwyczaj oczekuje, że widok będzie posiadał wszystkie charakterystyczne atrybuty, takie jak: ramkę umożliwiającą modyfikowanie wielkości okna programu, pasek tytułowy, menu systemowe i tym podobne. Choć nie jest to oczywiste dla wszystkich użytkowników, rzeczy te umieszczane są w oknie ramki. To, co użytkownicy uważają zazwyczaj za jedno okno jest w rzeczywistości dwoma elementami - oknem ramki oraz widokiem.

Istnieją dwa podstawowe rodzaje ramek. Programy SDI (ang.: Single Document Interface - Pojedynczy Interfejs Dokumentu) mogą posiadać tylko jedno okno. Najprostszym przykładem programu SDI jest standardowy program Notepad (patrz rysunek 1.1). Program ten posiada tylko jedno okno i umożliwia edycję tylko jednego dokumentu. Jeśli Notepad byłby programem napisanym z użyciem biblioteki MFC (o ile wiem, Notepad nie korzysta z MFC), jego wnętrze byłoby widokiem, a ramka, pasek menu i menu systemowe należałoby do obiektu klasy wyprowadzonej z klasy CFrameWnd.

Drugim rodzajem programów są programy MDI (ang.: Multiple Document Interface -Wielokrotny Interfejs Dokumentu). Programy te używają obiektów klasy CMDIFrameWnd jako swojego głównego okna. Programy tego typu (na przykład Microsoft Word) umieszczają każdy otwierany dokument w odrębnym oknie potomnym (patrz rysunek 1.2). Okno potomne składa się z obiektu klasy wyprowadzonej z klasy CView oraz z obiektu klasy CMDIChildWnd.

Widoki oraz ramki są obiektami klas potomnych ogólnej klasy reprezentującej okno - CWnd. Wszystko, co możesz zrobić z widokiem lub ramką, możesz także zrobić z całym oknem programu. Wynika z tego, że programy SDI składają się z dwóch osobnych okien (widoku oraz ramki). Programy MDI natomiast, posiadają jedno okno ramki, jak również ramkę oraz widok dla każdego otworzonego dokumentu. Programy MDI po­siadają także swoje okno robocze, które jest jednak całkowicie ignorowane przez MFC (patrz rysunek l .2). Okno to znajduje się wewnątrz głównej ramki okna programu. Cho­ciaż okna lego możesz nie widzieć, o jego istnieniu możesz się przekonać, za pomocą programu Spy. Jest on oknem rodzicielskim dla ramek dokumentów. Gdyby nie było tego okna, ramki poszczególnych dokumentów mogłyby przesłaniać pasek narzędzi oraz pasek stanu programu. W każdym przypadku główna ramka posiada menu, pasek stanu, paski narzędzi oraz wszelkie inne elementy tego typu.

Prowadzi to do bardzo interesującego pytania: Czy cały kod obsługi menu umieszczany jest w obiekcie ramki? Odpowiedź na to pytanie brzmi: nie. Aby dowiedzieć się w jaki sposób obsługiwane jest menu, będziesz musiał poznać sposób, w jaki programy MFC obsługują komunikaty.


Kilka słów o mapach komunikatów

Jeśli korzystasz tylko i wyłącznie z kreatora Class Wizard, możesz nie zdawać sobie sprawy z. tego, w jaki sposób wykonuje on swoje magiczne sztuczki. W jakiś dziwny sposób sprawia on, że komunikaty systemu Windows powodują wywoływanie odpowiednich metod Twojej klasy.

Jeśli miałbyś zaprojektować bibliotekę klas, to może uległbyś pokusie, aby użyć funkcji wirtualnych do obsługi komunikatów. Dla przykładu, klasa CWnd mogłaby posiadać metodę wirtualną o nazwie OnSize. Dzięki takiemu rozwiązaniu pętla obsługująca ko­munikaty mogłaby wywoływać metodę OnSize w odpowiedzi na komunikat WM_SIZE.

Rozwiązanie takie działałoby poprawnie w wielu sytuacjach. Jednakże posiada ono dwie główne wady:

Większość okien obsługuje jedynie niewielką ilość komunikatów każde okno musiałoby posiadać ogromną tablicę funkcji wirtualnych, która zawierałaby metody odpowiadające wszystkim dostępnym komunikatom.

Funkcje wirtualne nie pozwalają na łatwą obsługę komunikatów definiowanych przez użytkownika, komunikatów rejestrowanych oraz innych przypadków tego typu.

Aby uniknąć tego problemu, kreator Class Wizard symuluje funkcje wirtualne za pomocą mapy komunikatów. Mapa ta jest po prostu tablicą wpisów, które MFC przegląda przed podjęciem decyzji, jaką funkcję należy wywołać. W tablicy tej przechowywanych jest kilka niesłychanie istotnych informacji np.

Obsługiwany komunikat

Identyfikator elementu kontrolnego, do którego odnosi się komunikat (jeśli w ogóle element taki istnieje - zagadnienie to zostanie niebawem omówione)

Argumenty przekazywane przez komunikat

Typ wyniku, jakiego oczekuje komunikat

Szczególnie ważny jest drugi element. Niektóre komunikaty (takie jak WM_COMMAND oraz WM_NOTIFY) są dalej dzielone ze względu na identyfikator polecenia lub elementu kontrolnego, do którego się odnoszą. Dlatego też nie tworzy się metody obsługującej komunikat WM_COMMAND; zamiast tego pisze się metodę obsługującą komunikat WM_COMMAND z identyfikatorem ID_MENU_FILE_OPEN (lub jakimkolwiek innym).

Taki schemat niesie za sobą kilka istotnych implikacji. Po pierwsze, umożliwia on odpowiednie przeanalizowanie parametrów komunikatów lParam oraz wParam, roz­pakowanie ich i dostarczenie do wywoływanych funkcji parametrów odpowiednich typów. Po drugie, mapa komunikatów jest rozwiązaniem niesłychanie elastycznym, które umożliwia Ci obsługę dowolnych, wybranych komunikatów.

Jest to szczególnie istotne podczas obsługiwania przez mapę komunikatów poszczególnych komunikatów WM_COMMAND. W swoim programie możesz posiadać setki przeróżnych identyfikatorów. Nikt nie byłby w stanie odgadnąć, jakiego identyfikatora chciałbyś użyć. Jednakże wybór identyfikatora nie jest istotny, o ile tylko mapa komunikatów zo­stanie poprawnie skonstruowana.

Gdy biblioteka MFC otrzymuje komunikat, określa ona jego okno docelowe oraz odpowiadającą mu instancję obiektu MFC. Następnie przeszukiwana jest mapa komu­nikatów okna w celu odnalezienie funkcji, która ma zostać użyta do obsługi konkretnego komunikatu. Jeśli w oknie docelowym nie została zdefiniowana funkcja obsługująca komunikaty tego typu, MFC będzie rekurencyjnie przeszukiwać klasy bazowe okna docelowego. Jeśli funkcja obsługi komunikatu nie zostanie odnaleziona także w oknie bazowym, MFC skieruje obsługę komunikatu do domyślnej procedury obsługi (innymi słowy, do kodu, który nie jest już częścią MFC).

Oczywiście ręczne tworzenie mapy komunikatów byłoby zajęciem uciążliwym i sprzy­jającym powstawaniu wielu błędów. Na szczęście MFC udostępnia kreatora Class Wizard, który w ogromnym stopniu ułatwia nasze zadanie. Jednak również na niższym poziomie istnieją pewne ułatwienia- są nimi makra, których używa Class Wizard.

Dostępnych jest kilka makr, których używa kreator Class Wizard, oraz kilka innych, których kreator ten nie używa, lecz które mogą zostać użyte przez Ciebie. Nie powinieneś mieć żadnych problemów z dodawaniem Twoich własnych makr do istniejącej mapy komunika­tów. Pamiętaj jednak, aby swoich modyfikacji dokonywać poza specjalnymi komentarzami generowanymi przez kreatora Class Wizard. Rozważmy przykład poniższej mapy komuni­katów pochodzącej z programu wygenerowanego za pomocą kreatora App Wizard.

BEGIN_MESSAGE_MAP(CLinkckView, CFormView)

//{{AFX_MSG_MAP(CLinkckView)

ONJ3N_CLICKED(IDC_SCAN, OnScan)

ON_COMMAND(ID_FILE_PRINT_PREVIEW, OnFilePrintPreview)

ON_COMMAND(ID_FILE_SCAN, OnScan)

ON_LBN_DBLCLK(IDC_LB, OnS c an)

ON_UPDATE_COMMAND_UI (ID_FILE_PRINT, OnUpdateFilePrint)

ON_UPDATE__COMMAND_UI (ID_FILE_PRINT_PREVIEW, OnUpdateFilePrint)

//} }AFX_MSG_MAP

// Put your extra macros here

// Standard printing commands

ON_COMMAND(ID_FILE_PRINT, CFormYiew::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_DIRECT, CFormView::OnFilePrint) END_MESSAGE_MAP()

Makra znajdujące się pomiędzy liniami AFX_MSGJV1AP zostały wygenerowane przez kreatora Class Wizard. Po drugim wystąpieniu linii AFX_MSG_MAP możesz umieszczać wszystko co chcesz, a Class Wizard nie będzie już o tym wiedział. Dwa makra znajdujące się w tej części mapy komunikatów umieszczone zostały tam przez kreatora App Wizard. Zostały one umieszczone w tym miejscu po to, aby nie było ich widać w kreatorze Code Wizard.

Jak możesz zorientować się na przykładzie przedstawionej powyżej mapy komunikatów, dwa najbardziej podstawowe makra to BEGIN_MESSAGE_MAP oraz END_ MESSAGE_MAP. Te dwa proste makra służą do stworzenia tablicy mapy komunika­tów, umieszczenia w niej kilku prostych wpisów oraz zakończenia tej tablicy. Wszystkie najważniejsze makra (patrz tabela 1.2) umieszczane są pomiędzy makrami BEGIN_MESSAGE_MAP oraz END_MESSAGE_MAP. Oczywiście istnieją makra dla wszystkich komunikatów, np.: ON_WM_CLOSE dla komunikatu WM_CLOSE lub ON WM_PAINT dla komunikatu WM_PAINT.


Przekazywanie komunikatów

Dowiedziałeś się już, że główne okno ramki posiada pasek menu. Czy to ma oznaczać, że cały kod służący do obsługi menu musi być umieszczony w tym głównym oknie? W żadnym wypadku. MFC automatycznie przekazuje komunikaty WM_COMMAND (oraz niektóre inne) do pozostałych części Twojego programu. MFC przeszukuje wszystkie części programu w poszukiwaniu funkcji obsługujących komunikat; jeśli jednak funkcja taka nie zostanie odnaleziona, MFC skieruje komunikat do głównego okna ramki. Do­kładna kolejność przeszukiwania programu podana została w tabelach l .3 oraz l .4.

Tabela 1.2. Użyteczne makra stosowane przy definiowaniu mapy komunikatów.

Nazwa/Argumenty Opis

ON_COMMAND Obsługuje komunikaty WM_COMMAND

ID Określa ID skojarzony z komunikatem WM_COMMAND

func Metoda do wywołania [void func(void)]

ON_COMMAND_RANGE Obsługuje określony zakres identyfikatorów dla komunikatów WM_COMMAND

ID Pierwszy identyfikator zakresu

IDLast Ostatni identyfikator zakresu

func Metoda do wywołania [void(WORD id)]

ON_COMMAND_EX Działa podobnie jak makro ON_COMMAND, lecz do funkcji obsługującej komunikaty przekazywany jest argument będący identyfikatorem polecenia, a funkcja zwraca typ BOOL

ID Identyfikator polecenia

IDLast Metoda do wywołania [BOOL func(WORD id)]

ON_COMMAND_RANGE_EX Działa podobnie jak makro ON_COMMAND_EX, lecz dla określonego zakresu identyfikatorów.

ID Pierwszy identyfikator zakresu

IDLast Ostatni identyfikator zakresu

func Metoda do wywołania [BOOL func(WORD id)]

ON_UPDATE_COMMAND_UI Obsługuje prośbę MFC o podanie stanu odpowiedniego elementu interfejsu użytkownika

ID Identyfikator elementu

func Metoda do wywołania [void func(CcmdUI *pCmdUI)]

ON_UPDATE_COMMAND_RANGE_UI Działa podobnie jak ON_UPDATE_COMMAND_UI, lecz dla określonego zakresu identyfikatorów

ID Pierwszy identyfikator

IDLast Ostatni identyfikator

func Metoda do wywołania [void func(CcmdUI *pCmdUI)]

ON_NOTIFY Obsługuje komunikaty WM_NOTIFY dla elementów interfejsu użytkownika działających według nowego stylu

code Kod informacyjny

ID Identyfikator elementu kontrolnego

func Funkcja do wywołania [void func(NMHDR *hdr, LRESULT *result)]

ON_NOTIFY_RANGE Działa jak ON_NOTIFY, tylko dla określonego zakresu identyfikatorów

code Kod informacyjny

ID Pierwszy identyfikator zakresu

IDLast Ostatni identyfikator zakresu

func Funkcja do wywołania [void func(NMHDR *hdr, LRESULT *result)]

ON_NOTIFY_EX Działa podobnie jak ON_NOTIFY wywołuje metodę zwracającą wynik BOOL

Code Kod informacyjny

Id Identyfikator elementu kontrolnego

func Funkcja do wywołania [BOOL func(NMHDR *hdr, LRESULT *result)]

ON_NOTIFY_EX_RANGE Działa podobnie jak ON_NOTIFY_EX dla określonego zakresu

identyfikatorów

code Kod informacyjny

ID Pierwszy identyfikator zakresu

IDLast Ostatni identyfikator zakresu

func Funkcja do wywołania [BOOL func(NMHDR *hdr, LRESULT *result)]

ON_CONTROL Obsługuje komunikaty WM_COMMAND będące komunikatami powiadomienia elementów kontrolnych (np. Komunikaty rozpoczynające się od EN_ oraz BN_).

code Kod informacyjny

ID Pierwszy identyfikator

IDLast Ostatni identyfikator

func Funkcja do wywołania [void func(void)]

ON_MESSAGE Obsługuje każdy komunikat (nawet te, definiowane przez użytkownika), bez jakiegokolwiek przetwarzania przekazywanych argumentów.

msg Obsługiwany komunikat

func Funkcja do wywołania (LRESULT func(WPARAM wParam, LPARAM lParam)]

ON_REGISTERED_MESSAGE Obsługuje zarejestrowane komunikaty (stworzone przy użyciu funkcji RegisterWindowsMessage).

msg Zmienna zawierająca identyfikator zarejestrowanego komunikatu

func Funkcja do wywołania [LRESULT func(WPARAM wParam, LPARAM lParam)]


Tabela 1.3. Kolejność przeszukiwania programów MDI

Kolejność przeszukiwania Obiekt

  1. Aktywny widok (jeśli taki jest)

  2. Aktywny dokument (jeśli taki jest)

  3. Szablon Dokumentu który stworzył dokument (jeśli jest)

  4. Aktywna ramka okna potomnego (jeśli jest)

  5. Obiekt aplikacji

  6. Główna ramka okna


Tabela 1.4. Kolejność przeszukiwania programów SDI

Kolejność przeszukiwania Obiekt

  1. Aktywny widok

  2. Aktywny dokument

  3. Szablon dokumentu

  4. Główna ramka okna

  5. Obiekt aplikacji


Gdzie umieszczać procedury obsługi komunikatów?

Chociaż tabele 1.3 i 1.4 mogą Ci wprowadzić troszkę zamieszania w głowie, nie przejmuj się -jest jedna prosta zasada: Procedury obsługi komunikatów należy umieszczać tam, gdzie w najprostszy sposób do­stępne są dane, na których procedury te operują.

Przekazywanie komunikatów jest jedną z najpotężniejszych możliwości MFC. Wyobraź sobie, że stworzyłeś wielozadaniowy program zawierający w sobie edytor tekstu oraz arkusz kalkulacyjny. W dowolnej chwili użytkownik może mieć otworzonych kilka okien obu typów. Może zdarzyć się i tak, iż w danej chwili nie będzie żadnego otwo­rzonego okna (jedynie puste główne okno ramki programu MDI).

Załóżmy, że chciałbyś zaimplementować polecenia “wyświetl status". Jeśli aktualnym oknem byłby arkusz kalkulacyjny, status wyświetlałby ilość wypełnionych komórek, wierszy oraz kolumn. Natomiast w przypadku, gdyby aktualnym oknem był edytor tekstu, status wyświetlałby ilość słów oraz linii w dokumencie. Jeśli nie będzie żadnego aktywnego dokumentu, polecenie to powinno wyświetlić ilość dostępnego miejsca na twardym dysku.

Załóżmy teraz, że biblioteka MFC nie dokonywałaby przekazywania komunikatów. W takim wypadku musiałbyś umieścić procedurę realizującą plecenie “wyświetl status" w głównym oknie ramki programu. Procedura ta musiałby określić aktywny widok, jego typ, i na tej podstawie wyświetlić odpowiednie dane. Co więcej, jeśli chciałbyś przenieść kod arkusza kalkulacyjnego do innej aplikacji, musiałbyś w kodzie głównej ramki odszukać kod wyświetlający status arkusza i odseparować go. Niezbyt przyjemne i eleganckie rozwiązanie.

Jednakże dzięki przekazywaniu komunikatów realizowanemu przez MFC, rozwiązanie naszego hipotetycznego problemu jest po bardzo łatwe. Wszystko, co musisz zrobić, to napisać trzy niezależne procedury do obsługi polecenia “wyświetl status". Pierwszą z nich musiałbyś umieścić w klasie dokumentu edytora tekstu, drugą - w klasie doku­mentu arkusza kalkulacyjnego, a trzecią- w klasie aplikacji. W pierwszej kolejności komunikat będzie mógł być obsłużony przez aktywny dokument. Jeśli nie ma aktywnego dokumentu, komunikat zostanie obsłużony przez obiekt aplikacji. Żadna z trzech procedur obsługi polecenia nie musi wiedzieć o istnieniu pozostałych dwóch. Oto proste i bardzo eleganckie rozwiązanie.

Możesz się zastanawiać w jaki sposób obiekty dokumentu oraz aplikacji mogą obsługiwać komunikaty. W końcu nie są to okna, a klasy te nie są klasami potomnymi klasy CWnd. MFC ma na to swoje sposoby. Otóż każda klasa potomna klasy CCmdTarget może mieć mapę komunikatów. Klasy CWnd, CWinApp oraz CDocument są klasami potomnymi klasy CCmdTarget. Oczywiście, klasy nie będące potomnymi klasy CWnd mogą obsługiwać jedynie te komunikaty, które zostaną przekazane do nich przez klasy potomne klasy CWnd.


Szablony dokumentów

Ostatnim klockiem w tej układance jest to jak to wszystko poskładać w jedną całość. Dla przykładu, jeśli otwierasz nowy plik, będziesz musiał wybrać odpowiednią klasę dokumentu, stworzyć jej nową instancję, jak również stworzyć odpowiednie obiekty widoku i ramki. Będziesz musiał także połączyć te wszystkie obiekty, tak aby mogły razem działać. Do tego właśnie służą szablony dokumentów. Definiują one relacje pomiędzy dokumentem, widokiem oraz ramką.

Większość osób nie ma do czynienia z szablonami dokumentów, gdyż kreator App Wizard tworzy pierwszy z nich automatycznie. Jednakże, jeśli będziesz chciał skojarzyć z do­kumentem drugi widok lub zdefiniować kolejny typ dokumentu w aplikacji, niestety będziesz się musiał dowiedzieć znacznie więcej o szablonach dokumentów.

Szablony dokumentów są zazwyczaj tworzone w metodzie CWinApp::InitInstance

i dodawane do specjalnej listy, którą posiada każda aplikacja (służy do tego metoda AddDocTemplate). Jeśli na tej liście jest więcej niż jedna pozycja, to MFC wykonuje kilka specjalnych czynności. Po pierwsze, gdy MFC będzie próbowało stworzyć nowy dokument (co dzieje się, w sytuacji, gdy zostanie wybrana opcja File^New), aplikacja wyświetli listę wszystkich dostępnych szablonów dokumentu. Także podczas otwierania pliku, MFC będzie przeszukiwać listę dostępnych szablonów dokumentów, w poszukiwaniu szablonu pasującego do rozszerzenia otwieranego pliku.

Czasami będziesz chciał użyć możliwości udostępnianych przez szablony do stworzenia dokumentu, widoku oraz ramki, całkowicie pod kontrolą programu. W takim wypadku nie będziesz musiał dodawać szablonu dokumentu do listy szablonów aplikacji.


Informacje szczegółowe

Wystarczy już tych rozważań teoretycznych. Teraz przyjrzymy się szczegółom każdej z przedstawionych wcześniej, głównych klas. Również i teraz będziemy szczególnie zainteresowani tym, jak te obiekty działają, nawet jeśli kreator App Wizard oraz Class Wizard ukrywają te szczegóły przed nami. Zrozumienie tych detali jest zagadnieniem kluczowym, zwłaszcza jeśli będziesz chciał zrobić coś, na co nie pozwalają Ci kreatory.


Klasa CWinApp

Każdy program MFC ma dokładnie jeden obiekt klasy wyprowadzonej z klasy CWinApp. Kreator App Wizard tworzy ten obiekt jako zmienną globalną i umieszcza w głównym pliku źródłowym projektu. Chociaż jest to zmienna globalna (o nazwie theApp), nie możesz się do niej bezpośrednio odwoływać z innych plików źródłowych projektu. Kreator App Wizard nie umieszcza bowiem deklaracji tej zmiennej w żadnym pliku na­główkowym.

Oficjalnym sposobem uzyskania dostępu do obiektu aplikacji jest wywołanie globalnej funkcji o nazwie AfxGetApp. Funkcja ta nie jest jednak idealna, gdy zwraca wskaźnik do obiektu aplikacji. Jeśli więc będziesz chciał uzyskać dostęp do metod zdefiniowanego przez siebie obiektu aplikacji, będziesz musiał rzutować uzyskany wskaźnik do odpo­wiedniego typu.


Pobieranie obiektu aplikacji

Jeśli chcesz uzyskać dostęp do metod swojego obiektu aplikacji, możesz zastosować jedno z kilku rozwiązań. Pierwszym z nich jest dodanie deklaracji zmiennej theApp do pliku nagłówkowego:

extern CCustomApp theApp;

Inną możliwością jest zdefiniowanie makra o nazwie APP:

#define APP (CCustomApp *)AfxGetApp

Obiekt aplikacji ma kilka istotnych składowych (zostały one przedstawione w tabeli 1.1). Jedną z takich składowych jest m_pCmdLine, która zawiera łańcuch znaków z linią poleceń użytą do uruchomienia programu. Zamiast samemu przetwarzać linię wywołania programu, znacznie łatwiej jest użyć wbudowanego w MFC mechanizmu analizy.

App Wizard automatycznie generuje kod służący do przeanalizowania linii poleceń użytej do uruchomienia programu. Kod ten jest niejednokrotnie trudny do zrozumienia i modyfikacji. Spójrz na poniższy przykład:

//Parse command linę for standard shell commands, DDE, file open

CCommandLinelnfo cmdlnfo;

ParseCommandLine(cmdlnfo);

// Dispatch commands specified on the command line

if ( !ProcessShellCommand(cmdlnfo))

return FALSE;

Kluczem do zrozumienia powyższego fragmentu kodu jest klasa CCommandLinelnfo (patrz tabela 1.5). Funkcja ParseCommandLine analizuje linię poleceń i określa, czy zawiera ona nazwę pliku, czy też opcjonalny przełącznik (zaczynający się od znaku “/" lub “-"). W zależności od odnalezionych informacji, funkcja ta zapisuje odpowiednie informacje w obiekcie klasy CCommandLinelnfo. Funkcja ParseCommandLine analizuje linię poleceń, lecz wciąż nie wykonuje jakichkolwiek innych czynności. Do tego służy inna funkcja - ProcessShellCommand, do której przekazywany jest obiekt klasy CCommandLinelnfo. Sposób działania tej funkcji zależy od wartości składowej m_nShellCommand klasy CCommandLinelnfo (patrz tabela 1.6).

Oczywiście, jeśli nie chcesz analizować linii poleceń, nie musisz wywoływać przedstawionych powyżej funkcji. Możesz także zmodyfikować wartość składowej m_nShellCommand, tak aby osiągnąć zamierzony efekt.


Tabela 1.5. Najistotniejsze składowe klasy CCommandLinelnfo.

Nazwa Opis

ParseParam Przesłoń tę metodę, aby zdefiniować własny sposób analizy linii poleceń

m_bShowSplash Określa, czy powinien być wyświetlany winieta programu.

m_bRunEmbedded Określa, czy w linii poleceń pojawiła się opcja /Embedding.

m_bRunAutomated Określa, czy w linii poleceń pojawiła się opcja /Automation.

m_nShellCommand Zawiera polecenie jakie należy wykonać.

m_strFileName Zawiera nazwę pliku, który powinien być otworzony lub wydru­kowany; pusty łańcuch znaków, jeśli wydano polecenie New lub jedno z poleceń DDE.

m_strPrinterName Określa nazwę drukarki, jeśli wydano polecenie Print To; w przeciwnym wypadku pusty łańcuch znaków.

m_strDriverName Określa nazwę sterownika, jeśli wydano polecenie Print To; w przeciwnym wypadku pusty

łańcuch znaków.

m_strPortName Określa nazwę portu, jeśli wydano polecenie Print To; w przeciwnym wypadku pusty łańcuch znaków.


Tabela 1.6. Możliwa wartości składowej m_nShellCommand.

Wartość: FileNew FileOpen FilePrint FilePrintTo FileDDE FileNothing


Dla przykładu, poniższa linia kodu umożliwia Ci zapobieżenie stworzeniu nowego dokumentu podczas uruchamiania programu. Powinieneś umieścić ją pomiędzy wywo­łaniami funkcji ParseCommandLine oraz ProcessShellCommand:

if (cmdlnfo.m_nShellCommand==FileNew)

cmdlnfo.m_nShellCommand=FileNothing;

Dzięki temu Twój kod wciąż będzie mógł obsługiwać parametry wywołania programu.

Możesz także stworzyć swoją własną klasę pochodną klasy CCommandLinelnfo i przesłonić domyślną implementację metody ParseParam, uzyskując możliwość obsługi własnych parametrów wywołania programu. Funkcja ParseCommandLine wywołuje metodę ParseParam dla każdego parametru odnalezionego w linii poleceń. Argumenty te możesz obsługiwać w dowolny sposób.

A gdzie podziała się standardowa metoda WinMain? Teraz została ona umieszczona głęboko w kodzie źródłowym MFC. Na szczęście cały czas dysponujesz pełną kontrolą nad wszystkim, co się dzieje. Najważniejsze jest to, że konstruktor klasy CWinApp za­pisuje wskaźnik do Twojego obiektu aplikacji ze zmiennej globalnej. W tym wypadku nie ma żadnego problemu z użyciem takiej zmiennej, gdyż istnieje tylko jeden obiekt aplikacji.

Ze względu na fakt, iż obiekt aplikacji jest obiektem globalnym, jego konstruktor uruchamiany jest w pierwszej kolejności - przed jakimkolwiek innym kodem. W momencie gdy wywoływana jest wewnętrzna metoda WinMain zdefiniowana w MFC, /mienna globalna aplikacji jest już poprawnie zainicjalizowana. Funkcja WinMain wywołuje metody InitApplication oraz Initlnstance obiektu aplikacji. Funkcje te są funkcjami wirtualnymi, a więc możesz przesłonić ich domyślne definicje oraz stworzyć i wykonać własne. Jeśli metody te nie zostaną przesłonięte, to wykonane zostaną ich domyślne wersje zdefiniowane w klasie CWinApp. W starych programach przeznaczonych dla systemu Windows 3.x była poważna różnica pomiędzy funkcjami InitApplication oraz Initlnstance; w systemach Windows NT oraz Windows 95 wywoływane są obie te metody.

Gdy inicjalizacja zostanie już pomyślnie zakończona, funkcja WinMain wywoła meto­dę Run obiektu aplikacji. Zazwyczaj nie będziesz chciał przesłaniać domyślnej definicji tej metody, gdyż zawiera ona całą obsługę pętli komunikatów. Domyślny sposób obsłu­gi tej pętli jest całkowicie zadowalający. Jednakże, zgodnie ze stylem MFC, jeśli tylko chcesz możesz przesłonić domyślną definicję tej metody i stworzyć własną. Domyślna metoda Run dostarcza także kilku innych przesłanialnych metod (jak na przykład Onldle lub PreTransIateMessage), dzięki którym możesz

Gdy działanie pętli obsługi komunikatów zostanie zakończone, funkcja WinMain wywoła metodę Terminatelnstance. Metoda ta może zostać użyta do wykonania wszelkich czynności związanych z zakończeniem działania aplikacji. Jak widać, pomimo tego, funkcja WinMain jest umieszczona głęboko w kodzie MFC, cały czas możesz w dowolny sposób sterować sposobem wykonywania swojej aplikacji. W większości wypadków całkowicie wystarczą Ci domyślne ustawienia dostarczane przez MFC, dzięki czemu nie będziesz musiał niczego zmieniać.dostosować działanie tej metody do swoich potrzeb.


CView

Widok jest tą klasą, którą użytkownik najłatwiej identyfikuje z aplikacją. Oprócz metod i danych składowych zdefiniowanych w samym widoku, będziesz mógł używać w nim także składowych dostępnych w klasie CWnd (która jest klasą bazową klasy CYiew).

Jedną z najważniejszych metod klasy CView, której domyślną definicję zazwyczaj będziesz przesłaniał, jest metoda OnDraw. Metody tej będziesz używał do rysowania w oknie aplikacji. Zauważ, iż nie jest to to samo co obsługiwanie komunikatu WM_PAINT. System Windows wysyła komunikat WM_PAINT za każdym razem gdy okno aplikacji wymaga odświeżenia. Jednakże klasa CView obsługuje ten komunikat samodzielnie, wywołując metodę OnDraw jedynie w koniecznych sytuacjach.

Cały proces został przedstawiony w tabeli 1.7. Dlaczego używać metody OnDraw zamiast OnPaint? Metoda OnDraw jest używana także podczas drukowania oraz tworzenia podglądu listingu. Jeśli będzie ona poprawnie obsługiwana, możliwość drukowania oraz tworzenia podglądu listingu będziesz miał “za darmo" (więcej informacji na temat dru­kowania i tworzenie podglądu listingu znajdziesz w Rozdziale 3).


Tabela 1.7. Proces rysowania.

MFC wywołuje Dlaczego ? Przesłoń gdy ...

CView::OnUpdate Wywołałeś metodę CDocument::UpdateAllViews Chcesz obsługiwać optymalizację wywołań funkcji InvalidateRect

CWnd::OnPaint Zgłoszony został komunikat WM_PAINT Chcesz bezpośrednio obsługiwać komunikat WM_PAINT

CView::OnPrepareDC Aby zainicjalizować kontekst urządzenia (DC) Chcesz przydzielić zasoby GDI albo zmodyfikować bądź pracować bezpośrednio z kontekstem urządzenia

CView::OnDraw Aby zaktualizować zawartość okna programu Chcesz coś narysować (czyli pawie zawsze

)

Cały proces rysowania rozpoczyna się, gdy wywołana zostanie metod OnUpdate. Dzieje się to zazwyczaj w odpowiedzi na wywołanie metody UpdateAllViews obiektu dokumentu. Domyślna implementacja metody OnUpdate unieważnia cały obszar robo­czy widoku. Powoduje to wygenerowanie przez system Windows komunikatu WM_PAINT, przy czym aktualizacji podlegać ma cały obszar roboczy. Często bę­dziesz chciał zoptymalizować rysowanie, tak aby przerysowywane były jedynie frag­menty widoku, a nie cały jego obszar. W tym właśnie miejscu zaczynają odgrywać rolę dodatkowe informacje przekazywane do metody OnUpdate.

Aby móc wykorzystać te informacje, będziesz musiał przesłonić domyślną definicję metody OnUpdate. Do metody tej przekazywane są dwa 32-bitowe argumenty. Tech­nicznie rzecz biorąc, parametry te są wartościami typów LPARAM oraz CObject *; w praktyce i tak będziesz musiał rzutować ich typy na takie, jakich będziesz używał. Twoim zadaniem jest przekształcenie przekazywanych informacji do postaci współ­rzędnych prostokąta, które będziesz mógł przekazać jako argumenty wywołania metody InvalidateRectangle. To co przekażesz jako informacje dodatkowe do metody OnUpdate, zależy tylko i wyłącznie od Ciebie (z pewnymi ograniczeniami).

Co powinno być przekazywane jako informacje dodatkowe? Coś, co pozwoli Ci na określenie, jaka część Twojego widoku wymaga przerysowania. Niektóre widoki, takie jak CScrollView, wywołują metodę OnUpdate samodzielnie. W takim wypadku nie będą przekazywane żadne informacje dodatkowe. Oznacza to, że Ty musisz przekazywać jakieś informacje. Oznacza to także, że Twoja wersja metody OnUpdate musi poprawnie obsługiwać sytuację, gdy nie zostaną do niej przekazane żadne informacje. W takim wypadku możesz wywołać metodę OnUpdate klasy bazowej lub metodę InvalidateRectangle z argumentem o wartości NULL. Rzeczą, której na pewno nie będziesz chciał robić, jest obsłużenie informacji dodatkowych, wywołanie metody InvalidateRectangle i metody OnUpdate klasy bazowej. Takie postępowanie doprowadzi do od­świeżenia całego obszaru roboczego widoku, niezależnie od przekazanych informacji dodatkowych.

Załóżmy (jeszcze raz), że tworzysz arkusz kalkulacyjny. W celu odświeżenia widoku tabelarycznego danych możesz przekazywać, jako informacje dodatkowe, adres komórki, którą należy odświeżyć (pod warunkiem, że adres ten będzie różny od zera). Nie będziesz chciał przekazywać bezwzględnych koordynat określanych w pikselach. A dlaczego nie? Dlatego, że możesz dysponować wieloma widokami, a niektóre z nich mogą być przewinięte do innej pozycji bezwzględnej. Odświeżana komórka może być wyświetlona w jednym położeniu w pierwszym widoku, w innym położeniu w drugim widoku, a całko­wicie niewidoczna w trzecim. A co jeśli dodatkowo będziesz miał otworzony widok przedstawiający dane w postaci wykresu kołowego? Jest absolutnie jasne, że położenie komórki na wykresie kołowym będzie całkowicie odmienne od położenia komórki w widoku tabelarycznym.

Jeśli chcesz, możesz także wybiórczo ignorować informacje dodatkowe przekazywane do metody OnUpdate. Dla przykładu, możesz obsługiwać informacje dodatkowe w wi­doku tabelarycznym, lecz ignorować je w widoku wykresu kołowego. Będzie to oznaczało, że wykres kołowy będzie w całości przerysowywany za każdym razem gdy zmieni się wartość jakiejś komórki. Rozwiązanie takie możesz zastosować jeśli okaże się, że obliczenie wymiarów aktualizowanego prostokąta będzie zbyt skomplikowane.

W podobny sposób możesz być “nieuważny" w określaniu współrzędnych odświeżanego prostokąta, o ile tylko jego rozmiar będzie równy lub większy od minimalnego wyma­ganego rozmiaru. Dla przykładu, możesz dojść do wniosku, że łatwiej jest określić jaka ćwiartka wykresu kołowego wymaga przerysowania, i aktualizować tylko ją. Będzie to rozwiązanie nieco mniej efektywne, jednakże wciąż lepsze od przerysowywania całego wykresu. Innym rozwiązaniem jest określenie, czy komórka jest w ogóle widoczna na wykresie kołowym. Jeśli nie jest, nie musisz wywoływać funkcji InvalidateRectangle.

Dostępnych jest wiele różnych typów klasy CView służących specjalnym celom (patrz tabela 1.8). Dzięki użyciu tych specjalnych widoków możesz ogromnie uprościć tworzenie programie. Oczywiście, każda z tych klas jest klasą potomną klasy CView, a więc wszystkie operacje, jakie możesz wykonać na obiektach klasy CView możesz także wykonać na obiektach tych wyspecjalizowanych klas potomnych.

Kreator App Wizard może stworzyć dla Twojego programu jeden dokument i jeden widok. Kreator ten wie, że klasy te działają wspólnie; dzięki tej wiedzy może on dokonać specjal­nych modyfikacji w klasie widoku. MFC kojarzy każdy widok z dokumentem. Aby określić do jakiego dokumentu należy dany widok, możesz posłużyć się metodą GetDocument. Metoda ta zwraca zazwyczaj wynik typu CDocument *. Jednak kreator App Wizard przesłania zazwyczaj domyślną definicję tej metody i tworzy własną - zwracającą wskaźnik na dokument odpowiedniego (Twojego) typu.

Oznacza to, że jeśli stworzyłeś własne metody w obiekcie dokumentu, będą one bez żadnych problemów dostępne także w obiekcie widoku. Działa to tak sprawnie tylko w przypadku dokumentu i widoku wygenerowanego przez kreatora App Wizard. Jeśli stworzysz nową klasę widoku korzystając z kreatora Class Wizard lub sam zupełnie od podstaw wywołanie metody GetDocument spowoduje zwrócenie wyniku typu CDocument *. Kreator Class Wizard nie ma bowiem żadnego pojęcia o tym, jakiego dokumentu chcesz użyć z tworzonym widokiem. Może to spowodować zamieszanie, gdy okaże się, że nie będziesz w stanie skorzystać z metod zdefiniowanych w klasie do­kumentu. Jaka jest na to rada? Wystarczy rzutować typu wyniku zwracanego przez metodę GetDocument do odpowiedniego typu dokumentu. Lepszym rozwiązaniem jest przesłonięcie definicji metody GetDocument, tak jak robi to App Wizard. Oczywiście, przy takim rozwiązaniu zakłada się, że dany widok nie będzie jednocześnie używany z dokumentami kilku różnych typów.


Tabela 1.8. Klasy potomne klasy CVie

wKlasa Przeznaczenie

CEditView Prosty edytor tekstowy opierający swoje działanie na standardowym edycyjnym elemencie kontrolnym.

CListView Widok obsługujący prostą listę.

CTreeView Widok obsługujący złożoną listę hierarchiczną.

CRichEditView Złożony edytor obsługujący różnego rodzaju czcionki, obiekty OLE oraz format zapisu RTF.

CScrollView Widok udostępniający możliwość przewijania.

CFormView Widok w którym głównym oknem jest okno dialogowe.

CRecordView Widok zapewniający połączenie z bazą danych ODBC.

CDaoRecordView Widok zapewniający połączenie z bazą danych DAO.


CDocument

Nie zapomnij: dokument jest miejscem, gdzie przechowywane są dane używane przez Twój program. Większość metod, które będziesz definiował w tej klasie, zależeć będzie od indywidualnych wymagań Twojej aplikacji. Jedynie kilka standardowych metod wykorzystywanych jest we wszystkich przypadkach (patrz tabela 1.9).

Podstawowe możliwości funkcjonalne klasy CDocument pozwalają na wykonywanie następujących operacji:


Tabela 1.9. Podstawowe metody dokumentów

.AddView Dodaje widok do listy widoków.

RemoveView Usuwa widok z listy dostępnych widoków.

GetDocTemplate Pobiera szablon dokumentu, za pomocą którego dokument został stworzony.

GetFirstViewPosition Pobiera obiekt POSSIT1ON poprzedzający pierwszy widok umieszczony w liście dostępnych widoków.

GetNextView Pobiera następny widok dostępny na liście widoków.

GetPathName Pobiera nazwę.

GetTitle Pobiera tytuł.

SetPathName Określa ścieżkę dostępu.

SetTitle Określa tytuł.

IsModified Sprawdza flagę modyfikacji.

SetModifiedFlag Ustawia flagę modyfikacji.

UpdateAllViews Wywołuje metodę OnUpdate wszystkich dołączonych widoków.

OnNewDocument Tworzy nowy dokument.

OnOpenDocument Wywoływana w celu otworzenia dokumentu (zazwyczaj wykorzystuje metodę Serialize).

OnSaveDocument Wywoływana w celu zapisania dokumentu (zazwyczaj wykorzystuje metodę Serialize).

Serialize Używana do odczytania lub zapisania dokumentu w pliku archiwalnym.

ReportSaveLoadException Przesłoń domyślną definicję, aby móc przechwytywać wyjątki generowane podczas serializacji.

OnFileSendMail Obsługuje polecenie MAPI File Send.

OnUpdateFileSendMail Obsługuje komunikat aktualizacji interfejsu użytkownika polecenia MAPI File Send.


Większość programów będzie wykorzystywała jedynie niektóre z przedstawionych powyżej możliwości. Dokument na pewno będzie chciał zapisać swoją zawartość oraz odczytać ją z pliku, (patrz rozdział 2). Kiedy będziesz modyfikował dokument, do Ciebie będzie należało wywołanie metody SetModifiedFlag w celu oznaczenia, że dokument został zmodyfikowany. Gdy zostanie zamknięty ostatni widok skojarzony z dokumentem wewnętrzne metody klasy CDocument sprawdzą, czy flaga modyfikacji jest ustawiona, czy nie. Jeśli jest, to użytkownik zostanie poproszony o zapisanie dokumentu. Wszystkim co musisz zrobić, aby mechanizm ten zaczął działać, jest wywołanie metody SetModifiedFlag - reszta stanie się automatycznie.

Dokument przechowuje listę wszystkich widoków, które są z nim skojarzone. Jeśli chcesz, możesz przeanalizować zawartość tej listy pobierając kolejne jej elementy za pomocą metod GetFirstYiewPossition oraz GetNextView. Najczęstszym powodem, dla którego mógłbyś chcieć pobrać wszystkie widoki, jest chęć ich aktualizacji w mo­mencie gdy zawartość dokumentu ulegnie zmianie (co można zrobić za pomocą metody OnUpdate). Jednakże klasa CDocument udostępnia metodę UpdateAHViews, której użycie powoduje wywołanie metod OnUpdate wszystkich widoków skojarzonych z danym dokumentem. Jako argument wywołania tej metody można podać wskaźnik do widoku, którego nie chcesz aktualizować. Jest to bardzo istotne, gdyż czasami, gdy widok dokonuje modyfikacji dokumentu, unieważnia on jednocześnie odpowiedni fragment swojej powierzchni. Dzięki przekazaniu wskaźnika this do metody UpdateAHViews widok zapobiegnie aktualizacji siebie samego, podczas procesu aktualizacji wszystkich pozostałych widoków. Jeśli będziesz chciał zaktualizować wszystkie widoki, przekaż wartość NULL jako argument wywołania tej metody. Metoda UpdateAHYiews umożliwia także przekazanie opcjonalnych informacji. Informacje te dokument przekazuje bezpo­średnio do metody OnUpdate aktualizowanych widoków.

Naprawdę ciekawym zagadnieniem związanym z użytkowaniem dokumentów jest serializa-cja danych. Zazwyczaj serializacja powoduje odczytanie dokumentu lub zapisanie go w pliku. Jednak w rozdziale 2 zobaczysz, że miejscem docelowym serializacji niekoniecznie musi być plik. Równie dobrze możesz umieszczać serializowane dane w rekordzie bazy danych, w części innego pliku lub w połączeniu sieciowym.


Klasa CFrameWnd i klasy pokrewne

Tak naprawdę, ramki są niewidocznymi bohaterami aplikacji MFC. Użytkownicy nie­jednokrotnie nie odróżniają ramki od widoku, jednak Ty, jako programista, musisz te różnice doskonale rozumieć. Klasa CFrameWnd (patrz tabela 1.10) jest klasą bazową dwóch klas używanych tylko i wyłącznie w aplikacjach MDI - CMIDChildWnd oraz CMDIFrameWnd.

Bardzo dużo istotnych czynności wykonywanych przez klasę CFrameWnd realizowana jest w sposób niewidoczny. Pamiętaj, że pasek menu, paski narzędzi oraz pasek stanu należą do okna głównej ramki. Dlatego też ramka ta jest odpowiedzialna za przekazywanie komunikatów pochodzących z tych właśnie elementów. Oczywiście, rzeczywistą klasą zarządzającą mapą komunikatów jest klasa potomna klasy CCmdTarget (klasa bazowa klasy CFrameWnd). Jeśli będziesz chciał zmienić sposób przekazywania komunikatów, możesz przesłonić standardową definicję metody OnCmdMsg. Przesłonięcie tej metody umożliwi Ci zmodyfikowanie sposobu przekazywania komunikatów poleceń. W praktyce jednak takie modyfikacje rzadko kiedy będą przydatne.


Tabela 1.10. Główne składowe klasy CFrameWnd.

Składowa Przeznaczenie

M_bAutoMenuEnabled Jeśli ma wartość TRUE, ramka powoduje, że opcje menu nie posiadające odpowiednich procedur obsługi stają się nieaktywne.

RectDefault Składowa statyczna używana do określenia domyślnego rozmiaru podczas wykonywania metody Create.

Create Tworzy rzeczywiste okno ramki.

LoadFrame Tworzy rzeczywiste okno ramki używając do tego informacji przechowywanych w zasobach aplikacji.

LoadAccelTable Ładuje tablicę skrótów ramki.

LoadBarState Ładuje informacje na temat stanu paska statusu aplikacji (zakotwiczony, widoczny, poziomy, itp.) z pliku INI aplikacji lub z rejestru systemowego.

SaveBarState Zapamiętuje aktualny stan paska stanu, tak aby mógł potem być odtworzony za pomocą metody LoadBarState.

ShowControlBar Wyświetla lub chowa skojarzony pasek.

SetDockState Określa stan paska (patrz LoadBarState); metoda użyteczna jeśli chcesz dokonywać serializacji stanu paska wraz z dokumentem.

GetDockState Pobiera stan paska (patrz LoadBarState); metoda użyteczna jeśli chcesz dokonywać serializacji stanu paska wraz z dokumentem.

EnableDocking Włącza możliwość zakotwiczania pasków przy wybranych lub wszystkich krawędziach okna programu.

DockControlBar Powoduje zakotwiczenie paska narzędzi.

FloatControlBar Powoduje uwolnienie zakotwiczonego paska elementów kontrolnych.

GetControlBar Pobiera pasek narzędzi.

RecalcLayout Przelicza położenie okna uwzględniając aktualny widok i paski elementów kontrolnych.

ActivateFrame Uaktywnia ramkę.

InitialUpdateFrame Wywołuje metodę OnlnitialUpdate we wszystkich widokach należących do ramki.

GetActiveFrame Pobiera aktywną ramkę (wykorzystywana w programach MDI).

SetActiveView Określa, który z widoków ramki będzie widokiem aktywnym.

GetActiveView Pobiera aktywny widok ramki.

CreateView Tworzy nowy widok lub okno przy wykorzystaniu struktury CreateContext.

GetActiveDocument Pobiera obiekt aktywnego dokumentu.

GetMessageString Pobiera treść komunikatu skojarzonego z poleceniem o określonym identyfikatorze.

SetMessageText Wyświetla komunikat tekstowy na pasku stanu.

GetMessageBar Pobiera wskaźnik do paska stan

uDodatkowo, przy przeszukiwaniu map komunikatów w celu odnalezienia odpowiedniej procedury obsługi, okno ramki wie, czy dla poszczególnych opcji menu zdefiniowane zostały procedury obsługi. Wszystkie elementy, dla których procedury obsługi nie zostały zdefiniowane, będą wyłączone. Jeśli nie chcesz, aby opcje menu były automatycznie wyłączane, będziesz musiał przypisać składowej m_bAutoMenuEnable wartość FALSE.

Musisz także wiedzieć, że nie tylko komunikaty WM_COMMAND podlegają przeka­zywaniu. Komunikaty WM_COMMAND są zazwyczaj generowane w odpowiedzi na operacje, które użytkownik wykonuje na elementach interfejsu użytkownika (takich jak przyciski lub opcje menu). Jednakże MFC wysyła także swój własny komunikat -MWJLDLEUPDATECMDUI. Komunikat ten generowany jest dla każdego zdefinio­wanego identyfikatora polecenia i przekazywany w dokładnie taki sam sposób, w jaki przekazywane są komunikaty WM_COMMAND. Jednakże do obsługi tego komunikatu (w mapie komunikatów) używane jest makro ON_UPDATE_COMMANDJUI. Makro to powoduje skojarzenie komunikatu z odpowiednią procedurą obsługi, do której prze­kazywany jest obiekt klasy CCmdUI (patrz tabela 1,11). Obiekt ten jest abstrakcyjną reprezentacją odpowiedniego elementu interfejsu użytkownika. Używając go, jesteś w stanie wyłączyć lub uaktywnić dany element. Możesz także określać pozostałe parametry elementu. Taką procedurę obsługi elementu interfejsu użytkownika będziesz musiał defi­niować tylko i wyłącznie wtedy, gdy zechcesz mieć pełną kontrolę nad danym elementem.

Tabela 1.11. Składowe klasy CCmdUI

Składowa Przeznaczenie.

m_nID Identyfikator elementu (jeśli jest dostępny).

m_nIndex Indeks elementu (jeśli jest dostępny).

m_pMenu Uchwyt do głównego menu (jeśli jest dostępny).

m_pSubMenu Uchwyt do podmenu (jeśli jest dostępny).

m_pOther Uchwyt okna do elementu innego typu.

Enable Aktywuje (lub wyłącza) element.

SetCheck Ustawia (lub usuwa) zaznaczenie elementu.

SetRatio Zaznacza element i usuwa zaznaczenie pozostałych elementów w grupie.

SetText Ustawia tekst elementu.

ContinueRouting Przesyła komunikat do następnej procedury obsługi w łańcuchu procedur obsługi komunikatów.

Dlaczego MFC używa obiektów klasy CCmdUI? Dlaczego nie przekazuje po prostu wskaźnika do przycisku, opcji menu lub innego obiektu interfejsu, na którym użytkownik wykonał jakąś czynność? Załóżmy, że dysponujesz opcją menu, która posiada odpo­wiadający jej funkcjonalnie przycisk na pasku narzędzi. W takiej sytuacji możesz użyć tej samej procedury do sterowania działaniem obu tych elementów. Twój kod nie musi się przejmować różnicami pomiędzy obydwoma tymi elementami. Można więc założyć, że jeśli w przyszłych wersjach systemu Windows udostępniony zostanie element interfejsu użytkownika przypominający tablicę do rzutków, klasa CCmdUI będzie wiedziała jak należy go obsługiwać.

Różnicę pomiędzy ramka a innymi oknami jest to, iż można załadować menu ramki, jej ikonę, kursory oraz tytuł bezpośrednio z zasobów programu. Możesz to zrobić w bardzo prosty sposób - wystarczy nadać każdemu elementowi ten sam identyfikator (na przykład, IDR_MAINFRAME) i wywołać metodę LoadFrame przekazując do niej ten identyfi­kator. Wywołanie tej metody spowoduje, że MFC stworzy ramkę oraz automatycznie załaduje odpowiednie zasoby i skojarzy je z oknem ramki. Ramki SDI używają pierw­szej części łańcucha zasobu jako swojego tytułu. Łańcuch ten służy do wielu celów (używany jest on także przez szablon dokumentu). To właśnie dlatego musisz go dzielić na odrębne pola używając do tego znaku “\n". Nowa ramka stworzona za pomocą sza­blonu dokumentu także użyje metody LoadFrame. Podczas tworzenia nowej ramki użyte zostaną także inne pola zdefiniowane w łańcuchu zasobów.

Wewnątrz ramki możesz własnoręcznie tworzyć nowe widoki posługując się do tego metodą CreateYiew. Postępowanie takie jest jednak przysparzaniem sobie dodatkowych problemów, gdyż metoda ta nie wykonuje za Ciebie żadnej pracy. Dlatego powinieneś pozwolić obiektom wchodzącym w skład szablonu dokumentu robić to, co potrafią najlepiej -tworzyć i kojarzyć ze sobą dokumenty, widoki i ramki.


CDocTemplate

Elementem łączącym dokumenty, widoki oraz ramki, są obiekty klasy CDocTemplate. Programy SDI używają w rzeczywistości obiektów klasy CSingleDocumentTemplate, natomiast programy MDI - obiektów klasy CMultiDocTemplate. Obie te klasy są klasami potomnymi klasy CDocTemplate.

Klasa CDocTemplate ma wiele interesujących składowych (patrz tabela 1.12). Większość z czynności wykonywanych przez obiekty tej klasy jest odpowiedzią na działania użyt­kownika (np.: wybranie z menu opcji Plik>Nowy lub Plik>Otwórz).

Tabela 1.12. Kluczowe składowe klasy CDocTemplate

Składowa Przeznaczenie

GetFirstDocPosition Zwraca obiekt klasy POS1TION poprzedzający pierwszy dokument na liście.

GetNextDoc Pobiera następny dokument z listy.

GetDocString Zwraca określone pole łańcucha znaków zasobu dokumentu

CreateNewDocument Tworzy nowy dokument.

CreateNewFrame Tworzy nowe okno ramki wraz z towarzyszącymi mu obiektami widoku i dokumentu.

InitialUpdateFrame Powoduje wywołanie metody OnlnitialUpdate we wszystkich widokach ramki.

SaveAllModified Zapisuje wszystkie dokumenty.

CloseAllDocuments Zamyka wszystkie dokumenty.

OpenDocumentFile Otwiera plik, lub tworzy nowy, pusty dokument.

SetDefaultTitlef Umożliwia zmianę domyślnego tytułu.


Szablon dokumentu używa tego samego łańcucha znaków przechowywanego w zasobach programu, który jest wykorzystywany przez ramkę. Łańcuch ten jest w rzeczywistości złożony z siedmiu części, oddzielonych od siebie znakami “\n" (patrz tabela 1.13). Identyfikator łańcucha znaków podajesz podczas tworzenia szablonu dokumentu. Oprócz tego, podczas tworzenia szablonu dokumentu, będziesz musiał podać także nazwę klasy widoku, dokumentu oraz ramki.

Tabela 1.13. Pola łańcucha znaków używanego przez obiekty klasy CDocTempIate.

Pole Nazwa Przeznaczenie

0 CdocTemplate::windowTitle Tytuł (używany jedynie w programach SDI)

1 CdocTemplate::docName Nazwa nadawana nowemu dokumentowi (MFC dodaje do tej nazwy kolejny numer tworzonego dokumentu)

2 CdocTemplate::fileNewName Nazwa jaką MFC wyświetla w oknie dialogowym Nowy plik jeśli dostępny jest więcej niż jeden szablon dokumentu

3 CdocTemplate::filterName Nazwa rozszerzenia plików dokumentów tego typu

4 CdocTemplate::filterExt Rozszerzenie dokumentów tego typu (np. BLK)

5 CdocTemplate::regFileTypeId Wewnętrzna nazwa rejestru dokumentów tego typu

6 CdocTemplate::regFileTypeName Dostępna dla użytkownika nazwa tego typu dokumentów, umieszczana w rejestrze

Po raz kolejny mamy tu przykład dosyć nieszczęśliwego doboru nazwy. “Dokument" rozumiany w kontekście szablonu dokumentu, oznacza dokument w takim znaczeniu, w jakim myśli o nim użytkownik. Dla przykładu, mógłbyś posiadać osobny szablon dokumentu dla arkusza kalkulacyjnego, edytora lub terminala komunikacyjnego. Nie pomyl tego z obiektem dokumentu. Szablon dokumentu umożliwia stworzenie połączenia pomiędzy dokumentem, widokiem oraz ramką. Właśnie to połączenie jest najczęściej rozumiane przez użytkowników jako dokument.

Kreator App Wizard sam zadba o to, aby Twój program, w czasie wykonywania metody Initlnstance, inicjalizował na starcie szablon dokumentu. Następnie szablon ten umieszcza­ny jest na liście szablonów programu za pomocą metody AddDocTemplate. Dla większości standardowych programów na tej liście umieszczany będzie tylko jeden szablon. Ze względu na to, wybór szablonu dokumentu, na którym należy działać, nie sprawia za­zwyczaj żadnych problemów.

Jednak, istnieje możliwości zdefiniowania większej ilości szablonów. Jeśli dodasz nowy szablon do listy aplikacji, wszystko stanie się mniej proste i oczywiste. Jeśli będziesz próbował otworzyć nowy dokument, MFC spróbuje dopasować jego rozszerzenie do rozszerzenia podanego w łańcuchu zasobów. Jeśli użytkownik poprosi o stworzenie nowego pliku, MFC stworzy okno dialogowe zawierające nazwy wszystkich dostępnych szablonów dokumentów (nazwy te określane są za pomocą drugiego pola przedstawionego w tabeli 1.13). Dzięki temu użytkownik będzie mógł określić typ pliku, który chce stworzyć

.

Dodatkowe szablony dokumentów

Nie ma żadnego powodu, dla którego musiałbyś dodawać szablony dokumentów do listy szablonów aplikacji, jeśli nie chcesz, aby były one uwzględniane podczas takich operacji jak tworzenie nowego pliku lub otwieranie pliku już istniejącego.

Warto tu zwrócić uwagę na kilka rzeczy. Po pierwsze - nie musisz dodawać szablonów dokumentów do listy szablonów aplikacji. Dlaczego miałbyś to robić? Załóżmy, że piszesz program do gry w warcaby. W takiej sytuacji chciałbyś, aby główny szablon dokumentu tworzył planszę do gry. Mógłbyś stworzyć także inny widok, który prezentowałby listę wszystkich wykonanych ruchów. Nie jest to jednak widok, który mógłby być tworzony za pomocą polecenia Plik^Nowy. Do obsługi takiego widoku mógłby posłużyć niezależny szablon dokumentu.

Kolejną rzeczą, która może nie być oczywista, jest sposób w jaki możesz użyć szablonu do stworzenia nowego dokumentu bez posługiwania się przy tym obiektem aplikacji. Dla przykładu załóżmy, że nie podoba Ci się domyślne działanie polecenia Plik&Nowy. Może chciałbyś mieć dodatkowe podmenu, które pozwalałoby na wybranie typu tworzone­go dokumentu. Aby postąpić w taki sposób, wystarczy wywołać metodę OpenDocument-File odpowiedniego szablonu dokumentu, z argumentem o wartości NULL. Może to dziwne, lecz wywołanie tej metody spowoduje stworzenie nowego dokumentu. Przykład takiego zastosowania metody OpenDocumentFile będziesz mógł znaleźć w praktycznej części tego rozdziału.

Jednym z niestandardowych zastosowań szablonów dokumentów jest tworzenie nowych widoków skojarzonych z jednym i tym samym dokumentem. Aby to zrobić, wystarczy najpierw wywołać metodę CreateNewFrame odpowiedniego szablonu dokumentu, a następnie metodę InitialUpdateFrame. Odpowiedni przykład znajdziesz w części praktycznej znajdującej się pod koniec tego rozdziału.

Uważaj na klasy

Pamiętaj, aby wywołać metodę InitialUpdateFrame obiektu szablonu dokumentu. Również obiekty klasy CFrameWnd dysponują tą samą metodą, jednakże to nie ją będziesz chciał w tym przypadku wywołać.


Obsługa obiektów podczas działania programu

Ile razy zdarzyła Ci się sytuacja, gdy zaczynałeś coś robić w domu i okazywało się, że nie możesz tego skończyć z powodu nieodpowiedniego śrubokręta? To samo może Ci się przydarzyć w programach pisanych przy wykorzystaniu biblioteki MFC. Optymalnie byłoby, gdybyś mógł obsługiwać komunikaty w tych klasach, w których jest to najbardziej wygodne i sensowne. Przypuśćmy, że piszesz edytor tekstu. Gdzie powinno być obsługi­wane polecenie Edycja>Wklej? Przypuszczalnie najlepiej byłoby je obsługiwać w obiekcie dokumentu. Zastanów się nad tym przez chwilę. Masz zamiar pobrać tekst ze Schowka i umieścić go w dokumencie. Potem będziesz musiał wywołać metodę UpdateAllViews w celu aktualizacji wszystkich widoków.

To jest prosty przykład. Jednak teraz zastanów się nad obsługą polecenia Edycja ^Kopiuj. Gdzie powinno być obsługiwane to polecenie? Jeśli umieścisz jego obsługę w obiekcie dokumentu, okaże się, że masz problem, gdyż nie jesteś w stanie dowiedzieć, się jaki fragment tekstu jest aktualnie zaznaczony w aktywnym widoku. No dobra, w takim razie może trzeba umieścić procedurę obsługi tego polecenia w obiekcie widoku? Jednak widok nie ma bezpośredniego dostępu do danych, które chcesz umieścić w Schowku. Okazuje się, że nie ma żadnego dobrego rozwiązania tego problemu. Niezależnie od tego, w którym obiekcie umieścisz procedurę obsługi tego polecenia, i tak będziesz musiał uzyskać dostęp do drugiej klasy, aby móc wykonać wszystkie potrzebne operacje. Wbrew pozorom jest to sytuacja bardzo częsta.

Na szczęście MFC dostarcza wielu sposobów na uzyskanie dostępu do wszelkiego typu obiektów. (Wszystkie potrzebne informacje przedstawione zostały w tabeli 1.14). Aby ich użyć, w lewej kolumnie tabeli odszukaj obiekt, w którym będziesz wykonywał operacje. Następnie, w środkowej kolumnie, odszukaj obiekt, do którego chcesz uzyskać dostęp. W tym samym wierszu, w prawej kolumnie tabeli, znajdziesz metodę, której będziesz musiał użyć. Jeśli nie będziesz w stanie odszukać bezpośredniej metody uzyskania dostępu do obiektu, którego Ci potrzeba, będziesz musiał wykonać kilka odpowiednich kroków pośrednich.

Tabela 1.14. Mapa skarbów MFC.

Aby z obiektu

Dokument


Dokument

Widok

Widok

Ramka

Ramka

Główna ramka w programie MDI

Ramka potomna w programie MDI

Uzyskać dostęp do ...

Widoku


Szablonu

Dokumentu

Ramki

Widoku

Dokumentu

Ramki potomne programu MDI

Ramki głównej programu MDI

Użyj metody GetFirstYiewPosition i GetNextView

GetDocTemplate

GetDocument

GetParentFrame

GetActiveView

GetActiveDocument

MDIGetActive

GetParentFrame

W większości wypadków, wywołanie metody umożliwiającej dostęp do potrzebnej Ci klasy nie będzie trudne. Dla przykładu, aby uzyskać dostęp do dokumentu w obiekcie widoku, wystarczy wywołać metodę GetDocument. Jednakże zdarzają się pewne wy­jątkowe sytuacje, o których powinieneś wiedzieć.

Najczęstszą sytuacją wyjątkową jest odszukiwanie widoków. Jeśli widok, który chcesz odszukać jest widokiem aktywnym - to znaczy posiada ognisko wprowadzania - nie będziesz miał żadnych problemów z uzyskaniem dostępu do niego. Jeśli jednak będziesz chciał uzyskać dostęp do widoku z obiektu dokumentu, możesz mieć z tym problemy. Jednemu dokumentowi może bowiem odpowiadać wiele widoków; dlatego jedno wy­wołanie metody może nie wystarczyć do odszukania odpowiedniego widoku. Możesz uzyskać dostęp do wszystkich widoków za pomocą metod GetFirstViewPosition oraz GetNextView. Oczywiście, jeśli wiesz, że w Twoim programie jest tylko jeden widok, możesz bez obaw wywołać metodę GetNextView jeden raz, i przyjąć, że jest to właśnie ten widok, o który Ci chodzi.

Ciekawe rzeczy mogą się także zdarzać w programach MDI. Jeśli wywołasz metodę GetActiveView w głównej ramce programu MDI, otrzymasz wynik o wartości NULL. To samo zdarzy się, gdy wywołasz metodę GetActiveDocument (co nie jest w zasadzie niczym zaskakującym, gdyż metoda ta jest kamuflażem wywołania dwóch metod GetActiveView i GetDocument). Na pierwszy rzut oka takie zachowanie może się wydać zaskakujące i zagadkowe. Jednak jeśli się nad nim trochę zastanowić, okaże się, iż jest ono całkiem sensowne. Główna ramka programu MDI nie ma bowiem żadnego aktywnego widoku; zamiast tego ma ona aktywne okno ramki potomnej. A z kolei to okno będzie miało aktywny widok. No i problem został rozwiązany. Dostęp do aktywnego okna ramki potomnej można w programie MDI uzyskać za pomocą metody MDIGetActive.

Inne problemy mogą się pojawić podczas pracy z oknami dialogowymi (klasą CDialog). Konstruktor okna dialogowego posiada argument umożliwiający Ci określenie okna rodzicielskiego dialogu. Załóżmy, że jako okna rodzicielskiego użyjesz widoku. Mógłbyś przypuszczać, że wywołanie metody GetParent w obiekcie okna dialogowego spowoduje zwrócenie wskaźnika na obiekt CView. Tak się jednak nie dzieje. Okno dialogowe au­tomatycznie sprawdza, czy jego okno rodzicielskie jest oknem najwyższego poziomu. Jeśli tak nie jest, to automatycznie pobiera okno rodzicielskie okna przekazanego w konstruktorze. Jeśli to okno też nie jest oknem najwyższego poziomu, pobierane jest jego okno rodzicielskie; procedura ta powtarzana jest do momentu odszukania okna najwyższego poziomu. Dopiero to okno stanie się oknem rodzicielskim okna dialogowego. Oczywiście, dysponując dostępem do okna najwyższego poziomu, które zapewne będzie oknem ramki, będziesz mógł użyć metod przedstawionych w tabeli 1.14 do odszukania aktywnego widoku.


Obiekty pomocnicze

MFC posiada wiele obiektów pomocniczych, które nie są w żaden sposób związane z architekturą programów pisanych przy użyciu tej biblioteki. Klasy, takie jak: CFile lub CString, mogą Ci jednak bardzo ułatwić tworzenie programów. Oczywiście nie bę­dziesz musiał używać tych klas, jeśli nie będzie Ci to do niczego potrzebne. W końcu możesz przetwarzać łańcuchy znaków za pomocą normalnych tablic znakowych, a pliki obsługiwać za pomocą klasycznych uchwytów lub strumieni dostępnych w C++. Może tak robić, ale jeśli raz przyzwyczaisz się do korzystania z klas MFC, zapewne będziesz wolał ich używać.

Jedną z największych zalet MFC jest fakt, iż biblioteka ta zna swoje własne możliwości i ograniczenia. MFC zdaje sobie sprawę z tego, że nie jest w stanie zrobić wszystkiego, i że od czasu do czasu będziesz musiał skorzystać bezpośrednio z API systemu Win­dows lub z funkcji dostępnych w dodatkowej bibliotece DLL. Dlatego też niektóre kla­sy mogą być bardzo łatwo konwertowane na inne, odpowiadające im typy danych sto­sowanych w programach nie korzystających z MFC.

Doskonałym przykładem takiego działania jest klasa CString. Obiekty tej klasy doskonale wiedzą w jaki sposób mogą zostać zamienione w stały wskaźnik na znak. Oznacza to że obiektów klasy CString możesz używać wszędzie tam, gdzie wcześniej używałeś wskaźnika na niemodyfikowalny łańcuch znaków. Takiego łańcucha znaków nie możesz zmodyfikować, gdyż klasa CString nie byłaby w stanie określić długości łańcucha. Swoją drogą, jeśli będziesz potrzebował dostępu do wskaźnika na znak, którego zawartość możesz zmodyfikować, to możesz posłużyć się metodą GetBuffer, określając jednocześnie ilość znaków, na której będzie można operować.

Innym przykładem klas, które bardzo łatwo można poddawać konwersjom, są klasy CRect, CPoint oraz CSize. Klasy te wyprowadzone zostały ze struktur danych używanych w systemie Windows - RECT, POINT oraz SIZE. Nic nie stoi na przeszkodzie, aby klasa była wyprowadzona ze struktury - jest to jak najbardziej legalne. Cały kruczek wymienionych powyżej klas polega na tym, iż MFC nie dodaje do nich żadnych dodat­kowych składowych - ani danych ani funkcji wirtualnych. Dzięki temu, instancje tych klas w C++ będą identyczne jak struktury definiowane w C. Oznacza to, że możesz taktować RECT jako CRect, i na odwrót. Jest to bardzo wygodne. Jednakże ze względu na to, iż klasy te nie mają funkcji wirtualny, nie działają one jak większość klas MFC (patrz sekcja Pomoc ze strony klasy CObject, znajdująca się w dalszej części rozdziału).


Obiekty klasy CWnd

Spośród wszystkich obiektów pomocniczych, żadne nie są tak istotne, jak obiekty klasy CWnd. Klasa ta używana jest w MFC jako reprezentacja wszystkich okien (w tym wido­ków oraz ramek, które także są wyprowadzone z tej klasy).

Pracując z obiektami tej klasy musisz być świadomy kilku rzeczy. Po pierwsze, nie myśl o tych obiektach jako o rzeczywistych oknach, gdyż nimi nie są. Są one obiektami C++ zawierającymi okna. Ja przyzwyczaiłem się do myślenia o obiektach klasy CWnd jako o butelkach. Gdy stworzysz butelkę, jest ona początkowo pusta. Butelkę tę możesz wypełnić - stworzyć w niej nowe okno - używając do tego odpowiednich funkcji (Create, DoModal lub LoadFrame). Możesz także skojarzyć istniejące już okno z obiektem klasy CWnd.

Załóżmy, że dysponujesz uchwytem do okna, i że chciałbyś taktować go jako obiekt klasy CWnd. Jeśli potrzebujesz go tylko na chwilkę, wskaźnik do obiektu CWnd możesz uzyskać za pomocą statycznej metody CWnd::FromHandle. Jeśli okno ma już odpo­wiadający mu obiekt klasy CWnd, metoda FromHandle zwraca wskaźnik do tego obiektu. Jeśli okno nie ma skojarzonego z nim obiektu MFC, metoda ta powoduje stworzenie tymczasowego obiektu skojarzonego z tym oknem. Pamiętaj jednak, że takie obiekty klasy CWnd nie obsługują żadnych komunikatów. Co więcej - MFC automa­tycznie usunie taki obiekt niedługo po zakończeniu obsługiwania aktualnego komunikatu. Nie możesz więc zapamiętać takiego wskaźnika i używać go później.

Jeśli chcesz skojarzyć okno z obiektem klasy CWnd w bardziej trwały sposób, musisz użyć metody SubclassWindow (lub SubclassDlgltem). Metoda ta kojarzy okno 7. obiektem klasy CWnd, aktywuje pętlę obsługi komunikatów i pozwala Ci na pracę z tak stworzonym obiektem, jak gdyby został on stworzony przez MFC.

Jak widać, łatwo jest przekształcić uchwyt do okna w obiekt klasy CWnd. Przekształcenie w przeciwnym kierunku jest jeszcze łatwiejsze. Uchwyt do okna jest zawsze przecho­wywany w publicznej zmiennej składowej m_hWnd klasy CWnd. Jest to kolejny przykład, ilustrujący jak MFC umożliwia współpracę ze standardowymi funkcjami AP1 systemu Windows.

Stworzenie obiektu klasy CWnd nie jest tym samym, co stworzenie okna. Proces ten określany jest mianem “tworzenia dwuetapowego". Polega ono na tym, iż w pierwszej kolejności tworzysz obiekt C++, a następnie wywołujesz metodę Create (lub inną) w celu stworzenia okna skojarzonego z obiektem klasy CWnd. Takie postępowanie za­pewnia klasie CWnd maksymalną elastyczność. Pozwala ono na tworzenie zupełnie nowych okien lub kojarzenie okien już istniejących z obiektami klasy CWnd. Co więcej, kod służący do tworzenia okien umieszczony jest wewnątrz funkcji, co pozwala na łatwe zasygnalizowanie powstania błędu. Jak wiadomo, w języku C++ konstruktory klas nie mogą zwracać żadnych wartości, co utrudnia sygnalizowanie powstania błędu podczas działania konstruktora.

Nie tylko klasa CWnd wykorzystuje dwuetapowy proces tworzenia; w podobny sposób postępuje bardzo wiele innych klas MFC. Są to, dla przykładu, takie klasy jak: CBrush, CPen, jak również wiele innych obiektów związanych z zasobami GDI. Obiekty wielu prostszych klas można tworzyć przy użyciu specjalnego konstruktora (tworzenie jed­noetapowe). Obiekty te pozwalają także na chwilowe skojarzenie z rzeczywistymi obiektami GDI. Dla przykładu, możesz użyć metody CPen::FromHandle do skon-wertowania uchwytu typu HPEN do obiektu klasy CPen. W celu uzyskania dostępu do rzeczywistego obiektu GDI, możesz użyć zmiennej składowej m_hObject (będącej częścią klasy bazowej CGDIObject).


Pomoc ze strony klasy CObject

Większość najważniejszych klas MFC jest wyprowadzona z klasy CObject. Klasa ta udostępnia kilka ważnych cech, powszechnie wykorzystywanych we wszystkich programach MFC. Cechami tymi są:

możliwość użycia typu CObject * do reprezentowania większości obiektów MFC (dzięki polimorfizmowi dostępnemu w C++);

udostępnianie przez klasę CObject sposobów tworzenia i kontroli typów podczas działania programu;

możliwość wykorzystania, w obiektach klas wyprowadzonych z klasy CObject, mechanizmów do zapisywania i odczytywania zawartości obiektu z pamięci stałych (np.: z pliku);

Polimorfizm był wyjątkowo istotny zanim w Visual C++ udostępniono możliwość korzystania z szablonów. Ze względu na to, że w zasadzie wszystkie klasy MFC są wyprowadzone z klasy CObject, w MFC można było stworzyć pewne ogólne struktury danych (na przykład listy), w których można było przechowywać wskaźniki do obiektów klasy CObject. W tych strukturach danych mogłeś umieścić wskaźniki do obiektów dowolnych klas, które zostały wyprowadzone z klasy CObject. Dzisiaj, w MFC dostępne są takie same struktury danych, tworzone w oparciu o szablony (patrz sekcja Kilka słów o Kolekcjach, znajdująca się w dalszej części tego rozdziału). Stosowane dawniej struktury danych cały czas są dostępne - głównie z powodu konieczności zapewnienia kompatybilności z poprzednimi wersjami MFC.

Choć dzisiaj, w programach napisanych w C++, istnieją już standardowe możliwości sprawdzania typów podczas działania programu, to jednak w MFC luksusy tego typu dostępne były dużo wcześniej. MFC ma swojej własne sposoby kontrolowania typów. Każda klasa posiada specjalną wartość, która w unikalny sposób ją identyfikuje. Wartość ta uzyskiwana jest poprzez skonwertowanie nazwy klasy za pomocą makra RUNTIME_CLASS. Ten unikalny identyfikator klasy można także pobrać podczas działania programu; służy do tego metoda GetRuntimeClass. Aby dodać do klasy pełne możliwości kontroli typów podczas działania programu, będziesz musiał dodać makro DECLARE_DYNAMIC w definicji klasy (zazwyczaj definicja taka umieszczana jest w pliku nagłówkowym). Oprócz tego, w pliku źródłowym, będziesz musiał umieścić makro IMPLEMENT_DYNAMIC (makro to musi zostać umieszczone w globalnym zakresie widzialności - czyli poza jakimikolwiek funkcjami).

Załóżmy, że dysponujesz klasą o nazwie Base1, będąc klasą potomną klasy CObject. Co więcej, klasa Basel używa opisanych powyżej dynamicznych makr. Załóżmy dalej, że dysponujesz także klasą Class2, wyprowadzoną z klasy Basel. Również i ta klasa używa tych samych dynamicznych makr. Dysponując takimi dwoma klasami będziesz mógł zastosować przedstawiony poniżej fragment kodu:

Basel bl;

Class2 c2;

CObject *ol, *o2;

01 = &bl;

02 = &c2;

if (ol->IsKindOf(RUNTIME_CLASS(Basel)}} /* prawda */ ;

if (ol->IsKindOf(RUNTIME_CLASS(Class2))) /* fałsz */ ;

if (o2->IsKindOf(RUNTIME^CLASS(Class2))) /* prawda */ ;

// sprawdzaj dalej ...

if (o2->IsKindOf(RUNTIME_CLASS(Basel))) /* prawda! */ ;

// o2 jest typu Basel (i Class2)

// jeśli chcesz dokładnie sprawdzić czy o2 jest klasy Basel,

// użyj poniższego kodu:

if (o2->GetRuntimeClass()==RUNTIME_CLASS(Basel)) /* fałsz */ ;

Zasada działania metody IsKindOf okaże się sensowna po chwili zastanowienia. Jeśli posiadasz obiekt klasy CView i zapytasz czy jest on rodzaju CWnd, odpowiedź na to pytanie będzie twierdząca, l chociaż jest to widok, jest on tylko bardziej wyspecjalizo­wanym rodzajem okna. W obiektach klasy CView możesz odwoływać się do metod zdefiniowanych w klasie CWnd.

Klasa CObject ma także swój wkład w umożliwianie przechowywania stanu obiektów. Dzięki tej możliwości, program jest w stanie zapisać i odczytać z pamięci stałej stan obiektów klas wyprowadzonych z klasy CObject. Więcej na temat tego mechanizmu - zwanego serializacją — dowiesz się w Rozdziale 2.

Kolejnym poziomem zwiększania możliwości funkcjonalnych programu dostępnych podczas jego działania, jest możliwość dynamicznego tworzenia obiektów. Dodatkowe możliwości dostępne są dzięki użyciu makr DECLARE DYNCREATE oraz IMPLEMENT_DYNCREATE. Makra te zapewniają takie same możliwości, jak makra DECLA_RE_DYNAMIC oraz IMPLEMENT_DYNAMIC, dodatkowo je rozszerzając. Dzięki ich użyciu umożliwisz MFC dynamiczne stworzenie obiektu odpowiedniej klasy za pomocą wywołania metody CreateObject; jako argument tej metody przekazywany jest unikalny identyfikator klasy.

W MFC, opisane powyżej możliwości, są bardzo często wykorzystywane, na przykład podczas tworzenia szablonów dokumentów oraz powielania widoków (za pomocą opcji Widok>Nowe okno). Klasy umożliwiające dynamiczne tworzenie muszą posiadać domyśl­ny konstruktor (oto kolejny przykład, w którym widać znaczenie i celowość stosowania dwuetapowego tworzenia obiektów). Wiele klas definiuje domyślny konstruktor jako składową chronioną, stwarzając konieczność dynamicznego tworzenia instancji tej klasy.


Kilka słów o Kolekcjach

MFC dostarcza kilku interesujących klas kolekcji, umożliwiających tworzenie takich struktur danych jak tablice, listy oraz tablice asocjacyjne (zwane także mapami). Dzięki zastosowaniu szablonów możesz tworzyć powyższe struktury danych zawierające dane dowolnych typów. Dodatkowo, klasy te umożliwiają tworzenie funkcji pomocniczych, służących do modyfikowania działania kolekcji i dostosowywania jej do Twoich indy­widualnych wymagań.


Sposoby używania szablonów

Szablony są nowym narzędziem, dodanym do języka C++ stosunkowo niedawno. Pozwalają one na tworzenie funkcji i klas, które w bardzo łatwy sposób można wielokrotnie wy­korzystywać. Aby było to możliwe, szablony powodują modyfikację kodu podczas kompilacji programu; dzięki temu, pisząc program możesz się nie przejmować używanymi typami danych. Rozpatrzmy przykład funkcji, która ma zwrócić większą z dwóch wartości przekazanych do funkcji. Poniżej podałem przykład takiej funkcji operującej na liczbach całkowitych:

int Max(int vi, int v2)

{

return vl>v2?vl:v2;

}

Działanie powyższego kodu zależy tylko i wyłącznie od operatora większości. Nie ma żadnego powodu, aby powyższy kod nie mógł być wykorzystany do zwrócenia wartości maksymalnej jakiegokolwiek innego typu danych, pod warunkiem, iż dla tego typu zde­finiowany jest operator większości. Jasne jest jednak, że powyższy kod może być użyty tylko dla danych typu int; typ ten jest na stałe zakodowany w definicji funkcji. O ile funkcja może operować tylko na danych typu int, o tyle stosowany przez nią algorytm (porównanie opierające się na wykorzystaniu operatora większości) może być zastosowany także dla innych typów danych. Rozwiązaniem powyższego dylematu jest zastosowanie szablonów, które pozwalają zdefiniować algorytm funkcji uniezależniając go od typu danych, na których funkcja ta będzie operować. Spójrz na poniższy przykład:

template <class TYPE> TYPE Max(TYPE vi, TYPE v2 )

{

return vl>v2?vl : v2 ;


}Dysponując taką funkcją, gdy w kodzie umieścisz jej wywołanie, którego argumentami będą dwie liczby typu int, kompilator stworzy funkcję identyczną jak funkcja przedsta­wiona na początku tej sekcji. Jeśli jednak wywołasz funkcję Max przekazując do niej argumenty typu float, kompilator stworzy nową wersję tej funkcji operującą na argu­mentach typu float. Pamiętasz zapewne, że język C++ pozwala na definiowanie funkcji o tej samej nazwie, o ile przekazywane do niej argumenty są różne (mechanizm ten na­zywany jest przeciążaniem funkcji). Zauważ, że w szablonach stosowane jest słowo kluczowe class, chociaż w tym kontekście nie ma ono niczego wspólnego z klasami definiowanymi przez użytkownika. W szablonach możesz używać zarówno wbudowanych typów danych (np.: int, char * czy też float), jak również typów danych definiowanych przez użytkownika.

Szablony są szczególnie użyteczne do enkapsulacji algorytmów w klasy. Załóżmy, że chciałbyś zdefiniować klasę, która pozwalałaby na podanie dwóch wartości i udostępniała metodę zwracającą większą z nich. Poniżej przedstawiony został kod szablonu definiu­jącego klasę, która może operować na danych dowolnego typu:

template <class TYPE> class Selctor

{

private:

TYPE vi;

TYPE v2 ;

public:

Selector (TYPE xvi, TYPE xv2) { v1=xv1; v2=xv2; };

void Set1 (TYPE v) {v1=v; };

void Set2 (TYPE v) {v2=v; };

TYPE GetMax(void) {return v1>v2?v1:v2; };

};

Jak widać, algorytm działania tej klasy jest niezależny do typu danych, na których klasa operuje. Aby stworzyć klasę Selector, operującą na liczbach całkowitych, użyj poniższego fragmentu kodu:

Selector<int> sel;

Selector<int> *selptr;

Zauważ, że we wszystkich miejscach, gdzie w szablonie umieszczone zostało słowo TYPE, w kodzie wynikowym umieszczone zostanie słowo kluczowe int. Oczywiście, możesz stworzyć tyle klas Selector ile będzie Ci potrzebnych (dla przykładu, klasy operujące na liczbach typu float lub obiektach klasy CString). Kompilator zajmie się wygenerowaniem odpowiednich funkcji. Warto abyś pamiętał, że nie musisz definiować metod takich klas jako Mine. Jeśli chciałbyś stworzyć metodę GetMax w tradycyjny sposób, możesz to zrobić w następujący sposób:

template <class TYPE>

TYPE Selector<TYPE>::GetMax(TYPE vi, TYPE v2)

{

return vl>v2?vl:v2;

}

Chociaż metody nie muszą być metodami Mine, to jednak wszystkie one muszą być widoczne w pliku źródłowym, w którym szablon jest wykorzystywany. Dlatego też wszystkie metody (niezależnie od tego, czy są to metody inline, czy też nie) będą za­zwyczaj umieszczane w pliku nagłówkowym. Jedynym wyjątkiem będzie sytuacja, w której szablon zostanie umieszczony w tym samym pliku źródłowym, w którym jest on wykorzystywany; przy dodatkowym założeniu, że nie będzie on wykorzystywany w żadnym innym pliku źródłowym.

Przedstawione do tej pory przykłady były wyjątkowo proste, gdyż żaden kod nie ulegał modyfikacjom z powodu zastosowanego typu danych. Często jednak będziesz chciał użyć specjalnego kodu obsługującego dane konkretnego typu. W takim wypadku będziesz mógł stworzyć ogólną klasę bazową zawierającą odpowiednie funkcje wirtualne.

Dla przykładu załóżmy, że chcesz rozszerzyć możliwości przedstawionej powyżej klasy Selector tak, aby pozwalała ona na wydrukowanie wartości maksymalnej, sformatowanej odpowiednio, w zależności od jej typu. Co więcej, załóżmy, że standardowy sposób formatowania dostępny w strumieniach wyjściowych nie spełnia Twoich oczekiwań. Szablonów można użyć także w tym przypadku:

template <class TYPE> class SelectorBase

{

private:

TYPE vi;

TYPE v2;

public:

Selector(TYPE xvi, TYPE xv2) { vl=xvl; v2=xv2; };

void SetKTYPE v) {vl=v;};

void Set2(TYPE v) {v2=v; } ;

TYPE GetMax(void) {return vl>v2?vl:v2;};

//czysta funkcja wirtualna - klasa pochodna musi ja przesłonić

virtual void PrintMax(void) = O;

};

class IntSelector : public Selector<int>

{

void PrintMax(void) { printf("Maksimum=%d (Ox%x)\n",GetMax());};

}

Chociaż argumenty określające typy są użyteczne, to w szablonach wykorzystywane są także argumenty stałe. Załóżmy, że chciałbyś stworzyć klasę Select umożliwiającą wybór wartości maksymalnej z grupy kilku liczb. Poniżej przedstawiony został odpowiedni przykład:

template <class T, int ct> class ArySelect

{

T * ary;

public:

ArySelectO { ary=new T[et]; };

-ArySelect() { delete [] ary; };

void Set(int n, T v) { ary[n]=v; };

T GetMax(void);

};

template <class T, int ct> T ArySelect<T,ct>::GetMax(void)

{

T maxv = ary[0];

for (int n=1; n<ct; n++)

if (maxv<ary[n]) maxv=ary[n]

return maxv;

};

W powyższym przykładzie do szablonu przekazywana jest stała całkowita określająca wielkość tablicy. Łatwo jest wyobrazić sobie prosty szablon, w którym nie byłyby wy­konywane żadne operacje na typach. Dla przykładu:

template <int szs> struct dataholder

{

char *name[sz] ;

int score [sz] ;

struct datahołder<sz> *links [2*sz] ;

};

Jest jeszcze jedno specjalne zastosowanie argumentów szablonów. Załóżmy, że chcesz stworzyć klasę, której zachowanie jest identyczne dla prawie wszystkich typów danych, za wyjątkiem wskaźników na znaki i obiektów klasy CString. W takim przypadku możesz stworzyć następujące szablony:

template <class T> class Special

{

// kod ogólny

};

template <char *> class Special

{

// kod obsługi danych typu char *

};

template <CString> class Special

{

// kod obsługi danych typu Cstring

};


Szczegółowe informacje o kolekcjach

MFC udostępnia trzy typy kolekcji działających w oparciu o szablony. Są to:

CList - Kolekcja przechowująca dane w taki sposób, iż są one umieszczone w odpowiednim porządku. Obiekty tej klasy są bardzo dobre do implemento­wania kolejek, stosów i innych podobnych struktur danych. (Lista składowych klasy CList przedstawiona została w tabeli 1.15.)

CArray - Jednowymiarowa tablica (patrz tabela 1.16). Obiekty klasy CArray mogą być rozszerzane, aby umożliwić przechowywanie dodatkowych danych. Dane dostępne są w dowolnej kolejności.

CMap - Kolekcja CMap jest podobna do tablicy, której indeksami mogą być wartości dowolnego typu. I tak, możesz stworzyć obiekt klasy CMap, w którym imiona (obiekty klasy CString) byłyby skojarzone z odpowiednimi oknami (obie­ktami klasy CWnd). Metody klasy CMap przedstawione zostały w tabeli 1.17.

Tabela 1.15. Metody klasy CList.

Metoda Opis

GetHead Zwraca pierwszy element (początek listy).

GetTail Zwraca ostatni element (koniec listy).

RemoveHead Usuwa pierwszy element listy.

RemoveTail Usuwa ostatni element listy.

AddHead Dodaje nowy element na początku listy.

AddTail Dodaje nowy element na końcu listy.

RemoveAll Usuwa wszystkie elementy listy.

GetHeadPosition Określa pozycję pierwszego elementu listy (jej początku). Pozycja określona

jest za pomocą wartości typu POSITION.

GetTailPosition Określa pozycję ostatniego elementu listy (jej końca). Pozycja określona jest za

pomocą wartości typu POSITION.

GetNext Zwraca następny element listy.

GetPrev Zwraca poprzedni element listy.

GetAt Zwraca określony element listy; metoda ta traktuje listę jak tablicę (co nie jest

efektywne).

SetAt Ustawia określony element listy; metoda ta traktuje listę jak tablicę (co nie jest

efektywne).

RemoveAt Usuwa określony element listy.

InsertBefore Wstawia nowy element przed wskazanym elementem listy.

InsertAfter Wstawia nowy element za wskazanym elementem listy.

Find Szuka elementu listy i zwraca jego pozycję (wartość typu POSITION).

FindIndex Zwraca wartość typu POSITION określającą pozycję wybranego elementu

listy.

GetCount Zwraca ilość elementów na liście.

IsEmpty Sprawdza, czy lista jest pusta.

Tabela 1.16. Główne metody klasy CArray.

Metoda Opis

GetSize Zwraca wielkość tablicy.

GetUpperBound Zwraca maksymalny indeks tablicy.

SetSize Określa wielkość tablicy.

FreeExtra Zwalnia wolną pamięć, w której nie są przechowywane żadne elementy.

RemoveAll Usuwa wszystkie elementy z tablicy.

GetAt Zwraca element tablicy.

SetAt Ustawia element tablicy.

ElementAt Pobiera odwołanie do elementu.

GetData Zwraca wskaźnik do danych elementu tablicy.

SetAtGrow Ustawia element, jednocześnie rozszerzając tablicę jeśli okaże się
to konieczne.

Add Dodaje nowy element na końcu tablicy, rozszerzając ją jeśli jest to konieczne.

Append Dodaje wskazaną tablicę do aktualnej, rozszerzając wielkość tablicy

wynikowej jeśli będzie to konieczne.

Copy Kopiuje tablicę.

InsertAt Wstawia element lub tablicę w określonym miejscu.

RemoveAt Usuwa wskazany element tablicy.

operator [] Pobiera lub ustawia wskazany element tablicy (zazwyczaj używany
zamiast metod SetAt oraz GetAt).

Tabela 1.17. Najważniejsze metody klasy CMa

pMetoda Opis

Lookup Odnajduje wartość na podstawie klucza.

SetAt Definiuje parę klucz/wartość.

Operator[] Umożliwia odwoływanie się do elementów tablicy mieszającej tak jak

do elementów tablicy.

RemoveKey Usuwa element.

RemoveAll Usuwa wszystkie elementy.

GetStartPosition Zwraca wartość określającą pozycję pierwszej pary klucz/wartość

(wartość typu POSITION).

GetNextAssoc Zwraca następną parę klucz/wartość.

GetHashTableSize Zwraca wielkość tablicy mieszającej.

InitHashTable Inicjuje wielkość tablicy mieszającej (użyteczne przy optymalizacji).

GetCount Zwraca ilość elementów tablicy mieszającej.

IsEmpty Sprawdza czy tablica mieszająca jest pusta.

Dlaczego miałbyś używać klas kolekcji? Oczywiście, w żaden sposób nie jesteś zobli­gowany, aby to robić. W wielu przypadkach używanie standardowych tablic dostępnych w języku C jest równie dobre, jak używanie obiektów klasy CArray. Jednak w wielu wypadkach klasy kolekcji dają znaczące korzyści. Jedną z ich podstawowych zalet jest możliwość zwiększania się pojemności tych klas. Kolejną zaletą jest możliwość stosowania standardowych w MFC mechanizmów testowania typów oraz serializacji kolekcji.

Klasy kolekcji pozwalają także na dostosowywanie swojego działania do Twoich indy­widualnych potrzeb. Dostosowywanie to odbywa się za pomocą specjalnych funkcji pomocniczych (patrz tabela 1.18). Funkcje te są wymagane tylko wtedy, gdy modyfi­kować działanie kolekcji. Kompilator sam zdecyduje, jakiej funkcji pomocniczej użyć, w zależności od zastosowanych argumentów. Załóżmy, że napisałeś funkcję pomocniczą SerializeElements pobierającą argument klasy CBlackBook. Ta funkcja pomocnicza będzie od tej pory używana przy serializacji wszystkich kolekcji zawierających dane typu CBlackBook.

Tabela 1.18. Funkcje pomocnicze kolekcji

Funkcja pomocnicza Opis

CompareElements Porównuje dwa elementy,

ConstructElements Konstruuje (inicjalizuje) stworzony już element.

CopyElements Kopiuje elementy z jednej kolekcji do drugiej.

DestructElements Niszczy elementy podczas ich usuwania.

DumpElements Wyświetla szczegółowe informacje o elementach (podczas testowania).

HashKey Tworzy wartość dla podanego klucza tablicy mieszającej.

SerializeElements Zapisuje/odczytuje elementy do/z archiwum.

Funkcje pomocnicze są prawie zawsze stosowane opcjonalnie. I tak, jeśli chcesz serializować kolekcję, dla której nie została zdefiniowana funkcja SerializeElements, serializacja będzie polegała na zapisaniu kolejnych bitów umieszczonych w kolekcji. Jeśli kolekcja zawiera liczby całkowite, wszystko będzie w porządku; jeśli jednak są w niej umieszczone wskaźniki na obiekty CWnd, taki sposób serializacji nie jest najlepszym rozwiązaniem. Więcej informacji na temat serializacji znajdziesz w Rozdziale 2.

Oczywiście nie wszystkie funkcje pomocnicze mogą być stosowane we wszystkich rodzajach kolekcji. Funkcja HashKey może być stosowana tylko w przypadku korzy­stania z kolekcji klasy CMap. A nawet wtedy, funkcja ta stosowana jest jedynie wtedy, gdy chcesz zdefiniować własny algorytm mieszający lub jeśli stosowane przez Ciebie klucze nie mogą być skonwertowane do wartości typu DWORD.


Podsumowanie

Jeśli chcesz nagiąć MFC do swoich własnych potrzeb, będziesz musiał wiedzieć co, jak i dlaczego dzieje się w tej bibliotece. Jeśli będziesz w pełni polegał na stosowanych na­rzędziach, wkrótce staniesz się ich niewolnikiem. W optymalnej sytuacji powinieneś wiedzieć jak działa MFC oraz dlaczego została zaprojektowana w taki, a nie inny sposób.

Wielu specjalistów uskarża się na to, że MFC nie jest zaprojektowana w dobry sposób, że nie jest dobrym przykładem programowania obiektowego, że są znacznie lepsze metody osiągnięcia tych samych celów. Być może, niektóre z ich skarg są uzasadnione.

Jeśli chodzi o sposób, w jaki biblioteka MFC została zaprogramowana, jest on taki, a nie inny i skarżenie się nic w tej kwestii nie zmieni. Według mnie, uskarżanie się nie wniesie niczego dobrego. Nie wiem dlaczego, ale podejrzewam, że skargi na MFC są bardzo podobne do skarg na firmę Microsoft... MFC, system Windows, C++ - ani je­den z tych produktów nie jest doskonały, a jednak wszystkich ich używamy do tworze­nia naszych programów.


Praktyczny przewodnik Architektura

Obsługa komunikatów użytkownika

Tworzenie nowych typów dokumentów

Tworzenie prywatnych dokumentów

Dołączanie większej ilości widoków do jednego dokumentu

Tworzenie oddzielnych menu Plik^Nowy

Zapobieganie tworzeniu nowego dokumentu przy starcie programu

Analiza argumentów wywołania programu

Obliczanie wielkości widoku

Używanie słowa kluczowego typedef ze wzorcami

Dwuwymiarowe obiekty CArray.

Czasami znajomość wewnętrznych zasad działania MFC może się okazać bardzo przydatna. Nikomu nie chce się grzebać w mapach komunikatów i własnoręcznie tworzyć widoków, jednakże czasami nie będziesz miał większego wyboru i będziesz musiał to zrobić. Przedstawione poniżej praktyczne porady pomogą Ci w tych wszystkich sytuacjach, gdy zostaniesz postawiony przed koniecznością zmodyfikowania lub nagięcia do swoich potrzeb standardowej architektury MFC.


Obsługa komunikatów użytkownika

Jeśli zdefiniujesz swoje własne komunikaty (komunikaty o identyfikatorach większych lub równych WM_USER), nie będziesz mógł w pełni polegać na tym, że kreator Class Wizard poprawnie je obsłuży. Masz kilka sposobów rozwiązania tego problemu. Możesz użyć makra ON_MESSAGE do obsługi takich komunikatów. Makro to pozwala na skojarzenie komunikatu z funkcją, która będzie używana do jego obsługi; wystarczy w tym celu podać identyfikator komunikatu oraz nazwę funkcji.

Funkcja obsługująca komunikat musi zwracać wartość typu LONG i pobierać dwa argumenty (odpowiednio typów: WPARAM i LPARAM), nawet jeśli funkcja nie zwraca żadnego wyniku i nie potrzebuje żadnych argumentów. Oczywiście, jeśli nie planujesz używania argumentów, możesz pominąć ich nazwy; typy jednakże będą musiały zostać podane.

Technicznie rzecz biorąc, przedstawionej powyżej metody możesz używać do obsługi wszystkich komunikatów. Jednak zrobisz znacznie lepiej używając wbudowanych makr tam, gdzie tylko będzie to możliwe. Makra te wiedzą w jaki sposób należy przetworzyć argumenty konkretnego typu komunikatów oraz jaki typ wyniku powinna zwracać funkcja służąca do ich obsługi. Jeśli identyfikator komunikatu jest przechowywany w zmiennej -jak się dzieje w przypadku zarejestrowanych komunikatów możesz użyć makra - ON_REGISTERED_MESSAGE. Poniżej przedstawiony został przykład za­stosowania makra ON_MESSAGE:

#define WM_BLACKBOOK (WM_USER-ł5)

BEGIN_MESSAGE“MAP

//{{AFX_MSG_MAP(CMainFrame)

// Makra umieszczone automatycznie przez kreatora Class Wizard

//}}AFX_MSG_MAP

ON“MESSAGE(WM_BLACKBOOK, OnBlackBook)

END_MESSAGE_MAP

Kreator Class Wizard nie pomoże Ci w dodaniu do mapy komunikatów makr obsługujących komunikaty definiowane przez użytkownika (takich jak ON_MESSAGE, patrz Tabela 1.19). Dlatego też będziesz je musiał umieszczać własnoręcznie. Dobrym pomysłem jest umieszczanie tych makr poza nawiasami oznaczającymi część mapy komunikatów definiowaną przez kreatora Class Wizard (nawiasy te mają postać: / / { { oraz //}}). Dzięki temu możesz uniknąć problemów związanych z nieprawidłową obsługą definio­wanych prze/. Ciebie komunikatów przez kreatora Class Wizard.

Tabela 1.19. Wybrane, specjalne makia obsługi map komunikatów

Makro Znaczenie

ON_MESSAGE Obsługuje komunikaty zdefiniowane przez użytkownika.

ON_REGISTERED_MESSAGE Obsługuje zarejestrowane komunikaty.

ON_THREAD_MESSAGE Używane do obsługi komunikatów definiowanych przez

użytkownika w wątkach (CWinThread).

ON_REGISTERED_THREAD_MESSAGE Używane do obsługi zarejestrowanych

komunikatów w wątkach (CWinThread).

ON_COMMAND_RANGE Obsługuje określony zakres komunikatów

WM_COMMAND.

ON_CONTROL_RANGE Obsługuje określony zakres komunikatów informacyjnych

WM_COMMAND.

ON_UPDATE_COMMAND_UI_RANGE Działa podobnie jak makro

ON_UPDATE_COMMAND_UI, lecz dla określonego

zakresu komunikatów.

ON_NOT IF Y_RANGE Obsługuje określony zakres komunikatów informacyjnych

WM_NOTIF

YInnym sposobem obsługi komunikatów definiowanych przez użytkownika, jest zdefiniowa­nie swoich własnych makr, które będziesz mógł stosować w mapach komunikatów. Choć nie jest to oficjalnie dozwolone ani powiedziane, jednak możesz się domyśleć jak to zrobić analizując zawartość pliku nagłówkowego AFXMSG_.H. Pomysł polega na tym, aby stworzyć odpowiednie elementy mapy komunikatów (która jest tablicą struktur). Struktura tworząca mapę komunikatów jest typu AFX_MSGMAP_ENTRY (patrz Tablica 1.20).


Tablica 1.20. Struktura AFX_MSGMAP_ENTRY.

Składowa


Typ


Definicja


nMessage

nCode

nID


nSig

pfn


UINT

UINT

UINT


UINT

AFX_PMSG


Obsługiwany komunikat

Kod elementu kontrolnego lub komunikatu WM_NOTIFY

Kod elementu kontrolnego lub 0 dla normalnych komunikatów

Kod określający sygnaturę funkcji (patrz Tablica 1.21) Wskaźnik do funkcji obsługującej komunikaty


Jedynym polem, które zasługuje na większą uwagę jest pole nSig. Zawiera ono specjalny kod (typu wyliczeniowego AfxSig), który informuje MFC o sygnaturze funkcji obsługującej komunikaty. Jeśli sygnatura Twojej funkcji nie należy do typu wyliczeniowego, to niestety nie będziesz mógł jej bezpośrednio używać do obsługi komunikatów.


Tabela 1.21. Sygnatury funkcji, które mogą być używane w mapach komunikatów.

Kod Sygnatura

AfxSig_end Brak wartości - oznacza koniec mapy komunikatów

AfxSig_bD BOOL (CDC*)

AfxSig_bb BOOL (BOOL)

AfxSig_bWww BOOL (CWnd*, UINT, UINT)

AfxSig_hDWw HBRUSH (CDC*, CWnd*, UINT)

AfxSig_hDw HBRUSH (CDC*, UINT)

AfxSig_iwWw int (UINT, CWnd*, UINT)

AfxSig_iww int (UINT, UINT)

AfxSig_iWww int (CWind*, UINT, UINT)

AfxSig_is int (LPTSTR)

AfxSig_lwl LRESULT (UINT, LPARAM)

AfxSig_lwwM LRESULT (UINT, UINT, CMenu*)

AfxSig_vv void (void)

AfxSig_vw void (UINT)

AfxSig_vww void (UINT, UINT)

AfxSig_vvii void (int, int) (WPARAM jest ignorowany)

AfxSig_vwww void (UINT, UINT, UINT)

AfxSig_vwii void (UINT, int, int)

AfxSig_vwl void (UINT, LPARAM)

AfxSig_wbWW void (BOOL, CWnd*, CWnd*)

AfxSig_vD void (CDC*)

AfxSig_vM void (CMenu*)

AfxSig_vMwb void (CMenu*, UINT, BOOL)

AfxSig_vW void (CWnd*)

AfxSig_vWww void (CWnd*, UINT, UINT)

AfxSig_vWp void (CWnd*, CPoint)

AfxSig_vWh void (CWnd*, HANDLE)

AfxSig_vwW void (UINT, CWnd*)

AfxSig_vwWb void (UINT, CWnd*, BOOL)

AfxSig_vwwW void (UINT, UINT, Cwnd*)

AfxSig_vwwx void (UINT, UINT)

AfxSig_vs void (LPTSTR)

AfxSig_vOWNER void (int, LPTSTR) (wymusza wynik o wartości TRUE)

AfxSig_iis int (int, LPTSTR)

AfxSig_wp UINT (CPoint)

AfxSig_wv UINT (void)

AfxSig_vPOS void (WINDOWPOS*)

AfxSig_vCALC void (BOOL, NCCALCSIZE_PARAMS*)

AfxSig_vNMHDRpl void (NMHDR*, LRESULT*)

AfxSig_bNMHDRpl BOOL (NMHDR*, LRESULT*)

AfxSig_vwNMHDRpl void (UINT, NMHDR*, LRESULT*)

AfxSig_bwNMHDRpl BOOL (UINT, NMHDR*, LRESULT*)

AfxSig_bHELPINFO BOOL (HELPINFO*)

AfxSig_vwSIZIGN void (UINT, LPRECT) (wymusza zwrócenie wartości TRUE)

AfxSig_cmdui void (CCmdUI*)

AfxSig_cmduiw void (CCmdUI*, UINT)

AfxSig_vpv void (void*)


Tabela 1.21. cd. Sygnatury funkcji, które mogą być używane w mapach komunikatdw.



Kod


Sygnatura



AfxSig_bpv


BOOL (void*)


AfxSig_vwwh


void (UINT, UINT, HANDLE)


Af xSig_vwp


void (UINT, CPoint)


AfxSig_bw


BOOL (UINT)


AfxSig_bh


BOOL (HANDLE)


AfxSig_iw


int (UINT)


AfxSig_ww


UINT (UINT)


AfxSig_bv


BOOL (void)


AfxSig_hv


HANDLE (void)


AfxSig_vb


void (BOOL)


AfxSig_vbh


void (BOOL, HANDLE)


Af xSig_vbw


void (BOOL, UINT)


AfxSig_vhh


void (HANDLE, HANDLE)


AfxSig_vh


void (HANDLE)


AfxSig_viSS


void (int, STYLESTRUCT*)


AfxSig_bwl


LRESULT (WPARAM, LPARAM)


AfxSig_vwMOVING


void (UINT, LPRECT) (wymusza zwrócenie wartości TRUE)


AfxSig_vW2


void (CWnd*) (Cwnd* pobierany z parametru IParam)


AfxSig_bWCDS


BOOL (CWnd*, COPYDATASTRUCT*)


AfxSig_bwsp


BOOL (UINT, short, CPoint)


AfxSig_vws


void (UINT, LPCSTR)


Makro BEGIN_MESSAGE_MAP rozpoczyna deklarację tablicy mapy komunikatów, dlatego makro definiowane przez użytkownika musi wypełniać elementy struktury jako statyczny inicjalizator. Spójrz na poniższy fragment pliku AFXMSG_.H:

#define ON__WM_CREATE () {WM_CREATE, O, O, O, AfxSig_is, \ (AFX_PMSG)(AFX_PMSGW)(int (AFX_MSG_CALL CWnd::*)\ (LPCREATESTRUCT) )ŁOnCreate },

W powyższym przykładzie obsługiwany jest komunikat WM_CREATE, a funkcja używana do jego obsługi posiada sygnaturę AfxSig_is (chociaż wskaźnik wskazuje na strukturę typu LPCREATESTRUCT to jednak sygnatura traktuje go jako wskaźnik typu LPTSTR). Ostatni argument makra określa funkcję jaką należy wywołać - oczywiście jest to funkcja OnCreate.


Tworzenie nowych typów dokumentów

Kiedy tworzysz aplikację za pomocą kreatora App Wizard, automatycznie tworzy on dla Ciebie domyślną klasę dokumentu oraz widok. Jednak co się stanie w sytuacji, kiedy będziesz chciał stworzyć dodatkowy dokument? W takim przypadku kreator App Wizard nie jest zbyt przydatny. Poniżej przedstawione zostały czynności, które powinieneś wykonać w takiej sytuacji:

1. Użyj kreatora Class Wizard do stworzenia nowej klasy, wyprowadzonej z klasy CDocument.

2. Użyj kreatora Class Wizard do stworzenia nowej klasy, wyprowadzonej z klasy CView (lub jej klasy potomnej).

3. (Opcjonalnie) W klasie widoku, stworzonej w poprzednim kroku, przesłoń metodę GetDocument, tak aby zwracała wskaźnik na klasę dokumentu stworzoną w kroku l.

4. Do zasobów programu (a konkretnie do tablicy łańcuchów) dodaj nowy łańcuch i przypisz mu identyfikator, który chcesz nadać nowemu typowi dokumentów. Format tego łańcucha znaków przedstawiony został w Tabeli 1.13. Poniżej przedstawiony został typowy łańcuch znaków opisujący dokumenty, które nie są zainteresowane obsługą jakichkolwiek plików:

"Extra Document\n\nExtra\n\nXtra.Document\nExtra Document"

5. Do zasobów programu dodaj wszelkie dodatkowe zasoby (menu, ikonę, itp.) przypisując im ten sam identyfikator, którego użyłeś w poprzednim kroku.

6. W metodzie InitInstance obiektu aplikacji stwór/ nowy szablon dokumentu używając do tego operatora new. Załóżmy, że identyfikator łańcucha znaków definiującego dokument ma symbol ID_NEWDOC, klasa nowego dokumentu nazywa się CNewDoc, a klasa widoku nazywa się CNewView. Załóżmy do­datkowo, że chcesz użyć standardowej ramki CMDIChildWnd jako ramki okna. W takim przypadku szablon dokumentu powinien zostać stworzony w następujący sposób:

CMultiDocTemplate *dtp=new CMultiDocTemplate(ID_NEWDOC, RUNTIME^CLASS (CNewDoc) , RUNTIME^CLASS(CMDIFrameWnd), RUNTIME_CLASS(CNewView);

7. Użyj metody AddDocTemplate, aby dodać nowy dokument do listy dokumentów aplikacji.

Po wykonaniu powyższych czynności, za każdym razem gdy będziesz tworzył nowy dokument, MFC wyświetli okno dialogowe zawierające listę wszystkich dostępnych szablonów dokumentów. Odpowiedni szablon dokumentu będzie także wybierany podczas otwierania plików (wybór dokonywany będzie na podstawie rozszerzenia pliku zapisanego w łańcuchu zasobów).

Jeśli piszesz aplikację MDI, to powinieneś użyć szablonu dokumentu typu CMultiDocTemplate (tak jak w powyższym przykładzie). Jeśli natomiast piszesz program SD1, to użyj szablonu typu CSingleDocTemplate. Jeśli w szablonie dokumentu chcesz obsługiwać komunikaty generowane przez opcje menu (co jest ewentualnością rzadką, lecz praw­dopodobną), będziesz musiał stworzyć nową klasę potomną odpowiedniego szablonu i używać tej właśnie klasy.


Tworzenie prywatnych dokumentów

Czasami będziesz chciał stworzyć dokumenty, które nie będą dostępne w oknie dialo­gowym Nowy. Być może będzie Ci to potrzebne do stworzenia dokumentu testującego lub dokumentu, który chcesz stworzyć własnoręcznie po uruchomieniu aplikacji.

Możesz to osiągnąć bez żadnych problemów. W tym celu wystarczy stworzyć szablon dokumentu w standardowy sposób; pamiętaj jednak, aby nie dodawać go do listy do­kumentów aplikacji. Kiedy będziesz chciał stworzyć nową instancję dokumentu (czyli dokument, widok oraz ramkę), wystarczy wywołać metodę OpenDocumentFile sza­blonu dokumentu z argumentem NULL.


Dołączanie wielu widoków do dokumentu

Jedną z największych zalet architektury dokument/widok jest łatwa możliwość dołączenia wielu widoków do tego samego dokumentu. Dla przykładu, jeśli tworzysz program arkusza kalkulacyjnego, to powinieneś móc stworzyć dwa widoki tabelaryczne oraz wykres ko­łowy prezentujące dane tego samego arkusza. Dzięki temu użytkownik będzie mógł wyświetlać różne fragmenty danych w każdym z widoków.

Jest to niewątpliwie bardzo istotne zagadnienie, jednakże w dokumentacji ciężko jest znaleźć cokolwiek na temat jego rozwiązania. Jeśli spróbujesz zrobić wszystko samemu, to wkrótce każe się, iż jest to przerażające zadanie przekraczające Twoje siły i wy­trzymałość. Musisz bowiem stworzyć nowe okno ramki, dołączyć do niego odpowiedni widok i, w końcu, poinformować dokument o tym, że dostępny jest nowy widok. Na szczęście szablony dokumentów wiedzą jak to wszystko należy zrobić; dlaczego więc nie poprosić ich o wykonanie całej tej brudnej roboty za nas?

Załóżmy, że dysponujesz programem MFC przedstawiającym planszę do gry w warcaby. Program ten tworzy obiekty klas CCheckerDoc oraz CCheckerView. Teraz jednak chciałbyś dodać do tego samego dokumentu nowy widok klasy CDebugView. Oto co powinieneś zrobić:

1. Stwórz lub pobierz obiekt szablonu dokumentu (na przykład, obiekt ten możesz zapamiętać w odpowiedniej zmiennej podczas wykonywania metody Initlnstance).

2. Wywołaj metodę CreateNewFrame obiektu szablonu dokumentu. Jako argu­mentu tej metody użyj wskaźnika do dokumentu, którego chcesz użyć (drugi argument powinien być wartością NULL). Metoda ta zwraca wskaźnik do nowego okna ramki, zawierającego widok skojarzony z Twoim istniejącym dokumentem.

3. Następnie wywołaj metodę InitialUpdateFrame obiektu ramki. Powoduje to automatyczne wykonanie metody OnInitialUpdate. Jeśli pominiesz ten krok, to Twój widok będzie działał tak jak gdyby nie zależał od metody OnInitialUpdate. Niektóre widoki (takie jak CScrollView) wymagają wywołania metody OnlnitialUpdate i używającej do swoich własnych potrzeb.

Przykład zastosowania opisanej powyżej techniki możesz zobaczyć na Wydruku 1.2 oraz Rysunku l .2. Przedstawiony kod obsługuje wybór opcji menu w klasie dokumentu; rozwiązanie takie zostało zastosowane dlatego, iż łatwo było uzyskać dostęp do wskaźnika na obiekt dokumentu (jest to po prostu wskaźnik this). Inne widoki i klasy możesz, znaleźć na CD-ROMie dołączonym do książki.


Wydruk 1 .2. Tworzenie okna Debug.

void CheckerDoc : :OnDebugWin{ )

{

CMultiDocTemplate temptemplate ( IDR_DEBUGTYPE,

RUNTIME“CLASS ( CCheckerDoc ) ,

RUNTIME_CLASS (CMDIChildWnd) ,

RUNTIME“CLASS (DebugView) ) ;

CFrameWnd *frame = temptemplate. CreateNewFrame (this, NULL) ;

if ( ! f ramę)

AfxMessageBox ( "Nie można stworzyć okna ! " ) ;

else

frame->InitialaUpdateFrame ( this , TRUE) ;

}

Tworzenie oddzielnego menu Plik>Nowy

Domyślnie opcja menu Plik>Nowy posiada identyfikator ID_FILE_NEW. Domyślna procedura obsługi tego polecenia tworzy okna dialogowe wyświetlające wszystkie sza­blony dokumentów dodane do listy szablonów aplikacji (oczywiście, o ile na liście tej jest więcej niż jeden szablon). Chociaż zachowanie takie jest standardowe dla MFC, to jednak nie jest ono normalne dla większości aplikacji działających w systemie Win­dows. Zazwyczaj, aplikacje stworzone bez zastosowania MFC będą posiadały oddzielne opcje menu pozwalające na tworzenie dokumentów różnych typów. Przy takim rozwiązaniu dostępne typy dokumentów są zazwyczaj umieszczone w odrębnym podmenu (patrz Rysunek 1.3).

Nic jest to zadanie trudne, o ile dysponujesz dostępem do szablonów każdego z typów dokumentów. W takim wypadku wystarczy zdefiniować procedurę obsługi opcji menu, odpowiadających poszczególnym typom dokumentów. Każda z tych procedur obsługi będzie używała odpowiedniego szablonu. Aby stworzyć odpowiedni dokument, wystarczy wywołać metodę OpenDocumentFile przekazując jako argument wartość NULL. Poniżej pokazany został przykładowy fragment kodu:

void CCheckDoc::OnNewText()

{

// Zakładamy, że składowa m_pTextTemplate zawiera wskaźnik do wzorca dokumentu

m_pTextTemplate->OpenDocumentFile(NULL);

}


Zapobieganie tworzeniu nowego dokumentu podczas uruchamiania programu

W starszych wersjach MFC bez zbytnich problemów można było zrozumieć sposób analizy linii poleceń użytej do wywołania programu. W metodzie Initlnstance kreator App Wizard umieszczał kod, który analizował postać linii wywołania programu. Jeśli linia ta nie była pusta, program traktował ją jako nazwę pliku. Jeśli linia była pusta, program tworzył nowy dokument. Jednakże w nowszych wersjach MFC pojawiła się konieczność umieszczania w linii poleceń argumentów wymaganych przez programy korzystające z technologii OLE oraz przez inne nowsze narzędzia. Konieczność obsługi tych nowych argumentów zmusiła firmę Microsoft do bardzo dużego skomplikowania sposobu obsługi linii poleceń.

Nowoczesne programy pisane przy wykorzystaniu biblioteki MFC stosują obiekt klasy CCommandLinelnfo i na jego podstawie określają czynności, jakie będą wykonywane podczas uruchamiania programu. Metoda ParseCommandLine analizuje linię poleceń użytą do uruchomienia programu i na jej podstawie odpowiednio wypełnia składowe obiektu CCommandLinelnfo. Następnie wywoływana jest metoda ProcessShell-Command, która wykonuje czynności określone w obiekcie CCommandLinelnfo.

Znając zasadę działania całego tego mechanizmu, w łatwy sposób można sprawdzić zawartość obiektu CCommandLinelnfo po wywołaniu metody ParseCommandLine i podjąć odpowiednie czynności w zależności od zapisanych w nim informacji. Dla przykładu, jeśli nie chcesz tworzyć nowego dokumentu podczas uruchamiania programu, będziesz mógł zastosować rozwiązanie przedstawione na poniższym przykładzie:

CCommandLinelnfo cmdlnfo;

ParseCommandLine(cmdlnfo);

//Przetworzenie poleceń umieszczonych w linii wywołania programu

if (cmdlnfo.m_nShellCommand != FileNew)

if (!ProcessShellCommand(cmdlnfo))

return FALSE;


Analiza parametrów linii poleceń

Jeśli przeczytałeś poprzednią sekcję, to ucieszy Cię wiadomość, że opisane powyżej metody mogą zostać użyte do obsługi Twoich własnych parametrów wywołania pro­gramu. Aby to zrobić, w pierwszej kolejności będziesz musiał stworzyć nową klasę potomną klasy CCommandLinelnfo. W tej klasie będziesz musiał przesłonić metodę ParseParam. Klasa bazowa będzie wywoływała tę wirtualną metodę dla każdego pa­rametru umieszczonego w linii poleceń. Jeśli parametr będzie się zaczynał od znaku “-" lub ukośnika, drugi argument wywołania metody ParseParam będzie miał wartość TRUE; jednocześnie MFC usunie znak z początku parametru. Pierwszy argument me­tody ParseParam zawiera analizowany parametr linii poleceń. Trzeci argument tej metody przybiera wartość TRUE jeśli analizowany jest ostatni parametr linii.

Na Wydruku l.3 przedstawiona została klasa potomna klasy CCommandLinelnfo. Ta prosta klasa, na podstawie argumentów wywołania programu, ustawia kilka wartości w obiekcie aplikacji. Obiekt aplikacji przedstawiony został na Wydruku 1.4. Na wydruku l .5 przedstawiony został obiekt widoku, w którym wykorzystywane są informacje uzy­skane z obiektu aplikacji.


Wydruk 1 .3. Analiza parametrów wywołania.

#include "stdafx.h"

#include "string.h"

#include "params.h"

#include "customcmd.h"

void CCustomCmd: :ParseParam( LPCSTR IpszParam, BOOL bFlag, BOOL bLast)

{

CParamApp *app= (CParamsApp * ) Af xGetApp ( ) ;

if (bFlag && ! strcmp { IpszParam, "Pl" ))

app~>m_Pl=TRUE;

else

if (bFlad && ! strcmp ( IpszParam, "P2 "))

app->m_P2=TRUE;

// inne parametry - zastosuj ustawienia domyślne

else CCommandLinelnfo: : ParseParam ( IpszParam, bFlag, bLast) ;

}

Wydruk 1 .4. Użycie obiektu CCustomCmd.

//Parse command linę for standard shell commands, DDE, file open

CCustomCmd cmdlnfo;

ParseCommandLine (cmdlnfo) ;

//Dispatch commands specified on the command linę

if ( ! ProcessShellCommand (cmdlnfo) )

return FALSE;

Wydruk 1.5. Odczytywanie opcji.

void CParamsView::OnInitialUpdate()

{

CParamsApp *app=(CParamsApp *)AfxGetApp();

//odczytaj wartości z obiektu aplikacji przed wyświetleniem formularza

m_Pl=app->m_Pl;

m_P2=app->m_P2;

CFormView::OnInitialUpdate();

}


Określanie wielkości widoków

Czasami może się zdarzyć, że będziesz chciał stworzyć widok o ściśle określonych wymiarach. Załóżmy, że chciałbyś stworzyć szachownicę o polach wielkości 50 pikseli (cała plansza miałaby wymiary 400x400 pikseli). Problem polega jednak na tym, że nie jesteś w stanie bezpośrednio zmodyfikować wymiarów widoku. Zamiast tego będziesz musiał zmienić wielkość ramki, w której widok ten jest umieszczony. Modyfikując wielkość ramki (na przykład, za pomocą metod MoveWindow lub SetWindowPos) zmieniasz wymiary całego okna, a nie tylko wielkość obszaru roboczego. Dlatego, aby uzyskać widok o wymiarach 400x400 będziesz musiał nadać ramce większe wymiary.

Jednak o ile większa musi być ramka? To zależy od kilku czynników. Po pierwsze, wielkość elementów okna leżących poza obszarem roboczym zależy od używanego sprzętu, konfiguracji dokonanej przez użytkownika, oraz wielu innych czynników. Co więcej, także sam widok zawiera niewielki obszar “nie-roboczy". Aby nadać widokowi wymiary 400x400 pikseli, będziesz musiał obliczyć całkowitą wielkość widoku, uwzględniając w tym wszystkie jego elementy. Następnie będziesz musiał obliczyć jakie wymiary powinna mieć ramka, aby można w niej było umieścić widok o określonych wymiarach.

System Windows potrafi obliczyć całkowitą wielkość okna, na podstawie zadanej wielkości jego obszaru roboczego. Służy do tego funkcja ::AdjustWindowRectEx. W wywołaniu tej funkcji możesz określić styl okna oraz podać czy posiada ono menu, czy też nie. Oczywiście, powinieneś także podać pożądaną wielkość obszaru roboczego okna.

Ani widoki, ani okna widoków w programach MDI nie posiadają pasków menu. Informacje o stylu okna możesz uzyskać za pomocą funkcji GetStyle lub GetStyIeEx. W sytuacjach kiedy będziesz chciał określić precyzyjnie wielkość okna przez jego stworzeniem, bę­dziesz mógł określić styl okna za pomocą takich narzędzi jak program Spy++ i na stałe zakodować odpowiednie ustawienia stylu. Warto pamiętać o tym, iż wielkości mogą się zmieniać na różnych komputerach -jednakże style powinny być zawsze takie same.

Poniżej przestawiłem przykładowy kod, który pokazuje w jaki sposób możesz nadać widokowi (v) wymiary 400x400 pikseli:

CRect vsize(0,O,400,400);

CWnd *frame=v->GetParentFrame();

::AdjustWindowRectEx(&vsize,v->GetStyle(),FALSE,v->GetExStyleO);

//vsize może nie zaczynać się od punktu 0,0

::AdjustWindowRectEx(&vsize, frame->GetStyle (} , FALSE, frame->GetStyleEx());

frame->SetWindowPos(NULL,0,0,

vsize.Width(),vsize.Height(), SWP_NOACTIVATE|SWP_NOMOVE|SWP_NOZORDER);


Używanie słowa kluczowego typedef ze wzorcami

Kolekcje dostarczane przez MFC są bardzo przydatnymi narzędziami. Jednakże opierają one swoje działanie na szablonach, a składnia konieczna do ich zastosowania jest nie­jednokrotnie bardzo nieprzyjemna. Dla przykładu załóżmy, że chciałbyś stworzyć listę obiektów klasy CWnd. W takim przypadku musiałbyś użyć następującego kodu

CList<CWnd,CWnd &> winlist;

Lepiej nie myśleć o rzutowaniu wskaźnika typu void na wskaźnik wskazujący na taką listę. Ale spróbujmy, wyglądałoby to w następujący sposób:

Clist<CWnd, CWnd &> *lp = (Clist<CWnd, CWnd &> *)vp

;Zamiast się męczyć, lepiej użyć słowa kluczowego typedef, dzięki czemu cały kod sta­nie się znacznie bardziej czytelny i zrozumiały. Zresztą, spójrz na poniższy przykład:

typedef CList<CWnd, CWnd &> CWndList;

CWndList winList;

CWndList *lp= (CWndList *)vp;

Chyba zgodzisz się, że jest to znacznie lepsze rozwiązanie?


Dwuwymiarowe obiekty CArray

Czy zdarzyło Ci się, że stworzyłeś tablicę z wykorzystaniem klasy CArray i zdałeś sobie w pewnej chwili sprawę z tego, że potrzebna Ci jest tablica kilkuwymiarowa? Na szczęście istnieje kilka sposobów stworzenia takich obiektów (jednym z nich jest zastosowanie normalnych tablic dostępnych w języku C). Jednak sposobem, który zawsze wywierał na mnie największe wrażenie, jest stworzenie obiektu klasy CArray zawierającego elementy klasy CArray (co umożliwia stworzenie tablicy dwuwymiarowej; odpowiednie rozszerzenie tej koncepcji pozwala na stworzenie tablic o większej ilości wymiarów).

Przedstawiony poniżej przykład pokazuje praktyczne zastosowanie tej metody. W pierwszej kolejności definiowany jest typ tablicy zawierającej liczby typu int (CIntArray), a następnie typ tablicy zawierającej obiekty klasy CIntArray. Tablica tablic nosi nazwę CIntArray2. Ostatnim krokiem jest stworzenie nowej klasy (CIntMatrix), będącej klasą potomną klasy CIntArrayZ. W klasie tej zdefiniowany zostaje konstruktor, który inicjalizuje wszystkie tablice nadając im odpowiednie wymiary:

#include <afxtempl.h>

typedef CArray<int,int> CIntArray;

typedef CArray<CIntArray,CIntArray> CIntArray2;

class CIntMatrix : public CIntArray2

public:

CIntMatrix(unsigned x, unsigned y)

{

SetSize(y);

for (unsigned i = 0 ; i<y; i-t-+)

ElementAt(i).SetSize(x);

}





Rozdział 2 Serializacja

Archiwizacja jest jednym z elementów MFC, którego działanie w przeważającej większości wypadków, przebiega tak, jakbyśmy sobie tego życzyli. A co z tymi nielicznymi wypadkami, kiedy standardowe działanie mechanizmów archiwizacji nie odpowiada naszym celom? Zajrzyj do wnętrza klasy CArchive i naucz się dostosowywać ją do swoich własnych potrzeb.

Moja żona Pad i ja uwielbiamy oglądać filmy. Nie mówcie jej, lecz tak naprawdę lubię tylko jeden gatunek filmów. Podobnie jak wielu innych programistów lubię filmy fantastyczno-naukowe. Nie jestem jednak szczególnym fanem wszystkich tych nowoczesnych i ekstrawaganckich filmów, najeżonych efektami specjalnymi i wyprodukowanymi dzięki wysoce zaawansowanej technologii. Ja wolę te starsze filmy - Plan Ninę form Outer Space, Queen ofOuter Space oraz (oczywiście!) Destination Saturn (Buck Rogers). Flash Gordon także mi się podoba, jednak zawsze wolałem Bucka Rogersa.

Kiedy dzisiaj idę do teatru czuję się... tak, jakbym wypełniał formularz w aplikacji bankowej. W czasach gdy byłem dzieckiem, mogłem iść do kina za ćwierć dolara (lub nawet mniej). Jednak najlepsze było to, że dawne filmy były serialami. Mogłeś iść do kina i oglądać jak Buck próbuje uwolnić Wilme ukrytą gdzieś przez złego Kane'a. Kane po­wiedziałby coś w stylu: “Kiedy nacisnę ten guzik, Buck Rogers zginie!". I film by się skończył. W następną niedzielę po południu znowu musiałbyś iść do kina (i wydać następne ćwierć dolara). Znajomi mówili mi, że westerny (które z resztą nigdy mnie szczególnie nie interesowały) często kończyły się widokiem bohatera zwisającego na jednej ręce za krawędzią ogromnego kanionu, stąd też tytuł “Cliffhanger". Niektóre seriale były lepsze od innych; ja zawsze lubiłem te produkowane przez Republic Studios, bo musiały mieć przynajmniej milion odcinków.

Dzięki VCR możemy wciąż jeszcze cieszyć się tymi starymi serialami (czasami możesz je także złapać w programie American Movie Classic). Jednakże jest jedna poważna wada: kiedy tak oglądasz ten sam film po raz kolejny, zauważasz nagle, że na końcu od­cinka 21 palec Kane'a naciska guzik, a na początku odcinak 22 ten sam palec zatrzymuje się na ułamek milimetra nad przyciskiem a ktoś wrzeszczy “Poczekaj!".

Aby uchronić się przed takimi problemami, niektóre firmy tworzyły seriale jako jeden długi, pełnometrażowy film. Chyba dobrym określeniem na taką metodę, byłoby słowo “deserializacja".

W MFC używane jest słowo “serializacja" określające sposób zapamiętania na stałe stanu obiektów. W większości wypadków polega ono na zapisaniu danych do strumienia. Każdy fragment danych jest umieszczany w strumieniu przed kolejnym fragmentem danych. Sytuacja jak w kinie - musisz oglądnąć pierwszy element zanim będziesz mógł ogląd­nąć drugi.

A czym są strumienie? To zależy. W przeważającej większości wypadków są to pliki. W rzeczywistości, narzędzia dostarczane przez MFC zawsze zakładają, że są to pliki, więc użytkownicy MFC automatycznie także tak robią. Jednak strumieniem może być każdy rodzaj pamięci, w którym możesz umieszczać dane w celu ich późniejszego od­tworzenia.


Stałość a pamięć

Jak zwykle, jedno słowo jest używane w MFC do opisania wielu pojęć. W jednym ro­zumieniu, serializacja oznacza proces, w którym obiekt zapisuje swój stan w strumieniu tak, aby można było później ten obiekt odtworzyć. Obiekt może być odtworzony nieco później podczas działania tego samego programu. Możesz odtworzyć go w zupełnie in­nym programie, w innej kopii tego samego programu lub nawet na zupełnie innym komputerze. Możesz odtworzyć go także i na drugim końcu połączenia sieciowego.

Jednak, najczęstszym przypadkiem użycia serializacji jest automatyczne zapisywanie swojego stanu przez obiekty klasy CDocument. Czy to nie jest to samo? Niezupełnie. Załóżmy, że piszesz program edytora tekstu używając do tego MFC. Co w takim przypadku chcesz zapisywać? Na pewno nie obiekt klasy CDocument. Kto chciałby otworzyć edytor i zobaczyć w nim wewnętrzne dane obiektu? Chyba nikt. Dlatego też klasa CEditView udostępnia metodę SerializeRaw służącą do zapisywania tekstu do pliku. Jak widać, w tym przypadku serializacja obiektu klasy CDocument nie oznacza za­pewnienia stałości tego obiektu. Oznacza ona zapamiętanie stanu programu, a to nie jest to samo. Innym przykładem mógłby być program starający się zapamiętać w pliku układ ekranu. Ta informacja nie jest przechowywana w obiekcie klasy CDocument, jednakże to właśnie ten obiekt będzie musiał ją zapisać.

Zapewnienie stałości obiektu nie jest wcale tak proste, jak mogłoby się wydawać. Pomyśl, jak należałoby zapisywać w pliku obiekt tablicy. Jeśli tablica ta zawierała liczby całkowite, nie jest to zadanie skomplikowane. Wystarczy zapisywać wartości kolejnych komórek tablicy wraz z dowolnymi informacjami koniecznymi do jej późniejszego odtworzenia (na przykład ilością komórek tablicy). Jednakże co się stanie, gdy tablica będzie zawierać wskaźniki na łańcuchy znaków? W tym przypadku miałbyś znacznie więcej pracy. Jednak co zrobić, gdyby kilka elementów tablicy wskazywało na ten sam łańcuch znaków? Jeśli byś zapisał w pliku kilka razy ten sam łańcuch znaków, po odtworzeniu otrzymałbyś tablicę różną od tablicy oryginalnej. W ostateczności musiałbyś stworzyć tablicę łańcuchów znaków oraz tablicę indeksów odpowiadającą tablicy oryginalnej.

Niestety, sytuacja może być jeszcze bardziej nieprzyjemna. Co zrobić, gdy tablica będzie zawierała wskaźniki na obiekty. Lub jeszcze gorzej: co zrobić, gdy w tablicy umieszczone są wskaźniki na wirtualną klasę bazową, wskazujące na obiekty kilku różnych klas po­tomnych? Obiekty te mogą zawierać wskaźnik na łańcuchy znaków, wskaźnik na inne obiekty różnych typów lub cokolwiek innego.

MFC udostępnia mechanizm pozwalający na bardzo sprawne rozwiązanie wszystkich tych problemów. Dowolna klasa, będąca klasą pochodną klasy CObject, może serializować swój stan, jeśli tylko twórca klasy umieścił w niej makra DECLARE_SERIAL oraz IMPLEMENT_SERIAL. Każda klasa jest także odpowiedzialna za zapisanie i odczy­tanie ze strumienia (klasy CArchive) wszystkich danych, których może potrzebować. Klasy bazowe muszą same zadbać o siebie. Jeśli Twój obiekt zawiera inny obiekt, jedyne, co musisz zrobić, to poprosić go o serializację (jako jeden z elementów serializacji Twojego obiektu).


Serializacja klas

Klasa nie musi zawierać makr DECLARE_SERIAL oraz IMPLEMENT_ SERIAL, o ile nie chcesz serializować obiektów tej klasy. Nie będziesz potrzebował wszystkich możliwości udostępnianych przez te makra, tylko po to, aby zapamiętać stan programu. Dla przykładu, aplikacje wygenerowane przez kreatora App Wizard używają makra IMPLEMENT_DYNCREATE (zobacz rozdział 1) przy implementacji obiektów dokumen­tów, a pomimo tego możesz przesłaniać metodę Serialize. Makro IMPLEMENT_DYNCREATE dodaje do klasy operator », dodaje in­formacje o klasie do listy połączonej, jak również zapamiętuje numer wersji (schemat) obiektów. Elementy te umożliwiają serializację całych obiektów (co będziesz mógł zobaczyć w dalszej części tego rozdziału).

Czy musisz używać serializacji? Nie. Jak to się zwykle dzieje, narzędzia nie rozumieją tego mechanizmu, a więc jeśli nie chcesz używać serializacji, nie zostanie ona użyta. Ale dlaczego miałbyś jej nie używać? Może piszesz program książki adresowej i chcesz zapisywać informacje wprowadzane przez użytkownika bezpośrednio po ich podaniu? Może dane używane w Twoim programie pochodzą bezpośrednio z urządzenia podłą­czonego do komputera przez łącze szeregowe? A może musisz zapisywać dane w pliku o ściśle określonym formacie? W tych wszystkich przypadkach, stosowanie serializacji może nie być dla Ciebie optymalnym rozwiązaniem.


Szybka prezentacja klasy CArchive

Nie będziesz w stanie zrozumieć zasad serializacji bez dogłębnej znajomości działania klasy CArchive. Klasa ta powoduje wiele niejasności i zamieszania. A jest ona jedynie reprezentacją miejsca służącego do zapisywania i odczytywania danych. Miejscem tym jest zazwyczaj plik, choć może to być cokolwiek innego. Składowe klasy CArchive przedstawione zostały w Tabeli 2.1. W znacznej większości wypadków, do zapisywania i odczytywania danych z archiwum będziesz używał operatorów « i ». Jest to dobrze znany sposób zapisu, stosowany w strumieniach C++. Dla przykładu, aby zapisać w archi­wum wartość zmiennej dwvar typu DWORD wystarczy użyć następującego kodu:

ar«dwvar;

Jeśli musisz wiedzieć, czy dane mają być odczytywane, czy też zapisywane, możesz zapytać o to obiekt archiwum. Metody IsStoring oraz IsLoading umożliwiają określenie operacji, jaką obiekt chce przeprowadzić na danych. Próba zapisania danych do archiwum pod­czas odczytywania (i na odwrót) spowoduje wygenerowanie wyjątku. Poniższy kod po­kazuje jak można określić czy dane mają być zapisane, czy odczytane:

if (ar.IsStoring())

ar«dwvar ; else

ar»dwvar;

Tabela 2.1. Składowe klasy CArchive.


Składowa Opis

m_pDocument Dokument używający tego archiwum.

Abort Zamyka archiwum bez zgłaszania wyjątku.

Close Zamyka archiwum.

Flush Zapisuje dane pozostające w buforze do strumienia.

operator << Zapisuje do archiwum obiekt lub daną jednego z podstawowych typów danych.

opertaor >> Odczytuje z archiwum obiekt lub daną jednego z podstawowych typów danych.

Read Odczytuje bajty.

Write Zapisuje bajty.

ReadString Odczytuje łańcuch znaków.

WriteString Zapisuje łańcuch znaków.

GetFile Pobiera skojarzony z archiwum obiekt klasy CFile lub klasy potomnej.

GetObjectSchema Odczytuje numer wersji obiektu (jeśli jest on znany).

SetObjectSchema Określa numer wersji obiektu.

IsLoading Zwraca wartość TRUE jeśli archiwum jest przeznaczone do odczytu.

IsStoring Zwraca wartość TRUE jeśli archiwum jest przeznaczone do zapisu.

IsBufferEmpty Wykrywa, czy bufor jest pusty (dla archiwów skojarzonych z gniazdami).

ReadObject Odczytuje i tworzy serializowany obiekt.

WriteObject Serializuje obiekt.

MapObject Umieszcza obiekt w mapie archiwum, bez jego rzeczywistej serializacji.

SetLoadParams Określa, o ile wzrośnie wielkość tablicy mieszającej archiwum podczas zapisywania kolejnych obiektów.

SetStoreParams Określa początkową wielkość tablicy mieszającej, używanej do śledzenia obiektów zapisywanych do archiwum.

ReadClass Odczytuje informacje o klasie.

WriteClass Zapisuje informacje o klasie.

SerializeClass Zapisuje lub odczytuje informacje o klasie z zależności do tego, czy archiwum jest przeznaczone do odczytu, czy do zapisu

Jeśli będziesz chciał zapisywać całe obiekty, łańcuchy znaków lub określone ilości bajtów, będziesz mógł posłużyć się metodami: WriteObject, WriteString oraz Write. Oczywiście istnieją także odpowiednie metody służące do odczytywania obiektów, łań­cuchów znaków i bajtów; są to: ReadObject, ReadString oraz Read.

Na razie te informacje o klasie CArchive w zupełności Ci wystarczą. Kłopoty zaczynają się bowiem dopiero w momencie, gdy chcesz, aby klasa CArchive została użyta do re­prezentacji czegoś naprawdę dziwnego, jak na przykład plik zaszyfrowany, baza danych albo gniazdo sieciowe. Do tych tematów wrócimy w dalszej części rozdziału.


Tajniki poleceń służących do otwierania i zapisywania plików

Gdy tworzysz programy za pomocą kreatora App Wizard, to w jakiś cudowny sposób doskonale one wiedzą, jak należy odczytywać i zapisywać pliki. Twoim jedynym zadaniem jest stworzenie schematycznej metody Serialize w klasie dokumentu. Rozwiązanie ta­kie jest bardzo wygodne, o ile chcesz zapisywać dane do pliku o określonym formacie. Jednak gdy chcesz zrobić coś innego, to jak zwykle będziesz musiał polegać wyłącznie na sobie. Zanim zaczniesz zastanawiać się nad rozwiązaniami alternatywnymi, zapoznaj się ze sposobem obsługi polecenia Plik^Otwórz (tabela 2.2).


Tabela 2.2. Obsługa polecenia Plik -> Otwórz

MFC wywołuje Przesłoń gdy

CWinApp::OnFileOpen Chcesz wszystko zrobić samemu

CWinApp::OpenDocumentFile Chcesz aby MFC pobrało jedynie nazwę pliku

CDocTemplate::OpenDocumentFile Chcesz aby MFC pobrało plik i wybrało odpowiedni

wzorzec dokumentu

CDocTemplate::OnOpenDocument Chcesz aby MFC pobrało plik, wybrało odpowiedni wzorzec dokumentu i stworzyło obiekty dokumentu, widoku i ramki

CDocument::Serialize Chcesz aby MFC zrobiło za Ciebie całą robotę

włącznie z otworzeniem pliku i skojarzeniem go z obiektem archiwum

Opcji menu Plik ^Otwórz odpowiada zazwyczaj identyfikator ID_FILE_OPEN. Jeśli nie zdefiniujesz własnej procedury obsługi tego polecenia, będzie ono domyślnie obsługiwane przez procedurę obsługi zdefiniowaną w klasie aplikacji. Jeśli będziesz chciał samemu obsługiwać to polecenie, wystarczy zdefiniować odpowiednią metodę w do-wolnej z klas i dodać ją do mapy komunikatów. Oczywiście, równie dobrze możesz użyć innego identyfikatora polecenia. Rozwiązanie takie daje Ci pełną kontrolę nad procesem otwierania pliku. Czasami może się jednak okazać, że tak absolutna kontrola to troszkę za dużo jak na Twoje potrzeby. Pomyśl, co MFC musi zrobić w celu otworzenia pliku. Poniżej podane zostały główne czynności:

1. Poprosić o podanie nazwy pliku.

2. Wybrać szablon dokumentu odpowiadający podanej nazwie.

3. Stworzyć obiekty dokumentu, ramki i widoku.

4. Otworzyć plik.

5. Skojarzyć plik z archiwum.

6. Wywołać metodę Serialize nowego obiektu dokumentu.

Szczególnie nieprzyjemne mogą okazać się etapy l i 2. Czy nie można wykorzystać MFC do wykonania przynajmniej kilku powyższych czynności?

Domyślna procedura obsługi skojarzona z identyfikatorem BD_FILE_OPEN prosi użytkownika o podanie nazwy pliku wywołując metodę CWinApp::DoPromptFiIeName. Czy nie mógłbyś przesłonić tej metody i stworzyć swoje własne okno dialogowe? Nie jest to, niestety, takie proste. Problem polega na tym, iż metoda DoPromptFileName nie jest metodą wirtualną. Nawet jeśli przesłoniłbyś oryginalną metodę, to MFC i tak jej nie wywoła. Jedynym rozwiązaniem jest więc zmodyfikowanie metod wywołujących metodę DoPromptFileName (czyli wirtualnych metod CWinApp::OnFileOpen oraz CWinApp::DoSave).

Kiedy już określona zostanie nazwa otwieranego pliku, MFC wywoła metodę CWinApp::OpenDocumentFile. Metoda ta przeszukuje listę szablonów dokumentów dostępną w aplikacji i wybiera jeden z nich (zazwyczaj na podstawie rozszerzenia pliku). Metodę tę należy przesłonić, jeśli wybór szablonu dokumentu musi się w Twojej aplikacji odbywać na bardziej wyrafinowanych zasadach.

Po wybraniu szablonu dokumentu, MFC wywołuje metodę OpenDocumentFile (dostępną w obiekcie szablonu). Nieczęsto będziesz chciał przesłaniać standardową definicję tej metody, gdyż jest ona odpowiedzialna za stworzenie obiektów dokumentu, widoku oraz ramki i ich wzajemne skojarzenie ze sobą. Gdy wszystko będzie już gotowe, szablon dokumentu wywoła metodę CDocument::OnOpenDocument.

Metoda CDocument::OnOpenDocument jest idealnym miejscem do umieszczenia swojego własnego kodu. Kiedy jest ona wywoływana wszystkie, najważniejsze obiekty (dokument, widok i ramka) są już utworzone, a dodatkowo znana jest nazwa otwieranego pliku. Jeśli dysponujesz kodem służącym do otwierania pliku określonego typu (określanego na podstawie rozszerzenia), powinieneś go umieścić właśnie w tej metodzie. Domyślna definicja tej metody otwiera plik jako obiekt klasy CFile, kojarzy go z archiwum CArchive i wywołuje metodę Serialize obiektu dokumentu. Również i tym razem (jak zwykle) MFC pozwala Ci na zaakceptowanie standardowego postępowania lub przesłonięcie wybranej metody i wykonanie odpowiednich czynności samemu. Kontrolę nad procesem otwierania pliku możesz przejąć w dowolnie wybranym momencie. Jeśli chcesz, będziesz mógł oddać sterownie otwieraniem pliku w ręce MFC.

Załóżmy, że modyfikujesz stary program, a w jego nowej wersji chcesz skorzystać z MFC. Istniejący format pliku jest tajemnicą firmową i nie śmiesz go tknąć. Masz do rozwiązania następujące trzy problemy:

Rozszerzenie pliku nie ma znaczenia - typ pliku określany jest za pomocą jego trzech pierwszych bajtów.

Ze względu na to, że rozszerzenie pliku nie ma znaczenia, będziesz musiał operować na wszystkich plikach (filtr plików powinien mieć postać “* . *").

Dysponujesz kodem służącym do odczytywania pliku, a używającym standardowych wskaźników typu FILE w języku C.

Tak naprawdę, nie masz żadnego problemu. Swój problem możesz rozwiązać na dwa sposoby. Pierwszy z nich polega na przesłonięciu metody OnFileOpen, poproszeniu użytkownika o podanie nazwy pliku, określeniu odpowiedniego szablonu dokumentu (na podstawie trzech pierwszych bajtów pliku), a następnie na wywołaniu metody OpenDocumentFile obiektu szablonu dokumentu. Wszystkie pozostałe czynności zostałyby wykonane przez MFC. Drugie rozwiązanie polegałoby na przesłonięciu metody DoPromptFileName (oraz pozostałych, skojarzonych z nią metod) i metody CWinApp::OpenDocument. Obie te metody są w pełni satysfakcjonujące. Przesłonięcie metody OnFileOpen nie ma żadnego wpływu na działanie polecenia Plik ^Zapisz jako (jak sam się za chwilę przekonasz).

A co z trzecim problemem? W rzeczywistości jego rozwiązanie jest jeszcze prostsze. Wystarczy bowiem przesłonić metodę CDocument::OnOpenDocument. Wewnątrz niej będziesz musiał otworzyć plik za pomocą funkcji fopen (może pamiętasz, że jest to standardowa funkcja biblioteczna języka C); dysponując wskaźnikiem pliku zwróconym przez tę funkcję będziesz mógł skorzystać z kodu obsługującego otwieranie dokumentu.

Procedura obsługi polecenia Plik&Zapisz jest znacznie prostsza (patrz tabela 2.3). Można się było tego spodziewać, gdyż w tym wypadku nie ma konieczności wybierania wzorca dokumentu i tworzenia jakichkolwiek obiektów. Podczas zapisywania pliku, wszystkie obiekty zostały już dawno stworzone. Jeśli będziesz próbował zapisać dokument, któremu nie została jeszcze nadana nazwa, zostanie wykonana procedura zapisywania pliku pod nową nazwą (polecenie Zapisz jako). Jednakże wywołanie metody OnFileSave nigdy nie spowoduje wywołania metody OnFileSaveAs. Jeśli będziesz chciał w specjalny sposób obsługiwać oba te polecenia, będziesz musiał przesłonić definicje obu tych metod (lub zmienić identyfikatory poleceń w menu).

Ze względu na to, iż polecenia te odnoszą się do aktywnego dokumentu, procedury ich obsługi umieszczane są w obiekcie dokumentu. Oznacza to, że procedury obsługi tych poleceń wiedzą jaki dokument jest aktywny. Jeśli nazwa pliku nie jest znana, MFC wywołuje metodę DoPromptFileName. Jest to ta sama metoda, która używana jest w procedurze

Tabela 2.3. Proces obsługi poleceń Plik>Zapisz oraz Plik>Zapisz jako.

MFC wywołuje Przesłoń gdy ...

CDocument::OnFileSave Chcesz wszystko zrobić sam

CDocument::OnFileSaveAs Chcesz aby MFC pobrało nazwę pliku

CDocument::OnSaveDocument

CDocument::Serialize Chcesz aby MFC wykonało za ciebie całą pracę, włącznie z otworzeniem pliku i skojarzeniem go z archiwum (CArchive

)obsługi polecenia Plik=>Otwórz, jednak tym razem służy ona do określenia nazwy zapi­sywanego pliku. Kiedy nazwa pliku zostanie określona, procedura obsługi polecenia wywoła metodę OnSaveDocument. W podanym powyżej przykładzie tworzenia nowej wersji programu, musiałbyś przesłonić tę metodę, aby otworzyć w niej plik (za pomocą funkcji fopen) i przekazać wskaźnik kodu obsługującego zapisywanie danych.

Domyślne działanie metody OnSaveDocument polega na stworzeniu obiektu klasy CFile, skojarzeniu go z archiwum CArchive) oraz wywołaniu metody Serialize. Lecz przecież metoda ta wywoływana jest także przez procedurę obsługi polecenia Plik=>Otwórz. Na szczęście, w prosty sposób możesz określić, czy przeprowadzana jest operacja zapisywania danych czy też ich odczytywania. Służą do tego metody IsStoring oraz IsLoading.


Dlaczego metoda Serialize wykorzystywana jest zarówno do odczytu, jak i do zapisu danych?

Bardzo często pojawia się pytanie dlaczego metoda Serialize służy za­równo do zapisywania danych w archiwum, jak i do ich odczytywania. Dlaczego zamiast tego nie można stworzyć dwóch odrębnych metod: Serializeln oraz SerializeOut. Odpowiedź na to pytanie staje się jasne, gdy zaczniesz pracować z dokumentami zawierającymi inne obiekty, które także muszą być serializowane. Kreator App Wizard tworzy domyślną metodę Serialize o następującej postaci:

if (ar.IsStoring())

{ // TODO: add storing code here

} else

{ // TODO: add loading code here

}

Załóżmy, że Twój dokument przechowuje wszystkie informacje w tabli­cy CArray (patrz rozdział 1) o nazwie ary, która wie, w jaki sposób na­leży przeprowadzać serializację. Nie byłoby dobrym rozwiązaniem, gdybyś musiał serializować tę tablicę za pomocą następującego kodu:

if (ar.IsStoring())

{

ary.Serialize(ar);

}

else

{

ary.Serialize(ar);

}

Znacznie lepiej jest usunąć niepotrzebną instrukcję if i użyć kodu o na­stępującej postaci:

ary.Serialize(ar);

Tworzenie własnych okien dialogowych

Najprostszym sposobem stworzenia własnych okien dialogowym wyświetlanych podczas obsługi poleceń Plik=>Otwórz oraz Plik=>Zapisz jako, jest przesłonięcie standardowej definicji metody CWinApp::DoPromptFilename (która w zasadzie jest całkowicie nieudokumentowana -jedynie Notatka Techniczna numer 22 wspomina o tej metodzie). Metoda ta nie jest jednak tak prosta, jak mogłoby się zdawać. Wynika to z faktu, iż przeszukuje ona wzorce dokumentów, aby w dynamiczny sposób stworzyć łańcuch znaków używany w oknie dialogowym jako filtr określający typy plików, jakie będą wyświetlane. Dzięki zastosowaniu takiego rozwiązania, filtr stosowany w oknie dialo­gowym dostosowany będzie do istniejących wzorców dokumentów.

Dla Twoich własnych celów, zakodowanie tego filtra na stałe byłoby całkowicie wy­starczające. Jeśli nie jesteś pewny jak powinieneś zacząć, możesz skopiować oryginalny kod MFC i użyć go jako punktu wyjściowego przy tworzeniu własnej wersji metody. Metoda CWinApp::DoPromptFHeName wywołuje metodę o tej samej nazwie obiektu zarządcy dokumentów (jest to wewnętrzny obiekt MFC używany do zarządzania doku­mentami). Kod tej metody możesz znaleźć w kodzie źródłowym MFC (a konkretnie w pliku DOCMGR.CPP). Możesz skopiować ten kod i umieścić go w obiekcie wygene­rowanym przez kreatora App Wizard, a następnie zmodyfikować go zgodnie z Twoimi potrzebami. Podczas modyfikacji będziesz musiał zmienić kilka odwołań charaktery­stycznych dla obiektów zarządcy dokumentów, lecz na szczęście kompilator pomoże Ci odszukać odpowiednie miejsca (przykład przesłonięcia tej metody znajdziesz w dalszej części tego rozdziału).

Używanie kodu źródłowego MFC

Niemal każdy zgodzi się ze stwierdzeniem, że modyfikowanie kodu źródłowego MFC nie jest dobrym pomysłem. Chociaż teoretycznie możesz zmodyfikować kod źródłowy i skompilować go tworząc nowe biblioteki DLL, to jednak postępowanie takie można porównać do igrania z ogniem. Po pierwsze, Twoje biblioteki DLL staną się przez to niekompatybilne ze standardowymi bibliotekami DLL MFC. Po drugie, gdy firma Microsoft stworzy nową wersję MFC, będziesz musiał odpo­wiednio zmodyfikować nowy kod źródłowy biblioteki (który wcale nie musi przypominać kodu poprzednich wersji).

Czy to oznacza, że kod źródłowy MFC jest bezużyteczny? W żadnym wypadku. Przeglądając go bardzo często możesz zyskać wiedzę i pod­patrzeć interesujące rozwiązania. Co więcej, zawsze możesz skopiować część tego kodu, umieścić go w swoim programie i odpowiednio zmodyfikować. Także takie rozwiązanie może przysporzyć Ci nieco problemów, gdy zostanie wypuszczona nowa wersja MFC, jednakże tym razem problemy będą zlokalizowane w Twoim kodzie.

Czasami, także w kodzie MFC, będziesz mógł znaleźć błąd. W takim przypadku spróbuj stworzyć nową klasę wyprowadzoną z klasy zawie­rającej błąd i przesłonić nieprawidłowo napisaną metodę. Staraj się jednak oprzeć pokusie wprowadzania jakichkolwiek innych modyfikacji (jeśli jednak będziesz ich potrzebował, stwórz klasę potomną Twojej nowej klasy i dopiero w niej wprowadzaj modyfikacje). Dzięki temu, gdy błędy w MFC zostaną poprawione, będziesz mógł bez problemów odrzucić Twoje klasy i użyć klasy oryginalnej.

Jednak to nie wszystko. Metoda DoPromptFileName wywołuje statyczną metodę o nazwie AppendFilterSuffix. Również i tę metodę będziesz musiał skopiować z kodu źródłowego MFC i umieścić we własnym programie. Ze względu na to, iż metoda DoPromptFileName nie jest metodą wirtualną, będziesz musiał dodać do mapy komunikatów aplikacji makro OnFileOpen obsługujące polecenie otwierania pliku, oraz przesłonić metodę DoSave w klasie dokumentu. Kopiowany kod źródłowy możesz umieścić w swoim programie niemalże bez żadnych modyfikacji, gdyż jedynym prawdziwym powodem, dla którego go modyfikujesz, jest upewnienie się, że wywoływana będzie Twoja wersja metody DoPromptFileName. W zasadzie nie wiadomo, dlaczego metoda ta nie jest metodą wirtualną. W wewnętrznym obiekcie MFC CDocManager jest ona zadeklarowana jako metoda wirtualna; w żaden sposób nie poprawia to jednak Twojej sytuacji.

Bardzo prosty przykład zastosowania omówionych zagadnień będziesz mógł zobaczyć na Rysunku 2.1 oraz na listingach 2.1. i 2.2. W przykładzie tym, jak widać na rysunku, w oknie dialogowym umieściłem specjalny komunikat. Oczywiście, możesz zrobić co­kolwiek Ci przyjdzie do głowy - włącznie z całkowitym zmodyfikowaniem wyglądu okna dialogowego. Jeśli chcesz się dowiedzieć czegoś więcej na temat modyfikowania standardowych okien dialogowych, przeczytaj rozdział 5 niniejszej książki.

Dzięki przesłonięciu metody DoPromptFileName za jednym zamachem zmieniasz okna dialogowe wyświetlane przez procedury obsługi poleceń Plik=>Otwórz oraz Plik=> Zapisz jako. W bardziej wyrafinowanych aplikacjach, zamiast okna dialogowego klasy CFileDialog użyjesz zapewne okna jakiejś własnej klasy potomnej.



Listing 2.1. Własne okno dialogowe służące do obsługi plików (obiekt dokumentu).

// custdlgDoc.cpp : implementation of the CCustdlgDoc class

#include "stdafx.h"

#include "custdlg.h"

#include "custdlgDoc.h"

#ifdef __DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif

// CCustdlgDoc

IMPLEMENT_DYNCREATE(CCustdlgDoc, CDocument)

BEGIN__MESSAGE_MAP( CCustdlgDoc, CDocument)

//{{AFX_MSG_MAP(CCustdlgDoc)

//}}AFX_MSG_MAP END_MESSAGE_MAP()

//////////////////////////////////////////////////////////////////

// CCustdlgDoc construction/destruction

CCustdlgDoc::CCustdlgDoc()

{

// TODO: add one-tjime construction code here

}

CCustdlgDoc::~CCustdlgDoc()

{

}

BOOL CCustdlgDoc::OnNewDocument()

{

if (!CDocument::OnNewDocument())

return FALSE;

// TODO: add reinitialization code here

// (SDI documents will reuse this document)

return TRUE;

}

//////////////////////////////////////////////////////////

II CCustdlgDoc serialization

void CCustdlgDoc::Serialize(CArchive& ar) if (ar.IsStoringO )

// TODO: add storing code here else

// TODO: add loading code here

}

}

// CCustdlgDoc diagnostics

łtifdef JJEBUG

void CCustdlgDoc : :AssertValid( ) const

{

CDocument : : AssertValid ( ) ;

void CCustdlgDoc: :Dump (CDumpContextk dc) const {

CDocument : : Dump (dc ) ; } łfendif //_DEBUG

// CCustdlgDoc commands

// To jest mniej więcej dokładna kopia oryginalnej wersji // metody DoSave. Jedyna różnica polega na tym, iż wywołuje // ona Twoją wersję metody DoPromptFileName

BOOL CCustdlgDoc::DoSave(LPCTSTR IpszPathName, BOOL bReplace) {

CString newName = IpszPathName;

if (newName.IsEmpty())

{

CDocTempłate* pTemplate = GetDocTemplate(); ASSERTfpTemjSlate != NULL) ;

newName = m“strPathName;

if (bReplace && newName.IsEmpty())

#ifndef _MAC

#else

#endif

tfifndef “MAC


newName = m__strTitle;

// sprawdź poprawność nazwy

int iBad = newName.FindOneOf(_T(" #%;/\\ "));

int iBad = newName.FindOneOf(__T (":"));

if (iBad != -1)

newName.ReleaseBuffer(iBad);

// dodaj domyślne rozszerzenie jeśli zostało ono zdefiniowane

CString strExt;

if (pTemplate->GetDocString(strExt,

CDocTemplate::filterExt) &&

!strExt.IsEmpty())

ASSERT(strExt[0] == '.'). newName += strExt;

ttendif


CCustdlgApp *app=(CCustdlgApp *)AfxGetApp();

i f (!app->DoPromptFileName(newName,

bReplace ? AFX_IDS_SAVEFILE : AFX_IDS_SAVEFILECOPY, OFN_HIDEREADONLY | OFN_PATHMUSTEXIST, FALSE, pTemplate))

return FALSE; // nawet nie próbuj zapisywać

}

CWaitCursor wait;

if (!OnSaveDocument(newName))

if (IpszPathName == NULL)

// pamiętaj aby usunąć plik TRY

CFile::Remove(newName); CATCH_ALL(e)

TRACEO("Warning: failed to delete file after failed SaveAs.\n");

END_CATCH_ALL return FALSE;

// zresetuj tytuł i zmień nazwę dokumentu if (bReplace)

SetPathName(newName);

// powodzenie


return TRUE;


Listing 2.2. Własne okno dialogowe służące do obsługi plików (obiekt aplikacji).

// custdlg.cpp : Defines the class behaviors for the application.

_FILE_


ttinclude "stdafx.h" ttinclude "custdlg.h"

ttinclude "MainFrm.h" ttinclude "custdlgDoc.h" ttinclude "custdlgView.h"

ttifdef _DEBUG ttdefine new DEBUG_NEW ttundef THIS_FILE static char THIS_FILE[] #endif

// CCustdlgApp

BEGIN_MESSAGE_MAP(CCustdlgApp, CWinApp)

//{{AFX_MSG_MAP(CCustdlgApp)

ON_COMMAND(ID_APP_ABOUT, OnAppAbout)

ON_COMMAND(ID_FILE_OPEN, OnFileOpen)

//} }AFX_MSG_MAP

// Standard file based document comraands

ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)

ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)

// Standard print setup command

ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup) END_MESSAGE_MAP()

// CCustdlgApp construction CCustdlgApp::CCustdlgAppO

// TODO: add construction code here,

// Place all significant initialization in Initlnstance

// The one and only CCustdlgApp object CCustdlgApp theApp;

// CCustdlgApp initialization

BOOL CCustdlgApp::Initlnstance() {

AfxEnableControlContainer();

// Standard initialization

// If you arę not using these features and wish to reduce the size

// of your finał executable, you should remove from the following

// the specific initialization routines you do not need.

i

// Cali this when using MFC in //a shared DLL

// Cali this when linking to MFC // statically


#ifdef _AFXDLL

Enable3dControls();

#else

Enable3dControlsStatic(

#endif

// Change the registry key under which our settings arę stored. // You should modify this string to be something appropriate // such as the name of your company or organization. SetRegistryKey(_T("Local AppWizard-Generated Applications"));

LoadStdProfileSettings(); // Load standard INI file options (including MRU)

// Register the application's document templates. // Document templates

// serve as thę connection between documents, // framę windows and views.

CSingleDocTemplate* pDocTemplate;

pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CCustdlgDoc), RUNTIME__CLASS (CMainFrame) , RUNTIME_CLASS(CCustdlgView) ) ,-

AddDocTemplate(pDocTemplate);

// main SDI framę window

}


// Parse command linę for standard shell commands, DDE, // file open

CCommandLinelnfo cmdlnfo; ParseCommandLine(cmdlnfo);

// Dispatch commands specified on the command linę if (!ProcessShellCommand(cmdInfo)) return FALSE;

// The one and only window has been initialized, // so show and update it. m_pMainWnd->ShowWindow(SW__SHOW) ; m_pMainWnd->UpdateWindow();

return TRUE;

// CAboutDlg dialog used for App About

class CAboutDlg : public CDialog

{

public:

CAboutDlg();

// Dialog Data

/ / { {AFX_DATA (CAbo.utDlg) enum { IDD = IDD“ABOUTBOX }; //}}AFX_DATA

// ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CAboutDlg) protected:

virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV

// support //}}AFX_VIRTUAL

// Implementation protected:

//{{AFX_MSG(CAboutDlg)

// No message handlers

//}}AFX_MSG

DECLARE_MESSAGE_MAP()

CAboutDlg::CAboutDlgO : CDialog(CAboutDlg::IDD) {

//{{AFX_DATA“INIT(CAboutDlg)

//}}AFX_DATA_INIT


void CAboutDlg: : DoDataExchange (CDataExchange* pDX) {

CDialog: : DoDataExchange (pDX) ;

// { {AFX_DATA_MAP (CAboutDlg)

/ / } } AFX“DATA_MAP } BEGIN_MESSAGE_MAP (CAboutDlg, CDialog)

// { {AFX_MSG_MAP (CAboutDlg)

// No message handlers

//} }AFX_MSG_MAP END_MESSAGE_MAP ( )

// App command to run the dialog void CCustdlgApp: :OnAppAbout ( ) {

CAboutDlg aboutDlg;

aboutDlg . DoModal ( ) ;

// CCustdlgApp commands

// To jest kopia istniejącej (statycznej) metody

// obiektu docmgr

void AppendFilterSuf f ix(CString& filter, OPENFILENAME& ofn,

CDocTemplate* pTemplate, CString* pstrDefaultExt) {

ASSERT_VALID (pTemplate) ;

ASSERT_KINDOF (CDocTemplate, pTemplate) ;

CString strFilterExt , StrFilterName; i f (pTemplate->GetDocString(strFilterExt , CDocTemplate: : f ilterExt) && ! strFilterExt. IsEmpty ( ) && pTemplate- >GetDocS t ring (strFilterName, CDocTemplate :: f ilterName) && ! StrFilterName. IsEmpty ( ) ) {

// szablon dokumentu - dodaj do listy filtrów tłifndef _MAC

ASSERT(strFilterExt [0] == '.'); fłendif

if (pstrDefaultExt != NULL) {

// określ domyślne rozszerzenie

#ifndef _MAC

*pstrDefaultExt = ( (LPCTSTR) strFilterExt ) + 1; // pomiń '.'

#else

*pstrDefaultExt = strFilterExt ;

#endif

ofn. lpstrDefExt = (LPTSTR) (LPCTSTR) ( *pstrDefaultExt ); ofn.nFilter!ndex = ofn.nMaxCustFilter + 1; // numer począwszy od l

// dodaj do filtra

filter += strFilterName;

ASSERT (! filter . IsEmpty ()); // typ pliku musi być podany

filter += (TCHAR) '\0'; // następny łańcuch znaków proszę łtifndef _MAC

filter += (TCHAR) ' * ' ; łtendif

filter += strFilterExt;

filter += (TCHAR)'\0'; // następny łańcuch znaków proszę

ofn.nMaxCustFilter++;

//To jest kopia oryginalnej metody, która jednak

// stosuje nasz szablon okna dialogowego

BOOL CCustdlgApp::DoPromptFileName(CString & fileName, UINT

nIDSTitle, DWORD IFlags, BOOL bOpenFileDialog, CDocTemplate *

pTemplate)

CFileDialog dlgFile(bOpenFileDialog);

CString title; VERIFY(title.LoadString(nIDSTitle));

dlgFile.m_ofn.Flags |= IFlags;

CString strFilter; CString strDefault; if (pTemplate != NULL)

ASSERT_VALID(pTemplate);

AppendFilterSuffix(strFilter, dlgFile.m_ofn, pTemplate,

ŁstrDefault) ,-}

else {

// wykonaj dla wszystkich szablonów dokumentów

POSITION pos = GetFirstDocTemplatePosition();

BOOL bFirst = TRUE;

while (pos != NULL)

CDocTemplate* pTemplate = GetNextDocTemplate(pos) AppendFilterSuffix(strFilter, dlgFile.m_ofn,

pTemplate, bFirst ?

ŁstrDefault : NULL);

bFirst = FALSE;

// dodaj "*.*" do wszystkich filtrów plików

CString allFilter;

VERIFY(allFilter.LoadString(AFX_IDS_ALLFILTER));

strFilter += allFilter;

strFilter += (TCHAR)'\0'; // następny łańcuch znaków proszę łtifndef _MAC

strFilter += _T("*.*"); łtelse

strFilter += _t("****"),• ttendif

strFilter += (TCHAR)'\0'; // ostatni łańcuch znaków

dlgFile.m_ofn.nMaxCustFilter++;

dlgFile.m_ofn.IpstrFilter = strFilter; ttifndef _MAC

dlgFile.m_ofn.IpstrTitle = title; #else

dlgFile.m“ofn.IpstrPrompt = title; ttendif

dlgFile.m_ofn.IpstrFile = fileName.GetBuffer(_MAX_PATH);

FALSE;


// Modyfikuj działanie

dlgFile.m_ofn.Flags | =OFN_ENABLETEMPLATE; dlgFile . m_ofn. lpTemplateName="CustomTempl " ,-BOOL bResult = dlgFile . DoModal () == IDOK ? TRUE f ileName.ReleaseBuf fer ( ) ; return bResult;

która wywołuje


// To jest kopia oryginalnej metody, // naszą metodę DoPromptFileName void CCustdlgApp: :OnFileOpen ( )

// poproś użytkownika o dokonanie wyboru

// (dostępne wszystkie szablony) CString newName ,-if (!DoPromptFileName(newName, AFX_IDS_OPENFILE,

OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, TRUE, NULL)) return; // otwieranie przerwane

OpenDocumentFile(newName);

to użytkownik został


// jeśli zwróci NULL, // już powiadomiony


Inny przykład

Znakomitym miejscem, w którym można znaleźć interesujące przykłady kodu programów, są wszelkiego typu artykuły w czasopismach i książkach. Zazwyczaj nie musisz nawet płacić za kod pochodzący z tych źródeł. Innym doskonałym miejscem, gdzie można zdobyć darmowy kod źródłowy, jest Internet; jeszcze innym -jest sama firma Microsoft. Cztery razy do roku wydaje ona CD ROM-y serii Developer's Network. Ich subskrypcja, co prawda, nie jest tania, jednak można tam znaleźć ogromne ilości dokumentacji oraz przykładowych programów, których kod możesz używać we własnych programach.

Interesujący kod możesz także znaleźć w dostępnych na rynku pakietach narzędzio­wych. Zazwyczaj firmy produkujące te narzędzia udostępniają wsparcie dla programistów, jednak nie jest łatwo zdobyć kod źródłowy takich narzędzi. Najczęściej musisz także płacić za możliwość używania tych narzędzi we własnych, komercyjnych aplikacjach.

Problem z kopiowaniem kodu źródłowego polega na tym, że jest on bardzo często nie­kompatybilny z MFC. Rozpatrzmy przykład aplikacji będącej prostą przeglądarką map bitowych. Po przeszukaniu CD ROM-u Developer's Network okazuje się, że dostępna jest tam biblioteka DLL służąca do obsługi takich plików. Biblioteka ta (DIBAPI.DLL) potrafi wykonywać wiele podstawowych operacji na plikach BMP, w tym także otwie­rać je, czytać i wyświetlać. Biblioteka ta jest fragmentem przykładu WINCAP. Przykład ten możesz także znaleźć w niektórych SDK-ach oraz w Internecie. Odpowiedni dokument ma symbol S14049 lub (w nowszych wersjach) Q97193.

Dzięki temu, że dostępny jest kod źródłowy tej biblioteki, będziesz mógł w miarę prosto stworzyć nową jej wersję, wykorzystującą styl C++. Osobiście jednak wolę nie modyfi­kować cudzego kodu, który działa całkiem dobrze. Zamiast tego zdecydowałem się stworzyć nowy obiekt wykorzystujący funkcje dostępne w bibliotece DLL (CDib -patrz rozdział? oraz tabela 2.4). Nikt, kto używa klasy CDib, nie musi wiedzieć, że jej implementacja umieszczona jest w bibliotece DLL stworzonej w języku C. Moja oryginalna implementacja klasy CDib przeznaczona była do współpracy z MFC w wersji 16-bitowej. Podczas tworzenia nowej, 32-bitowej wersji klasy CDib, musiałem wprowadzić także pewne modyfikacje w kodzie źródłowym biblioteki DIBAPI (wszystkie pliki będziesz mógł znaleźć na CD ROM-ie dołączonym do książki.


Tajniki klasy CDib

Dokładne omówienie tej klasy znajdziesz w rozdziale 7 tej książki. Na razie wystarczy, abyś wiedział, że klasa CDib reprezentuje pliki BMP, podobnie jak obiekty klasy CWnd reprezentują okna. Klasa CDib, podobnie jak wiele innych obiektów MFC, stosuje dwuetapowy proces tworzenia obiektów. Zadeklarowanie obiektu CDib nie kojarzy go z żadną mapą bitową. Aby zainicjalizować obiekt klasy CDib musisz wywołać metodę Create. Obiekty tej klasy mogą być tworzone na podstawie pliku, innego obiektu tej klasy, okna lub całego ekranu. Możesz stworzyć nowe obiekty klasy CDib na wiele różnych sposobów. Jednak najważniejsze jest to, że obiekty te rozróżniają rozszerzenia plików chociaż nie korzystają z pomocy obiektów klas CFile ani CArchive.


Przykładowa aplikacja

Aby pokazać możliwości wykorzystania klasy CDib, stworzyłem prostą aplikację -BMPYIEW - służącą do przeglądania plików BMP (patrz, rysunek 2.2 i listing 2.3).

Aplikacja BMPYIEW stworzona została przez kreatora App Wizard. Jednak usunąłem z niej niektóre standardowe fragmenty kodu. Dla przykładu, BMPYIEW nic może mo­dyfikować przeglądanych plików - dlatego w tym programie nie jest dostępna opcja Plik=>Zapisz (możesz jednak zapisać plik pod inną nazwą; opcja Plik=>Zapisz jako jest dostępna). Nie ma również żadnej potrzeby tworzenia nowego dokumentu podczas uruchamiania programu.

Listing 2.3. Obiekt dokumentu stosowany w aplikacji BMPVIEW.

// bmpvidoc.cpp : implementation of the CBmpViewDoc class

//

ttinclude "stdafx.h"

ttinclude "bmpview.h"

ttinclude "bmpvidoc.h"

tłifdef _DEBUG

tłundef THIS“FILE

static char BASED_CODE THIS_FILE[] = _FILE_;

łfendif

///// // CBmpViewDoc

IMPLEMENT_DYNCREATE(CBmpViewDoc, CDocuraent)

BEGIN_MESSAGE“MAP(CBmpViewDoc, CDocument) //{{AFX_MSG_MAP(CBmpViewDoc) //} }AFXJYISG_MAP

END_MESSAGE_MAP()

11111111 / 11111111,

II CBmpViewDoc construction/destruction

CBmpViewDoc::CBmpViewDoc() {

m_dib=NULL; }

CBmpViewDoc::~CBmpViewDoc() { }

BOOL CBmpViewDoc::OnNewDocument() {

i f (!CDocument::OnNewDocument())

return FALSE; return TRUE; }

void CBmpViewDoc::DeleteContents(void)

{

if (m_dib) delete m_dib;

m_dib=NULL;

}

BOOL CBmpViewDoc::OnOpenDocument(const char * fn)

{

DeleteContents();

SetModifiedFlag(FALSE);

m_dib=new CDib;

return m_dib->Create((LPSTR)fn);

}

BOOL CBmpViewDoc::OnSaveDocument(const char *fn)

{

WORD err;

err=m_dib->Save(fn); if (err) CDib::ErrorMessage(err);

return err==0;

}



// CBmpViewDoc serialization

void CBmpViewDoc::Serialize(CArchive& ar) {

{

}

else

{

}


if (ar.IsStoring())

// TODO: add storing code here

// TODO: add loading code here

// CBmpViewDoc diagnostics

#ifdef _DEBUG

void CBmpViewDoc::AssertValid() const

CDocument::AssertValid(); void CBmpViewDoc::Dump(CDumpContext& dc) const

CDocument::Dump(dc); ttendif //_DEBUG


Pewnych modyfikacji wymaga obiekt dokumentu stosowany w aplikacji BMPYIEW. Oczywiście, w pierwszej kolejności musisz dodać do niego obiekt klasy CDib (składowa m_dib) oraz plik nagłówkowy CDIB.H. Ze względu na to, że klasa CDib nie stosuje standardowych metod serializacji MFC, należy przesłonić standardowe definicje metod OnOpenDocument oraz OnSaveDocument Metody te zastąpione zostają przez: wywołania konstruktora lub metody Save klasy CDib. Rozwiązanie takie wypacza standardowy proces serializacji, jednakże podstawowe czynności związane z wyświe­tleniem okna dialogowego, wyborem wzorca oraz stworzeniem dokumentu pozostają niezmienione. Tak naprawdę, aplikacja BMPY1EW nie potrzebuje prawdziwej serializacji. BMPYIEW nie jest wielką aplikacją, a cała jej wartość polega na umiejętności wyświetlania standardowych plików BMP.

W opisanym powyżej przypadku, standardowe metody serializacji stają się niewygodne. Program ani ich nie potrzebuje, ani nie chce używać, dlatego też musi je obejść. Dzięki przesłonięciu standardowych metod OnOpenDocument oraz OnSaveDocument jesteś w stanie przejąć pełną kontrolę nad przebiegiem procesu ładowania i zapisywania plików.


Serializacja obiektów

Serializacja wygląda w nieco inny sposób, jeśli jedyną rzeczą, do której chcesz jej użyć, jest zapisanie w archiwum stanu obiektu. Gdy zapisujesz całe obiekty, musisz w pierwszej kolejności wywołać metodę Serialize zdefiniowaną w klasie bazowej Twojego obiektu. Dzięki temu, klasa bazowa będzie w stanie zapisać wszystkie potrzebne informacje.

W rzeczywistości, obiekty klasy CObject nie przeprowadzają serializacji jakichkolwiek danych. Jednakże nikt nie wie, czy w przyszłych wersjach MFC obiekty tej klasy nie będą stosowały stabilnych danych, które trzeba będzie serializować. Jeśli Twoja klasa wyprowadzona jest z innej klasy, to z dużą dozą prawdopodobieństwa można założyć, że stosowana klasa bazowa będzie chciała przeprowadzić serializację swoich danych.

Innym istotnym powodem, dla którego należy przeprowadzać serializację klasy bazowej, jest konieczność zapewnienia poprawnego zapamiętania informacji o Twojej klasie. MFC tworzy i używa specjalnego archiwum, w którym przechowywane są ważne informacje na temat klas, które mogą być serializowane. Archiwum to zawiera, między innymi, nazwy klas, ich wielkości, metody służące do ich tworzenia, wersje klasy i wiele innych informacji. Za pierwszym razem, gdy przeprowadzasz serializację obiektu, MFC zapisuje także in­formacje o jego klasie. Na szczęście MFC jest wystarczająco inteligentne, aby zapisać te informacje tylko raz.

Po wywołaniu metody Serialize klasy bazowej, będziesz mógł serializować wszystkie potrzebne Ci dane Twojego obiektu. Od Ciebie zależy, jakie dane zostaną zapisane. W końcu, nie ma żadnej potrzeby zapisywania informacji, które w żaden sposób nie będą Ci przydatne przy odtwarzaniu stanu obiektu.

Nie powinieneś własnoręcznie ingerować w format pliku stosowanego przez MFC do serializacji obiektów. Jeśli jednak kiedyś będziesz musiał własnoręcznie go przeanalizować, przyda Ci się wiadomość, iż jego format opisany został w Notatce Technicznej numer 2.

Jedyną metodą poprawnej serializacji obiektów jest użycie metod WriteObject oraz ReadObject. Jednak rzadko będziesz odwoływał się do tych metod bezpośrednio. Zamiast tego, makro IMPLEMENT_SERIAL oraz wewnętrzny kod MFC ukrywają odwołania do tych metod dostarczając w zamian operatorów « i ».

Stosując operator » do odczytania obiektu z archiwum, dostarczasz wskaźnik na obiekt klasy CObject. MFC tworzy nowy obiekt odpowiedniej klasy i odczytuje jego dane. Jeśli wiesz, jakiego obiektu się spodziewałeś, to możesz odpowiednio rzutować jego typ (jeśli nie jesteś pewny jaki obiekt może zostać odczytany, możesz użyć metody IsKindOf do określenia jego typu).

Warto powiedzieć jeszcze jedną interesującą rzecz odnośnie serializowania obiektów: nie da się w prosty sposób przeprowadzić serializacji obiektów klasy CDocument. Wynika to z faktu, iż poprawna serializacja obiektu wymaga wywołania metody WriteObject. Jednakże metody używane przez MFC do odczytywania i zapisywania serializowanych danych wywołują bezpośrednio metodę Serialize obiektu dokumentu. Tak więc, nawet jeśli zmieniłbyś klasę dokumentu, używając w niej makra IMPLEMENT_SERIAL, nie doprowadziłoby to do zastosowania odpowiedniego formatu zapisu, w pliku używanym do serializacji.

Istnieje jednak pewna sytuacja, w której będziesz chciał użyć makra IMPLEMENT_ SERIAL w połączeniu z obiektami klasy potomnej klasy CDocument. Problem powstaje bowiem w momencie, gdy chcesz serializować obiekty zawierające wskaźniki na obiekty klasy CDocument. Nie musisz umieszczać w archiwum całych obiektów typu CDocument; musisz jednak umieścić w nich odwołanie, aby odpowiednie wskaźniki mogły zostać poprawnie odtworzone. Rozwiązanie tego problemu polega na użyciu metody MapObject. Metoda ta pozwala na umieszczenie w archiwum odwołania do wskazanego obiektu, bez umieszczania w archiwum jego kopii. Użycie tej metody pod­czas zapisu pozwala na umieszczenie w archiwum odpowiedniego odwołania, a podczas odczytu - na poinformowanie archiwum, jaki obiekt powinien zostać użyty.


Obsługa różnych wersji serializowanych obiektów

Spójrz na rysunki 2.3 oraz 2.4; są na nich przedstawione dwie, różne wersje programu obsługi poczty elektronicznej. Pierwsza z nich przechowuje jedynie listę nazwisk oraz odpowiadających im adresów e-mail. Możesz sprawdzić - obiekt dokumentu tej aplikacji używa pomocniczego obiektu klasy EMailDB. To właśnie ten obiekt zawiera bazę danych adresów oraz umożliwia ich serializację. Przeglądając kod źródłowy (listing 2.4) będziesz mógł zobaczyć, iż w wywołaniu makra IMPLEMENT_SERIAL zastosowany został trzeci argument - VERSIONABLE_SCHEMA|1.


Listing 2.4. Dokument stosowany w pierwszym programie obsługi poczty elektronicznej.

// emailsDoc.cpp : implementation of the CEmailsDoc class

ttinclude "stdafx.h" (łinclude "emails.h"

łtincłude "emailsDoc .h"

_FILE_


ttinclude "emailsView.h" tłifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] =

#endif

// CEmailsDoc

IMPLEMENT_DYNCREATE(CEmailsDoc, CDocument) IMPLEMENT_SERIAL (EMai IDB, COb j ect, VERSIONABLE_SCHEMA | l)

void EMailDB::Serialize(CArchive& ar)

int i ;

i f (ar.IsLoadingf))

int version=ar.GetObjectSchema(); if (version!=l)

AfxMessageBox("Unknown file message"); return;

CObject::Serialize(ar);

ar»m_count ;

for (i=0;i<=m_count;i++)

ar»name [i] ; ar»email [i] ; } }

else {

CObject::Serialize(ar);

ar«m_count ,-

for (i=0;i<=m_count;i++)

ar«name [i] ; ar«email [i] ;

BEGIN_MESSAGE_MAP(CEmailsDoc, CDocument) //{{AFX_MSG_MAP(CEmailsDoc)

// NOTĘ - the ClassWizard will add and remove mapping // macros here. DO NOT EDIT what you see in these blocks // of generated code! //}}AFX_MSG_MAP END_MESSAGE_MAP()

// CEmailsDoc construction/destruction CEmailsDoc::CEmailsDoc()

db=NULL; CEmailsDoc::-CEmailsDoc()

BOOL CEmailsDoc::OnNewDocument() {

if (!CDocument::OnNewDocument()) return FALSE;

delete db;

db=new EMailDB;

return TRUE;

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 // 1 1 1 1 1 1 1 1 1 1 1 1 // CEmailsDoc serialization

void CEmailsDoc : :Serialize (CArchive& ar) {

CObject *ob;

if (ar . IsStoring( ) )

ar«db;

else {

ar»ob;

db=(EMailDB *)ob;

CEmailsView *v;

POSITION pos;

pos=GetFirstViewPosition() ;

v=(CEmailsView *)GetNextView(pos);

v->m_current=0;

v->UpdateView();

11111 /11111111111 /11 /11111 /11

II CEmailsDoc diagnostics

łtifdef _DEBUG

void CEmailsDoc::AssertValidO const

CDocument::AssertValid(); void CEmailsDoc::Dump(CDumpContext& dc) const

CDocument::Dump(dc); ł ttendif //_DEBUG

// CEmailsDoc commands

void CEmailsDoc::DeleteContents()

delete db;

CDocument::DeleteContents();

Zazwyczaj ostatni argument makra IMPLEMENT_SERIAL określa numer wersji klasy. Za każdym razem, gdy wprowadzasz w klasie jakiekolwiek modyfikacje, powinieneś zmodyfikować numer jej wersji. W ten sposób, jeśli Twój program będzie próbował od­czytać starszą wersję obiektu, MFC zgłosi odpowiedni wyjątek. Kiedy do makra IMPLEMENT_SERIAL dodasz argument VERSIONALBLE_SCHEMA, będzie to oznaczało, że sam chcesz obsługiwać starsze wersje obiektów; w takim wypadku MFC nie będzie zgłaszać wyjątku.

Druga wersja programu umożliwia przypisanie każdemu nazwisku dwóch adresów poczty elektronicznej. Oznacza to, że w obiektach klasy EMailDB pojawiło się więcej danych; będziesz musiał zmienić numer wersji tej klasy (patrz listing 2.5). Jednak ze względu na zastosowanie flagi VERSIONABLE_SCHEMA, cały czas będziesz mógł odczytywać obiekty poprzedniej wersji.


Listing 2.5. Fragment kodu drugiej wersji programu do obsługi poczty elektronicznej.

IMPLEMENT_SERIAL(EMailDB,CObject,VERSIONABLE_SCHEMA|2) void EMailDB::Serialize(CArchive& ar)

int i ;

CObject::Serialize(ar) ;

i f (ar.IsLoading())

int version=ar.GetObjectSchema(); if (version>2||version<l)

AfxMessageBox("Unknown file message"); return;

ar»m_count ;

for (i=0;i<=m_count;i++)

ar»name [ i] ;

ar»email [ i] ;

if (version>l) ar»emaill [i] ;

else

ar «m_count ;

for (i=0; i<=m_count;

{

ar«name [i] ;

ar«email [i] ;

ar«emaill [i]

Aby możliwa była poprawna interpretacja typów obiektów, będziesz musiał, podczas odczytywania danych z archiwum, wywołać metodę GetObjectSchema obiektu CArchive. Dokumentacja twierdzi, że wywołanie tej metody powinno być pierwszą czynnością podczas odczytywania danych, nie stwierdziłem jednak, aby było to prawdą. Warto wie­dzieć, że z jakichś tajemniczych powodów kod implementujący metodę GetObjectSchema, po pierwszym wywołaniu tej metody, przypisuje numerowi wersji wartość -1. Oznacza to, że po serializacji będziesz mógł odczytać wersję obiektu tylko jeden raz; kolejne próby wywołania metody GetObjectSchema spowodują zwrócenie wartości -1.

Kiedy już określisz numer wersji, będziesz mógł odmówić odczytania danych, które nie są dostępne we wcześniejszych wersjach programu. W przedstawionym przykładzie tylko nowsza wersja programu korzysta z drugiego adresu poczty elektronicznej. Dzięki zapamiętaniu numeru wersji, kiedy podczas zapisywania obiektów wykryjesz, że zapisywa­ny jest obiekt starszej wersji, będziesz mógł zapytać użytkownika, czy chce zapisać obiekt w starym formacie, czy też skonwertować go do nowego.

Nie zapomnij: w odróżnieniu od tego, co się może wydawać, to nie obiekt dokumentu prze­prowadza serializację. Oznacza to, że nawet jeśli dodasz makro IMPLEMENT_SERIAL do swojego obiektu dokumentu, nie umożliwi to zapisania numeru wersji obiektu, ani ja­kichkolwiek innych informacji o jego klasie. Jeśli wywołasz metodę GetObjectSchema wewnątrz metody Serialize obiektu dokumentu, zwróci ona wartość -l (o ile nie umożliwiłeś poprawnej serializacji obiektu dokumentu za pomocą metody WriteObject oraz operatora «).


Wtasne metody serializacji

Co się stanie, gdy będziesz próbował serializować obiekty zapisując je nie w pliku, lecz gdzie indziej? Teoretycznie, mógłbyś spróbować wyprowadzić nową klasę potomną klasy CArchive, jednak w praktyce jest to rzadko stosowane. Wymagało by to bowiem zmodyfikowania przynajmniej czterech metod: Read, Write, Flush oraz FillBuffer. Jeśli nie chcesz zapisywać serializowanych danych w pliku, będziesz musiał w pierwszej kolejności zrozumieć, jak obiekty klasy CArchive buforują dane (i mieć nadzieję, że w przyszłości sposób buforowania nie ulegnie zmianie). Oczywiście, mógłbyś skopiować istniejący kod i odpowiednio go zmodyfikować.

Innym rozwiązaniem jest stworzenie własnej klasy potomnej klasy CFile. Właśnie to rozwiązanie jest wykorzystywane przez MFC w celu stworzenia archiwów współpra­cujących z gniazdami sieciowymi (patrz rozdział 9). Także i to rozwiązanie wymaga dużego wkładu pracy. Będziesz bowiem musiał stworzyć własne wersje metod Get-BufferPtr, Read, Write oraz Seek. Metoda GetBufferPtr wymagana jest jedynie w tych obiektach obsługujących pliki, w których stosowane są wewnętrzne bufory. Jeśli to nie dotyczy Twojej klasy obiektów, to będziesz mógł zwrócić z tej metody wartość 0.

Kiedy już stworzysz własny obiekt CFile, będziesz musiał przesłonić metodę CDocument::GetFiIe. Ta wirtualna metoda odpowiedzialna jest /,a stworzenie obiektu klasy CFile, wywołanie metody Open tego obiektu oraz zwrócenie wskaźnika do niego.

Jest pewna chytra metoda pozwalająca na uproszczenie wszystkich tych czynności (choć jej skutkiem ubocznym jest utrata efektywności). Zamiast klasy CFile możesz bowiem stworzyć klasę potomną klasy CMemFile. Cały proces modyfikacji byłby dosyć podobny do opisanego powyżej. Gdy MFC otwierałoby archiwum do zapisu, musiałbyś przekazać pusty obiekt CMemFile w konstruktorze CArchiye. Dzięki temu, MFC zapisało­by wszystkie informacje do bloku pamięci, zarządzanego przez obiekt CMemFile.

Dodatkowo, musiałbyś przesłonić standardowe definicje metod OnOpenDocument oraz OnSaveDocument, umieszczając w nich odpowiedni kod po wywołaniu metody Serialize. Po zakończeniu działania metody Serialize mógłbyś wywołać metodę CMemFile::Detach zwracającą wskaźnik na blok pamięci. Dysponując tym wskaźnikiem mógłbyś zrobić z danymi co Ci się żywnie podoba. Dla przykładu, mógłbyś umieścić je w bazie danych lub zapisać w pliku o innym formacie zapisu.

Aby odczytać dane zapisane w taki sposób, wystarczy wczytać je do pamięci i podać wskaźnik do niej oraz wielkość bloku w wywołaniu konstruktora obiektu klasy CMemFile (pamiętaj także, aby przekazać wartość O jako argument nGrowBytes). Obiekt klasy CMemFile zwróć jako wynik działania metody GetFile. Wywołanie metody Serialize spowoduje odczytanie danych z pamięci, tak jak gdyby były one zapisane w normalnym pliku. Jak zwykle, modyfikacje możesz rozpocząć od skopiowania orygi­nalnych wersji metod OnOpenDocument oraz OnSaveDocument.

Jeśli chciałbyś uniknąć przesłaniania metod OnOpenDocument oraz OnSaveDocument, możesz stworzyć nowa klasę potomną klasy CMemFile. Metoda Open nowej klasy byłaby odpowiedzialna za inicjalizację pliku, natomiast metoda Close, za przeniesienie zapisanych informacji do miejsca, gdzie będą przechowywane.

Przykład takiego rozwiązania zaprezentowany został na listingu 2.6. Prezentuje on ten sam, dobrze już znany, program do obsługi poczty elektronicznej, w którym książka adresowa jest kodowana.

Najistotniejszym elementem tego rozwiązania jest klasa CCryptFile (patrz listing 2.6). Jest ona specjalnym rodzajem klasy CMemFile udostępniającym własne wersje metod Open oraz Close. Metoda Read tej klasy, w pierwszej kolejności wczytuje cały plik do pamięci, dekoduje go (za pomocą algorytmu XOR), a następnie kojarzy ten obszar pamięci z obiektem CMemFile.


Listing 2.6. Klasa obsługująca szyfrowane pliki.

#include "stdafx.h" ttinclude "cryptfile.h"

BOOL CCryptFile::Open( LPCTSTR IpszFileName, UINT nOpenFlags, CFileException* pError)

CFile raw;

// mógłybś chcieć sprawdzić czy nie jest ustawiony tryb

// modeRead, jednakże sprawdzenie takie nie będzie działać

// gdyż tryb na wartość O

if ((nOpenFlags & (CFile::modeReadWrite|CFile::modeWrite))==0)

if (!raw.Open(IpszFileName,nOpenFlags,pError)) return FALSE;

unsigned n;

n=raw.GetLength();

unsigned char *p=(unsigned char *)malloc(n);

raw.Read(p,n); // przeczytaj cały plik

raw.Close(); // odszyfruj for (unsigned i=0;i<n;i++)

p[i]A=OxFF; Attach(p,n); } else

basefile=lpszFileName; return TRUE; }

void CCryptFile::Close( ) {

CFileException pError; CFile raw;

// jeśli zapisujemy, zaszyfruj dane i zapisz do rzeczywistego pliku

if (Ibasefile.IsEmptyO) {

unsigned n=GetLength(); if

(!raw.Openlbasefile,CFile::modeWrite CFile::modeCreate| CFile::shareExclusive,ŁpError)) throw ŁpError; unsigned char *p=Detach(); // zaszyfruj for (unsigned i=0;i<n;i++)

p[i]~=OxFF; raw.Write(p,n); raw.Close(); free(p);

Zapisywanie plików jest znacznie prostsze. Nazwa pliku zapamiętywana jest w składowej basefile, po czym obiekt klasy CMemFile tworzy obszar pamięci o odpowiedniej wielkości. Następnie metoda Close koduje cały blok pamięci i zapisuje go do rzeczywistego pliku. Poniżej przedstawiony został sposób użycia klasy szyfrującej dane:

CFile CEmailsDoc::GetFile(LPCTSTR IpszFileName, UINT nOpenFlags,

CFileException * pError) {

CCryptFile *pFile=new CCryptFile;

if (!pFile->Open(lpszFileName, nOpenFlags, pError)) delete pFile; pFile = NULL; } return pFile;

Oczywiście, szyfrowanie jest tylko jedną z rzeczy, jakie możesz osiągnąć stosując opisaną powyżej metodę. Równie dobrze mógłbyś zapisywać i odczytywać informacje z bazy danych. Użycie klasy CMemFile nie jest zbytnio efektywne, gdyż musisz przechowy­wać cały plik w pamięci. Alternatywne rozwiązanie polega na stworzeniu nowej klasy potomnej klasy CFile i przesłonięciu metod Read oraz Write tak, aby odczytywały i zapisywały dane z miejsca, w którym chcesz je przechowywać (szyfrując je lub zapisując w bazie danych). Przy takim rozwiązaniu, sposób użycia metody GetFile w nowej klasie CFile, pozostaje identyczny jak przedstawionego powyżej.


Drobne modyfikacje serializacji

Czasami możesz chcieć wprowadzić prostą modyfikację formatu pliku. Dla przykładu, ' zawsze uważałem, że bardzo dobrym rozwiązaniem jest umieszczanie na początku pliku j krótkiej informacji o jego zawartości, zakończonej kombinacją znaków Ctrl+Z. Dzięki takiemu rozwiązaniu, jeśli ktoś wpisze nazwę pliku w linii poleceń, na ekranie zostanie j wyświetlony jedynie komunikat informacyjny - zawartość pliku nie będzie wyświetlana.

Oczywiście, aby umieścić w pliku taki komunikat, mógłbyś użyć metody polegającej na ; stworzeniu nowej klasy potomnej klasy CFile (opisanej we wcześniejszej części rozdziału). ; Istnieje jednak prostsza metoda. Polega ona na pobraniu wskaźnika do pliku (CFile) wewnątrz metody Serialize i wprowadzeniu modyfikacji w pliku za pomocą tego wskaźnika. Wskaźnik do pliku możesz pobrać za pomocą metody GetFile.

Jeśli archiwum nie było jeszcze używane, będziesz mógł bez żadnych obaw wywołać metodę GetFile. Jeśli jednak w buforze archiwum są umieszczone jakiekolwiek dane, będziesz je musiał usunąć za pomocą wywołania metody Flush. Dopiero potem będziesz mógł wywołać metodę GetFile.

Na listingu 2.7 przedstawiona została kolejna wersja programu obsługującego pocztę elektroniczną. Tym razem, w metodzie Serialize wywoływana jest metoda GetFile, dzięki której możliwe jest dodanie do archiwum odpowiedniego nagłówka i zakończenia, Kiedy dane są odczytywane z archiwum, MFC przeskakuje (i pomija) nagłówek.


Listing 2.7. Fragment kodu kolejnej wersji programu obsługi poczty elektronicznej.

void CEmailsDoc::Serialize(CArchive& ar)

CObject *ob;

if (ar .IsStoringO )

ar.GetFileO->Write("File by AWC\r\n\xla",14);

ar«db;

ar.Flush();// trzeba opróżnić bufory przed użyciem //obiektu CFile

ar.GetFile()->Write("AWC-EOF\r\n",9); }

else {

ar.GetFile()->Seek(14,CFile::begin); // pomiń bajty

ar»ob;

db=(EMailDB *)ob;

CEmailsView *v;

POSITION pos;

pos=GetFirstViewPosition();

v=(CEmailsView *)GetNextView(pos);

v->m_current=0;

v->UpdateView();

Zagadnienia przenaszalności

Klasa CArchive usiłuje zapewnić przenaszalność tworzonym plikom archiwalnym. Dlaczego tylko próbuje? Spójrz na poniższy fragment kodu (gdzie ar jest obiektem archiwum):

int x = 10; ar«x;

MFC nie pozwoli Ci skompilować powyższego kodu. Problem polega na tym, iż typ int jest typem 16-bitowym na jednych komputerach, a 32-bitowym na innych. Dlatego też klasa CArchive nie posiada metody służącej do zapisywania liczb całkowitych. Jedyne proste typy danych, jakie możesz umieszczać w archiwach przedstawione zostały w Tabeli 2.5. Oczywiście, nic nie stoi na przeszkodzie, abyś odpowiednio rzutował typy danych podczas zapisywania ich do archiwum:

int x = 10; ar« (DWORD)x;

Tabela 2.5. Proste typy danych, które można serializować

Typ danych: BYTE WORD LONG DWORD float double

To jest dobre rozwiązanie, nieprawdaż? Może i tak, ale zastanówmy się w takim razie nad serializacją klasy CRect (klasy używanej w MFC do przedstawiania prostokątów). Możesz ją serializować tak samo jak wiele innych obiektów. Jednakże wielkość danych będzie różna na komputerach 16 i 32-bajtowych. To samo dzieje się podczas serializacji obiektów klasy CPoint oraz CSize. Mogłoby się wydawać, że jeśli archiwa pozwalają na stosowanie niektórych nieprzenaszalnych typów danych, mogłyby pozwolić na sto­sowanie ich wszystkich. Cóż, w każdym razie jest to jedynie drobna niewygoda.


Podsumowanie

Archiwa są jednymi z tych elementów MFC, z którymi zazwyczaj nie ma się wiele do czynienia. Kiedy jednak trzeba z nimi coś zrobić, okazuje się nagle, że bardzo niewiele wiadomo na ich temat. Archiwa nie są złym pomysłem, jeśli jednak stosujesz jakiś własny format zapisu, równie dobrze możesz w ogóle zaniechać wykorzystywania mechanizmów serializacji.

Jeśli natomiast nie musisz stosować ściśle określonego formatu zapisu informacji w pliku, to serializacja jest bardzo dobrym rozwiązaniem. Pliki tworzone za jej pomocą są efek­tywne w obsłudze, a dodatkowo pozwalają na prostą obsługę różnych wersji obiektów, Oczywiście, dane zapisane w taki sposób będą niedostępne dla programów, które nie wykorzystują MFC (lub dostęp do nich będzie wysoce utrudniony). Jednak rzadko kie­dy jest to znaczącym problemem


Praktyczny przewodnik Serializacja

Tworzenie serializowalnych klas

Określanie okien dialogowych używanych do podawania nazw plików

Używanie istniejącego lub własnego kodu obsługi plików

Tworzenie archiwów operujących na niestandardowych strumieniach

Odczytywanie starszych wersji plików

W MFC serializacja używana jest do zapewnienia stabilności obiektów oraz do zapisywania i odczytywania dokumentów. W przypadku wielu prostych programów w zupełności wystarczą Ci standardowe mechanizmy serializacji, jednakże w niektórych przypadkach będziesz potrzebował czegoś więcej. Dla przykładu, wiele programów musi korzystać z plików o ściśle określonych formatach. Inne programy muszą zapisywać dane w kilku różnych plikach lub stosować zupełnie inne medium do przechowywania danych. Informa­cje zawarte w tym rozdziale pomogą Ci dostosować serializację do Twoich potrzeb.


Tworzenie serializowalnych klas

Poniżej podane zostały czynności, jakie będziesz musiał wykonać, aby stworzyć klasę nadającą się do serializacji:

1. Jako klasę bazową tworzonej klasy wybierz klasę CObject (lub jedną z jej klas potomnych).

2. Do pliku nagłówkowego klasy dodaj makro DECLARE_SERIAL. Jeśli tworzona klasa korzysta już z któregoś z innych makr (DECLARE_DYNAMIC lub DECLARE_DYNCREATE), umieść makro DECLARE_SERIAL zamiast makra zastosowanego poprzednio.

3. Do definicji klasy dodaj makro IMPLEMENT_SERIAL. Jeśli tworzona klasa korzysta już z któregoś z innych makr (IMPLEMENTJDYNAMIC lub IMPEMENT_DYNCREATE), umieść makro IMPLEMENT_SERIAL zamiast makra zastosowanego poprzednio.

4. Zdefiniuj metodę Serialize, odpowiedzialną za zapisywanie i odczytywanie składowych klasy z archiwum.

Kiedy wykonasz powyższe czynności, będziesz mógł użyć operatorów « oraz » do zapisywania i odczytywania obiektów zdefiniowanej klasy z archiwum.


Określanie okien dialogowych używanych do podawania nazw plików

Wielu programistów lubi tworzyć własne okna dialogowe w swoich aplikacjach. Być może chcesz udostępnić swoim użytkownikom opcje szybkiego podglądu otwieranego pliku, odpowiednio przefiltrować listę plików, które można wybrać, lub wyznaczyć odpowiednią kartotekę, której zawartość zostanie początkowo wyświetlona w oknie dialogowym. Nie jest możliwe, aby w celu wprowadzenia tak nieznacznych modyfikacji, trzeba było tworzyć od podstaw algorytm służący do otwierania plików.

Istnieją dwie proste metody, aby dodać do aplikacji własne okno dialogowe umożliwiające wybranie pliku, który chcesz otworzyć. Pierwszą metodą jest przesłonięcie metody DoPromptFileName w obiekcie aplikacji. Najprostszym sposobem realizacji tej metody jest skopiowanie kodu źródłowego tej metody z pliku DOCMGR.CPP (znajdującego się w źródłach MFC) i zmodyfikowanie go w taki sposób, aby współpracował z Twoją aplikacją i dostarczonym przez Ciebie oknem dialogowym. Niestety, metoda DoPrompt­FileName nie jest metodą wirtualną, a więc będziesz musiał przesłonić także wszystkie metody, które ją wywołują. Są to na szczęście tylko dwie metody: OnFileOpen w obiekcie aplikacji oraz DoSave w obiekcie dokumentu. Będziesz także musiał sko­piować statyczną metodę AppendFilterSuffix. Pełny przykład praktycznego zastoso­wania tej metody możesz znaleźć na Wydruku 2. l.

Alternatywnym rozwiązaniem jest przesłonięcie metody OnFileOpen w obiekcie aplikacji oraz metod OnFileSave i OnFileSaveAs w obiekcie dokumentu. W tych metodach będziesz mógł pobrać nazwę pliku, a następnie przekazać ją do dalszej obsługi w stan­dardowym procesie serializacji (patrz Tabel 2.2 oraz 2.3). Tę metodę najlepiej stosować w sytuacjach, gdy nie chcesz wybierać pliku. Dla przykładu, zastosowanie tej metody byłoby doskonałym rozwiązaniem, gdyby Twój program miał wyświetlać stałą listę 10 plików, które może wybrać użytkownik. Pierwsza metoda jest natomiast najlepsza jeśli w szablonach dokumentów będziesz chciał używać filtrów określających jakie pliki użytkownik będzie mógł wybierać.


Używanie istniejącego lub własnego kodu obsługi plików

Jeśli dysponujesz kodem służącym do odczytywania i zapisywania plików, to jego użycie :w MFC może się okazać bardzo trudne - zwłaszcza, jeśli nie będziesz mógł zmienić formatu w jakim dane są zapisywane. Nie jest to jednak problemem. Chociaż MFC lubi
korzystać z archiwów, to jednak nie wymusza na nas używania ani archiwów, ani serializacji. :

Najprostszym sposobem zdefiniowania własnego kodu do odczytywania i zapisywania plików, jest przesłonienie domyślnych implementacji metod CDocument::OnSaveDocument oraz CDocument::OnOpenDocument. MFC wywołuje te metody przekazując do nich nazwę zapisywanego lub otwieranego pliku. Wewnątrz nich będziesz mógł wykonać dowolne czynności, które będą Ci potrzebne.

Oczywiście domyślne wersje tych metod otwierają wskazany plik, kojarzą go z archiwum i wywołują metodę Serialize. Oznacza to, że jeśli będziesz chciał zmienić ten standardowy sposób działania, nie powinieneś wywoływać tych metod dostępnych w klasach bazowych.


Tworzenie archiwów operujących na niestandardowych strumieniach

Czasami będziesz chciał stworzyć archiwum, które odczytuje i zapisuje dane do strumienia, nie skojarzonego z plikiem. Możesz chcieć, aby archiwum operowało na bazie danych. A nawet jeśli dane będą zapisywane w pliku, możesz chcieć odpowiednio go zaszyfrować lub też umieścić w nim dodatkowe informacje.

Są przynajmniej cztery metody modyfikowania formatu archiwów:

Pobierz wskaźnik do obiektu CFile za pomocą metody GetFile dostępnej w obiekcie archiwum i użyj tego obiektu do bezpośredniej modyfikacji skojarzo­nego z nim pliku. Jeśli archiwum jest aktualnie używane, usuń z niego dane (za pomocą metody Flush) przed pobieraniem wskaźnika do obiektu CFile. Ta metoda jest najlepsza, jeśli dodajesz informacje na samym początku lub końcu pliku.

Wyprowadź nową klasę potomną klasy CFile i zdefiniuj odpowiednie metody służące do zapisywania i odczytywania bajtów, charakterystyczne dla medium, którego chcesz używać. Obiekt dokumentu udostępnia metodę GetFile, którą możesz przesłonić i zwrócić z niej wskaźnik na obiekt odpowiedniego typu. Jest to rozwiązanie dające bardzo duże możliwości, jednakże wymaga dosyć dużo pracy.

Wyprowadź nową klasę potomną klasy CMemFile. Kiedy stworzysz odpo­wiedni plik skojarzony z obszarem pamięci, będziesz mógł w dowolny sposób zmodyfikować umieszczone w nim dane, a następnie zapisać je w wybrany sposób. Także i w tym wypadku, będziesz musiał zwrócić wskaźnik do Twojego obiektu za pomocą metody GetFile. Jest to prosta metoda; jej największą wadą jest to, iż wymusza ona przechowywanie całego pliku w pamięci.

Stwórz nową klasę potomną klasy CArchive i użyj jej zamiast standardowego obiektu. Rozwiązanie to jest dosyć trudne, gdyż klasa CArchive definiuje algoryt­my odczytywania i zapisywania obiektów, a algorytmów tych nie będziesz chciał modyfikować.

Przykłady pobierania obiektu klasy CFile i zastosowania obiektu klasy CMemFile będziesz mógł znaleźć na Wydruku 2.6.


Odczytywanie starszych wersji plików

Ostatnim argumentem makra IMPLEMENT_SERIAL jest numer wersji klasy. MFC zapamiętuje ten numer wraz z pozostałymi informacjami o klasie.

W standardowym procesie obsługi archiwów, jeśli MFC wykryje, że numer wersji pliku nie zgadza się z numerem podanym w makrze IMPLEMENT_SERIAL, zgłaszany jest wyjątek, a proces ładowania zostaje przerwany.

Jednak może się zdarzyć, że będziesz chciał dostosować swój kod do serializacji, tak aby obsługiwał różne wersje danych. Jeśli chcesz zastosować tę metodę, to jako ostatni argument makra IMPLEMENT_SERAIAL będziesz musiał umieścić numer wersji klasy połączony bitowym operatorem OR ( ) z flagą VERSIONABLE_SCHEMA. Podczas odczytywania obiektu z archiwum, jego wersja może zostać określona za po­mocą wywołania metody GetObjectSchema.

Twój kod będzie mógł odpowiednio obsłużyć odczytywany obiekt na podstawie nume­ru jego wersji:

void CXXXClass::Serialize(CArchive& ar)

{

int i ;

CObject: :Serialize (ar) ,-

if (ar.IsLoading())

int version=ar.GetObjectSchema(); if (version>2||version<l)

AfxMessageBox("Nieznany format dokumentu");

return;

ar»m_count ;

for (i=0;i<m_count;i++)

ar»vl_data;

ar»vl_moredata;

if (version>l) ar»v2_data;




Rozdział 3 Drukowanie

Programy wygenerowane przez kreatora App Wizard potrafią automatycznie drukować. Oczywiście, każdy program, który potrafi wyświetlać informacje na ekranie, potrafi także wydrukować te informacje na drukarce. Jednakże czasami otrzymane rezultaty nie są takie, jakich można by oczekiwać. Różnice w skalowaniu, dzielenie wydruku na strony oraz drukowanie dodatkowych informacji (jak na przykład czas wydruku lub nazwa drukowanego pliku), wszystkie te czynniki dodatkowo utrudniają drukowanie. Podgląd wydruku jest kolejnym miejscem, gdzie bardzo często dokonywane są modyfikacje, jed­nakże i tym razem MFC ukrywa szczegóły zastosowanych rozwiązań. W tym rozdziale dowiesz się, jak można dodać do programu możliwość edycji danych podczas wyświetla­nia podglądu wydruku.

Jeśli przeczytałeś ostatni rozdział, to nie będziesz zaskoczony tym, że jestem wielkim fanem serialu “Star Trek". Sądząc z wypowiedzi niektórych osób w telewizji, nie zaliczam się jednak do największych fanów tego serialu. Mój samochód nie przypomina statku kosmicznego i nie ubieram się w uniformy Floty Gwiezdnej. Z drugiej strony, posiadam kopie “Star Trek Technical Manuals" oraz publikacje autorstwa Leonarda Nimoya. Miałem nawet zaszczyt porozmawiać z Genem Roddenberym zaraz po emisji jednego z odcinków serialu. Tak więc może i nie jestem fanatykiem, ale na pewno godnym szacunku fanem.

Zawsze starałem się zainteresować dzieci serialem “Star Trek". Jestem przekonany, że można znaleźć znacznie gorszych bohaterów do Kirka i jego towarzyszy. Sądzę, że bo­haterowie niektórych nowych filmów także mogą zasługiwać na odrobinę szacunku. Ale i tak moje dzieci z założenia nie interesują się rzeczami, które lubię, dlatego też rzadko kiedy mam okazję oglądać “Star Trek" w towarzystwie.

Kiedyś, w rocznicę odkrycia Ameryki, postanowiliśmy pojechać na krótką wycieczkę -coś w stylu mini wakacji na koniec lata. Po spędzeniu całego dnia w muzeum Sea World wpadło nam do głowy przejechać na chwilę granicę meksykańską- tylko i wy­łącznie po to, żebyśmy mogli powiedzieć, że byliśmy w Meksyku. Powiedziałem mojemu najmłodszemu synowi, że jest to dla niego okazja, aby z dumnie podniesioną głową wkroczyć na obszary, w których nikt jeszcze nie przebywał (a przynajmniej, w których ja jeszcze nie przebywałem). Oczywiście, usłyszawszy tekst zaczerpnięty ze “Star Treka", syn przewrócił jedynie oczami i zrobił stosownie znudzoną minę.

Cóż, potwierdziło to jedynie fakt, iż jestem całkowicie niepodobny do wielkiego kapi­tana Kirka. Gdy dojechaliśmy do granicy uświadomiliśmy sobie, że nie mamy żadnego dokumentu identyfikacyjnego dla naszego siedmioletniego syna, Patryka. Pomyśl tylko, czy wozisz przy sobie dokumenty siedmioletniego dziecka? Jeśli wycieczka byłaby za­planowana, to niewątpliwie nie zapomnielibyśmy wziąć odpowiednich dokumentów. Szkoda tylko, że nie pomyśleliśmy o tym przed dojechaniem do granicy meksykańskiej. Bardzo poważnie zastanawialiśmy, się, czy mimo wszystko nie przejechać granicy, ale gdybyśmy to zrobili, Patryk mógłby nie wrócić do kraju!

Po rozmowie z amerykańskim Patrolem Granicznym (mili goście) zdecydowaliśmy się jednak zaryzykować i przekroczyć granicę. Muszę przyznać, że czułem dziwny dresz­czyk emocji, przekraczając granicę ze świadomością, że podejmujemy takie ryzyko. Oczywiście, wiedzieliśmy, że w każdej chwili możemy dowieść obywatelstwa Patryka, niemniej jednak narażaliśmy się na konieczność pozostania w Meksyku przez kilka dni lub nawet tygodni, gdyby coś poszło nie tak.

Wszystko oczywiście skończyło się dobrze -jak w filmie “Star Trek Następne Pokolenia", gdzie wszystkie problemy same się rozwiązują wraz ze zbliżaniem się do końca filmu. Patryk dostał na macado (targu) blaszany bębenek, a kiedy wracaliśmy, na granicy zapytano nas, czy jesteśmy obywatelami amerykańskimi, odpowiedzieliśmy, że jesteśmy i mogliśmy jechać dalej - nawet nie musiałem pokazywać swojego prawa jazdy. Żadnych problemów, żadnych silnych wrażeń.

Sądzę, że czasami programowanie jest bardzo podobne do opisanych powyżej zdarzeń. Jest naturalne, że jesteśmy pełni obaw i ostrożnie wkraczamy na nowe, nieznane terytoria (jak na przykład nowy system operacyjny, nowy język czy technologia). Odkrywanie czegoś nowego również przysparza miłego dreszczyku emocji.

Gdy rozmawiam z programistami używającymi MFC, odkrywam, że wielu z nich obawia się drukowania. Oczywiście każdy może coś wydrukować. Ale jak wydrukować popraw­nie wyskalowany, wielostronicowy dokument? Co z nagłówkami, stopkami, margine­sami albo numerami stron?

Na dobry początek pokażę prosty program do gry w kółko i krzyżyk. Sam program jest całkowicie nieinteresujący (patrz rysunek 3.1.), będzie jednak potrafił poprawnie dru­kować. W dalszej części rozdziału pokażę Ci, w jaki sposób można przejąć kontrolę nad sposobem sporządzania podglądu wydruku i dostosować go do swoich potrzeb. Zanim jednak zaczniemy, to porozmawiamy trochę o narzędziach wspomagających drukowanie, dostępnych w MFC.


Drukowanie w MFC - wielkie kłamstwo?

MFC twierdzi, że możesz rysować i drukować za pomocą tego samego kodu. Jednak czy rozwiązanie takie jest w rzeczywistości praktyczne? Czasami tak. W większości wypadków będziesz jednak musiał stworzyć odrębny kod służący tylko do drukowania. Jedyną pocieszającą wiadomością jest to, że kod, który będziesz musiał napisać, jest znacznie prostszy od tego, który wykorzystywany jest przez sam system Windows.

Kiedy użytkownik wybiera opcję drukowania z menu Plik, w aktywnym widoku wywoływana jest metoda OnPreparePrinting. Metoda ta posiada jeden argument -wskaźnik do struktury CPrintlnfo (patrz Tabela 3.1.). Określenie wartości składowych tej struktury pozwoli Ci na dokładne opisanie sposobu drukowania (zwanego także zada­niem drukowania). Jeśli już na tym etapie znasz ilość stron, które będziesz chciał wydruko­wać, to możesz je podać. Kiedy przekażesz strukturę w wywołaniu metody DoPrepare-Printing, to ustawione w niej dane zostaną odzwierciedlone w standardowym dialogu służącym do wybierania opcji drukowania. Standardowy kod generowany przez kreato­ra App Wizard zakłada, że nie wiesz niczego na temat zadania drukowania i w związku z tym przekazuje strukturę CPrintlnfo bez wprowadzenia jakichkolwiek modyfikacji.

Aby uruchomić zadanie drukowania, MFC wywołuje metodę OnBeginPrinting. Metoda ta wymaga podania dwóch argumentów: wskaźnika na obiekt klasy CDC oraz wskaźnika na strukturę CPrintlnfo. Obiektu CDC możesz użyć do określenia cech drukarki (wystar­czy w tym celu wywołać metodę GetDeviceCaps). Jest to Twoja ostatnia szansa określenia ilości drukowanych stron; możesz to zrobić za pomocą metody CPrintlnfo::SetMaxPage. Jeśli ilość stron wydruku zależna jest od cech charakterystycznych drukarki, to właśnie teraz powinieneś ją określić. Dzięki temu MFC będzie mogło wywołać metody służące do drukowania tylko raz dla każdej strony wydruku. Jeśli jednak wciąż nie jesteś w sta­nie określić ilości stron, które chcesz wydrukować, to będziesz musiał ręcznie określać wartość składowej m_bContinuePrinting struktury CPrintlnfo. Modyfikacji tej bę­dziesz musiał dokonywać w metodzie OnPrepareDC (jak się już wkrótce dowiesz, metoda ta wywoływana jest podczas drukowania).

Metoda OnBeginPrinting najlepiej nadaje się do przydzielenia zasobów, które będą Ci potrzebne podczas wykonywania zadania drukowania. Dla przykładu, specyficzne czcionki używane podczas drukowania przygotowywane są zazwyczaj właśnie w tej metodzie.

Tabela 3.1. Struktura CPrintlnfo.

Składowa Typ Opis

m_bDocObject zmienna Określa, czy drukowany jest dokument klasy CDocument.

m_dwFlags zmienna Określa operacje wykonywane przez obiekt DocObject.

m_nOffsetPage zmienna Określa przesunięcie pierwszej strony konkretnego obiektu DocObject w połączonym zadaniu drukowania.

m_pPD zmienna Wskaźnik na okno dialogowe Drukuj skojarzone z konkretnym zadaniem drukowania.

m_bDirect zmienna Określa ,czy dokument ma zostać wydrukowany bez wyświetlania okna dialogowego Drukuj.

m_bPreview zmienna Określa, czy program działa aktualnie w trybie podglądu wydruku.

m_bContinuePrinting zmienna Określ, czy aktualna strona powinna zostać wydrukowana (patrz tekst).

m_nCurPage zmienna Numer aktualnej strony.

m_nNumPreviewPages zmienna Ilość stron wyświetlanych w podglądzie wydruku (l lub 2).

m_lpUserData zmienna Wskaźnik do struktury danych dostarczanej przez użytkownika.

m_rectDraw zmienna Prostokąt określający wielkość obszaru strony dostępnego do drukowania (składowa ta może być używana dopiero po rozpoczęciu drukowania).

m_strPageDesc zmienna Zawiera łańcuch znaków określający postać nume­racji stron.

SetMinPage funkcja Określa numer pierwszej strony wydruku,

SetMaxPage funkcja Określa numer ostatniej strony wydruku,

GetMinPage funkcja Zwraca numer pierwszej strony wydruku,

GetMaxPage funkcja Zwraca numer ostatniej strony wydruku.

GetOffsetPage funkcja Zwraca ilość stron poprzedzających pierwszą stron? aktualnie drukowanego obiektu DocObject, wchodzącego w skład złożonego zadania drukowania.

GetFromPage funkcja Zwraca numer pierwszej drukowanej strony,

GetToPage funkcja Zwraca numer ostatniej drukowanej strony.

Podczas drukowania MFC wywołuje następujące trzy metody:

  1. OnPrepareDC;

2. OnPrint;

3. OnDraw.

Metody te wywoływane są dla każdej drukowanej strony. Zauważ, że metody OnPrepareDC oraz OnDraw wywoływane są także podczas rysowania na ekranie. Jeśli będziesz chciał określić, czy metoda została wywołana jako część procesu drukowania, to będziesz mógł to zrobić za pomocą metody CDC::IsPrinting.

Jest kilka powodów, dla których mógłbyś chcieć przesłonić standardową definicję metody OnPrepareDC. Po pierwsze możesz w ten sposób zmodyfikować kontekst urządzenia. Możesz to zrobić na przykład po to, aby zmienić początek układu współrzędnych widoku, dzięki czemu będziesz w stanie wydrukować odpowiednią stronę. Możesz zmienić tak­że tryb mapowania w kontekście urządzenia, dzięki czemu wydruk zostanie przeskalowany inaczej niż zawartość ekranu.

Możesz także podejmować decyzje, czy chcesz kontynuować drukowanie, czy też je prze­rwać. Jest to szczególnie przydatne w wypadkach, gdy nie jesteś w stanie dokładnie określić ilości stron, które chcesz wydrukować. Aby móc postąpić w ten sposób, będziesz musiał upewnić się, że argument pInfo ma wartość różną od NULL. Pamiętaj, że metoda OnPre­pareDC wywoływana jest zarówno w przypadku drukowania, jak i rysowania na ekranie. Jeśli argument pInfo nie ma wartości NULL, to będziesz mógł określić, czy chcesz dalej drukować, czy nie (badając wartość składowej m_nCurPage). Jeśli będziesz chciał konty­nuować drukowanie, to po wywołaniu metody OnPrepareDC klasy bazowej będziesz mu­siał przypisać składowej m_bContinuePrinting wartość TRUE. Metoda OnPrepareDC klasy bazowej zawsze przypisuje tej składowej wartość FALSE, a więc pamiętaj, aby od­powiednio ją zmodyfikować. Pamiętaj także, iż zawsze będziesz musiał wywołać tę metodę klasy bazowej, gdyż wiele rodzajów widoków używa jej do specjalnych celów.


Drukowanie

Proste programy zazwyczaj nie przejmują się takimi szczegółami. Dzieje się tak dlatego, iż metoda OnDraw (używana do drukowania widoku i wyświetlania go na ekranie), potrafi automatycznie zadbać o wszystkie sprawy związane z drukowaniem. Jednakże musisz poznać wszystkie tajniki procesu drukowania, aby móc go zmodyfikować - odpowiednio przeskalować widok, dodać nagłówki i stopki wydruku lub w jakikol­wiek inny sposób zmodyfikować proces drukowania.

Jeśli chcesz, aby kod metody OnDraw obsługiwał zarówno wyświetlanie danych na ekranie, jak i ich drukowanie, to nie będziesz musiał przesłaniać metody OnPrint. Do­myślna implementacja tej metody wywołuje metodę OnDraw za Ciebie. Jednakże jeśli będziesz chciał dodać specjalny kod, który modyfikuje postać wydruku i powoduje, że różni się ona od wyglądu informacji wyświetlanych na ekranie, to będziesz mógł dokonać odpowiednich modyfikacji właśnie w tej metodzie. Dla przykładu załóżmy, że tworzysz program wyświetlający na ekranie schemat sieci, jednakże podczas drukowania zamiast schematu tworzona jest tabela zawierająca zestawienie połączeń pomiędzy poszczegól­nymi węzłami sieci. Schemat sieci będzie w takim przypadku tworzony w metodzie OnDraw, tabela z zestawieniem połączeń - w metodzie OnPrint. Innym zastosowa­niem metody OnPrint jest wykorzystanie jej do wydrukowania elementów, które mają się pojawić tylko i wyłącznie na wydruku. Dla przykładu bardzo częstym rozwiązaniem jest tworzenie w tej metodzie nagłówków i stopek wydruku. Po ich stworzeniu wystar­czy zmienić początek układu współrzędnych widoku, aby zapobiec przesłonięciu na­główka przez kod używany przy wyświetlaniu informacji na ekranie. Możesz także zmody­fikować wielkość obszaru przycinania, aby zapobiec przesłonięciu stopki wydruku. Kiedy skończysz wprowadzać modyfikacje, będziesz mógł wywołać metodę OnDraw, aby wydrukowała ona resztę zawartości strony.

Oczywiście istnieje także alternatywne rozwiązanie, które polega na umieszczeniu kodu odpowiadającego za tworzenie nagłówków i stopek w kodzie metody OnDraw. Kod ten mógłby być wykonywany tylko wtedy, gdy metoda IsPrinting zwróci wartość TRUE. Wybór jednego z tych dwóch rozwiązań jest jedynie kwestią osobistych preferencji.

Metoda OnDraw odpowiada za wyświetlanie informacji na ekranie. Jeśli w odpowiedni sposób obsłużysz kontekst urządzenia w wywołaniach wcześniejszych metod, to w metodzie OnDraw nigdy nie będziesz musiał się interesować tym, czy prezentowane przez nią informacje kierowane są na ekran, czy na drukarkę. Zawsze jednak możesz skorzystać z metody IsPrinting, aby rozróżnić obie te sytuacje.

MFC cyklicznie wywołuje przedstawione powyżej trzy metody dla każdej strony wy­druku. Ilość stron wydruku określana jest na podstawie informacji zapisanych w struk­turze CPrintlnfo (lub też na podstawie Twoich operacji na składowej m_bContinuePrinting). Przed zakończeniem drukowania wywoływana jest metoda OnEndPrinting. W metodzie tej będziesz mógł zwolnić wszystkie zasoby przydzielone na potrzeby drukowania (w metodzie OnBeginPrinting).

Tak wygląda zarys procesu drukowania stosowanego przez MFC. Cały proces przed­stawiony został w Tabeli 3.2. Najciekawsze jest jednak to, że MFC używa dokładnie tego samego procesu do tworzenia podglądu wydruku. Jeśli drukowanie będzie działało poprawnie, to automatycznie uzyskasz poprawnie działający podgląd wydruku.


Tabela 3.2. Proces drukowania.

MFC wywołuje Powód Przesłoń gdy

CView::OnFilePrint wybór opcji z menu wszystko chcesz zrobić samemu

CView::OnPreparePrinting rozpoczęcie procesu drukowania chcesz wpisać odpowiednie

informacje w okno dialogowe Drukuj

CView::DoPreparePrinting wyświetlenie okna dialogowego Drukuj chcesz wyświetlić

własne okno dialogowe Drukuj

CView::OnBeginPrinting przydzielenie zasobów chcesz jeden raz przydzielić

zasoby GDI potrzebne do sporządzenia

wydruku

CView::OnPrepareDC określenie kontekstu urządzenia chcesz przydzielić zasoby lub

zmodyfikować kontekst urządzenia

CView::OnPrint wykonanie drukowania chcesz wydrukować widok różniący się

od widoku wyświetlanego na ekranie lub jeśli chcesz wydrukować dodatkowe informacje

CView::OnDraw odświeżenie ekranu chcesz coś wydrukować (prawie

zawsze)

Cview::OnEndPrinting zakończenie drukowania musisz zwolnić przydzielone wcześniej

zasoby GDI


Dylemat

W idealnej sytuacji powinieneś być w stanie użyć do drukowania dokładnie tego samego kodu, którego używasz do wyświetlania informacji na ekranie. W praktyce jednak ist­nieje kilka czynników, które poważnie utrudniają takie postępowanie. Najważniejszym z tych powodów jest skalowanie. Rozdzielczość ekranu oraz rozdzielczość drukarki są zazwyczaj zupełnie inne. Na ekranie linia o długości 100 pikseli będzie całkiem długa. Jednakże ta sama linia będzie bardzo krótka na drukarce o rozdzielczości 600 DPI (punktów na cal). Coś, co na ekranie ma rozsądne rozmiary, po wydrukowaniu będzie znacznie mniejsze, no chyba, że podejmiesz odpowiednie środki zaradcze.

Istnieją dwa sposoby rozwiązania tego problemu. Jednym z nich jest zastosowanie odpo­wiedniego trybu mapowania. Aby zastosować to rozwiązanie, wywołaj metodę CDC::SetMapMode, która pozwala Ci na wybranie trybu mapowania wykorzystującego jednostki logiczne. Dla przykładu, jeśli wybierzesz tryb MM_LOMETRIC, to podstawową jednostką będzie 0,1 mm (patrz Tabela 3.3.). Oczywiście wielkość ekranu nie będzie w ta­kim trybie bardzo precyzyjna, gdyż sterownik nie zna jego dokładnej rozdzielczości. Większość sterowników celowo zwiększa wymiary po to, aby małe elementy (na przykład, wielkości 10 pikseli) były łatwiej zauważalne. Jednakże drukarki dysponują precyzyjnie określoną ilością punków na cal, w związku z czym wymiary będą przeliczane bardzo dokładnie.

Tabela 3.3. Tryby mapowania.

Text Opis

MM_TEXT l jednostka logiczna == l piksel.

MM_HIENGLISH l jednostka logiczna == 0,001 cala.

MM_LOENGLISH l jednostka logiczna == 0,01 cala.

MM_HIMETRIC l jednostka logiczna == 0,01 milimetra.

MM_LOMETRIC l jednostka logiczna == 0,1 milimetra.

MM_TWIPS l jednostka logiczna == 1/1440 cala (lub 1/20 punktu drukarskiego).

Używa funkcji SetWindowExt oraz SetViewportExt do wyskalowania osi X i Y, zachowując przy tym stosunek wielkości krawędzi okna; okrąg w jednostkach logicznych będzie wyświetlony jako okrąg na ekranie.

Używa funkcji SetWindowExt oraz SetViewportExt do nadania podanych wielkości osiom X i Y; okrąg w jednostkach logicznych zostanie wyświetlony na ekranie jako elipsa, chyba że ręcznie przeliczysz współczynnik skalowania, tak aby zachować proporcje okręgu.

Stosowanie trybów mapowania

Wszystkie tryby mapowania, za wyjątkiem MM_TEXT, znacznie ułatwiają drukowanie, dlatego też zawsze warto ich używać. Należy jednak pamiętać, iż we wszystkich tych trybach (prócz MM_TEXT) oś Y jest odwrócona. Innymi słowy, należy używać liczb ujemnych jako współrzędnych tej osi (chyba że samemu przesunąłeś początek układu współrzędnych).

Oczywiście zdarzają się i takie wypadki, gdy zastosowanie pikseli jest wygodniejsze od użycia jednostek logicznych. Dobrym tego przykła­dem jest przedstawiony w tym rozdziale program do gry w kółko i krzy­żyk. Nie jest łatwo myśleć o planszy w milimetrach lub calach dlatego, że wymiary planszy ulegają zmianie wraz ze zmianami wielkości okna.

Domyślnym trybem mapowania jest MM_TEXT, w którym jednej jednostce logicznej odpowiada jeden piksel. W tym trybie kierunkiem poziomym jest oś X, a jednostki rosną od strony prawej do lewej. Współrzędne osi pionowej (Y) rosną od góry ku dołowi. Inne tryby mapowania (takie jak MM_LOENGLISH) odwracają kierunek osi Y. W ten sposób oś Y rozpoczyna się u góry kontekstu urządzenia, a współrzędne rosną od dołu ku gó­rze. Oznacza to, że wszystkie wartości, które będziesz stosował, będą wartościami ujemnymi! Oś X pozostaje w tym trybie niezmieniona. Te domyślne ustawienia możesz zmodyfikować za pomocą metody CDC::SetViewportOrg.

Choć może użycie tych samych układów współrzędnych dla wyświetlania danych na ekranie i drukowania ich może się wydawać bardzo atrakcyjnym rozwiązaniem, to jest jednak wiele przypadków, w których przysparza ono tyle samo kłopotów ile ich rozwiązuje, Czasami nie chcesz wyświetlać danych na ekranie, licząc wszystko w centymetrach i calach. Współrzędne dotyczące manipulacji myszką są zawsze podawane w pikselach. Może się także zdarzyć, że będziesz chciał, aby prezentowane dane zajmowały zawsze cały ekran oraz stronę wydruku, niezależnie od ich wielkości. W takich przypadkach logicz­ne tryby mapowania nie są zbytnio przydatne.

Jeśli mimo wszystko będziesz chciał pozostać przy stosowaniu pikseli, to najprawdopo­dobniej przyda Ci się wyskalowanie kontekstu drukarki w taki sposób, aby jednostki na ekranie i drukarce były podobne. Do wykonania tego zadania najlepiej nadają się meto­dy OnPrepareDC oraz OnPrint. Kod odpowiedzialny za skalowanie możesz także umieścić w metodzie OnDraw. W takim wypadku będziesz musiał sprawdzać, czy trwa proces drukowania (za pomocą metody IsPrinting), a jeśli tak, wykonać odpowiednie przeskalowanie. Wszystko zależy od tego, jak wiele metoda OnDraw ma wiedzieć o tym, czy drukuje, czy wyświetla dane na ekranie.


Pełny przykład drukowania

Rozpatrzmy przykład programu przedstawionego na Listingu 3.1. Jest to program do gry w kółko i krzyżyk pozwalający na wydrukowanie planszy z rozgrywką. Jedynymi ciekawszymi elementami jakie zastosowałem w tym programie są: winietka wyświetlana podczas uruchamiania programu oraz umieszczony na pasku stanu komponent przed­stawiający aktualny czas (umieszczony za pomocą Galerii Komponentów wywoływanej po wybraniu opcji Insert^Componeni).

Listing 3.1. Drukowanie widoku.

// mfctttView.cpp


ttinclude "mfctttDoc.h" ttinclude "mfctttview.h"

ttifdef _DEBUG

tdefine new DEBUG_NKW

ttundef THIS_FILE

static char THIS_FILE[] =

ttendif

/ / /1111111111111 /1111111111 /1111 II CMfctttYiew

IMPLEMENT_DYNCREATE(CMfctttYiew, CYiew)

BEGIN_MESSAGE_MAP(CMfctttYiew, CYiew)

//{{AFX_MSG_MAP(CMfCtttYiew)

ON_WM_LBUTTONDOWN()

ON_COMMAND(ID_EDIT_UNDO, OnEditUndo)

ON_UPDATE_COMMAND_UI (ID_EDIT_UNDO, OnUpdateEdi tUndo)

//}}AFX_MSG_MAP

// Standard printing commands

ON_COMMAND(ID_FILE_PRINT, CYiew::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_DIRECT, CYiew::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_PREVIEW, CYiew::OnFilePrintPreview) END_MESSAGE_MAP ()

11111111111111111111111111111111

II CMfctttYiew construction/destruction

CMfctttYiew::CMfctttYiew()

// TODO: add construction code here

CMfctttView: :-CMfctttView()

{

}

BOOL CMfctttView: :PreCreateWindow(CREATESTRUCT& es) {

return CView: :PreCreateWindow(cs) ;

/ / 1 1 1 1 1 1 1 1 1 1 / 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 II CMfctttYiew drawing

// Odstęp od krawędzi pola planszy ttdefine OFFSET 15

void CMfctttYiew: :OnDraw(CDC* pDC) {

CMfctttDoc* pDoc = GetDocument ( ) ASSERT_VALID(pDoc) ; // Stwórz panele

// Pióro linii planszy

CPen p(PS_SOLID, 10, RGB (O, O, 0) ) ; // X pióro

CPen xp(PS_SOLID,5,RGB(OxFF,O,0)); //O pióro

CPen op(PS_SOLID, 5,RGB(0, O, OxFF) ) ,-

CPen *old;

// rysuj plansze

CRect r;

GetClientRect(&r);

old=pDC->SelectObject(&p);

// Poniżej podałem przykładowy kod testujący // umożliwiający sprawdzenie czcionek //CFont f,*oldf;

//f.CreateFont(-30,O,O,O,O,O,O,O,O,O,O,O,O,"Arial" ); //oldf=pDC->SelectObject(&f); //pDC->TextOut(0,0,"Hello"); //pDC->SelectObject(oldf);

// Rysuj siatkę

pDC->MoveTo(r.right/3,0);

pDC->LineTo(r.right/3,r.bottom);

pDC->MoveTo(2*r.right/3,0);

pDC->LineTo(2*r.right/3,r.bottom);

pDC->MoveTo(0,r.bottom/3);

pDC->LineTo(r.right,r.bottom/3);

pDC->MoveTo(O,2*r.bottom/3);

pDC->LineTo(r.right,2*r.bottom/3); // Rysuj klocki

for (int x=0;x<3;x++)

for (int y=0;y<3;y++)

CPoint pt(x,y);

CPoint ptl(x+l,y+l);

GridToMouse(pt);

GridToMouse(ptl);

switch (pDoc->GetBoardState(x,y))

case X:

pDC->SelectObject(&xp);

pDC->MoveTo(pt.x+OFFSET,pt.y+OFFSET);

pDC->LineTo(ptl.x-OFFSET,ptl.y-OFFSET);

pDC->MoveTo(ptl.x-OFFSET,pt.y+OFFSET);

pDC->LineTo(pt.x+OFFSET,ptl.y-OFFSET);

break; case O:

pDC->SelectObject(&op);

pDC->SelectStockObject(HOLLOW_BRUSH) ;

pDC->Ellipse(pt.x+OFFSET,pt.y+OFFSET, ptl.x-OFFSET,ptl.y-OFFSET);

break;

pDC->SelectObject(old); }

// CMfctttView printing

BOOL CMfctttYiew::OnPreparePrinting(CPrintlnfo* plnfo) {

p!nfo->SetMaxPage(2);

return DoPreparePrinting(plnfo);

// Ta wersja metody OnPrint pozwala Ci na użycie trybu MM_TEXT w Twoim // widoku, dzięki czemu wydruk może być poprawnie wyskalowany void CMfctttView::OnPrint(CDC* pDC, CPrintlnfo* plnfo) {

CDC *dc=GetDC();

CString s;

int x=dc->GetDeviceCaps(LOGPIXELSX) ,-

int y=dc->GetDeviceCaps(LOGPIXELSY);

int xl=pDC----GetDeviceCaps(LOGPIXELSX) ;

int yl=pDC->GetDeviceCaps(LOGPIXELSY);

// nagłówek wyświetlaj jedynie podczas drukowania

s.Format("Tic Tac Toe gamę: %s",

GetDocument()->GetTitle() ) ; pDC->TextOut(O,O,s); pDC->MoveTo(0,75) ;

pDC->LineTo(pinfo->m_rectDraw.Width(),75); if (pinfo->m_nCurPage==2) {

CMfctttDoc *doc=GetDocument(); s.Format("Games I Won=%d, Games I Lost=%d" " Draw Games=%d",doc->wins,doc->loss, doc->draw);

pDC->TextOut (O, 100, s) ,-return; }

// Zmień tryb mapowania tak, aby piksele były poprawnie wyskalowane pDC->SetMapMode(MM_ISOTROPIC) ; pDC->SetWindowExt(x,y); pDC->SetViewportExt(xl,yl); // Górny margines wielkości 100 pikseli pDC->SetViewportOrg(0,100); CView::OnPrint(pDC, plnfo);

void CMfctttView: :OnBeginPrinting (CDC* /*pDC*/,

CPrintlnfo* /*plnfo*/) {

// TODO: add extra initialization before printing

void CMfctttView: :OnEndPrinting(CDC* /*pDC*/,

CPrintlnfo* /*plnfo*/) {

// TODO: add cleanup after printing

111 /1! 1111111111111111111111111 / II CMfctttView diagnostics

łtifdef _DEBUG

void CMfctttView::AssertValid() const

{

CView::AssertValid();

void CMfctttView::Dump(CDumpContext& dc) const {

CView::Dump(dc); } CMfctttDoc* CMfctttView::GetDocumentO

ASSERT(m_pDocument->IsKindOf( RUNTIME_CLASS(CMfctttDoc)));

return (CMfctttDoc*)m_pDocuinent; } #endi£ //_DEBUG

1111111111111111 /111111111111111 II CMfctttView message handlers

// Skonwertuj współrzędne myszy na współrzędne siatki

void CMfctttYiew::MouseToGrid(CPoint &pt>

{

CRect r;

GetClientRect(&r);

pt.x/=r.right/3;

pt.y/=r.bottom/3;

// Skonwertuj współrzędne siatki na współrzędne myszy

void CMfctttView: :GridToMouse (CPoint &pt)

{

CRect r;

GetClientRect (&r) ;

pt .x*=r . right/3, •

pt.y*=r .bottom/3;

void CMfctttView: :OnLButtonDown(UINT nFlags, CPoint point) {

CMfctttDoc *doc=GetDocument ( ) ;

// Skonwertuj do pozycji siatki MouseToGrid (point) ;

// Jeśli pole planszy nie jest puste, to zasygnalizuj i pomiń ruch

if (doc->GetBoardState (point .x, point .y) !=EMPTY) {

MessageBeep(MB_ICONEXCLAMATION) ; return; }

// Pusty - umieść w polu X lub O, w zależności od numeru ruchu // Uwaga: ten kod został umieszczony w celach testowych, // komputer zawsze wyświetla O, dlatego ten ruch zawsze musi // wyświetlać X

doc->SetBoardState (point .x, point .y,

(doc->turn&l)?0:X) ; doc->turn++;

doc->UpdateAHViews(NULL) ; doc->Play(); // Wykonaj ruch

void CMfctttYiew: :OnEditUndo O {

CMfctttDoc *doc=GetDocument ( ) ;

doc->undo (TRUE) ;

void CMfctttYiew: :OnUpdateEditUndo (CCmdUI* pCmdUI) {

CMfctttDoc *doc=GetDocument () ;

pCmdUI->Enable(doc->undo( FALSE) ) ;

Najbardziej interesującą częścią programu jest kod odpowiedzialny za drukowanie. W kodzie przedstawionym na Listingu 3.1. możesz pominąć metodę OnPrint lub wewnątrz niej odwołać się bezpośrednio do tej samej metody klasy bazowej. Jeśli tak zrobisz, to przekonasz się, że pomimo wszystko zarówno drukowanie, jak i podgląd wydruku działają poprawnie. Jednakże wydrukowana plansza będzie bardzo mała w porównaniu z planszą widoczną na ekranie.

Jest kilka możliwości rozwiązania tego problemu. Po pierwsze mógłbyś rysować planszę w trybie mapowania innym niż MM_TEXT. Spowoduje to jednak powstanie nowego problemu związanego z tym, iż plansza powinna zmieniać swoje wymiary wraz ze zmianami wielkości okna programu. Znacznie łatwiej jest modyfikować wielkość plan­szy w trybie mapowania MM_TEXT. Najprostszym rozwiązaniem jest przeskalowanie kontekstu drukarki w taki sposób, aby linie zajmujące cały ekran przebiegały także przez całą stronę wydruku.

Poniżej przedstawiona została uproszczona wersja kodu realizującego takie przeskalowanie w metodzie OnPrint (wersję pełną znajdziesz na Listingu 3.1.):

void CMfctttYiew::OnPrint(CDC* pDC, CPrintlnfo* plnfo)

CDC *dc=GetDC();

int x=dc->GetDeviceCaps(LOGPIXELSX);

int y=dc->GetDeviceCaps(LOGPIXELSY);

int xl=pDC->GetDeviceCaps(LOGPIXELSX);

int yl=pDC->GetDeviceCaps(LOGPIXELSY);

// modyfikacja trybu mapowania

pDC->SetMapMode(MM_ISOTROPIC);

pDC->SetWindowExt(x,y);

pDC->SetViewportExt(xl,yl);

CView::OnPrint(pDC,plnfo);

W pierwszej linii metody pobierany jest wskaźnik do kontekstu urządzenia (DC) skoja­rzonego z widokiem. Kontekst urządzenia, przekazywany jako argument wywołania metody (pDC), odnosi się do drukarki. Kolejne cztery linie kodu określają ilości pikseli przypadające na cal w każdym z kontekstów (zarówno w pionie, jak i w poziomie). Kolejnym krokiem jest przejście do trybu mapowania MM_ISOTROPIC i ustawienie rozmiarów tak, aby cal na ekranie odpowiadał mniej więcej calowi na wydruku. Przy okazji powinieneś zapamiętać, że po przejściu do trybu mapowania MM_ISOTROPIC powinieneś w pierwszej kolejności określić wymiary okna, a dopiero potem wymiary widoku. Po wykonaniu powyższego kodu narysowanie linii o długości x jednostek w kontekście drukarki spowoduje wydrukowanie linii o długości xl jednostek fizycznych.

Stosując ten lub inny, podobny sposób postępowania, musisz wiedzieć o jeszcze jednej istotnej rzeczy. Jeśli zmienisz używany tryb mapowania w opisany powyżej sposób, to nie będziesz mógł korzystać z domyślnych czcionek. Kod odpowiedzialny za rysowanie będzie musiał stworzyć czcionki po zmianie trybu mapowania. W przeciwnym wypadku wyniki drukowania nie będą zbyt atrakcyjne.

Kod przedstawiony na Listingu 3.1., oprócz samej planszy, drukuje także nagłówek (w postaci tekstu i poziomej linii - patrz rysunek 3.2.). W przykładzie użyta została meto­da SetViewportOrg, dzięki czemu metoda OnDraw rozpocznie rysowanie 100 jednostek poniżej początku strony, robiąc w ten sposób odpowiednio dużo miejsca dla nagłówka.

W MFC algorytm używany do drukowania wykorzystywany jest także przy tworzeniu podglądu wydruku, dzięki czemu ten sam kod obsługuje obie te czynności. Przy takim rozwiązaniu jedynym problemem pozostaje podział wydruku na strony. Jeśli wieś/, jaka będzie maksymalna ilość stron już podczas wywoływania metody OnPreparePrinting, to będziesz ją mógł od razu podać. W przeciwnym razie powinieneś ją określić podczas wykonywania metody OnBeginPrinting. Możesz także dokonywać zmian w podziale dokumentu na strony podczas jego drukowania - sprawdzając numer drukowanej strony przechowywany w strukturze CPrintlnfo. Jeśli będziesz chciał wydrukować stronę, to nadaj wartość TRUE składowej m_bContinuePrinting struktury CPrintlnfo. Przykłado­wy program na drugiej stronie wydruku wyświetla statystyki gry. Zauważ, że zastosowana została tu pierwsza omówiona metoda, a cały kod odpowiedzialny za wydrukowanie statystyki umieszczony jest w metodzie OnPrint.


Dostosowywanie podglądu wydruku do własnych potrzeb

Pamiętam, że gdy byłem dzieckiem fascynowało mnie struganie. Podejrzewam, że dzieci w dzisiejszych czasach już nie strugają (oczywiście nie oznacza to wcale, że nie mają noży, o czym łatwo można się przekonać oglądając informacje w telewizji). Nic zrozum mnie źle - nigdy nie byłem dobry w struganiu, chociaż zawsze chciałem być.

Niezależnie od tego, co zaczynałem strugać, to i tak w końcu wychodził z lego mały totem. W ten sposób udało mi się zrobić wiele całkiem fajnych totemów. Te same problemy miałem na zajęciach praktycznych w szkole średniej. Pewnie bym je oblał, gdyby nie część poświęcona elektryczności. Trochę gorzej szło mi z materiałami plastycznymi. Zaczynałem robić krzyż, ale w efekcie i tak wychodziło z tego coś przypominającego cyfrę “7". Całe szczęście, że instruktor nie pytał, co robimy, aż do momentu, gdy przychodziło się do niego z ukończonym dziełem.

Doświadczenia te nauczyły mnie dwóch rzeczy:

Powinienem trzymać się jak najdalej od noży i innych narzędzi;

Zazwyczaj łatwiej jest zmniejszać rzeczy, niż je powiększać.

To ostatnie stwierdzenie można także z powodzeniem zastosować do podglądu wydru­ku. W stosunkowo łatwy sposób można bowiem ograniczyć możliwości podglądu wy­druku i zmusić go, żeby robił tylko to, co chcemy. Znacznie trudniej jest dodać do niego nowe możliwości. W dalszej części rozdziału pokażę, jak można zrobić obie te rzeczy.

W celu przedstawienia możliwości dostosowywania podglądu wydruku do własnych potrzeb napisałem bardzo prosty program umożliwiający łączenie punktów na ekranie (patrz rysunek 3.3.). Gdy klikniesz lewym przyciskiem myszy, to program narysuje linię prostą łączącą ostatnio narysowany punkt z miejscem, w którym aktualnie umieszczony jest wskaźnik myszy. Gdy klikniesz prawym przyciskiem myszy, to program nie rysuje nowej linii, a jedynie zaznacza na ekranie nowy punkt, od którego zostanie rozpoczęta następna linia. Jak widać, program nie jest zbytnio skomplikowany, a mimo to doskonale będzie się nadawał na przykład prezentujący drukowanie.

Gdy kiedyś będziesz miał okazję pisania programu takiego jak len, to pierwszą rzeczą, na którą zwrócisz uwagę, będzie to, że opcja podglądu wydruku dostarczana przez MFC wykonuje za Ciebie “kawał porządnej roboty". Dla przykładu, MFC automatycznie obsłu­guje wyświetlanie dokumentów zawierających więcej stron, jak również pozwala na wyświetlanie dwóch stron jednocześnie.


Dostosowywanie podglądu wydruku

Dostosowanie podglądu wydruku do własnych potrzeb jest dość prostym zadaniem. Wszystko, co będziesz musiał zrobić, ogranicza się bowiem do usunięcia przycisków, których nie chcesz używać. Oczywiście, kod obsługujący odpowiednie polecenia cały czas jest gdzieś tam wewnątrz MFC. Ale co Ci to przeszkadza? Jeśli użytkownik nie może uruchomić kodu, to tak, jak gdyby w ogóle tego kodu nie było.

Cała sztuczka polega na przejęciu kontroli nad metodą OnFilePrintPreview. Standar­dowo, kreator App Wizard dodaje makro definiujące procedurę obsługi tego polecenia do mapy obsługi komunikatów widoku. Makro to jest jednak umieszczane poza ko­mentarzami oznaczającymi komunikaty obsługiwane przez kreatora Class Wizard, a do obsługi polecenia używana jest metoda klasy bazowej. Dlatego też nie jesteś w stanie zobaczyć ani kodu obsługi polecenie wyświetlania podgląd wydruku, ani odpowiednie­go elementu mapy obsługi komunikatów.

Naszym pierwszym krokiem będzie przeniesienie makra O1SL.COMMAND, zawierają­cego metodę OnFilePrintPreview, do obszaru makr zarządzanych przez kreatora Class Wizard. Kolejny krok to usunięcie operatora zakresu (często ma on postać: CView::) umieszczonego przed nazwą metody OnFilePrintPreview, dodanie deklaracji tej metody do pliku nagłówkowego i zdefiniowanie jej w pliku CPP. Po zakończeniu tych czynności mapa komunikatów Twojego widoku powinna przypominać tę przedstawioną poniżej.

BEGIN_MESSAGE_MAP(CConndotView, CView)

//{{AFX_MSG_MAP(CConndotYew)

ON_WM_LBUTTONDOWN()

ON_WM_RBUTTONDOWN ( )

ON_COMMAND(ID_FILE_PRINT_PREVIEW, OnFilePrintPreview)

//}}AFX_MSG_MAP

//Standard printing comments

ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint) END_MESSAGE_MAP()

Oryginalna wersja kodu metody OnFiIePrintPreview ma następującą postać (możesz ją odnaleźć w pliku VIEWPREV.CPP, w kodzie źródłowym MFC):

void CView::OnFilePrintPreview()

{

// In derived classes, implement special window handling here // Be surę to Unhook Frame Window close if hooked.

// must not create this on the frame. Must outlive this function CPrintPreviewState* pState = new CPrintPreviewState;

// DoPrintPreview's return value does not necessarily indicate that

// Print preview succeeded or failed, but rather what actions

// arę necessary at this point. If DoPrintPreview returns TRUE,

// it means that OnEndPrintPreview will be (or has already been)

// called and the pState structure will be/has been deleted.

// If DoPrintPreview returns FALSE, it means that

// OnEndPrintPreview WILL NOT be called and that cleanup,

// including deleting pState must be done here.

if (!DoPrintPreview(AFX_IDD_PREVIEW_TOOLBAR, this, RUNTIME_CLASS(CPreviewView), pState)) {

// In derived classes, reverse special window handling // here for Preview failure case

TRACEO("Error: DoPrintPreview failed.\n"); AfxMessageBox(AFX_IDP_COMMAND_FAILURE); delete pState; // preview failed to initialize, // delete State nów

Jest oczywiste, że wszystkie najważniejsze czynności muszą być wykonywane w metodzie DoPrintPreview. Jeśli z metody OnFilePrintPreview usuniesz niepotrzebne komenta­rze i makra używane przy testowaniu, to okaże się, że cała metoda ma niewiele więcej niż 4 linie kodu. Kluczem do dostosowania podglądu wydruku do swoich potrzeb jest metoda DoPrintPreview.

Przyjrzymy się teraz czterem argumentom wywołania tej metody. Pierwszym z nich jest identyfikator zasobu zawierającego pasek narzędzi podglądy wydruku. Zmiana tego za­sobu spowoduje automatyczną zmianę paska narzędzi. Proste, prawda? W zasadzie mógłbyś stworzyć zupełnie nowy pasek narzędzi, jednakże znacznie łatwiej jest posłu­żyć się oryginalnym paskiem, który przechowywany jest w pliku AFXPR1NT.RC (plik ten z niewiadomych powodów umieszczony został w kartotece MFC/INCLUDE}. Po skopiowaniu oryginalnego paska nar