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 rozwią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 oprogramowania, 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 naprawdę 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 poziomie. 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. Czasami 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 dysponujesz 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 wykorzystaniem 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 piktogramó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). Istnieją 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 zachowania.
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 podstawowych 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 programó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żywany 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 atmosferycznych. 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 kalkulacyjny, 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 (wyprowadzonej 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 przedstawionych 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 atmosferycznych? 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 dokument 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 dokumencie (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 informacje, które będziesz chciał później odtworzyć.
Praktycznie każdy program, opierający swoje działanie na wymianie informacji z użytkownikiem, 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 odpowiedzialny. 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 odpowiedzialny. 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 pozwalają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 posiadają 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. Chociaż 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 komunikaty 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, rozpakowanie 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 zostanie 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 komunikató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 sprzyjają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 komunikatów. Pamiętaj jednak, aby swoich modyfikacji dokonywać poza specjalnymi komentarzami generowanymi przez kreatora Class Wizard. Rozważmy przykład poniższej mapy komunikató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 komunikató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. Dokł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
Aktywny widok (jeśli taki jest)
Aktywny dokument (jeśli taki jest)
Szablon Dokumentu który stworzył dokument (jeśli jest)
Aktywna ramka okna potomnego (jeśli jest)
Obiekt aplikacji
Główna ramka okna
Tabela 1.4. Kolejność przeszukiwania programów SDI
Kolejność przeszukiwania Obiekt
Aktywny widok
Aktywny dokument
Szablon dokumentu
Główna ramka okna
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 dostę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 otworzonego 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 dokumentu 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 dokumentem 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 nagłó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 odpowiedniego 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 wydrukowany; 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 zapisuje 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 metodę 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ługi 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 drukowania 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 roboczy 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 fragmenty 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. Technicznie 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łkowicie 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 widoku 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 wymaganego 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ć specjalnych 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 dokumentu. 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:
Zapisywanie i odczytywanie dokumentów (poprzez mechanizm serializacji, patrz rozdział 2);
Przechowywane i obsługa listy widoków dołączonych do danego dokumentu;
Przechowywanie informacji o ścieżce i tytule dokumentu;
Modyfikowanie i obsługa flagi określającej czy dokument został zmodyfikowany;
Wysyłanie dokumentu za pomocą poczty elektronicznej.
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 momencie 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 niejednokrotnie 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ą przekazywaniu. 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 zdefiniowanego 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 przekazywany 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ł definiować 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 odpowiadają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 identyfikator. 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ą pierwszej 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ą szablonu 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żytkownika (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 umieszczany 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 zazwyczaj ż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 tworzonego 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ługiwane 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 wyją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 wywoł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 automatycznie 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 Windows lub z funkcji dostępnych w dodatkowej bibliotece DLL. Dlatego też niektóre klasy mogą być bardzo łatwo konwertowane na inne, odpowiadające im typy danych stosowanych 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 dodatkowych 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 widokó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ż odpowiadają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 automatycznie 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 przechowywany 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 zapewnia 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 jednoetapowe). 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 wyspecjalizowanym 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ślny 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 indywidualnych 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 wykorzystywać. 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 zdefiniowany 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 przedstawiona 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 argumentach 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 nazywany 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 definiują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ą zazwyczaj 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 wykonywane ż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 implementowania 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 (obiektami 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ś zobligowany, 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 indywidualnych potrzeb. Dostosowywanie to odbywa się za pomocą specjalnych funkcji pomocniczych (patrz tabela 1.18). Funkcje te są wymagane tylko wtedy, gdy modyfikować 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 korzystania 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 narzę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 jeden z tych produktów nie jest doskonały, a jednak wszystkich ich używamy do tworzenia 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 zastosowania 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ą definiowanych 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 zdefiniowanie 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 dodatkowo, ż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 prawdopodobną), 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 dialogowym 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 dokumentów aplikacji. Kiedy będziesz chciał stworzyć nową instancję dokumentu (czyli dokument, widok oraz ramkę), wystarczy wywołać metodę OpenDocumentFile szablonu 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 wytrzymał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 argumentu 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 szablony 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 Windows. 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 programu. 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 parametru 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 metody 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 uzyskane 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 niejednokrotnie 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 stanie 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 powiedział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 odcinka 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ądnąć 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 odtworzenia.
Stałość a pamięć
Jak zwykle, jedno słowo jest używane w MFC do opisania wielu pojęć. W jednym rozumieniu, 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 innym 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 zapewnienia 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 potomnych? 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 odczytanie 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 dokumentów, a pomimo tego możesz przesłaniać metodę Serialize. Makro IMPLEMENT_DYNCREATE dodaje do klasy operator », dodaje informacje 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 archiwum 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 podczas odczytywania (i na odwrót) spowoduje wygenerowanie wyjątku. Poniższy kod pokazuje 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 reprezentacji 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 takie 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 zapisywanego 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 zaró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 tablicy CArray (patrz rozdział 1) o nazwie ary, która wie, w jaki sposób należ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 nastę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 dialogowym 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 wystarczają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 dokumentami). 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 wygenerowanym przez kreatora App Wizard, a następnie zmodyfikować go zgodnie z Twoimi potrzebami. Podczas modyfikacji będziesz musiał zmienić kilka odwołań charakterystycznych 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ł odpowiednio 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 podpatrzeć 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 zawierają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ć cokolwiek 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ędziowych. 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 niekompatybilny 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 otwierać 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 modyfikować 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 modyfikować 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świetleniem 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 informacje 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 podczas 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ł odczytać 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 wiedzieć, ż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 zapisywany 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 przeprowadza serializację. Oznacza to, że nawet jeśli dodasz makro IMPLEMENT_SERIAL do swojego obiektu dokumentu, nie umożliwi to zapisania numeru wersji obiektu, ani jakichkolwiek 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ółpracują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łoby 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 oryginalnych 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 przechowywać 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 stosowanie 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ą efektywne 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 kiedy 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. Informacje 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 DoPromptFileName 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ł skopiować statyczną metodę AppendFilterSuffix. Pełny przykład praktycznego zastosowania 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 standardowym 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 skojarzonego 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 odpowiedni 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 algorytmy 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 pomocą wywołania metody GetObjectSchema.
Twój kod będzie mógł odpowiednio obsłużyć odczytywany obiekt na podstawie numeru 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, jednakż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świetlania 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 bohaterowie 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 kapitana 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 zaplanowana, 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 dreszczyk 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ć poprawnie wyskalowany, wielostronicowy dokument? Co z nagłówkami, stopkami, marginesami 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 drukować. 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 zadaniem drukowania). Jeśli już na tym etapie znasz ilość stron, które będziesz chciał wydrukować, 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 kreatora 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 (wystarczy 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 stanie 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ć numeracji 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:
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 przerwać. 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 OnPrepareDC 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ł kontynuować drukowanie, to po wywołaniu metody OnPrepareDC klasy bazowej będziesz musiał przypisać składowej m_bContinuePrinting wartość TRUE. Metoda OnPrepareDC klasy bazowej zawsze przypisuje tej składowej wartość FALSE, a więc pamiętaj, aby odpowiednio 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 jakikolwiek 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. Domyś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ólnymi węzłami sieci. Schemat sieci będzie w takim przypadku tworzony w metodzie OnDraw, tabela z zestawieniem połączeń - w metodzie OnPrint. Innym zastosowaniem 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 wystarczy zmienić początek układu współrzędnych widoku, aby zapobiec przesłonięciu nagłówka przez kod używany przy wyświetlaniu informacji na ekranie. Możesz także zmodyfikować 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 wydruku. Ilość stron wydruku określana jest na podstawie informacji zapisanych w strukturze 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 przedstawiony 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 istnieje 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 odpowiedniego 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 takim 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ładem 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 logiczne tryby mapowania nie są zbytnio przydatne.
Jeśli mimo wszystko będziesz chciał pozostać przy stosowaniu pikseli, to najprawdopodobniej 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ę metody 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 przedstawiają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ść planszy 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) skojarzonego 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 metoda 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ładowy 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 wydruku. W stosunkowo łatwy sposób można bowiem ograniczyć możliwości podglądu wydruku 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ługuje 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. Standardowo, 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 komentarzami 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 odpowiedniego 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 komentarze 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 zasobu 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 narzędzi i umieszczeniu w Twoim pliku zasobów, wystarczy go odpowiednio zmodyfikować, dostosowując jego postać do własnych potrzeb. Nie zapomnij także zmienić identyfikatora zasobu i użyć tego nowego identyfikatora w wywołaniu metody DoPrintPreview.
Przykład własnego podglądu wydruku
Na Listingu 3.2. przedstawiony został kod modyfikujący postać podglądu wydruku (wygląd zmodyfikowanego podglądu wydruku możesz zobaczyć na Rysunku 3.3.). Jedynym interesującym aspektem tworzenia własnej metody OnFilePrintPreview jest wykorzystanie w tej metodzie odwołania do obiektu CPreviewView. Obiekt ten jest używany w MFC do reprezentowania okna podglądu wydruku. Jedyny plik nagłówkowy zawierający deklarację tej klasy nosi nazwę AFXPRIV.H. Jeśli chcesz, aby Twój kod został poprawnie skompilowany, to będziesz musiał dołączyć do niego ten plik.
Używanie pliku nagłówkowego AFXPRIV.H
W zasadzie wszystkie deklaracje umieszczone w pliku AFXPRIV.H mogą się zmieniać w kolejnych wersjach MFC. Jednakże firma Microsoft okazała się na tyle łaskawa, aby nie zmieniać tych deklaracji, które mogą spowodować błędy w kodzie wielu programistów. De facto, wiele elementów umieszczonych początkowo w pliku nagłówkowym AFXPRIV.H (jak na przykład niektóre makra służące do konwersji danych) zostały ostatnio oficjalnie udokumentowane i przeniesione do innych plików nagłówkowych, a to wszystko z tego powodu, że były one tak często wykorzystywane.
Jeśli chcesz napisać profesjonalnie wyglądający program, to będziesz musiał czasami podjąć pewne ryzyko i wykorzystać te części MFC, które nie są oficjalnie udokumentowane. Może się zdarzyć, że po pojawieniu się nowszej wersji MFC będziesz musiał dokonać w swoim kodzie jakichś modyfikacji, jednakże taka już jest cena “życia na krawędzi".
Poniżej przedstawiłem oficjalny komentarz firmy Microsoft zaczerpnięty z pliku nagłówkowego AFXPRIV.H.
“Ten plik nagłówkowy zawiera przydatne klasy, które są udokumentowane jedynie w Notatkach Technicznych MFC. Klasy te mogą się zmieniać w kolejnych wersjach MFC, dlatego w przypadku wykorzystania tego pliku będziesz musiał być przygotowany na konieczność modyfikacji swojego kodu. W przyszłości częściej używane fragmenty tego pliku nagłówkowego mogą zostać oficjalnie udokumentowane i przeniesione w inne miejsca."
Metoda OnDraw przedstawiona na Listingu 3.2. wykonuje specjalne czynności podczas drukowania. W przypadku wykrycia, że program jest w trakcie drukowania (za pomocą metody CDC::IsPrinting), metoda ta zmienia tryb mapowania, dzięki czemu widok wydrukowany będzie wiernym odzwierciedleniem widoku ekranu. Aby móc to zrobić, kod tej metody zmienia tryb mapowania kontekstu drukarki (na MM_ISOTROPIC), pobiera kontekst aktualnego widoku i na podstawie jego wielkości określa parametry kontekstu drukarki.
Listing 3.2. Własny pasek narzędzi okna Podglądu Wydruku.
/ l conndotview.cpp : implementation of the CConndotView class
#include "stdafx.h"
#include "conndot.h"
tfinclude "CustomPreview.h" // dołącz okno podglądu wydruku
ttinclude "conndotDoc .h" tinclude "conndotView.h"
ttifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = _ FILE _ ;
ttendif
// CConndo tVi ew
IMPLEMENT_DYNCREATE(CConndotView, CView)
BEGIN_MESSAGE_MAP(CConndotView, CView) //{{AFX_MSG_MAP(CConndotView) ON_WM_LBUTTONDOWN() ON_WM_RBUTTONDOWN()
ON_COMMAND(ID_FILE_PRINT_PREVIEW, OnFilePrintPreview)
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
END_MESSAGE_MAP()
// CConndotView construction/destruction
CConndotView: :CConndotView( ) {
// TODO: add construction code here
}
CConndotView: : ~CConndotView( )
{ }
BOOL CConndotYiew: :PreCreateWindow(CREATESTRUCT& es) {
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT es
}
return
CView: : PreCreateWindow(cs) ;
/ / CConndotView drawing
void CConndotView: :OnDraw(CDC* pDC) {
CConndotDoc* pDoc = GetDocument ( ) ;
ASSERT_VALID(pDoc) ;
CPoint pt;
if (pDC->IsPrinting( ) )
{
CDC *vdc=GetDC() ; int n;
n=vdc->GetDeviceCaps (LOGPIXELSX) ; pDC->SetMapMode(MM_ISOTROPIC) ; pDC->SetWindowExt (n, n) ,-n=pDC->GetDeviceCaps (LOGPIXELSX) ; pDC->SetViewportExt (n, n) ; }
pDC->MoveTo(0, 0) ;
for (int i=0; i<pDoc->points -GetSize ( ) ; i++ {
pt=pDoc->points [ i] ;
if (pt.x<0)
{
pt .x=-pt .x; pt.y=-pt.y; pDC->MoveTo (pt) ;
else
pDC->LineTo
(pt) ;
// CConndotYiew printing
BOOL CConndotYiew::OnPreparePrinting(CPrintlnfo* plnfo)
// default preparation
return DoPreparePrinting(plnfo);
void CConndotView: :OnBeginPrinting (CDC* /*pDC*/, CPrintlnfo*
/*plnfo*/)
{
// TODO: add extra initialization before printing
void CConndotView: :OnEndPrinting (CDC* /*pDC*/, CPrintlnfo*
/*plnfo*/)
{
// TODO: add cleanup after printing
// CConndotView diagnostics
łtifdef _DEBUG
void CConndotView::AssertValid() const
{
CView::AssertValid();
void CConndotYiew::Dump(CDumpContext& dc) const CYi ew: : Dump (dc ) ;
CConndotDoc* CConndotView::GetDocument() // non-debug version is
inline
{
ASSERT (m_pDocument->IsKindOf (RUNTIME_CLASS (CConndotDoc) ) ) ,-
return (CConndotDoc*)m_pDocument; } łtendif //_DEBUG
// CConndotView message handlers
void CConndotView::OnLButtonDown(UINT nFlags, CPoint point) {
CConndotDoc *doc=GetDocument();
// Rysuj
doc->points.Add(point);
doc->UpdateAllViews(NULL);
CView::OnLButtonDown(nFlags, point); }
void CConndotView::OnRButtonDown(UINT nFlags, CPoint point)
{
// przesuń tylko aktualny punkt
CPoint mover=point;
mover.x--mover.x;
mover.y=-mover.y;
GetDocument()->points.Add(mover); CView::OnRButtonDown(nFlags, point);
void CConndotView::OnFilePrintPreview()
CPrintPreviewState* pState = new CPrintPreviewState;
if (!DoPrintPreview(IDD_PREVIEW_TOOLBAR, this,
RUNTIME_CLASS(CCustomPreview),pState))
// W klasie potomnej odtwórz tutaj specjalną obsługę okna // gdyż w przeciwnym razie okno podglądu wydruku nie będzie // działało prawidłowo
TRACEO("Error:
DoPrintPreview failed.\n");
Af
xMessageBox (AFX_IDP_COMMAND_FAILURE) ;
delete
pState; // okno podglądu nie zostało
//
zainicjalizowane usuń State
Bardziej zaawansowane dostosowywanie podglądu wydruku
Jeśli się dokładniej przyjrzysz pozostałym trzem argumentom wywołania metody DoPrintPreview, to najprawdopodobniej samemu zorientujesz się, do czego one służą. Drugim argumentem wywołania metody DoPrintPreview jest wskaźnik na widok, który ma zostać wydrukowany (zazwyczaj będzie to this). Trzecim argumentem jest klasa czasu wykonania (otrzymana za pomocą makra RUNTIME_CLASS) okna podglądu wydruku. Zazwyczaj jest to klasa CPrintView, jednakże nic nie stoi na przeszkodzie, aby użyć jakiejś innej klasy. Na przykład, można by stworzyć klasę potomną klasy CPreviewView, dodać do niej nowe możliwości, a następnie podać jej nazwę jako trzeci argument wywołania metody DoPrintPreview.
Ale co mógłbyś robić we własnej klasie widoku podglądu wydruku? Cokolwiek byś sobie wymyślił. Ot chociażby - napisać wielkimi literami “PODGLĄD WYDRUKU" (oczywiście oprócz pozostałej zawartości podglądu). Jednakże rewelacyjną modyfikacją byłoby uaktywnienie okna podglądu wydruku w taki sposób, aby użytkownik mógł modyfikować dane podczas oglądania podglądu (takie rozwiązanie mogłoby troszkę przypominać postać układu strony dostępny w wielu popularnych edytorach).
Podczas tworzenia własnej klasy potomnej klasy CPrintView możesz odkryć, że potrzebne Ci będą pewne dodatkowe elementy. Po pierwsze, bez większych trudności będziesz mógł dodawać nowe przyciski do paska narzędzi swojego widoku. Cała modyfikacja będzie się sprowadzała do zmienienia szablonu paska narzędzi przechowywanego w pliku zasobów (tak jak w poprzednim przykładzie). Możesz także stworzyć swoją klasę jako klasę potomną klasy CPrintPreviewState i dodać do niej dowolne dane, jakie będą Ci potrzebne. Jedyną rzeczą, o jakiej nie możesz zapomnieć, to umieszczenie nazwy Twojej nowej klasy w wywołaniu operatora new w metodzie OnFilePrintPreview.
Wyprowadzanie klasy
Niestety, podczas tworzenia nowej klasy kreator Class Wizard nie umożliwia wybrania klasy CPreviewView jako klasy bazowej. Klasa CPreviewView jest klasą potomną klasy CScroIlYiew; nie polecałbym Ci jednak wyprowadzania swojej nowej klasy z klasy CPreviewView ze względu na to, iż obsługuje ona wszystkie czynności charakterystyczne dla przewijalnych widoków (widoków wyprowadzonych z klasy CScrollView). Najlepszym wyjściem z sytuacji jest wyprowadzanie klasy widoku z klasy CView. Po stworzeniu klasy będziesz musiał zamienić wszystkie wystąpienia łańcucha znaków “CView" na “CPreviewView" (zarówno w pliku nagłówkowym - .H, jak i pliku źródłowym - .CPP). Upewnij się, że wszystkie wystąpienia łańcucha znaków “CView" zostały poprawnie zamienione. Będziesz także musiał dołączyć do pliku źródłowego plik nagłówkowy AFXPRIV.H, aby uzyskać definicję klasy CPreviewView.
Kolejnym etapem będzie zmodyfikowanie istniejącego widoku. Czynności, jakie będziesz musiał w tym celu wykonać, są bardzo podobne do czynności, które wykonywałeś podczas modyfikowania paska narzędzi w oknie podglądu wydruku. Jedyna różnica polega na tym, iż zamiast klasy CPreviewView, w wywołaniu metody DoPrintPreview użyjesz swojej, stworzonej przed chwilą klasy; oprócz tego, zamiast pliku AFXPR1V.H, dołączysz do widoku plik nagłówkowy Twojej klasy.
Jeśli będziesz chciał, to możesz przesłonić metodę OnDraw, aby samemu narysować coś na stronie (lub w obszarze poza stroną). Problem jednak polega na tym, że nie wiadomo gdzie rysować. Podobny problem pojawi się, kiedy będziesz chciał obsługiwać kliknięcia myszką- współrzędne kliknięcia będziesz musiał przeliczać na współrzędne, które mają jakikolwiek sens dla Twojego programu.
Wewnętrzne tajniki sporządzania podglądu wydruku
Kod odpowiadający za sporządzanie podglądu wydruku umieszczony jest w trzech plikach źródłowych MFC. Kod obsługujący okno podglądu wydruku umieszczony jest w pliku V1EWPREV.CPP. Specjalny kontekst urządzenia odpowiadający za poprawne drukowanie znajduje się w pliku DCPREY.CPP. I wreszcie wszystkie odpowiednie definicje umieszczone zostały w pliku AFXPRIV.H. Przestudiowanie tych trzech plików w znacznej mierze może wyjaśnić Ci szczegóły sposobu sporządzania podglądu wydruku.
Podstawowym źródłem informacji dla podglądu widoku jest składowa m_pPageInfo. Jest to tablica struktur typu PAGE_INFO (patrz Tabela 3.4.). W tablicy tej umieszczane są informacje określające każdą stronę sporządzanego wydruku. Jeśli cały wydruk składa się tylko z jednej strony, to oznacza to, że w tablicy tej umieszczony będzie tylko jeden element (zostanie on umieszczony w komórce tablicy o indeksie 0). Jeśli na wydruk składają się dwie strony, to odpowiednie informacje zostaną umieszczone w komórkach i indeksach O i 1.
W składowej rectScreen przechowywane są współrzędne obszaru (prostokątnego), jaki aktualna strona zajmuje na ekranie. Pozostałe trzy składowe tej struktury są obiektami klasy CSize. Jednakże MFC nie używa ich do określania wielkości. Zamiast tego są one używane do zapamiętania ułamka (cx/cy); będziesz musiał o tym pamiętać, analizując oryginalny kod MFC.
Tabela 3.4. Postać struktury PAGE_INFO.
Składowa Definicja
rectScreen Współrzędne tej strony na ekranie.
sizeUnscaled Prostokąt określający niewyskalowany obszar ekranu.
sizeScaleRatio Współczynnik (cx/cy) pomiędzy jednostkami drukarki a jednostkami ekranu.
sizeZoomOut Współczynnik skalowania używany podczas zmniejszania powiększonego podglądu wydruku.
Poprzez manipulowanie dostępnymi współczynnikami skalowania będziesz mógł przekształcać punkty na drukarce na punkty na ekranie (i na odwrót). Praktyczny sposób przekształcania będziesz mógł zobaczyć w przykładowym programie przedstawionym w dalszej części rozdziału. Niestety, nie udało mi się znaleźć łatwego sposobu przekształcania punków na ekranie na punkty, którymi możesz się posługiwać w programie. Na szczęście, jeśli raz napiszesz odpowiedni kod, to już nigdy więcej nie będziesz musiał tego robić powtórnie. Oczywiście, zamiast pisać własny kod, równie dobrze możesz skopiować kod przedstawiony w przykładowym programie.
Klasa CPreviewView dysponuje wieloma innymi składowymi, jednakże nie są one dla nas interesujące. Numer aktualnie drukowanej strony możesz określić za pomocą składowej m_nCurrentPage. Kontekst urządzenia podglądu wydruku jest przechowywany w składowej m_pPreviewDC.
Tworzenie podglądu wydruku umożliwiającego edycję
Na Rysunku 3.4. przedstawiona została kolejna wersja programu umożliwiającego rysowanie i łączenie punktów. Ta wersja programu dysponuje własnym obiektem klasy CPreviewView. Okno podglądu wydruku wygląda niemalże identycznie z oknem dostępnym w poprzedniej wersji programu; jedyną różnicą jest to, że na pasku narzędzi umieszczony został dodatkowy przycisk o nazwie Edit. Kliknięcie na tym przycisku powoduje zmianę kształtu kursora (będzie on przypominał strzałkę) i umożliwienie rysowania bezpośrednio w oknie podglądu wydruku. Powtórne kliknięcie przycisku (którego nazwa została zmieniona na Zoom) spowoduje przejście do normalnego trybu podglądu wydruku i zmianę kształtu kursora.
W jaki sposób można to wszystko zrobić? Otóż klasa podglądu wydruku stworzona na potrzeby tego przykładu zawiera składową typu Boolean (editmode) używaną do przechowywania informacji o trybie, w jakim aktualnie znajduje się okno podglądu wydruku (dostępne są dwa tryby: tryb edycji oraz tryb powiększania). Oprócz tego w mapie komunikatów dostępnych w tej klasie umieszczone zostały makra określające procedury obsługi kliknięć prawym i lewym przyciskiem myszy, procedurę obsługi komunikatu WM_SETCURSOR oraz procedurę obsługi przycisku ID_EDITBUTTON (używanego do przełączania trybu pracy okna podglądu wydruku).
Kiedy użytkownik naciska przycisk zmiany trybu pracy, wartość zmiennej editmode jest zamieniana na przeciwną. Dodatkowo zdefiniowana została procedura obstugi UPDATE_COMMAND_UI odpowiadająca za modyfikowanie nazwy przycisku (więcej informacji na temat procedur używanych do aktualizacji opcji menu i innych elementów interfejsu użytkownika). Gdy program otrzymuje komunikat WM_SETCURSOR, to sprawdza czy podgląd wydruku znajduje się w trybie edycji (czy składowa editmode posiada wartość TRUE) oraz czy wskaźnik myszy znajduje się w obszarze roboczym okna podglądu wydruku. Jeśli oba te warunki zostaną spełnione, to wskaźnikowi myszy nadawany jest kształt strzałki. W przeciwnym razie, wywoływane są odpowiednie metody klasy bazowej, dzięki czemu wykonane zostaną standardowe czynności.
Listing 3.3. Edycja danych w oknie podglądu wydruku.______
// CustomPreview.cpp : implementation file
_FILE_
łtinclude
"stdafx.h" ttinclude "conndot.h"
ttinclude "CustomPreview.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] =
#endif
//
CCustomPreview
IMPLEMENT_DYNCREATE(CCustomPreview, CPreviewView)
CCustomPreview::CCustomPreview() {
editmode=FALSE; }
CCustomPreview::~CCustomPreview()
BEGIN_MESSAGE_MAP(CCustomPreview, CPreviewView)
//{{AFX_MSG_MAP(CCustomPreview)
ON_WM_RBUTTONDOWN()
ON_WM_LBUTTONDOWN ( )
ON_COMMAND(ID_EDITMODE, OnEditMode)
ON_UPDATE_COMMAND_UI(ID_EDITMODE, OnUpdateEditMode)
ON_WM_SETCURSOR()
//} }AFX_MSG_MAP END_MESSAGE_MAP()
// CCustomPreview drawing
// CCustomPreview diagnostics
łłifdef _DEBUG
void CCustomPreview::AssertValid() const
{
CYiew::AssertValid();
void CCustomPreview::Dump(CDumpContext& dc) const
CView::Dump(dc); #endif //_DEBUG
11111111 /11111111111111111111 /11111111111111111111111 /1111 /11111111 II CCustomPreview message handlers
void CCustomPreview::OnRButtonDown(UINT nFlags, CPoint point) if (editmode)
ConvertPoint(point);
// przesuń tylko aktualny punkt
CPoint mover=point;
mover.x=-mover.x;
mover.y=-mover.y;
GetDocument()->points.Add(mover);
else
CPreviewView::OnRButtonDown(nFlags,
point);
void CCustomPreview::OnLBUttonDown(UINT nFlags, CPoint point)
i
f (editmode)
CConndotDoc *doc=GetDocument ( ) ; ConvertPoint (point) ; doc->points .Add (point) ; doc->UpdateAHViews (NULL) ;
else
CPreviewView:
.-OnLButtonDown (nFlags, point)
CConndotDoc *CCustomPreview: :GetDocument ( ) {
return (CConndotDoc * ) CPreviewView: :GetDocument ( ) ,-
void CCustomPreview: : ConvertPoint (CPoint & point)
{
// zakładamy, że widoczna jest tylko l strona,
// jeśli to nie jest dobre założenie, to będziesz musiał sprawdzić
// numer wyświetlanej strony - możesz to zrobić, badając wartość
// każdej składowej rectScreen elementu tablicy m_pPage!nfo.
// Nie zapomnij, że podgląd wydruku używa sizeScaleRatio jako ułamka, // nie są stosowane czynniki skalujące x, y. CPoint ViewportOrg; if (nunZoomState != ZOOM_OUT)
ViewportOrg = -GetDeviceScrollPosition ( ) ; else
ViewportOrg=GetDC ( ) ->GetViewportOrg ( ) ; m_pPreviewDC->SetScaleRatio (m_pPage!nf o [
m_nCurrentPage-l ] . sizeScaleRatio . ex,
m_pPage!nfo [m_nCurrentPage-l] . sizeScaleRatio . cy) ,-
// określ wielkość marginesu CSize PrintOffset; m_pPreviewDC->Escape(GETPRINTINGOFFSET, O, NULL,
(LPVOID)&PrintOffset) ;
m_pPreviewDC->PrinterDPtoScreenDP( (LPPOINT) &PrintOf f set ) ; PrintOffset += (CSize) m_pPage!nf o [
m_nCurrentPage-l] . rectScreen. TopLef t () ; PrintOffset += CSized, 1);
PrintOffset += (CSize) ViewportOrg; // na potrzeby przewijania point- =PrintOf f set ;
// dostosuj punkt do pozycji strony
point. x = MulDiv(point .x, m_pPage!nfo[
m_nCurrentPage-l] . sizeScaleRatio . cy, m_pPage!nf o [m_nCurrentPage-l ] . sizeScaleRatio. ex) ; point. y = MulDiv(point .y, m“pPageInf o [
m_nCurrentPage-l] . sizeScaleRatio . cy,
m_pPage!nfo [m_nCurrentPage-l ] . sizeScaleRatio. ex) ;
void CCustomPreview: : OnEdi tMode ( ) {
ed t tmode- i editmode ;
void CCustomPreview::OnUpdateEditMode(CCmdUI* pCmdUI) {
pCmdUI->SetText(editmode?"Zoom":"Edit"};
pCmdUI->Enable();
BOOL CCustomPreview: :OnSetCursor (CWnd* pWnd, UINT nHitTest, UINT
message) {
if (editmode && nHitTest==HTCLIENT) {
: : SetCursor (Af xGetApp ( ) ->LoadStandardCursor ( IDC_ARROW) ) ; return TRUE;
else
return
CPreviewView: :OnSetCursor (pWnd, nHitTest,
message) ;
Najważniejsza część pracy wykonywana jest w procedurach obsługi kliknięć prawym i lewym przyciskiem myszy. Kod umieszczony w tych procedurach musi w pierwszej kolejności sprawdzić wartość składowej editmode. Jeśli wartość tej składowej wynosi FALSE, to program nie wykonuje żadnych dodatkowych czynności i wywołuje metodę klasy bazowej umożliwiającą standardową obsługę obydwu komunikatów. Jeśli jednak wartość tej składowej wynosi TRUE, to program pobiera współrzędne położenia wskaźnika myszy w momencie kliknięcia i przekształca je na współrzędne stosowane w programie. Następnie współrzędne te zapisywane są w dokumencie.
Aby maksymalnie uprościć program, konwersja punktów realizowana jest w specjalnej metodzie o nazwie ConvertPoint. Funkcja ta wykonuje za Ciebie całą “brudną robotę", o czym łatwo możesz się przekonać, przeglądając jej kod. Wbrew pozorom to nie przeliczenie współrzędnych za pomocą funkcji MulDiv oraz odpowiednich współczynników skalujących stwarza najwięcej problemów. Prawdziwych kłopotów przysparza określenie wielkości marginesów strony wydruku oraz przeliczenie tych marginesów na piksele. Co więcej, musisz także wziąć po uwagę możliwość przewinięcia okna. Na szczęście będziesz mógł skopiować tę metodę bezpośrednio ż przykładu do swojego kodu.
W przykładowym programie wykorzystana została jeszcze jedna chytra sztuczka, dotyczy ona pobierania wskaźnika do obiektu dokumentu. W Rozdziale 1. miałeś okazję zobaczyć, że kreator App Wizard przesłania metodę GetDocument obiektu widoku w taki sposób, iż zwraca ona wskaźnik na dokument typu wykorzystywanego w Twoim programie, a nie wskaźnik na ogólny typ dokumentu - CDocument. Jednakże gdy tworzysz nową klasę widoku za pomocą kreatora Class Wizard, to nie ma on żadnego pojęcia o tym, z jakim dokumentem nowy widok będzie współpracował. Dlatego też podczas tworzenia nowego widoku za pomocą tego kreatora metoda GetDocument nie jest przesłaniana. Jeśli wywołasz tę metodę w nowej klasie widoku, to zostanie zwrócony wskaźnik do obiektu CDocument. W takim wypadku, aby móc skorzystać z metod charakterystycznych dla Twojego dokumentu, będziesz musiał odpowiednio rzutować otrzymany wskaźnik. Oczywiście możesz samemu przesłonić standardową definicję metody GetDocument, takie rozwiązanie zostało zastosowane w przedstawionym przykładzie.
Podsumowanie
Jeśli jesteś podobny do wielu innych programistów, to będziesz odkładał implementowanie drukowania aż do ostatnich etapów powstawania programu. Na szczęście MFC zachęca Cię i umożliwia takie postępowanie, gdyż drukowanie i rysowanie na ekranie są w MFC bardzo ściśle ze sobą związane. Czasami bez obaw możesz powierzyć drukowanie MFC, a samemu zająć się rozwiązywaniem bardziej złożonych problemów.
Jednakże jeśli wymagania stawiane sposobowi drukowanie lub działania podglądu wydruku w Twoim programie są bardziej wymagające, to będziesz musiał dokładnie zrozumieć, jak MFC realizuje drukowanie i tworzy podgląd wydruku. Dzięki informacjom przedstawionym w tym rozdziale będziesz w stanie zmodyfikować sposób drukowania tak, aby dokładnie odpowiadał Twoim wymaganiom.
Praktyczny przewodnik Drukowanie
Zarządzanie oknem dialogowym Drukuj
Skalowanie wydruku
Drukowanie innych elementów
Drukowanie nagłówków i stopek
Modyfikowanie paska narzędzi okna podglądu wydruku
Modyfikowanie działania podglądu wydruku
W przypadku wielu aplikacji jedyną rzeczą, jaką będziesz musiał zrobić, aby opcje drukowania zaczęły działać poprawnie, będzie użycie innego trybu mapowania niż MM_TEXT. Kreator App Wizard dołącza do tworzonych aplikacji prostą obsługę drukowania i wybór jakiegoś “normalnego" trybu mapowania powinien spowodować, że wydruki sporządzane przez program będą automatycznie i poprawnie skalowane.
Zarządzanie oknem dialogowym Drukuj
Pierwszą metodą wywoływaną po rozpoczęciu procesu drukowania jest metoda OnPreparePrinting zdefiniowana w klasie widoku. Jej działanie ogranicza się do wywołania kolejnej metody - DoPreparePrinting. Do metody OnPreparePrinting przekazywany jest obiekt klasy CPrintlnfo, dzięki któremu będziesz w stanie przejąć kontrolę nad informacjami wyświetlanymi w oknie dialogowym Drukuj (czyli, jednocześnie, także nad samym procesem drukowania).
Dwiema najbardziej interesującymi składowymi dostępnymi w klasie CPrintlnfo są: m_pPD (w której przechowywany jest wskaźnik do okna dialogowego CPrintDialog wyświetlanego przez MFC) oraz SetMaxPages, która to metoda pozwala Ci na określenie ilości stron wydruku.
Jeśli w ogóle nie będziesz chciał wyświetlać okna dialogowego Drukuj, to przed wywołaniem metody DoPreparePrinting przypisz składowej m_bDirect (klasy CPrintlnfo)
wartość TRUE. W takim wypadku wykonane zostanie zadanie drukowania, podczas którego, bez żadnych interwencji ze strony użytkownika, wydrukowane zostaną wszystkie strony. Podczas drukowania wykorzystana zostanie domyślna drukarka.
Skalowanie wydruku
Jeśli w programie wykorzystujesz tryb mapowania MM_TEXT lub któryś inny jednorodny (isotropic) tryb, to sporządzony wydruk będzie się zazwyczaj różnił od wyglądu ekranu. Po zastanowieniu przyznasz, że nie jest to pozbawione sensu. Rozdzielczość monitora wynosi bowiem około 72 punktów na cal. Dlatego też, jeśli narysujesz linię składającą się z 75 punktów, to będzie ona miała długość około jednego cala (około - gdyż rozdzielczości ekranów nie są dokładne). Jednakże na drukarce o rozdzielczości 300 punktów na cal (dpi), ta sama linia będzie miała długość dokładnie 1/4 cala; na drukarce o rozdzielczości 600 dpi - ta sama linia będzie już miała tylko 1/8 cala długości.
Istnieje kilka sposobów przezwyciężenia tego problemu. Najprostszym z nich jest użycie trybu mapowania niezależnego od wykorzystywanego sprzętu. Zamiast tego można użyć takich trybów mapowania jak: MM_LOENGLISH lub MM_HIMETRIC (patrz Tabela 3.3).
Chociaż nie zawsze jest to możliwe do wykonania, to jednak czasami będziesz musiał przeskalować wydruk, używając przy tym jednego z bardziej popularnych trybów mapowania, Pomysł tego rozwiązania polega na tym, żeby używając trybu MM_ISOTROPIC tak ustawić parametry kontekstu drukarki, aby narysowanie na drukarce linii prostej składającej się z 75 punków spowodowało wydrukowanie linii o długości nieco powyżej jednego cala.
Sposób przeskalowania kontekstu drukarki, tak aby był on zgodny z parametrami ekranu, przedstawiony został na Listingu 3.1. (metoda OnPrint). Oczywiście, może się zdarzyć, że będziesz chciał postąpić w nieco inny sposób. Na przykład mógłbyś chcieć, aby element na wydruku był dokładnie dwa razy większy od tego samego elementu na ekranie. Możesz to osiągnąć bez jakichkolwiek większych problemów poprzez odpowiedni współczynników skalowania.
Drukowanie innych elementów
Generalnie rzecz biorąc, MFC zakłada, że będziesz chciał drukować te same elementy, które są widoczne na ekranie. Zazwyczaj jest to całkiem słuszne założenie; mogą się jednak zdarzyć takie przypadki, kiedy na wydruku będziesz chciał umieścić zupełnie co innego. Załóżmy, że częścią Twojego widoku jest formularz służący do wprowadzania danych. Formularz ten prezentuje pojedynczy rekord z bazy danych. Podczas drukowania danych takiej aplikacji nie będziesz chciał wydrukować tylko jednego - aktualnego rekordu; zapewne będziesz chciał sporządzić tabelaryczny wydruk wszystkich rekordów zapisanych w bazie.
Taka modyfikacja sposobu drukowania jest wyjątkowo prosta. Wystarczy przesłonić standardową definicję metody OnPrint. Domyślna definicja tej metody powoduje wywołanie metody OnDraw, jednakże możesz utworzyć swoją własną wersję tej metody, która umożliwi Ci wydrukowanie dowolnych informacji pod dowolną postacią. Rozwiązanie to jest szczególnie istotne w przypadku tworzenia programów, w których widok jest klasą potomną klasy CFormView. Wynika to z faktu, iż widoki takie nie mają możliwości wydrukowania swojej zawartości.
Drukowanie nagłówków i stopek
Kolejnym powodem, dla którego mógłbyś chcieć przesłonić standardową definicję metody OnPrint, jest konieczność lub chęć umieszczenia na wydruku dodatkowych elementów. Doskonałym przykładem takiego wykorzystania metody OnPrint jest drukowanie nagłówków i stopek, które nie są widoczne na ekranie.
Przykład takiego zastosowania metody OnPrint możesz znaleźć na Listingu 3.1. Bardzo istotną rzeczą, o której nie można zapomnieć podczas drukowania nagłówków i stopek, jest konieczność określenia odpowiedniej wielkości regionu przycinania (w celu ochronienia stopki) oraz współrzędnych początku układu współrzędnych (w celu ochronienia nagłówka). Jeśli uważasz, że powyższe rozwiązanie wymaga zbyt wiele pracy, to zawsze będziesz mógł umieścić kod tworzący nagłówki i stopki wydruku wewnątrz metody OnDraw.
Modyfikowanie paska narzędzi okna podglądu wydruku
Jeśli będziesz chciał zmodyfikować postać standardowego paska narzędzi wyświetlanego w oknie podglądu wydruku, to będziesz musiał przesłonić standardową definicję metody OnFilePrintPreview. Poniżej przedstawione zostały czynności, który będziesz musiał wykonać:
1. Umieścić makro ON_COMMAND zawierające metodę OnFilePrintPreview
wewnątrz części mapy komunikatów zarządzanej przez kreatora Class Wizard.
2. Z wywołania metody OnFilePrintPreview usunąć modyfikator zawierający nazwę klasy bazowej (CView).
3. Dodać własną definicję metody OnFilePrintPreview (do pliku nagłówkowego - H i źródłowego - CPP).
4. Wewnątrz metody OnFilePrintPreview stworzyć na stercie (za pomocą operatora New) nowy obiekt klasy CPrintPreviewState.
5. Wywołać metodę DoPrintPreview, przekazując jako argumenty jej wywołania następujące dane: identyfikator zasobu określającego postać paska narzędzi, wskaźnik RUNTIME_CLASS(CPrintView) oraz obiekt, który stworzyłeś w poprzednim kroku.
6. Jeśli metoda DoPrintPreview zwróci wartość FALSE, to powinieneś wyświetlić komunikat o błędzie i usunąć obiekt stworzony w kroku 4.
7. Upewnić się, że do pliku źródłowego Twojego nowego widoku dołączyłeś plik nagłówkowy AFXPRIV.H.
8. Stworzyć zasób określający postać paska narzędzi i nadać mu ten sam identyfikator, którego użyłeś w kroku 5. (określanie postaci paska narzędzi możesz rozpocząć od skopiowania oryginalnego paska, który możesz znaleźć w pliku AFXPRINT.RC znajdującym się w kartotece MFC/INCLUDE).
Gdy zakończysz pracę, to Twoja mapa komunikatów powinna mieć następującą postać:
BEGIN_MESSAGE_MAP (CConndotYiew, CView)
// { {AFX_MSG_MAP (CConndotView)
ON_WM_LBUTTONDOWN ( )
ON_WM_RBUTTONDOWN ( )
ON_COMMAND ( ID_FILE_PRINT_PREVIEW, OnFilePrintPreview)
//}}AFX_MSG_MAP
//Standard printing corranents
ON_COMMAND(ID“FILE_PRINT, CView: : OnFilePrint )
ON_COMMAND(ID“FILE_PRINT_DIRECT, CView: : OnFilePrint ) END_MESSAGE_MAP ( )
Poniżej przedstawiona została postać Twojej wersji metody OnFilePrintPreview:
void CMyYiew: : OnFilePrintPreview ( ) {
CPrintPreviewState* pState = new CPrintPreviewState; if ( !DoPrintPreview(AFX_IDD_PREVIEW_TOOLBAR, this,
RUNTIME_CLASS ( CPreviewView) , pState) ) {
// domyślny komunikat o błędzie Af xMessageBox (AFX_IDP_COMMAND_FAILURE) ; delete pState;
Kompletny przykład prezentujący opisaną tutaj metodę modyfikowania paska narzędzi okna podglądu wydruku możesz znaleźć na Listingu 3.2.
Modyfikowanie działania podglądu wydruku
Opisane powyżej czynności służące do zmodyfikowania postaci paska narzędzi okna podglądu wydruku, mogą zostać użyte także do całkowitego zmienienia wyglądu oraz sposobu działania okna podglądu. Jednakże w takim przypadku będziesz musiał utworzyć swoją własną klasę, wyprowadzoną z klasy CPreviewView i użyć jej nazwy jako trzeciego argumentu wywołania metody DoPrintPreview. Przykład zastosowania tej metody przedstawiony został na Listingu 3.3.
Rozdział 4 Okna, widoki i elementy kontrolne
Elementy kontrolne są jednym z najlepszych sposobów wielokrotnego używania tego samego kodu w systemie Windows. Dzięki zastosowaniu MFC możesz dostosować elementy kontrolne tak, aby odpowiadały Twoim potrzebom. W tym rozdziale, między innymi, dowiesz się jak można modyfikować widok listy oraz używać elementów kontrolnych rysowanych przez użytkownika.
Kiedy różni ludzie zadają mi pytania, bardzo często jestem zaszokowany tym, jak mało wspólnego ma treść pytania z zagadnieniem, o którym pytający chce się czegoś dowiedzieć. Dla przykładu, już kilka razy moi sąsiedzi lub znajomi nie mający wiele wspólnego z komputerami, pytali mnie w jaki sposób mogą dodać trochę pamięci do swoich komputerów. Gdybym nie był uważny, za każdym razem rozwodziłbym się nad 30 pinowymi SIMM-ami, 72 pinowymi SIMM-ami i innymi szczegółami tego typu. Po dziesięciu minutach takiej rozmowy i obserwowaniu, jak twarze moich rozmówców powoli szarzeją, zdawałem sobie sprawę z tego, że chodziło im po prostu o większy dysk.
Po kilku doświadczeniach tego typu, stałem się bardzo ostrożny i zacząłem podejrzliwie podchodzić do wszystkich pytań, które są zbyt szczegółowe. Dla przykładu, jeśli ktoś mnie zapyta: “W jaki sposób mam napisać sterownik obsługujący komunikację przez port szeregowy1?"' moja odpowiedź będzie miała postać następującego pytania: ,,A co chcesz zrobić?". Dopiero gdy dowiem się czegoś więcej o tym, do czego dana osoba chce wykorzystać modem, będę mógł jej zasugerować odpowiednie rozwiązanie wykorzystujące TAPI lub normalne procedury dostępne w systemie Windows.
Ostatnio pojechałem do Disneyland, i zdałem sobie sprawę z tego, że ludzie, którzy go zbudowali doskonale rozumieli tę zasadę postępowania. Gdy spacerujesz Główną Ulicą Magicznego Królestwa przechodząc koło piekarni możesz poczuć zapach pieczonych ciastek. W takiej sytuacji, w naturalny sposób założysz, że w rzeczywistości są to pieczone ciasteczka, nieprawdaż ? I będziesz się mylił. Istnieją dwa główne problemy związane z wydmuchiwaniem zapachu pieczonych ciasteczek na ulicę. Pierwszy z nich polega na tym. że piekarnia przeważnie nie piecze ciasteczek cały czas, dlatego też nie byłoby możliwe ciągle utrzymywanie zapachu na ulicy koło piekarni. Po drugie, przepuszczanie powietrza ponad piekącymi się ciasteczkami powoduje ich wysuszanie i pękanie. Dlatego też, zamiast naturalnych sposobów tworzenia zapachu piekących się ciasteczek, twórcy Disneylandu wyprodukowali sztuczny zapach ciasteczek i wydmuchują go na ulicę. Jak widać, efekt końcowy jest znacznie ważniejszy od sposobów, w jaki zostaje on uzyskany.
Ta sama zasada obowiązuje podczas pisania programów. Czasami osiągnięcie zamierzonego celu jest ważniejsze od sposobów jego uzyskania. Dawno temu, w czasach gdy na rynku systemów operacyjnych pojawił się Windows 3.0, przyszedł do mnie mój przyjaciel. Powiedział, że czytał trochę o systemie Windows, lecz nie jest pewny jak należy obsługiwać komunikat WM_PAINT. Zamiast bezpośrednio odpowiedzieć na jego pytanie, zapytałem go jakiego typu program chce napisać. Odpowiedział, że mato być prosty program pobierający informacje z bazy danych i wyświetlający je na ekranie.
Kiedy to usłyszałem, poradziłem mu, aby użył okna dialogowego z normalnymi polami edycyjnymi. W ten sposób znajomy w ogóle nie musiał obsługiwać komunikatu WM_PAINT (co jednocześnie pozbawiło go wielu innych problemów). Oczywiście pole edycyjne musi gdzieś obsługiwać komunikat WM_PAINT, jednakże kto by się tym przejmował. Nie ma to w końcu żadnego znaczenia, o ile tylko pole edycyjne działa poprawnie.
Zazwyczaj będziesz chciał, aby ktoś inny przejmował się możliwie największą liczbą szczegółów. Jednak postępowanie takie niesie za sobą pewne niebezpieczeństwo. Co zrobić, jeśli element kontrolny nie zachowuje się dokładnie tak, jakbyś sobie tego życzył? W takim przypadku musiałbyś tworzyć od początku nowy element kontrolny, użyć innego elementu albo zmodyfikować dostępny element tak, aby spełniał Twoje wymagania. Dla przykładu, jeśli program bazy danych pisany przez mojego znajomego miałby wyświetlać pewne informacje w innym kolorze, byłby to pewien problem. Standardowe elementy kontrolne wyświetlają tekst w jednym, ściśle określonym kolorze, dlatego też, w takim przypadku, trzeba by zastosować rozwiązanie polegające na wyświetlaniu danych za pomocą komunikatu WM_PAINT, wraz ze wszystkimi konsekwencjami, które to za sobą pociąga.
Próbując rozwiązać te problemy, MFC udostępnia obszerny zbiór własnych elementów kontrolnych, okien i widoków, dzięki którym można znacznie szybciej tworzyć własne programy. Jeśli narzędzia te będą działały zgodnie z Twoimi wymaganiami, to doskonale! Jeśli jednak okażą się dla Ciebie niewystarczające, to będziesz musiał wymyślić inne rozwiązanie.
Generalnie rzecz biorąc, tworzenie elementu kontrolnego od podstaw powinno być absolutnie ostatecznym rozwiązaniem. Nie tylko przysparza ono znacznej ilości pracy, lecz powoduje, że wszystko trzeba będzie robić jeszcze raz, gdy sposób działania lub możliwości elementu kontrolnego będą musiały zostać zmodyfikowane. W takiej sytuacji modyfikowanie oryginalnego kodu przysparza wielu problemów. To samo dotyczy okien i widoków, które mogą zmieniać się w kolejnych wersjach MFC oraz systemu Windows.
Znacznie lepszym rozwiązaniem w takiej sytuacji jest zmodyfikowanie istniejącego elementu kontrolnego. Jak? Jest na to wiele metod. W MFC najprostszą metodą jest stworzenie nowej klasy, wyprowadzonej z klasy elementu kontrolnego (widoku lub okna), którego zachowanie chcesz zmienić, a następnie wprowadzenie odpowiednich zmian w kodzie nowej klasy. Dzięki takiemu rozwiązaniu, jedynym problemem będzie użycie nowego elementu w tworzonym programie.
Poprawiony element CListCtrl
W celu zademonstrowania dodawania nowych możliwości funkcjonalnych do istniejących elementów kontrolnych, zmodyfikujemy listę klasy CListCtrl. MFC używa tej klasy do tworzenia i obsługi list nowego typu (będących częścią standardowego pakietu elementów kontrolnych). Element ten udostępnia kilka bardzo interesujących możliwości. Jedną z nich, prawdopodobnie najczęściej wykorzystywaną, jest styl raportu. Dzięki niemu, możesz stworzyć w elemencie kontrolnym kolumny prezentujące dowolne dane (patrz rysunek 4.1). Elementy kontrolne wykorzystujące ten styl można zobaczyć w wielu miejscach w systemie Windows. Dla przykładu, w listach tego typu prezentowane są wyniki wyszukiwania dostępnego, jako opcji menu Startowego.
W elemencie tym nie podobają mi się dwie rzeczy. Po pierwsze, aby zaznaczyć element na liście, musisz kliknąć w pierwszej kolumnie listy - klikanie w innych kolumnach nie daje żadnego rezultatu. Drugą rzeczą, która mnie drażni, jest fakt, że po zaznaczeniu elementu listy modyfikowany jest wygląd jedynie jej pierwszej kolumny; pozostałe pozostają nie zmienione.
Zmodyfikowanie działania listy nie przysparza wielu kłopotów. Jest ono niewątpliwie znacznie prostsze od stworzenia całego elementu kontrolnego od początku. Jednym z możliwych rozwiązań byłoby zrozumienie wewnętrznych zasad działania elementu kontrolnego listy i zmodyfikowanie (w klasie potomnej) kodu odpowiadającego za zaznaczanie elementów listy. Oczywiście można tak zrobić, istnieje jednak jeszcze prostsze rozwiązanie.
Zamiast samemu się martwić o zaznaczanie elementów listy, możesz stworzyć klasę potomną, w której obsługa kliknięć zostanie tak zmodyfikowana, że elementowi kontrolnemu będzie się wydawało, iż użytkownik klika w oczekiwanym miejscu. Innymi słowy, jeśli
użytkownik kliknie gdziekolwiek w obszarze listy, będziesz musiał tak zmodyfikować współrzędne punktu kliknięcia, aby wypadały one zawsze w pierwszej kolumnie odpowiedniego wiersza.
Modyfikowanie elementu kontrolnego
Aby móc poprawnie konwertować kliknięcia, będziesz musiał zdefiniować procedury obsługi komunikatów WM_LBUTTONUP, WM_LBUTTONDOWN oraz WM_LBUTTONDBLCLK. Pierwsza wersja procedury obsługującej wciśnięcie przycisku myszy mogłaby mieć następującą postać:
void CFullList::OnLButtonDown(UINT nFlags, CPoint point)
{
point.x = 0;
CListCtrl::OnLButtonDown(nFlags, point);
}
Wydaje się, że może to być całkiem dobre rozwiązanie - określenie, że kliknięcie nastąpiło w punkcie o współrzędnej x równej O, i wywołanie procedury obsługi klasy bazowej. Rozwiązanie takie powoduje jednak powstanie dwóch nowych problemów. Po pierwsze, przypisanie składowej point.x wartości O nie da oczekiwanego rezultatu. Dlaczego? Gdyż w rzeczywistości komunikat WM_LBUTTONDOWN nie jest obsługiwany w klasie CListCtrl. Domyślna procedura obsługi tego komunikatu powoduje przekazanie jego obsługi do elementu kontrolnego systemu Windows, reprezentowanego przez klasę CListCtrl.
Gdy MFC otrzymuje komunikat WM_LBUTTONDOWN, parametry wParam i lParam (przekazywane w komunikacie) przekształcane są do postaci parametrów nFlags oraz point przekazywanych do procedury obsługi komunikatu. Zmodyfikowanie wartości parametru point i przekazanie go do procedury obsługi zdefiniowanej w klasie bazowej da rezultaty tylko wtedy, gdy procedura ta będzie obsługiwać komunikat wewnątrz MFC. Jednakże w tym przypadku obsługa komunikatu przekazywana jest do elementu kontrolnego.
Do przekazania komunikatu wykorzystywana jest metoda o nazwie Default. Poniżej przedstawiony został jej kod:
LRESULT CWnd::DefaultO
{
// call DefWindowProc with the last message
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
return DefWindowProc(pThreadState->m_lastSentMsg.message,pThreadState->nv__lastSentMsg.wParam, pThreadState->m_lastSentMsg.IParam);
}
W metodzie tej warto zwrócić uwagę na dwie rzeczy. Po pierwsze, procedura DefWindowProc nie jest standardową funkcją systemu Windows o tej samej nazwie. Jest to metoda klasy CWnd używana do wywołania odpowiedniej funkcji systemowej (którą mogłoby być, na przykład, : :DefWindowProc). W naszym przypadku nie jest jednak wywoływana funkcja ::DefWindowProc, lecz domyślna procedura obsługi elementu kontrolnego listy.
Po drugie, zwróć uwagę, iż MFC nie koduje uprzednio odkodowanych parametrów komunikatu. Zamiast tego, MFC wykorzystuje oryginalne wartości parametrów wParam oraz lParam przechowywane w bloku danych wątku.
To właśnie z tego ostatniego powodu zmiana wartości składowej point.x nie wystarcza do wymuszenia odpowiedniego działania listy - element kontrolny listy otrzymuje oryginalne wartości parametrów wParam i lParam. W naszym wypadku klasa bazowa w ogóle nie przetwarza argumentów. Jednak, dla zachowania całkowitego bezpieczeństwa, najlepiej zrobisz zamieniając argumenty w obu miejscach (składowej point.x oraz w bloku danych przechowywanym w wątku):
void CFullList::OnLButtonDown(UINT nFlags, CPoint point)
{
int margin;
point.x = 0;
// obsługuje MFC
// obsłuż wszystkie inne przypadki
_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
pThreadState->m_lastSentMsg.lParam=MAKELONG(point.x, HIWORD(pThreadState->m_lastSentMsg.IParam));
CListCtrl::OnLButtonDown(nFlags, point);
}
Jednak nawet takie rozwiązanie nie zapewni poprawnego działania listy. Problem polega bowiem na tym, że z lewej strony każdego elementu listy tworzony jest pewien niewielki margines. Przypisanie wartości 0 współrzędnej x punktu kliknięcia powoduje, że element kontrolny uważa, kliknięty został margines, iż a nie sam element. Z powodzeniem mógłbyś odgadnąć szerokość tego marginesu, uważam jednak, że bezpieczniej będzie go obliczyć. Margines ten jest taki sam dla wszystkich elementów listy, jeśli więc obliczysz go dla jednego elementu, będziesz mógł go zapamiętać i używać podczas zaznaczania wszystkich pozostałych elementów listy. Poniżej przedstawiona została ostateczna wersja procedury obsługi komunikatu WM_LBUTTONDOWN:
void CFullList::OnLButtonDown(UINT nFlags, CPoint point)
{
int margin;
CRect r;
GetltemRect(O,&r,LVIR_LABEL);
margin=r.left;
point.x=margin;
// obsłuż MFC
// obsłuż wszystkie inne przypadki
_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
pThreadState->m_lastSentMsg.lParam=MAKELONG(point.x, HIWORD(pThreadState->m_lastSentMsg.IParam));
CListCtrl::OnLButtonDown(nFlags, point);
}
Zaznaczanie wybranego elementu listy
Zastosowanie powyższego fragmentu kodu do obsługi wszystkich wymienionych wcześniej komunikatów generowanych przez mysz, spowoduje rozwiązanie naszego pierwszego problemu. Jednak co zrobić z drugim problemem - zaznaczaniem całego wiersza, a nie jedynie jego pierwszej kolumny? Rozwiązanie tego problemu jest nieco trudniejsze i wymaga więcej sprytu. Najprostszym sposobem byłoby zaznaczenie wiersza samemu. Jednak wymaga to zbyt wiele pracy.
Prostszą metodą będzie pozwolenie elementowi kontrolnemu na zaznaczenie wyboru, a następnie zmodyfikowanie tego wyboru w odpowiedni sposób. Aby uprościć sprawę, postanowiłem zaznaczać wiersz listy poprzez narysowanie wokół niego prostokąta (spójrz na rysunek 4.2). W ten sposób pierwsza kolumna listy pozostanie podświetlona, a wszystkie pozostałe będą umieszczone wewnątrz narysowanego prostokąta.
Pierwszym krokiem będzie przesłonięcie oryginalnej wersji metody OnPaint. Kreator Class Wizard podpowie Ci, aby w nowej wersji metody nie wywoływać tej samej metody klasy bazowej. Tym razem nie posłuchasz podpowiedzi kreatora. Nie będziesz także stosował sugerowanego kontekstu urządzenia CPaintDC, gdyż kontekst ten wykorzystywany jest przez klasę bazową. Zamiast tego pobierzesz kontekst samemu, po zakończeniu rysowania przez klasę bazową.
Poniżej przedstawiony został kod służący do zaznaczania elementu:
void CFullList : rOnPaint ( )
CRect r;
int n,nO,nl;
CListCtrl::OnPaint O ;
nO=GetTop!ndex ( ) ;
nl=GetCountPerPage ( ) ;
nl+=nO;
CDC *dc=GetDC();
for (n=nO,-n! =nl;n++)
//nie używaj CPaintDC
if
(GetItemState(n.LVIS_SELECTED) !=LVIS_SELECTED) continue;
Getltemrect (n, &r , LVIR_BOUNDS) ;
r.InflateRect (-2, 0) ;
dc->SelectStockObject ( HOLLOW_BRUSH ) ;
dc->Rectangle (&r) ;
Pierwszą czynnością wykonywaną w tej metodzie jest wywołanie metody OnPaint klasy bazowej, dzięki czemu wybrany element zostanie zaznaczony w tradycyjny sposób, Następnie pobierany jest indeks pierwszego widocznego elementu oraz ilość elementów wyświetlanych na stronie (no oraz ni). Następnie w zmiennej ni umieszczany jest indeks ostatniego wyświetlanego elementu listy.
Po pobraniu kontekstu urządzenia (zapisanego w zmiennej dc klasy CDC) wykonywana jest pętla for, która sprawdza, czy któryś z widocznych elementów jest zaznaczony. Jeśli element nie jest zaznaczony, pętla przechodzi do analizy kolejnego elementu. Jeśli element jest zaznaczony, pobierany jest prostokąt zajmowany przez niego; następnie szerokość tego prostokąta jest zmniejszana (w celu lepszego dopasowania wielkości rysowanej ramki). Ostatnim krokiem jest narysowanie prostokąta wokół zaznaczanego wiersza listy. Jak widać, metoda ta jest nieporównywalnie prostsza od brania na siebie odpowiedzialności za cały mechanizm zaznaczania wybieranych elementów listy.
Wykorzystanie zmodyfikowanej listy
Pełny kod zmodyfikowanej listy przedstawiony został na listingu 4.1. Prościutki program demonstracyjny wykorzystujący tę listę wyświetla z góry określone dane.
Listing 4.1 . Zastosowanie zmodyfikowanej listy.
// FullList.cpp : implementation file
// A ListCtrl that allows you to select full lines
#include "stdafx.h"
#include "lister.h"
#include "FullList.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CFullList
CFullList::CFullList()
{
}
CFullList::~CFullList()
{
}
BEGIN_MESSAGE_MAP(CFullList, CListCtrl)
//{{AFX_MSG_MAP(CFullList)
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_LBUTTONDBLCLK()
ON_WM_PAINT()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// Procedury obsługi komunikatów klasy CFullList. Pomysł polega na tym, iż w momencie kliknięcia przesuwamy współrzędne punktu kliknięcia
// maksymalnie na lewo Punkt ten nie może mieć jednak współrzędnej 0, gdyż nie jest to poprawna wartość. Co więcej, w domyślnej procedurze
// obsługi komunikatu, MFC nie używa wartości przekazanej przez Ciebie. Używane są wartości podane w ostatnim komunikacie. Dlatego, jeśli klasa
// bazowa używa MFC możemy zmienić współrzędne punktu, jednak jeśli sterowanie przekazywane jest do okna (a jest) będziemy musieli
// zmodyfikować m_lastMsgSent w strukturze stanu wątku. Poniższy kod wykonuje obie czynności; na wszelki wypadek gdyby Microsoft zmienił
// pewnego danie kod klasy bazowej.
void CFullList::GoMargin(UINT &nFlags, CPoint &point)
{
int margin;
CRect r;
GetItemRect(0,&r,LVIR_LABEL);
margin=r.left;
point.x=margin; // obsłuż MFC
// obsłuż wszystkich innych
_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
pThreadState->m_lastSentMsg.lParam=
MAKELONG(point.x,HIWORD(pThreadState->
m_lastSentMsg.lParam));
}
void CFullList::OnLButtonDown(UINT nFlags, CPoint point)
{
GoMargin(nFlags,point);
CListCtrl::OnLButtonDown(nFlags, point);
}
// Same logic here
void CFullList::OnLButtonUp(UINT nFlags, CPoint point)
{
GoMargin(nFlags,point);
CListCtrl::OnLButtonUp(nFlags, point);
}
void CFullList::OnLButtonDblClk(UINT nFlags, CPoint point)
{
GoMargin(nFlags,point);
CListCtrl::OnLButtonDblClk(nFlags, point);
}
void CFullList::OnPaint()
{
CRect r;
int n,n0,n1;
CListCtrl::OnPaint(); // Niech element kontrolny sam się narysuje
// określ pierwszą i ostatnią wyświetlaną linię
n0=GetTopIndex();
n1=GetCountPerPage();
n1+=n0;
CDC *dc=GetDC(); // nie używaj CPaintDC
// Jeśli znajdziemy wybrany element, to narysujmy wokół niego prostokąt
for (n=n0;n!=n1;n++)
{
if (GetItemState(n,LVIS_SELECTED)!=LVIS_SELECTED) continue;
GetItemRect(n,&r,LVIR_BOUNDS);
r.InflateRect(-2,0); // zmniejszmy troszkę prostokąt
dc->SelectStockObject(HOLLOW_BRUSH);
dc->Rectangle(&r);
}
}
Aby zmusić widok formularza do użycia nowej klasy listy, będziesz musiał skojarzyć listę z Twoją nową klasą (CFullList) za pomocą kreatora Class Wizard. Poniżej przedstawiony został fragment kodu z pliku nagłówkowego widoku:
public :
/
/ { {AFX_DATA (CListerView)
enum
{ IDD = IDD_LISTER_VIEW } ;
CFullList m_list;
/
/ } } AFX_DATA
Elementy kontrolne używane w oknach dialogowych
Istnieje oczywiście więcej sposobów na stworzenie listy klasy CFullList. Jeśli będziesz potrzebował jedynie elementu kontrolnego, możesz posłużyć się metodą Create. Możesz obsługiwać proste okna dialogowe w taki sam sposób, w jaki przedstawiony wcześniej przykładowy program obsługuje widok CFormView (czyli za pomocą DDX; temat ten zostanie omówiony w Rozdziale 5).
Jest jeszcze jedna metoda pozwalająca Ci na zamianę istniejącego elementu kontrolnego listy na listę klasy CFullList; jest nią wyprowadzanie klas (ang.: subclassing). Aby wyprowadzić nową klasę istniejącego okna, stwórz obiekt klasy CFullList, a następnie wywołaj jego metodę SubclassWindow lub SubclassDlgItem. Obie te metody zakładają, że element kontrolny nie jest jeszcze skojarzony z żadnym obiektem klasy CWnd lub dowolnej z jej klas potomnych. Takie rozwiązanie mogłoby zostać zastosowane w przypadku, gdy element kontrolny został stworzony przez bibliotekę DLL, lub jeśli element umieszczony jest w oknie dialogowym, a Ty chcesz go jawnie skojarzyć z Twoją zmienną.
Dołączanie elementów kontrolnych okien dialogowych
W celu dołączenia elementu kontrolnego okna dialogowego do obiektu klasy potomnej klasy CWnd, możesz użyć metody SubclassDlgItem. Zazwyczaj jednak będziesz to robił za pomocą DDX. Jednak od czasu do czasu, możesz chcieć wykorzystać metodę SubclassDlgItem, dlatego najlepiej będzie, abyś poznał obie te metody postępowania.
Ogólne operacje na oknach
We wszystkich miejscach programu, gdziekolwiek byś nie spojrzał, możesz zauważyć okna. Sądzę, że właśnie dlatego firma Microsoft nazwała swój system operacyjny Windows. Jednak MFC bardzo często ukrywa te okna przed programistami. Kreator App Wizard tworzy ramki i widoki bez interwencji z Twojej strony. Jest to doskonałe i wygodne, aż do mementu, kiedy będziesz chciał coś zmienić.
MFC daje Ci wiele możliwości ingerowania w proces tworzenia okna (patrz tabela 4.1). Poprzez przejęcie kontroli nad fragmentem procesu tworzenia okna, możesz znacznie precyzyjniej określać wygląd oraz zachowanie tworzonych okien (w tym ramek, okien dialogowych, widoków, elementów kontrolnych i wszystkich innych elementów, które w rzeczywistości są oknami).
Tabela 4.1. Proces tworzenia okna.
MFC wywołuje Przesłoń gdy ...
CWnd::Create lub Chcesz podać wartości domyślne lub zastosować specyficzny sposób CWnd::CreateEx tworzenia okna.
CWnd::PreCreateWindow Chcesz zmodyfikować standardową strukturę CREATESTRUCT (jest to dobre miejsce na zarejestrowanie klasy).
CWnd::OnGetMinMaxInfo Chcesz zmienić minimalne lub maksymalne wymiary okna (metoda wywoływana wiele razy, nie tylko podczas procesu tworzenia okna).
CWnd::OnNCCreate Chcesz narysować coś poza obszarem roboczym okna (rzadko stosowana).
CWnd::OnCreate Chcesz obsługiwać komunikat WM_CREATE (okno już istniej, lecz jeszcze nie jest widoczne).
Określanie stylów oraz warunków początkowych
System operacyjny Windows pozwala Ci na modyfikowanie okien poprzez określanie bitów reprezentujących styl tworzonego okna. Dla przykładu, dzięki użyciu stylu WS_THICKFRAME, możesz stworzyć okno, którego wymiarów nie będziesz mógł zmieniać. Możesz także stworzyć okno, które od razu będzie zajmowało cały ekran (styl WS_MAXIMIZE). Jeśli chcesz umieścić na pasku tytułowym okna przycisk umożliwiający maksymalizację okna, wystarczy użyć stylu WS_MAXIMIZEBOX. Te, i wiele innych stylów, możesz połączyć i przekazać do wywołania metody CWnd::Create lub CWnd::CreateEx.
Co jednak zrobić, gdy zechcesz stworzyć okno, którego będą mogli używać inni programiści? Nie byłoby uprzejmie mówić im, jakich stylów powinni użyć przy tworzeni okna za pomocą metody Create. A co z oknami, które różne inne części MFC mogą automatycznie tworzyć w Twoim imieniu? Nie zawsze będziesz mógł zmodyfikować style, używane do tworzenia takich okien. Jednym z możliwych rozwiązań może być przesłonięcie metody Create i połączenie stylów podanych przez użytkownika, ze st; wykorzystywanymi przez Ciebie.
Znacznie częstszym rozwiązaniem stosowanym w MFC jest przesłonięcie metody PreCreateWindow. MFC wywołuje tę metodę tuż przed wywołaniem standardowej metody Create dostępnej w API systemu Windows. Parametrem przekazywanym do metody PreCreateWindow jest struktura CREATESTRUCT (patrz tabela 4.2). W strukturze tej umieszczone są wszystkie informacje potrzebne do stworzenia okna. Jeśli przesłonisz tę metodę, i wewnątrz jej nowej wersji zmienisz wartości zapisane w strukturze CREATESTRUCT, to podczas tworzenia nowego okna MFC użyje zmodyfikowanych wartości. W ten właśnie sposób możesz zapewnić zastosowanie odpowiedniego stylu okna (jak również innych warunków początkowych).
Tabela 4.2. Struktura CREATESTRUCT.
Składowa Typ Opis
lpCreateParams LPVOID Dostarczany przez użytkownika parametr o wielkości 32 bitów.
hInstance HINSTANCE Uchwyt do programu tworzącego okno.
hMenu HMENU Uchwyt do menu okna.
hwndParent HWND Uchwyt do okna rodzica.
cx int Szerokość okna.
cy int Wysokość okna.
x int Współrzędna x położenia lewego, górnego rogu okna.
y int Współrzędna y położenia lewego, górnego rogu okna.
style LONG Styl okna (np.: WS_OVERLAPPED).
lpszName LPCSTR Tytuł okna.
lpszClass LPCSTR Nazwa klasy (klasy systemu Windows, a nie klasy C++).
dwExStyle DWORD Rozszerzony styl okna.
Często, podczas tworzenia nowego okna, będziesz chciał dodać style, których chcesz użyć, lub usunąć te, które Ci nie odpowiadają. Dodawanie stylów realizowane jest za pomocą operatora alternatywy bitowej (OR; operator ten to: |); natomiast usuwanie stylów realizowane jest za pomocą operatora bitowej koniunkcji (AND; operator ten to: &). Tworzenie okna wymaga podania wielu stylów, a Ty rzadko będziesz chciał kontrolować je wszystkie; przeważnie będziesz się ograniczał do dodania lub usunięcia kilku wybranych stylów.
Jednym z pól struktury CREATESTRUCT jest pole zawierające nazwę klasy okna. Jest to klasa systemu Windows i nie ma ona absolutnie nic wspólnego z klasami MFC. System Windows dostarcza wielu predefiniowanych nazw klas (na przykład: BUTTON, EDIT, itp.). Możesz także tworzyć swoje własne klasy okien. Klasa okna określa kolor jego tła, funkcję obsługi komunikatów oraz kilka innych cech charakterystycznych okna. Jeśli w tym miejscu (lub wcześniej) sam nie określisz nazwy klasy okna, MFC automatycznie dobierze odpowiednią klasę.
Własne klasy okien
W Tabeli 4.3 przedstawiona została struktura WNDCLASS. W strukturze tej podawane są różnego typu informacje dotyczące klasy okna. Jeśli satysfakcjonują Cię wartości domyślne, to nie będziesz musiał rejestrować nowej klasy okna. Bardzo ważna może się wydawać procedura obsługi komunikatów; jednakże dzięki mapom komunikatów stosowanym w MFC, procedura ta nie jest istotna (przynajmniej dla programów korzystających z MFC).
W większości programów MFC, do rejestrowania klas wykorzystywana jest metoda AfxRegisterWndClass. Jako argumenty wywołania tej metody podawane są: styl klasy (jeśli taki jest), uchwyt do kursora, uchwyt do pędzla używanego do czyszczenia tła okna oraz uchwyt do ikony okna. W wyniku wykonania, metoda ta zwraca nazwę klasy okna, która spełnia podane przez Ciebie wymagania. Możesz wywoływać tę metodę dowolną ilość razy, i, o ile podasz te same argumenty, w wyniku będziesz otrzymywał tę samą nazwę klasy. Jaka to będzie nazwa? W zasadzie dokładna nazwa klasy nie ma dla Ciebie specjalnego znaczenia -jedyne co musisz zrobić, to przekazać ją do wywołania metody Create lub CreateEx, dzięki czemu stworzone zostanie okno o odpowiednich cechach.
Jeśli zechcesz kontrolować nazwę klasy, będziesz musiał wywołać metodę AfxRegisterClass i wypełnić strukturę WNDCLASS (patrz tabela 4.3). W przypadku programów EXE, powyższe czynności odpowiadają wywołaniu standardowej funkcji ::RegisterClass dostępnej w API systemu Windows. Jednak, w przypadku bibliotek DLL, metoda AfxRegisterClass wykonuje pewne dodatkowe czynności; dlatego też warto wyrobić sobie nawyk rejestrowania klasy za pomocą tej właśnie metody.
Tabela 4.3. Struktura WNDCLASS.
Składowa Typ Opis
style UINT Styl okna
lpfnWndProc WNDPROC Wskaźnik na procedurę obsługi komunikatów która będzie wykorzystywana w tej klasie okna
cbClsExtra int Ilość dodatkowych bajtów rezerwowanych w klasie na potrzeby użytkownika
cbWndExtra int Ilość dodatkowych bajtów rezerwowanych w oknie na potrzeby użytkownika
hInstance HINSTANCE Uchwyt do programu tworzącego okno
hIcon HICON Uchwyt do ikony
hCursor HCURSOR Uchwyt do domyślnego kursora
hbrBackground HBRUSH Uchwyt do pędzla stosowanego do czyszczenia tła okna
lpszMenuName LPCSTR Nazwa menu okna
lpszClassName LPCSTR Nazwa jaka ma zostać nadana tej klasie
Dlaczego miałbyś zwracać uwagę na nazwę klasy okna? W zasadzie nie ma ku temu żadnego powodu. Jednak jeśli będziesz musiał używać tworzonej klasy w oknie dialogowym, powinieneś znać jej nazwę (więcej informacji na temat używania własnych okien w oknach dialogowych znajdziesz w Rozdziale 5).
Ograniczanie wielkości okna
Jedną z najprostszych operacji, jaką możesz wykonać na oknie ramki, jest ograniczenie jej wymiarów. Oczywiście, jeśli chcesz żeby okno to miało stałą wielkość, możesz ją określić podczas tworzenia okna, i wymusić, aby okno miało cienką ramkę uniemożliwiającą zmianę jego wielkości. Jednak, okno stworzone w taki sposób wygląda dosyć dziwnie. Rozwiązanie nie pozwala także na stworzenie okna, którego rozmiary mogłyby się zmieniać w pewnych określonych granicach.
Starając się rozwiązać ten problem mógłbyś wpaść na pomysł obsługiwania komunikatu WM_SIZE. Nie jest to jednak najlepsze rozwiązanie, gdyż komunikat ten generowany jest/7o zmianie wielkości okna. Oczywiście, jeśli wielkość okna nie będzie Ci odpowiadała, możesz ponownie ją zmienić, jednak spowoduje to bardzo drażniący efekt wizualny. Poza tym, powtórna zamiana wielkości okna spowoduje wygenerowanie kolejnego komunikatu WM_SIZE, dzięki czemu będziesz miał doskonałą szansę stworzenia nieskończonej pętli komunikatów.
Na szczęście istnieje znacznie lepsze rozwiązanie problemu ograniczania wielkości okna. Aby je poznać wystarczy przyjrzeć się kolejnym etapom tworzenia okien (patrz tabela 4.1). Zwróć uwagę na wywołanie metody OnGetMinMaxInfo (odpowiadające obsłudze komunikatu WM_GETMINMAXINFO). System Windows generuje ten komunikat (czyli wywołuje metodę OnGetMinMaxInfo) za każdym razem, gdy chce się dowiedzieć jakie mogą być minimalne i maksymalne wymiary okna. Komunikat ten generowany jest podczas tworzenia nowego okna, modyfikowania wielkości okna przez użytkownika i w kilku innych wypadkach.
Metoda OnGetMinMaxInfo powoduje zapisanie odpowiednich informacji do specjalnej tablicy. W tablicy tej możesz podać: minimalną dopuszczalną wielkość okna, maksymalną dopuszczalną wielkość okna, wielkość “zmaksymalizowanego" okna, oraz pozycję lewego górnego rogu ,,zmaksymalizowanego" okna. Zazwyczaj okno “zmaksymalizowane" kojarzy się z oknem zajmującym całą powierzchnię ekranu, jednak nie zawsze tak jest. Dla przykładu, spróbuj zmaksymalizować okno Trybu MS-DOS. Okno zostanie powiększone w taki sposób, aby mieściło się w nim 80 znaków w wierszu i 25 w kolumnie; nie da się go jednak bardziej powiększyć; nie miałoby to żadnego sensu.
Teraz, gdy już wiesz od czego służy metoda OnGetMinMaxInfo, ograniczenie wielkości okna staje się banalnie proste. Wystarczy wywołać tę metodę klasy bazowej, aby poznać ustawienia domyślne, a następnie zmodyfikować je tak, aby spełniały Twoje wymagania.
System Windows wywołuje tę metodę za każdym razem, gdy zmieniana jest wielkość okna. Dzięki temu będziesz mógł zmieniać dopuszczalne wielkości okna podczas działania programu. Na przykład, możesz chcieć stworzyć program, którego okno podczas inicjalizacji będzie miało wymiary 100 x 100 pikseli. Dopiero gdy pobrane zostaną dane, użytkownik będzie mógł zwiększyć okno do 100 x 600 pikseli. Dzięki zastosowaniu metody OnGetMinMax stworzenie programu działającego w taki sposób nie nastręcza żadnych problemów.
Jeśli kiedykolwiek będziesz chciał stworzyć okno o ściśle określonych wymiarach, pamiętaj, aby określać jego wielkość na podstawie obszaru zajmowanego przez całe okno. Jedynie część tego obszaru zajmuje obszar roboczy okna. Jeśli wiesz, jaka jest wielkość obszaru roboczego, możesz obliczyć wielkość całego okna za pomocą funkcji ::AdjustWindowRectEx.
Określenie wielkości widoku nie jest takie proste i wymaga zastosowania pewnej sztuczki. Problem polega na tym, że widoki umieszczane są wewnątrz ramek. Oznacza to, że będziesz musiał tak powiększyć wielkość okna aby zmieścił się w nim cały widok. Sam widok także posiada ramkę, w związku z czym będziesz musiał wywołać metodę ::AdjustWindowRectEx dwa razy - pierwszy raz po to, aby obliczyć wielkość widoku, a drugi, aby obliczyć wielkość ramki. Załóżmy, że chciałbyś stworzyć widok o wielkości 200x200 pikseli. Jak już mówiliśmy, będziesz musiał wywołać funkcję ::AdjustWindowRectEx, jednakże w jej wywołaniu będziesz musiał podać styl okna. Ale jak możesz określić styl okna? Cóż, jeśli widok został już stworzony, to możesz posłużyć się metodami GetStyle oraz GetStyleEx. Jednakże co zrobić, jeśli widok, któremu chcesz nadać odpowiednią wielkość, jeszcze nie istnieje?
Nie jest to tak wielki problem, jak by się mogło wydawać. Style nie zmieniają się tak często, a przynajmniej nie powinny się zmieniać w tej samej wersji MFC. Dlatego wystarczy odczytać je i na stałe zakodować w programie. Równie dobrze możesz zbadać okno widoku za pomocą programu Spy++ (lub dowolnego innego programu tego typu), i w ten sposób określić styl okna. Jednakże najlepszym wyjściem z sytuacji będzie zastosowanie stałej AFX_WS_DEFAULT_VIEW (zdefiniowanej w pliku nagłówkowym AFXRES.H
Załóżmy, że w wyniku wywołania funkcji ::AdjustWindowRectEx dowiedziałeś się, iż widok powinien mieć wymiary 202x202 pikseli. Bądź ostrożny interpretując te wyniki. Prostokąt wynikowy może się kończyć w punkcie o współrzędnych 201x201. Co więcej, odkryjesz, że lewy górny róg widoku znajduje się w punkcie o współrzędnych -l, -l (dokładne wartości mogą się zmieniać w zależności od systemu). Oznacza to, że część widoku będzie pozostawała niewidoczna. W takiej sytuacji, aby wewnętrzna (widoczna) część widoku miała wymiary 200x200 pikseli, jego całość musi mieć wielkość 202x202.
Dysponując tymi informacjami możesz drugi raz wywołać funkcję ::AdjustWindowRectEx, przekazując jako parametr jej wywołania styl ramki. Pamiętaj, aby poprawnie określić, czy ramka ma pasek menu. Oczywiście, ramki MDI oraz widoki nigdy nie mają paska menu. Z kolei ramki SDI zazwyczaj go posiadają. Drugie wywołanie funkcji ::AdjustWindowRectEx może zwiększyć wielkość prostokąta opisującego wielkość okna z 202x202 pikseli do 210x242 pikseli. I to będą dopiero wymiary określające wielkość okna ramki, w którym widok będzie miał wymiary 200x200 pikseli. Przykład opisanego powyżej określania wielkości widoku, został przedstawiony na listingu 4.2.
Listing 4.2. Widok o wymiarach 200x200 pikseli.
// sizeView.cpp : implementation of the CSizeView class
//
#include "stdafx.h"
#include "size.h"
#include "sizeDoc.h"
#include "sizeView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CSizeView
IMPLEMENT_DYNCREATE(CSizeView, CView)
BEGIN_MESSAGE_MAP(CSizeView, CView)
//{{AFX_MSG_MAP(CSizeView)
// NOTE - 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()
/////////////////////////////////////////////////////////////////////////////
// CSizeView construction/destruction
CSizeView::CSizeView()
{
// TODO: add construction code here
}
CSizeView::~CSizeView()
{
}
BOOL CSizeView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CSizeView drawing
void CSizeView::OnDraw(CDC* pDC)
{
CSizeDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// narysuj dwie linie po 200 punktów każda
// zakończone strzałkami.
pDC->MoveTo(5,194);
pDC->LineTo(10,199);
pDC->LineTo(10,0);
pDC->LineTo(5,5);
pDC->MoveTo(10,0);
pDC->LineTo(15,5);
pDC->MoveTo(10,199);
pDC->LineTo(15,194);
pDC->MoveTo(5,95);
pDC->LineTo(0,100);
pDC->LineTo(199,100);
pDC->LineTo(194,95);
pDC->MoveTo(199,100);
pDC->LineTo(194,105);
pDC->MoveTo(0,100);
pDC->LineTo(5,105);
}
/////////////////////////////////////////////////////////////////////////////
// CSizeView diagnostics
#ifdef _DEBUG
void CSizeView::AssertValid() const
{
CView::AssertValid();
}
void CSizeView::Dump(CDumpContext& dc) const
{
CView::Dump(dc);
}
CSizeDoc* CSizeView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CSizeDoc)));
return (CSizeDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CSizeView message handlers
void CSizeView::OnInitialUpdate()
{
DWORD style,exstyle;
CRect framerect, viewrect(0,0,200,200);
CFrameWnd *frame=GetParentFrame();
style=GetStyle();
exstyle=GetExStyle(); // style widoku!
::AdjustWindowRectEx(&viewrect,style,FALSE,exstyle);
style=frame->GetStyle();
exstyle=frame->GetExStyle(); // style ramki!
// UWAGA: ramki SDI mają menu (TRUE), ale ramki potomne MDI nie mają menu (FALSE)
::AdjustWindowRectEx(&viewrect,style,TRUE,exstyle);
frame->GetWindowRect(&framerect); // określ lewy górny róg
framerect.right=framerect.left+viewrect.Width();
framerect.bottom=framerect.top+viewrect.Height();
frame->MoveWindow(&framerect);
CView::OnInitialUpdate();
}
Określanie tytułu
Określenie tytułu ramki jest proste, prawda? Przynajmniej mogłoby się tak wydawać. Wystarczy wywołać metodę SetWindowText dla odpowiedniego okna. Tytuł okna, możesz także określić w łańcuchu dokumentu (patrz Rozdział l, tabela 1.12). Zdarza się jednak, że MFC robi rzeczy, które mogą wyprowadzić Cię z równowagi. Jedną z nich jest dodawanie nazwy otworzonego dokumentu do tytułu okna.
Jeśli Twój program rzeczywiście obsługuje jakieś dokumentu, to takie postępowanie jest całkiem sensowne. Jednakże wszystko wygląda inaczej gdy tworzysz grę planszową i nagle okazuje się, że ma ona tytuł: BitZapper-Untitled. Rozwiązaniem jest wyczyszczenie bitu FWS_ADDTOTITLE w stylu okna ramki. Do wykonania tego zadania najlepiej nadaj się metoda PreCreateWindow:
BOOL CMyFrame::PreCreateWindow (CREATESTRUCT Łcs)
BOOL rv=CFrameWnd::PreCreateWindow (es) ;
es . styleŁ=~FWS_ADDTOTITLE;
// metoda klasy bazowej
return rv;
Jeśli chcesz, aby nazwa dokumentu wyświetlana była z lewej strony nazwy okna, w stylu okna ramki ustaw bit FWS_PREFIXTITLE.
Kilka uwag o stylu FWS_ADDTOTITLE
Zauważyłem, że często używam stylu FWS_ADDTOTITLE razem z widokami formularza. Większość tworzonych przeze mnie widoków tego typu nie operuje na żadnych plikach, i zazwyczaj chcę im nadawać ściśle określony tytuł. Jeśli piszesz programy przeznaczone dla systemu Windows 3.1, to przez zanegowaniem wartości stylu FWS_ADDTOTITLE będziesz musiał skonwertować ten styl do typu DWORD; w przeciwnym bowiem razie niektóre bity tej wartości nie zostaną poprawnie ustawione.
Stosowanie UpdateCMDUI
Kontrolowanie postaci tytułów i atrybutów elementów kontrolnych często wymaga zastosowania procedury obsługi OnUpdateCmdUI (patrz Rozdział 1). Za pomocą tej metody MFC nieustannie aktualizuje postać opcji menu, przycisków na paskach narzędzi oraz innych elementów kontrolnych. Dlatego też wszelkie modyfikacje tych elementów muszą zostać wykonane wewnątrz niej (jeśli oczywiście chcesz, aby były one widoczne przez dłuższy czas).
Klasa CScrollView
Spośród wszystkich widoków udostępnianych przez MFC, największe możliwości ma widok CScrolIView. Niestety, próby jego zastosowania w normalnych programach przysparzaj ą także najwięcej problemów i frustracji. Dlaczego? Istnieją dwa, podstawowe problemy związane z zastosowanie widoku CScrollView. Po pierwsze, widok ten nie udostępnia żadnych wbudowanych mechanizmów przewijania swojej zawartości za pomocą klawiatury. Po drugie, wszelkie próby nadania widokowi wirtualnej wielkości powyżej 32767 powoduje dziwne zachowanie w systemie Windows 95.
Możesz pomyśleć, że nigdy nie będziesz potrzebował widoku o takiej wielkości, Zapewne masz rację, pomyśl jednak o tekście. Każda linia tekstu wyświetlonego w widoku zabiera określoną ilość pikseli (powiedzmy: 16). Oznacza to, że używając trybu mapowania MM_TEXT, możesz stworzyć widok zawierający jedynie 2048 linii tekstu. Im większy będzie rozmiar czcionki, tym mniej linii tekstu będziesz mógł umieścić w widoku.
Zanim zajmiemy się rozwiązaniem tego problemu, przyjrzyjmy się jednak działaniu klasy CScrollView. Mówiąc jak najprościej, CScrollView okłamuje twój program w taki sposób, że nie jesteś świadomy przewijania wykonywanego przez widok. Widokowi podajesz jedynie całkowitą, logiczną wielkość dokumentu - czyli określasz jak duża ma być sumaryczna wielkość widoku (bez zwracania uwagi na aktualną wielkość okna). Dodatkowo, za pomocą metody SetScrollSizes, określasz tryb mapowania, którego chcesz używać, szerokość linii, oraz wielkość strony.
Pierwszą rzeczą jaką robi przewijalny widok, jest określenie, czy logiczna wielkość dokumentu jest mniejsza od fizycznej wielkości okna. Jeśli dokument jest większy od okna, w widoku wyświetlane są paski przewijania. Jest całkiem prawdopodobne, że Twój dokument będzie mniejszy od wymiarów okna w jednym wymiarze (na przykład w poziomie), natomiast większy w drugim (na przykład w pionie). W takim wypadku wyświetlony zostanie tylko jeden pasek przewijania.
Jeśli paski przewijania nie są wyświetlone lub jeśli są one ustawione w pozycji początkowej, nie dzieje się nic specjalnego. Wszystkie rysowane informacje wyświetlane są przez Twoją metodę OnDraw. A co się dzieje, jeśli masz więcej informacji, nie zmieszczą się one na jednym ekranie? Nic, system Windows nie wyświetli informacji wychodzących poza widok. Jednak nie jest to dramatyczna sytuacja, gdyż rysowanie informacji, których system nie wyświetla (gdyż znajdują się one poza obszarem przycinania), jest znacznie szybsze od rysowania, którego efekty widzimy na ekranie. W dalszej części rozdziału pokażę Ci, w jaki sposób można poprawić efektywność rysowania. Należy powiedzieć jeszcze jedną, bardzo istotną rzecz: Otóż Twoja metoda OnDraw nie powinna ustawiać trybu mapowania. Tryb mapowania jest automatycznie ustawiany przez metodę OnPrepareDC klasy CScrollView; będzie to ten sam styl, który podałeś w wywołaniu metody SetScrollSizes.
Załóżmy teraz, że użytkownik przewinie widok w dół, do połowy wysokości. Twoja metoda OnDraw nie zauważy żadnej różnicy. Jednakże widok CScrollView tak przekształci współrzędne początku układu współrzędnych okna oraz widoku, że 50 procent zawartości dokumentu znajdzie się ponad oknem. I znowu, system Windows odrzuci niewidoczne części wyświetlanego dokumentu. W którymś momencie rysowania zawartości dokumentu dojdziesz to tej jego części, która będzie się znajdowała wewnątrz obszaru przycinania ta część dokumentu zostanie narysowana i wyświetlona na ekranie. Jeśli jakaś część dokumentu znajdzie się poniżej obszaru przycinania, zostanie ona odrzucona przez system.
Jeśli Twój program jest dostatecznie prosty, tak będą wyglądały wszystkie wykonywane czynności. Określ wielkości pasków przewijania i za każdym razem wyświetlaj zawartość całego dokumentu. Jeśli wielkość Twojego dokumentu nigdy nie będzie modyfikowana, możesz wywołać metodę SetScrollSizes w metodzie OnlnitialUpdate; możesz także dynamicznie modyfikować wielkości dokumentu w metodzie OnUpdate.
Istnieją dwa wypadki, w których musisz wziąć pod uwagę pozycję wyświetlanego obszaru dokumentu. Pierwszym z nich jest obsługa komunikatów generowanych przez mysz. Komunikaty te zawsze podają pozycję wskaźnika myszy w pikselach (w trybie mapowania MM_TEXT). Koordynaty przekazane przez komunikat będziesz musiał powiększyć o wielkość przesunięcia pasków przewijania. Możesz to zrobić w bardzo prosty sposób posługując się wartościami zwróconymi przez metodę GetDeviceScrollPosition. Następnie będziesz musiał przeliczyć piksele na używane przez Ciebie jednostki logiczne (chyba że tymi jednostkami logicznymi są piksele - w takim wypadku żadnego przeliczania nie będziesz musiał wykonywać).
Drugim przypadkiem, w którym wszystko się troszkę komplikuje, jest pobieranie kontekstu (obiektów klasy CDC) poza metodą OnDraw. Klasa ScrollView automatycznie określa pozycję początku układu współrzędnych w kontekście CDC przekazywanym do metody OnDraw. Załóżmy teraz, że chciałbyś narysować coś na ekranie, bezpośrednio wewnątrz procedury obsługi jakiegoś komunikatu wygenerowanego przez mysz (jest to bardzo często spotykane rozwiązanie, szczególnie w przypadku rysowania prostokątów wokół zaznaczanych elementów). Jeśli wywołasz metodę GetDC, a nie wywołasz metody OnPrepareDC, to wyniki rysowania nie będą zadowalające. Oznacza to także, że nie będziesz musiał wywoływać metody SetMapMode obiektu kontekstu urządzenia (CDC).
Zarówno pominięcie wywołania metody OnPrepareDC, jak i pominięcie zmodyfikowania współrzędnych punktu przekazanego w komunikacie myszy, daje takie same objawy. Wszystko będzie działało bez zarzutu, o ile tylko oba paski przewijanie umieszczone będą w pozycjach początkowych (zerowych). Jeśli jednak paski przewijania nie będą się znajdowały w pozycjach początkowych, wszystko będzie troszkę przesunięte. Im dalej od pozycji początkowej zostaną przewinięte paski, tym większe będzie przesunięcie w widoku.
Warto wspomnieć, że możesz wywołać metodę SetScaleToFitSize, co spowoduje takie przeskalowanie dokumentu, że zmieści się on cały w oknie widoku. Oczywiście, nie ma to nic wspólnego z przewijaniem.
Umożliwianie przewijania za pomocą klawiatury
Pierwszy problem obsługi widoków klasy CScrollView — brak możliwości przewijania zawartości widoku za pomocą klawiatury jest najprawdopodobniej najprostszy do rozwiązania. Na listingu 4.3 przedstawiony został przykład widoku, w którym zaimplementowano metodę OnKeyDown. Gdy widok otrzymuje komunikat o wciśnięciu klawisza, wywoływana jest metoda KeyScroll, która bada kod wirtualny klawisza, i na jego podstawie generuje odpowiednie zdarzenie przewijania. Metodę KeyScroll będziesz mógł bez kłopotów skopiować i wkleić do własnego widoku.
Listingu 4.3. Przewijanie za pomocą klawiatury
// scrollerView.cpp : implementation of the CScrollerView class
#include "stdafx.h"
#include "scroller.h"
#include "scrollerDoc.h"
#include "scrollerView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
#define METHOD 1 // 0 - zopytmalizowane; 1 - normalne po linii
/////////////////////////////////////////////////////////////////////////////
// CScrollerView
IMPLEMENT_DYNCREATE(CScrollerView, CScrollView)
BEGIN_MESSAGE_MAP(CScrollerView, CScrollView)
//{{AFX_MSG_MAP(CScrollerView)
ON_WM_SIZE()
ON_WM_VSCROLL()
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CScrollView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CScrollView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CScrollView::OnFilePrintPreview)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CScrollerView construction/destruction
CScrollerView::CScrollerView()
{
// TODO: add construction code here
}
CScrollerView::~CScrollerView()
{
}
BOOL CScrollerView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CScrollView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CScrollerView drawing
void CScrollerView::OnDraw(CDC* pDC)
{
#if 0 // normalne
CScrollerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int ct=sizeof(pDoc->list)/sizeof(pDoc->list[0]);
pDC->SetBkMode(TRANSPARENT);
for (int i=0;i<ct;i++)
{
CString s;
s.Format("(%d)=%u",i,pDoc->list[i]);
pDC->TextOut(0,i*lineht,s);
}
#endif
#if METHOD==0 // zoptymalizowane rysowanie
CScrollerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int ct;
pDC->SetBkMode(TRANSPARENT);
int start=GetDeviceScrollPosition().y/lineht-1;
if (start<0) start=0;
CRect client;
GetClientRect(&client);
// rysuj trochę wicęj, aby przewijanie nie działało na pełnych liniach
ct=start+client.Height()/lineht+3;
if (ct>sizeof(pDoc->list)/sizeof(pDoc->list[0]))
ct=sizeof(pDoc->list)/sizeof(pDoc->list[0]);
for (int i=start;i<ct;i++)
{
CString s;
s.Format("(%d)=%u",i,pDoc->list[i]);
pDC->TextOut(0,i*lineht,s);
}
#endif
#if METHOD==1
CScrollerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int ct;
pDC->SetBkMode(TRANSPARENT);
int start=GetDeviceScrollPosition().y;
CRect client;
GetClientRect(&client);
// rysuj trochę wicęj, aby przewijanie nie działało na pełnych liniach
ct=start+client.Height()/lineht+3;
if (ct>sizeof(pDoc->list)/sizeof(pDoc->list[0]))
ct=sizeof(pDoc->list)/sizeof(pDoc->list[0]);
pDC->SetMapMode(MM_TEXT); // usuń fałszywy tryb mapowania
pDC->SetViewportOrg(0,0);
pDC->SetWindowOrg(0,0);
for (int i=start;i<ct;i++)
{
CString s;
s.Format("(%d)=%u",i,pDoc->list[i]);
pDC->TextOut(0,(i-start)*lineht,s);
}
#endif
}
void CScrollerView::OnInitialUpdate()
{
#if METHOD==0
CScrollView::OnInitialUpdate();
CScrollerDoc* doc = GetDocument();
CSize sizeTotal,sizePg;
CRect r;
GetClientRect(&r);
CDC *pDC=GetDC();
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lineht=tm.tmHeight+tm.tmExternalLeading;
sizeTotal.cx = 100;
sizeTotal.cy = (sizeof(doc->list)/sizeof(doc->list[0]))*lineht;
sizePg.cx=10;
sizePg.cy=r.Height();
SetScrollSizes(MM_TEXT, sizeTotal,sizePg,CSize(10,lineht));
#endif
#if METHOD==1
CScrollView::OnInitialUpdate();
CScrollerDoc* doc = GetDocument();
CSize sizeTotal,sizePg;
CRect r;
GetClientRect(&r);
CDC *pDC=GetDC();
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lineht=tm.tmHeight+tm.tmExternalLeading;
sizeTotal.cx = 100;
sizeTotal.cy = sizeof(doc->list)/sizeof(doc->list[0])+r.Height()-r.Height()/lineht;
sizePg.cx=10;
sizePg.cy=r.Height()/lineht;
SetScrollSizes(MM_TEXT, sizeTotal,sizePg,CSize(10,1));
#endif
}
/////////////////////////////////////////////////////////////////////////////
// CScrollerView printing
BOOL CScrollerView::OnPreparePrinting(CPrintInfo* pInfo)
{
// default preparation
return DoPreparePrinting(pInfo);
}
void CScrollerView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add extra initialization before printing
}
void CScrollerView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add cleanup after printing
}
/////////////////////////////////////////////////////////////////////////////
// CScrollerView diagnostics
#ifdef _DEBUG
void CScrollerView::AssertValid() const
{
CScrollView::AssertValid();
}
void CScrollerView::Dump(CDumpContext& dc) const
{
CScrollView::Dump(dc);
}
CScrollerDoc* CScrollerView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CScrollerDoc)));
return (CScrollerDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CScrollerView message handlers
void CScrollerView::OnSize(UINT nType, int cx, int cy)
{
#if METHOD==0
int dummy;
CScrollView::OnSize(nType, cx, cy);
CSize sizeTotal,pg,dummysize;
CRect r;
GetClientRect(&r);
pg=CSize(10,r.Height());
GetDeviceScrollSizes(dummy,sizeTotal,dummysize,dummysize);
SetScrollSizes(MM_TEXT, sizeTotal,pg,CSize(10,lineht));
#endif
#if METHOD==1
CScrollView::OnSize(nType, cx, cy);
CSize sizeTotal,pg;
CRect r;
CScrollerDoc *doc=GetDocument();
GetClientRect(&r);
pg=CSize(10,r.Height()/lineht);
sizeTotal.cx=100;
sizeTotal.cy=sizeof(doc->list)/sizeof(doc->list[0])+r.Height()-r.Height()/lineht;
SetScrollSizes(MM_TEXT, sizeTotal,pg,CSize(10,1));
#endif
}
void CScrollerView::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
CScrollView::OnVScroll(nSBCode, nPos, pScrollBar);
#if METHOD==1 // Trzeba odwołać sposób obsługi przewijania
InvalidateRect(NULL);
#endif
}
W bardzo prosty sposób można zmodyfikować metodę KeyStroke i dodać do niej obsługę innych klawiszy. Dodatkowo możesz badać stan takich klawiszy jak Shift oraz Ctrl. Dla przykładu, możesz w różny sposób obsługiwać naciśnięcie klawisza HOME oraz kombinacji klawiszy Ctrl+HOME. Innym sposobem jest umożliwienie przewijania tylko wtedy, gdy wciśnięty jest klawisz Shift. Wszystkich tych modyfikacji możesz dokonać wywołując metodę GetKeyState i testując stan interesujących Cię klawiszy. Pamiętaj, że metoda GetKeyState nie zwraca wartości TRUE lub FALSE. W przypadku gdy testowany klawisz jest wciśnięty, metoda ta zwraca wartość, której najwyższy bit jest ustawiony. Najprostszym sposobem sprawdzenia, czy klawisz jest wciśnięty, jest sprawdzenie, czy wartość zwrócona przez metodę GetKeyState jest mniejsza od zera. Jeśli jest, oznacza to, że klawisz jest wciśnięty.
Optymalizacja przewijania
Program przedstawiony na Rysunku 4.3 wyświetla na ekranie 100 liczb. W przypadku takiego programu żadna optymalizacja nie jest konieczna. Nawet jeślibyś powiększył ilość wyświetlanych liczb do 1000, nie powinieneś zauważyć żadnego zmniejszenia efektywności pracy programu. Jeśli jednak zwiększysz ilość wyświetlanych liczb do 10000, zauważysz znaczne zwolnienie pracy programu. Zauważysz także, że w systemie Windows 95 zawartość okna programu wygląda w dosyć dziwny sposób (spójrz na rysunek 4.3). Problem niepoprawnego wyświetlania informacji zostanie omówiony w następnej części rozdziału.
MFC nie wymaga od Ciebie, aby w widoku CScrollView rysowana była tylko widoczna część widoku; nie ma jednak żadnego powodu, dla którego nie mógłbyś tak robić. Postępowanie takie jest bardzo dobrym pomysłem, szczególnie, jeśli masz bardzo dużo danych, lub jeśli proces ich wyświetlania jest skomplikowany.
Pomysł jest bardzo prosty: wystarczy określić pozycję początku wyświetlanego obszaru (za pomocą metody GetDeviceScrollPosition) i nie wyświetlać żadnych danych, które umieszczone są ponad tą pozycją. Następnie należy określić wielkość obszaru roboczego okna (GetClientRect) i nie wyświetlać niczego, co znajduje się w dokumencie poniżej tego obszaru. Poniżej przedstawiona została zoptymalizowana wersja metody OnDraw programu wyświetlającego liczby parzyste:
void CScrollerView::OnDraw(CDC* pDC)
{
CScrollerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int et;
pDC->SetBkMode(TRANSPARENT);
int start=GetDeviceScrollPosition().y/lineht-1;
if (start<0) start=0;
CRect client;
GetClientRect(Łclient);
// rysuj trochę wicęj, aby przewijanie nie działało na pełnych liniach
ct=start+client .Height () /lineth+3 ,-
if (ct>sizeof(pDoc->list)/sizeof(pDoc->list[0]))
ct=sizeof(pDoc->list)/sizeof(pDoc->list[0]); for (int i=start;i<ct;i++)
{
CString s;
s.Format("(%d)=%u" , i, pDoc->list[i]) ;
pDC->TextOut(O,i*lineth,s);
}
W praktyce będziesz musiał zwracać uwagę na to, aby także rysować linie, które są przynajmniej częściowo widoczne. Powodem tego są próby automatycznej optymalizacji rysowania wykonywane za Ciebie przez widok CScrollView. Gdziekolwiek jest to możliwe widok ten przesuwa narysowane już dane i prosi Cię jedynie o wyświetlenie zawartości zmienionych obszarów. Jeślibyś nie rysował linii częściowo wyświetlonych na ekranie, mogłoby to doprowadzić do powstania przerw w prezentowanej zawartości dokumentu. Jeśli będziesz chciał zobaczyć ten efekt, usuń -Iz obliczenia wartości zmiennej start oraz +3 z obliczenia wartości zmiennej ct.
Praktyczne zastosowanie optymalizacji
W większości wypadków nie będziesz musiał przejmować się optymalizowaniem wyświetlania danych w widoku klasy CScrollView. Jedynym wyjątkiem od tej reguły jest sytuacja, gdy chcesz wyświetlić bardzo skomplikowane dane (na przykład podczas rysowania fraktala), których pobranie zajmuje dużo czasu lub gdy musisz przedstawić bardzo dużo danych (patrz następna sekcja). W przypadku prostych dokumentów, optymalizowanie wyświetlania danych zazwyczaj nie jest opłacalne.
Przewijanie o więcej niż 32K jednostek
Powróćmy na chwilę do przykładu przestawionego w poprzedniej części rozdziału. Jeśli będziesz chciał w nim wyświetlić bardzo dużą liczbę parzystych liczb (na przykład 10000), i jeśli dodatkowo korzystasz z systemu Windows 95, zauważysz, że program nieprawidłowo wyświetla dane (patrz rysunek 4.3). Powodem takiego zachowania programu, jest przekroczenie wartości 32767 przez logiczną ilość pikseli w dokumencie. Chociaż system Windows 95 pozwala na stosowanie 32 bitowych współrzędnych, przed ich przetworzeniem górne 16 bitów zostaje odrzucone.
Powoduje to powstanie poważnego problemu, który będą musieli rozwiązać wszyscy programiści, którzy chcą używać widoku klasy CScrollView. W jaki sposób możesz użyć widoku tej klasy do wyświetlenia dokumentu o wielkości przekraczającej 32767 jednostek logicznych? Niestety, odpowiedź na to pytanie nie jest prosta. W rzeczywistości, dla wielu programistom znaczenie prościej będzie ręcznie obsługiwać przewijanie w normalnym widoku, niż naprawiać działanie widoku CScrollView. Bez większego problemu możesz zmusić widok klasy CScrollView do obsługi około 30000 jednostek logicznych. Czy to jednak coś zmienia? Załóżmy, że linia ma 16 pikseli wysokości, w takim razie 30000 linii będzie zajmowało 480000 pikseli. Nie jesteś w stanie obsłużyć takiej ilości danych za pomocą widoku CScrollView działającego w systemie Windows 95.
Możesz jednak zrobić coś innego - powiedzieć widokowi CScrollView iloma elementami dysponujesz, a nie ile pikseli zabierze ich wyświetlenie. Załóżmy przez chwilę, że będziemy używali trybu mapowania MM_TEXT. Aby upewnić się, że będziesz mógł wyświetlić wszystkie elementy, będziesz musiał oszukać widok CScrollView, i przekazać mu ilość elementów nieco większą od rzeczywistej. O ile będziesz musiał powiększyć ilość elementów, zależy od wielkości Twojego okna.
Powodem takiego postępowania jest to, że widok będzie przewijany tak długo, aż zostanie w nim wyświetlony piksel o maksymalnej logicznej współrzędnej. Problem polega na tym, że nie pracujemy w pikselach. Załóżmy, że chcesz wyświetlić 10000 linii tekstu. Powiesz więc widokowi CScrollView, iż dokument ma wysokość 10000 pikseli. Teraz załóżmy, że Twoje okno ma 500 pikseli wysokości. W takiej sytuacji widok przestanie przewijać swoją zawartość, kiedy linia wyświetlana tuż pod jego górną krawędzią będzie odpowiadała pikselowi o współrzędnej 9500. Problem polega na tym, że nie będziesz mógł wyświetlić 8000 (500x16) pikseli. Rozwiązanie tego problemu polega na obliczeniu wysokości okna, odjęciu od otrzymanej wartości wysokości jednej linii i dodaniu otrzymanego wyniku do ilości “psedopikseli" określających wysokość widoku.
Właśnie przez ten dodatkowy obszar, który musisz zarezerwować, nie będziesz mógł wyświetlać pełnych 32767 elementów. Osobiście zakładam, że maksymalnie można wyświetlić 32767-2048 - czyli 31719 elementów. Jeśli będziesz musiał wyświetlić więcej elementów, najprawdopodobniej skończysz tworząc własną klasę widoku (lub wymuszając na użytkownikach zastosowanie systemu Windows NT).
Oczywiście, to wszystko nie jest tak proste, jak mogłoby się wydawać. Teraz bowiem będziesz musiał wyświetlić odpowiednie dane. Wyświetlanie, jak zwykle, wykonywane jest w metodzie OnDraw. Proces wyświetlania danych będzie w dużym stopniu przypominał zoptymalizowane wyświetlanie przedstawione w poprzedniej części rozdziału. W pierwszej kolejności będziesz musiał określić numer pierwszego wyświetlanego wiersza; możesz do tego użyć metody GetDeviceScrollPosition. Kolejnym krokiem będzie obliczenie ilości linii, które mogą zostać wyświetlone na ekranie.
Jak na razie, sposób postępowania nie różni się niczym od zoptymalizowanego wyświetlania przedstawionego w poprzedniej sekcji. Cała sztuczka omawianej metody polega na zastąpieniu trybu mapowania MM_TEXT innym trybem, który będzie Ci bardziej odpowiadał. Dzięki temu, będziesz mógł ustawić początek układu współrzędnych w punkcie okna o współrzędnych (0,0) i wyświetlić odpowiednią ilość wierszy, rozpoczynając od odpowiedniej pozycji.
Proste, prawda? Niestety, rzeczywiste rozwiązanie nie jest aż tak proste. Jeśli spróbujesz postępować dokładnie według opisanego sposobu, zobaczysz, że na ekranie pojawi się straszliwy bałagan. Stanie się tak dlatego, że widok klasy CScrollView próbuje samodzielnie przewijać s woj ą zawartość, jeśli tylko jest to możliwe. Ostatnim krokiem pozwalającym na poprawne rozwiązanie naszego problemu, będzie przesłonięcie metod OnYScroll (oraz OnHScroll, jeśli będzie to konieczne). W nowej metodzie OnYScroll będziesz musiał wywołać tę samą metodę klasy bazowej, a następnie unieważnić cały obszar roboczy okna, wymuszając tym samym jego ponowne wyświetlenie. Rozwiązanie to nie jest może tak efektywne, jak oryginalny sposób działania widoków klasy CScrollView, jednakże widoki tej klasy w żaden sposób nie pozwalają na wyświetlanie 30000 elementów.
Na listingu 4.4 przedstawiona została kolejna wersja programu wyświetlającego liczby parzyste, w której zastosowana została omówiona powyżej metoda. Zauważ, że logiczny rozmiar dokumentu zmienia się wraz ze zmianą wielkości okna. Dlatego też wewnątrz metody OnSize umieszczone jest wywołanie metody SetScrollSize. Powyższy kod może być skompilowany na dwa sposoby: z włączoną prostą optymalizacją (METHOD=0) lub ze skalowaniem wymaganym przez system Windows 95 (METHOD=1).
Przewijanie w poziomie
Przedstawiony w tym przykładzie program umożliwia przewijanie jedynie w pionie. Jednak w przypadku przewijania w poziomie mogą być zastosowane dokładnie te same rozwiązania. W tym przykładzie wyświetlanymi elementami są krótkie linie tekstu, równie dobrze mogłyby to być szerokość i długość geograficzna wyświetlana na mapie
Listing 4.4. Skomplikowane przewijanie.
// scrollerView.cpp : implementation of the CScrollerView class
//
#include "stdafx.h"
#include "scroller.h"
#include "scrollerDoc.h"
#include "scrollerView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
#define METHOD 1 // 0 - zopytmalizowane; 1 - normalne po linii
/////////////////////////////////////////////////////////////////////////////
// CScrollerView
IMPLEMENT_DYNCREATE(CScrollerView, CScrollView)
BEGIN_MESSAGE_MAP(CScrollerView, CScrollView)
//{{AFX_MSG_MAP(CScrollerView)
ON_WM_SIZE()
ON_WM_VSCROLL()
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CScrollView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CScrollView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CScrollView::OnFilePrintPreview)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CScrollerView construction/destruction
CScrollerView::CScrollerView()
{
// TODO: add construction code here
}
CScrollerView::~CScrollerView()
{
}
BOOL CScrollerView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CScrollView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CScrollerView drawing
void CScrollerView::OnDraw(CDC* pDC)
{
#if 0 // normalne
CScrollerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int ct=sizeof(pDoc->list)/sizeof(pDoc->list[0]);
pDC->SetBkMode(TRANSPARENT);
for (int i=0;i<ct;i++)
{
CString s;
s.Format("(%d)=%u",i,pDoc->list[i]);
pDC->TextOut(0,i*lineht,s);
}
#endif
#if METHOD==0 // zoptymalizowane rysowanie
CScrollerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int ct;
pDC->SetBkMode(TRANSPARENT);
int start=GetDeviceScrollPosition().y/lineht-1;
if (start<0) start=0;
CRect client;
GetClientRect(&client);
// rysuj trochę wicęj, aby przewijanie nie działało na pełnych liniach
ct=start+client.Height()/lineht+3;
if (ct>sizeof(pDoc->list)/sizeof(pDoc->list[0]))
ct=sizeof(pDoc->list)/sizeof(pDoc->list[0]);
for (int i=start;i<ct;i++)
{
CString s;
s.Format("(%d)=%u",i,pDoc->list[i]);
pDC->TextOut(0,i*lineht,s);
}
#endif
#if METHOD==1
CScrollerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int ct;
pDC->SetBkMode(TRANSPARENT);
int start=GetDeviceScrollPosition().y;
CRect client;
GetClientRect(&client);
// rysuj trochę wicęj, aby przewijanie nie działało na pełnych liniach
ct=start+client.Height()/lineht+3;
if (ct>sizeof(pDoc->list)/sizeof(pDoc->list[0]))
ct=sizeof(pDoc->list)/sizeof(pDoc->list[0]);
pDC->SetMapMode(MM_TEXT); // usuń fałszywy tryb mapowania
pDC->SetViewportOrg(0,0);
pDC->SetWindowOrg(0,0);
for (int i=start;i<ct;i++)
{
CString s;
s.Format("(%d)=%u",i,pDoc->list[i]);
pDC->TextOut(0,(i-start)*lineht,s);
}
#endif
}
void CScrollerView::OnInitialUpdate()
{
#if METHOD==0
CScrollView::OnInitialUpdate();
CScrollerDoc* doc = GetDocument();
CSize sizeTotal,sizePg;
CRect r;
GetClientRect(&r);
CDC *pDC=GetDC();
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lineht=tm.tmHeight+tm.tmExternalLeading;
sizeTotal.cx = 100;
sizeTotal.cy = (sizeof(doc->list)/sizeof(doc->list[0]))*lineht;
sizePg.cx=10;
sizePg.cy=r.Height();
SetScrollSizes(MM_TEXT, sizeTotal,sizePg,CSize(10,lineht));
#endif
#if METHOD==1
CScrollView::OnInitialUpdate();
CScrollerDoc* doc = GetDocument();
CSize sizeTotal,sizePg;
CRect r;
GetClientRect(&r);
CDC *pDC=GetDC();
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lineht=tm.tmHeight+tm.tmExternalLeading;
sizeTotal.cx = 100;
sizeTotal.cy = sizeof(doc->list)/sizeof(doc->list[0])+r.Height()-r.Height()/lineht;
sizePg.cx=10;
sizePg.cy=r.Height()/lineht;
SetScrollSizes(MM_TEXT, sizeTotal,sizePg,CSize(10,1));
#endif
}
/////////////////////////////////////////////////////////////////////////////
// CScrollerView printing
BOOL CScrollerView::OnPreparePrinting(CPrintInfo* pInfo)
{
// default preparation
return DoPreparePrinting(pInfo);
}
void CScrollerView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add extra initialization before printing
}
void CScrollerView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add cleanup after printing
}
/////////////////////////////////////////////////////////////////////////////
// CScrollerView diagnostics
#ifdef _DEBUG
void CScrollerView::AssertValid() const
{
CScrollView::AssertValid();
}
void CScrollerView::Dump(CDumpContext& dc) const
{
CScrollView::Dump(dc);
}
CScrollerDoc* CScrollerView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CScrollerDoc)));
return (CScrollerDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CScrollerView message handlers
void CScrollerView::OnSize(UINT nType, int cx, int cy)
{
#if METHOD==0
int dummy;
CScrollView::OnSize(nType, cx, cy);
CSize sizeTotal,pg,dummysize;
CRect r;
GetClientRect(&r);
pg=CSize(10,r.Height());
GetDeviceScrollSizes(dummy,sizeTotal,dummysize,dummysize);
SetScrollSizes(MM_TEXT, sizeTotal,pg,CSize(10,lineht));
#endif
#if METHOD==1
CScrollView::OnSize(nType, cx, cy);
CSize sizeTotal,pg;
CRect r;
CScrollerDoc *doc=GetDocument();
GetClientRect(&r);
pg=CSize(10,r.Height()/lineht);
sizeTotal.cx=100;
sizeTotal.cy=sizeof(doc->list)/sizeof(doc->list[0])+r.Height()-r.Height()/lineht;
SetScrollSizes(MM_TEXT, sizeTotal,pg,CSize(10,1));
#endif
}
void CScrollerView::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
CScrollView::OnVScroll(nSBCode, nPos, pScrollBar);
#if METHOD==1 // Trzeba odwołać sposób obsługi przewijania
InvalidateRect(NULL);
#endif
}
Kilka słów o klasie CEditView
Może się wydawać, że klasa CEditView najbardziej użytecznym narzędziem jest MFC, jakie może się znaleźć w arsenale programisty. Czy może bowiem być coś lepszego, niż mały edytor tekstowy, który robi wszystko za Ciebie?. Szkoda jednak, że kilka błędów w projekcie tego widoku powoduje, iż jest on dosyć daleki od doskonałości. Pierwszy i najpoważniejszy problem polega na tym, że widok ten przechowuje wszystkie dane wewnątrz siebie. Dzieje się tak dlatego, iż jest on jedynie rodzajem “otoczki" zbudowanej na standardowym elemencie kontrolnym systemu Windows. Oznacza to, że widok ten niezbyt dobrze współpracuje z dokumentami. Oczywiście musisz dostarczyć obiekt dokumentu, aby widok mógł poprawnie działać, jednak jest on używany tylko i wyłącznie do przekazywania widokowi poleceń związanych z serializacją danych.
Inne problemy, jakich przysparza widok CEditView, są identyczne jak, te które możemy napotkać używając standardowego pola edycyjnego dostępnego w systemie Windows. Problemami tymi są: wyświetlanie danych za pomocą jednej czcionki oraz bardzo duże ograniczenia pamięci w systemie Windows 95. Te dwa problemy są dosyć trudne do rozwiązania (o ile nie chcesz zastosować widoku CRichEditView, o którym będzie mowa w dalszej części rozdziału). Jednak w niektórych aplikacjach możesz skłonić widok CEditView do lepszej współpracy z dokumentem.
Pomysł na usprawnienie tej współpracy jest bardzo prosty: wystarczy przechwytywać wszystkie dane wprowadzane do widoku i przekazywać je do dokumentu, a nie do widoku. Po otrzymaniu danych wprowadzanych przez użytkownika, dokument przekazywałby je do wszystkich skojarzonych z nim widoków. Oczywiście sprawa wygląda tak prosto jedynie w teorii - praktyka zawsze jest nieco bardziej skomplikowana. Oprócz wprowadzania danych, będziesz bowiem musiał obsługiwać także wybieranie fragmentów tekstu (oraz kursor) i umieszczać wprowadzane dane w tym samym miejscu, we wszystkich widokach skojarzonych z dokumentem.
Usprawnianie widoku CEditView
Na wydrukach 4.5 oraz 4.6 przedstawione są klasy widoku i dokumentu, których wzajemną współpracę usprawniono w opisany powyżej sposób. Gdy do obiektu widoku przekazany zostanie jakiś znak (za pomocą wywołania metody OnChar, zostanie on przekazany do dokumentu. Metoda OnChar klasy bazowe nie będzie przy tym wywoływana. Po otrzymaniu znaku i jego zapisaniu, znak zostanie przekazany do metody DoChar obiektu widoku. Metoda ta wywoła metodę OnChar klasy bazowej, dzięki czemu przekazany znak będzie mógł zostać poprawnie wyświetlony.
Metoda OnChar w dokumencie przeanalizuje listę wszystkich widoków skojarzonych z dokumentem, ustawi pozycję kursora we wszystkich widokach tak, aby była ona identyczna z pozycją kursora w aktualnym widoku Następnie wywoła metodę DoChar każdego z widoków przekazując do niej wprowadzany znak. Jedyną ciekawą częścią tej metody jest kod odpowiedzialny za ustawienie kursora we wszystkich nieaktywnych widokach skojarzonych z dokumentem; zakładamy przy tym, że widoki te zawsze są odpowiednio zsynchronizowane.
Jedynym momentem w którym, widoki mogą nie być zsynchronizowane, jest stworzenie nowego widoku. Nowy widok nie będzie posiadał żadnych danych zapisanych w poprzednim widoku. Aby rozwiązać ten problem, obiekt dokumentu odnotowuje wypadki zmodyfikowania listy skojarzonych z nim widoków. Każdy widok posiada flagę initialized, której przypisywana jest wartość FALSE w momencie inicjalizowania widoku. Jeśli dokument odkryje, że istnieje nie zainicjalizowany widok, zapisze do niego cały tekst umieszczony w pierwszym dostępnym i zainicjalizowanym widoku. Kod odpowiadający za inicjalizację nowych widoków umieszczony jest w metodzie OnChangedViewList na listingu 4.6.
Listing 4.5. Klasa CEditView przystosowana do współpracy z dokumentami.
// multeditView.cpp : implementation of the CMulteditView class
//
#include "stdafx.h"
#include "multedit.h"
#include "multeditDoc.h"
#include "multeditView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CMulteditView
IMPLEMENT_DYNCREATE(CMulteditView, CEditView)
BEGIN_MESSAGE_MAP(CMulteditView, CEditView)
//{{AFX_MSG_MAP(CMulteditView)
ON_WM_CHAR()
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CEditView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CEditView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CEditView::OnFilePrintPreview)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMulteditView construction/destruction
CMulteditView::CMulteditView()
{
// Document will set this TRUE after setting our
// text buffer to match our sister edit controls
initialized=FALSE;
}
CMulteditView::~CMulteditView()
{
}
BOOL CMulteditView::PreCreateWindow(CREATESTRUCT& cs)
{
BOOL bPreCreated = CEditView::PreCreateWindow(cs);
cs.style &= ~(ES_AUTOHSCROLL|WS_HSCROLL); // Włącz przenoszeni wyrazów
return bPreCreated;
}
/////////////////////////////////////////////////////////////////////////////
// CMulteditView drawing
void CMulteditView::OnDraw(CDC* pDC)
{
CMulteditDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
}
/////////////////////////////////////////////////////////////////////////////
// CMulteditView printing
BOOL CMulteditView::OnPreparePrinting(CPrintInfo* pInfo)
{
// default CEditView preparation
return CEditView::OnPreparePrinting(pInfo);
}
void CMulteditView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
// Default CEditView begin printing.
CEditView::OnBeginPrinting(pDC, pInfo);
}
void CMulteditView::OnEndPrinting(CDC* pDC, CPrintInfo* pInfo)
{
// Default CEditView end printing
CEditView::OnEndPrinting(pDC, pInfo);
}
/////////////////////////////////////////////////////////////////////////////
// CMulteditView diagnostics
#ifdef _DEBUG
void CMulteditView::AssertValid() const
{
CEditView::AssertValid();
}
void CMulteditView::Dump(CDumpContext& dc) const
{
CEditView::Dump(dc);
}
CMulteditDoc* CMulteditView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CMulteditDoc)));
return (CMulteditDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CMulteditView message handlers
// Przekaż znaki do dokumentu bez ich obsługi
void CMulteditView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CMulteditDoc *doc=GetDocument();
doc->OnChar(this,nChar,nRepCnt,nFlags);
}
// Dokument przekazuje tutaj znaki, więc trzeba je obsłużyć
void CMulteditView::DoChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditView::OnChar(nChar,nRepCnt,nFlags);
}
Listing 4.6. Dokument współpracujący z nową wersją klasy CEditView. ___________
// multeditDoc.cpp : implementation of the CMulteditDoc class
//
#include "stdafx.h"
#include "multedit.h"
#include "multeditDoc.h"
#include "multeditView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CMulteditDoc
IMPLEMENT_DYNCREATE(CMulteditDoc, CDocument)
BEGIN_MESSAGE_MAP(CMulteditDoc, CDocument)
//{{AFX_MSG_MAP(CMulteditDoc)
// NOTE - 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()
/////////////////////////////////////////////////////////////////////////////
// CMulteditDoc construction/destruction
CMulteditDoc::CMulteditDoc()
{
// TODO: add one-time construction code here
}
CMulteditDoc::~CMulteditDoc()
{
}
BOOL CMulteditDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
return TRUE;
}
/////////////////////////////////////////////////////////////////////////////
// CMulteditDoc serialization
void CMulteditDoc::Serialize(CArchive& ar)
{
// CEditView zawiera edycyjny elemente kontrolny, który obsługuje serializację
// ((CEditView*)m_viewList.GetHead())->SerializeRaw(ar);
CString s="Unimplemented!";
WriteString(s);
}
/////////////////////////////////////////////////////////////////////////////
// CMulteditDoc diagnostics
#ifdef _DEBUG
void CMulteditDoc::AssertValid() const
{
CDocument::AssertValid();
}
void CMulteditDoc::Dump(CDumpContext& dc) const
{
CDocument::Dump(dc);
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CMulteditDoc commands
// Obsługa znaku
void CMulteditDoc::OnChar(CMulteditView *view0,UINT nChar,
UINT nRepCnt, UINT nFlags)
{
int ct;
CMulteditView *view;
POSITION pos;
int st,en,st0,en0;
view0->GetEditCtrl().GetSel(st,en);
pos=GetFirstViewPosition();
// Prześlij do wszystkich widoków
while (view=(CMulteditView *)GetNextView(pos))
{
ct=nRepCnt;
while (ct--)
{
int offset=1;
if (nChar==8) offset=-1; // backspace
view->GetEditCtrl().GetSel(st0,en0);
if ((st==st0&&st!=en0)||(st>st0 && st<=en0)) en0+=offset;
if (st<st0) st0+=offset,en0+=offset;
view->GetEditCtrl().SetSel(st,en); // ustaw położenie
view->DoChar(nChar,1,nFlags); // wykonaj
if (view!=view0) view->GetEditCtrl().SetSel(st0,en0); // ustaw oryginalną pozycję
}
}
}
// Przekaż łańcuch znaków do wszystkich widoków (bardzo podobne do przekazywana znaków)
void CMulteditDoc::WriteString(char const * s)
{
CMulteditView *view;
POSITION pos;
pos=GetFirstViewPosition();
while (view=(CMulteditView *)GetNextView(pos))
{
int n=view->GetEditCtrl().GetWindowTextLength();
view->GetEditCtrl().SetSel(n,n);
view->GetEditCtrl().ReplaceSel(s);
}
}
// Jeśli dodany zostanie nowy widok, to musimy uaktualnić jego bufor
void CMulteditDoc::OnChangedViewList()
{
POSITION pos=GetFirstViewPosition();
CMulteditView *view;
view=(CMulteditView *)GetNextView(pos);
if (view)
{
CString txt;
view->GetWindowText(txt); // zakładamy, że pierwszy widok jest OK
while (view=(CMulteditView *)GetNextView(pos))
if (!view->initialized) // jeśli nowy widok
{
view->SetWindowText(txt); // wyślij
view->initialized=TRUE; // zignoruj
}
}
CDocument::OnChangedViewList();
}
Klasa CEditView i okna dzielone
Byłoby bardzo fajnie, gdyby można było używać klasy CEditView razem z oknami dzielonymi. Nie jest to jednak zadanie proste, gdyż zarówno widok klasy CEditView, jak i okna dzielone chcą udostępniać paski przewijania. Jeśli wyłączysz paski przewijania w oknie dzielonym, utracisz standardowe możliwości obsługi okien tego typu. Jeśli jednak dostarczysz alternatywną metodę dzielenia okna, będziesz mógł usunąć styl odpowiedzialny za tworzenie pasków przewijania w wywołaniu metody Create okna dzielonego.
Na listingu 4.7 przedstawiony został kod ramki okna potomnego zawierającego okno dzielone, które nie posiada pasków przewijania. Okno to doskonale współpracuje z dokumentem i widokiem z poprzedniego przykładu. Na Rysunku 4.4 przedstawiony został nasz nowy program podczas pracy. Zwróć uwagę na to, że wszystkie paski przewijania należą do pola edycyjnego. Dlatego też dzielenie okna realizowane jest poprzez wydawanie odpowiednich poleceń, dostępnych w menu Window=>Split. Procedura obsługi tego polecenia bada aktualny stan okna dzielonego i, w zależności od niego, wywołuje metodę SplitRow lub DeleteSplit okna dzielonego. W przypadku, gdybyś chciał obsługiwać dzielenie okna za pomocą myszki, mógłbyś się zastanowić nad zastosowaniem metody DoKeyboardSplit.
Listing 4.7. Dzielenie widoku edycyjnego (CEditView).
// ChildFrm.cpp : implementation of the CChildFrame class
//
#include "stdafx.h"
#include "multedit.h"
#include "ChildFrm.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CChildFrame
IMPLEMENT_DYNCREATE(CChildFrame, CMDIChildWnd)
BEGIN_MESSAGE_MAP(CChildFrame, CMDIChildWnd)
//{{AFX_MSG_MAP(CChildFrame)
ON_COMMAND(IDM_SPLIT, OnSplit)
ON_UPDATE_COMMAND_UI(IDM_SPLIT, OnUpdateSplit)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CChildFrame construction/destruction
CChildFrame::CChildFrame()
{
split=FALSE;
}
CChildFrame::~CChildFrame()
{
}
BOOL CChildFrame::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CMDIChildWnd::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CChildFrame diagnostics
#ifdef _DEBUG
void CChildFrame::AssertValid() const
{
CMDIChildWnd::AssertValid();
}
void CChildFrame::Dump(CDumpContext& dc) const
{
CMDIChildWnd::Dump(dc);
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CChildFrame message handlers
BOOL CChildFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
return splitter.Create(this,2,1,CSize(1,1),pContext,WS_CHILD | WS_VISIBLE | SPLS_DYNAMIC_SPLIT);
}
void CChildFrame::OnSplit()
{
CRect r;
GetClientRect(r);
if (split)
splitter.DeleteRow(1);
else
splitter.SplitRow(r.Height()/2);
split=!split;
}
void CChildFrame::OnUpdateSplit(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(split);
pCmdUI->Enable();
}
Klasa CRichEditView
Odpowiedzią firmy Microsoft na liczne problemy z wykorzystaniem klasy CEditView jest klasa CRichEditView. Widoki tej klasy są edytorami tekstowymi wykorzystującymi standardowy element kontrolny rich edit (co można przetłumaczyć jako: “bogaty edytor"). Element ten nie podlega tak surowym ograniczeniom pamięci jak normalny obszar tekstowy, umożliwia korzystanie z technologii OLE oraz daje możliwość formatowania tekstu. Również jako widok zachowuje się on znacznie lepiej (choć i on nie jest doskonały).
Rysunek 4.5 przedstawia bardzo prosty edytor tekstowy zaimplementowany za pomocą widoku CRichEditView. Chociaż przeważająca część programu została automatycznie wygenerowana przez kreatora App Wizard, to i tak dysponuje on możliwościami wybierania czcionek, formatowania paragrafów i stosowania technologii OLE. Oprócz tego edytor potrafi obsługiwać zarówno normalne pliki ASCII, jak i pliki zapisane w formacie RTF.
Najprostszym sposobem napisania programu przypominającego nasz przykładowy program EZWP jest skorzystanie z usług kreatora App Wizard. Jeśli zechcesz, to podczas działania kreatora możesz zaznaczyć opcję umożliwiającą dodanie do programu pełnej obsługi technologii OLE dostępne są także; inne opcje wzbogacające możliwości programu. W ostatnim oknie dialogowym kreatora (rysunek 4.6) zaznacz klasę widoku, która zostanie wygenerowana na potrzeby tworzonego programu, i zmień jej klasę bazową na CRichEditView. Kreator App Wizard jednocześnie odpowiednio zmodyfikuje tworzoną klasę dokumentu.
Program wygenerowany przez kreatora będzie wyglądał bardzo podobnie do programu EZWP, jednak nie będzie dysponował żadnymi opcjami umożliwiającym stosowanie różnych czcionek, czy też formatowanie paragrafów. Na szczęście, dodanie tych opcji nie wymaga wiele wysiłku. Krótki rzut okna na listing 4.8 pokaże Ci, że zaimplementowanie większości z tych opcji wymagać będzie jedynie zastosowania kilku wbudowanych metod. Dla przykładu, aby zmienić używaną aktualnie czcionkę na pogrubioną, wystarczy wywołać metodę OnCharEffect przekazując do niej kilka prostych argumentów. Dostępna jest jednocześnie metoda OnUpdateCharEffect, umożliwiająca odpowiednie zaktualizowanie stanu opcji menu oraz przycisków umieszczonych na pasku narzędzi,
Podobne metody umożliwiają formatowanie paragrafów. Wszystkie te metody są dostępne w klasie CRichEditView; aby ich użyć, będziesz musiał jedynie skojarzyć je z odpowiednimi opcjami menu, skrótami klawiaturowymi lub przyciskami umieszczonymi na pasku narzędzi.
Listing 4.8. Zastosowanie klasy CRichEdiWiew.
// ezwpView.cpp : implementation of the CEzwpView class
//
#include "stdafx.h"
#include "ezwp.h"
#include "ezwpDoc.h"
#include "CntrItem.h"
#include "ezwpView.h"
#include "CharPage.h"
#include "LinePage.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CEzwpView
IMPLEMENT_DYNCREATE(CEzwpView, CRichEditView)
BEGIN_MESSAGE_MAP(CEzwpView, CRichEditView)
//{{AFX_MSG_MAP(CEzwpView)
ON_COMMAND(ID_CANCEL_EDIT_SRVR, OnCancelEditSrvr)
ON_COMMAND(ID_CHARACTER_BOLD, OnCharacterBold)
ON_UPDATE_COMMAND_UI(ID_CHARACTER_BOLD, OnUpdateCharacterBold)
ON_COMMAND(ID_PARAGRAPH_CENTER, OnParagraphCenter)
ON_UPDATE_COMMAND_UI(ID_PARAGRAPH_CENTER, OnUpdateParagraphCenter)
ON_COMMAND(ID_CHARACTER_ITALIC, OnCharacterItalic)
ON_UPDATE_COMMAND_UI(ID_CHARACTER_ITALIC, OnUpdateCharacterItalic)
ON_COMMAND(ID_PARAGRAPH_LEFT, OnParagraphLeft)
ON_UPDATE_COMMAND_UI(ID_PARAGRAPH_LEFT, OnUpdateParagraphLeft)
ON_COMMAND(ID_PARAGRAPH_RIGHT, OnParagraphRight)
ON_UPDATE_COMMAND_UI(ID_PARAGRAPH_RIGHT, OnUpdateParagraphRight)
ON_COMMAND(ID_CHARACTER_FONT, OnCharacterFont)
ON_COMMAND(IDM_Statistics, OnStatistics)
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CRichEditView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CRichEditView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CRichEditView::OnFilePrintPreview)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CEzwpView construction/destruction
CEzwpView::CEzwpView()
{
// TODO: add construction code here
}
CEzwpView::~CEzwpView()
{
}
BOOL CEzwpView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CRichEditView::PreCreateWindow(cs);
}
void CEzwpView::OnInitialUpdate()
{
CRichEditView::OnInitialUpdate();
CHARFORMAT cf;
memset(&cf,0,sizeof(cf));
cf.cbSize=sizeof(cf);
cf.dwMask=CFM_FACE|CFM_SIZE|CFM_BOLD|CFM_ITALIC;
cf.yHeight=400; // 20 points
cf.bCharSet=DEFAULT_CHARSET;
cf.bPitchAndFamily=DEFAULT_PITCH|FF_DONTCARE;
strcpy(cf.szFaceName,"Times New Roman");
SetCharFormat(cf);
}
/////////////////////////////////////////////////////////////////////////////
// CEzwpView printing
BOOL CEzwpView::OnPreparePrinting(CPrintInfo* pInfo)
{
// default preparation
return DoPreparePrinting(pInfo);
}
/////////////////////////////////////////////////////////////////////////////
// OLE Server support
// The following command handler provides the standard keyboard
// user interface to cancel an in-place editing session. Here,
// the server (not the container) causes the deactivation.
void CEzwpView::OnCancelEditSrvr()
{
GetDocument()->OnDeactivateUI(FALSE);
}
/////////////////////////////////////////////////////////////////////////////
// CEzwpView diagnostics
#ifdef _DEBUG
void CEzwpView::AssertValid() const
{
CRichEditView::AssertValid();
}
void CEzwpView::Dump(CDumpContext& dc) const
{
CRichEditView::Dump(dc);
}
CEzwpDoc* CEzwpView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CEzwpDoc)));
return (CEzwpDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CEzwpView message handlers
void CEzwpView::OnCharacterBold()
{
OnCharEffect(CFM_BOLD,CFE_BOLD);
}
void CEzwpView::OnUpdateCharacterBold(CCmdUI* pCmdUI)
{
OnUpdateCharEffect(pCmdUI,CFM_BOLD,CFE_BOLD);
}
void CEzwpView::OnParagraphCenter()
{
OnParaAlign(PFA_CENTER);
}
void CEzwpView::OnUpdateParagraphCenter(CCmdUI* pCmdUI)
{
OnUpdateParaAlign(pCmdUI,PFA_CENTER);
}
void CEzwpView::OnCharacterItalic()
{
OnCharEffect(CFM_ITALIC,CFE_ITALIC);
}
void CEzwpView::OnUpdateCharacterItalic(CCmdUI* pCmdUI)
{
OnUpdateCharEffect(pCmdUI,CFM_ITALIC,CFE_ITALIC);
}
void CEzwpView::OnParagraphLeft()
{
OnParaAlign(PFA_LEFT);
}
void CEzwpView::OnUpdateParagraphLeft(CCmdUI* pCmdUI)
{
OnUpdateParaAlign(pCmdUI,PFA_LEFT);
}
void CEzwpView::OnParagraphRight()
{
OnParaAlign(PFA_RIGHT);
}
void CEzwpView::OnUpdateParagraphRight(CCmdUI* pCmdUI)
{
OnUpdateParaAlign(pCmdUI,PFA_RIGHT);
}
void CEzwpView::OnCharacterFont()
{
CHARFORMAT cf;
cf=GetCharFormatSelection();
CFontDialog dlg(cf,CF_FORCEFONTEXIST|CF_INITTOLOGFONTSTRUCT|CF_SCREENFONTS);
if (dlg.DoModal()==IDOK)
{
dlg.GetCharFormat(cf);
SetCharFormat(cf);
}
}
void CEzwpView::OnStatistics()
{
CCharPage cp;
CLinePage lp;
CString tmp;
CPropertySheet sheet("Statistics");
sheet.AddPage(&cp);
sheet.AddPage(&lp);
tmp.Format("%d",GetTextLength());
cp.m_count=tmp;
tmp.Format("%d",GetRichEditCtrl().GetLineCount());
lp.m_count=tmp;
sheet.DoModal();
}
Na pierwszy rzut oka, udostępnienie możliwości wybierania czcionek może się wydać dosyć trudnym zadaniem. Okno dialogowe klasy CFontDialog (standardowe okno dialogowe służące do wybierania czcionek) zwraca wybraną czcionkę jako struktur? LOGFONT, zmuszając jednocześnie użytkownika do wywołania odpowiedniej metody. Z drugiej strony, widok CRichEditView wymaga podania informacji o czcionce w strukturze CHARFORMAT. Mógłbyś, oczywiście, odczytać aktualne ustawienia czcionki, przekształcić je do postaci struktury LOGFONT, wywołać okno dialogowe służące do wyboru czcionki, a następnie przekształcić strukturę LOGFONT na strukturę CHARFORMAT; jest to jednak zbyt skomplikowane.
Na szczęście firma Microsoft dodała do klasy CFontDialog narzędzia umożliwiające współpracę z widokami klasy CRichEditView. Szkoda tylko, że zapomniano tego udokumentować. Klasa CFontDialog dysponuje dwoma konstruktorami: pierwszym - tym udokumentowanym oraz drugim, którego pierwszym argumentem jest wskaźnik do struktury CHARFORMAT. Kolejnym brakującym elementem jest nieudokumentowana metoda GetCharFormat. Metoda ta zwraca informacje o czcionce zapisane w taki sposób, aby były one zrozumiałe dla klasy CRichEditView. Poniżej przedstawiony zosta) cały kod służący do wybierania i ustawiania stosowanej czcionki:
void CEzwpYiew: :OnCharacterFont ( )
{
CHARFORMAT cf;
cf =GetCharFormatSelection( ) ;
CFontDialog
dlg (cf , CF_FORCEFONTEXIST | CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS) if (dlg.DoModal{)==IDOK)
{
dlg.GetCharFormat (cf ) ; SetCharFormat (cf ) ,-
}
}
Nie rozumiem tylko dlaczego metody te nie zostały udokumentowane. Dzięki nim współpraca okna dialogowego do wyboru czcionek z widokami klasy CRichEditView staje się po prostu trywialna.
Praca z elementami kontrolnymi rysowanymi przez użytkownika
Przy zastosowaniu standardowych metod programowania w systemie Windows, praca z elementami kontrolnymi rysowanymi przez użytkownika (a w zasadzie przez programistę) jest, delikatnie mówiąc, niewygodna. Czym są elementy kontrolne tego typu? Może to być przycisk, opcja menu, etykieta, lista lub inny element kontrolny, który nie ma żadnego domyślnego wyglądu. Kiedy system Windows chce wyświetlić taki element kontrolny (lub obsłużyć go w jakikolwiek inny sposób), wysyła odpowiedni komunikat do okna rodzicielskiego danego elementu. To okno rodzicielskie odpowiedzialne jest za wyświetlenie elementu w odpowiednim stanie.
Zazwyczaj, programy wykorzystywały elementy kontrolne rysowane przez użytkownika do tworzenia przycisków zawierających bitmapy. Jednakże nowsze wersje systemu Windows udostępniają takie przyciski bez zbytecznych problemów związanych z koniecznością ich własnoręcznego rysowania. Z drugiej strony, można sobie wyobrazić przycisk, którego zawartość mogłaby się zmieniać. Dla przykładu, mógłbyś chcieć stworzyć przycisk prezentujący wskaźnik paliwa, którego strzałka może się przesuwać od pozycji minimalnej do maksymalnej. W takim wypadku musiałbyś się jednak posłużyć elementem kontrolnym rysowanym przez użytkownika.
Elementy takie przysparzają jednak wielu problemów. Po pierwsze, jesteś odpowiedzialny za rysowanie wszystkich stanów elementu. W przypadku przycisku oznacza to, że będziesz musiał rysować przycisk normalny, przycisk aktywny (gdy jest na nim ustawione ognisko wprowadzania), wciśnięty, i tak dalej. Oczywiście nie jest to takie złe, gdyż dzięki takiemu sposobowi obsługi przycisku, dysponujesz nad nim pełną kontrolą.
Największym problemem związanym z obsługą elementów kontrolnych rysowanych przez użytkownika jest to, iż nie są one modularne. Wyobraź sobie okno dialogowe, w którym chcesz umieścić trzy przyciski rysowane przez Ciebie oraz jedną listę, także rysowaną przez Ciebie. Wszystkie cztery elementy kontrolne będą przesyłały do okna dialogowego ten sam komunikat z prośbą o ich wyświetlenie. Okno dialogowe będzie musiało przeanalizować dodatkowe informacje przekazywane przez system w komunikacie, aby móc określić, co tak na prawdę ma narysować.
Załóżmy teraz, że jeden z tych przycisków przedstawia pulsującą helisę DNA, i że będziesz chciał użyć go w zupełnie innym programie. W takim wypadku będziesz musiał umieścić w programie cały kod odpowiedzialny za rysowanie przycisku, skopiowany z okna dialogowego pierwszego programu. Oznacza to także skopiowanie kodu odpowiedzialnego za obsługę komunikatu WM_TIMER i tworzenie efektu pulsacji. Cały skopiowany kod obsługi przycisku będzie musiał zostać w jakiś sposób scalony z kodem nowego programu. Ze względu na to, że wszystkie elementy kontrolne rysowane przez użytkownika generują te samem komunikaty, może to być wyjątkowo paskudne zadanie.
Rozwiązanie stosowane w MFC: samodzielne rysowanie
MFC rozwiązuje ten problem poprzez wprowadzenie nowego rodzaju elementów kontrolnych rysowanych przez użytkownika, zwanych elementami samodzielnego rysowania. Pomysł jest bardzo prosty; polega on na tym, aby okno rodzicielskie otrzymujące komunikaty od elementu rysowanego przez użytkownika nie obsługiwało ich samodzielnie, lecz odesłało z powrotem do elementu, który je wygenerował. Powróćmy do przykładu przestawionego powyżej. Załóżmy, że system Windows chce wyświetlić przycisk przedstawiający helisę DNA. System wysyła więc komunikat do okna rodzicielskiego (czyli okna dialogowego, wyprowadzonego z klasy CDialog). W tym oknie standardowa procedura obsługi komunikatów przejmuje komunikat zawierający prośbę o wyświetlenie przycisku i przesyła jaz powrotem do przycisku przestawiającego helisę DNA.
Najprawdopodobniej przycisk prezentujący helisę DNA będzie klasą potomną klasy CButton, posiadającą odpowiednie metody umożliwiające rysowanie. Również przy takim rozwiązaniu będziesz musiał sam rysować przycisk w różnych stanach, jednakże będziesz już dysponował modularnym komponentem, którego będziesz mógł wielokrotnie używać. Aby użyć tego przycisku w innym projekcie, wystarczy dodać do niego klasę CHeIixButton (lub inną, w zależności od tego, jak nazwałeś swój przycisk).
Inne rozwiązania
Chociaż dostępne aktualnie przyciski są w stanie wyświetlać bitmapy lub ikony, to jednak cały czas posiadają one ten sam standardowy kształt przycisku. Może się jednak zdarzyć, że będziesz chciał stworzyć przycisk o zupełnie innym kształcie (patrz rysunek 4.7). MFC może uprościć to zadanie dzięki udostępnianej klasie o nazwie CBitmapButton. Klasa ta jest czymś więcej, niż tylko implementacją modularnego przycisku działającego zgodnie z założeniami metody samodzielnego rysowania. Aby użyć tego przycisku, będziesz musiał umieścić w zasobach swojego programu jedną lub kilka bitmap. Bitmapy te nie mogą używać identyfikatorów numerycznych. Każda z nich określać będzie wygląd przycisku w ściśle określonym stanie. Obowiązkowo musisz udostępnić bitmapę określającą normalny wygląd przycisku; pozostałe bitmapy są opcjonalne. Jeśli je określisz, to dobrze; jeśli nie, to MFC samo zatroszczy się o wyświetlanie przycisku w innych stanach (efekt może nie być zadowalający, gdyż niczym nie będzie się on różnił od normalnego przycisku).
Załóżmy, że chciałbyś stworzyć przycisk, na którym byłby widoczny napis “OK". Aby stworzyć taki przycisk, będziesz musiał dostarczyć bitmapę OKU (określającą normalny wygląd przycisku). Oprócz tego możesz podać także bitmapy: OKD (przycisk wciśnięty), OKF (przycisk aktywny) oraz OKX (przycisk wyłączony). Po co jednak masz je udostępniać, skoro MFC ich nie potrzebuje? Powód jest taki, że mógłbyś chcieć zmienić wygląd przycisku w każdym ze stanów. Dla przykładu, mógłbyś chcieć, aby bitmapa OKD przedstawiała wizerunek podniesionego do góry kciuka, a bitmapa OKX duży, czerwony znak X. Nie musisz w tym celu dostarczać swojej własnej bitmapy OKX.
Identyfikatory zasobów
System Windows przechowuje zasoby używając do tego identyfikatorów. Identyfikatory te mają zazwyczaj postać liczb. Kiedy stworzysz nowy zasób, dajmy na to ikonę lub bitmapę, Visual C++ automatycznie skojarzy nazwę tego zasobu z liczbą i wygeneruje odpowiednią instrukcję #define w pliku nagłówkowym RESOURCE.H. Działanie takie umożliwia Ci stosowanie w programie nazw (w których istotna jest wielkość liter) zamiast automatycznie generowanych liczb.
Jednakże identyfikatorami zasobów mogą być również łańcuchy znaków (w takim przypadku wielkość liter ma znaczenie). Jest to możliwe, choć rozwiązanie takie jest mniej efektywne od identyfikatorów liczbowych. Jednakże Visual C++ automatycznie przetwarza nazwy na liczby; możesz się więc zastanawiać, w jaki sposób możliwe jest użycie identyfikatorów łańcuchowych. Rozwiązaniem tego problemu jest umieszczenie łańcucha znaków wewnątrz znaków cudzysłowu. Dzięki temu zapobiegniesz przekonwertowaniu nazwy zasobu na liczbę.
CBitmapButton wymaga zastosowania identyfikatorów łańcuchowych. Dzieje się tak, gdyż określa on nazwy bitmap, których należy użyć pobierając nazwę przycisku, dodając do niej odpowiednią literę i stosując otrzymany łańcuch znaków jako łańcuchowy identyfikator zasobu. Gdyby Visual C++ skonwertował ten łańcuch do postaci liczby, żaden zasób nie zostałby odnaleziony.
Jeśli nie chcesz używać mechanizmu tego typu do określania nazw bitmap stosowanych w przycisku, będziesz mógł określać je za pomocą metody SetBitmap. W takim wypadku będziesz mógł używać zarówno identyfikatorów łańcuchowych, jak i numerycznych.
Na listingu 4.9 przedstawiony został bardzo prosty program, w którym wykorzystane zostały, między innymi, dwa przyciski: standardowy przycisk wyświetlający bitmapy oraz przycisk klasy CBitmapButton. Oprócz nich w programie zastosowano także inne elementy kontrolne działające zgodnie z zasadami metody samodzielnego rysowania. Wszystkie te elementy umieszczone zostały na widoku będącym obiektem klasy potomnej klasy CFormView. Standardowy przycisk stworzony został przy wykorzystaniu stylu Bitmap. Zapewne zauważysz, że pomimo faktu, iż możesz użyć stylu Bitmap podczas tworzenia obiektu, nie będziesz mógł jednocześnie określić bitmapy, której chcesz użyć. Bitmapa ta może zostać określona za pomocą metody SetBitmap (patrz metoda OnlnitialUpdate na listingu 4.9). Przykładowy program wykorzystuje zmienną DDX do uzyskania zmiennej klasy CButton, odpowiadającej obsługiwanemu przyciskowi.
Listing 4.9. Zmodyfikowane elementy kontrolne.
// buttonsView.cpp : implementation of the CButtonsView class
//
#include "stdafx.h"
#include "buttons.h"
#include "odcombo.h"
#include "buttonsDoc.h"
#include "buttonsView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CButtonsView
IMPLEMENT_DYNCREATE(CButtonsView, CFormView)
BEGIN_MESSAGE_MAP(CButtonsView, CFormView)
//{{AFX_MSG_MAP(CButtonsView)
ON_BN_CLICKED(IDC_STDBTN, OnStdbtn)
ON_COMMAND(ID_SELFDRAW, OnSelfdraw)
ON_COMMAND(ID_BITMAP, OnBitmap)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CButtonsView construction/destruction
CButtonsView::CButtonsView()
: CFormView(CButtonsView::IDD)
{
//{{AFX_DATA_INIT(CButtonsView)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
// TODO: add construction code here
}
CButtonsView::~CButtonsView()
{
}
void CButtonsView::DoDataExchange(CDataExchange* pDX)
{
CFormView::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CButtonsView)
DDX_Control(pDX, IDC_ODCOMBO, m_odcombo);
DDX_Control(pDX, IDC_STDBTN, m_stdbtn);
//}}AFX_DATA_MAP
}
BOOL CButtonsView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CFormView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CButtonsView diagnostics
#ifdef _DEBUG
void CButtonsView::AssertValid() const
{
CFormView::AssertValid();
}
void CButtonsView::Dump(CDumpContext& dc) const
{
CFormView::Dump(dc);
}
CButtonsDoc* CButtonsView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CButtonsDoc)));
return (CButtonsDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CButtonsView message handlers
// Tutaj wykonywana jest cała robota
void CButtonsView::OnInitialUpdate()
{
CFormView::OnInitialUpdate();
// załaduj bitmapę dla przycisku (jeśli jeszcze nie jest załadowana)
if (bm.m_hObject==NULL)
bm.LoadBitmap(IDB_BOMB);
// Ustaw standardowy przycisk bitmapowy
m_stdbtn.SetBitmap(bm);
// Automatycznie załaduj przycisk bitmapowy MFC
if (m_MFCBtn.m_hWnd==NULL)
m_MFCBtn.AutoLoad(IDC_MFCBTN,this);
// Prawdziwy przycisk rysowany przez użytkownika/siebie (mógłby używać DDX)
if (m_odbtn.m_hWnd==NULL)
m_odbtn.SubclassDlgItem(IDC_ODBTN,this);
// Element statyczny rysowany przez użytkownika/siebie (mógłby używać DDX)
if (m_odstatic.m_hWnd==NULL)
m_odstatic.SubclassDlgItem(IDC_ODSTATIC,this);
// Ustaw style na OWNERDRAW
SetWindowLong(m_odstatic.m_hWnd,GWL_STYLE,
m_odstatic.GetStyle()|SS_OWNERDRAW);
// Użycie DDX do dołączenia pola kombo; zamiast tego moża by użyć SubclassDlgItem
// Ze względnu na to, że pole istnieje, będziemy musieli obliczyć wysokość opcji
// a następnie ustawić ją...
MEASUREITEMSTRUCT mis;
m_odcombo.MeasureItem(&mis);
m_odcombo.SetItemHeight(0,mis.itemHeight);
// Dodajemy niektóre elementy, łańcuchy znaków nie mają tutaj znaczenie gdyż
// używamy w tym przypadku danych z elementów
m_odcombo.ResetContent();
int n=m_odcombo.AddString("0");
m_odcombo.SetItemData(n,0);
n=m_odcombo.AddString("1");
m_odcombo.SetItemData(n,1);
// Teraz menu
CMenu *mainmenu=AfxGetApp()->m_pMainWnd->GetMenu(); // Pobierz główne menu
CMenu *tempmenu=mainmenu->GetSubMenu(1); // odszukaj nasze podmenu
m_selfMenu.Attach(tempmenu->m_hMenu); // dołącz je do specjalnej klasy
// ustaw flagę rysowania przez użytkownika (OWNERDRAW)
m_selfMenu.ModifyMenu(ID_SELFDRAW,MF_BYCOMMAND|MF_OWNERDRAW,
ID_SELFDRAW,(char *)1);
// Standardowe menu bitmap
tempmenu=mainmenu->GetSubMenu(2);
tempmenu->ModifyMenu(ID_BITMAP,MF_BYCOMMAND|MFT_BITMAP,
ID_BITMAP,(char *)bm.m_hObject);
}
void CButtonsView::OnStdbtn()
{
m_MFCBtn.EnableWindow(!m_MFCBtn.IsWindowEnabled());
}
void CButtonsView::OnSelfdraw()
{
MessageBox("Unimplemented");
}
void CButtonsView::OnBitmap()
{
MessageBox("Unimplemented");
}
W takim przypadku, do określania wyświetlanej ikony będzie wykorzystywana metoda Setlcon. W przyciskach tej klasy można także małym nakładem pracy stworzyć atrakcyjny efekt przestrzenny. Jeszcze jedna rzecz - jeśli bitmapa wykorzystywana w przycisku zostanie stworzona na stosie, przycisk będzie się wydawał pusty. Stanie się tak dlatego, że bitmapa bardzo szybko wyjdzie poza zakres widzialności, co spowoduje zniszczenie obrazka wyświetlonego na przycisku. W przedstawionym powyżej przykładowym programie, bitmapa określająca wygląd przycisku przechowywana jest w składowej okna dialogowego, dzięki czemu nie wychodzi ona nigdy poza zakres widzialności. Dobrym rozwiązaniem byłoby zniszczenie tej bitmapy po zamknięciu okna dialogowego, lub przechowywanie jej w zmiennej statycznej, dzięki czemu wszystkie przyciski tego samego typu mogłyby jej używać.
Przyciski klasy CBitmapButton są bardzo podobne do przycisków wykorzystujących metodę samodzielnego rysowania. Wszystko, co musisz zrobić, aby ich użyć, to zastosować odpowiedni styl (SS_OWNERDRAW) i określić tytuł przycisku. Przyciski tego typu umożliwiają określenie trzech bitmap: bitmapy normalnej, bitmapy określającej wygląd wciśniętego przycisku oraz bitmapy określającej wygląd przycisku wyłączonego. Skojarzeniem odpowiednich bitmap z przyciskiem zajmuje się metoda AutoLoad. W naszym przykładowym programie, kliknięcie normalnego przycisku powoduje wyłączenie przycisku klasy CBitmapButton; dzięki temu będziesz mógł zobaczyć jak wygląda wyłączony przycisk.
Możesz zauważyć, że przycisk udostępniany przez MFC nie wygląda tak ładnie, jak przycisk standardowy. Z drugiej jednak strony, przyciski udostępniane przez MFC są bardziej elastyczne od standardowych. Jeśli byś zmodyfikował przyciski MFC tak, aby wykorzystywały one wszystkie efekty przestrzenne, zapewne ich wygląd byłby znacznie bardziej atrakcyjny. Niestety wymaga to trochę pracy. Jednak zaletą przycisków dostępnych w MFC jest to, iż umożliwiają one tworzenie niestandardowych efektów graficznych.
Wykorzystanie elementów działających zgodnie i metodą samodzielnego rysowania
Stosowanie elementów kontrolnych, działających zgodnie z zasadami samodzielnego rysowania, jest w MFC stosunkowo proste. Cały problem polega na przesłonięciu w Twojej klasie potomnej odpowiednich metod klasy bazowej (patrz tabela 4.4). Najprostsza jest obsługa przycisków (listing 4.10) i elementów statycznych (listing 4.11), które wymagają przesłonięcia tylko jednej metody - Drawltem. Do metody tej przekazywany jest jeden argument - struktura DRAWITEMSTRUCT (patrz tabela 4.5). W strukturze tej zapisane są wszystkie informacje związane ze sposobem rysowania elementu (oraz z tym, jaki element powinien zostać narysowany - pamiętasz zapewne, że bez pomocy MFC nie jest to takie oczywiste).
Tabela 4.4. Implementowanie elementów kontrolnych metodą samodzielnego rysowania.
DrawItem MeasureItem CompareItem DeleteItem
Button V
Menu V V
List V V V V
Combo Box V V V V
Static V
Tabela 4.5. Struktura DRAWITEMSTRUCT
Składowa Typ Opis
CtlType In Typ elementu kontrolnego (ODT_BUTTON, ODT_MENU, itp.)
CtlID In Identyfikator przycisku, pola kombo, listy lub elementu statycznego
itemAction In Polecenie (ODS_DRAWENTIRE, ODA_DRAWFOCUS lub ODA_SELECT)
itemState In Stan (ODS_Checked, ODS_GRAYED, itp.)
hwndItem In Uchwyt HWND do elementu kontrolnego lub uchwytu HMENU do menu
hDC In Kontekst urządzenia
rcItem In Prostokąt zawierający element kontrolny (nie dotyczy menu)
itemData In Dowolne dane definiowane przez użytkownika (nie może to być łańcuch znaków)
Oczywiście, nie jesteś w żaden sposób zobligowany do tego, aby cokolwiek rysować. Elementy kontrolne działające według zasad samodzielnego rysowania, które nie wyświetlają niczego na swojej powierzchni, są bardzo wygodne do tworzenia w oknie obszarów, które można kliknąć, bez konieczności pisania kodu określającego płożenie wskaźnika myszy podczas kliknięcia. Choć nie jest to standardowe zastosowanie elementów kontrolnych samodzielnego rysowania, to jednak doskonale ono pokazuje, że na elementach tych możesz narysować cokolwiek zechcesz lub zgoła nic.
Zauważ, że kontekst urządzenia, uchwyt do okna oraz pozostałe dane przekazywane w strukturze DRAWITEMSTRUCT, nie są obiektami MFC, lecz normalnymi uchwytami systemu Windows. Aby użyć tych uchwytów, będziesz musiał je odpowiednio skonwertować (w przypadku kontekstu urządzenia będziesz musiał wywołać metod? CDC:FromHandle).
Drawltem a OnDrawItem
Niech nie zmylą Cię wywołania metod Drawltem oraz OnDrawItem (lub jakiejkolwiek innej podobnej pary metod). Metoda OnDrawItem jest metodą obsługującą komunikaty WM_DRAWITEM; jest ona zdefiniowana w klasie bazowej elementu kontrolnego rysowanego przez użytkownika. Domyślny kod obsługi zdefiniowany w klasie CWnd wywołuje metodę Drawltem odpowiedniego elementu kontrolnego. Obie te metody robią więc to samo, z tym, że umieszczone są w różnych miejscach: OnDrawItem w oknie rodzicielskim, a Drawltem w oknie potomnym. Tak samo skonstruowane zostały metody OnMeasureltem oraz Measureltem, a także pozostałe pary metod.
Listing 4.10. Przycisk działający według metody samodzielnego rysowania.
// ODButton.cpp : implementation file
//
#include "stdafx.h"
#include "buttons.h"
#include "ODButton.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CODButton
CODButton::CODButton()
{
}
CODButton::~CODButton()
{
}
BEGIN_MESSAGE_MAP(CODButton, CButton)
//{{AFX_MSG_MAP(CODButton)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CODButton message handlers
void CODButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC *dc=CDC::FromHandle(lpDrawItemStruct->hDC);
CBrush *btnface;
CString caption;
GetWindowText(caption);
btnface=CBrush::FromHandle(GetSysColorBrush(COLOR_BTNFACE));
if (lpDrawItemStruct->itemState&ODS_SELECTED==ODS_SELECTED)
btnface=(CBrush *)dc->SelectStockObject(WHITE_BRUSH);
else
btnface=dc->SelectObject(btnface);
dc->Ellipse(&lpDrawItemStruct->rcItem);
dc->SetBkMode(TRANSPARENT);
dc->DrawText(caption,-1,&lpDrawItemStruct->rcItem,
DT_SINGLELINE|DT_CENTER|DT_VCENTER);
dc->SelectObject(btnface);
}
Listing 4.11. Element statyczny działający według zasad samodzielnego rysowania.
// ODStatic.cpp : implementation file
//
#include "stdafx.h"
#include "buttons.h"
#include "ODStatic.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CODStatic
CODStatic::CODStatic()
{
}
CODStatic::~CODStatic()
{
}
BEGIN_MESSAGE_MAP(CODStatic, CStatic)
//{{AFX_MSG_MAP(CODStatic)
// ON_WM_DRAWITEM()
ON_WM_DRAWITEM_REFLECT()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CODStatic message handlers
void CODStatic::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC *dc=CDC::FromHandle(lpDrawItemStruct->hDC);
dc->Ellipse(&lpDrawItemStruct->rcItem);
}
Szczególne kłopoty sprawiają elementy statyczne (etykiety). Spowodowane jest to tym, że MFC nie jest w stanie zrozumieć, iż elementy takie mogą być rysowane przez użytkownika (w starszych wersjach systemu Windows nie istniały elementy statyczne rysowane przez użytkownika). Edytor zasobów nie udostępnia nawet opcji pozwalającej na tworzenie statycznych elementów kontrolnych tego typu. Nie jest to jednak wielka przeszkoda gdyż możesz określić styl elementów podczas działania programu (patrz listing 4.9). Chociaż MFC przekazuje komunikaty z powrotem do statycznego elementu kontrolne go rysowanego przez użytkownika, to jednak domyślna implementacja tego elementu obsługuje tych komunikatów. Będziesz więc musiał sam dodać makro ON_WM_D do mapy komunikatów Twojej klasy (będącej klasą potomną klasy CStatic). Przykład takiego postępowania możesz zobaczyć na listingu 4.1 1.
Listy oraz pola kombo rysowane przez użytkownika
Nieco bardzie skomplikowana jest obsługa list oraz pól kombo rysowanych przez użytkownika. Istnieją dwa rodzaje tych elementów - o ustalonej lub zmiennej wysokości. W elementach pierwszego rodzaju każdy element listy ma tą samą wysokość. Elementy kontrolne o zmiennej wysokości umożliwiają tworzenie opcji o różnej wysokości.
Do poinformowania programu o wysokości opcji używana jest metoda MeasureItem, zwracająca strukturę MEASUREITEMSTRUCT (patrz tabela 4.6). Zauważ, że w przypadku elementów kontrolnych o stałej wysokości, metoda ta wywoływana jest tylko raz, zaraz po stworzeniu elementu kontrolnego. Elementy kontrolne o zmiennej wysokości wywołują tę metodę za każdym razem, gdy konieczne jest określenie wysokości opcji.
Tabela 4.6. Struktura MEASUREITEMSTRUCT.
Składowa Typ Opis
CtlType In Typ elementu kontrolnego
CtlID In Identyfikator elementu kontrolnego (nie używany w przypadku menu)
itemID In Identyfikator menu lub identyfikator listy bądź pola kombo (jeśli element jest zmiennej wysokości)
itemWidth Out Szerokość elementu
itemHeight Out Wysokość elementu
itemData In Dowolne dane definiowane przez użytkownika
To z kolei stwarza problemy w przypadku, gdy element kontrolny używany jest w oknie dialogowym. Zarządca okien dialogowych tworzy element, a system mierzy wysokość umieszczonych w nim opcji. Jak dotąd, nie miałeś jednak okazji do skojarzenia elementu kontrolnego z obiektem MFC. Dlatego nigdy nie będziesz miał okazji do ustawienia wysokości opcji w elemencie.
Istnieją dwie metody rozwiązania tego problemu. Pierwsza polega na zastosowaniu elementów kontrolnych o zmiennej wysokości i określeniu, że wszystkie opcje tych elementów będą posiadały tę samą wysokość. Bardziej efektywne jest rozwiązanie polegające na wywołaniu metody SetltemHeight, tuż po stworzeniu elementu kontrolnego. Ta metoda została zastosowana w programie przedstawionym na listingu 4.9. Metoda Measureltem wywoływana jest bezpośrednio, tylko w jednym miejscu programu.
Jeśli nie chcesz zezwolić na sortowanie elementów list, nie będziesz musiał definiować metody Compareltem (zwracającej strukturę COMPAREITEMSTRUCT, przedstawionej w Tablicy 4.7). Jeśli jednak będziesz chciał sortować opcje dostępne na Twojej liście, będziesz musiał udostępnić metodę porównania opcji. Działanie tej metody będzie podobne do działania funkcji strcmp porównującej łańcuchy znaków. Oznacza to, że dostarczona przez Ciebie metoda powinna zwracać wartość O, jeśli oba porównywane elementy są identyczne, wartość -l, jeśli pierwszy element jest mniejszy od drugiego oraz wartość l, jeśli pierwszy element jest większy.
Czasami, podczas obsługi elementów kontrolnych samodzielnego rysowania, możesz przydzielić pamięć lub zasoby, które są potrzebne do wyświetlenia konkretnego elementu listy bądź pola kombo. Jeśli program usunie taki element z listy, będziesz musiał zwolnić przydzielone mu wcześniej zasoby. Do tego celu służy metoda Deleteltem oraz wykorzystywana przez nią struktura DELETEITEMSTRUCT (patrz tabela 4.8). Wywołanie tej metody służy jedynie celom informacyjnym. Jeśli nie będziesz musiał reagować na usuwanie elementów z listy, nie musisz definiować tej metody.
Tabela 4.7. Struktura COMPAREITEMSTRUCT
Składowa Typ Opis
CtlType In Typ elementu kontrolnego
CtlID In Identyfikator elementu kontrolnego
hwndItem In Uchwyt do okna elementu
itemID1 In Identyfikator pierwszego porównywanego elementu list
itemData1 In Dane dostarczone przez użytkownika, skojarzone z pierwszym porównywanym elementem
itemID2 In Identyfikator drugiego porównywanego elementu list
itemData2 In Dane dostarczone przez użytkownika, skojarzone z drugim porównywanym elementem
Tabela 4.8. Struktura DELETEITEMSTRUCT (przypisek skanującego w książce jest COMAPREITEMSTRUCT, ale jest to błąd)
Składowa Typ Opis
CtlType In Typ elementu kontrolnego
CtlID In Identyfikator elementu kontrolnego
hwndItem In Uchwyt do okna usuwanego elementu listy
itemID In Identyfikator usuwanego elementu list
itemData In Dane dostarczane przez użytkownika, skojarzone z usuwanym elementem
Metoda Drawitem odpowiedzialna jest za proces rysowania całego elementu kontrolnego listy lub pola kombo (podobnie jak to było w przypadku przycisków). Na listingu 4.12 przedstawiony został kod implementujący pole kombo. Tworzone pole jest wyjątkowo proste — nawet nie zmienia wyglądu, gdy jest zaznaczone. Za to używa dwóch różnych czcionek, umożliwiając oznaczenie aktualnie wybranej opcji.
Listing 4.12. Pole kombo działające według metody samodzielnego rysowania
// ODCombo.cpp : implementation file
//
#include "stdafx.h"
#include "buttons.h"
#include "ODCombo.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CODCombo
CODCombo::CODCombo()
{
}
CODCombo::~CODCombo()
{
}
BEGIN_MESSAGE_MAP(CODCombo, CComboBox)
//{{AFX_MSG_MAP(CODCombo)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CODCombo message handlers
void CODCombo::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC *dc=CDC::FromHandle(lpDrawItemStruct->hDC);
CString txt;
if (lpDrawItemStruct->itemData==0)
{
txt="Fixed";
dc->SelectStockObject(ANSI_FIXED_FONT);
}
else
{
txt="Variable";
dc->SelectStockObject(ANSI_VAR_FONT);
}
dc->DrawText(txt,-1,&lpDrawItemStruct->rcItem,DT_SINGLELINE|DT_LEFT|DT_VCENTER);
}
void CODCombo::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
CDC *dc=GetDC();
TEXTMETRIC tm1,tm2;
dc->SelectStockObject(ANSI_FIXED_FONT);
dc->GetTextMetrics(&tm1);
dc->SelectStockObject(ANSI_VAR_FONT);
dc->GetTextMetrics(&tm2);
lpMeasureItemStruct->itemWidth=max(tm1.tmAveCharWidth*25, tm2.tmAveCharWidth*25);
lpMeasureItemStruct->itemHeight=max(tm1.tmHeight,tm2.tmHeight);
}
Menu samodzielnego rysowania
W odróżnieniu od wszystkich innych elementów kontrolnych działających według metody samodzielnego rysowania, menu tego typu nie możesz stworzyć podczas pisania programu. Muszą one być budowane podczas działania programu (patrz listing 4.13). W przypadku tworzenia takiego menu będziesz musiał przesłonić jedynie dwie metody - Drawltem oraz Measureltem. Jednakże będziesz to musiał zrobić sam, gdyż klasa CMenu nie jest jedną z klas bazowych, których można użyć w kreatorze Class Wizard. Będziesz więc musiał samodzielnie stworzyć klasę potomną klasy CMenu na potrzeby swojego programu.
Listing 4.13. Menu działające według zasad samodzielnego rysowania.
// ODCombo.cpp : implementation file
//
#include "stdafx.h"
#include "buttons.h"
#include "ODCombo.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CODCombo
CODCombo::CODCombo()
{
}
CODCombo::~CODCombo()
{
}
BEGIN_MESSAGE_MAP(CODCombo, CComboBox)
//{{AFX_MSG_MAP(CODCombo)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CODCombo message handlers
void CODCombo::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC *dc=CDC::FromHandle(lpDrawItemStruct->hDC);
CString txt;
if (lpDrawItemStruct->itemData==0)
{
txt="Fixed";
dc->SelectStockObject(ANSI_FIXED_FONT);
}
else
{
txt="Variable";
dc->SelectStockObject(ANSI_VAR_FONT);
}
dc->DrawText(txt,-1,&lpDrawItemStruct->rcItem,DT_SINGLELINE|DT_LEFT|DT_VCENTER);
}
void CODCombo::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
CDC *dc=GetDC();
TEXTMETRIC tm1,tm2;
dc->SelectStockObject(ANSI_FIXED_FONT);
dc->GetTextMetrics(&tm1);
dc->SelectStockObject(ANSI_VAR_FONT);
dc->GetTextMetrics(&tm2);
lpMeasureItemStruct->itemWidth=max(tm1.tmAveCharWidth*25, tm2.tmAveCharWidth*25);
lpMeasureItemStruct->itemHeight=max(tm1.tmHeight,tm2.tmHeight);
}
Kolejnym problemem jest skojarzenie twojego obiektu menu z rzeczywistym menu, do którego MFC będzie przekazywało komunikaty umożliwiające samodzielne rysowanie. Twoim pierwszym zadaniem będzie odnalezienie odpowiedniego podmenu, w który umieszczona zostanie opcja (lub opcje), która będzie samodzielnie się rysowała. Po określeniu tego podmenu, będziesz musiał wywołać jego metodę Attach. Oznacza to, że jeden obiekt MFC może być odpowiedzialny za kilka opcji menu działających metodą samodzielnego rysowania.
Kod odpowiadający za narysowanie opcji menu możesz znaleźć na listingu 4.13. Jak zwykle jest to przykład bardzo prosty - nie pozwala na rysowanie opcji wyłączonej, zaznaczonej ani nawet wybranej. System Windows nie wykonuje absolutnie żadnych czynności w celu narysowania takiego elementu menu - wszystko będziesz musiał zrobić sam.
Jeśli w opcji menu zechcesz wyświetlić bitmapę, będziesz mógł wywołać metodę ModifyMenu, przekazując do niej flagę MFT_BITMAP (patrz listing 4.13). MFC nie dokumentuje takiego postępowania, chociaż można je znaleźć w dokumentacji SDK (w części poświęconej metodzie SetMenuItemlnfo, która nie jest bezpośrednio udostępniana przez MFC). Choć nieudokumentowana, to jednak metoda ta działa całkiem dobrze - wystarczy wywołać metodę ModifyMenu przekazując w jej wywołaniu flagę MFT_BITMAP oraz uchwyt do bitmapy (jako łańcuch znaków, przekazany jako dodatkowe dane elementu menu). Jeśli chcesz zmienić postać bitmap, jakich system Windows używa do oznaczania zaznaczonych oraz niezaznaczonych opcji menu, możesz użyć metody SetMenuItemBitmaps.
Edycja elementów list oraz drzew w oknach dialogowych
Widoki drzewa oraz listy były jednymi z najlepiej przyjętych, nowych elementów kontrolnych wprowadzonych w systemie Windows 95. Te udoskonalone listy pozwalają na wyświetlanie małych ikonek, tworzenie list hierarchicznych, i są znacznie lepsze od wcześniej dostępnych list. MFC udostępnia te elementy kontrolne jako klasy CListCtrl oraz CTreeCtrl. Zdawać by się mogło, że nie można zbyt dużo o nich powiedzieć. A jednak ...
Wszystko byłoby w porządku, gdyby elementy te działały poprawnie. Istnieje jednak pewien szczególny przypadek kiedy obie klasy zawodzą - gdy umieści się je w oknie dialogowym. Prawdę mówiąc, nie jest to problem spowodowany przez MFC; pojawia się on także wtedy, gdy te elementy kontrolne zostaną wykorzystane w oknie dialogowym w programie, który nie wykorzystuje jakichkolwiek możliwości MFC. Co więcej, elementy te działają poprawnie nawet w oknach dialogowych, o ile tylko nie zaczniesz wykorzystywać możliwości edycji etykiet poszczególnych opcji listy lub drzewa. Napewno już spotkałeś się z edycją etykiet opcji. Jest to cecha listy umożliwiająca kliknięcie na etykiecie opcji i zamienieniu jej na pole edycyjne, w którym możesz zmienić nazwę etykiety. Po kliknięciu na klawiszu Enter, pole edycyjne znika, a oryginalna nazwa etykiety zostaje zastąpiona nową.
To właśnie to kliknięcie na przycisku Enter powoduje wystąpienie problemów. Naciśnięcie klawisza Enter w oknie dialogowym jest dla okna równoznaczne z kliknięciem przycisku OK - powoduje zamknięcie okna. Co bynajmniej nie jest zamierzonym rezultatem podczas edycji etykiety listy.
Rozwiązanie tego problemu jest stosunkowo łatwe. Przykład programu przedstawionego na listingu 4.14 i Rysunku 4.8 pokazuje, w jaki sposób można rozwiązać nasz problem, przesłaniając domyślną implementację metody OnOK. Przed przekazaniem obsługi do metody klasy bazowej, kod umieszczony w metodzie OnOK odnajduje element posiadający ognisko wprowadzania. Następnie odnajdywane jest okno rodzicielskie tego elementu kontrolnego. W przypadku widoku drzewa lub listy oknem tym będzie sam element kontrolny widoku (a nie okno dialogowe, jak to jest w przypadku wszystkich innych elementów kontrolnych).
Listing 4.14. Okno dialogowe umożliwiające poprawne działanie elementów kontrolnych drzew i lisi.
// BadDlg.cpp : implementation file
//
#include "stdafx.h"
#include "treebug.h"
#include "BadDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CBadDlg dialog
CBadDlg::CBadDlg(CWnd* pParent /*=NULL*/)
: CDialog(CBadDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CBadDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
}
void CBadDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CBadDlg)
DDX_Control(pDX, IDC_TREE1, m_tree);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CBadDlg, CDialog)
//{{AFX_MSG_MAP(CBadDlg)
ON_NOTIFY(TVN_ENDLABELEDIT, IDC_TREE1, OnEndlabeleditTree1)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CBadDlg message handlers
BOOL CBadDlg::OnInitDialog()
{
CDialog::OnInitDialog();
HTREEITEM item;
item=m_tree.InsertItem("Home Number");
m_tree.InsertItem("555-1252",item);
item=m_tree.InsertItem("Work Number");
m_tree.InsertItem("555-1253",item);
return TRUE; // zwróć TRUE jeśli nie ustawiłeś ogniska wprowadzania w elemencie
// EXCEPTION: OCX Property Pages powinna zwrócić wartość FALSE
}
void CBadDlg::OnOK()
{
if (Fix)
{
if (CWnd::GetFocus()->GetParent()==&m_tree) // bogus OK from edit control
{
CWnd *ectl=CWnd::GetFocus();
ectl->SendMessage(WM_KEYDOWN,VK_RETURN);
// Choć może się to wydawać dziwne, to jednak nie należy
// wywyłać komuniaktu KEYUP, gdyż do czasu jego przekazania
// do okno, okna już nie będzie.
// ectl->SendMessage(WM_KEYUP,VK_RETURN);
return;
}
}
CDialog::OnOK();
}
void CBadDlg::OnEndlabeleditTree1(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* info = (TV_DISPINFO*)pNMHDR;
if (info->item.pszText)
{
info->item.mask=TVIF_TEXT;
m_tree.SetItem(&info->item);
}
*pResult = 0;
}
Kiedy odnalezione okno rodzicielskie nie będzie oknem dialogowym, program zasymuluje j naciśnięcie klawisza Enter, prześle odpowiedni komunikat do edycyjnego elementu j kontrolnego, i zakończy działanie metody OnOK bez wywoływania metody klasy bazowej. W przeciwnym razie, metoda OnOK klasy bazowej zostanie wywołana, dzięki] czemu przekazane zostaną odpowiednie dane, a okno dialogowe zostanie zamknięte.
Okna dzielone
Okna dzielone są unikalną cechą MFC. Zazwyczaj, jeśli zobaczysz takie okna w programie, możesz być pewny, że został on napisany za pomocą MFC (chociaż teraz już także Delphi 3 udostępnia podobny rodzaj okien). Okna dzielone w MFC nie zachowują się tak, jak reszta okien.
Teoretycznie, dzięki temu specyficznemu rodzajowi okien, Twój program może się. odróżniać od wszystkich pozostałych - okna dzielone, w połączeniu z architekturą dokument/widok, dają bowiem możliwość tworzenia bardzo interesujących rozwiązań interfejsu użytkownika. Jeśli jednak spróbujesz zastosować je do jakichś bardziej nietypowych rozwiązań, wkrótce odkryjesz, że okna dzielone są słabo udokumentowane, i w wielu przypadkach nie działają poprawnie.
Co widzi użytkownik
Zanim zaczniemy rozwiązywać problemy związane ze stosowaniem okien dzielonych, zastanówmy się w jaki sposób mają one działać. Okna dzielone można pogrupować w dwa rodzaje: statyczne oraz dynamiczne. Dynamiczne okna dzielone mają postać małego elementu kontrolnego, umieszczanego zwykle tuż ponad paskiem przewijania. Gdy ustawisz wskaźnik myszy na tym elemencie kontrolnym i przeciągniesz go ku dołowi, okno zostanie podzielone na dwie części. W obydwu częściach podzielonego okna widoczny będzie aktualny widok. Po dokonaniu podziału, jeden z widoków może zostać przewinięty, dzięki czemu będzie można wyświetlić w nim dane, które nie są widoczne w drugim widoku (patrz rysunek 4.9). Podwójne kliknięcie na linii podziału spowoduje usunięcie dodatkowego widoku (oczywiście element kontrolny umożliwiający powtórne podzielenie okna będzie dostępny, tak więc w każdej chwili będziesz mógł powtórnie je podzielić). Niektóre okna można dzielić więcej niż raz, i to zarówno w poziomie, jak i w pionie.
Statyczne okna dzielone wyglądają identycznie, z tym, lecz podział jest stały i widoczny mały czas. W odróżnieniu od dynamicznych okien dzielonych, widoki używane w obydwu częściach okien dzielonych statycznie nie muszą być tego samego typu. Dla przykładu, w pierwszej części statycznego okna dzielonego mógłbyś wyświetlić arkusz kalkulacyjny, w drugiej - wykres kołowy ilustrujący dane z tego arkusza. W oknach dzielonych tego typu możesz modyfikować wielkość dzielonych części, jednakże nie możesz usunąć żadnej z nich.
Programowa obsługa okien dzielonych
Jeśli chcesz automatycznie dodać dynamiczne okna dzielone do swojego programu, możesz to zrobić bez najmniejszego kłopotu. Po kliknięciu przycisku Advanced w kreatorze App Wizard, wyświetlone zostanie specjalne okno dialogowe, w którym będziesz mógł wybrać opcję pozwalającą na zastosowanie okien dzielonych w tworzonym programie. Creator Class Wizard także udostępnia opcje związane z oknami dzielonymi - pozwala on na tworzenie specjalnych okien ramek, które użytkownik może podzielić.
To właśnie jest dziwne w oknach dzielonych - są one częścią ramek. Zazwyczaj, jeśli w programie MFC twórca chce dodać nowe możliwości funkcjonalne do okna, tworzy nową klasę potomną, wyprowadzoną z jednej z dostępnych klas definiujących okna. Dla przykładu, jeśli chcesz używać widoku dysponującego możliwością przewijania, stworzysz ją jako klasę potomną klasy CScrollView, a nie klasy CView.
Okna dzielone tworzy się jednak w inny sposób. Nie wyprowadzasz żadnej ze swoich klas z klasy definiującej okna dzielone (CSplitterWnd). Zamiast tego, umieszczasz instancję tej klasy w swojej ramce. W ten sposób ramka będzie posiadała okno dzielone. W metodzie OnCreateClient ramki możesz zainicjalizować okno dzielone, określając przy tym jego typ - statyczny lub dynamiczny. Standardowo, okna dzielone tworzone przez kreatora App Wizard są oknami dynamicznymi. Jeśli w swojej ramce będziesz tworzył okno dzielone, nie wywołuj metody OnCreateClient klasy bazowej.
Jeśli w systemie pomocy MFC poszukasz informacji na temat metod Create, CreateStatic lub CreateView, przekonasz się, że są to metody o stosunkowo prostym działaniu. Jedyną niezwykłą rzeczą jest to, iż metody te wymagają podania argumentu będącego strukturą CCreateContext. Jeśli jeszcze nie słyszałeś o takiej strukturze, nie masz się czym przejmować - kiedy spojrzysz na argumenty wołania metody OnCreateClient zobaczysz, że ostatnim jej argumentem jest właśnie struktura CCreateContext. W przypadku tworzenia prostych okien dzielonych, jedyną rzeczą, jaką będziesz musiał zrobić, będzie przekazanie tego argumentu do metod obsługujących okna dzielone.
Zagnieżdżanie okien dzielonych
Prawdziwa zabawa zaczyna się dopiero wtedy, gdy zechcesz umieścić jedno lub kilka okien dzielonych w innym, statycznym oknie dzielonym. (Przemyśl to sobie przez chwilkę - nie możesz umieścić okien dzielonych w dynamicznym oknie dzielonym.) Jest to bardzo ważne zagadnienie, szczególne jeśli będziesz chciał podzielić okno swojego programu na niesymetryczne części, lub umieścić dynamiczne okna dzielone w statycznym oknie dzielonym.
Teoretycznie, zrobienie czegoś takiego nie powinno być trudne. Załóżmy, że chciałbyś stworzyć okno dzielone umożliwiające stworzenie podziału poziomego. Dodatkowo wyobraź sobie, że chciałbyś, aby górna część okna była dynamicznym oknem dzielonym. Poniżej przedstawione zostały czynności, które musiałbyś wykonać, aby stworzyć taki program:
1. W normalny sposób stwórz statyczne okno dzielone.
2. Wywołaj metodę CreateView dla dolnej części okna.
3. Zmień obiekt klasy CCreateContext, zapisując w jego składowej m_pNewViewClass nazwę klasy widoku (zwróconą przez makro RUNTIME_CLASS), w którym chcesz umieścić dynamiczne okno dzielone (ten krok należy stosować jedynie wtedy, gdy chcesz stworzyć zagnieżdżone okna dzielone).
4. Wywołaj metodę IdFromRowCol okna dzielonego, aby określić identyfikator okna znajdującego się w pierwszym wierszu zerowej kolumny (będzie to identyfikator okna potomnego zagnieżdżonego okna dzielonego).
5. Stwórz zagnieżdżone okno dzielone, używając pierwszego (statycznego) okna dzielonego jako okna rodzicielskiego ora?, identyfikatora okna potomnego, określonego w poprzednim kroku (jak widać, będziesz się musiał posłużyć argumentami, których zazwyczaj nie musisz podawać).
Kod prezentujący opisane powyżej czynności możesz znaleźć na listingu 4.15. Jedyną różnicą, w stosunku do opisanych czynności, jaką możesz zauważyć tym przykładzie, jest zastosowanie klasy CNestSplit zamiast CSplitterWnd, jako klasy bazowej zagnieżdżonego okna dzielonego.
15. Zagnieżdżanie okien dzielonych.
// ChildFrm.cpp : implementation of the CChildFrame class
//
#include "stdafx.h"
#include "split.h"
#include "nestsplt.h" // poprawiony błąd zagłębionych okien dzielonych
#include "ChildFrm.h"
#include "statuspane.h"
#include "splitdoc.h"
#include "splitview.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CChildFrame
IMPLEMENT_DYNCREATE(CChildFrame, CMDIChildWnd)
BEGIN_MESSAGE_MAP(CChildFrame, CMDIChildWnd)
//{{AFX_MSG_MAP(CChildFrame)
// NOTE - 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()
/////////////////////////////////////////////////////////////////////////////
// CChildFrame construction/destruction
CChildFrame::CChildFrame()
{
// TODO: add member initialization code here
}
CChildFrame::~CChildFrame()
{
}
BOOL CChildFrame::OnCreateClient( LPCREATESTRUCT /*lpcs*/,CCreateContext* pContext)
{
CRect r;
BOOL rv;
GetClientRect(&r);
rv=m_StatusSplit.CreateStatic(this,2,1);
if (rv) rv=m_StatusSplit.CreateView(1,0,RUNTIME_CLASS(CStatusPane),CSize(640,r.Height()/3),pContext);
m_StatusSplit.SetRowInfo(0,2*r.Height()/3,10);
pContext->m_pNewViewClass=RUNTIME_CLASS(CSplitView);
if (rv) rv=m_wndSplitter.Create( &m_StatusSplit,
2, 1,
CSize( 10, 10 ),
pContext,
WS_CHILD | WS_VISIBLE |WS_HSCROLL | WS_VSCROLL | SPLS_DYNAMIC_SPLIT,
m_StatusSplit.IdFromRowCol(0,0) );
return rv;
}
BOOL CChildFrame::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CMDIChildWnd::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CChildFrame diagnostics
#ifdef _DEBUG
void CChildFrame::AssertValid() const
{
CMDIChildWnd::AssertValid();
}
void CChildFrame::Dump(CDumpContext& dc) const
{
CMDIChildWnd::Dump(dc);
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CChildFrame message handlers
Dlaczego nie używać klasy CSplitterWnd?
Jeśli chcesz się dowiedzieć, dlaczego w powyższym przykładzie nie została użyta klasy CSplitterWnd, zmień typ zmiennej m_wndSplitter na CSplitterWnd, i ponownie skompiluj i uruchomisz program. Po uruchomieniu programu kliknij na przycisku Reset, tak aby ognisko wprowadzania umieszczone zostało w dolnej części okna. Teraz spróbuj podzielić górne okno - spowoduje to wygenerowanie i zgłoszenie błędu. Co się stało? Otóż dynamiczne okno dzielone zakłada, że jest ono jedynym oknem dzielonym w programie, czyli automatycznie przyjmuje, że jeden z jego widoków musi być aktywny,
Jednakże założenie to jest błędne, gdy w programie używane są zagnieżdżone okna dzielone. Klasa CNestSplit (patrz listing 4.16) przechwytuje wywołania tworzące podział okna i wymusza uaktywnienie pierwszego widoku przed przekazaniem sterowania do metody klasy bazowej. W niektórych rzadkich przypadkach może to spowodowali dziwne przekazywanie ogniska wprowadzania, jednak w pełni zapobiega to jakimkolwiek błędom podczas dzielenia zagnieżdżonych dynamicznych okien dzielonych.
Listing 4.16. Korekcja błędnego zachowania okien dzielonych.
#include "stdafx.h"
#include "nestsplt.h"
void CNestSplit::SetMeActive(void)
{
CFrameWnd *frame=GetParentFrame();
if (GetDlgItem(IdFromRowCol(0, 0)) == NULL) return; // 1st time?
CView *view=(CView *)GetPane(0,0);
if (view->IsKindOf(RUNTIME_CLASS(CView)))
frame->SetActiveView(view,TRUE);
}
BOOL CNestSplit::SplitRow(int cybefore)
{
SetMeActive();
return CSplitterWnd::SplitRow(cybefore);
}
BOOL CNestSplit::SplitColumn(int cxbefore)
{
SetMeActive();
return CSplitterWnd::SplitColumn(cxbefore);
}
Podsumowanie
Elementy kontrolne oraz wszelkie inne komponenty, które można stosować wiele razy w różnych programach, są cechą zapewniającą potęgę systemowi Windows. Jednak informacje przedstawione w tym rozdziale pokazują, że wiele elementów kontrolnych oraz klas MFC posiada błędy lub zachowuje się w dziwny sposób. Na szczęście, architektura MFC pozwala Ci na zmianę lub eliminację rzeczy, które Ci się nie podobają -wszystko dzięki możliwości wyprowadzenia i zmodyfikowania klasy potomnej.
Dużą pomocą przy korygowaniu nieprawidłowo działających elementów kontrolnych i klas, jest kod źródłowy MFC oraz dobra znajomość systemu Windows. Powinieneś się także oprzeć pokusie umieszczania nowych możliwości funkcjonalnych w skorygowanych wersjach dostępnych klas MFC. Dlaczego? Gdyż Microsoft może kiedyś samemu skoryguje te błędy, a wtedy znacznie lepiej będzie użyć tych nowych, poprawionych klas niż klas stworzonych przez Ciebie.
Praktyczny Przewodnik Okna, widoki oraz elementy kontrolne
Określanie stylu okien
Usuwanie tytułu dokumentu
Stosowanie własnych ikon, wskaźników myszy oraz tła
Tworzenie widoków o określonej wielkości
Tworzenie listy, w której zaznaczane będą wszystkie kolumny
Przewijanie przy wykorzystaniu klawiatury
Przewijanie dużych ilości elementów w systemie Windows 95
Stosowanie wielu widoków CEditView z tym samym dokumentem
Określanie sposobu formatowania w widoku CRichEditView
Stosowanie elementów kontrolnych rysowanych przez użytkownika
Efektywne stosowanie edycji etykiet w listach i drzewach
Zagnieżdżanie okien dzielonych
Kluczowym elementem efektywnego tworzenia aplikacji w systemie Windows jest stosowanie elementów kontrolnych oraz klas definiujących okna. Problem jednak polega na tym, że te elementy kontrolne wymuszają zastosowanie określonego sposobu pracy. Jest to szczególnie niewygodne, gdy element kontrolny, którego chcesz użyć, nie działa w sposób, jaki byś sobie życzył. Przy odrobinie pracy i inteligencji będziesz jednak mógł zmodyfikować te elementy kontrolne i zmusić je do działania odpowiadającego Twoim potrzebom.
Określanie stylu okien
Kiedy chcesz określić styl okna, rozwiązaniem, które samo się narzuca, jest podanie określonego stylu logicznego w wywołaniu metody Create. Jednak wiele najbardziej interesujących okien tworzonych jest wewnątrz MFC, przez co nie możesz ich stworzyć sam.
Lepszym, choć mniej naturalnym rozwiązaniem jest przesłonięcie metody PreCreateWindow w klasie okna i określenie stylu wewnątrz niej. Taka metoda sprawia, że okno staje się bardziej niezależnym i samodzielnym elementem - dzięki niej okno zawsze będzie miało taki sam styl (modyfikacja okna może polegać nie tylko na ustawieniu, lecz także na skasowaniu wybranych bitów określających jego styl).
Metoda PreCreateWindow pozwala na zmodyfikowanie struktury CREATESTRUCT (patrz Tabela 4.2). Struktura ta pozwala na określenie wielu podstawowych cech okna.
Usuwanie tytułu dokumentu
Czasami bardzo niepożądane jest wyświetlanie tytułu otworzonego dokumentu na pasku tytułowym aplikacji. Opisana powyżej metoda zmodyfikowania stylu okna może zostać z powodzeniem użyta do wyłączenia wyświetlania tytułu dokumentu. Jedyną rzeczą, jaką będziesz musiał zrobić, aby tytuł dokumentu nie był wyświetlany, jest wyczyszczenie bitu FWS_ADDTOTITLE w stylu okna. Oto przykład:
BOOL CMyFrame: : PreCreateWindow (CREATESTRUCT &cs)
{
BOOL rv=CFrameWnd::PreCreateWindow(cs);
// klasa bazowa
es.style&=-FWS_ADDTOTITLE;
return rv;
}
Stosowanie własnych ikon, wskaźników myszy oraz tła
Przesłonięcie standardowej definicji metody PreCreateWindow daje Ci okazję do zmodyfikowania wielu cech okna, w tym jego stylu, wielkości oraz położenia. Można także określić własną ikonę, postać wskaźnika myszy oraz kolor tła; jednak sposób, w jaki należy to zrobić, wcale nie jest oczywisty. Dzieje się tak dlatego, że informacje te przechowywane są w strukturze opisującej klasę okna (CREATESTRUCT).
Zazwyczaj nie podajesz nazwy klasy tworzonego okna nie jest to potrzebne, gdyż MFC automatycznie przypisze odpowiednią klasę za Ciebie. Jednakże możesz podać tę nazw? w wywołaniu metody Create lub PreCreateWindow.
Ale w jaki sposób określić nazwę klasy okna? Najprostszym sposobem jest zastosowanie metody AfxRegisterWndClass. Argumentem wywołania tej metody jest struktura WNDCLASS (patrz Tabela 4.3). Nie musisz przejmować się funkcją służącą do obsługi komunikatów - zostanie ona automatycznie określona przez MFC (zostanie nią metoda obsługująca Twoją mapę komunikatów). Wystarczy, że do obsługi komunikatów wybierzesz metodę DefWinProc, a MFC samo zatroszczy się o resztę.
Oczywiście, zarówno ikonę, jak i postać wskaźnika myszy oraz kolor tła możesz określić poprzez obsługę odpowiednich komunikatów. Dla przykładu, jeśli będziesz chciał zastosować swoją własną ikonę, wystarczy, że w metodzie OnPaint (gdy wywołanie metody Islconic zwróci wartość TRUE) przypiszesz standardowej ikonie wartość NULL i narysujesz swoją własną. Postać wskaźnika myszy możesz określić podczas obsługi komunikatu WM_SETCURSOR, a podczas obsługi komunikatu WM_ERASEBKGND możesz nadać dowolny wygląd tłu okna.
Tworzenie widoków o określonej wielkości
Choć przesłonięcie metody PreCreateWindow pozwala Ci na określenie wielkości okna, to możliwość ta nie jest zbyt przydatna w praktyce (a przynajmniej nie bezpośrednio). Problem polega bowiem na tym, że na przeszkodzie w bezpośrednim określaniu wielkości okna staje sama architektura MFC.
Określając wielkość okna chcesz nadać odpowiednią wielkość nie całemu oknu, lecz widokowi. Jednakże wielkość widoku jest zazwyczaj ściśle uzależniona od wielkości ramki. Co więcej, same widoki mają niewielką ramkę, którą także będziesz musiał uwzględnić podczas obliczania wielkości widoku.
Aby poprawnie określić wielkość ramki, w której będziesz mógł umieścić widok o pożądanych wymiarach, będziesz musiał dwa razy wywołać metodę ::AdjustWindowRectEx. Pierwsze wywołanie tej metody pozwoli Ci określić wielkość widoku, a drugie — wielkość ramki. Przykład określania wielkości widoku możesz znaleźć na Wydruku 4.3. Przedstawiony tam kod jest bardzo prosty i nie powinien sprawić Ci żadnych trudności. Wykorzystuje on jedynie prostokąt oraz odpowiednie style okien. Następnie, na podstawie stylu, odpowiednio pomniejsza wynikowe prostokąty. Zauważ, że do obliczenia wielkości widoku nie są potrzebne jakiekolwiek okna.
Najlepszym miejscem do umieszczenia kodu obliczającego wielkość widoku jest metoda OnInitialUpdate obiektu widoku. W metodzie tej możesz bowiem bez problemu określić style zarówno widoku, jak i ramki; możesz zrobić to za pomocą metod GetStyle oraz GetStyleEx.
Alternatywna metoda polega na określeniu wielkości ramki podczas jej tworzenia (dla przykładu, w metodzie PreCreateWindow). Jednak w momencie wywoływania tej metody nie istnieje jeszcze obiekt widoku. Dlatego też, aby użyć tego sposobu, musiałbyś na stałe zakodować styl widoku, który będzie używany w programie.
Jeśli określić dopuszczalną wielkości widoku lub uniemożliwić jej zmodyfikowanie, będziesz musiał zrobić to w procedurze obsługi komunikatu WM_GETMINMAXINFO. Komunikat ten pozwala na określenie maksymalnej i minimalnej wielkości ramki. Także i tym razem, gdy nadasz odpowiednią wielkość ramce, wielkość widoku zostanie określona automatycznie.
Tworzenie listy, w której zaznaczane będą wszystkie kolumny
Jedną z najbardziej niewygodnych cech list jest to, że aby zaznaczyć element listy musisz kliknąć w pierwszej kolumnie wybranego wiersza. Co więcej, jedynie ta pierwsza kolumna zostanie podświetlona. Sposób poprawienia działania listy przedstawiony został na Wydruku 4.l, wyniki zastosowania tego kodu pokazane zostały na Rysunku 4.1).
Rozwiązanie tego problemu polega na zmodyfikowaniu współrzędnych punktu kliknięcia, w taki sposób, aby liście wydawało się, że kliknięcie nastąpiło w pierwszej kolumnie. W przykładowym kodzie z Wydruku 4.1 zmodyfikowany został także sposób oznaczania wybranego elementu listy - cały wybrany wiesz zostaje zaznaczony za pomocą prostokąta.
Przewijanie przy wykorzystaniu klawiatury
Jedną z bardzo przydatnych możliwości, której nie wiadomo dlaczego nie ma w klasie CScroIlView, jest możliwość przewijania widoku za pomocą klawiszy strzałek. Zamiast modyfikowania działania całej klasy, znaczenie prostszym rozwiązaniem tego problemu jest wykonywanie odpowiednich operacji na paskach przewijania w odpowiedzi na naciśnięcie odpowiednich klawiszy. Poniżej podany został przykładowy kod stosujący takie rozwiązanie:
void CScrollerView: :OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
BOOL processed;
for (unsigned int i=0; i<nRepCnt&&processed; i++) processed=KeyScroll (nChar) ;
if (Iprocessed) CScrollView::OnKeyDown(nChar, nRepCnt, nFlags);
BOOL SCrollerView::KeyScroll (UINT nChar)
{
switch (nChar)
{
case VK_UP:
OnVScroll(SB_LINEUP,O,NULL);
break;
case VK_DOWN: OnVScroll(SB_LINEDOWN,O,NULL); break;
case VK_LEFT: OnYScroll(SB_LINELEFT,O,NULL); break;
case VK_RIGHT: OnVScroll(SB_LINERIGHT,O,NULL); break;
case VK_HOME: OnVScroll(SB_LEFT,O,NULL); break;
case VK_END: OnVScroll (SB_RIGHT, O,NULL) ,-break;
case VK_PRIOR: OnVScroll(SB_PAGEUP,O,NULL); break;
case vk_NEXT: OnYScroll (SB_PAGEDOWN, O,NULL) ; break;
return TRUE;
default:
return FALSE; // tego nie obsługujemy
}
}
}
Przewijanie dużych ilości elementów w systemie Windows 95
W systemie Windows 95 widok CScrollView nie umożliwia przewijania większej ilości elementów niż 32767. Dlaczego? Otóż dlatego, że system Windows 95 jest niczym więcej, jak tylko cieniutką, 32-bitową oprawką kodu, który w przeważającej większości jest kodem 16-bitowym. Oznacza to, że zakresy pasków przewijania nie mogą wykraczać poza magiczną wartość 32767; dotyczy to także maksymalnych współrzędnych stosowanych przy operacjach GDI. Co gorsza, przekroczenie tej wartości nie spowoduje wystąpienia jakichkolwiek błędów - wszystko zacznie wyglądać w niezaplanowany sposób (patrz Rysunek 4.3).
Jednym z potencjalnych rozwiązań tego problemu jest zaimplementowanie przewijania w klasie CView, jednakże rozwiązanie takie przysporzyłoby Ci ogromnej ilości pracy. Znacznie lepszym rozwiązaniem jest oszukanie widoku CScrollView w taki sposób, aby traktował on dowolny większy obszar (np.: linię) jako jeden piksel.
Stosując tę metodę będziesz jednak musiał pamiętać o kilku rzeczach. Po pierwsze, widok CScrollView będzie pokazywał tylko tyle pikseli, ile wystarczy do wyświetlenia piksela o odpowiednim numerze u dołu widoku. Ze względu na to, że będziesz chciał rozszerzyć wielkość piksela do całej linii (lub dowolnej innej wielkości), będziesz musiał przypisać widokowi wielkość większą do tej, której rzeczywiście potrzebujesz. O ile wielkość ta będzie musiała być większa od wielkości rzeczywistej, zależeć będzie od wielkości okna.
Kolejną rzeczą, o której będziesz musiał pamiętać podczas modyfikowania sposobu przewijania, jest konieczność zmiany trybu mapowania w metodzie OnDraw. Dzięki temu, informacje umieszczone w domyślnej implementacji metody OnPrepareDC nie będą przeszkadzały przy przewijaniu zawartości widoku nowym sposobem. Przy zastosowaniu tej metody Twoim zadaniem będzie odpowiednie modyfikowanie widoku, w zależności od operacji wykonywanych na paskach przewijania.
Pełny przykład prezentujący zastosowanie tej metody możesz znaleźć na Wydruku 4.4. Innym sposobem rozwiązania tego problemu jest uruchomienie programu w systemie Windows NT.
Stosowanie wielu widoków CEditView z tym samym dokumentem
Widoki klasy CEditView nie współpracują dobrze z architekturą dokument/widok stosowaną powszechnie w MFC. Projektanci MFC woleli maksymalnie uprościć działanie systemowego edycyjnego elementu kontrolnego (na ich miejscu też bym tak postąpił). Jednak, ze względu na to, że to element kontrolny przechowuje dane (a nie dokument), nie będziesz mógł użyć widoków tej klasy wraz z bardziej zaawansowanymi narzędziami (takimi jak okna dzielone, kilka widoków operujących na tym samym dokumencie) bez zastosowania specjalnych środków zaradczych.
Jedną z kilku możliwych metod rozwiązania tego problemu jest przechwytywanie wszystkich komunikatów odpowiadających naciśnięciom klawiszy, które mogą spowodować zmianę zawartości edycyjnego elementu kontrolnego. Po przechwyceniu komunikatów, będą one przekazywane do obsługi w obiekcie dokumentu, który po ich przetworzeniu roześle je z powrotem do wszystkich skojarzonych z nim widoków.
Rozwiązanie to nastręcza pewne trudności (dla przykładu, dane wklejane ze Schowka systemowego powinny zostać umieszczone we wszystkich widokach, chociaż operacja wklejania wykonywana jest jedynie w widoku aktualnym). Prosty przykład zastosowania tej metody przedstawiony został na Wydrukach 4.5 i 4.6. W zależności od rozwiązań stosowanych w Twoim programie, będziesz musiał także odpowiednio modyfikować rozwiązania przedstawione w przykładowym programie.
Określanie sposobu formatowania w widoku CRichEditView
Mało znane i nieudokumentowane jest powiązanie pomiędzy klasami dwoma MFC -CFontDialog oraz CRichEditView. Klasa CFontDialog dysponuje konstruktorem, do którego można przekazać strukturę CHARFORMAT. Strukturę tę można następnie pobrać za pomocą metody GetCharFormat.
Ponieważ określanie postaci czcionki w widoku CRichEditView wymaga właśnie podania struktury CHARFORMAT, powiązanie obu tych klas stwarza możliwości zastosowania w widoku CRichEditView przeróżnych czcionek (patrz Rysunek 4.5 i Wydruk 4.8). Sposób formatowania zaznaczonego fragmentu widoku można pobrać za pomocą metody GetCharFormatSelection i ustawić za pomocą metody SetCharFormat.
Stosowanie elementów kontrolnych rysowanych przez użytkownika
Tradycyjnie elementy kontrolne rysowane przez użytkownika, w momencie gdy ma zostać narysowana ich zawartość, wysyłają odpowiednie komunikaty do okna rodzicielskiego. MFC umożliwia przekazanie tych komunikatów z powrotem do elementu kontrolnego, dzięki czemu element ten może sam narysować swoją zawartość.
Cała sztuczka polega na stworzeniu klasy potomnej dla każdego elementu kontrolnego. W takiej klasie będziesz mógł przesłonić standardowe definicje metod Drawltem, Measureltem, Compareltem oraz Deleteltem w zależności od możliwości, jakimi tworzony element będzie musiał dysponować. Metody, które będziesz musiał przesłonić, w bezpośredni sposób zależeć będą od typu tworzonego elementu kontrolnego (patrz Tabela 4.4).
Za pomocą elementów rysowanych przez użytkownika możesz tworzyć przyciski (CButton), etykiety (CStatic), menu (CMenu), listy (CList) oraz niektóre typy pól kombo (CComboBox). Każdy z tych typów elementów kontrolnych ma swoje cechy szczególne; dokładny ich opis znajdziesz w tekście tego rozdziału.
Efektywne stosowanie edycji etykiet w listach i drzewach
Elementy kontrolne list oraz drzew umożliwiają edycję etykiet. Dzięki tej cesze użytkownik może kliknąć na wybranym elemencie listy i zmodyfikować jego nazwę. Elementy kontrolne działające w taki sposób możesz znaleźć w wielu programach przeznaczonych dla systemu Windows 95, w tym także i w Edytorze rejestru.
Jeśli jednak spróbujesz skorzystać z tej możliwości w oknach dialogowych, otrzymane rezultaty nie zadowolą Twoich oczekiwań. Okaże się bowiem, że w momencie naciśnięcia klawisza Enter w celu zakończenia edycji etykiety, zostanie zamknięte także całe okno dialogowe. Efekt ten z pewnością zaskoczy każdego użytkownika.
Na szczęście rozwiązanie tego problemu jest stosunkowo proste. Polega ono na zmodyfikowaniu działania metody OnOK okna dialogowego. W metodzie tej będziesz musiał w pierwszej kolejności określić aktywny element kontrolny, a następnie jego okno rodzicielskie. Jeśli okaże się, że tym oknem rodzicielskim jest element kontrolny listy (lub drzewa), będziesz musiał przesłać do niego fałszywy komunikat odpowiadający naciśnięciu klawisza Enter i zapobiec zamknięciu okna dialogowego (aby to zrobić, wystarczy nie wywoływać metody OnOK klasy bazowej).
Poniżej przedstawione zostało przykładowe zastosowanie tej metody:
void CBadDlg::OnOK()
if (CWnd::GetFocus()->GetParent()==&m_tree)
// OK. dla edytowanej etykiety
CWnd *ectl=Cwnd::GetFocus();
ectl->SendMessage(WM_KEYDOWN,VK_RETURN) ,-
// Może się to wydawać dziwne, ale nie należy wysyłać komunikatu
// WM_KEYUP, gdyż w chwili gdy zostałby on przekazany do pola
// tekstowego pola tego już nie będzie
// ectl->SendMessage(WM_KEYUP,VK_RETURN);
return OK.;
CDialog::OnOK();
Zagnieżdżanie okien dzielonych
Okna dzielone są jedną z najbardziej unikalnych cech MFC. Jednak okna tego typu nie pracują poprawnie jeśli zostaną zagnieżdżone (jeśli jedno okno dzielone zostanie umieszczone w drugim). Teoretycznie, zagnieżdżanie okien dzielonych nie powinno stwarzać żadnych problemów. Powinno bowiem wystarczyć użycie jednego okna dzielonego jako okna rodzicielskiego podczas tworzenia drugiego okna dzielonego. Powinieneś także odpowiednio określić identyfikator tworzonego okna dzielonego, tak aby zostało ono wyświetlone w odpowiednim miejscu okna rodzicielskiego (patrz Wydruk 4.15).
Cały problem polega na tym, że dynamiczne okna dzielone zawsze uważają, że są jedynymi oknami dzielonymi w programie; dlatego mogą automatycznie założyć, że jeden z ich widoków będzie widokiem aktywnym. To jednak nie jest prawdą w przypadku stosowania zagnieżdżonych okien dzielonych; co gorsza, w pewnych warunkach może to spowodować wygenerowanie wyjątku.
Rozwiązanie tego problemu polega na wymuszeniu uaktywnienia jednego z widoków okna dzielonego zgodnie z jego oczekiwaniami. W taki właśnie sposób działa klasa CNestSplit przedstawiona na Wydruku 4.16. Klasy tej możesz użyć zamiast klasy CSplitterWnd, dzięki czemu wszystkie problemy związane z ogniskiem wprowadzania i aktywnym widokiem zostaną rozwiązane.
Rozdział 5 Okna dialogowe
Okna dialogowe są najprostszym sposobem interakcji z użytkownikiem. Stosowane w MFC technologie DDX i DDV sprawiają, że przekazywanie danych z okien dialogowych stało się prostsze niż kiedykolwiek. W tym rozdziale przedstawione zostaną metody dostosowywania DDK oraz DDV do przekazywania Twoich własnych typów danych. Zobaczysz także, w jaki sposób można zmodyfikować standardowe okna dialogowe, dopasowując je do wymagań programu.
Jestem najbardziej denerwującą osobą, uwielbiającą analizowanie seriali i programów telewizyjnych. Jest to niewątpliwie mój najgorszy nawyk (i ciągłe źródło utrapienia dla mojej biednej żony, Pat). Dla przykładu weźmy serial M*A*S*H. Serial jest niewątpliwie doskonały. Ale czy kiedykolwiek przyszło Ci do głowy, żeby się zastanowić dlaczego, Ci wszyscy ludzie są przydzieleni do tego samego szpitala tak długo? Tylko regularni żołnierze przebywali w Korei dłużej niż kilka lat. Oczywiście, aby wyprodukować serial, w którym widzowie będą mogli poznać i polubić głównych bohaterów, konieczne są pewne odstępstwa od faktów historycznych. Wiem, że tak musi być, ale cały czas zauważam niedokładności tego typu i bardzo mnie one śmieszą.
Jak już wiesz, uwielbiam serial Star Trek. Może dlatego właśnie przyglądam mu się uważniej niż innym serialom i programom telewizyjnym. Oryginalny serial robiony był przez wiele lat z kilkoma przerwami, dlatego najprawdopodobniej musiałeś przegapić ogromne, masywne lampy i przełączniki, jakie mieli w tym serialu. Najprawdopodobniej przegapiłeś także odgłosy wydawane przez ich komputery (tak, jestem pewny, że nie mieli stacji dysków 3,5 cala).
W pojedynczych filmach nie można znaleźć wytłumaczenia na podobne niedociągnięcia. Star Trek jest kolejnym filmem, w którym cała grupa aktorów zostaje ze sobą na lata. W każdym filmie całkowicie zmieniany jest wygląd statku, a czasami także mundury Floty Gwiezdnej. W niektórych odcinkach, do przechodzenia do prędkości “worpowej" Sulu używał urządzenia, które do złudzenia przypominało przekładnię transmisyjną ze starego, dobrego Mustanga.
Znacznie jednak grosze były niektóre technologie komputerowe Star Treka - zwłaszcza te, mające coś wspólnego z interfejsem użytkownika. Pamiętam jeden odcinek, w którym Spock siedział przed panelem komputerowym, na którym, w losowy sposób, migało ponad 200 neonowych lampek. Lampki te ułożone były w prostokąt o niewielkich rozmiarach, bez jakichkolwiek podpisów lub innych cech charakterystycznych. Od razu uderzyło mnie to, jako przykład fatalnie zaprojektowanego interfejsu użytkownika. Bardzo mało realne jest przypuszczenie, aby ktoś mógł nauczyć się znaczenia setek nie oznakowanych lampek i potrafił sprawnie się nimi posługiwać. Nawet jeśli Spock mógł to robić (on przecież potrafił wszystko), to kto inny, spośród ludzkiej części załogi, byłby w stanie obsługiwać ten panel?
Powodem istnienia okien dialogowych jest przemyślany i dobrze zaprojektowany interfejs użytkownika. Okna dialogowe pozwalają bowiem wprowadzić standardowy sposób wyświetlania elementów kontrolnych, wykorzystywanych podczas interakcji z użytkownikiem. Dzięki nim użytkownicy zawsze (a przynajmniej w przeważającej ilości wypadków) wiedzą, jak poruszać się pomiędzy poszczególnymi elementami kontrolnymi, gdyż okna dialogowe działają identycznie we wszystkich programach (z bardzo nielicznymi wyjątkami).
Nie oznacza to wcale, że okna dialogowe zapewniają dobrą komunikację z użytkownikiem. Uwielbiam okna dialogowe wyświetlane przez program Microsoft Internet Explorer. Prawie za każdym razem można na nich przeczytać coś w stylu: “Nie można otworzyć pliku: http://www.al-williams.com/awc. Operacja zakończona pomyślnie." Cóż, okna dialogowe nie są lekarstwem na wszystkie choroby; mimo co są one wspaniałym sposobem na wyświetlanie i pobieranie informacji.
MFC i okna dialogowe
MFC ma dziwne relacje z oknami dialogowymi - udostępnia bowiem specjalną klas? służącą do ich obsługi (CDialog); klasa ta wykorzystywana jest zazwyczaj tylko do obsługi modalnych okien dialogowych. Jeśli będziesz chciał stworzyć okno dialogowe niemodalne, będziesz musiał wykonać trochę dodatkowej pracy.
Okna dialogowe: modalne, niemodalne
Większość użytkowników utożsamia okna dialogowe z modalnymi oknami dialogowymi. Ten rodzaj okien blokuje po wyświetleniu program, który je wyświetlił - pozostałe narzędzia programu stają się dostępne dopiero po zamknięciu okna dialogowego. Do zamykania okien dialogowych tego typu zazwyczaj używane są przyciski OK oraz Anuluj. Jednakże istnieją także okna dialogowe niemodalne, które pozwalają na przełączanie aktywnego okna w aplikacji, nawet kiedy okno dialogowe jest wyświetlone. Najczęściej spotykanym przykładem takich okien dialogowych są narzędzia wyszukiwania i zastępowania tekstu, które możesz znaleźć w każdym edytorze.
Jak jest różnica pomiędzy oknami dialogowymi a normalnymi oknami? Niewielka. Okno dialogowe może być pobrane z zasobów. Okna dialogowe mają wbudowany algorytm ułatwiający ich obsługę - chociażby przechodzenie pomiędzy poszczególnymi elementami kontrolnymi za pomocą klawisza Tab. Oczywiście te same czynności mógłbyś wykonywać w normalnym oknie - ich oprogramowanie wymagałoby jedynie więcej pracy.
Warto zauważyć, że okna dialogowe MFC nie są prawdziwymi oknami dialogowymi, klasycznym znaczeniu tego słowa. Zamiast tego okna dialogowe MFC imitują tradycyjne okna dialogowe. Dziej się tak po to, aby można było udostępnić w oknach dialogowych specjalne, niedostępne standardowo narzędzia - takie jak umieszczanie komponentów ActiveX w oknach dialogowych.
Zazwyczaj MFC stosuje klasę CDialog do tworzenia zarówno modalnych, jak i niemodalnych okien dialogowych. Jednak w praktyce, klasa ta oczekuje, że będzie wykorzystywana jedynie do pracy z modalnymi oknami dialogowymi. Oto dlaczego tak się dzieje:
• Kiedy obiekt klasy CDialog wykrywa naciśnięcie klawisza OK lub Cancel, wywoływana jest metoda ::EndDialog. Metoda ta wywoływana jest jedynie w przypadku modalnych okien dialogowych. Okna dialogowe niemodalne zamykane są za pomocą metody DestroyWindow.
• Okno dialogowe nie zwalnia automatycznie przydzielonej mu pamięci. Nie jest to problemem w przypadku modalnych okien dialogowych, które zazwyczaj są tworzone na stosie. Jednakże niemodalne okna dialogowe “żyją" w programie zazwyczaj przez dłuższy czas. Bardzo często będziesz je tworzył na stercie i zazwyczaj będziesz chciał je sam usuwać.
Co się stanie, jeśli przez przypadek pozwolisz, aby MFC wywołało metodę EndDialog? Chociaż dokumentacja nie opisuje tego przypadku szczegółowo, to jednak można odnaleźć w niej informację, że metoda ta nie spowoduje usunięcia okna dialogowego z pamięci. Zamiast tego okno stanie się niewidoczne, a w modalnej pętli obsługi komunikatów zostanie ustawiona flaga nakazująca zniszczenie okna. Niemodalne okna dialogowe nie używają modalnej pętli obsługi komunikatów, dlatego po wywołaniu metody EndDialog okno dialogowe będzie cały czas istniało. Uniemożliwi to także ponowne wyświetlenie okna dialogowego.
Skąd MFC wie, jakiego okna dialogowego używasz? Modalne okna dialogowe wyświetlane są za pomocą metody DoModal, natomiast okna niemodalne - za pomocą metody Create. Nie ma wielu innych różnic pomiędzy obydwoma typami okien dialogowych.
Implementacja niemodalnego okna dialogowego
Oczywiście, w bardzo prosty sposób można rozwiązać wszystkie problemy związane z niemodalnymi oknami dialogowymi - poprzez wyprowadzenie nowej klasy (nazwijmy ją CModelessDialog) i dodanie do niej odpowiednich modyfikacji. Rozwiązanie takie nie pozwoli Ci jednak na używanie kreatora Class Wizard do tworzenia nowej klasy potomnej, wyprowadzonej z Twojej klasy niemodalnego okna dialogowego. Pozostaje Ci stworzenie nowej klasy, wyprowadzonej z klasy CDialog i ręczna modyfikacja jej kodu źródłowego.
Przykład klasy CModelessDialog znajdziesz na listingu 5.1. Przykład ten prezentuje sposób, w jaki można zmienić działanie okna dialogowego umożliwiając jego poprawne działanie jako okna dialogowego niemodalnego. Należy zauważyć, że okna dialogowe tej klasy muszą być tworzone za pomocą operatora new i przechowywane na stercie.
Listing 5.1.Klasa CModelessDialog
// modeless.cpp : implementation file
//
#include "stdafx.h"
#include "modeless.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CModelessDlg dialog
CModelessDlg::CModelessDlg()
{
//{{AFX_DATA_INIT(CModelessDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
}
void CModelessDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CModelessDlg)
// NOTE: the ClassWizard will add DDX and DDV calls here
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CModelessDlg, CDialog)
//{{AFX_MSG_MAP(CModelessDlg)
// NOTE: the ClassWizard will add message map macros here
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CModelessDlg message handlers
void CModelessDlg::PostNcDestroy()
{
delete this;
}
void CModelessDlg::OnCancel()
{
DestroyWindow();
}
void CModelessDlg::OnOK()
{
UpdateData(TRUE);
DestroyWindow();
}
void CModelessDlg::OnClose()
{
DestroyWindow();
}
Możesz wpaść na pomysł, aby nie umieszczać w oknie dialogowym przycisków o identyfikatorach IDOK oraz IDCANCEL, dzięki czemu metody OnOK oraz OnCancel nie byłyby wywoływane. Metoda taka nie jest jednak wystarczająca. W niektórych przypadkach system Windows generuje zdarzenia w odpowiedzi na naciśnięcia klawiszy. Zdarzenia te mogą być traktowane podobnie, jak kliknięcie przycisku OK lub Cancel. Oznacza to, że nawet jeśli nie umieścisz w oknie dialogowym przycisków o identyfikatorach IDOK oraz IDCANCEL i tak mogą pojawić się zdarzenia generowane przez te przyciski.
Jedyną dodatkową czynnością wykonywaną przez przedstawioną klasę, jest usunięcie okna dialogowego z pamięci podczas jego niszczenia. Kiedy system Windows niszczy okna dialogowe (jak również wszystkie inne okna), ostatnim przesyłanym do nich komunikatem jest komunikat WM_DESTROY. Informuje on okno o tym, iż powinno wyczyścić całą swoją powierzchnię (Włącznie z częściami leżącymi poza obszarem roboczym - czyli paskami przewijania, paskiem tytułowym, paskiem menu, ramką, itd.). MFC wie, że jest to ostatni komunikat, jaki okno może trzymać, dlatego też, po jego otrzymaniu, wywoływana jest metoda PostNcDestry. To jest odpowiednie miejsce do usunięcia z pamięci obiektu reprezentującego okno dialogowe.
Niestety, aby usunąć obiekt okna dialogowego z pamięci, będziesz musiał napisać wyjątkowo nieeleganckie polecenie:
delete this;
Może się ono wydawać dziwaczne, jednak jest całkowicie poprawne i dozwolone. Pamiętaj tylko, aby po tej linii kodu nie odwoływać się do jakichkolwiek składowych obiektu, gdyż zostaną one usunięte z pamięci.
Stosowanie techniki DDX/DDV
Dynamiczna wymiana danych (DDX) oraz dynamiczna weryfikacja danych (DDV) są cechami, które nadają MFC niemalże magiczną moc. Teoretycznie rzecz biorąc, DDX umożliwia połączenie elementu kontrolnego w oknie dialogowym ze zmienną zdefiniowaną w klasie potomnej klasy CDialog. Tak by to właśnie mogło wyglądać, przynajmniej w większości wypadków.
W rzeczywistości jednak DDX działa zupełnie inaczej. Połączenie elementu kontrolnego ze zmienną jest jedynie iluzją. Gdy prosisz kreatora Class Wizard o stworzenie połączenia pomiędzy elementem kontrolnym i zmienną (poprzez podanie odpowiednich informacji na zakładce Member Yariables), dodaje on nowy element do mapy danych. Oznacza to, że dodawane jest nowe wywołanie metody do metody DoDataExchange okna dialogowego (metoda DoDataExchange jest zarządzana prze kreatora Class Wizard).
Kiedy wywołasz metodę UpdateData(FALSE), MFC automatycznie wywoła metod? DoDataExchange. Metody, których wywołania są umieszczane w metodzie DoDataExchange, powodują skopiowanie wartości zmiennych z Twojej klasy i umieszczenie ich w odpowiednich elementach kontrolnych okna dialogowego. Jeśli wywołasz metod? UpdateData(TRUE), MFC odwróci kierunek przekazywania danych - zostaną one pobrane z elementów kontrolnych okna dialogowego i umieszczone w zmiennych.
Powodem, dla którego może się wydawać, że cały proces przebiega automatycznie, jest to, iż metoda UpdateData(FALSE) jest zawsze wywoływana w metodzie OnlnitDialog. Oznacza to, że o ile tylko wywołasz metodę OnlnitDialog klasy bazowej (lub w ogóle jej nie przesłonisz), wartości składowe Twojej klasy w tajemniczy sposób będą pojawiały się w odpowiednich elementach kontrolnych okna dialogowego. Standardowa implementacja metody OnOK także powoduje wywołanie metody UpdateData, jednakże w jej wywołaniu przekazywana jest wartość TRUE. Wydaje się więc, że modalne okna dialogowe potrafią same się o siebie zatroszczyć. Poniższy kod na pewno będzie działał zgodnie z oczekiwaniami:
CNameDlg dlg;
dlg.m_name="Nowy użytkownik";
if (dlg.DoModal()==IDOK) MessageBox(dlg.m_name,"Witamy");
Zastanówmy się teraz, co się dzieje podczas działania niemodalnego okna dialogowego, Okna dialogowe tego typu otrzymują komunikat powodujący wywołanie metody OnlnitDialog; dzięki temu, początkowy transfer danych przeprowadzany jest poprawnie. Zazwyczaj jednak niemodalne okna dialogowe nie czekają na kliknięcie przycisku OK, aby przekazać dane z elementów kontrolnych do składowych obiektu. Oznacza to, że sam będziesz musiał wywołać metodę UpdateData w momencie, gdy będziesz chciał dokonać transferu danych.
UpdateData: TRUE czy FALSE?
Oto dobry sposób zapamiętania jaki argument powinien zostać podany w wywołaniu metody UpdateData: wystarczy, abyś zapamiętał sobie, że wartość TRUE działa jak przycisk OK - powoduje ona przekazanie danych z elementów kontrolnych do składowych obiektu. Wynika z tego, że wartość FALSE powoduje dokonanie transferu danych w przeciwnym kierunku.
Czasami, nawet modalne okna dialogowe potrzebują niewielkiej pomocy. Załóżmy, że piszesz program obsługujący pocztę elektroniczną. Użytkownik wpisuje nazwę oraz adres w oknie dialogowym. Obok pola adresowego umieszczony jest przycisk “Książka adresowa", którego kliknięcie powoduje pobranie nazwy adresata i automatyczne wypełnienie pola adresu.
Tworząc kod obsługujący przycisk “Książka adresowa" możesz postąpić na dwa sposoby, Albo odczytać nazwę użytkownika bezpośrednio z elementu kontrolnego (za pomocą metody GetDlgItemText(), albo wywołać metodę UpdateData(TRUE), aby dane zostały automatycznie przekazane do odpowiednich składowych Twojego obiektu. W podobny sposób możesz postępować by wypełnić element kontrolny prezentujący elektroniczny adres odbiory wiadomości - możesz go podać własnoręcznie lub wywołać metod? UpdateData(FALSE).
W przypadku obsługi niemodalnych okien dialogowych, bardzo dobrym zwyczajem jest modyfikowanie wartości składowych obiektu, za każdym razem, gdy zostaną zmienione wartości w elementach kontrolnych okna dialogowego. Algorytm brutalnej siły pozwalający na realizację takiej obsługi okna, polega na przechwytywaniu sygnałów zmiany danych wysyłanych przez poszczególne elementy kontrolne i wywoływaniu metody UpdateData w odpowiedzi na nie.
Elementy kontrolne powiadamiają o zmianie stanu wysyłając komunikat WM_COMMAND. Oznacza to, że najlepszym sposobem obsługi zmiany danych jest stworzenie procedury obsługi komunikatu WM_COMMAND za pomocą kreatora Class Wizard. W ten sposób możesz umieścić makro ON_WM_COMMAND w mapie komunikatów. Ten sposób obsługi komunikatu WM_COMMAND różni się od standardowego, gdyż tym razem do procedury obsługi przekazywane będą komunikaty ze wszystkich elementów kontrolnych, a nie tylko z jednego - wybranego elementu. Obsługując komunikat WM_COMMAND będziesz musiał najpierw wywołać metodę klasy bazowej, a następnie metodę UpdateData(TRUE). Jeśli w oknie dialogowym będziesz używał elementów kontrolnych generujących komunikaty WM_NOTIFY, w podobny sposób będziesz musiał postąpić z metodą OnNotify.
Prosty przykład zastosowania tej techniki możesz znaleźć na listingu 5.2. Uruchom przykładowy program i z jego menu wybierz opcję Go^Go. Następnie odsuń okno dialogowe na bok, tak aby zarówno główne okno programu, jak i okno dialogowe były jednocześnie widoczne. Teraz, podczas modyfikowania danych w oknie dialogowym, odpowiednie zmiany będą widoczne także w głównym oknie.
W tym prostym programie warto zwrócić uwagę na kilka rzeczy. Po pierwsze, może się zdarzyć, że obsługa komunikatu WM_COMMAND spowoduje zniszczenie okna dialogowego i jego unieważnienie. Dlatego bardzo ważne jest, aby nie wywoływać metody UpdateData o ile metoda ::IsWindow nie zwróci wartości TRUE (przeanalizuj metodę OnCommand przedstawioną na listingu 5.2). Po drugie, jeśli jakaś część kodu wywoła metodę UpdateData(FALSE), może to spowodować wygenerowanie komunikatu WM_COMMAND przez niektóre elementy kontrolne. Rekurencyjne wywołanie metody UpdateData może z kolei spowodować wygenerowanie asercji. Dlatego też metoda OnlnitDialog przypisuje zmiennej in_init wartość TRUE. Metoda OnCommand nie wywoła metody UpdateData, jeśli wartość zmiennej in_init będzie wynosiła TRUE. Za każdym razem, kiedy w swoim kodzie (poza kodem obsługi okna dialogowego) będziesz wywoływał metodę UpdataData, powinieneś przypisać zmiennej in_init wartość TRUE
W szczególnym przypadku naszego programu przykładowego, widok aplikacji musi wiedzieć, kiedy dane ulegają zmianie. Dlatego, gdy jakiekolwiek dane zostaną zmienione, wywołana zostanie metoda UpdateAHYiews obiektu dokumentu. Jeśli Twoją pierwszą myślą była chęć określenia okna rodzicielskiego okna dialogowego, to wiedz, że metoda taka nie zdałaby się na nic. Oknem rodzicielskim okna dialogowego nigdy nie będzie widok (nawet jeśli jawnie określisz, że oknem rodzicielskim ma być widok). Dzieje się tak dlatego, że oknem rodzicielskim okna dialogowego nie może być okno potomne. Bardzo prostym rozwiązaniem tego problemu jest zapamiętanie w zmiennej wskaźnika do widoku; tak właśnie postępuje kod przedstawiony na listingu 5.2.
Listing 5.2. Natychmiastowa wymiana danych DDX.
// LiveDialog.cpp : implementation file
//
#include "stdafx.h"
#include "livedlg.h"
#include "LiveDialog.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CLiveDialog dialog
CLiveDialog::CLiveDialog(CWnd* pParent /*=NULL*/)
: CDialog(CLiveDialog::IDD, pParent)
{
//{{AFX_DATA_INIT(CLiveDialog)
m_email = _T("");
m_name = _T("");
//}}AFX_DATA_INIT
m_View=NULL;
}
void CLiveDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CLiveDialog)
DDX_Text(pDX, IDC_EMAIL, m_email);
DDX_Text(pDX, IDC_NAME, m_name);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CLiveDialog, CDialog)
//{{AFX_MSG_MAP(CLiveDialog)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CLiveDialog message handlers
BOOL CLiveDialog::OnCommand(WPARAM wParam, LPARAM lParam)
{
BOOL rv=CDialog::OnCommand(wParam, lParam);
// nie rób tego jeśli wykonywane polecenie zniszczyło okona lub jeśli
// trwa inicjalizacja
if (::IsWindow(m_hWnd)&&!in_init)
UpdateData(TRUE); // Aktualizuj po każdej zmiaie
// Gdyż ten program chce aby zmiany były uwidaczniane odrazu
ASSERT(m_View!=NULL);
m_View->GetDocument()->UpdateAllViews(NULL);
return rv;
}
void CLiveDialog::OnOK()
{
DestroyWindow();
}
void CLiveDialog::OnCancel()
{
DestroyWindow();
}
BOOL CLiveDialog::OnInitDialog()
{
in_init=TRUE;
CDialog::OnInitDialog();
in_init=FALSE;
return TRUE;
}
Kilka słów o weryfikacji danych
Oprócz zapewnienia wymiany danych, mapy danych mogą także służyć do kontrolowania poprawności przesyłanych informacji. Zazwyczaj kontrola poprawności polega na sprawdzeniu, czy długość przekazywanego łańcucha znaków nie przekracza określonej wartości, albo czy liczba mieści się w podanym przedziale wartości. Informacje konieczne do sprawdzenia poprawności danych podawane są w kreatorze Class Wizard, i w mniej lub bardziej automatyczny sposób pojawiają się w mapie danych.
Jednak może się zdarzyć, że weryfikacja danych nie będzie spełniała Twoich oczekiwania. Dlaczego? Dlatego, że jest ona przeprowadzana jedynie podczas przekazywania danych z elementów kontrolnych do składowych klasy. Oznacza to, że użytkownik będzie musiał wpisać dane, kliknąć na przycisku OK i dopiero wtedy zostanie wyświetlony komunikat o błędzie.
Weryfikacja danych podczas ich wprowadzania
Czy istnieje możliwość sprawdzania poprawności informacji wprowadzanych przez użytkownika, tuż po ich podaniu? To zależy. Weryfikację danych po każdym naciśnięciu klawisza najlepiej jest tworzyć w wyspecjalizowanych elementach kontrolnych (lub nawet w komponentach ActiveX). Jednak można sprawdzać poprawność danych tuż po ich podaniu przez użytkownika; wymaga to jedynie niewielkiej dodatkowej pracy.
Generalnie rzecz biorąc, zasada takiego sprawdzania danych niewiele się różni od przekazywania danych tuż po ich wprowadzeniu (patrz Listing 5.2). Także i tym razem będziesz przechwytywał komunikaty WM_COMMAND, ale teraz będą Cię interesowały jedynie te komunikaty, w których w bardziej znaczącym słowie parametru wParam umieszczona będzie wartość EN_KILLFOCUS. Komunikaty takie oznaczają bowiem, że ognisko wprowadzania usuwane jest z elementu kontrolnego. To właśnie jest odpowiedni moment na sprawdzenie poprawności danych wpisanych w elemencie kontrolnym.
Problem jednak polega na tym, że nie będziesz chciał sprawdzać poprawności wszystkich elementów kontrolnych okna dialogowego. Chcesz, aby MFC sprawdziło poprawność tylko jednego elementu kontrolnego - konkretnie tego, z którego usuwane jest ognisko wprowadzania (identyfikator tego elementu kontrolnego zapisany jest w mniej znaczącym słowie parametru wParam). Problem ten możesz rozwiązać na kilka sposobów. Ja zastosowałem metodę ręcznego modyfikowania mapy danych.
Pierwszym etapem rozwiązania jest sprawienie, aby mapa danych działała w taki sposób, w jaki sobie życzysz (oczywiście, pomijając fakt, że sprawdzanie odbywa się nie wtedy, kiedy chcemy). Teraz będziesz musiał przesunąć cały kod znajdujący się w metodzie DoDataExchange (mapę danych) i umieścić go poza specjalnymi komentarzami stosowanymi przez kreatora Class Wizard. Teraz dodaj do klasy składową, w której przechowywany będzie identyfikator elementu kontrolnego, którego wartość uległa zmianie. W konstruktorze klasy przypisz tej składowej wartość 0. W kolejnym kroku będziesz musiał zmodyfikować każdą linię kodu realizującą transfer i weryfikację danych, w taki sposób, aby była ona wykonywana tylko wtedy, gdy identyfikator elementu kontrolnego ma wartość O lub jest równy identyfikatorowi elementu, do którego dana linia się odnosi (patrz Listing 5.3). Ostatnią czynnością będzie dodanie zmiennej umożliwiającej określenie że program jest w trakcie weryfikacji danych. Zmiennej tej przypisz wartość FALSE w konstruktorze okna dialogowego, oraz wartość TRUE na samym początku metody DoDataExchange. Pod koniec działania tej metody ponownie przypisz tej zmiennej wartość FALSE.
Kiedy będziesz chciał zweryfikować wartość konkretnego pola (na przykład w odpowiedzi na komunikat EN_KILLFOCUS), będziesz musiał zapisać identyfikator elementu kontrolnego w zmiennej i wywołać metodę UpdateData(TRUE). Przed wywołaniem tej metody upewnij się, że nie jesteś już w trakcie wykonywania weryfikacji (to dlatego dodałeś dodatkową zmienną która określa czy program aktualnie weryfikuje dane).
Jedyny kruczek tej metody polega na odpowiednim obsłużeniu sytuacji, gdy weryfikacja danych się nie powiedzie. W takim bowiem przypadku MFC generuje wyjątek przerywający pracę metody DoDataExchange. Oznacza to, że jeśli wpisana przez użytkownika liczba nie będzie się mieścić w podanym przez Ciebie dopuszczalnym zakresie, wartość zmiennej określającej, że program weryfikuje dane, nie zostanie odpowiednio zmodyfikowana na końcu metody DoDataExchange. Najprostszym rozwiązaniem tego problemu jest ustawienie wartości tej zmiennej po wywołaniu metody UpdateData. Inne możliwe rozwiązanie polega na przechwyceniu wyjątków zgłaszanych przez metody weryfikacji danych, odpowiednim ustawieniu wartości zmiennej i ponownym zgłoszeniu przechwyconego wyjątku.
Na listingu 5.3 przedstawiony został program wykorzystujący klasę CFormView umożliwiającą weryfikację danych tuż po ich wprowadzeniu. Ta sama metoda może zostać z powodzeniem zastosowana także w przypadku obsługi okien dialogowych. Wszystkie najważniejsze czynności w przedstawionym przykładzie wykonywane są w metodach OnCommand oraz DoDataExchange. W metodzie DoDataExchange przechwytywane są wyjątki, dzięki czemu wartość zmiennej validating będzie odpowiednio ustawiana nawet w przypadku wykrycia błędu podczas weryfikacji danych.
Listing 5.3. Natychmiastowa weryfikacja danych DDV.
// validView.cpp : implementation of the CValidView class
//
#include "stdafx.h"
#include "valid.h"
#include "validDoc.h"
#include "validView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CValidView
IMPLEMENT_DYNCREATE(CValidView, CFormView)
// Kreator Class Wizard nie umieści tego makra w mapie komunikatów
// gdyż sądzi, że to okna dialogowe obsługują OnOK. Obsługują, to
// prawda, jednakże my dysponujemy widokiem dialogu a nie oknem
// dialogowym
BEGIN_MESSAGE_MAP(CValidView, CFormView)
//{{AFX_MSG_MAP(CValidView)
ON_COMMAND(IDOK,OnOK)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CValidView construction/destruction
CValidView::CValidView()
: CFormView(CValidView::IDD)
{
validating=FALSE;
vid=0;
//{{AFX_DATA_INIT(CValidView)
m_age = 18;
m_name = _T("");
m_wager = 1.0f;
//}}AFX_DATA_INIT
// TODO: add construction code here
}
CValidView::~CValidView()
{
}
void CValidView::DoDataExchange(CDataExchange* pDX)
{
CFormView::DoDataExchange(pDX);
validating=TRUE; // zapobiegniej rekursju
// wywołania usunięte z komentarzy kreator Class Wizard i zmodyfikowane
try
{
if (!vid||vid==IDC_AGE) DDX_Text(pDX, IDC_AGE, m_age);
if (!vid||vid==IDC_AGE) DDV_MinMaxInt(pDX, m_age, 18, 150);
if (!vid||vid==IDC_NAME) DDX_Text(pDX, IDC_NAME, m_name);
if (!vid||vid==IDC_NAME) DDV_MaxChars(pDX, m_name, 64);
if (!vid||vid==IDC_WAGER) DDX_Text(pDX, IDC_WAGER, m_wager);
if (!vid||vid==IDC_WAGER) DDV_MinMaxFloat(pDX, m_wager, 1.f, 100.f);
//{{AFX_DATA_MAP(CValidView)
//}}AFX_DATA_MAP
validating=FALSE;
}
catch (...)
{
validating=FALSE; // pamiętaj aby ustawić tą flagę
throw; // ponownie zgłoś wyjątek
}
}
BOOL CValidView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CFormView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CValidView diagnostics
#ifdef _DEBUG
void CValidView::AssertValid() const
{
CFormView::AssertValid();
}
void CValidView::Dump(CDumpContext& dc) const
{
CFormView::Dump(dc);
}
CValidDoc* CValidView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CValidDoc)));
return (CValidDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CValidView message handlers
void CValidView::OnOK()
{
if (UpdateData(TRUE))
MessageBox("Wager placed");
}
BOOL CValidView::OnCommand(WPARAM wParam, LPARAM lParam)
{
if (HIWORD(wParam)==EN_KILLFOCUS&&!validating)
{
vid=LOWORD(wParam);
UpdateData(TRUE);
// zresetuj warunki
// Poniższą linię będziesz musiał wykonywać jesli
// nie przechwytujesz wyjątków w metodzie DoDataExchange
// validating=FALSE;
vid=0;
}
return CFormView::OnCommand(wParam, lParam);
}
Inne sztuczki związane z mapami danych
Kiedy już zdasz sobie sprawę z tego, że tak zwana “mapa danych" to jedynie wywołania metod, dostrzeżesz wiele możliwości wykorzystania tego faktu. Poniżej przedstawiony został sposób sprawdzania poprawności kwoty wpisanej w polu określającym wielkość kredytu:
DDV_MinMaxFloat(pDX, m_credit, l.f, credit_limit);
Oczywiście, kreator Class Wizard nic nie wie o wprowadzanych modyfikacjach, dlatego też nie musisz przesuwać kodu poza komentarze stosowane przez kreatora.
Kolejną możliwością jest zastosowanie selektywnej weryfikacji danych. Załóżmy, że dysponujesz własnymi metodami weryfikacji kodów pocztowych (w następnej części rozdziału dowiesz się jak można tworzyć takie metody). W takim przypadku mógłbyś napisać następującą linię kodu:
if (country==USA) DDV_ZipCode(pDX, m_zip);
Pamiętaj tylko, aby kod weryfikacji danego pola umieszczony został zaraz za wywołaniem metody służącej do transferu danych (DDX). W przeciwnym wypadku Twój program może nieprawidłowo określić pole zawierające błędne dane (w przypadku nie spełnienia warunków weryfikacji).
Tworzenie własnych metod DDX/DDV
Kiedy już zaczniesz rozumieć zasady działania map danych, będziesz mógł pomyśleć o tworzeniu własnych procedur umożliwiających transfer informacji oraz ich weryfikację. Nie ma żadnego powodu, dla którego nie mógłbyś tego zrobić. Funkcje weryfikacji i przekazywania danych są normalnymi, globalnymi funkcjami, które potrafią obsługiwać obiekty klasy CDataExchange. Nie ma w nich nic szczególnie niezwykłego.
Czasami wygodnie jest przeprowadzić weryfikację danych podczas ich przekazywania. Jest to bardzo wygodne, szczególnie w przypadku, kiedy weryfikacja nie wymaga podawania żadnych dodatkowych argumentów. Dla przykładu, jeśli piszesz procedurę zamieniającą nazwę hosta na adres IP, podczas przekazywania tych informacji możesz jednocześnie sprawdzić ich poprawność.
W niektórych przypadkach będziesz jednak chciał tworzyć własne procedury weryfikacji danych pobierające argumenty. W obu przypadkach możesz zintegrować swoje procedury weryfikacji danych, tak aby pojawiały się one w kreatorze Class Wizard wraz z innymi, standardowymi procedurami DDX/DDV.
Pierwszym krokiem będzie stworzenie funkcji i zapewnienie, że działa ona poprawnie. W przypadku funkcji wymiany danych, będziesz musiał stworzyć globalną funkcję pobierającą jako argument wskaźnik do obiektu klasy CDataExchange, identyfikator elementu kontrolnego, którego wartość ma być przekazywana, oraz odwołanie do zmiennej, w której wartość będzie przechowywana. Chociaż możesz chcieć nadać swojej funkcji nazwę nie zaczynającą się od “DDV_", lepiej będzie, jeśli oprzesz się tej pokusie (już wkrótce okaże się dlaczego).
Wszystkie szczegółowe informacje dotyczące transferu danych możesz pobrać z obiektu klasy CDataExchange (patrz Tabela 5.1). Zazwyczaj będziesz chciał sprawdzić wartość składowej m_bSaveAndValidate. Wartość tej składowej odpowiada argumentowi podanemu przy wywoływaniu metody UpdateData (wartość TRUE oznacza pobranie wartości zapisanej w elemencie kontrolnym i zapisanie jej w odpowiedniej zmiennej, natomiast wartość FALSE oznacza pobranie wartości zmiennej i zapisanie jej w elemencie kontrolnym).
Tabela 5.1. Klasa CDataExchang
eSkładowa Opis
m_bSaveAndValidate Składowa posiada wartość TRUE jeśli dane mają zostać pobrane z elementu kontrolnego i zapisane w zmiennej.
m_pDlgWnd Uchwyt od okna kontrolującego lub okna dialogowego.
PrepareCtrl Wywołaj tę metodę, aby oznaczyć aktualny element kontrolny (jeśli nie jest on polem tekstowym).
PrepareEditCtrl Wywołaj tę metodę, aby oznaczyć aktualny element kontrolny, o ile jest to pole tekstowe.
Fail Zgłasza niepowodzenie weryfikacji danych i przekazuje ognisko wprowadzania do ostatniego zaznaczonego elementu kontrolnego (metoda ta może być używana zarówno w funkcjach przekazywania danych (DDX), jak i funkcjach weryfikacji (DDV).
Jeśli istnieje jakakolwiek możliwość nieprawidłowego przekazania danych, powinieneś wywołać metodę PrepareEditCtrl (w przypadku obsługi pól tekstowych) lub metodę PrepareCtrl (w przypadku obsługi wszystkich pozostałych typów elementów kontrolnych). Jeśli metody te zostaną wywołane, późniejsze wywołanie metody Fail spowoduje przekazanie ogniska wprowadzania do odpowiedniego elementu kontrolnego. Operacja przekazania ogniska wprowadzania zakończy się poprawnie, nawet jeśli inne metody (takie jak sprawdzenie poprawności danych) zgłoszą wystąpienie błędów.
Jeśli stwierdzisz, że sprawdzane dane nie są poprawne, powinieneś wyświetlić stosowny komunikat i wywołać metodę Fail. Powoduje to zgłoszenie wyjątku, który przerwie działanie metody DoDataExchange i spowoduje umieszczenie ogniska wprowadzania w ostatnim oznaczonym elemencie kontrolnym. Oczywiście weryfikacja danych powinna być przeprowadzana tylko wtedy, gdy składowa m_bSaveAndValidate ma wartość TRUE. Podczas przekazywania danych z programu do okna dialogowego, zazwyczaj zakłada się, że dane są poprawne.
Funkcje weryfikujące informacje wyglądają niemal tak samo, jak funkcje służące do przekazywania danych; różnią się jedynie argumentami. W nazwie funkcji użyj prefiksu DDV_.
Jeśli chodzi o argumenty, to funkcje służące do weryfikacji danych pobierają wskaźnik do obiektu CDataExchange, wartość odpowiedniego typu oraz jeden lub dwa argumenty dodatkowe (Np. górną i dolną granicę dopuszczalnego zakresu wartości).
W tym przypadku Twoje zadanie jest proste. Jeśli składowa m_bSaveAndVatidate będzie miała wartość TRUE, wykonaj dowolne czynności, za pomocą których będziesz mógł określić, czy przekazana wartość jest poprawna czy nie. Jeśli wartość jest poprawna zakończ działanie funkcji, w przeciwnym wypadku wywołaj metodę Fail. Poprzednia metoda transferu danych oznaczyła element kontrolny, którego dotyczą wykonywane operacje, więc ognisko wprowadzania zostanie umieszczone w odpowiednim miejscu. To właśnie dlatego transfer danych z wybranego elementu kontrolnego i ich weryfikacja powinny następować bezpośrednio po sobie.
Na listingu 5.4 przedstawiona została kolejna wersja programu OTB (patrz listing 5.2). W programie tym wykorzystywana jest niestandardowa procedura weryfikująca poprawność danych (patrz listing 5.5). Kod weryfikujący sprawdza, czy po przecinku dziesiętnym nie podano więcej niż dwóch cyfr. Wywołuje on także standardową metodę DDV_MinMaxFloat sprawdzającą czy analizowana liczba jest poprawnie zapisaną liczbą zmiennoprzecinkową. No i dobrze, bo po co wywarzać otwarte drzwi?
Zwróć uwagę na to, że funkcja sprawdzająca poprawność danych nie otrzymuje jawnie identyfikatora elementu kontrolnego, tak jak funkcja służąca do transferu danych. Dzieje się tak dlatego, że zazwyczaj funkcja weryfikująca interesuje się jedynie wartością elementu kontrolnego. Jeśli chcesz, możesz pobrać uchwyt do okna elementu kontrolnego, który jest zapisany w składowej m_hWndLastControl obiektu CDataExchange.
Listing 5.4. Stosowanie własnych metod DDX i DDV
// validView.cpp : implementation of the CValidView class
//
#include "stdafx.h"
#include "valid.h"
#include "validDoc.h"
#include "validView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CValidView
IMPLEMENT_DYNCREATE(CValidView, CFormView)
// Kreator Class Wizard nie umieści tego makra w mapie komunikatów
// gdyż sądzi, że to okna dialogowe obsługują OnOK. Obsługują, to
// prawda, jednakże my dysponujemy widokiem dialogu a nie oknem
// dialogowym
BEGIN_MESSAGE_MAP(CValidView, CFormView)
//{{AFX_MSG_MAP(CValidView)
ON_COMMAND(IDOK,OnOK)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CValidView construction/destruction
CValidView::CValidView()
: CFormView(CValidView::IDD)
{
validating=FALSE;
vid=0;
//{{AFX_DATA_INIT(CValidView)
m_age = 18;
m_name = _T("");
m_wager = 1.0f;
//}}AFX_DATA_INIT
// TODO: add construction code here
}
CValidView::~CValidView()
{
}
void CValidView::DoDataExchange(CDataExchange* pDX)
{
CFormView::DoDataExchange(pDX);
validating=TRUE; // zapobiegniej rekursju
// wywołania usunięte z komentarzy kreator Class Wizard i zmodyfikowane
try
{
if (!vid||vid==IDC_AGE) DDX_Text(pDX, IDC_AGE, m_age);
if (!vid||vid==IDC_AGE) DDV_MinMaxInt(pDX, m_age, 18, 150);
if (!vid||vid==IDC_NAME) DDX_Text(pDX, IDC_NAME, m_name);
if (!vid||vid==IDC_NAME) DDV_MaxChars(pDX, m_name, 64);
if (!vid||vid==IDC_WAGER) DDX_Text(pDX, IDC_WAGER, m_wager);
if (!vid||vid==IDC_WAGER) DDV_MinMaxFloat(pDX, m_wager, 1.f, 100.f);
//{{AFX_DATA_MAP(CValidView)
//}}AFX_DATA_MAP
validating=FALSE;
}
catch (...)
{
validating=FALSE; // pamiętaj aby ustawić tą flagę
throw; // ponownie zgłoś wyjątek
}
}
BOOL CValidView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CFormView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CValidView diagnostics
#ifdef _DEBUG
void CValidView::AssertValid() const
{
CFormView::AssertValid();
}
void CValidView::Dump(CDumpContext& dc) const
{
CFormView::Dump(dc);
}
CValidDoc* CValidView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CValidDoc)));
return (CValidDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CValidView message handlers
void CValidView::OnOK()
{
if (UpdateData(TRUE))
MessageBox("Wager placed");
}
BOOL CValidView::OnCommand(WPARAM wParam, LPARAM lParam)
{
if (HIWORD(wParam)==EN_KILLFOCUS&&!validating)
{
vid=LOWORD(wParam);
UpdateData(TRUE);
// zresetuj warunki
// Poniższą linię będziesz musiał wykonywać jesli
// nie przechwytujesz wyjątków w metodzie DoDataExchange
// validating=FALSE;
vid=0;
}
return CFormView::OnCommand(wParam, lParam);
}
Listing 5.5. Niestandardowe metody DDX i DDV.
#include <stdafx.h>
#include "customdd.h"
// Transfer
void DDX_EnableWindow(CDataExchange *pDX, int id, BOOL &flag)
CWnd *ctl=pDX->m_pDlgWnd->GetDlgItem(id);
if (pDX->m_bSaveAndValidate)
flag=ctl->IsWindowEnabled();
else
ctl->EnableWindow(flag);
// Weryfikacja
void DDV_MinMaxCurrency(CDataExchange *pDX, float val, float min, float max)
{
CWnd *editctl=CWnd::FromHandle(pDX->m_hWndLastControl);
CString s;
int n;
if (pDX->m_bSaveAndValidate)
{
// Zastosowanie operacji matematycznych do określenia czy coś
// zostało jest złym rozwiązaniem ze względu na błędy zaokrągleń;
// zamiast tego należy zastosować metody służące do obsługi
// łańcuchów znaków
editctl->GetWindowText(s);
n=s.Find('.');
if (n!=-l && n+3<s.GetLength())
{
AfxMessageBox("Please enter the data to the nearest penny!");
pDX->FailO;
> DDV_MinMaxFloat(pDX,val,min,max); // niech domyślna
// funkcja zrobi resztę
Na listingu 5.5 przedstawiona została także metoda wymiany danych, która zamienia wartość typu BOOL na stan zaznaczenia elementu kontrolnego. Standardowo, musiałbyś użyć kreatora Class Wizard i skojarzyć ze zmienną cały element kontrolny (np.: typu CButton). Wszystko to, tylko i wyłącznie w celu umożliwienia ustawiania stanu elementu kontrolnego. Zaprezentowana na listingu 5.5 funkcja operuje na normalnych zmiennych, dzięki czemu stan elementu kontrolnego można określać za pomocą prostej zmiennej typu BOOL. Pamiętaj tylko, że żadne modyfikacje nie będą uwzględniane aż do momentu wywołania metody UpdateData.
Integracja z kreatorem Class Wizard
Kiedy już stworzysz własne funkcje DDX oraz DDV będziesz je mógł zintegrować bezpośrednio z kreatorem Class Wizard (spójrz na rysunek 5.1). Jest to szczególnie wygodne, gdy pracujesz wraz z innymi programistami nad dużym projektem.
Jeśli chcesz, żeby Twoje procedury dostępne były tylko w jednym projekcie, będziesz mógł umieścić je w pliku CLW tego projektu. Oczywiście, jeśli coś się zdarzy i plik CLW zostanie zniszczony (a to się czasami zdarza), informacje o własnych metodach DDX i DDV będziesz musiał wpisywać od nowa. Innym rozwiązaniem jest stworzenie pliku DDX.CLW i umieszczenie go w kartotece BIN, razem z biblioteką DLL o nazwie MFCCLWZ.DLL (zazwyczaj jest to kartoteka \Proram Files\DevStudio\SharedIDRBin). Przy takim rozwiązaniu Twoje funkcje DDX będą mogły być stosowane we wszystkich projektach.
Poniżej opisałem sposób, w jaki to wszystko działa. Jeśli stworzysz plik DDX.CLW będziesz musiał dodać do niego sekcję o nazwie [ExtraDDX] (patrz listing 5.6). Wygląda to prawie tak samo, jak dobrze znane sekcje z plików INI, z tym jednym wyjątkiem, iż w nazwie sekcji ważna jest wielkość liter. Jeśli chcesz zmodyfikować tylko plik CLW jednego projektu, wystarczy, że wprowadzisz odpowiednie zmiany w sekcji [GeneralInfo] tego pliku. Pierwszą modyfikacją, jaką będziesz musiał wprowadzić będzie dodanie linii o następującej postaci:
ExtraDDXCount=x
Zamiast x powinieneś podać ilość procedur DDX, które chcesz dodać do kreatora Class Wizard. Oczywiście liczba ta może się zmieniać, na przykład wtedy, gdy do projektu dodasz kolejne metody służące do transferu danych. Dla pierwszej dodawanej funkcji DDX będziesz następnie musiał dodać nową linię zaczynającą się od wyrażenia ExtraDDXl=. W liniach odpowiadających kolejnym metodom cyfry na końcu wyrażenia będą odpowiednio modyfikowane, np.: ExtraDDX2, ExtraDDX3, itp.
Jakie informacje podawane są w dalszej części linii? Może to być 7, 10 lub 12, pól w zależności od tego, co chcesz zrobić. Każde pole zakończone jest średnikiem. Znaczenie każdego z pól zostało opisane w Tabeli 5.2, a przykład ich zastosowania przedstawiłem na listingu 5.6. Zwróć uwagę, iż ze względu na szerokość strony, linia rozpoczynająca się od ExtraDDXl=E... została zapisana w dwóch liniach.
________Tabela 5.2. Rejestracja własnych funkcji DDX i PDV.
Pole Opis
1 Typ elementu kontrolnego, którego dotyczy funkcja DDX (E - stosowane dla pól tekstowych).
2 Nie używane.
3 Typ właściwości (zazwyczaj wartość: “Yalue"; odnosi się do pierwszego pola kombo w oknie dialogowym kreatora Class Wizard).
4 Typ zmiennej.
5 Wartość początkowa.
6 Nazwa funkcji DDX bez prefiksu “DDX_".
7 Komentarz.
8 Nazwa funkcji DDV bez prefiksu “DDV_" (opcjonalna).
9 Nazwa pierwszego argumentu funkcji DDV (opcjonalna).
10 Typ pierwszego argumentu funkcji DDV (na przykład: f- float; opcjonalny).
11 Nazwa drugiego argumentu funkcji DDV (opcjonalna).
12 Typ drugiego argumentu funkcji DDV (opcjonalny).
Listing 5.6. Plik DDX.CLW.
[ExtraDDX] ExtraDDXCoutn=2
ExtraDDXl=E;;Value;
Curency;O.0;Text;Floating Point
Currency;minMaxCurrency;
^Mi&nimum;f;Ma&ximum;f
ExtraDDX2=bBECcRLlMnn; ;Enable State; BOOL.-TRUE; EnableWindow; Window
Enabled Status
Zauważ, że trzecie pole ma zazwyczaj wartość "Value". Użycie tej wartości umożliwia wyświetlenie w oknie dialogowy DDX wszystkich standardowych typów danych. Oznacza to także, że powinieneś zdefiniować swój własny typ danych zamiast ponownie używać jednego z typów standardowych. Oto dlaczego przykładowy program sprawdzający poprawność zapisania waluty używa typu Currency (zdefiniowanego za pomocą instrukcji typedef), a nie typu float. W oknie dialogowym kreatora możesz używać określonych przez siebie nazw. I tak, w definicji funkcji służącej do transferu wartości logicznych, użyty został napis “Enable Status". Kreator Class Wizard wyświetla tę opcję wraz z innymi, standardowymi typami w polach Control oraz Value.
Jeśli nie chcesz, nie musisz definiować żadnych własnych metod DDV. Możesz także mieszać własne metody weryfikacji danych ze standardowymi metodami ich przekazywania (tak właśnie zrobiłem w linii ExtraDDXl). Zauważ, że nazwy funkcji DDX i DDV nie zaczynają się od prefiksu DDX_ oraz DDV_ - to kreator Class Wizard dodaje te prefiksy do generowanego kodu. Dlatego ważne jest, abyś dodał je także do funkcji, które tworzysz sam.
Stosowanie pasków dialogowych
Bliskimi krewnymi okien dialogowych są paski dialogowe. Paski dialogowe są stosunkowo rzadko spotykanym narzędziem, które przypomina paski narzędzi. Przy tworzeniu pasków dialogowych zamiast bitmap stosowane są jednak szablony okien dialogowych. Przykładowy pasek dialogowy przedstawiony został na rysunku 5.2. Nie jest on efektowny, jednak bez problemu mógłbyś uatrakcyjnić jego wygląd stosując przyciski z bitmapami oraz inne elementy kontrolne.
Dodawanie pasków dialogowych do programu jest proste - są one tworzone niemal identycznie jak paski narzędzi, z tą jedną różnicą, iż są to klasy potomne klasy CDialogBar. Za pomocą Galerii Komponentów (Component Gallery) możesz automatycznie dodać pasek dialogowy do swojego projektu. Wystarczy w tym celu wybrać z menu głównego opcję Projecf=>Add, a następnie Project^Components and Controls. Po wyświetleniu okna dialogowego Galerii Komponentów wybierz opcję Developer Studio Components, a następnie opcję Dialog Bar.
Jak możesz zobaczyć na rysunku 5.2, na pasku dialogowym można umieszczać nie tylko przyciski, lecz także inne elementy kontrolne. Dlaczego w takim razie, większość programistów cały czas stosuje tradycyjne paski narzędzi? Istnieje ku temu kilka powodów. Po pierwsze, kreator App Wizard automatycznie generuje pasek narzędzi przy tworzeniu programu. Po drugie, w systemie Windows 3.1 (a także w nieco mniejszym stopniu, w systemie Windows 95) posiadanie paska dialogowego z dużą ilością elementów kontrolnych w dużym stopniu wyczerpuje zasoby systemu. Standardowe paski narzędzi zużywają znacznie mniej zasobów. Ostatnim powodem jest to, że pasków dialogowych nie można zakotwiczyć przy wszystkich krawędziach okna aplikacji. Wynika to z prostego faktu, że MFC nie wie, w jaki sposób należy obracać wyświetlane elementy kontrolne (obracanie bitmap jest znacznie prostsze).
Aby zrozumieć, na czym polega problem obracania pasków dialogowych, spróbuj przeciągnąć dowolny pasek narzędzi wyświetlony pod paskiem menu programu (na przykład Developer Studio) i umieścić go przy prawej krawędzi okna. Pasek narzędzi zostanie obrócony, dostosowując się do nowego położenia. Teraz spójrz na rysunek 5.3. Przedstawia on wygląd programu z rysunku 5.2 po przeciągnięciu paska dialogowego i zakotwiczeniu go przy lewej krawędzi okna. Nie jest to ładny widok, prawda?
Aby zapobiec takim sytuacjom, podczas dodawania paska dialogowego Galeria Komponentów umożliwi Ci określenie krawędzi ekranu, przy której tworzony pasek zostanie umieszczony. Wybranie jednej z krawędzi spowoduje, iż pasek będzie mógł być zakotwiczony tylko przy niej lub przy drugiej, równoległej krawędzi. Musiałem zmodyfikować kod umieszczony w klasie CMainFrame, aby móc sporządzić rysunek 5.3, gdyż normalne przeciągnięcie i zakotwiczenie paska przy tej krawędzi byłoby niemożliwe.
Modyfikowanie pasków narzędzi
Kolejną cechą pasków narzędzi, powodującą, że są one chętniej stosowane niż paski dialogowe, jest udostępnienie możliwości modyfikowania zawartości paska przez użytkownika. No, a przynajmniej coś w tym stylu. Zaraz opowiem całą historię. Otóż starsze wersje MFC udostępniały cały kod potrzeby do tworzenia pasków narzędzi. Nowsze wersje tworzą jedynie “otoczkę" pozwalającą na wykorzystywanie w programie standardowego elementu kontrolnego. To właśnie ten standardowy element kontrolny udostępnia wszystkie narzędzia potrzebne do modyfikowania postaci paska. Jednak ze względu na to, że MFC bardzo się stara, aby obiekty klasy CToolBar wyglądały tak samo jak dotychczas, niezwykle trudno jest udostępnić użytkownikowi narzędzia do modyfikacji paska. Fakt, jest to zadanie trudne, ale nie niemożliwe.
Jakie sposoby modyfikowania pasków narzędzi są dostępne? Otóż istnieją narzędzia pozwalające na przesuwanie przycisków w obrębie paska (należy w tym celu wcisnąć przycisk Shift i przeciągnąć przycisk za pomocą myszki). Jeśli przeciągniesz przycisk poza powierzchnię paska narzędzi i upuścisz, przycisk zniknie. Standardowy element kontrolny udostępnia także specjalne okno dialogowe, które pozwala użytkownikowi dodawać, usuwać oraz zmieniać kolejność przycisków na pasku narzędzi. Dodatkowo paski narzędzi dysponują także metodami pozwalającym na zapisanie ich stanu w rejestrze systemowym i późniejsze jego odtworzenie.
Wszystkie problemy z klasą CToolBar wynikają z faktu, iż jest ona “jednokierunkową otoczką" standardowego elementu kontrolnego. Oznacza to, że klasa ta wykorzystuje klasę CToolBarCtrl (standardowy element kontrolny implementujący pasek narzędzi), jednak jakiekolwiek modyfikacje dokonane na elemencie kontrolnym nie będą zauważone i uwzględnione w obiekcie klasy CToolBar. Z tego właśnie powodu, metody służące do zapisywania i odczytywania postaci paska z rejestru systemowego, są całkowicie bezużyteczne, gdyż po powtórnej inicjalizacji paska, modyfikacje wcześniej w nim dokonane nie zostaną uwzględnione. Jeśli będziesz chciał zapamiętywać i odtwarzać stan paska narzędzi, będziesz musiał stworzyć własne metody umożliwiające poprawne wykonywanie tych operacji.
Umożliwienie wykorzystania innych cech standardowego elementu kontrolnego nie przysparza, na szczęście, większych trudności. Aby umożliwić modyfikowanie postaci paska, wystarczy nadać oknu standardowego elementu kontrolnego styl CCS_ADJUSTABLE, Nie możesz podać tego stylu podczas tworzenia paska narzędzi, gdyż MFC stosuje ten sam styl (jego wartość) do zupełnie innych celów. Możesz go określić już po stworzeniu paska:
::SetWindowsLong(m_wndToolBar.GetToolBarCtrl().m_hWnd,GWL_STYLE, m_wndToolBar.GetToolBar().GetStyleO |CCS^ADJUSTABLE);
Będziesz także musiał obsługiwać niektóre komunikaty WM_NOTIFY przesyłane z paska narzędzi do okna ramki (lub innego okna, w którym umieszczony jest pasek narzędzi). Procedury obsługi tych komunikatów będziesz mógł bez problemów zdefiniować za pomocą makr ON_NOTIFY umieszczonych w mapie komunikatów. Oczywiście kreator Class Wizard nie będzie miał pojęcia co chcesz zrobić, dlatego też makra te będziesz musiał umieścić w mapie komunikatów samodzielnie.
Pełną listę komunikatów wysyłanych przez paski narzędzi możesz znaleźć w elektronicznej dokumentacji dostarczanej wraz z programem Visual C++. W Tabeli 5.3 przedstawione zostały jedynie te komunikaty, które są istotne z punktu widzenia modyfikowania postaci pasków narzędzi omawianych w tym rozdziale (patrz listing 5.7).
Tabela 5.3. Komunikaty powiadamiania generowane przez paski narzędzi.
Komunikat Opis
TBN_QUERYINSERT Czy użytkownik może wstawić ten przycisk?
TBN_QUERYDELETE Czy użytkownik może usunąć ten przycisk?
TBN_GETBUTTONINFO Pobierz informacje o przycisku i umieść je w strukturze TBNOTIFY.
TBN_RESET Przywróć oryginalną postać paska narzędzi.
TBN_TOOLBARCHANGE Pasek narzędzi został zmodyfikowany.
Listing 5.7. Ramka z paskiem narzędzi pozwalającym na modyfikacje swojej postaci.
// MainFrm.cpp : implementation of the CMainFrame class
//
#include "stdafx.h"
#include "tools.h"
#include "MainFrm.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CMainFrame
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_WM_CREATE()
ON_COMMAND(ID_CUSTOMIZE, OnCustomize)
ON_NOTIFY(TBN_QUERYINSERT,AFX_IDW_TOOLBAR,NotifyQI)
ON_NOTIFY(TBN_QUERYDELETE,AFX_IDW_TOOLBAR,NotifyQI)
ON_NOTIFY(TBN_GETBUTTONINFO,AFX_IDW_TOOLBAR,NotifyInfo)
ON_NOTIFY(TBN_RESET,AFX_IDW_TOOLBAR,NotifyReset)
ON_NOTIFY(TBN_TOOLBARCHANGE,AFX_IDW_TOOLBAR,NotifyChange)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
static UINT indicators[] =
{
ID_SEPARATOR, // status line indicator
ID_INDICATOR_CAPS,
ID_INDICATOR_NUM,
ID_INDICATOR_SCRL,
};
/////////////////////////////////////////////////////////////////////////////
// CMainFrame construction/destruction
CMainFrame::CMainFrame()
{
// TODO: add member initialization code here
}
CMainFrame::~CMainFrame()
{
}
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return -1;
// Ustawienie w tym miejscu stylu CCS_ADJUSTABLE nie daje żadnego rezultatu
if (!m_wndToolBar.Create(this,WS_CHILD | WS_VISIBLE |
CBRS_TOP| CCS_ADJUSTABLE|CCS_ADJUSTABLE) ||
!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Failed to create toolbar\n");
return -1; // fail to create
}
if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Failed to create status bar\n");
return -1; // fail to create
}
// TODO: Remove this if you don't want tool tips or a resizeable toolbar
m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC ) ;
// TODO: Delete these three lines if you don't want the toolbar to
// be dockable
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
// Umożliwij modyfikowanie zawartości paska narzędzi
::SetWindowLong(m_wndToolBar.GetToolBarCtrl().m_hWnd,GWL_STYLE,m_wndToolBar.GetToolBarCtrl().GetStyle()| CCS_ADJUSTABLE);
// Odtwórz zapamiętany stan paska narzędzi
RestoreTBState();
return 0;
}
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CFrameWnd::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CMainFrame diagnostics
#ifdef _DEBUG
void CMainFrame::AssertValid() const
{
CFrameWnd::AssertValid();
}
void CMainFrame::Dump(CDumpContext& dc) const
{
CFrameWnd::Dump(dc);
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CMainFrame message handlers
// Handle QueryInsert and Delete as always true
void CMainFrame::NotifyQI(NMHDR *hdr,LRESULT *res)
{
*res=(LRESULT)TRUE;
}
void CMainFrame::NotifyChange(NMHDR *hdr,LRESULT *res)
{
SaveTBState(); // zapisz wszystkie zmiany
}
void CMainFrame::NotifyReset(NMHDR *hdr,LRESULT *res)
{
m_wndToolBar.LoadToolBar(IDR_MAINFRAME); // wyświetl standardowy pasek
RecalcLayout();
}
void CMainFrame::NotifyInfo(NMHDR *hdr, LRESULT *res)
{
// Twoim zadaniem w tej metodzie jest podanie wszystkich przycisków, które
// mogą zostać umieszczone na pasku narzędzi, nawet jeśli aktualnie nie są na nim widoczne.
// To jest dobre rozwiązanie, choć trudne gdyż w MFC przyciski są zapisane w jednym zasobie.
// Moje rozwiązanie: Wyświetl przyciski, które są aktualnie umieszczone na
// pasku, jeśli użytkownik będzie chciał uzyskać dostęp do wszystkich
// przycisków, to będzie musiał zresetować pasek narzędzi.
TBNOTIFY *nfy=(TBNOTIFY *)hdr;
int n=nfy->iItem;
*res=(n<=m_wndToolBar.GetCount());
if (*res) m_wndToolBar.GetToolBarCtrl().GetButton(n,&nfy->tbButton);
//MFC Tool bars don't usually have text so no need to put it in
//Paski narzędzi w MFC zazwyczaj nie mają żadnego tekstu
}
// Restore TB from registry
void CMainFrame::RestoreTBState()
{
CWinApp *app=AfxGetApp();
int n=app->GetProfileInt("Toolbar","Count",0);
if (n==0) return;
m_wndToolBar.SetButtons(NULL,n);
int i;
for (i=0;i<n;i++)
{
int cmd,style,image;
CString tag,root="TBTN%s%d";
tag.Format(root,"CMD",i);
cmd=app->GetProfileInt("Toolbar",tag,0);
tag.Format(root,"STY",i);
style=app->GetProfileInt("Toolbar",tag,0);
tag.Format(root,"IMG",i);
image=app->GetProfileInt("Toolbar",tag,0);
m_wndToolBar.SetButtonInfo(i,cmd,style,image);
}
RecalcLayout();
}
// Zapisz stan paskna narzędzi do rejestru systemowego
void CMainFrame::SaveTBState()
{
CWinApp *app=AfxGetApp();
int n=m_wndToolBar.GetToolBarCtrl().GetButtonCount();
app->WriteProfileInt("Toolbar","Count",n);
int i;
for (i=0;i<n;i++)
{
unsigned int cmd,style;
int image;
CString tag,root="TBTN%s%d";
m_wndToolBar.GetButtonInfo(i,cmd,style,image);
tag.Format(root,"CMD",i);
app->WriteProfileInt("Toolbar",tag,cmd);
tag.Format(root,"STY",i);
app->WriteProfileInt("Toolbar",tag,style);
tag.Format(root,"IMG",i);
app->WriteProfileInt("Toolbar",tag,image);
}
}
// Wyświetl standardowe okno dialogowe
void CMainFrame::OnCustomize()
{
m_wndToolBar.GetToolBarCtrl().Customize();
}
Komunikaty TBN_QUERYINSERT oraz TBN_QUERYDELETE pozwalają Ci na stworzenie przycisków, których nie będzie można dodać ani usunąć. Nasz przykładowy program zwraca wartość TRUE dla obu tych komunikatów, dzięki czemu cała zawartość paska narzędzi może zostać zmodyfikowana.
Kiedy użytkownik wybierze z menu programu opcję Edit^Customize Toolbar, wywoływana zostanie metoda Customize. Jest to wbudowana metoda służąca do wyświetlenia okna dialogowego pozwalającego ma modyfikację zawartości paska narzędzi. W wyniku wywołania tej metody element kontrolny generuje komunikat TBN_GETBUTTONINFO. Podczas obsługi tego komunikatu możesz określić, jakie przyciski użytkownik będzie mógł umieścić na pasku narzędzi. Zadanie to, w przypadku pasków narzędzi wykorzystywanych przez MFC, jest jednak skomplikowane, gdyż przyciski umieszczone są w zasobach programu. Ja osobiście wybrałem inny sposób rozwiązania tego zagadnienia. W odpowiedzi na komunikat TBN_GETBUTTONINFO wyświetlam wszystkie przyciski, jakie aktualnie są dostępne na pasku narzędzi. Użytkownik może usuwać przyciski i zmieniać ich kolejność, jednak nie może dodawać nowych przycisków do paska narzędzi.
Cóż więc się stanie, jeśli użytkownik usunie przycisk z paska narzędzi, a potem zmieni zdanie i będzie chciał z powrotem go dodać? W takiej sytuacji wystarczy kliknąć na przycisku Reset co spowoduje wygenerowanie komunikatu TBN_RESET. W odpowiedzi na ten komunikat, kod obsługi standardowego elementu kontrolnego załaduje oryginalną postać paska narzędzi z zasobów programu. Teraz będziesz mógł j ą ponownie zmodyfikować.
Ostatnim kawałkiem tej układanki jest metoda umożliwiająca zapisanie stanu paska narzędzi. Okno ramki z listingu 5.7 definiuje dwie metody o nazwach: SaveTBState oraz RestoreTBState. Metody te pobierają informacje o każdym z przycisków, przechowywane w obiekcie CToolBar (nie CToolBarCtrl), a proste odwołania do metod obiektu aplikacji umożliwiają zapisanie i odczytanie informacji z rejestru systemowego.
Przedstawione powyżej rozwiązania nie są idealne, jednak mogą w ogromnym stopniu zwiększyć możliwości funkcjonalne pasków narzędzi. Innym, alternatywnym, rozwiązaniem może być rezygnacja ze stosowania pasków narzędzi udostępnianych przez MFC i zaimplementowanie własnej klasy działającej na bazie standardowego elementu kontrolnego. Jest to zadanie możliwe do zrealizowania, choć wcale nie będę zazdrościł temu, kto się go podejmie; rozwiązanie przedstawione w tej sekcji jest znacznie prostsze.
Dostosowywanie standardowych okien dialogowych
Standardowe okna dialogowe są doskonałym pomysłem. Pozwalają one na bardzo proste dodanie do programu często wykorzystywanych okien dialogowych. Użytkownicy lubią to rozwiązanie, gdyż powoduje ono, że te same możliwości funkcjonalne są we wszystkich programach obsługiwane w taki sam sposób. Dzięki MFC stosowanie większości standardowych okien dialogowych stało się jeszcze prostsze (patrz Tabela 5.4).
Tabela 5.4. Standardowa okna dialogowe.
Okno dialogowe Klasa MFC Struktura systemowa Lokalizacja szablonu
Okno wyboru kolorów CColorDialog CHOOSECOLOR COLOR.DLG / COLORDLG.H
Okno wyboru plików CFileDialog OPENFILENAME FILEOPEN.DLG / DLGS.H
Okno wyboru czcionek CFontDialog CHOOSEFONT FONT.DLG / DLGS.H
Okno drukowania CPrintDialog PRINTDLG PRNSETUP.DLG / DLGS.H
Okno wyszukiwania/zastępowania CFindReplaceDialog FINDREPLACE FINDTEXT.DLG / DLGS.H
Jednak, w wielu przypadkach będziesz chciał użyć czegoś, co w bardzo niewielkim stopniu różni się od standardu. Projektanci standardowych okien dialogowych wiedzieli o tym i postarali się w jak największym stopniu ułatwić możliwości modyfikacji tych okien dialogowych. Jeśli kiedykolwiek próbowałeś to zrobić za pomocą Windows API, zapewne wiesz, że wymaga to ogromnego nakładu pracy. MFC znacznie ułatwia to zadanie, sprawiając, że jego wykonanie staje się realne i warte zachodu.
Wszystkie standardowe okna dialogowe pozwalają na przeprowadzanie modyfikacji tego samego typu; jedynym wyjątkiem jest charakterystyczne okno dialogowe do wyboru plików, stosowane w Eksploratorze Windows (w systemach Windows 95 i Windows NT 4.0). W następnej części rozdziału zobaczysz jak działają te okna dialogowe. Na razie jednak, określenie “standardowe okna dialogowe" będzie dotyczyło standardowych okien dialogowych, za wyjątkiem okna służącego do wyboru plików.
Szczegółowy opis sposobu modyfikowania
Czasami będziesz chciał wprowadzić proste modyfikacje do sposobu obsługi lub wyglądu standardowego okna dialogowego. Dla przykładu, możesz chcieć zmienić etykietę jakiegoś przycisku. W takim przypadku możesz posłużyć się istniejącym, predefiniowanym szablonem okna dialogowego. Takie modyfikacje sprowadzają się do prostej zmiany informacji już umieszczonych w szablonie okna. W przypadku bardziej złożonych modyfikacji, będziesz prawdopodobnie wolał użyć własnego szablonu okna dialogowego. Zazwyczaj będziesz go tworzył poprzez zmodyfikowanie szablonu już istniejącego. Poniżej przedstawione zostały czynności, jakie będziesz musiał wykonać podczas modyfikowania standardowych okien dialogowych (w obu wspomnianych wyżej przypadkach):
1. Zaimportuj odpowiedni szablon okna dialogowego i zmodyfikuj go zgodnie ze swoimi potrzebami.
2. Za pomocą kreatora Class Wizard stwórz klasę potomną.
3. W konstruktorze stworzonej klasy zmodyfikuj stosowaną strukturę systemową. Jeśli chcesz użyć swojego własnego szablonu okna dialogowego (a nie szablonu standardowego), podaj identyfikator jego zasobu w składowej lp_TemplateName a następnie dodaj flagę ocjr_ENABLETEMPLATE do stylów tworzonego okna (xx jest odpowiednim prefiksem, zależnym od rodzaju tworzonego okna dialogowego). W składowej hInstance stworzonego okna dialogowego powinieneś zapisać uchwyt określony za pomocą funkcji AfsGetInstanceHandle.
4. Zastosuj standardowy mechanizm map komunikatów do przechwycenia i obsługi komunikatów generowanych w oknie dialogowym.
Błąd w oknie dialogowym służącym do wyboru kolorów!
Z jakichś niewyjaśnionych powodów, składowa hInstance struktury CHOOSECOLOR zdefiniowana została jako HWNDN, a nie jako HINSTANCE. W starszych kompilatorach nie ma to żadnego znaczenia. Jednakże w kompilatorach C++ powoduje to wygenerowanie błędu. Jedynym rozwiązaniem tego problemu jest konwersja typu uchwytu na HWND, mimo, że jest to jawnym błędem.
Przykładowe okno wyboru kolorów
Wygląda na to, że jest to raczej proste, nieprawdaż? Faktycznie, zazwyczaj zmodyfikowanie standardowego okna dialogowego jest w miarę prostym zadaniem, choć czasami osiągnięcie oczekiwanych rezultatów będzie wymagało przeprowadzenia kilku eksperymentów. Zastanówmy się nad standardowym oknem do wyboru kolorów przedstawionym na rysunku 5.5 (oraz na listingu 5.8). Wygląda na to, że modyfikacje, które chcesz wprowadzić są bardzo proste. Polegają one na zmianie etykiet dwóch przycisków i dodaniu nowego, trzeciego przycisku. Jeśli jedyną modyfikacją, którą chciałbyś przeprowadzić, byłaby zmieniana etykiety przycisku, mógłbyś to zrobić bez zmieniania szablonu okna dialogowego. Oczywiście, trzeci przycisk także można by dodać podczas wykonywania programu, jednak rozwiązanie takie byłoby nieco bardziej skomplikowane.
Listing 5.8. Zmodyfikowane okno wyboru kolorów
// customco.cpp : implementation file
//
#include "stdafx.h"
#include "color.h"
#include "customco.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CCustomColor dialog
CCustomColor::CCustomColor(COLORREF color,DWORD flag,CWnd* pParent)
: CColorDialog(color,flag,pParent)
{
//{{AFX_DATA_INIT(CCustomColor)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
m_reset=FALSE;
/* m_cc jest typu CHOOSECOLOR */
m_cc.lpTemplateName="CHOOSECOLOR";
m_cc.Flags|=CC_ENABLETEMPLATE;
// nie usuwaj istniejących flag
// śmieszne, że składowa hInstance zadeklarowano jako zmienną typu HWND
m_cc.hInstance=(HWND)AfxGetInstanceHandle();
}
void CCustomColor::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CCustomColor)
// NOTE: the ClassWizard will add DDX and DDV calls here
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CCustomColor, CColorDialog)
//{{AFX_MSG_MAP(CCustomColor)
ON_BN_CLICKED(IDC_RESET, OnReset)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CCustomColor message handlers
void CCustomColor::OnReset()
{
m_reset=TRUE;
EndDialog(IDOK);
}
BOOL CCustomColor::OnInitDialog()
{
BOOL result=CColorDialog::OnInitDialog();
CWnd *newbutton=GetDlgItem(IDC_RESET);
newbutton->ShowWindow(SW_SHOW);
newbutton->EnableWindow();
return result;
}
W programie przedstawionym na listingu 5.8 wszystkie czynności wykonywane są przez szablon okna dialogowego. Jednakże bez wykonania pewnych szczególnych operacji nowy przycisk nie zostanie wyświetlony. Dlaczego? Standardowe okno służące do wyboru kolorów jest nieco dziwne. Początkowo, wyświetlane okno dialogowe jest niewielkie, a w jego dolnej części wyświetlany jest przycisk umożliwiający powiększenie okna do pełnych wymiarów. To okno dialogowe działa w następujący sposób: w momencie wyświetlania okna chowane są wszystkie elementy kontrolne umieszczone wewnątrz niego; następnie określany jest stan okna, i na jego podstawie wyświetlane odpowiednie elementy kontrolne. Jednakże Twój nowy przycisk nie znajduje się na liście używanej przez standardowy kod okna przy wyświetlaniu umieszczonych na nim elementów. Dlatego też, jeśli będziesz chciał, aby Twój przycisk był widoczny, będziesz go musiał wyświetlić samemu (na przykład w metodzie OnlnitDialog).
Kliknięcie na przycisku Reset powoduje ustawienie odpowiedniej flagi (m_reset) i ukrycie okna dialogowego. Program wywołujący okno dialogowe musi sprawdzić stan tej flagi, aby określić czy użytkownik kliknął na przycisku Reset. Zapewnia to największą elastyczność działania okna dialogowego, gdyż każdy z programów używających okna może zdecydować, co oznacza kliknięcie przycisku Reset i jak należy na nie zareagować.
Modyfikowanie wszystkich innych standardowych okien dialogowych jest bardzo podobne do modyfikowania okna dialogowego do wyboru kolorów. Modyfikowanie pozostałych okien dialogowych jest nawet prostsze, gdyż nie ukrywają one umieszczonych w nich elementów kontrolnych i nie posiadaj ą błędów w odpowiadających im strukturach systemowych. Pamiętaj jednak, że okno dialogowe służące do wyboru kolorów nie działa w taki sam sposób jak pozostałe okna dialogowe.
Modyfikowanie okna dialogowego służącego do wyboru plików
Modyfikacja standardowego okna dialogowego służącego do wyboru plików, polegająca na zmodyfikowaniu jego szablonu, przysparza znacznie większych kłopotów, a to z tego prostego powodu, że szablonu tego nie będziesz mógł zmienić. Zamiast tego, podczas tworzenia okna dialogowego, będziesz musiał podać szablon, na podstawie którego zostanie stworzone okno potomne standardowego okna dialogowego. W tym oknie dialogowym możesz określać położenie istniejących elementów kontrolnych oraz dodawać nowe elementy. Istnieją także specjalne komunikaty, które możesz wysyłać do okna dialogowego w celu określenia jego wyglądu.
Twój szablon powinien definiować okno potomne, stosować elementy kontrolne 3D i nie mieć ramki. Podczas określania stylów tworzonego okna dialogowego pamiętaj także, aby było ono widoczne, było elementem kontrolnym oraz stosowało styl WS_CLIPSIBLINGS. Jeśli chcesz kontrolować położenie, w którym będzie wyświetlana standardowa część okna dialogowego, możesz posłużyć się niewidocznym elementem kontrolnym (dowolnego typu) o specjalnym identyfikatorze stc32. Miejsce, w którym zostanie umieszczony ten element kontrolny, będzie odpowiadało miejscom, w których zostaną wyświetlone elementy kontrolne okna dialogowego. Jednakże Visual C++ nie zna tego specjalnego identyfikatora (jest on zdefiniowany w pliku nagłówkowym DLGS.H) i definiuje swój własny identyfikator o identycznej nazwie. Będziesz więc musiał otworzyć plik nagłówkowy RESOURCE.H i usunąć z niego definicję identyfikatora stc32. Niestety czasami powoduje to błędne działanie kreatora Class Wizard.
Kiedy już wykonasz wszystkie powyższe czynności, będziesz musiał stworzyć klasę potomną klasy CFileDialog (w czym pomoże Ci kreator Class Wizard). Tworząc nową klasę odkryjesz, że w kreatorze Class Wizard nie będą widoczne dodane przez Ciebie elementy kontrolne; dzieje się tak dlatego, że, de facto, nie są one częścią standardowego elementu kontrolnego. Dlatego też procedury służące do obsługi tych komunikatów będziesz musiał ręcznie dodać do mapy komunikatów. Poniższa linia kodu służy do skojarzenia Twojego szablonu ze standardowym oknem dialogowym:
SetTemplate(O,IDD_CUSTOMTEMPLATE);
Jako drugi argument wywołania tej metody powinieneś podać identyfikator zasobu Twojego szablonu okna dialogowego. Pierwszy argument tej metody określa identyfikator zasobu, który zostanie użyty w przypadku uruchamiania programu w innych systemach operacyjnych niż Windows 95 i NT 4.0 (gdyż tylko w tych systemach dostępne jest specjalne okno dialogowe obsługi plików, wyświetlane w Eksploratorze Windows).
Klasa MFC, umożliwiająca obsługę tego okna dialogowego, nie udostępnia wszystkich metod, które mogłyby Ci się przydać. Zamiast metod możesz jednak używać odpowiednich komunikatów (patrz Tabela 5.5). Pamiętaj jednak, że Twoje okno dialogowe jest oknem potomnym, a komunikaty muszą być wysyłane do okna rodzicielskiego (czyli standardowego okna dialogowego).
Tabela 5.5. Komunikaty stosowane do obsługi okna dialogowego służącego do wyboru kolorów
Komunikat Znaczenie
CMD_GETFILEPATH Pobierz pełną nazwę pliku
CMD_GETSPEC Pobierz nazwę pliku (bez informacji o ścieżce dostępu)
CMD_GETFOLDERPATH Pobierz ścieżkę dostępu do pliku
CMD_HIDECONTROL Ukryj element kontrolny (odpowiednie identyfikatory znajdziesz w plikach nagłówkowych DLGS.H oraz WINUSER.H)
CMD_SETCONTROLTEXT Określ tekst elementu kontrolnego (odpowiednie identyfikatory znajdziesz w plikach nagłówkowych DLGS.H oraz WINUSER.H)
CMD_SETDEFEXT Określ domyślne rozszerzenie.
Przykład zmodyfikowanego standardowego okna dialogowego służącego do wyboru plików możesz zobaczyć na rysunku 5.6; kod użyty do stworzenia tego okna przedstawiony został na listingu 5.9. Przykładowe okno posiada dodatkowy przycisk, tekst oraz pole wyświetlające nazwę aktualnej kartoteki. Przykładowy program przedstawiony na listingu 5.9 nie stosuje tego okna dialogowego do obsługi poleceń dostępnych w menu File, chociaż bez problemu można by użyć go do tego celu (patrz Rozdział 2).
Listing 5.9. Zmodyfikowane okno wyboru plików.
// CustomFile.cpp : implementation file
//
#include "stdafx.h"
#include "custfile.h"
#include "CustomFile.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CCustomFile
IMPLEMENT_DYNAMIC(CCustomFile, CFileDialog)
CCustomFile::CCustomFile(BOOL bOpenFileDialog, LPCTSTR lpszDefExt, LPCTSTR lpszFileName,
DWORD dwFlags, LPCTSTR lpszFilter, CWnd* pParentWnd) :
CFileDialog(bOpenFileDialog, lpszDefExt, lpszFileName, dwFlags, lpszFilter, pParentWnd)
{
SetTemplate(NULL,"FileTemplate"); // podaj szablon zasobu
}
BEGIN_MESSAGE_MAP(CCustomFile, CFileDialog)
//{{AFX_MSG_MAP(CCustomFile)
ON_COMMAND(IDC_TRYBTN, OnTry)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
void CCustomFile::OnTry() // jedno z niezliczonych niemądrych haseł
{
MessageBox("You can try, but it doesn't do any good.");
}
// Ustaw tekst kartoteki
void CCustomFile::OnFolderChange()
{
char dirbuf[256];
if (GetParent()->SendMessage(CDM_GETFOLDERPATH,sizeof(dirbuf),(LPARAM)dirbuf))
SetDlgItemText(IDC_DIR,dirbuf);
}
Podsumowanie
Okna dialogowe są bardzo istotną częścią programowania w systemie Windows. Bez ich użycia, oprogramowanie nawet najprostszego interfejsu użytkownika wymagałoby dużego nakładu pracy. MFC dostarcza klas, które umożliwiają traktowanie okien dialogowych jako widoków, pasków dialogowych lub normalnych okien dialogowych. Nawet standardowe okna dialogowe posiadają swoje klasy w MFC, dzięki czemu, bez szczególnych kłopotów, będziesz mógł je modyfikować.
Dzięki technikom DDX i DDV możesz tworzyć okna dialogowe, których obsługa wymaga minimalnych ilości kodu. Obsługa okien dialogowych może zostać jeszcze bardziej uproszczona dzięki tworzeniu własnych funkcji DDX i DDV.
Chociaż paski dialogowe stanowią pewną alternatywę dla pasków narzędzi, to jednak paski narzędzi zużywają mniej zasobów i łatwiej można je umieszczać w różnych miejscach okna aplikacji. Jedną z cech, dzięki której atrakcyjność pasków narzędzi znacznie wzrasta, jest możliwość modyfikowania ich przez użytkownika w trakcie działania programu. Wykonywanie tych samych operacji na paskach dialogowych byłoby znacznie trudniejsze.
Praktyczny przewodnik Okna dialogowe
Tworzenie niemodalnych okien dialogowych
Uaktualnianie zmiennych DDX po modyfikacji elementów kontrolnych
Weryfikacja danych po ich zmodyfikowaniu
Tworzenie własnych funkcji DDX i DDV
Integrowanie własnych funkcji DDX i DDV z kreatorem Class Wizard
Paski dialogowe a paski narzędzi
Modyfikowanie standardowych okien dialogowych
Okna dialogowe są najczęściej wykorzystywanymi elementami systemu Windows. MFC zdaje sobie sprawę z tego faktu i, próbując ułatwić Ci pracę, udostępnia wiele różnego typu narzędzi związanych z obsługą okien dialogowych.
Tworzenie niemodalnych okien dialogowych
Z technicznego punktu widzenia, tworzenie niemodalnych okien dialogowych niczym się nie różni do tworzenia modalnych okien dialogowych. Jedyną różnicą jest to, iż w przypadku tworzenia niemodalnego okna dialogowego wywołujesz metodę Create (i przekazujesz do niej identyfikator szablonu okna dialogowego), a nie metodę DoModal. W praktyce jednak, obsługa niemodalnych okien dialogowych przysparza nieco kłopotów. Po pierwsze, standardowe implementacje metod OnOK oraz OnCancel wywołują metodę EndDialog; chociaż metoda ta może być wywoływana tylko w przypadku obsługi modalnych okien dialogowych. Nawet jeśli w oknie dialogowym nie umieścisz przycisków OK i Cancel, i tak, w pewnych okolicznościach, wyżej wspomniane metody mogą zostać wywołane. Dla przykładu, naciśnięcie klawisza Esc spowoduje wywołanie metody OnCancel. Najbezpieczniejszym rozwiązaniem tego problemu jest przesłonięcie standardowych implementacji metod OnOK oraz OnCancel, i umieszczenie w nich wywołania metody DestroyWindow zamiast metody EndDialog.
Zazwyczaj modalne okna dialogowe tworzone są na stosie. No, bo dlaczego nie? Pomyśl nad tym. Kiedy zmienna reprezentująca okno dialogowe wychodzi poza zakres? Kiedy funkcja kończona jest funkcja, w której okno dialogowe zostało utworzone. Jednakże funkcja ta nie może się zakończyć, dopóki użytkownik nie zamknie okna dialogowego.
Dlatego, w chwili gdy funkcja będzie niszczyła zmienną reprezentującą okno dialogowe, użytkownik już dawno nie będzie go potrzebował. Cykl życia niemodalnych okien dialogowych jest całkowicie inny — działają one zazwyczaj znacznie dłużej, niż istnieją zmienne tworzone na stosie. Jeśli stworzysz niemodalne okno dialogowe jako składową klasy (na przykład, klasy ramki), nie napotkasz żadnych problemów. Często spotykanym rozwiązaniem jest tworzenie niemodalnych okien dialogowych na stercie, za pomocą operatora new. Nie jest to żaden szczególny problem, należy jedynie pamiętać, aby w odpowiednim momencie zwolnić pamięć przydzieloną oknu dialogowemu za pomocą operatora delete.
Możesz także tak skonstruować program, aby śledził on działanie okna dialogowego i sam, w odpowiednim momencie, usuwał go i zwalniał przydzieloną mu pamięć. Nie jest to jednak zbyt eleganckie rozwiązanie. Powinieneś raczej tak zaprogramować okno dialogowe, aby samo usunęło zajmowaną przez siebie pamięć, w momencie gdy użytkownik wyda polecenie jego zamknięcia. Można to zrobić w bardzo prosty sposób: Wystarczy przesłonić metodę PostNCDestry i umieścić w niej instrukcję delete this.
W przypadku tworzenia klas obsługujących niemodalne okna dialogowe (popatrz na przykład klasy CModelessDialog przedstawionej na listingu 5.1). Kod przedstawiony na tym listingu, w odpowiednich sytuacjach stosuje operator delete lub metodę DestroyWindow. Przykładowa klasa potrafi także samodzielnie zainicjalizować transfer danych DDX, w odpowiedzi kliknięcie na przycisku OK.
Uaktualnianie zmiennych DDX po modyfikacji elementów kontrolnych
Choć technika DDX sprawia, iż możesz sądzić że składowe w Twojej klasie są skojarzone z określonymi elementami kontrolnymi okna dialogowego, to rzeczywistość wygląda nieco inaczej. MFC, podczas inicjalizacji okna dialogowego, kopiuje zawartość Twoich składowych i umieszcza je we wskazanych elementach kontrolnych. Kiedy użytkownik kliknie na przycisku OK, cały proces wymiany danych wykonywany jest powtórnie, lecz w przeciwnym kierunku.
Czasami będziesz jednak chciał, aby dane zostały przekazane w wybranych przez Ciebie momentach, a nie tylko podczas inicjalizacji i zamykania okna dialogowego. W takich momentach będziesz mógł posłużyć się metodą UpdateData. Jeśli zechcesz, aby każda zmiana wprowadzona w elemencie kontrolnym okna dialogowego powodowała transfer danych, możesz przechwytywać komunikaty zmiany stanu elementu kontrolnego i w odpowiedzi na nie wywoływać metodę UpdateData.
Choć jest to rozwiązanie zapewniające poprawną pracę programu, nie jest ono jednak eleganckie, gdyż wymaga umieszczenia w mapie komunikatów pozycji definiujących procedurę obsługi zmiany wartości wszystkich elementów kontrolnych okna dialogowego. Co więcej, w momencie dodawania nowych pól, będziesz także musiał pamiętać o tym, aby dodać nowe pozycje do mapy komunikatów. Znacznie lepszym rozwiązaniem jest przesłonięcie metody OnCommand (oraz metody OnNotify, jeśli w oknie dialogowym korzystasz z elementów kontrolnych generujących komunikat WMJNOTIFY). W ten sposób, gdy zawartość któregokolwiek z elementów kontrolnych zostanie zmodyfikowana, automatycznie zostanie wywołana Twoja metoda, która z kolei wywoła metodę Update-Data. Rozwiązanie to w żaden sposób nie przeszkadza normalnym sposobom obsługi komunikatów WM_COMMAND. Przykład zastosowania tej metody przedstawiony został na listingu 5.1.
Musisz bardzo uważać, jeśli sprawdzasz poprawność danych techniką DDV przy wykorzystaniu tej metody. Każda zmiana zawartości elementu kontrolnego spowoduje bowiem transfer danych, nawet jeśli nie są one jeszcze poprawne. Może to doprowadzić do zgłaszania błędów podczas wykonywania funkcji DDV. Jeśli jednak musisz skorzystać z funkcji DDV, możesz się zastanowić na zastosowaniem flagi, ustawianej na początku metody OnCommand, która będzie sprawiała, że funkcje DDV nie będą wywoływane. Pod koniec metody OnCommand wartość flagi byłaby kasowana, dzięki czemu funkcje DDV byłyby normalnie wywoływane. (Przykłady modyfikowania mapy danych DDV zostały omówione w następnej sekcji.)
Weryfikacja danych po ich zmodyfikowaniu
DDV sprawdza poprawność danych na podstawie kryteriów podanych przez Ciebie w kreatorze Class Wizard. Jednak weryfikacja danych następuje tylko wtedy, gdy następuje transfer danych pomiędzy oknem dialogowym i zmiennymi, w których informacje mają być przechowywane. Większość użytkowników wolałaby jednak wiedzieć, czy dane są poprawne zaraz po ich podaniu.
Jeśli chcesz sprawdzać poprawność danych kontrolując po kolei każdy wpisywany znak, będziesz musiał stworzyć nową klasę potomną odpowiedniego elementu kontrolnego (więcej informacji na temat wyprowadzania klas potomnych znajdziesz w Rozdziale 4). DDV można jednak wykorzystać do weryfikacji danych, w momencie gdy użytkownik zakończy ich wprowadzanie w jednym polu i przeniesie ognisko wprowadzania do następnego. Cała sztuczka polega na tym, aby zorientować się, że to, co w MFC nazywane jest “mapą danych", jest w rzeczywistości prostą metodą o nazwie DoDataExchange. Metodę tę można modyfikować na wiele sposobów. Dla przykładu, możesz ustawiać flagę, której wartość będzie pozwalała na zweryfikowanie tylko jednego, ściśle określonego pola. Dzięki takiej modyfikacji metody DoDataExchange, w momencie gdy ognisko wprowadzania jest usuwane z interesującego Cię elementu kontrolnego, aby sprawdzić jego zawartość wystarczy przypisać odpowiednią wartość fladze i wywołać metodę UpdateData. Przykład zastosowania tej metody weryfikacji danych możesz zobaczyć na listingu 5.3.
Tworzenie własnych funkcji DDX i DDV
Dzięki wiedzy, że metoda DoDataExchange nie jest niczym magicznym, możesz zacząć tworzenie swoich własnych funkcji DDX i DDV. Najważniejszym elementem tych funkcji jest struktura CDataExchange (patrz tabela 5.1). Struktura ta zawiera informacje o aktualnie realizowanym transferze danych. Najlepszą metodą poznania sposobów pisania funkcji DDX i DDV jest przejrzenie predefiniowanych funkcji umieszczonych w kodzie źródłowym MFC lub przykładów zamieszczonych w tym rozdziale (patrz listing 5.5).
Interesujące rezultaty możesz także uzyskać modyfikując kod metody DoDataExchange (oczywiście, poza komentarzami umieszczanymi w metodzie przez kreatora Class Wizard). Dla przykładu, możesz w taki sposób użyć zmiennej jako jednego z parametrów weryfikacji poprawności danych; możesz także zarządzać transferem danych lub ich weryfikacją posługując się własnoręcznie podawanymi warunkami.
Integrowanie własnych funkcji DDX i DDV z kreatorem Class Wizard
Kiedy już stworzysz swoje własne metody DDX i DDV będziesz mógł dodać je do kreatora Class Wizard. Proces ten wymaga dodania specjalnych informacji do pliku .CL W (jeśli chcesz zmodyfikować możliwości kreatora tylko dla jednego projektu) lub do pliku DDX.CLW (jeśli chcesz udostępnić Twoje funkcje DDX i DDV we wszystkich projektach).
Składnia informacji, jakie będziesz musiał podać, jest dziwna, ale na szczęście nie jest trudna. Pamiętaj tylko, aby nazwy stworzonych przez Ciebie funkcji rozpoczynały się od prefiksów DDV_ lub DDX_. Szczegółowe informacje na temat składni danych dodawanych do pliku .CLW znajdziesz w Tabeli 5.2.
Paski dialogowe a paski narzędzi
Standardowo, podczas tworzenia aplikacji, kreator App Wizard umieszcza poniżej paska menu specjalny pasek narzędzi. Pasek ten pełni funkcję graficznego odpowiednika menu, na którym umieszczone są przyciski. Przyciski te są w rzeczywistości fragmentami jednej bitmapy (określanej czasami mianem zasobu paska narzędzi). Jeśli będziesz chciał umieścić na pasku przyciski (lub jakiekolwiek inne elementy kontrolne), będziesz musiał posłużyć się klasą CDialogBar, a nie klasą CToolBar.
Każda z tych dwóch klas ma swoje zalety i wady. Obiekty klasy CToolBar nie zużywają wielu zasobów systemowych. Mogą także automatycznie zmieniać orientację z poziomej na pionową. Z drugiej jednak strony, jest niezwykle ciężko umieścić na pasku narzędzi CToolBar jakiekolwiek inne elementy kontrolne oprócz normalnych przycisków. Nawet tak niewielka modyfikacja, jak umieszczenie na pasku narzędzi przycisków o różnej wielkości, jest bardzo trudna.
Paski dialogowe można stworzyć w bardzo prosty sposób (nawet jeśli chcesz na nich umieszczać elementy kontrolne nie będące przyciskami). Paski te nie potrafią jednak zmieniać swojej orientacji, a poza tym zużywają bardzo dużo zasobów systemowych.
Kolejnym czynnikiem przemawiającym na korzyść standardowych pasków narzędzi, jest możliwość modyfikowania ich zawartości przez użytkownika podczas wykonywania programu. Jest to możliwe dzięki specjalnym funkcjom udostępnianym przez system, operacyjny Windows. Modyfikacja pasków narzędzi obejmuje usuwanie przycisków z paska, zmienianie ich kolejności oraz dodawanie ich za pomocą specjalnego systemowego okna dialogowego. Zastosowanie MFC do obsługi pasków narzędzi sprawia jednak, że udostępnienie użytkownikowi możliwości modyfikowania pasków narzędzi jest zadaniem stosunkowo trudnym (patrz listing 5.7).
Jeśli chcesz stworzyć program, w którym zamiast paska narzędzi byłyby dostępny pasek dialogowy, musisz stworzyć program bez paska narzędzi (za pomocą kreatora App Wizard), a następnie dodać do niego pasek dialogowy za pomocą Galerii Komponentów.
Modyfikowanie standardowych okien dialogowych
Za pomocą MFC bardzo prosto można zmodyfikować standardowe okna dialogowe. Jeśli nie musisz podawać szablonu standardowego okna dialogowego, to możesz wyprowadzić nową klasę potomną z odpowiedniej klasy dostępnej w MFC (patrz tabela 5.4). W takim wypadku, do obsługi komunikatów (na przykład WM_INITDIALOG) będziesz mógł posłużyć się standardowym mechanizmem map komunikatów; wszystkie potrzebne operacje będziesz mógł wykonywać w procedurach obsługi komunikatów.
Jeśli będziesz musiał podać szablon okna dialogowego, a nie chcesz obsługiwać okna obsługi plików, charakterystycznego dla Eksploratora Windows (dostępnego w systemach Windows 95 oraz Windows NT 4.0), będziesz musiał wykonać następujące czynności:
1. Zaimportuj odpowiedni szablon okna dialogowego i zmodyfikuj go zgodnie ze swoimi potrzebami.
2. Za pomocą kreatora Class Wizard wyprowadź nową klasę potomną.
3. W konstruktorze stworzonej klasy zmodyfikuj odpowiednią strukturę systemową. Jeśli zastępujesz standardowy szablon okna dialogowego, to w składowej lp_TemplateName podaj identyfikator zasobu nowego szablonu okna, a do flagi dodaj wartość xx_ENABLETEMPLATE (gdzie xx jest odpowiednim prefiksem). Poza tym określ wartość składowej hInstance, przypisując jej wartość zwróconą przez metodę AfxGetInstanceHandle.
4. Użyj standardowego mechanizmu obsługi komunikatów do obsługi komunikatów generowanych w oknie dialogowym.
Jeśli zechcesz użyć okna obsługi plików, które przypomina okno dostępne w Eksploratorze Windows, będziesz musiał stworzyć szablon okna zawierający tylko i wyłącznie nowe elementy kontrolne, jakie chcesz dodać do standardowego okna dialogowego; oraz, ewentualnie, specjalny element o identyfikatorze stc32. Identyfikator ten informuje system, w którym miejscu mają być umieszczone domyślne elementy kontrolne. Stworzony przez Ciebie szablon okna dialogowego powinien mieć następujące style: WS_CHILD, WS_CLIPSIBLINGS, WS_VISIBLE, WS_3DLOOK oraz DS_CONTROL. Wewnątrz konstruktora klasy wywołaj metodę SetTemplate i określ jakiego szablonu okna dialogowego chcesz użyć.
Po otworzeniu okna dialogowego, będziesz mógł kontrolować jego stan i zarządzać nim, za pomocą komunikatów (patrz tabela 5.5) przesyłanych do jego okna rodzicielskiego. Przykład tworzenia i obsługi takich okien dialogowych znajdziesz na listingu 5.9.
Rozdział 6 Arkusze właściwości i kreatory
Choć wiele technik stosowanych przy arkuszach właściwości jest podobnych do technik wykorzystywanych przy tworzeniu okien dialogowych, arkusze właściwości posiadają kilka unikalnych aspektów. Można konstruować kreatory, niemodalne arkusze właściwości, a nawet własne kreatory AppWizard, przeznaczone do generowania nowych programów o zadanych właściwościach.
Nie mam wielu hobby. Prawdopodobnie dlatego, że nie mam zbyt wiele wolnego czasu. Lubię łowić ryby, czasami też prowadzę treningi drużyny mojego syna. Poza tym wszystkie moje zainteresowania można zaliczyć do grupy oryginalnych. Oczywiście, obecnie przede wszystkim zajmuję się komputerami. Oprócz tego, przez ponad dwadzieścia lat byłem zapalonym krótkofalowcem (jeśli Cię to interesuję, moim znakiem wywoławczym był WD5GNR).
W ciągu ostatnich dwudziestu lat wiele się w krótkofalarstwie zmieniło. Teraz, większość krótkofalówek posiada wbudowane mikroprocesory. Gdy obchodziłem swoją dwudziestą rocznicę jako krótkofalowiec, zdecydowałem się na kupno nowego sprzętu z wszystkimi nowoczesnymi “bajerami" radia Kenwood TS70D. Być może nie było to szczytowe osiągnięcie techniki, ale też radio nie należało do najgorszych. Jeśli oglądałeś film pt. Park Jurajski 2, przy odrobinie uwagi mogłeś dostrzec taki sprzęt w jednym z wozów, którymi poruszali się bohaterowie.
Obecnie wszystkie dostępne na rynku nadajniki posiadają dużo mniej przełączników i pokręteł niż ich starsze odpowiedniki. Można powiedzieć, że mają lepszy interfejs użytkownika. Stary radionadajnik miał tuziny gałek i przełączników, jednak w codziennej praktyce rzadko korzystało się z więcej niż kilku z nich. Z drugiej strony, każdy przełącznik pełnił pewną funkcję, więc sprzęt po prostu musiał je posiadać.
Obecnie wszystkie funkcje są kontrolowane przez mikroprocesory. W związku z tym przełączniki i pokrętła służą jedynie do przekazywania poleceń do mikroprocesora, który wykonuje całą ,,czarną robotę" i dzięki temu pojedyncze pokrętło może spełniać kilka różnych funkcji.
Niektóre bardzo małe radia przeznaczone dla samochodów, jachtów czy sportowych samolotów w ogóle nie mają pokręteł. Wszystko w nich jest obsługiwane przez menu przy pomocy kilku przycisków. Dobrym przykładem mogą też być przenośne radia, które dawniej posiadały tylko bardzo niewiele funkcji, gdyż mieściło się na nich bardzo niewiele przełączników. Teraz, dzięki systemowi menu, także takie radia mogą mieć ogromną ilość funkcji.
Jeśli się nad tym zastanowisz, dojdziesz do wniosku, że projektanci wykorzystali mikroprocesor do pogrupowania funkcji i pokazania tylko tych z nich, które są potrzebne w danym momencie. Podobną ewolucję przeszedł interfejs użytkownika w programach komputerowych. Przy pomocy obecnie istniejących narzędzi można bardzo łatwo stworzyć ogromne okno dialogowe z dziesiątkami możliwych kontrolek; z drugiej strony, takie okno bardzo zniechęca potencjalnych użytkowników. Nawet zawodowi programiści i zaawansowani użytkownicy nie lubią mieć do dyspozycji wszystkich możliwych opcji w jednym oknie.
Na początku programiści próbowali ukrywać część okna dialogowego. Jeśli potrzebowałeś dodatkowych kontrolek (lub informacji), klikałeś na przycisku o nazwie Szczegóły lub Zaawansowane, po czym okno się rozwijało ukazując kolejne, zwykle bardziej skomplikowane elementy. Był to już krok we właściwym kierunku, który jednak tylko ukrywał skomplikowane okno do momentu, w którym musiałeś z niego skorzystać.
Lepszym pomysłem jest wykorzystanie okna dialogowego zawierającego zakładki, z których każda odnosi się do grupy powiązanych ze sobą elementów. Krótkofalowiec korzysta jedynie z kilku elementów naraz; także użytkownik komputera zwykle nie używa jednocześnie więcej niż kilku opcji.
Niektórzy nieustraszeni programiści próbowali (w trudzie i znoju) samodzielnie tworzyć okna dialogowe z zakładkami i doprowadzili do tego, że idea zakładek stała się bardzo popularna. Po pewnym czasie Microsoft dodał obsługę zakładek do MFC, a także do podstawowego Windows API (konkretnie, do standardowych kontrolek). Okna dialogowe zawierające zakładki w terminologii Microsoftu noszą nazwę arkuszy właściwości (ang. property sheets).
Przykładowe arkusze właściwości znajdują się w samym pakiecie Visual C++. Aby się o tym przekonać, wybierz polecenie Tools Options lub Project | Settings. Jak inaczej wyobrażasz sobie przedstawienie takiej ilości informacji w pojedynczym oknie dialogowym? I piętnastu okien mogło by być za mało. Dzięki arkuszom właściwości ta ogromna liczba opcji może być sensownie obsłużona.
Przegląd arkuszy właściwości
Jeśli jeszcze nie pracowałeś z arkuszami właściwości, przekonasz się, że nie różnią się zbytnio od zwykłych okien dialogowych. Możesz po prostu stworzyć szablon dialogu dla każdej z zakładek. Tytuł szablonu okna pojawi się na odpowiedniej zakładce. Możesz korzystać przy tym z mechanizmów DDX, DDV oraz Class Wizarda. Szablon okna dialogowego musi mieć włączonych (oraz wyłączonych) kilka opcji, ale teraz nie musisz się tym przejmować. Po prostu kliknij prawym przyciskiem w panelu zasobów i z menu kontekstowego wybierz polecenie Insert (nie wybieraj polecenia Insert Dialog). Pojawi się okno dialogowe przedstawione na rysunku 6.1; kliknij na znaku plus obok pozycji Dialog rozwijając ją, i spójrz na trzy predefiniowane rodzaje arkuszy właściwości. Wybierz jeden z nich i gotowe.
Gdy tworzysz szablon okna dialogowego i wywołasz Class Wizarda, kreator zauważy, że stworzyłeś nowy dialog, i zaoferuje stworzenie dla niego nowej klasy. Jako klasę podstawową zasugeruje CDialog. Podobnie dzieje się w przypadku tworzenia arkuszy właściwości. W tym wypadku, jednak jako klasę podstawową, zamiast CDialog, wybierzesz CPropertyPage.
Gdy będziesz gotowy do stworzenia obiektu arkusza właściwości, będziesz potrzebował również obiektu klasy CPropertySheet (lub jej klasy pochodnej). Musisz przy tym stworzyć także obiekty każdej z klas wyprowadzonych z klasy CPropertyPage, których chcesz użyć. Właśnie te klasy tworzy dla Ciebie Class Wizard w momencie tworzenia szablonu dialogu.
W obiektach CPropertyPage może wykorzystać zwykłe wywołania DDX. Do włączania szablonów dialogów do arkusza właściwości służy metoda CPropertySheet: :AddPage, z kolei metoda CPropertySheet::DoModal umożliwia wyświetlenie przygotowanego arkusza.
A oto typowy przykład:
CPropertySheet sheet(“Przykładowy arkusz właściwości");
CPropPgl propl;
// wyprowadzone z CPropertyPage
CPropPg2 prop2,-
// wyprowadzone z CPropertyPage
sheet.AddPAge(&propl);
sheet. AddPAg&-tó>rop2) •
propl.m_valuel ^\100; // DDK
propl.m_value2 = *Test"; // DDK
prop2.m_position = 25; // także
if (sheet.DoModaK) == IDOK)
// odwrotne DDX
AfxMessageBox(propl.m_value2,"Zwrócone dane");
Kod wygląda podobnie do tego, czego mógłbyś się spodziewać przy okazji zwykłego okna dialogowego. Jedyna różnica sprowadza się do tego, że posiadasz dwa lub więcej dialogów (a właściwie obiektów CPropertyPage).
Klasa CPropertyPage posiada kilka metod, które możesz przesłonić (patrz tablica 6.1). Dzięki tym metodom możesz dowiedzieć się kiedy użytkownik kliknął na przycisku OK, Zastosuj, Anuluj. Możesz także zmienić niektóre z tych przycisków. I tak, metoda CancelToCIose służy do zmiany nazwy przycisku Anuluj na Zamknij. Jest to przydatne zwłaszcza wtedy, gdy zmiany dokonane w oknie dialogowym nie mogą być anulowane, i chcesz poinformou-ać użytkownika że kliknięcie na przycisku Anuluj spowoduje jedynie zamknięcie arkusza właściwości, bez anulowania zmian. Podobną metodą jest SetModified, która powoduje uaktywnienie przycisku Zastosuj.
Tabela 6.2. Metody klasy CPropertyPag
eFunkcja Opis
OnCancel Wywoływana przez MFC w momencie kliknięcia na przycisku Anuluj.
OnKillActive Wywoływana przez MFC w momencie gdy strona przestaje być stroną aktywną. W tym momencie powinieneś zatwierdzić dane.
OnOK Wywoływana przez MFC w momencie kliknięcia na przycisku OK, Zastosuj bądź Zamknij.
OnSetActive Wywoływana przez MFC w momencie gdy strona staje się stroną aktywną.
OnApply Wywoływana przez MFC w momencie kliknięcia na przycisku Zastosuj.
OnReset Wywoływana przez MFC w momencie kliknięcia na przycisku Anuluj.
OnQueryControl Wywoływana przez MFC w momencie kliknięcia na przycisku Anuluj, ale jeszcze przed samym przystąpieniem do anulowania.
Korzystanie z pojedynczego szablonu
Czasem zdarza się, że chcesz stworzyć arkusz właściwości, który korzysta z tego samego szablonu dialogu na kilku różnych zakładkach. Jako przykład niech posłuży rysunek 6.2. Taki arkusz właściwości mógłby pojawić się w grze w warcaby. Widzimy na nim ilość czerwonych i czarnych pionków. Obie zakładki są takie same, lecz jedna odnosi się do pionków czerwonych, a druga do czarnych.
Narzucające się od razu rozwiązanie mogłoby polegać na zaprojektowaniu dwóch identycznych szablonów i nazwaniu ich “Czerwone" i “Czarne." To by oczywiście zadziałało, ale nie jest to najelegantsze rozwiązanie. Za każdym razem gdy zmieniałbyś jeden szablon, musiałbyś pamiętać o zmianie także drugiego. Oprócz tego, wykorzystanie dwóch szablonów nie byłoby efektywne; wszystko mówi Ci, że powinien wystarczyć tylko jeden z nich.
Istnieje możliwość wykorzystania pojedynczego szablonu do stworzenia kilku zakładek; sztuczka polega na tym, że każdy z nich musi posiadać inny tytuł. Dobrym sposobem jest przekazanie właściwego tytułu jako drugiego argumentu konstruktora CPropertyPage. Istnieje jednak kilka pułapek. Po pierwsze, tytuł musi być przekazany jako numeryczny identyfikator łańcucha w tablicy łańcuchów; nie można po prostu przekazać odpowiedniego łańcucha.
Drugi problem polega na tym, że normalnie generowana przez Class Wizarda klasa posiada tylko domyślny konstruktor. Ten konstruktor wywołuje konstruktor klasy podstawowej z poprawnie wypełnionym tylko pierwszym argumentem (identyfikatorem szablonu dialogu). Gdy spróbujesz zmienić konstruktor tak, by jako argument móc przekazać tytuł zakładki, Twój program się nie skompiluje. Dlaczego? Ponieważ klasa CPropertyPage korzysta z makra DECLARE_DYNCREATE, które oczekuje domyślnego konstruktora. Bardzo mało prawdopodobne jest abyś naprawdę tego makra potrzebował, ale Class Wizard i tak automatycznie je umieszcza.
Istnieją trzy sposoby ominięcia tego problemu. Po pierwsze, możesz usunąć makro DECLARE_DYNCREATE (oraz powiązane z nim makro IMPLEMENT_DYNCREATE). Po drugie, możesz zostawić w spokoju domyślny konstruktor i dodać drugi, korzystający z dodatkowego argumentu. Trzecie rozwiązanie polega na zastosowaniu w konstruktorze domyślnego argumentu, tak by konstruktor akceptował zero argumentów lub jeden argument. W ten sposób zaspokoi oczekiwania makra dynamicznego tworzenia, a w razie potrzeby pozwoli na przekazanie identyfikatora tytułu.
Taki przykładowy arkusz właściwości znajdziesz na listingu 6.1. Widok, który z niego korzysta, znajduje się na listingu 6.2. Zwróć uwagę, że w odróżnieniu od poprzedniego przykładu, każda strona arkusza wymaga podania argumentu w konstruktorze (oraz odpowiedniego identyfikatora łańcucha w tablicy łańcuchów).
Czy zastosowanie dwóch szablonów dialogu nie spełniłoby zadania? Owszem, ale po co tworzyć dwa identyczne szablony. Choć zastosowanie pojedynczego szablonu wymaga więcej pracy, może znacznie ułatwić Ci (lub komuś innemu) późniejszą modyfikację lub rozbudowę programu.
Listing 6.1. Arkusz właściwości.
// chkpropView.cpp : implementation of the CChkpropView class
//
#include "stdafx.h"
#include "chkprop.h"
#include "chkpropDoc.h"
#include "chkpropView.h"
#include "statuspg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CChkpropView
IMPLEMENT_DYNCREATE(CChkpropView, CView)
BEGIN_MESSAGE_MAP(CChkpropView, CView)
//{{AFX_MSG_MAP(CChkpropView)
ON_WM_LBUTTONDOWN()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CChkpropView construction/destruction
CChkpropView::CChkpropView()
{
// TODO: add construction code here
}
CChkpropView::~CChkpropView()
{
}
BOOL CChkpropView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CChkpropView drawing
void CChkpropView::OnDraw(CDC* pDC)
{
CChkpropDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
}
/////////////////////////////////////////////////////////////////////////////
// CChkpropView diagnostics
#ifdef _DEBUG
void CChkpropView::AssertValid() const
{
CView::AssertValid();
}
void CChkpropView::Dump(CDumpContext& dc) const
{
CView::Dump(dc);
}
CChkpropDoc* CChkpropView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CChkpropDoc)));
return (CChkpropDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CChkpropView message handlers
void CChkpropView::OnLButtonDown(UINT nFlags, CPoint point)
{
CPropertySheet sheet("Ilość pionków");
StatusPage redpage(IDS_REDSTRING), blackpage(IDS_BLKSTRING);
redpage.m_kings=0;
redpage.m_pieces=9;
blackpage.m_kings=2;
blackpage.m_pieces=10;
sheet.AddPage(&redpage);
sheet.AddPage(&blackpage);
if (sheet.DoModal()==IDOK)
{
// ten arkusz właściwości tylko przekazuje informacje, więc nie musimy się tym przejmować
}
}
Tryb kreatora
Innym przykładem użycia arkuszy właściwości jest korzystanie z kreatorów. Kreatory przewijają się cały czas w różnych produktach Microsoftu; poza tym nie spotykamy ich zbyt często. Nie wiadomo dlaczego nie cieszą się popularnością, choć dzięki MFC można je bardzo łatwo tworzyć.
Tworzenie kreatora przypomina tworzenie arkusza właściwości, z dwoma niewielkimi różnicami. Po pierwsze, zanim wywołasz metodę DoModal, musisz wywołać metodę CPropertySheet::SetWizardMode. Po drugie, dla każdego z obiektów zakładek musisz przesłonić metodę OnSetActłve. Wewnątrz tej metody odwołaj się do nadrzędnego okna (czyli do samego arkusza właściwości) i wywołaj metodę CPropertySheet::SetWizardButtons w celu przygotowania przycisków, które mają się pojawić (patrz tabela 6.2). Zwróć uwagę na to, że przyciski Dalej i Zakończ to w rzeczywistości ten sam przycisk - nie da się ich włączyć obu jednocześnie.
Tabela 6.2. Przyciski kreatora
Przycisk Stała
Wstecz PSWIZB_BACK
Dalej PSWIZB_NEXT
Zakończ PSWIZB_FINISH
Wyłączony PSWIZB_DISABLED
Zakończ FINISH
Przykładowy dialog kreatora i wywołujący go program znajdziesz na listingach 6.3 oraz 6.4. Jeśli nie przesłonisz metody OnSetActive, pojawią się przyciski Wstecz i Dalej. Możesz pomyśleć, że tylko ostatni krok kreatora (ostatnia strona) wymaga zmiany nazwy przycisku z Dalej na Zakończ, jednak nie zawsze tak jest. Wyobraź sobie, że użytkownik doszedł do ostatniej strony, i przycisk Dalej zmienił się na przycisk Zakończ. Następnie użytkownik kliknął na przycisku Wstecz. Przycisk Dalej w dalszym ciągu, opisany jako Zakończ, dopóki nie zostanie jawnie zmieniony. Właśnie dlatego strony kreatora wymagają użycia metody OnSetActive. Poza tym, pierwsza strona nie potrzebuje przycisku Wstecz.
Listing 6.3. Jedna ze stron arkusza właściwości stworzonego w stylu kreatora
// Page1.cpp : implementation file
//
#include "stdafx.h"
#include "wiz.h"
#include "Page1.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// Page1 property page
IMPLEMENT_DYNCREATE(Page1, CPropertyPage)
Page1::Page1() : CPropertyPage(Page1::IDD)
{
//{{AFX_DATA_INIT(Page1)
m_name = _T("");
//}}AFX_DATA_INIT
}
Page1::~Page1()
{
}
void Page1::DoDataExchange(CDataExchange* pDX)
{
CPropertyPage::DoDataExchange(pDX);
//{{AFX_DATA_MAP(Page1)
DDX_Text(pDX, IDC_NAME, m_name);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(Page1, CPropertyPage)
//{{AFX_MSG_MAP(Page1)
// NOTE: the ClassWizard will add message map macros here
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// Page1 message handlers
BOOL Page1::OnSetActive()
{
CPropertySheet *sheet=(CPropertySheet *)GetParent();
sheet->SetWizardButtons(PSWIZB_NEXT);
return CPropertyPage::OnSetActive();
}
Listing 6.4. Wykorzystanie kreatora.
// wizView.cpp : implementation of the CWizView class
//
#include "stdafx.h"
#include "wiz.h"
#include "wizDoc.h"
#include "wizView.h"
#include "page1.h"
#include "page2.h"
#include "page3.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CWizView
IMPLEMENT_DYNCREATE(CWizView, CView)
BEGIN_MESSAGE_MAP(CWizView, CView)
//{{AFX_MSG_MAP(CWizView)
ON_WM_LBUTTONDOWN()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CWizView construction/destruction
CWizView::CWizView()
{
// TODO: add construction code here
}
CWizView::~CWizView()
{
}
BOOL CWizView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CWizView drawing
void CWizView::OnDraw(CDC* pDC)
{
CWizDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
}
/////////////////////////////////////////////////////////////////////////////
// CWizView diagnostics
#ifdef _DEBUG
void CWizView::AssertValid() const
{
CView::AssertValid();
}
void CWizView::Dump(CDumpContext& dc) const
{
CView::Dump(dc);
}
CWizDoc* CWizView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CWizDoc)));
return (CWizDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CWizView message handlers
void CWizView::OnLButtonDown(UINT nFlags, CPoint point)
{
CPropertySheet sheet;
Page1 pg1;
Page2 pg2;
Page3 pg3;
sheet.AddPage(&pg1);
sheet.AddPage(&pg2);
sheet.AddPage(&pg3);
sheet.SetWizardMode();
if (sheet.DoModal()==ID_WIZFINISH)
{
CString greet="Dziękuję ";
greet+=pg1.m_name;
MessageBox("Możesz przejść",greet);
}
else
{
MessageBox("Spadasz w przepaść");
}
}
Niemodalne arkusze właściwości
Tworzenie niemodalnych arkuszy właściwości także niewiele się różni od tworzenia niemodalnych okien dialogowych. Zamiast metody DoModal wywołuje się metodę Create. Oczywiście tak jak w przypadku okna dialogowego dane z arkusza nie zostaną automatycznie przesłane, musisz więc je samodzielnie przekazać przy pomocy metody UpdateData (patrz rozdział 5).
Jednym z problemów związanych z niemodalnymi arkuszami właściwości jest czas życia obiektów. Modalne okna dialogowe i arkusze właściwości zwykle korzystają ze zmiennych tworzonych na stosie, gdyż czas życia takich okien nigdy nie wykracza poza czas życia zmiennych. Z drugiej strony, niemodalne okna dialogowe i arkusze właściwości zwykle żyją dużo dłużej niż funkcja, w której zostały otwarte. W związku z tym trzeba tworzyć je (i wszystko co jest z nimi związane) na stercie lub jako część obiektu, który będzie istniał przez cały okres życia okna.
Jedną z poręcznych technik (przedstawioną na listingach 6.5 do 6.8) jest wyprowadzenie klasy z CPropertySheet. Nowa klasa może zawierać zmienne składowe dla każdej strony w arkuszu właściwości. W konstruktorze klasy pochodnej wywołujemy metodę AddPage dla każdej ze stron. Następnie tworzymy obiekt klasy arkusza właściwości, tak jak w przypadku “normalnych" obiektów klasy CPropertyPage.
Listing 6.5. Niemodalny arkusz właściwości
// MouseSheet.cpp : implementation file
//
#include "stdafx.h"
#include "mdlsprop.h"
#include "MouseSheet.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CMouseSheet
IMPLEMENT_DYNAMIC(CMouseSheet, CPropertySheet)
CMouseSheet::CMouseSheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{
}
CMouseSheet::CMouseSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
leftct=rightct=midct=0;
AddPage(&m_clickpage);
AddPage(&m_pospage);
}
CMouseSheet::~CMouseSheet()
{
}
BEGIN_MESSAGE_MAP(CMouseSheet, CPropertySheet)
//{{AFX_MSG_MAP(CMouseSheet)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMouseSheet message handlers
BOOL CMouseSheet::OnInitDialog()
{
return CPropertySheet::OnInitDialog();
}
void CMouseSheet::SetXY(int x, int y)
{
m_pospage.m_x=x;
m_pospage.m_y=y;
if (::IsWindow(m_pospage.m_hWnd)) m_pospage.UpdateData(FALSE);
}
void CMouseSheet::MouseDown(int cmd)
{
switch (cmd)
{
case WM_LBUTTONDOWN:
leftct++;
break;
case WM_RBUTTONDOWN:
rightct++;
break;
case WM_MBUTTONDOWN:
midct++;
break;
}
m_clickpage.m_left=leftct;
m_clickpage.m_right=rightct;
m_clickpage.m_middle=midct;
if (::IsWindow(m_clickpage.m_hWnd)) m_clickpage.UpdateData(FALSE);
}
Listing 6.6. Strona pozycji myszy.
// PosPage.cpp : implementation file
//
#include "stdafx.h"
#include "mdlsprop.h"
#include "PosPage.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CPosPage property page
IMPLEMENT_DYNCREATE(CPosPage, CPropertyPage)
CPosPage::CPosPage() : CPropertyPage(CPosPage::IDD)
{
//{{AFX_DATA_INIT(CPosPage)
m_x = 0;
m_y = 0;
//}}AFX_DATA_INIT
}
CPosPage::~CPosPage()
{
}
void CPosPage::DoDataExchange(CDataExchange* pDX)
{
CPropertyPage::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CPosPage)
DDX_Text(pDX, IDC_X, m_x);
DDX_Text(pDX, IDC_Y, m_y);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CPosPage, CPropertyPage)
//{{AFX_MSG_MAP(CPosPage)
// NOTE: the ClassWizard will add message map macros here
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CPosPage message handlers
Listing 6.7. Strona kliknięć.
// ClickPage.cpp : implementation file
//
#include "stdafx.h"
#include "mdlsprop.h"
#include "ClickPage.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CClickPage property page
IMPLEMENT_DYNCREATE(CClickPage, CPropertyPage)
CClickPage::CClickPage() : CPropertyPage(CClickPage::IDD)
{
//{{AFX_DATA_INIT(CClickPage)
m_left = 0;
m_middle = 0;
m_right = 0;
//}}AFX_DATA_INIT
}
CClickPage::~CClickPage()
{
}
void CClickPage::DoDataExchange(CDataExchange* pDX)
{
CPropertyPage::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CClickPage)
DDX_Text(pDX, IDC_LEFT, m_left);
DDX_Text(pDX, IDC_MIDDLE, m_middle);
DDX_Text(pDX, IDC_RIGHT, m_right);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CClickPage, CPropertyPage)
//{{AFX_MSG_MAP(CClickPage)
// NOTE: the ClassWizard will add message map macros here
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CClickPage message handlers
Listing 6.8. Wykorzystanie niemodalnego arkusza właściwości.
// mdlspropView.cpp : implementation of the CMdlspropView class
//
#include "stdafx.h"
#include "mdlsprop.h"
#include "mdlspropDoc.h"
#include "mdlspropView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CMdlspropView
IMPLEMENT_DYNCREATE(CMdlspropView, CView)
BEGIN_MESSAGE_MAP(CMdlspropView, CView)
//{{AFX_MSG_MAP(CMdlspropView)
ON_WM_LBUTTONDOWN()
ON_WM_MOUSEMOVE()
ON_WM_RBUTTONDOWN()
ON_WM_MBUTTONDOWN()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMdlspropView construction/destruction
CMdlspropView::CMdlspropView() : psheet("Właściwości myszy")
{
// TODO: add construction code here
}
CMdlspropView::~CMdlspropView()
{
}
BOOL CMdlspropView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CMdlspropView drawing
void CMdlspropView::OnDraw(CDC* pDC)
{
CMdlspropDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
}
/////////////////////////////////////////////////////////////////////////////
// CMdlspropView diagnostics
#ifdef _DEBUG
void CMdlspropView::AssertValid() const
{
CView::AssertValid();
}
void CMdlspropView::Dump(CDumpContext& dc) const
{
CView::Dump(dc);
}
CMdlspropDoc* CMdlspropView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CMdlspropDoc)));
return (CMdlspropDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CMdlspropView message handlers
void CMdlspropView::OnLButtonDown(UINT nFlags, CPoint point)
{
if (::IsWindow(psheet.m_hWnd)) psheet.MouseDown(WM_LBUTTONDOWN);
CView::OnLButtonDown(nFlags, point);
}
void CMdlspropView::OnMouseMove(UINT nFlags, CPoint point)
{
if (::IsWindow(psheet.m_hWnd)) psheet.SetXY(point.x,point.y);
CView::OnMouseMove(nFlags, point);
}
void CMdlspropView::OnRButtonDown(UINT nFlags, CPoint point)
{
if (::IsWindow(psheet.m_hWnd)) psheet.MouseDown(WM_RBUTTONDOWN);
CView::OnRButtonDown(nFlags, point);
}
void CMdlspropView::OnMButtonDown(UINT nFlags, CPoint point)
{
if (::IsWindow(psheet.m_hWnd)) psheet.MouseDown(WM_MBUTTONDOWN);
CView::OnMButtonDown(nFlags, point);
}
void CMdlspropView::OnInitialUpdate()
{
CView::OnInitialUpdate();
psheet.Create(this, DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP |
DS_SETFONT | WS_CHILD | WS_VISIBLE | WS_CAPTION);
}
I tak, arkusz właściwości myszy z rysunku 6.3 należy do zawierającego go widoku. Ten widok posiada zmienną typu CMouseSheet; z kolei ta klasa zawiera dwie strony arkusza właściwości (CClickPage oraz CPosPage). CMouseSheet zawiera także funkcje obsługujące przekazywanie danych pomiędzy stronami arkusza a światem zewnętrznym. Zwróć uwagę, że te funkcje muszą uważać, by nie wywoływać metody UpdateData jeśli dana strona akurat nie istnieje. Właśnie z tego powodu każde wywołanie UpdateData jest zabezpieczone wcześniejszym testem ::IsWindow. Jak widać na rysunku, niemodalne arkusze właściwości nie zawierają żadnych domyślnych przycisków. Wszystko, co chcesz umieścić na stronach, musisz dodać samodzielnie.
Własne kreatory App Wizard
Nie jestem dobrym handlowcem. To może nieco dziwić, zwłaszcza jeśli wziąć pod uwagę fakt, że mój ojciec był doskonałym handlowcem. Gdy byłem dzieckiem, ojciec zajmował się sprzedażą dokładnie wszystkiego - mam tylko nadzieję, że z wyjątkiem odkurzaczy i encyklopedii. Ojciec uwielbiał sprzedawać do tego stopnia, że gdy odszedł na emeryturę, całkowicie oddał się temu zajęciu.
Gdy ojciec wraz z matką zajęli się sprzedażą zaproszeń na wesela (czym moja matka zresztą zajmuje się do dzisiaj), rozpoczynali od niewielkiego interesu - bardzo niewielkiego. Polegało to na przeglądaniu rubryki towarzyskiej w lokalnej gazecie, dzwonieniu do przyszłej panny młodej i umawianiu się na wizytę z katalogiem w jej domu. W tym czasie byłem bardzo młody i to, czym zajmowali się dorośli, było dość mgliste, więc ten sposób zarabiania wcale mnie nie obchodził. Gdy trochę podrosłem, czasem czułem zakłopotanie w związku z tym, że moi rodzice chodzą do obcych ludzi prezentować swoje towary. Oczywiście każde dziecko przechodzi okres, w którym poczynania rodziców wydają się nieco dziwne (moje dzieci nie różnią się pod tym względem). Wtedy, prawie nastolatkowi, dzwonienie do nieznajomych i nawiedzanie ich w domach wydawało się czymś nad wyraz niestosownym.
Później, rodzice rozbudowali interes na tyle, że ludzie sami zaczęli przychodzić do nas do domu. W tym czasie ich domowa firma miała już całkiem niezłą reputację. Praktycznie wszystkie zaproszenia weselne w mieście wychodziły z pod ręki mojej matki. Później, gdy ojciec odszedł na emeryturę, matka w dalszym ciągu prowadziła interes.
Myśląc o tym wszystkim, dochodzę do wniosku, że niewłaściwie oceniałem domową sprzedaż ojca jako swego rodzaju głupotę. Teraz widzę, że robił to, co powinien robić każdy dobry handlowiec: ułatwiał zakup produktu.
Z pewnością zastanawiasz się jak ta historyjka ma się do programowania. Zastanów się: jeśli sam piszesz swoje programy, musisz je sam sprzedawać. Być może nie w tradycyjnym sensie tego słowa, ale w każdym razie rozprowadzasz - sprzedajesz - swoje programy i swoje pomysły.
Czy chcesz, by klient używał Twojego interfejsu użytkownika? Czy inni programiści zaadoptują twoją bibliotekę kodu, Twój styl pisania, czy nawet Twoje techniki? Czy chcesz, aby Twój szef miał Cię na uwadze przy planowaniu szczególnie interesującego projektu? Wszystko to - w pewien sposób - nawiązuje do handlu. Jedną z trudności “sprzedaży" innym programistom swoich rozwiązań jest to, że często trudno je im “kupić." Jako programista, jeśli musiałbym odcyfrowywać Twój kod, wygrzebywać go z kontekstu i modyfikować struktury danych, prawdopodobnie wolałbym napisać go samodzielnie.
Visual C++ oferuje kilka sposobów, dzięki którym możesz łatwiej sprzedać swoje programistyczne pomysły. Jeśli kiedykolwiek próbowałeś napisać rozszerzenie powłoki
Windows 95 zajmujące się obsługą dynamicznych ikon, prawdopodobnie wiesz już, że to nie łatwe zadanie. A gdybyś tak miał kreatora AppWizard, który automatycznie wygenerowałby szkielet takiego programu, wymagając przy tym bardzo niewiele lub żadnej wiedzy z Twojej strony (na przykład kreatora z rysunku 6.4)?
To jeszcze nie wszystko! Nawet jeśli nie wiesz, jak działa kreator, możesz użyć go do stworzenia rozszerzenia powłoki. Gdy raz wykonasz żmudną pracę stworzenia szkieletu programu, możesz łatwo stworzyć z niego kreatora AppWizard. Możesz go później sam używać lub udostępnić go innym - nawet jeśli nie mają pojęcia, jak Twój program działa. Nie mam zamiaru objaśniać kodu generowanego przez kreatora. Zamiast tego skupimy się na samej pracy naszego AppWizarda. Jeśli chcesz dowiedzieć się czegoś więcej o obsłudze ikon, zajrzyj do dodatku A.
Tworzenie kreatora
Stworzenie kreatora było proste. Rozpocząłem od działającego projektu programu obsługi ikon. Następnie stworzyłem nowy projekt. Jako typ projektu wybrałem Custom AppWizard. Potem musiałem poinformować Visuala, że chcę oprzeć mojego nowego kreatora AppWizard na istniejącym projekcie, i wskazać, jakiego projektu chcę użyć (patrz rysunki 6.5 i 6.6).
W rezultacie otrzymałem serię plików źródłowych i zasobów. Po zbudowaniu projektu, zostanie stworzony plik AWX (automatycznie umieszczany w kartotece \Program Files\ DevStudio\SharedIDE\Teplate). Od tego momentu, przy tworzeniu nowego projektu, na liście projektów pojawi się nowy kreator AppWizard (patrz rysunek 6.7).
Dostosowywanie projektu kreatora
To działa zadziwiająco dobrze. Własny AppWizard jest na tyle bystry, że potrafi zmienić Twoje identyfikatory tak, by odpowiadały nazwie tworzonego projektu. Niestety, nie zawsze wykonuje wszystkie zmiany, których możesz sobie życzyć. Oprócz tego, nie zawsze daje sobie radę z tworzeniem pliku nowego projektu.
Na przykład, gdy pierwszy raz tworzyłem kreatora obsługi ikon, stworzył plik DEF, ale nie dodał go do projektu. Ponadto nie zmienił identyfikatora GUID projektu. Wynika z tego, że musisz samodzielnie zmodyfikować wygenerowany kod w celu rozwiązania tych problemów.
Gdy spojrzysz na kod własnego kreatora AppWizard, nie powinieneś mieć problemów z odcyfrowaniem go. Przede wszystkim występuje w nim obiekt wyprowadzony z klasy CCustomAppWiz, reprezentujący Twojego AppWizarda. Może być interesujący tylko wtedy, gdy chcesz dostosować konkretne kroki lub dokonać własnej inicjalizacji (na przykład, wygenerować identyfikator GUID).
Pliki szablonów tworzą główną część projektu. Są to szkieletowe pliki sterujące generowaniem nowych plików. Jeśli otworzysz pliki szablonów, przekonasz się, że wyglądają podobnie do Twojego oryginalnego kodu źródłowego, z tym, że Visual C++ zastąpił niektóre części makrami. Wszędzie tam, gdzie występowała nazwa projektu, teraz znajduje się napis $$ROOT$$. To makro jest rozwijane w nazwę nowego projektu. Listę standardowych makr znajdziesz w tabeli 6.3. To nie jest zbyt skomplikowane - po rozwinięciu makr, kreator po prostu kopiuje pliki szablonów do nowego projektu. Makro $$IF działa podobnie jak polecenie #if preprocesora. Możesz je wykorzystać do wybrania lub odrzucenia części pliku szablonu. Jeśli chcesz zmodyfikować pliki szablonów, otwórz je z poziomu Dependencies panelu plików - możesz także otworzyć je jako zasoby, ale wtedy będziesz musiał modyfikować je przy pomocy edytora binarnego.
Tworzenie projektu
Dwa pliki, których możesz nie znać, to CONFIRM.INF i NEWPROJ.INF. Plik CONFIRM. INF generuje końcowe okienko tekstowe, wyświetlane tuż przed tym, zanim AppWizard zabiera się do generowania kodu. NEWPROJ.INF jest nieco bardziej skomplikowany. Steruje sposobem, w jaki AppWizard generuje pliki i konstruuje aktualny projekt. W odróżnieniu od innych plików, AppWizard nie tworzy pliku MĄKĘ z szablonu. Zamiast tego, plik MĄKĘ konstruowany jest na podstawie pliku NEWPROJ.INF. Spójrzmy na plik NEWPROJ.INF:
Tabela 6.3. Standardowe makra AppWizarda
Makro Znaczenie
$$IF Polecenie warunkowego włączenia bloku kodu
$$ELSE Polecenie warunkowego włączenia bloku kodu
$$ENDIF Polecenie warunkowego włączenia bloku kodu
$$ELIF Polecenie warunkowego włączenia bloku kodu
$$INCLUDE Włączenie innego pliku
$$BEGINLOOP Początek pętli
$$ENDLOOP Koniec pętli
$$SET_DEFAULT_LANG Ustawienie domyślnego języka
$$// Komentarz
$$$$ Wyemitowanie ciągu "$$"
$$ROOT Nazwa projektu bez rozszerzenia (wielkimi literami)
$$Root Podobnie jak $$ROOT, ale z zachowaniem wielkości liter
$$FULL_DIR_PATH Ścieżka do kartoteki zawierającej projek
t
newproj.inf = template for list of template fileś
format is 'sourceResName' \t 'destFileName'
The source res name may be preceded by any combination of
'=', '+', and/or '*'
'=' => the resource is binary
'+' => the file should be added to the project '*' => bypass the custom AppWizard's resources when loading if name starts with / => create new subdir
ROOT.CLW $$root$$.clw +ROOT.CPP $$root$$.cpp ROOT.H $$root$$.h +ROOT.DEF $$root$$.def STDAFK.H StdAfx.h +STDA&X.CPP StdAfK.cpp RESOURCE. H resource. h +ROOT.RCV $$root$$.rc ROOT.REG $$root$$.reg ICONHANDLER.H IconHandler.h +ICONHANDLER.CPP IconHandler.cpp = ICON1. ICO iconl. ico ROOT.RC2 res\$$root$$.rc2
Pierwsza kolumna zawiera nazwę szablonu, zaś druga zawiera nazwę, jaką należy nadać plikowi (używając rozwinięcia makra). Gdy pierwszy raz wygenerowałem ten plik, plik DE nie znalazł się w projekcie (to właśnie przeszkadza w poprawnym działaniu DLL-a). Rozwiązanie: dodaj znak + przed nazwą szablonu ROOT.DEF.
Równie łatwo rozwiązuje się problem z identyfikatorem GUID. Obiekt AppWizarda w zmiennej m_Dictionary posiada słownik makr. Aby stworzyć własne makro, po prostu dodaj je do słownika:
//W konstruktorze obiektu AppWizarda
m_Dictionary[_T("Ulubiony_Magazyn")] = _T("Visual Developer");
Teraz makro $$Ulubiony_Magazyn$$ da wynik, jakiego oczekujesz. Makro _T rzutuje łańcuch do typu LPTSTRT, wymaganego przez to i wiele innych wywołań OLE.
Uzbrojony w powyższe informacje możesz łatwo stworzyć makro dla identyfikatora GU1D. Musisz wywołać CoCreateGuid w celu wygenerowania liczby. Potem powinieneś przekonwertować ją na łańcuch przy pomocy StringFromCLSID (CLSID, czyli Class ID to jedna z postaci identyfikatora GUID). Łańcuch będzie w formacie UNICODE, więc jeśli operujesz zwykłymi łańcuchami, powinieneś także przekonwertować go na ANS1. Kod wymaga, by GUID był podany w postaci długiego łańcucha oraz jako seria liczb szesnastkowych oddzielonych przecinkami. W przykładowym kodzie znajdziesz makra dla obu przypadków.
Dostosowywanie szablonów jest proste do momentu, w którym zechcesz poprawić plik REG. Plik REG dostarcza pozycji Rejestru wymaganych przez powłokę dla rozpoznania programu obsługi. Idealnie byłoby, gdybyś mógł określić rozszerzenie używanego pliku w jednym z kroków kreatora. Niestety, to wymaga napisania nieco kodu, a ponieważ dotąd nie zniżyliśmy się do napisania ani linijki prawdziwego kodu kreatora, wstydem byłoby zrobienie tego teraz. Oprócz tego, Rejestr wymaga podania pełnej ścieżki dostępu do DLL-a. To nie powinno być problemem, jeśli tylko weźmiemy pod uwagę, fakt, że do oddzielenia ścieżki w Rejestrze wymagane są podwójne znaki backslash. Oczywiście możesz odczytać zmienną $$PATH$$, programowo podwoić znaki backslash i dostarczyć nowe makro dla takiej wartości. Pozostałoby jeszcze tylko podjęcie decyzji, w jaki sposób obsługiwać odmienne położenia plików programu dla debbugera (Debug) i programu w końcowej postaci (Release).
Zdecydowałem, że szkoda na to wysiłku. Zamiast tego, w podejrzane miejsca w miejsce pliku REG wstawiłem ciąg XXX i oznaczyłem je komentarzem TODO. Oprócz tego zamieściłem nazwy DLL-a bez informacji o ścieżce. Musisz umieścić DLL-a w miejscu, w którym Windows znajdzie go samo, lub ręcznie zmodyfikować plik REG.
Gdy już stworzysz kreatora programu obsługi ikon, możesz bez końca tworzyć to rozszerzenie powłoki - nawet jeśli zupełnie nie masz pojęcia jak to działa. Podobnie możesz stworzyć swojego własnego kreatora i udostępnić go innym programistom, którzy mogą nie być w stanie samodzielnie stworzyć odpowiedniego kodu.
Inne opcje
Oparcie nowego kreatora na istniejącym projekcie jest tylko jednym ze sposobów tworzenia takich kreatorów. Możesz także zacząć od zwykłych kroków kreatora (i ewentualnie je zmodyfikować) lub możesz sam stworzyć wszystkie kroki. Jeśli chcesz, możesz dodać do swojego kreatora kontekstową pomoc.
Jako twórcę kreatorów mogą zainteresować Cię trzy nowe klasy: CCustomAppWiz, CAppWizStepDlg oraz OutputStream. W Twoim kodzie znajduje się klasa wyprowadzona z klasy CCustomAppWiz (odpowiednik obiektu aplikacji w normalnym programie). Obiekt ten zarządza, między innymi, wspomnianym wcześniej słownikiem makr. Możesz przesłaniać jego funkcje składowe w celu dostosowania działań kreatora.
Zwykle nie będziesz zajmował się klasą OutputStream (o dziwo, nazwa tej klasy MFC nie zaczyna się od litery C). Klasa zawiera dwie proste funkcje: WriteLine oraz Write-Block. WriteLine przenosi linie tekstu z szablonu do strumienia wyjściowego, podczas gdy WriteBlock przenosi do niego zasoby binarne (na przykład bitmapy).
Jeśli chcesz stworzyć własne kroki kreatora, będziesz musiał stworzyć szablon dialogu dla każdego kroku i powiązać go z klasą wyprowadzoną z CAppWizStepDlg (która sama w sobie pochodzi z klasy CDialog). W każdym kroku, w wyprowadzonej klasie zostaje przesłonięta metoda CAppWizStepDlg::OnDismiss. Ta metoda przejmuje kontrolę w momencie, gdy użytkownik kliknie na przycisku Next, Back lub Finish. W rym momencie masz okazję do zaktualizowania słownika.
DLL kreatora AppWizard użytkownika także eksportuje kilka funkcji, których możesz użyć. Funkcja GetDialog udostępnia jeden ze standardowych kroków, który możesz wykorzystać. Główna funkcja Twojego DLL-a wywołuje SetCustomAppWizClass (AppWizard przygotowuje ją dla Ciebie automatycznie). Możesz także określić ilość kroków (SetNumberOfSteps) lub wybrać obsługiwane języki (ScanForAvailableLanguages / SetSupportedLanguages).
Dalsze modyfikacje
Nie byłbym sobą, gdybym zostawił program taki, jaki jest, więc zdecydowałem się dodać do kreatora jeden krok (patrz rysunek 6.4). Zmiany w kodzie były minimalne. Oczywiście, wywołanie SetNumberOfSteps wymagało jako argumentu l zamiast 0. Oprócz tego musiałem zaprojektować okno dialogowe. Tworzone okno dialogowe powinno mieć nadany styl Child oraz Control. Jeśli nie zastosujesz stylu Control, nie będziesz mógł przemieszczać się pomiędzy elementami kontrolnymi przy pomocy klawisza Tab. Nie dodawaj belki tytułowej ani żadnego ze standardowych przycisków (Finish, Next, Back itd.).
ClassWizard nie ma pojęcia o klasie CAppWizStepDlg, więc zamiast niej, jako klasy podstawowej, musiałem użyć klasy CDialog. Następnie ręcznie zmieniłem wszystkie wystąpienia klasy CDialog w plikach nagłówkowych i CPP. Oprócz tego musiałem zmodyfikować konstruktor stworzony przez ClassWizarda, który konstruktorowi klasy CDialog przekazywał dwa argumenty, identyfikator zasobu i uchwyt okna nadrzędnego; konstruktor CAppWizStepDlg wymaga tylko pierwszego z nich.
Przy pomocy ClassWizarda stworzyłem połączenie DDX pomiędzy trzema polami edycji a zmiennymi obiektu CSteplDlg (klasy, którą stworzyłem w ClassWizardzie). Następnie przesłoniłem metodę OnDismiss. Ta funkcja wywołuje metodę UpdateData(TRUE) w celu przekazania danych do zmiennych, po czym aktualizuje słownik. Oprócz tego zmodyfikowałem pliki REG i CONFIRM.INF, tak by odzwierciedlały nowe pozycje słownika. Właśnie tę wersję kodu znajdziesz na listingach 6.9 i 6.10.
Listing 6.9. ShlconWzAw.CPP.
// shiconwzaw.cpp : implementation file
//
#include "stdafx.h"
#include "shiconwz.h"
#include "shiconwzaw.h"
#include <objbase.h>
#include <afxpriv.h>
#ifdef _PSEUDO_DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
// This is called immediately after the custom AppWizard is
// loaded. Initialize the state of the custom AppWizard here.
void CShiconwzAppWiz::InitCustomAppWiz()
{
// There are no steps in this custom AppWizard.
SetNumberOfSteps(1);
// Inform AppWizard that we're making a DLL.
m_Dictionary[_T("PROJTYPE_DLL")] = _T("1");
// TODO: Add any other custom AppWizard-wide initialization here.
char clsid[66],part[66];
GUID g;
LPOLESTR str;
CoCreateGuid(&g);
StringFromCLSID(g,&str);
USES_CONVERSION;
strcpy(clsid,W2A(str));
// Set up CLSID in various ways in the dictionary
m_Dictionary[_T("EXT_CLSID")]=_T(clsid);
strcpy(part,"0x");
clsid[9]='\0';
strcpy(part+2,clsid+1);
m_Dictionary[_T("CLSID_P1")]=_T(part);
clsid[14]='\0';
strcpy(part+2,clsid+10);
m_Dictionary[_T("CLSID_P2")]=_T(part);
clsid[19]='\0';
strcpy(part+2,clsid+15);
m_Dictionary[_T("CLSID_P3")]=_T(part);
part[2]=clsid[20];
part[3]=clsid[21];
strcpy(part+4,",0x");
part[7]=clsid[22];
part[8]=clsid[23];
for (int i=0;i<6;i++)
{
strcpy(part+9+5*i,",0x");
part[9+5*i+3]=clsid[25+i*2];
part[9+5*i+4]=clsid[26+i*2];
}
part[39]='\0';
m_Dictionary[_T("CLSID_P4")]=_T(part);
}
// This is called just before the custom AppWizard is unloaded.
void CShiconwzAppWiz::ExitCustomAppWiz()
{
// TODO: Add code here to deallocate resources
// used by the custom AppWizard
}
// This is called when the user clicks "Create..." on the
// New Project dialog or next
CAppWizStepDlg* CShiconwzAppWiz::Next(CAppWizStepDlg* pDlg)
{
// Set template macros based on the project name entered by the user.
// Get value of $$root$$ (already set by AppWizard)
if (pDlg==NULL) // first time
{
CString strRoot;
m_Dictionary.Lookup(_T("root"), strRoot);
// Set value of $$Doc$$, $$DOC$$
CString strDoc = strRoot.Left(6);
m_Dictionary[_T("Doc")] = strDoc;
strDoc.MakeUpper();
m_Dictionary[_T("DOC")] = strDoc;
// Set value of $$MAC_TYPE$$
strRoot = strRoot.Left(4);
int nLen = strRoot.GetLength();
if (strRoot.GetLength() < 4)
{
CString strPad(_T(' '), 4 - nLen);
strRoot += strPad;
}
strRoot.MakeUpper();
m_Dictionary[_T("MAC_TYPE")] = strRoot;
return &step_1; // bring up step 1
}
// only 1 step so we are done if we get here (and we
// should never get here). The step_1 dialog updates
// the dictionary
return NULL;
}
// Here we define one instance of the CShiconwzAppWiz class.
// You can access m_Dictionary and any other public members
// of this class through the global Shiconwzaw.
CShiconwzAppWiz Shiconwzaw;
W pierwszej wersji kreatora kod korzystał z domyślnej procedury Next. W tym momencie Next wymaga modyfikacji w celu wywołania dodatkowego kroku kreatora. Główna klasa (CShiconAppWiz) obecnie zawiera dodatkową zmienną składową (step_l), będącą obiektem klasy CSteplDlg. Jeśli procedura Next otrzyma wskaźnik NULL, oznacza to, że powinieneś zwrócić wskaźnik do pierwszego kroku. Kod zwraca adres zmiennej step_l. Jeśli chciałbyś użyć kilku stron, musiałbyś sprawdzać wskaźnik, aby zdecydować, który obiekt dialogu należy zwrócić. Jeśli chciałbyś jeden ze standardowych kroków, możesz to osiągnąć wywołując metodę GetDialog, aby dowiedzieć się, którego wskaźnika użyć.
Jeśli w oryginalnej wersji kreatora określiłbym, że chcę użyć kroków kreatora, AppWizard dostarczyłby klasę CDialogChooser do zarządzania wieloma dialogami. W przypadku jednego roku to zbyt duża sprawa; zdecydowałem się więc obsłużyć dialog samodzielnie. Oprócz tego, jeśli oprzesz swojego kreatora na istniejącym projekcie, AppWizard nie doda od siebie żadnej obsługi pliku pomocy. Nie jest to jednak duży problem. Po prostu nadaj plikowi pomocy taką samą nazwę, jaką ma Twój kreator.' Gdy użytkownik klika na przycisku Pomoc w jednym z Twoich kroków kreatora, Visual C++ uruchamia program WinHelp z nazwą Twojego pliku pomocy i identyfikatorem kontekstu równym 131072 plus identyfikator okna dialogowego. Jeśli tylko w pliku pomocy znajduje się temat o odpowiednim identyfikatorze kontekstu, wszystko działa poprawnie.
Debuggowanie kreatorów
Jeśli zechcesz zrobić cokolwiek skomplikowanego z kreatorem, prawdopodobnie będziesz musiał go debuggować. Visual C++ aranżuje projekt w taki sposób, że powstaje wersja release i wersję do debuggowania, ale to jeszcze nie wszystko. Zauważysz, że IDE oznacza wersję inną niż release jako pseudo-debug. W każdym przypadku Visual C++, używa bibliotek dla wersji release. Jednak w trybie pseudo-debug istnieje minimalne wsparcie debuggowania. Jeśli uruchomisz kreatora, uruchamia się kolejna kopia Visual C++, i do wykonania kodu musisz użyć polecenia New Project Workspace.
Kolejne pomysły na kreatory
Spróbuj stworzyć własne kreatory na podstawie projektu dowolnego typu. To doskonały sposób na wspomożenie innych programistów i promocję własnych dzieł. Po prostu napisz minimalny program, przypraw go komentarzami TODO i stwórz z niego kreatora. Jeśli umieścisz w kreatorze zbyt dużo specyficznego kodu, przeszkodzisz innym w jego używaniu; staraj się więc uogólniać.
Jeśli masz żyłkę handlowca, powinieneś być w stanie sprzedać swoje kreatory. Pełny zestaw kreatorów rozszerzeń powłoki prawdopodobnie cieszyłby się powodzeniem (jakieś oferty?). A co z kreatorem do tworzenia gier przygodowych, programów baz danych, serwerów WWW lub serwerów Winsock? Tu leżą pieniądze. Szkoda, że nie jestem dobrym handlowcem.
Podsumowanie
MFC bardzo ułatwia tworzenie arkuszy właściwości i kreatorów. Jedyna trudność polega na szukaniu wymówek, aby ich nie tworzyć. Modalne arkusze właściwości są proste. Niemodalne arkusze właściwości są tylko odrobinę bardziej skomplikowane.
MFC potwierdza to, o czym przekonują reklamy (przynajmniej w tym przypadku). W oparciu o istniejące projekty można bardzo prosto tworzyć własne kreatory. Jest to jedna z najbardziej ekscytujących i prawdopodobnie najmniej wykorzystana możliwość pakietu Visual C++.
Praktyczny przewodnik Arkusze właściwości i kreatory
Tworzenie arkusza właściwości
Tworzenie kreatora
Korzystanie z pojedynczego szablonu
Niemodalne arkusze właściwości
Tworzenie własnych kreatorów AppWizard
Praca z arkuszami właściwości nie różni się zbytnio od obsługi zwykłych okien dialogowych. Należy tylko przyzwyczaić się do operowania grupą dialogów, gdyż każda zakładka (strona) odpowiada pojedynczej, podobnej do dialogu klasie.
Tworzenie arkusza właściwości
Stworzenie arkusza właściwości jest kwestią wykonania kilku prostych kroków:
1. Kliknij prawym przyciskiem w panelu zasobów i w menu kontekstowym wybierz polecenie Insert.
2. Gdy pojawi się okno dialogowe, kliknij na znaku plus rozwijając gałąź Dialog.
3. Wybierz jeden ż rodzajów arkuszy właściwości (small - mały, medium - średni lub large - duży
4. Zapełnij okno dialogu elementami kontrolnymi, tak jak w przypadku zwykłego okna dialogowego.
5. Wywołaj ClassWizarda i użyj go do stworzenia nowej klasy (wyprowadzonej z CPropertyPage) odpowiadającej stworzonej stronie.
6. Przy pomocy zakładki Member Variables, w oknie ClassWizarda stwórz zmienne sterujące stworzonymi przez Ciebie elementami kontrolnymi.
7. Powtórz poprzednie kroki tworząc pozostałe zakładki.
8. Aby wyświetlić arkusz, stwórz obiekty dla klas każdej ze stron, które stworzyłeś.
9. Stwórz obiekt klasy CPropertySheet.
10. Zmodyfikuj wszystkie zmienne składowe stron, które chcesz zainicjować.
11. Dla każdej ze stron wywołaj metodę CPropertySheet::AddPage.
12. Wywołaj metodę CPropertySheet::DoModal i obsłuż ją tak samo jak w przypadku normalnego okna dialogowego.
A oto typowy przykład:
CPropertySheet
sheet(“Przykładowy arkusz właściwości");
CPropPgl
propl; // wyprowadzone z CPropertyPage
CPropPg2
prop2; // wyprowadzone z
CPropertyPage
sheet.AddPAge(Łpropl);
sheet.AddPAge(&prop2);
propl.m_valuel
= 100; // DDK
propl.m_value2
= "Test"; // DDK
prop2.m_position = 25; // także DDK
if(sheet.DoModal() == IDOK)
// odwrotne DDK
AfxMessageBox(propl.m_value2,"Zwrócone dane");
Tworzenie kreatora
Aby stworzyć kreatora, wykonaj te same kroki co w przypadku arkusza właściwości. Pamiętaj tylko, aby przed wywołaniem metody DoModal wywołać metodę CPropertySheet::SetWizardMode.
Oprócz tego, jeśli chcesz poprawnie obsłużyć przyciski, musisz przesłonić metodę OnSet-Active każdego obiektu zakładki arkusza. Wewnątrz nowej metody musisz odwołać się do nadrzędnego okna (czyli do właściwego arkusza) i wywołać metodę CPropertySheet::SetWizardButtons w celu ustalenia przycisków, które mają się pojawić (patrz tabela 6.2).
Korzystanie z pojedynczego szablonu
Czasem możesz użyć tego samego szablonu strony dla kilku zakładek okna. Dozwolone jest użycie kilku egzemplarzy obiektu strony w arkuszu właściwości, ale ponieważ tytuł dialogu staje się nazwą zakładki, w rezultacie otrzymasz arkusz właściwości z zakładkami o tych samych nazwach.
Odpowiedź jest prosta. W konstruktorze klasy CPropertyPage musisz dostarczyć identyfikator łańcucha w zasobach. Ten identyfikator określa ciąg, który zawiera nazwę zakładki. Jeśli zdecydujesz się na przekazanie identyfikatora do konstruktora wyprowadzonej przez siebie klasy, okaże się, że MFC żąda użycia domyślnego konstruktora. Aby to ominąć, możesz albo przeciążyć konstruktor, albo argumentowi zawierającemu identyfikator przypisać domyślną wartość, tak by mógł służyć także jako domyślny konstruktor. Szczegóły znajdziesz w listingach 6.1 i 6.2.
Niemodalne arkusze właściwości
Poprzez wywołanie metody Create zamiast DoModal można tworzyć niemodalne arkusze właściwości. Oczywiście będziesz musiał samodzielnie użyć metody UpdateData i pamiętać o zasięgu zmiennych (podobnie jak w niemodalnych oknach dialogowych).
Jednym z możliwych sposobów radzenia sobie z problemem zasięgu jest wyprowadzenie nowej klasy z klasy CPropertySheet. Nowa klasa powinna zawierać zmienne składowe, odpowiadające obiektom każdej z klas zakładek zawartych w arkuszu właściwości. Możesz następnie stworzyć obiekt nowej klasy na stercie (przy pomocy new), a program sam zajmie się resztą (przejrzyj listingi od 6.5 do 6.8).
Tworzenie własnych kreatorów AppWizard
Do Visual C++ możesz dodawać własne kreatory AppWizard. Dzięki temu możesz łatwo powielać projekty lub udostępniać innym programistom szkieletowe aplikacje zgodne z Twoimi specyfikacjami.
Najprostszym sposobem osiągnięcia tego celu jest rozpoczęcie od gotowej aplikacji, którą chcesz powielić. Następnie rozpocznij nowy projekt, jako jego typ wybierając Gustom AppWizard. W tym momencie będziesz mógł wskazać istniejący projekt, po czym kreator przygotuje dla Ciebie nowy kreator. To naprawdę jest takie proste.
Możesz także zacząć od standardowych kroków kreatora, a nawet stworzyć go od początku, jednak wymaga to dużo więcej wysiłku (szczegóły znajdziesz w tekście rozdziału oraz na listingach 6.9 i 6.10).
Rozdział 7 Biblioteki DLL i MFC
Biblioteki DLL stanowią fundamentalny element systemu operacyjnego Windows. Korzystając z MFC możesz używać kodu zawartego w bibliotekach DLL, a także tworzyć własne DLL-e. Możesz także rozszerzyć MFC o własne biblioteki, a także stworzyć zindywidualizowane narzędzia programistyczne.
Dorastałem w Bay St. Louis w stanie Mississippi; w tym czasie było to bardzo małe miasteczko. Teraz znajduje się tam kilka dużych kasyn, ale gdy tam mieszkałem, miejscowość była naprawdę niewielka. Pamiętam jak dużym wydarzeniem było powstanie Burger Kinga przy autostradzie. Do tego momentu najbliższy fastfood był odległy o 30 mil. Bay St. Louis urzekało (i urzeka) swoimi plażami i zabytkami, ale z pewnością nie była to metropolia.
Miasteczko tego typu doskonale nadaje się na spędzenie słodkiego dzieciństwa na prowincji, ale dla chłopaka, który, tak jak ja, interesował się krótkofalarstwem i dużymi systemami komputerowymi (tylko takie wtedy istniały), Bay St. Louis przypominało więzienie.
Nie chodzi o to, że w pobliżu nie było żadnego przemysłu. Niedaleko zatoki NASA testowała swoje silniki rakietowe (tam, gdzie obecnie znajduje się Stennis Space Center). Tak więc w okolicy żyło parę osób, które znały się na nauce i technice. Po prostu nie było żadnego punktu oparcia dla osób takich jak my. Nie było wielu księgarń, a zwłaszcza brakowało Księgarni technicznej. Przez długi czas nie było nawet żadnego sklepu z częściami elektronicznymi. Jeśli coś było potrzebne, trzeba było jechać 30 mil do Biloxi (obok bazy sił powietrznych) lub wędrować 60 mil do Nowego Orleanu (gdzie już można było dostać wszystko - i to naprawdę wszystko - co się chciało).
Jedną z rzeczy, jakie znajdowały się miasteczku, była przyjemna biblioteka. Jej budynek był zawsze odnowiony, a w środku znajdowało się mnóstwo książek. Lecz nie były to książki, które mnie interesowały. Wszystkie książki dotyczące elektroniki miały po dwadzieścia lub więcej lat, a o komputerach nie było nawet wzmianki. Uczyłem się więc z tych książek, które były, w związku z czym moja wiedza o elektronice była nieco przestarzała. Dorastałem, czytając o tubach głosowych, podczas gdy świat zaczynał powszechnie używać tranzystorów. Rozmawiałem o kondenserach i reostatach, podczas gdy na całym świecie używano już kondensatorów i potencjometrów. Do dzisiaj, od czasu do czasu, zdarza mi się powiedzieć “cykli na sekundę" zamiast herców.
Czy więc taka biblioteka przyniosła mi więcej szkody niż pożytku? Nie wiem. Gdy przeprowadziłem się do Starkville, aby rozpocząć zajęcia na uniwersytecie stanowym Mississippi, czułem się jak Rip Van Winkle. Uniwersytet posiadał potężną, nowoczesną bibliotekę i więcej książek o elektronice, niż kiedykolwiek widziałem. Wieki całe spędzałem w bibliotece, czytając wszystko, co wpadło mi w ręce. Na studiach dowiedziałem się mnóstwa rzeczy, ale większość tej wiedzy pochodzi właśnie z uniwersyteckiej biblioteki.
Różnego rodzaju biblioteki są ważne także w programowaniu. Gdy odrzucisz standardową bibliotekę C, będziesz miał mnóstwo roboty. Czy naprawdę chcesz się podjąć bezpośredniej obsługi systemu plików? Czy chcesz ręcznie formatować strumień wyjściowy? Osobiście nie chciałbym tego robić, więc korzystam ze standardowej biblioteki C. Także Windows API jest przede wszystkim biblioteką.
Jeszcze nie tak dawno jedynym używanym rodzajem bibliotek kodu były biblioteki statyczne. Oczywiście, nikt ich tak nie nazywał, gdyż wtedy znano tylko takie rozwiązanie. Pomysł polega na wyodrębnieniu kodu z biblioteki i skopiowaniu go do programu (przy pomocy programu łączącego - linkera). W ten sposób biblioteka stawała się nieodłączną częścią programu.
Takie podejście posiada kilka wad. Po pierwsze, każdy program zawiera własną kopię biblioteki, co oznacza marnotrawstwo miejsca. Po drugie, co się stanie, gdy zmieni się biblioteka? Aby móc skorzystać z poprawek, musisz ponownie połączyć kod z biblioteką. Wyobraź sobie, że musiałbyś ponownie linkować swoje programy dla każdej wersji Windows!
Większość systemów operacyjnych (łącznie z Windows) w dalszym ciągu potrafi wykorzystać statyczne biblioteki. Jednak większość systemów potrafi także obsłużyć różne metody łączenia dynamicznego. Dzięki temu możesz wywoływać biblioteki kodu w czasie wykonywania programu, a nie w czasie łączenia. Za każdym razem, gdy program zostaje uruchomiony, na dysku odszukiwane są odpowiednie biblioteki, które są ładowane do pamięci. Oznacza to, że program nie musi zawierać kopii biblioteki, a także, że podczas każdego uruchomienia program automatycznie korzysta z jej najnowszej wersji.
Windows obsługuje dynamiczne łączenie wykorzystując biblioteki DLL (dynamie link library). Przy pomocy MFC możesz tworzyć i używać w programach kilku typów bibliotek DLL, musisz jedynie pamiętać o kilku technikach, których trzeba użyć w celu zapewnienia zgodności.
Proces łączenia
Gdy Visual C++ linkuje program, łączy kod programu z plikami LIB, które (zwykle) zawierają statyczne biblioteki. Możesz jednak określić biblioteki importowe (tzw. IMPL1B), które w rzeczywistości nie zawierają żadnego kodu, a tylko funkcje startowe (stub) wymuszające załadowanie i wykonanie kodu z biblioteki DLL.
Oznacza to, że gdy łączysz bibliotekę, nie musisz wiedzieć, czy biblioteka wstawia kod do Twojego programu czy tylko instruuje go, by załadował kod w czasie wykonania.
W rzeczywistości możesz nawet określić, czy MFC ma pobierać swoje standardowe procedury ze statycznej biblioteki (co powoduje generowanie dużych, samodzielnych plików wykonywalnych), czy korzystać z wersji DLL bibliotek (w wyniku czego otrzymasz mniejsze pliki wykonywalne, które do działania potrzebują bibliotek DLL).
Istnieje jeszcze jeden sposób załadowania biblioteki w czasie wykonywania programu. Możesz użyć funkcji LoadLibrary w celu wczytania DLL-a, i ręcznie połączyć go z programem. To wymaga nieco więcej pracy, ale w niektórych przypadkach jest bardzo użyteczne. Przypuśćmy, że tworzysz program, który może operować w kilku językach. Mógłbyś wtedy użyć osobnych DLL-i dla każdego z języków i wybrać odpowiedni z nich dopiero w czasie wykonywania programu. Każdy DLL zawierałby te same funkcje, ale odnoszące się do wybranych języków. Zamierzony efekt trudno byłoby osiągnąć przy pomocy bibliotek importowych, ale dzięki LoadLibrary jest to stosunkowo proste.
Zagadnienia dotyczące języków programowania
Ponieważ DLL tworzą kręgosłup Windows, sensowne byłoby, gdyby bibliotekę DLL mógł wywoływać program napisany w dowolnym języku. W związku z tym twórcy DLL-i zwykle starają się, by większość programów mogła z ich bibliotek korzystać. Oczywiście nie musisz się tym przejmować, ale w większości przypadków opłaca się podjąć kilka dodatkowych kroków.
W praktyce oznacza to, że większość DLL-i, jakie napotkasz, będzie posiadało jedynie proste funkcje (nie będące funkcjami składowymi) korzystające z konwencji wywołania funkcji języka Pascal. Istnieją oczywiście wyjątki, ale taka jest ogólna zasada.
Czy to oznacza, że w swoich DLL-ach nie możesz umieszczać klas czy funkcji ze zmienną liczbą argumentów? Oczywiście że nie. Oznacza to tylko, że jeśli umieścisz w DLL-u takie elementy, stanie się on mniej dostępny.
Korzystanie ze zwykłych DLL-i
Gdy chcesz wywołać bibliotekę DLL, musisz zdefiniować funkcje, które DLL deklaruje. Często zdarza się, że wraz z DLL-em znajduje się plik nagłówkowy, który możesz wykorzystać. Jeśli jednak DLL nie korzysta z funkcji C++ (a większość nie korzysta), musisz zwrócić uwagę na to, by funkcje w pliku nagłówkowym były zdefiniowane jako funkcje C, a nie funkcje C++. W przeciwnym razie C++, podczas łączenia zmodyfikuje nazwy funkcji, przez co łączenie się nie powiedzie.
Nagłówek powinien wyglądać tak:
extern "C" {
// deklaracje funkcji
Jeśli jednak plik nie wygląda w ten sposób, możesz łatwo sobie z tym poradzić. Po prostu wstaw dyrektywę #include wewnątrz polecenia extern:
extern "C" {
łinclude "dllhf ile. h"
Gdy już deklaracje znajdą się na swoich miejscach, musisz jedynie dodać do swojego projektu odpowiedni plik LIB. Plik LIB możesz dodać tak samo, jak każdy inny plik. Jeśli chcesz ukryć swoją bibliotekę DLL-i, możesz także przejść do okna dialogowego Project | Settings i dodać plik LIB do listy bibliotek używanych w projekcie. Tę listę > znajdziesz na zakładce Link okna dialogowego.
Kompilator automatycznie wygeneruje poprawny kod nie zawierający nic, ponad nazwy funkcji. Jeśli jednak zaznaczysz funkcje jako _ declspec(dllimport), Twój kod stanie się nieco bardziej efektywny. W ten sposób możesz także importować zmienne. Możesz na przykład napisać:
_ declspec (dl l import) int dll_flag;
Spowoduje to zaimportowanie z DLL-a zmiennej (o nazwie dll_flag). Zwróć uwagę, że podczas pisania DLL-a nie używasz tej notacji - stosujesz ją tylko wtedy, gdy chcesz coś z biblioteki zaimportować.
To byłoby to wszystko, jeśli chodzi o użycie zwykłych DLL-i, przynajmniej do momentu, w którym nie zechcesz połączyć programu z bibliotekami importowymi. Zwykle jednak właśnie tego chcesz, choć czasami wolałbyś dopiero podczas wykonywania programu zdecydować, którą bibliotekę należy załadować. Możesz to osiągnąć przy pomocy funkcji LoadLibrary, ale wymaga to nieco więcej pracy.
Problem polega na tym, że samo załadowanie biblioteki nie daje dostępu do funkcji w niej zawartych. Aby to osiągnąć, musisz zdobyć wskaźnik do każdej funkcji, którą chcesz ^wywołać. Na szczęście, funkcja GetProcAddress zwraca taki wskaźnik i możesz go Wykorzystać do wywołania funkcji.
Jednym ze sposobów jest inicjalizacja wszystkich wskaźników przed rozpoczęciem wywoływania funkcji. Na przykład:
void (*dllfuncl) (int) ; // globalne
void
(*dllfunc2) (void) ; // także globalne
HANDLE
theDLL;
theDLL = : : LoadLibrary ( "MyDLL.dll" ) ;
dl l fund = :: GetProcAddress (theDLL,"dllfuncl");
dllfunc2 = :: GetProcAddress ( theDLL,"dllfunc2 " ) ;
dllfuncl (100) ;
Zwróć uwagę, że w tym przypadku wywołanie GetProcAddress wymaga podania nazwy funkcji. Większość DLL-i eksportuje swoje funkcje poprzez nazwy. Jednak w celu osiągnięcia maksymalnej wydajności, niektóre DLL-e eksportują swoje funkcje poprzez numery porządkowe. Można powiedzieć, że każda funkcja zamiast nazwy posiada swój unikalny numer. Gdy używasz bibliotek importowych (IMPLIB), ten mechanizm jest niewidoczny. Jeśli jednak wywołujesz GetProcAddress, musisz znać numer (jeśli taki występuje). Takie informacje zwykle znajdują się w dokumentacji DLL-a, a jeśli ich nie ma, powinno pomóc uruchomienie programu DUMPBIN w celu poznania funkcji eksportowanych przez bibliotekę.
Program DUMPBIN to aplikacja dostarczana przez Microsoft razem z pakietem Visual C++. Podobne programy są dostępne także u innych producentów. Ten program linii poleceń znajduje się w kartotece BIN kompilatora. Przeznaczeniem programu jest wyświetlenie zawartości pliku EXE lub DLL w sensowny sposób. Jeśli chcesz dowiedzieć się, jakie funkcje są eksportowane przez DLL-a, jako parametru uruchomienia programu użyj /EKPORTS oraz nazwy pliku. Jeśli chcesz dowiedzieć się, jakich DLL-i program lub biblioteka wymagają, użyj parametru /IMPORTS. DUMPBIN akceptuje także wiele innych przełączników (aby je poznać, uruchom program bez żadnych parametrów). Opcja /ALL powoduje zdekodowanie wszystkich dostępnych informacji. Starzy asemblerowi hackerzy (tacy jak ja) z pewnością docenią opcję /DISASM. Wypróbuj ją!
Tworzenie zwykłego DLL-a
Samodzielne stworzenie DLL-a jest prawdziwym wyzwaniem. Dzięki AppWizardowi, z MFC sprawa upraszcza się do kilku kliknięć, trzeba jedynie pamiętać o kilku rzeczach.
Jak widać, na rysunku 7. l, kreator zawiera tylko jedno okno dialogowe. W tym momencie możesz ograniczyć się tylko do zwykłego DLL-a (DLL-ami MFC zajmiemy się w dalszej części rozdziału). Dwie opcje odnoszące się do zwykłych DLL-i umożliwiają użycie statycznej lub dynamicznie łączonej kopii biblioteki MFC. Której z nich powinieneś użyć? To zależy. Tworzenie statycznej kopii biblioteki pozwala na użycie DLL-a bez żadnych dodatkowych plików (przynajmniej jeśli chodzi o obsługę MFC). Nie będziesz musiał przejmować się wersją MFC ani nawet tym, czy MFC w ogóle jest zainstalowane na danym komputerze, ponieważ Twój DLL będzie samowystarczalny. Jednak za tę wygodę trzeba zapłacić pewną cenę. Twoja biblioteka DLL będzie całkiem duża, gdyż będzie zawierać Twój kod, plus wszystko to, co jest wykorzystywane przez MFC.
Odpowiedzią, oczywiście, może być pozwolenie by DLL dynamicznie łączył się z biblioteką MFC poprzez inny DLL. Oznacza to także, że nie możesz oczekiwać iż Twój DLL będzie działał bez bibliotek MFC. Z drugiej strony, Twój DLL będzie niewielki i efektywny.
Innym powodem korzystania z bibliotek w plikach DLL jest tworzenie wielu programów i DLL-i, które korzystają z MFC. Ponieważ wszystkie Twoje programy i biblioteki mogą wspólnie dzielić te same DLL-e bibliotek MFC, otrzymujesz w wyniku mniejsze pliki, wymagające mniejszej ilości dyskietek instalacyjnych (lub krótszego czasu ładowania).
Główny plik
Przykład DLL-a znajdziesz w listingu 7. l. Zwróć uwagę, że chociaż mamy do czynienia z DLL-em, posiada on obiekt klasy wyprowadzonej z CWinApp. W tym przypadku ta klasa nie reprezentuje działającej aplikacji, zamiast tego stanowi połączenie DLL-a z biblioteką MFC.
Listing 7.1. Przykładowy DLL.
// dumbdll.cpp : Defines the initialization routines for the DLL.
//
#include "stdafx.h"
#include "dumbdllmfc.h" // changed from dumbdll.h
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
//
// Note!
//
// If this DLL is dynamically linked against the MFC
// DLLs, any functions exported from this DLL which
// call into MFC must have the AFX_MANAGE_STATE macro
// added at the very beginning of the function.
//
// For example:
//
// extern "C" BOOL PASCAL EXPORT ExportedFunction()
// {
// AFX_MANAGE_STATE(AfxGetStaticModuleState());
// // normal function body here
// }
//
// It is very important that this macro appear in each
// function, prior to any calls into MFC. This means that
// it must appear as the first statement within the
// function, even before any object variable declarations
// as their constructors may generate calls into the MFC
// DLL.
//
// Please see MFC Technical Notes 33 and 58 for additional
// details.
//
/////////////////////////////////////////////////////////////////////////////
// CDumbdllApp
BEGIN_MESSAGE_MAP(CDumbdllApp, CWinApp)
//{{AFX_MSG_MAP(CDumbdllApp)
// NOTE - 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()
/////////////////////////////////////////////////////////////////////////////
// CDumbdllApp construction
CDumbdllApp::CDumbdllApp()
{
// TODO: add construction code here,
// Place all significant initialization in InitInstance
}
/////////////////////////////////////////////////////////////////////////////
// The one and only CDumbdllApp object
CDumbdllApp theApp;
// Przkładowa eksportowana funkcja
__declspec(dllexport) void Alert(char *s)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
MessageBeep(-1);
AfxMessageBox(s);
}
Ponieważ DLL posiada obiekt aplikacji, jest w stanie wykonywać czynności, które może wykonywać aplikacja, takie jak ładowanie zasobów, pobieranie uchwytu instancji itp. Ważne jest jednak, aby zdawać sobie sprawę z tego, że DLL staje się częścią innej aplikacji, i wykonuje się w jej wirtualnej przestrzeni adresowej. Jeśli tą aplikacją jest program MFC, będzie on posiadał własny obiekt aplikacji.
Do wygenerowanego przez AppWizarda projektu wprowadziłem jedną zmianę. Chciałem żeby programy korzystające z tego DLL-a używały nagłówka DUMBDLL.H. Jednak podczas generowania plik jest wypełniany mnóstwem elementów MFC, które albo nie są ważne, albo w ogóle są zbędne dla innych programów. Tak więc zmieniłem nazwę wygenerowanego pliku z DUMBDLL.H na DUMBDLLMFC.H, po czym stworzyłem prosty plik DUMBDLL.H w celu włączania przez inne programy (patrz listing 7.2). Oprócz tego, na listingu 7.3 znajdziesz prostą aplikację terminala korzy stającą z tego DLL-a.
Testowanie przy pomocy aplikacji terminala
Z pewnością zauważyłeś, że kod z listingu 7.3, testujący DLL-a, nie jest programem MFC. Często wygodnie jest stworzyć mały program testujący część Twojego DLL-a; po co pisać do tego pełną aplikację Windows? Oczywiście, czasem nie ma wyboru; jeśli jednak Twój DLL wykonuje obliczenia lub posiada niezależny interfejs użytkownika (tak jak ten), możesz zaoszczędzić sobie dużo pracy. Po prostu stwórz nowy projekt typu Win32 Console Application. Na początku projekt nie będzie zawierał żadnych plików źródłowych. Użyj polecenia New tworząc nowy plik CPP, i gotowe. Jeśli jeszcze nie tworzyłeś aplikacji terminala, nie martw się - to bardzo proste. Aplikacje terminala są bardzo podobne do starych programów DOS-a czy Unix-a. Możesz spokojnie używać funkcji printf czy cout, które poznałeś, gdy uczyłeś się C lub C++. Różnice? Programy terminala mają dostęp do Windows API, mogą alokować megabajty pamięci, wywoływać okna komunikatów, a nawet wywoływać okna dialogowe. Znajdziesz wiele zastosowań dla aplikacji terminala. Wystarczy
Listing 7.2. Nagłówek DLL-a.
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllimport) void Alert(char *s);
#ifdef __cplusplus
}
#endif
Listing 7.3. Użycie DLL-a (aplikacja terminala)
#include <iostream.h>
#include "dumbdll.h" // not MFC version
void main()
{
char c;
cout<<"Get ready for the alert!\n";
cout.flush();
Alert("Alert! Alert!");
cout<<"I told you it was a dumb dll\n";
cout<<"Press Enter to continue";
cin.get(c);
}
Eksportowanie funkcji
Mając na uwadze innych programistów (którzy być może nie używają Visual C++), dobrym pomysłem jest zadeklarowanie swoich dostępnych z zewnątrz funkcji w taki sposób, aby każdy mógł je wywołać. Oznacza to użycie funkcji globalnych (zamiast składowych), ze składnią wy wołania języka C. Czyli:
extern "C" {
_declspec(dllexport) _declspec(dllexport)
Polecenie _declspec(dllexport) wskazuje, że funkcja ma być dostępna dla zewnętrznego świata. Możesz osiągnąć to samo, dodając do projektu plik DEF i umieszczając nazwę funkcji w sekcji EXPORTS (listing 7.4).
Listing 7.4. Przykładowy plik DEF.
EKPORTS
ManiaBegin
ManiaEnd
ProduceMania
Bez polecenia extern "C" kompilator przekształciłby moją niewinną funkcję Alert w ?Alert@VAXPAD@Z (czy coś w tym stylu). Ponieważ byłoby to bardzo niewygodne, stosujemy konwencję deklaracji języka C, co zabezpiecza nas przed konwersją nazw. Zauważ też, że w pliku unikałem wszelkich związanych z MFC makr i innych zależności.
Gdy spróbujesz nazwać funkcje w pliku DEF lub w zewnętrznym programie, okazuje się, że kompilator może nieco zmienić ich nazwę. W pliku DEF możesz przemianować funkcję znaną kompilatorowi jako _f na funkcję o nazwie f; służy do tego następująca konstrukcja:
EKPORTS f=_f
Jeśli chcesz wyeksportować funkcję poprzez jej numer porządkowy (co jest nieco bardziej wydajne niż w przypadku eksportowania nazwy), będziesz musiał użyć pliku DEF. Aby poprzednio wyeksportowanej funkcji nadać numer 5, napiszesz:
EKPORTS f=_f @5
Jeśli chcesz, by nazwa w ogóle nie pojawiała się w pliku DLL, użyj dyrektywy NONAME.
Istnieje trzecia metoda eksportowania funkcji. Po prostu dopisz funkcję w opcji /EXPORT w linii poleceń programu łączącego (możesz ustawić tę opcję w menu Projects | Settings). Po przełączniku /EXPORT dodaj dwukropek i tę samą linię, jaką umieściłbyś w pliku DEF. Całkowicie akceptowalne jest użycie wszystkich trzech metod (_declspec, pliku DEF i opcji /EXPORT) w tym samym programie.
Plik DUMBDLLMFC.H włącza plik DUMBDLL.H, a zawarte w nim deklaracje zawsze są aktualne. Jednak nagłówek DUMDLL.H podaje funkcję jako _declspec(dllimport). W tym przypadku to nie jest poprawne (w tym miejscu chcemy użyć _declspec(dllexport)). Kod stosuje makro wymuszające zmianę wszystkich poleceń _declspec w pliku DUMBDLL na polecenia eksportowe. Na przykład mógłbyś napisać:
tłifndef DLLFUNC
ttdefine DLLFUNC _declspec(dllimport) ttendif
Następnie mógłbyś zdefiniować DLLFUNC jako _declspec(dllexport) w pliku DUMBDLLMFC.H, jeszcze przed włączeniem pliku DUMBDLL.H.
Jeśli z wnętrza DLL-a korzystasz z MFC w postaci biblioteki DLL, musisz pamiętać o jeszcze jednej ważnej rzeczy. W tym samym czasie, z tej samej biblioteki MFC DLL, może korzystać jeszcze inny program. Przypuśćmy, że Twój DLL używa biblioteki MFC, a program używający Twojego DLL-a także korzysta z tej biblioteki. Ważne jest, aby MFC nie pogubiło się w tej sytuacji.
Sztuczka polega na tym, że, gdy z wnętrza DLL-a używasz MFC w postaci biblioteki DLL, każdą eksportowaną funkcję powinieneś rozpoczynać od następującej instrukcji:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
Jeśli o tym zapomnisz, w momencie wykonania czegoś we wnętrzu funkcji, zostanie wygenerowany wyjątek. Jeśli używasz MFC w postaci biblioteki statycznej, powyższa linia kodu nie jest potrzebna.
Z biblioteki DLL możesz eksportować zmienne globalne. Używa się przy tym tej samej techniki co w przypadku funkcji. Z drugiej strony, zmienne w DLL-u mogą mieć określony charakter, o czym powinieneś wiedzieć (zajrzyj do następnej sekcji).
Zmienne prywatne i dzielone
Kto posiada globalne zmienne DLL-a? Teraz uważaj. W Win 16 odpowiedź brzmi: każdy. Nie jest to niespodzianką, ponieważ każdy program w rzeczywistości korzysta z tej samej pamięci. W przypadku Win32 odpowiedź nie jest już tak jednoznaczna. Domyślnie, jeśli korzystasz z 32-bitowego kompilatora Borlanda, sytuacja wygląda tak samo. Jeśli z Twojego DLL-a korzystałyby trzy programy, każdy z nich widziałby te same zmienne globalne. W przypadku Microsoft Visual C++ dzieje się zupełnie inaczej. Domyślnie, DLL Microsoftu zawsze przydzielają programom prywatne kopie zmiennych globalnych. Przyjrzyj się poniższemu fragmentowi kodu z DLL-a:
n) // funkcja eksportowana
int pobierz_próg(void) // funkcja eksportowana
return próg; }
Jeśli tego DLL-a załadowałyby trzy programy Win 16, i jeden z nich ustawiłby wartość progową, pozostałe dwa programy widziałby właśnie tę wartość. Zwróć uwagę, że gdy DLL zostałby usunięty z pamięci (żaden program by z niego nie korzystał), ta wartość nie zostałaby zachowana. Jeśli skompilowałbyś DLL-a przy pomocy kompilatora Borlana, otrzymałbyś takie samo zachowanie (chyba że w pliku DEF użyłbyś instrukcji MULTIPLE). W przypadku Microsoftu, aby otrzymać taki efekt, trzeba wykonać nieco dodatkowej pracy.
Plan jest następujący:
• Oznacz zmienną przy pomocy specjalnej instrukcji pragma. Spowoduje to umieszczenie zmiennej w sekcji danych.
• Upewnij się, że zmienna ma przypisaną wartość początkową.
• Użyj opcji linkera (lub pliku DEF) w celu ustawienia atrybutów sekcji danych zawierającej zmienną.
A oto deklaracja tej samej zmiennej przy pomocy pragmy współdostępu:
tpragma data_seg(".ASHARE")
int próg = O;
//w tym miejscu mogą znaleźć się inne wspólne dane
ttpragma data_seg ()
Jeśli nie zainicjujesz zmiennej, sposób nie zadziała. Ważne jest, aby o tym pamiętać. Samo zdefiniowanie nie wystarczy, musisz dostarczyć wartość początkową.
Nazwa sekcji jest specjalna. Nie może składać się z więcej niż ośmiu znaków i musi zaczynać się od kropki. W przeciwnym razie mógłby to być każdy dozwolony identyfikator. Oczywiście nie możesz użyć nazw zarezerwowanych przez kompilator do czegoś innego (tj. .CODE, .DATA, .BSS itd.).
Następnie musisz przekazać linkerowi specjalną opcję. Możesz dokonać tego w ustawieniach projektu (dokładnie w którym miejscu, zależy od wykorzystywanej wersji VC++). Okno dialogowe ustawień nie posiada specjalnego miejsca dla opcji, której potrzebujesz; musisz zmodyfikować linię poleceń (w dolnej części okna). Jeśli nie możesz jej zmienić, upewnij się, że wybrałeś albo wersję Release, albo Debug, (nie obie naraz). Jeśli zostaną wybrane obie wersje, VC++ nie pozwoli na modyfikację linii poleceń. A oto co powinieneś wpisać:
/SECTION:.ASHARE,RWS
Oczywiście, jeśli nadasz sekcji inną nazwę, użyj tej nazwy w miejsce ASHARE. Napis RWS oznacza Read, Write oraz Shared. Atrybut shared wymusza na kompilatorze by segment (obszar danych) był dzielony pomiędzy różne programy.
Jeśli wolisz, możesz stworzyć plik DEF i w nim umieścić linię SECTIONS:
LIBRARY DLL SECTIONS
.ASHARE READ,WRITE,SHARED
Jeśli użyjesz tej metody, pamiętaj o dodaniu pliku DEF do projektu. MSVC nie używa plików DEF domyślnie. Jeśli chcesz dzielić pomiędzy programy wszystkie zmienne, przy pomocy jednej z powyższych technik (linia poleceń lub plik DEF) zaznacz po prostu sekcje .DATA i .BSS jako dzielone.
Gdy używasz wspólnej pamięci, powinieneś mieć na uwadze sposób, w jaki Windows NT ładuje DLL-e. Każdy DLL posiada preferowany adres ładowania. Gdy Windows NT ładuje DLL-a dla procesu, umieszcza go -jeśli to możliwe - pod tym wirtualnym adresem. Jeśli to nie jest możliwe, NT musi przeładować DLL-a (tzn. przesunąć go i zmodyfikować adresy). To oznacza, że choć sekcja danych może być współdzielona, adresy zmiennych dla różnych procesów mogą być różne. Wniosek: nie przechowuj adresów do dzielonej pamięci w dzielonej pamięci.
W celu zmiany preferowanego adresu ładowania możesz użyć programu REBASE. Możesz także użyć opcji /BASE linkera. Dodanie opcji /FIXED zabezpieczy system przed realokacją DLL-a (co oznacza, że jeśli DLL nie może być załadowany pod preferowany adres, otrzymasz komunikat błędu). Innym sposobem ustawienia adresu bazowego jest użycie opcji BASE w poleceniu LIBRARY pliku DEF. Technicznie rzecz biorąc, pliki wykonywalne także posiadają preferowany adres ładowania. Nie musisz jednak się nim zwykle przejmować, gdyż programy jako pierwsze są ładowane do przestrzeni adresowej procesu, w związku z czym zawsze są ładowane tam, gdzie chcą.
Poza tym, cała ta dyskusja nie dotyczy Windows 95. W Windows 95 wszystkie DLL-e są ładowane do pojedynczego zarezerwowanego bloku pamięci. Oznacza to, że gdy DLL już zostanie załadowany (i to zwykle nie pod preferowanym adresem), pozostaje w tym samym miejscu dla wszystkich procesów, aż do momentu zwolnienia go z pamięci.
DLL-e MFC
Łatwo jest stworzyć DLL-e rozszerzające samą bibliotekę MFC. Jedyne ograniczenie polega na tym, że tylko programy korzystające z MFC w postaci biblioteki DLL mogą używać DLL-i rozszerzających MFC. Aby stworzyć jeden z takich rozszerzających DLL-i, możesz uruchomić AppWizarda dla DLL-i, i w oknie dialogowym (rys. 7.1) wybrać trzecią opcję (MFC Extension DLL). Jako rezultat otrzymujemy pusty projekt.
Aby wypełnić rozszerzającego DLL-a, powinieneś po prostu dodać klasy i zasoby odpowiadające Twoim wymaganiom. Aby udostępnić klasy innym programom, zdefiniuj je w pliku nagłówkowym przy pomocy makra AFX_EXT_CLASS:
class AFX_EXT_CLASS CCustomClass : Public CWindow . . .
Spowoduje to wyeksportowanie przez kompilator całej klasy. Choć możesz eksportować poszczególne funkcje (przy pomocy _declspec(dllexport)), okazuje się to nieco uciążliwe, gdyż wiele makr MFC generuje funkcje, które także trzeba wyeksportować. Oczywiście, ponieważ takie eksportowane funkcje mają długie, udekorowane nazwy C++, nie jest to zbyt efektywna metoda. Alternatywa jest jednak bardzo żmudna. Musisz wygenerować listing, odcyfrować wszystkie długie nazwy i ręcznie stworzyć plik DEF, tak by móc eksportować funkcje według numerów porządkowych. Jeśli się na to zdecydujesz, poczekaj przynajmniej do momentu, w którym będziesz gotów do wypuszczenia DLL-a, a dopiero potem zbuduj plik DEF, tak by nie musieć ręcznie go tworzyć.
DLL-e rozszerzeń nie mają obiektu CWinApp - jest to przywilej wyłącznie głównych aplikacji. Zamiast tego DLL-e MFC posiadają obiekt klasy CDynLinkLibrary. Jeśli chcesz przechowywać osobne zmienne dla każdej instancji MFC korzystającej z Twojego DLL-a, możesz wyprowadzić nową klasę z klasy CDynLinkLibrary i użyć jej w miejsce funkcji DLLMain. Po prostu umieść wszystkie dane, którymi chcesz manipulować, w tej nowej klasie.
Inną rzeczą, którą DLL-e rozszerzeń dzielą z głównym programem, są zasoby. Jest to przydatne jeśli chcesz wspólnie korzystać z zasobów w DLL-u i w programie, ale przedstawia pewien problem. Identyfikatory zasobów są globalne, jeśli więc chcesz dostarczyć bitmapę, lepiej żeby miała identyfikator, z którego główny program nie korzysta. Większość programistów rezerwuje dla swoich DLL-i blok identyfikatorów zasobów, ale sprawa się komplikuje, gdy korzystasz z kilku DLL-i z kilku źródeł. MFC poszukuje zasobów we wszystkich DLL-ach (łącznie DLL-ami systemowymi) oraz w głównym programie.
Jeśli jesteś pewien, że chcesz załadować zasób z określonego modułu, możesz sprawić, by MFC szukało tylko w jednym miejscu. Najpierw wywołaj AfxGetResourceHandle w celu otrzymania uchwytu bieżących zasobów, i przechowaj go. Następnie możesz wywołać AfxSetResourceHandle ustawiając uchwyt na uchwyt instancji modułu, który chcesz przeszukać. Gdy już będziesz miał uchwyt szukanego zasobu, nie zapomnij o ponownym wywołaniu AfxSetResourceHandle i przywróceniu poprzedniego ustawienia.
Aby skorzystać z DLL-a rozszerzającego MFC, po prostu włącz plik nagłówkowy i plik LIB do zwykłego projektu. Czym to się różni od zwykłego DLL-a? Możesz przekazywać obiekty MFC w obie strony, eksportować całe klasy i wspólnie korzystać z zasobów, co byłoby trudne w przypadku zwykłych DLL-i. Oczywiście, nie muszę chyba wspominać, że DLL-e rozszerzające MFC działają tylko z programami MFC. Nie można ich w prosty sposób użyć z innym językiem programowania lub środowiskiem programowym.
Przykład prostego rozszerzenia biblioteki MFC znajdziesz na listingach 7.5 i 7.6. To rozszerzenie to nic więcej ponad niemodalną klasę dialogową z rozdziału 5 - nie ma w niej żadnych zmian odnoszących się do DLL-a. Różnica? Programista, który chciałby użyć teraz tej klasy, potrzebuje jedynie DLL-a, pliku nagłówkowego i pliku LIB.
Listing 7.5 przedstawia główną część DLL-a. Tworzony jest w niej obiekt CDynLinkLibrary. Oczywiście, jeśli chcesz, możesz użyć klasy wyprowadzonej z tej klasy. Możesz także dodać kod rozpoznający moment, w którym system ładuje tego DLL-a, choć w przypadku MFC nie jest to zwykle zbyt interesujące.
Listing 7.5. Główny plik biblioteki DLL rozszerzającej bibliotekę MFC
// mlessdlg.cpp : Defines the initialization routines for the DLL.
//
#include "stdafx.h"
#include <afxdllx.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
static AFX_EXTENSION_MODULE MlessdlgDLL = { NULL, NULL };
extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
// Remove this if you use lpReserved
UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH)
{
TRACE0("MLESSDLG.DLL Initializing!\n");
// Extension DLL one-time initialization
if (!AfxInitExtensionModule(MlessdlgDLL, hInstance))
return 0;
// Insert this DLL into the resource chain
// NOTE: If this Extension DLL is being implicitly linked to by
// an MFC Regular DLL (such as an ActiveX Control)
// instead of an MFC application, then you will want to
// remove this line from DllMain and put it in a separate
// function exported from this Extension DLL. The Regular DLL
// that uses this Extension DLL should then explicitly call that
// function to initialize this Extension DLL. Otherwise,
// the CDynLinkLibrary object will not be attached to the
// Regular DLL's resource chain, and serious problems will
// result.
new CDynLinkLibrary(MlessdlgDLL);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
TRACE0("MLESSDLG.DLL Terminating!\n");
// Terminate the library before destructors are called
AfxTermExtensionModule(MlessdlgDLL);
}
return 1; // ok
}
Listing 7.6. Klasa DLL-a rozszerzenia. ______________
// ModelessDlg.cpp : implementation file
//
#include "stdafx.h"
#include "stdafx.h"
#include "ModelessDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CModelessDlg dialog
CModelessDlg::CModelessDlg(CWnd* pParent /*=NULL*/)
{
//{{AFX_DATA_INIT(CModelessDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
}
void CModelessDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CModelessDlg)
// NOTE: the ClassWizard will add DDX and DDV calls here
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CModelessDlg, CDialog)
//{{AFX_MSG_MAP(CModelessDlg)
// NOTE: the ClassWizard will add message map macros here
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CModelessDlg message handlers
void CModelessDlg::OnOK()
{
UpdateData(TRUE);
DestroyWindow();
}
void CModelessDlg::OnCancel()
{
DestroyWindow();
}
void CModelessDlg::PostNcDestroy()
{
delete this;
}
A co z DLL-ami OLE (lub ActiveX)?
Programiści Visual Basica często mówią o DLL-ach OLE (lub ActiveX). Programiści C++ zwykle nazywają je serwerami ActiveX. Choć fizycznie są to DLL-e, mamy do czynienia z DLL-ami, których oprogramowanie systemu ActiveX używa do tworzenia obiektów. Osobiście będziesz zajmował się nimi jako czystymi obiektami ActiveX, a to już inny rozdział (konkretnie rozdział 8). W tym momencie powinieneś jedynie zdawać sobie sprawę z tego, że materiał w tym rozdziale tylko w niewielkim stopniu odnosi się do DLL-i ActiveX, a to dlatego, że te DLL-e tylko z grubsza przypominają zwykłe biblioteki.
Posumowanie
DLL-e stanowią kamień węgielny programowania w Windows. Choć obecnie współdzielenie kodu odbywa się coraz częściej przy pomocy ActiveX, DLL-e w dalszym ciągu stanowią pierwszy (i czasem najlepszy) sposób współużytkowania fragmentu kodu przez różne programy. DLL-e są efektywne i powszechnie zrozumiałe (jeśli zostaną poprawnie skonstruowane).
Z najciekawszych zastosowań DLL-i jest umożliwienie samodzielnego dostosowywania oprogramowania. MFC skwapliwie korzysta z tej możliwości pozwalając na uzupełnianie swojej biblioteki o własne DLL-e, co bardzo ułatwia tworzenie własnych obiektów programowych.
Praktyczny przewodnik Biblioteki DLL i MFC
Wyznaczanie DLL-i używanych przez program oraz funkcji eksportowanych przez DLL-a
Łączenie w czasie budowy kodu
Łączenie w czasie wykonywania programu
Tworzenie DLL-a
Eksportowanie funkcji i danych
Tworzenie DLL-i rozszerzających MFC
Optymalizowanie adresu ładowania DLL-a
Wyznaczanie DLL-i używanych przez program oraz funkcji eksportowanych przez DLL-a
Możesz przeglądać programy i DLL-e przy pomocy dostarczonego przez Microsoft narzędzia o nazwie DUMPBIN. Ten program linii poleceń wymaga podania kilku opcji i nazwy pliku. Jeśli użyjesz opcji /EXPORTS, program wyświetli symbole eksportowane, przez bibliotekę. Użycie opcji /IMPORTS owocuje wyświetleniem listy DLL-i i funkcji wymaganych przez program do działania. Uruchomienie programu bez żadnych parametrów powoduje wyświetlenie listy dostępnych opcji.
Łączenie w czasie budowy kodu
Najprostszym sposobem dołączenia DLL-a do swojego programu jest włączenie pliku nagłówkowego i dodanie do projektu odpowiedniego pliku LIB. Jeśli używasz DLL-a przeznaczonego do wykorzystania przez program w C, powinieneś ująć instrukcję #include w specjalną formę instrukcji statement:
extern "C" {
ttinclude "dllheader.h"
}
Jeśli przed funkcjami, które planujesz pobrać z DLL-a, umieścisz specjalne słowo kluczowe _declspec(dllimport), działanie programu będzie nieco bardziej wydajne. W przypadku danych, które chcesz importować, użycie polecenia _declspec jest konieczne.
Łączenie w czasie wykonywania programu
Możesz także dołączyć bibliotekę DLL już w trakcie działania programu. Sztuczka polega na wywołaniu funkcji LoadLibrary z nazwą pliku DLL-a. W rezultacie otrzymuje się uchwyt, który można przekazać funkcji GetProcAddress. Oprócz tego do tej funkcji przekazuje się nazwę funkcji lub jej numer porządkowy. Funkcja GetProcAddress zwraca z kolei wskaźnik do funkcji. Gdy już nie potrzebujesz DLL-a, pamiętaj o zwolnieniu go funkcją FreeLibrary.
Tworzenie DLL-a
Przy pomocy MFC możesz tworzyć trzy rodzaje DLL-i. W każdym przypadku powinieneś użyć kreatora AppWizard dla DLL-i. DLL pierwszego rodzaju używa MFC i zawiera w sobie pełną kopię tej biblioteki. W rezultacie otrzymuje się duże, ale za to samowystarczalne pliki wykonywalne. Druga możliwość polega na stworzeniu DLL-a, który korzysta z MFC w postaci bibliotek DLL. W wyniku otrzymuje się mniejszy plik wykonywalny, ale jest on zależny od posiadania właściwej wersji bibliotek DLL MFC. Trzeci rodzaj DLL-i, jakie możesz tworzyć, stanowi bibliotekę rozszerzającą samo MFC.
Dwa pierwsze rodzaje DLL-i posiadają własny obiekt aplikacji wyprowadzony z klasy CWinApp. Ten obiekt (który w rzeczywistości nie reprezentuje aplikacji) pozwala bibliotece na załadowanie zasobów, poznanie uchwytu instancji itd.
Eksportowanie funkcji i danych
Gdy chcesz wyeksportować globalne funkcje lub dane, możesz uczynić to na jeden /, trzech sposobów. Po pierwsze, możesz oznaczyć eksportowane elementy modyfikatorem _ declspec(dllexport). Po drugie, możesz dodać do projektu plik DEF i umieścić listę eksportowanych symboli w sekcji EXPORTS. Po trzecie, możesz eksportować symbole, używając przełącznika /EXPORT w linii poleceń linkera.
Jeśli używasz MFC w postaci biblioteki DLL (w odróżnieniu do posiadania w pliku wykonywalnym jej prywatnej kopii), musisz poinformować MFC za każdym razem gdy wchodzisz do funkcji DLL-a. Możesz to osiągnąć, dodając poniższą linię na początku każdej eksportowanej funkcji:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
Tworzenie DLL-i rozszerzających MFC
Jeśli chcesz wyeksportować całe klasy do innego programu MFC, musisz stworzyć DLL-a rozszerzającego bibliotekę MFC. W tym przypadku także korzystasz z tego samego kreatora, tym razem jednak program docelowy (tzn. program, który będzie korzystał z DLL-a) musi używać MFC w postaci biblioteki DLL.
Aby wyeksportować klasę, zaznacz ją przy pomocy makra AFX_EXT_CLASS. Możesz także eksportować poszczególne elementy, używając jednej z poznanych technik (patrz sekcja “Eksportowanie funkcji i danych"). Eksportowanie tylko kilku funkcji /, klasy może spowodować problemy, ponieważ MFC często wstawia składowe, które także trzeba wyeksportować. Zwykle najlepszym rozwiązaniem jest wyeksportowanie całej klasy, chyba że masz ważny powód, aby tego nie robić.
Twój DLL będzie zawierał obiekt klasy CDynamicLinkLibrary, używany przez MFC do zarządzania biblioteką. Jeśli chcesz zachować prywatne egzemplarze zmiennych dla każdej instancji swojej biblioteki, możesz wyprowadzić z CDynamicLinkLibrary własną klasę i wykorzystać ją w jej miejsce.
Optymalizowanie adresu ładowania DLL-a
Gdy Windows NT ładuje DLL-a żądanego przez proces, stara się umieścić go w wirtualnej przestrzeni adresowej procesu pod preferowanym przez DLL-a adresem ładowania. DLL jest już przygotowany do ładowania pod ten adres. Jeśli to się powiedzie, kolejne procesy, które ładują DLL-a, mogą użyć tej samej kopii biblioteki. Jeśli jednak któryś proces nie ma wolnego odpowiedniego miejsca w swojej przestrzeni adresowej (czyli. mówiąc inaczej, coś innego znajduje się w tym miejscu), NT musi przealokować DLL, co może łączyć się ze stosunkowo dużym narzutem czasowym.
Odpowiedzią może być wybór różnych preferowanych adresów dla każdego DLL-a, którego planujesz użyć. l tak, z systemowych DLL-i każdy posiada własne adresy, aby zapobiec wzajemnym konfliktom. Adres bazowy możesz ustawić przy pomocy opcji /BASE w linii poleceń linkera lub korzystając z programu narzędziowego o nazwie REBASE. Po prostu użyj opcji -r i podaj nowy adres ładowania.
Windows 95 ładuje DLL-e do pamięci dzielonej przez wszystkie programy. W związku z tym, omawiane przed chwilą zagadnienia nie dotyczą tego systemu.
Rozdział 8 ActiveX
ActiveX przenoszą korzyści z programowania obiektowego na poziom kodu binarnego. Przy pomocy wbudowanych narzędzi specyficznych dla ActiveX, MFC znacznie ułatwia korzystanie z tych elementów, jeśli tylko wiesz, w jaki sposób z nich skorzystać.
Jak wiele razy słyszałeś już stwierdzenie, że “całość jest większa od sumy poszczególnych elementów?" Rzeczywiste przykłady narzucają się same. Weźmy dziecięce zabawki. Tinkertoys czy klocki Lego to proste elementy z plastiku lub drewna. Osobno, elementy są zupełnie nierozróżnialne. Ale gdy połączysz je z wykorzystaniem dziecięcej wyobraźni, możesz otrzymać naprawdę coś ciekawego. Nie tak dawno NASA zorganizowała objazdową wystawę statków kosmicznych zbudowanych z klocków Lego, a mój syn Patryk i ja zbudowaliśmy z nich bardzo ciekawy klucz kodu Morsea. Bez tych podstawowych komponentów nie moglibyśmy zbudować żadnej skomplikowanej struktury.
To samo odnosi się do sprzętu i oprogramowania komputerów. Jak wielu twórców klonów mogłoby zbudować komputer PC bez procesora Pentium? Z drugiej strony, do czego przydałby się komputer Pentium bez pamięci i innych układów? Lata temu pisałem kompletne programy korzystając tylko z assemblera. Teraz korzystam z bibliotek w celu wczytania pliku, drukowania i wykonywania mnóstwa innych prostych, nieciekawych zadań.
Praktyka programowa ewoluowała z czasem, co pomogło w stworzeniu części coraz lepiej nadających się do ponownego wykorzystania. Wczesne programy, jako sposobu ponownego wykorzystania kodu, używały podprogramów. Później, inne systemy programistyczne wprowadziły biblioteki kodu i inne sposoby jego ponownego użycia.
Obecnie preferowanym sposobem ponownego wykorzystania kodu jest programowanie zorientowane obiektowo. Istnieje wiele sposobów pisania programów obiektowych. Najprościej jest wykorzystać w tym celu język, który pozwala na pisanie obiektowe, na przykład SmallTalk. Można jednak pisać obiektowo w dowolnym języku, jeśli tylko zachowa się dyscyplinę. Język C++ jest pod tym względem nieco dziwny, gdyż pozwala używać konstrukcji obiektowych gdy się tego chce, lub zignorować je, jeśli ktoś tak woli.
Ale nawet wewnątrz C++, na ile Twój kod nadaje się do ponownego użycia? Oczywiście, możesz tworzyć obiekty kodu źródłowego, ale czy możesz przenieść je do innego kompilatora. Zwykle nie na poziomie kodu obiektu, a często nawet przeniesienie samego kodu źródłowego będzie wymagać pewnej pracy. A już z pewnością nie uda się to, gdy zechcesz ponownie wykorzystać kod w innym języku, na przykład w Visual Basicu czy Delphi. Jeśli Twój kod nie jest bardzo prosty, prawdopodobnie nie powiedzie się przeniesienie go na inną platformę, na przykład do Unixa.
Aby móc mówić o oprogramowaniu nadającym się do ponownego wykorzystania, powinno ono być łatwo przenaszalne pomiędzy różnymi językami i platformami. Jest to szczególnie ważne w dobie Internetu, gdzie są połączone ze sobą najróżnorodniejsze komputery.
Obecnie istnieją już różne rozwiązania tego problemu, a jednym z takich rozwiązań jest ActiveX. ActiveX to technologia używana do budowania binarnie zgodnych obiektów. Być może nie jest to stwierdzenie, które na ten temat słyszałeś. Wszyscy wiedzą, że ActiveX służy do osadzania dokumentów w pojemnikach i przekazywania kontrolek przez Internet, czyż nie? Nie całkiem. Pewnie, możesz robić to wszystko przy pomocy ActiveX, ale to nie jest samo ActiveX - podobnie jak sterta klocków Lego nie jest domkiem. Budując obiekty ActiveX możesz tworzyć kontrolki wykorzystywane w Internecie. Te obiekty są binarnie zgodne z innymi obiektami ActiveX używanymi przez przeglądarkę WWW. Możesz budować obiekty ActiveX pozwalające na umieszczanie dokumentów wewnątrz innych dokumentów (lub zarządzanie innymi dokumentami). Te dokumenty są obiektami ActiveX. Możesz użyć cegiełek Lego do zbudowania domku -możesz z nich także zbudować kozły do cięcia drewna. Podobnie, obiekty ActiveX mogą służyć do tworzenia dużych systemów lub małych (i efektywnych) elementów.
W czym ActiveX różni się od OLE, OCX i COM?
Gdy Microsoft stworzył technologię pozwalającą na umieszczenie (na przykład) arkusza kalkulacyjnego w dokumencie procesora tekstu, nazwał ją OLE, co oznacza osadzanie i łączenie obiektów (object linking and embedding). Jednak ten sam termin odnosił się do systemu używanego do tworzenia zgodnych binarnie obiektów. Z powodu stopnia skomplikowania łączącego się z konstruowaniem obiektów pozwalających na umieszczenie jednego dokumentu wewnątrz drugiego, wielu programistów uważało, że OLE było skomplikowane, nawet jeśli podstawowa technologia była dość prosta.
Microsoft dążył do tego, aby jak najwięcej osób wykorzystywało OLE, zdecydowano się więc na zmianę nazwy podstawowej technologii na COM (Component Object Model). Z pewnych powodów mogło się wydawać, że także ta nazwa odstrasza większość programistów. Także później podjęto wiele prób zmiany nazwy tej technologii. Kontrolki Visual Basica i podobne im programy stały się znane jako OCX (OLE Control Extensions).
Na koniec, aby zacząć z czystym kontem, Microsoft ogłosił, że następne główne wydanie specyfikacji OLE będzie określane jako ActiveX. W dalszym ciągu jednak spotyka się inne terminy. Osobiście przywykłem do tego, by jako OLE określać tradycyjne osadzanie jednego dokumentu w innym. Wizualne kontrolki w dalszym ciągu określam jako OCX. COM to proste obiekty nie zaprojektowane do działania wewnątrz pojemników takich jak Visual Basic, zaś ActiveX to technologia, która sprawia, że to wszystko działa. Oczywiście takie są moje osobiste przyzwyczajenia.
Czym jest obiekt ActiveX?
Może to być najprostsze stwierdzenie w tej książce: obiekt ActiveX to każdy kod, który zawiera jedną lub więcej tablic wskaźników do funkcji. Dopóki dostawca obiektu (serwer) i użytkownik tego obiektu (klient) zgadzają się co do przeznaczenia funkcji, nic innego nie jest ważne. Funkcja nie musi być nawet napisana w tym samym języku, ani nawet na tej samej platformie sprzętowej.
Więc gdzie, możesz spytać, jest część zorientowana obiektowo? Cóż, ActiveX jest nieco bardziej skomplikowane niż to do tej pory przedstawiłem (ale tylko nieco). Pierwsze trzy funkcje w każdej z tablic wskaźników służą zawsze do tego samego. Tablice wskaźników do funkcji w obiektach ActiveX noszą nazwę interfejsów, a trzy zawsze obecne funkcje tworzą interfejs o nazwie lUnknown.
Funkcje interfejsu lUnknown nie są zbyt skomplikowane. Dwie z nich (AddRef i Release) śledzą ilość kopii interfejsu będących w użyciu. Przypuśćmy, że tworzysz obiekt zawierający dwa interfejsy (posiadanie kilku interfejsów przez pojedynczy obiekt nie jest niczym niezwykłym). Licznik odwołań do obiektu wynosi l (ponieważ właśnie go stworzyłeś). Załóżmy teraz, że przekazujesz obiekt do innego wątku w Twoim programie. Zanim prześlesz obiekt poprzez jeden z jego interfejsów, powinieneś wywołać funkcję AddRef. Spowoduje to zwiększenie licznika odwołań do dwóch. Zwykle nie ma znaczenia, którego interfejsu użyjesz, gdyż praktycznie wszystkie obiekty używają pojedynczego licznika, bez względu na liczbę obsługiwanych przez siebie interfejsów. Później, gdy skończysz z obiektem, wywołasz funkcję Release. Spowoduje to zmniejszenie licznika odwołań o jeden. Możemy założyć, że wątek uczyni to samo. Gdy obiekt stwierdzi, że wartość licznika spadła do zera, będzie mógł sam zwolnić się z pamięci.
Ostatnia funkcja w interfejsie lUnknown to Querylnterface. Miej na uwadze to, że ponieważ mamy do czynienia z tablicami wskaźników, nazwy funkcji stanowią jedynie konwencję. Celem Querylnterface jest wypytanie jednego z interfejsów o istnienie innego. Przypuśćmy, że obiekt posiada dwa interfejsy, IA i IB. Jeśli masz wskaźnik do interfejsu IA, przy pomocy Querylnterface możesz zapytać o interfejs IB (lub odwrotnie). Jeśli zapytałbyś o jakiś inny interfejs, otrzymałbyś w odpowiedzi wartość NULL. Każde zakończone sukcesem wywołanie funkcji Querylnterface powoduje zwiększenie o jeden licznika odwołań (poprzez funkcję AddRef). Ponieważ każdy interfejs rozpoczyna się od lUnknown, możesz potraktować każdy wskaźnik do interfejsu jako interfejs lUnknown i przepytywać go, aż otrzymasz coś, co Ci się przyda. Niczym niezwykłym nie jest pytanie interfejsu czy wie o samym sobie (w wyniku czego funkcja Querylnterface zwraca wskaźnik do tego samego interfejsu, od którego zacząłeś).
Jak widać, trzy omówione funkcje nie są zbyt skomplikowane. Jednak podobnie jak 7, klocków Lego, można zbudować na ich podstawie potężny, obiektowo zorientowany system. Jeśli jednak nie jesteś przekonany co do tego, że interfejs lUnknown rzeczywiście umożliwia programowanie obiektowe, zastanów się nad podstawowymi założeniami obiektowo zorientowanego systemu programowania.
ActiveX i programowanie zorientowane obiektowo
Jakie są podstawowe założenia każdego systemu obiektowo zorientowanego programowania? Ale uwaga, mam na myśli konkretne założenia, a nie cele ogólne, takie jak lepsza opieka nad oprogramowaniem czy użycie specyficznych narzędzi języka, na przykład C++.
Większość osób zgodzi się, że system zorientowany obiektowo wymaga trzech poniższych elementów:
• Kapsułkowanie - Szczegóły implementacji obiektu powinny być prywatne.
• Ponowne wykorzystanie - Obiekt powinien móc łatwo wykorzystać kod należący do innego obiektu. Jeszcze lepiej, jeśli możesz nieco zmodyfikować kod, gdy nie jest dokładnie taki, jakiego potrzebujesz. Nie musisz też zmieniać kodu, który Ci dokładnie odpowiada.
• Polimorfizm - To po prostu określenie na chęć traktowania obiektów różnych klas tak, jakby były obiektami odrębnej, powiązanej z nimi klasy. Przykład, którym lubię się posługiwać, dotyczy sprzedawcy samochodów, który potrzebuje bazy danych. Na początku projektujesz klasę Vehicle (pojazd). Przypuśćmy także, że posiadasz dwa typy obiektów klasy Vehicle, Car (samochód) i Truck (ciężarówka). W głównej bazie danych nie chcesz jednak przechowywać osobnej listy samochodów i osobnej listy ciężarówek. Zamiast tego, polimorfizm umożliwia potraktowanie zarówno obiektów Car, jak i Truck tak, jakby były obiektami klasy Vehicle. Później, gdy to będzie potrzebne, możesz zdecydować o specyficznym typie każdego obiektu.
Zwróć uwagę na fakt, że na powyższej liście nie ma wyprowadzania. Nie ma go, gdyż wyprowadzanie nie jest założeniem, a jedynie sposobem, w jaki C++ (i wiele innych języków) osiąga ponowne wykorzystanie kodu i polimorfizm.
Kapsułkowanie ActiveX
W jaki sposób ActiveX spełnia te założenia? Kapsułkowanie jest proste. Ponieważ jedyną częścią obiektu widoczną na zewnątrz są jego tablice interfejsów, obiekty są ściśle kapsułkowane. Jeśli obiekt zawiera funkcję, której wskaźnik nie występuje w interfejsie, ta funkcja jest prywatna. Być może zwróciłeś uwagę na to, że nie istnieje coś takiego jak dana składowa ActiveX. Jeśli masz jakiekolwiek zmienne, które świat zewnętrzny będzie mógł odczytywać lub zapisywać, musisz dostarczyć do tego specjalne funkcje. Jak więc widać, obiekty ActiveX są ściśle kapsułkowane.
Gdy definiujesz interfejs, tworzysz kontrakt pomiędzy serwerem obiektu a użytkownikami tego obiektu. Kontrakt nie powinien być zmieniany. Dopóki interfejs się nie zmienia, możesz dowoli zmieniać szczegóły implementacji (łącznie z językiem programowania).
Ponowne wykorzystanie kodu w obiektach ActiveX
Ponowne wykorzystanie kodu jest nieco bardziej skomplikowane. Istnieją dwa sposoby, w jakie obiekty ActiveX mogą ponownie wykorzystać kod: pojemniki i agregacja. Pojemniki nie są skomplikowane. Przypuśćmy, że posiadasz obiekt Vehicle i obiekt Car. Nie ma powodu, dla którego obiekt Car nie mógłby zawierać obiektu Vehicle przejmującego na siebie część pracy. Wyobraźmy sobie, że te obiekty posiadają interfejs IVehicle. Interfejs obiektu Car może nie być niczym więcej, niż funkcjami stanowiącymi obwolutę tych samych funkcji w zawieranym obiekcie Vehicle. Pamiętaj, mamy do czynienia ze ścisłym kapsułkowaniem, więc nikt z zewnątrz nie będzie miał nawet pojęcia, że tak się dzieje.
W C++, dla umożliwienia ponownego wykorzystania kodu stosuje się wyprowadzanie klas. Gdy wyprowadzasz z klasy podstawowej, możesz przesłaniać metody. W metodzie przesłaniającej, jako część operacji, możesz wywołać metodę klasy podstawowej. W ActiveX, używanie funkcji nie robiącej nic ponad wywołanie zawartego obiektu, jest podobne do wyprowadzania z zawartego obiektu i zrezygnowania z dostarczenia funkcji przesłaniającej. Jeśli Twoja funkcja zdecyduje się na wykonanie jakiejś pracy przed lub po wywołaniu zawartej klasy, będzie to podobne do wywołania w C++ klasy podstawowej z metody przesłaniającej. Ostatecznie, Twój obiekt może w danej funkcji wykonywać coś zupełnie unikalnego, nie wywołując przy tym odpowiedniej funkcji w zawartej klasie. Jest to podobne do przesłonięcia klasy i dostarczenia funkcji przesłaniającej, która nie wywołuje klasy podstawowej.
Innym sposobem, w jaki obiekty ActiveX mogą ponownie wykorzystywać kod, jest agregacja. Agregacja jest szczególnie użyteczna wtedy, gdy obiekt chce udostępnić cały interfejs innego obiektu, nawet jeśli na temat tego obiektu wie bardzo niewiele. Na przykład, Visual Basic używa agregacji w celu dodania własnych interfejsów użytkownika do obiektów innych producentów.
Zarówno Twój, obiekt jak i ponownie wykorzystywany obiekt muszą wspomagać agregację. Oznacza to, że możesz zechcieć zagregować obiekt, podczas gdy okaże się że nie zapewnia on odpowiedniego wspomagania. Oprócz tego, agregacja nie daje możliwości zmiany udostępnianego interfejsu. Aby móc wspierać agregację, Twój interfejs lUnknown musi odpowiednio współpracować. Wyobraź sobie, że posiadasz obiekt Car agregujący obiekt Vehicle. Nic nie osiągniesz, jeśli po przepytaniu interfejsu IVehicle okaże się, że nie będzie on wiedział nic na temat interfejsu ICar w tym samym obiekcie.
Dlaczego ActiveX nie korzysta z wyprowadzania? Oczywiście, wyprowadzanie stanowi użyteczny sposób ponownego wykorzystania kodu. Z drugiej strony, języki takie jak C++ mają ogromne trudności z ponownym wykorzystaniem kodu w momencie, gdy nie mają do dyspozycji kodu źródłowego. Nawet w przypadku biblioteki DLL potrzebny jest plik nagłówkowy. Jednym z założeń ActiveX jest to, by obiekty były niezależne od języka. Dzięki pojemnikom i agregacji dowolny obiekt może ponownie wykorzystać inny obiekt, bez względu na języki, w jakich oba obiekty były stworzone. Kod źródłowy nie jest w ogóle potrzebny. Możesz napisać obiekt C++ zawierający obiekt Visual Basica. Co zrobisz, jeśli twórca obiektu Visual Basica zdecyduje się na rozpoczęcie pisania w Delphi? Dopóki interfejs pozostanie bez zmian, nikogo nie będzie obchodził język, w jakim stworzono dany obiekt.
Polimorfizm ActiveX
Być może już zgadłeś, w jaki sposób ActiveX osiąga polimorfizm. Przypuśćmy, że masz obiekt Car z interfejsami ICar oraz IVehicle. Teraz wyobraź sobie, że posiadasz obiekt Truck z interfejsem ITruck oraz z interfejsem IVehicle. Oba te obiekty to obiekty Vehicle. Jeśli masz wskaźnik do interfejsu IVehicle, nie dbasz o to, czy to obiekt Car, czy Truck. Później, jeśli będziesz tego potrzebował, możesz zapytać interfejs IVehicle o interfejs, który Cię interesuje. Jeśli zapytasz o ICar, a funkcja Querylnterface zwróci wartość NULL, oznacza to, że masz do czynienia z obiektem Truck (lub jakimś innym obiektem rodzaju Vehicle).
To bardzo sensowna idea. Możesz pisać obiekty polimorficzne z innymi obiektami, nawet jeśli nie masz do dyspozycji ich kodu źródłowego. Oprócz tego, obiekty mogą być polimorficzne z dowolną ilością innych obiektów. Na przykład, obiekty Car i Truck mogą posiadać interfejsy IPersistFile. To standardowy interfejs obiektów, które zapisują swój stan w pliku. Dodając ten interfejs sprawiasz, że obiekty stają się polimorficzne z wszystkimi innymi obiektami udostępniającymi interfejs IPersistFile.
Oczywiście, nie wykorzystasz ponownie kodu tak, jak na to zezwala wyprowadzanie w C++. Udostępnienie interfejsu IPersistFile oznacza konieczność samodzielnego napisania całego kodu (chyba że zawrzesz lub zagregujesz obiekt, który wykona to, czego potrzebujesz).
Zabawa z interfejsami
Każdy interfejs posiada unikalny 128-bitowy identyfikator (IID). Oprócz tego, każdy obiekt także posiada taki 128-bitowy identyfikator (CLSID). Te 128-bitowe liczby noszą nazwę identyfikatorów GUID (Globally Unique Identifier) lub UUID (Universally Unique Identifier). Właśnie te identyfikatory występują w Rejestrze systemowym dla jednoznacznej identyfikacji obiektu i jego interfejsów. Istnieją także sposoby na przechowanie w Rejestrze informacji o bibliotece typów. Biblioteka typów opisuje obiekt i jego interfejsy. Jest to konieczne, ponieważ klient musi wiedzieć jakie funkcje są dostępne w obiektach udostępnianych przez serwer.
Podstawowe obiekty ActiveX przy wyszukiwaniu metod korzystają z tzw. wczesnego łączenia; co oznacza, że musisz zawczasu znać numer pozycji w tablicy interfejsu. Jednak w pewnych przypadkach korzystniej jest używać tzw. późnego łączenia (wyszukiwania funkcji w czasie wykonania). Nie jest to jednak problem, gdyż możesz łatwo zdefiniować interfejs umożliwiający późne łączenie. W rzeczywistości, Microsoft już zdefiniował taki interfejs; nosi on nazwę IDispatch.
Używając interfejsu IDispatch, obiekty mogą udostępniać zewnętrznemu światu właściwości, metody oraz zdarzenia. Stanowi to podstawową technologię kontrolek ActiveX (OCX), skryptów ActiveX (automatyzacji) oraz bazę dla wielu innych technologii.
Właściwości
Dla programu klienta właściwości widoczne są jako zwykłe zmienne. Niektóre właściwości mogą być przeznaczone tylko do odczytu (lub nawet tylko do zapisu). Inne umożliwiają pełny dostęp. Gdy klient odczytuje lub zapisuje właściwość, ActiveX wywołuje odpowiednie funkcje serwera. Te funkcje mogą odwoływać się do rzeczywistej zmiennej, ale mogą też zatwierdzać lub konwertować dane. Dla przykładu, przypuśćmy, że napisałeś obiekt Termostat, który posiada właściwość SetPoint, określającą temperaturę, przy której termostat powinien się włączyć. Oryginalnie obiekt operował stopniami Fahrenheita. Później zdecydowałeś się na wewnętrzne użycie stopni Celsjusza. Jednak publiczny interfejs musi pozostać bez zmian. Żaden problem. Funkcje operujące na właściwościach mogą łatwo dokonać konwersji na stopnie Celsjusza, i odwrotnie. Możesz także dodać nową właściwość akceptującą bezpośrednio temperaturę w stopniach Celsjusza.
Jako właściwości nie możesz użyć dowolnego typu danej. Możesz stosować jedynie określone typy (zwane czasami typami automatyzacji OLE). Do dyspozycji jest jednak większość użytecznych typów, takich jak łańcuchy, obiekty, waluta, data itd.
Metody
Metody to nic więcej niż zwykłe funkcje, które serwer udostępnia zewnętrznemu światu. Oczywiście, ich argumenty i zwracane wartości muszą należeć do aprobowanych typów (podobnie jak w przypadku właściwości). Gdy klient wywołuje metodę, wykonywana jest Twoja funkcja. Funkcje mogą mieć wewnętrzne nazwy takie same jak nazwy metod, ale nie jest to wymagane.
Zdarzenia
Zdarzania to coś więcej niż funkcje występujące w kliencie, wywoływane przez Twój obiekt. Zdarzenia zwykle są wykorzystywane do powiadomienia klienta o tym, że coś nastąpiło. Możesz posiadać zdarzenie sygnalizujące klientowi fakt kliknięcia myszką w obszarze obiektu. Zdarzenia to zwrotne wywołania metod, podlegają więc tym samym ograniczeniom co metody.
Nazwy i numery
Każda właściwość, metoda i zdarzenie (ogólnie noszące nazwę składowych), w interfejsie IDispatch posiada identyfikującą j ą wartość, identyfikator DISPID. Aby odwołać się do składowych, klient używa nazw lub numerów.
Pewne identyfikatory DISPID są zarezerwowane dla powszechnie używanych składowych. Na przykład kolor kontrolki posiada taki zarezerwowany identyfikator. Dzięki temu programy mogą manipulować tymi składowymi, bez konieczności znania ich nazw (które mogą być w różnych językach). Takie składowe noszą nazwę składowych magazynowych (ang. stock members). Kolor kontrolki jest właściwością magazynową.
Istnieje jeszcze jeden rodzaj właściwości dostępnych dla kontrolek ActiveX: właściwości otoczenia (ang. ambient properties). Są to właściwości, które klient (lub jak wolisz, pojemnik) udostępnia kontrolce. Przypuśćmy, że napisałeś kontrolkę ActiveX wyświetlającą na stronie WWW wirujące logo. Chcesz także, by kontrolka miała taki sam kolor tła jak inne elementy strony. Umożliwi ci to właściwość otoczenia koloru tła przeglądarki WWW. Po prostu odczytujesz właściwość otoczenia i używasz jej do narysowania tła.
ActiveX i MFC
Jeśli powyższa dyskusja wydaje ci się bardzo ogólna, masz rację. MFC ukrywa przed tobą większość szczegółów dotyczących ActiveX, więc często nie musisz nawet o nich wiedzieć. Biblioteka MFC dobrze radzi sobie z tworzeniem obiektów obsługujących interfejs IDispatch. Jeśli chcesz stworzyć taki obiekt, uruchom AppWizarda i pamiętaj o włączeniu opcji Automation (automatyzacja - ang. automation - to synonim skryptów ActiveX, co oznacza, że obiekt obsługuje interfejs IDispatch).
Jeśli tworzysz program EXE, możesz dodać IDispatch do obiektu dokumentu. Zwykle jednak będziesz tworzył bibliotekę DLL. W tym przypadku musisz stworzyć obiekt wyprowadzony z klasy CCmdTarget. Ten obiekt posłuży do obsługi IDispatch. W oknie ClassWizarda kliknij na przycisku Add Class w celu dodania nowej klasy. W następnym oknie dialogowym wyprowadź nową klasę z CCmdTarget. Pamiętaj aby włączyć opcję Automation (patrz rysunek 8.1). Zwykle wybierzesz ostatnią opcję i podasz krótką nazwę obiektu. Często możesz jej użyć zamiast pełnego identyfikatora CLSID (ale tylko CLSID zapewni pełną unikalność).
Gdy już będziesz miał obiekt, kliknij na zakładce Automation okna dialogowego ClassWizard. Ta zakładka umożliwia dodanie metod lub właściwości. MFC obsługuje standardowe właściwości (takie jak kolory tła) kontrolek ActiveX. Są to właściwości magazynowe o z góry zdefiniowanych nazwach i składni. Z drugiej strony, w obiekcie ogólnego przeznaczenia takim jak nasz, nie ma właściwości magazynowych. Sam zdefiniujesz wszelkie metody i właściwości, których będziesz potrzebował.
Aby zdefiniować właściwość, wypełnij jej nazwę oraz typ (rysunek 8.3). Jeśli chcesz zamapować właściwość do zmiennej w swoim kodzie, upewnij się, że jest włączona opcja Member Variable, i wpisz nazwę swojej zmiennej w polu Variable Name. Class-Wizard automatycznie doda zmienną do twojej klasy. Jeśli chcesz, możesz w polu Notification Function dodać funkcję, którą MFC wywoła za każdym razem, gdy inny program zmieni twoją właściwość.
Niektóre właściwości nie są mapowane bezpośrednio do zmiennych. Przypuśćmy że oczekujesz właściwości podanej w centymetrach, ale wewnętrznie reprezentujesz ją w pikselach. Możesz się wtedy zdecydować na wybranie opcji Get/Set Methods zamiast opcji Memeber Variable. Dzięki temu będziesz mógł określić funkcję, którą MFC wywoła wtedy, gdy program spróbuje odczytać lub zapisać właściwość. W tych funkcjach możesz podjąć zupełnie dowolne działania. Jeśli potrzebujesz zmiennej, w tym przypadku musisz zdefiniować ją samodzielnie. Jeśli potrzebujesz właściwości tylko do odczytu, nie dostarczaj funkcji zapisu. Jeśli potrzebujesz właściwości tylko do zapisu (choć to brzmi dziwnie), nie dostarczaj funkcji odczytu.
Implementacja MFC funkcji GetIDsOfNames (funkcji konwertującej nazwy na identyfikatory DISPID) korzysta z nazwy właściwości. Jeśli używasz MFC, nie musisz interesować się identyfikatorami DISPID. Po prostu podajesz nazwy, a MFC automatycznie je przypisuje i wstawia odpowiednie identyfikatory.
Dodawanie metod wygląda bardzo podobnie. Okno dialogowe ClassWizarda (rysunek 8.4) umożliwia wybranie nazwy zewnętrznej (używanej w funkcji GetIDsOfNames) oraz nazwy wewnętrznej (nazwy twojej funkcji). Te nazwy mogą być takie same, jeśli chcesz. Oprócz tego wybierasz zwracany typ oraz wszelkie argumenty, wymagane przez metodę (możliwe jest wybranie do 16 argumentów). I tym razem ClassWizard wie o metodach magazynowych, choć są one przydatne jedynie w przypadku kontrolek ActiveX.
Inna powiązana zakładka w oknie ClassWizarda to zakładka ActiveX Events. Jest przydatna jedynie w przypadku kontrolek ActiveX. Możesz na niej definiować zdarzenia, bardzo podobnie jak w przypadku definiowania metod (ale tylko w przypadku gdy tworzysz kontrolkę ActiveX, w przeciwieństwie do zwykłego obiektu ActiveX).
Jak widać, budowanie obiektów IDispatch przy pomocy MFC jest bardzo proste. Możesz zaprząc ClassWizarda do wykonania większości roboty. MFC dostarcza mechanizmy klasy (używane przez ActiveX do stworzenia obiektu) oraz implementację IDispatch opartą na danych, które wprowadzasz w oknach ClassWizarda. Co więcej, kod tworzony przez MFC jest całkiem efektywny. Więcej o implementacji IDispatch w MFC możesz przeczytać w uwagach technicznych MFC (a konkretnie w Technical Note 38 oraz 39).
Aby przekonać się jak proste jest to w MFC, spójrz na listingi 8.1 i 8.2. Widać w nich prosty obiekt IDispatch posiadający właściwość i metodę. Właściwość określa rodzaj dźwięku (domyślnie -1), a metoda powoduje wydanie dźwięku wybranego typu (poprzez wywołanie funkcji MessageBeep ze standardowego API).
Zadziwiające, jak niewiele z tego kodu nie pochodzi od MFC. W konstruktorze dodałem pojedynczą linię, przypisującą zmiennej właściwości wartość -1. Uzupełniłem również funkcję Beep (której szkielet i tak został wcześniej przygotowany przez ClassWizarda). Oprócz tego, musiałem dopisać w pliku nagłówkowym linię DECLARE_OLECREATE, a w pliku CPP linię IMPLEMENT_OLECREATE. I to wszystko. Reszta kodu pochodzi od AppWizarda lub ClassWizarda.
Jeśli odpowiednio zarejestrujesz klasę (patrz listing 8.3), możesz wykorzystać ten obiekt w Visual Basicu lub dowolnym innym języku, który może działać jako sterownik automatyzacji bądź język skryptów ActiveX. Do pisania takich sterowników możesz także wykorzystać Visual C++ i MFC. Poproś ClassWizard-a o dodanie nowej klasy z biblioteki typów i wybierz plik DLL, EXE lub TLB (biblioteka typów) obiektu automatyzacji. ClassWizard stworzy nową klasę dla obiektu automatyzacji i wygeneruje proste funkcje wywołujące funkcję Invoke, w każdym przypadku z odpowiednią liczbą argumentów.
Listing 8.1. Przykład IDispatch
// Dispatcher.cpp : implementation file
//
#include "stdafx.h"
#include "dispatch.h"
#include "Dispatcher.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
//////////////////////////////////////
// CDispatcher
IMPLEMENT_DYNCREATE(CDispatcher, CCmdTarget)
// Add to Class Factory
IMPLEMENT_OLECREATE(CDispatcher,"Beeper", 0x8755aa4, 0xd365,0x11cf, 0xa7, 0xb2, 0x44, 0x45, 0x53,0x54, 0x0, 0x0)
CDispatcher::CDispatcher()
{
EnableAutomation();
m_beepType=-1; // domyślnie
}
CDispatcher::~CDispatcher()
{
}
void CDispatcher::OnFinalRelease()
{
CCmdTarget::OnFinalRelease();
}
BEGIN_MESSAGE_MAP(CDispatcher, CCmdTarget)
//{{AFX_MSG_MAP(CDispatcher)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
BEGIN_DISPATCH_MAP(CDispatcher, CCmdTarget)
//{{AFX_DISPATCH_MAP(CDispatcher)
DISP_PROPERTY(CDispatcher,"BeepType", m_beepType, VT_I4)
DISP_FUNCTION(CDispatcher,"Beep", Beep, VT_EMPTY, VTS_NONE)
//}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()
// {08755AA4-D365-11CF-A7B2-444553540000}
static const IID IID_IDispatcher = { 0x8755aa4, 0xd365, 0x11cf, { 0xa7, 0xb2, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };
BEGIN_INTERFACE_MAP(CDispatcher, CCmdTarget)
INTERFACE_PART(CDispatcher, IID_IDispatcher, Dispatch)
END_INTERFACE_MAP()
//////////////////////////////////////
// CDispatcher message handlers
void CDispatcher::Beep()
{
MessageBeep(m_beepType);
}
Listing 8.2. Nagłówek obiektu dispatch.
// dispatch.h : main header file for the DISPATCH DLL
//
#ifndef __AFXWIN_H__
#error include 'stdafx.h' before including this file for PCH
#endif
#include "resource.h" // main symbols
/////////////////////////////////////////////////////////////////////////////
// CDispatchApp
// See dispatch.cpp for the implementation of this class
//
class CDispatchApp : public CWinApp
{
public:
CDispatchApp();
// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CDispatchApp)
public:
virtual BOOL InitInstance();
//}}AFX_VIRTUAL
//{{AFX_MSG(CDispatchApp)
// NOTE - the ClassWizard will add and remove member functions here.
// DO NOT EDIT what you see in these blocks of generated code !
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
/////////////////////////////////////////////////////////////////////////////
Listing 8.3. Pozycje Rejestru.
REGEDIT4 [HKEY__CLASSES_ROOT\CLSID\{08755AA4-D365-11CF-A7B2 -444553 540000}]
[HKEY“CLASSES_ROOT\CLSID\{08755AA4-D365-11CF-A7B2-444553540000}\
InProcServer32]
@="c:\\mfcblack\\chap08\\dispatch\\debug\\dispatch.dll"
[HKEY_CLASSES_ROOT\CLSID\{08755AA4-D365-11CF-A7B2-444553540000 }\
ProgID]
@="Beeper"
[HKEY_CLASSES_ROOT\Beeper\CLSID]
@="{08755AA4-D365-11CF-A7B2-444553540000}"
Oprócz tego, na listingu 8.4 znajdziesz kod stworzony przez ClassWizarda dla naszej prostej klasy. Zwróć uwagę w jaki sposób szkielet funkcji identyfikuje typy argumentów i wykorzystuje CLSID.
Listing 8.4. Kod wygenerowany przez ClassWizarda.
// Machinę generated IDispatch wrapper // class(es) created with ClassWizard
ttinclude "stdafx.h" tłinclude "dispatch.h"
#ifdef J3EBUG tdefine new DEBUG_NEW
#undef THIS_FILE
static char THIS__FILE[] = _FILE_;
#endif
// IDispatcher properties
long IDispatcher::GetBeepType() {
long result;
GetProperty(0x1, VT_I4, (void*)Łresult); return result;
void IDispatcher::SetBeepType(long propVal) SetProperty(Oxl, VT_I4, propVal);
11 /11111111 /11 /11 /1111111111111H1111 / II IDispatcher operations
void IDispatcher::Beep() InvokeHelper(Ox2, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
Uzbrojony w taki szkielet klasy, nie możesz się oprzeć jej stworzeniu i wykorzystaniu. Listing 8.5 przedstawia prosty program wyświetlający okno dialogowe. Gdy klikniesz na przycisku OK, tworzony jest obiekt automatyzacji, po czym zostaje wywołana jego metoda Beep. Zwróć uwagę na to, że większość kodu została stworzona przez AppWizarda. Jedyne dopisane linie to linie włączające pliki nagłówkowe klasy dispatch oraz kod obsługujący przypadek IDOK w Initlnstance.
Listing 8.5. Sterownik obiektu automatyzacji
// User.cpp : Defines the class behaviors
// for the application.
//
#include "stdafx.h"
#include "User.h"
#include "UserDlg.h"
#include "dispatch.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
//////////////////////////////////////
// CUserApp
BEGIN_MESSAGE_MAP(CUserApp, CWinApp)
//{{AFX_MSG_MAP(CUserApp)
//}}AFX_MSG
ON_COMMAND(ID_HELP, CWinApp::OnHelp)
END_MESSAGE_MAP()
//////////////////////////////////////
// CUserApp construction
CUserApp::CUserApp()
{
// TODO: add construction code here,
// Place all significant initialization in InitInstance
}
//////////////////////////////////////
// The one and only CUserApp object
CUserApp theApp;
//////////////////////////////////////
// CUserApp initialization
BOOL CUserApp::InitInstance()
{
// Initialize OLE libraries
if (!AfxOleInit())
{
AfxMessageBox(IDP_OLE_INIT_FAILED);
return FALSE;
}
#ifdef _AFXDLL
Enable3dControls();
#else
Enable3dControlsStatic();
#endif
// Parse the command line
if (RunEmbedded() || RunAutomated())
{
COleTemplateServer::RegisterAll();
return TRUE;
}
COleObjectFactory::UpdateRegistryAll();
CUserDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
IDispatcher disp;
disp.CreateDispatch("Beeper"); // ProgID
disp.Beep();
}
else if (nResponse == IDCANCEL)
{
// No action
}
return FALSE;
}
MFC i kontrolki ActiveX
Podstawowe kroki wymagane do stworzenia kontrolki ActiveX w MFC są całkiem proste. Pamiętaj o tym, że kontrolka to obiekt posiadający interfejsy wymagane do istnienia wewnątrz przeglądarki WWW, Visual Basica lub okna MFC.
Najpierw należy uruchomić specjalnego kreatora w celu utworzenia podstawowej kontrolki. Kreator automatycznie wygeneruje odpowiedni identyfikator UUID dla kontrolki i przygotuje potrzebne pliki I DL oraz pliki źródłowe. Gdy zbudujesz projekt, otrzymasz ogólną, bezużyteczną (ale działającą) kontrolkę.
Gdy już będziesz miał obiekt, kliknij na zakładce Automation okna dialogowego ClassWizard (rysunek 8.2). Ta zakładka umożliwia dodanie metod lub właściwości. MFC obsługuje standardowe właściwości (takie jak kolor tła) kontrolek ActiveX. Są to właściwości
Aby jakoś sensownie narysować kontrolkę, musisz dodać własny kod. Także do tego służy specjalne narzędzie, ClassWizard, pozwalający na dodanie właściwości, metod oraz zdarzeń. W niektórych przypadkach ClassWizard stworzy cały kod za ciebie. Kiedy indziej, będziesz musiał sam napisać kod, aby osiągnąć pożądany efekt. W każdym przypadku ClassWizard przynajmniej przygotuje pole do działania i zaopiekuje się Twoim plikiem IDL oraz pozycjami Rejestru.
Oczywiście, do obsługi komunikatów, rysowania i innych standardowych zadań będziesz wykorzystywał normalne mechanizmy MFC. To duży plus, chyba że nie znasz MFC. Nie martw się jednak na zapas. Ilość wiedzy o MFC, którą musisz posiadać, aby napisać większość kontrolek, jest minimalna w porównaniu z ilością wiedzy potrzebnej do napisania całej, normalnej aplikacji. Jeśli przeraża Cię skomplikowana architektura widoków i dokumentów MFC, z pewnością odetchniesz z ulgą, gdy zobaczysz jak, niewiele potrzeba do stworzenia działającej kontrolki.
Korzystanie z ClassWizarda
Aby rozpocząć budowę kontrolki, w menu File wybierz polecenie New. Gdy klikniesz na zakładce, pojawi się lista typów projektów (rys. 8.5). W oknie dialogowym kliknij na opcji Create new workspace. W ten sposób stworzysz nowy projekt z własnym plikiem MĄKĘ i przykładowymi plikami źródłowymi. Wybierz projekt MFC ActiveX Control-Wizard. Po prawej stronie okna wpisz nazwę projektu i wskaż kartotekę, która ma go zawierać. Kliknij na przycisku OK aby przejść dalej.
Okaże się, że kreator ControlWizard składa się z dwóch okien, w których możesz wprowadzać informacje (rys. 8.6 i 8.7). Pierwsze okno pozwala na określenie:
• jak wiele kontrolek ma znaleźć się w projekcie,
czy chcesz zastosować licencję czasu wykonania (tzn. czy chcesz zastosować mechanizm wymagający od programistów podania klucza w momencie, gdy chcą zastosować twoją kontrolkę),
czy w plikach źródłowych mają znaleźć się komentarze, czy mają zostać wygenerowane szkieletowe pliki pomocy.
Gdy klikniesz na przycisku Next, pojawi się drugie, ostatnie okno dialogowe. W tym oknie możesz zmodyfikować nazwę każdej z klas, które stworzy dla ciebie kreator, a także zmienić nazwy plików zawierających te klasy (zwykle jednak pozostawisz domyślne nazwy). Możesz także zdecydować:
• czy kontrolka ma się uaktywniać gdy staje się widoczna,
• czy kontrolka może być niewidoczna w czasie działania,
• czy kontrolka ma się pojawiać w programach na liście możliwych do wstawienia obiektów ActiveX (obok innych dokumentów OLE),
• czy kreator ma stworzyć okno dialogowe About,
• czy kontrolka ma pełnić funkcję zwykłego pojemnika zawierającego inne kontrolki.
Możesz także zdecydować się na subclassing standardowej kontrolki Windows (na przykład przycisku). Jest to szczególnie użyteczne, gdy potrzebujesz listy, która ma udostępniać funkcje poprzez właściwości, metody i zdarzenia. Nie musisz wtedy tworzyć specyficznego kodu dla implementacji rysowania takiej klasy.
Samodzielnie tworzony kod
Często zdarza się, że jedynym kodem, jaki wpisujesz bezpośrednio do plików wygenerowanych przez Control Wizarda, jest funkcja OnDraw. Ta funkcja służy do narysowania kontrolki w kontekście urządzenia. Zamiast normalnego kontekstu urządzenia Windows, otrzymuje jednak kontekst urządzenia MFC, czyli klasę CDC.
Czasem możesz chcieć dodać nieco własnego kodu do konstruktora lub destruktora kontrolki, jeśli jednak chodzi o dodawanie właściwości, metod i zdarzeń, użyjesz Class-Wizarda albo do napisania kodu, albo do stworzenia przynajmniej szkieletów odpowiednich funkcji.
Inny kod, jaki możesz dodać do programu kontrolki, to kod obsługujący standardowe komunikaty okienkowe. Kontrolka pełniąca funkcję przycisku może reagować na kliknięcia myszką. Do obsługi takich zdarzeń możesz użyć ClassWizarda, dokładnie tak samo, jak w przypadku zwykłego programu MFC.
Dodawanie właściwości
Właściwości to serce i dusza kontrolek ActiveX. Dodawanie ich przy pomocy ClassWizarda nie mogłoby być prostsze. Otwórz ClassWizarda, kliknij na zakładce Automation, a następnie na przycisku AddProperty (rys. 8.3). W tym momencie możesz wybrać właściwość magazynową lub nazwę nową. Jeśli wybierzesz właściwość magazynową, ClassWizard automatycznie zajmie się jej przechowaniem i dostarczy funkcje powiadamiania, które możesz przesłonić dla wykrycia momentu zmiany wartości właściwości. I tak, gdy zmieni się właściwość magazynowa Text, MFC wywoła metodę OnText-Changed. Możesz odczytać tekst jako BSTR przy pomocy funkcji GetText lub jako CString, przy pomocy InternaIGetText. Musisz jedynie poinformować ClassWizarda, że chcesz użyć tej właściwości magazynowej.
Gdy dodajesz swoją właściwość, masz do wyboru dwa typy. Typ zmienna składowa (member variable) umożliwia definiowanie zmienny składowych odnoszących się do właściwości. Jeśli chcesz, możesz także zdefiniować funkcję wywoływaną w momencie, gdy zmienia się wartość właściwości. ClassWizard automatycznie tworzy szkielet takiej funkcji.
Drugą możliwością jest użycie funkcji składowych get i set. W tym przypadku określasz funkcję składową wywoływaną w momencie, gdy pojemnik ustawia wartość właściwości (funkcja set) oraz drugą funkcję, zwracającą wartość właściwości (funkcja składowa get). Jeśli chcesz otrzymać właściwość tylko do odczytu lub tylko do zapisu, nie musisz określać obu funkcji.
Gdy tworzysz właściwość przy pomocy funkcji składowych, ClassWizard automatycznie tworzy dla ciebie szkielet tych funkcji. Funkcja get zwraca wartość takiego samego typu, jaki ma właściwość. Funkcja set jako argument przyjmuje odpowiednią wartość. To, co zrobisz w tych funkcjach, zależy wyłącznie od ciebie. Jeśli w celu przechowania właściwości potrzebujesz zmiennej, musisz ją samodzielnie zdefiniować.
Domyślnie, właściwości magazynowe są trwałe. Pojemnik zapisuje je podczas zapisywania kontrolki, a MFC ładuje je z powrotem, gdy pojemnik ładuje kontrolkę. Jeśli chcesz, by Twoje właściwości były trwałe, do funkcji DoPropExchange musisz dodać specjalną funkcję PX_. Te funkcje (dla każdego z popularnych typów istnieje osobna funkcja) wymagają podania jako argumentu nazwy właściwości, odpowiadającej jej zmiennej w Twoim kodzie oraz domyślnej wartości. Oczywiście, niektóre właściwości nie muszą być trwałe. W takim przypadku w funkcji DoPropExchange nie musisz niczego modyfikować.
Korzystanie z właściwości otoczenia
Choć możesz ich używać, nie definiujesz ich w swoim kodzie. W przypadku standardowych właściwości otoczenia, MFC dostarcza prostych funkcji, zwracających ich wartości (patrz tabela 8.1). Możesz także uzyskać wartość każdej właściwości otoczenia; służy do tego funkcja GetAmbientProperty. Oczywiście musisz wtedy znać identyfikator DISPID danej właściwości.
Tabela 8.1. Standardowe funkcje zwracające właściwości otoczenia.
Funkcja
AmbientAppearance AmbientBackColor AmbientDisplayName AmbientFont AmbientForeColor AmbientLocaleID
AmbientScale Units AmbientTextAlign AmbientUserMode AmbientUIDead AmbientShowGrabHandles AmbientShowHatching
Dodawanie metod
Gdy zechcesz dodać metodę, na zakładce Automation ClassWizarda kliknij na przycisku AddMethod. Pojawi się okno dialogowe, które możesz wykorzystać do stworzenia metod (rys. 8.4). Każda metoda może posiadać do 16 parametrów.
Jeśli prawidłowo korzystasz z właściwości, niezbyt często będziesz używał metod. Przypuśćmy, że chcesz stworzyć metodę o nazwie Open, służącą do otwarcia pliku. Dlaczego zamiast niej nie miałbyś dostarczyć właściwości FileName? Wtedy, gdy właściwość się zmieni, możesz przeprowadzić otwarcie pliku.
Przeglądarka WWW nie współpracuje bezpośrednio z metodami kontrolek, jednak języki skryptów potrafią je wywoływać. Strony i serwery WWW mogą wykorzystać języki skryptów do zarządzania skomplikowaną zawartością HTML, zawierającą także kontrolki ActiveX.
Dodawanie zdarzeń
Dzięki ClassWizardowi dodawanie zdarzeń jest bardzo łatwe. Najpierw kliknij na zakładce ActiveX Events. Następnie nazwij zdarzenie (lub wybierz nazwę zdarzenia magazynowego). Zdarzenia mogą przyjąć do 15 argumentów, podawanych w dolnej części okna (rys. 8.8).
ClassWizard automatycznie generuje kompletną funkcję odpalającą zdarzenie (nazwa funkcji rozpoczyna się od słowa Fire, na przykład FireClick). Nie musisz robić nic więcej. Gdy chcesz odpalić zdarzenie, po prostu wywołaj funkcję odpalającą. Oczywiście musisz przekazać odpowiednie argumenty, zgodnie z tymi, które określiłeś podczas tworzenia zdarzenia.
Standardowy kod MFC domyślnie odpala kilka magazynowych zdarzeń. I tak, gdy kontrolka wykryje wciśnięcie klawisza (komunikat WM_CHAR), wywołuje funkcję FireKeyPress, chyba że przesłoniłeś komunikat OnChar i nie wywołujesz klasy podstawowej. W tabeli 8.2 znajdziesz listę komunikatów okienkowych konwertowanych przez MFC do zdarzeń.
Tabela 8.2. Zdarzenia odpalane automatycznie
Komunikat Zdarzenie
WM_KEYUP FireKeyUp
WM_KEYDOWN FireKeyDown
WM_CHAR FireKeyPress
WM_?BUTTONDOWN FireMouseDown
WM_?BUTTONUP FireMouseUp
WM_MOUSEMOVE FireClick
Symbol “?" oznacza rodzinę komunikatów
Przeglądarka WWW w rzeczywistości nie interesuje się zdarzeniami pochodzącymi od kontrolek. Mogą jednak wyłapywać je języki skryptów (takie jak YBScript lub Java-Script).
Dodawanie arkuszy właściwości
Choć nie jest to obowiązkowe, wiele kontrolek ActiveX posiada arkusze właściwości. Dzięki nim użytkownicy mogą wyświetlić okno dialogowe i ustawić wszelkie właściwości. Pojemniki implementują to polecenie przy pomocy metody IOleObject::DoVerb, żądając wykonania operacji OLEIVERB_PROPERTIES. Jak zwykle, MFC znacznie ułatwia implementację także i tej cechy kontrolek.
Kreator Control Wizard w sekcji zasobów dotyczącej dialogów umieszcza puste okno dialogowe. Możesz do niego dodać kontrolki (na przykład pola edycji), tak jak do każdego innego dialogu. Aby powiązać kontrolki z odpowiednimi właściwościami ActiveX, użyj zakładki Member Yariables okna ClassWizard. Możesz powiązać kontrolkę ze zmienną lub właściwością - ClassWizard zajmie się resztą. Wypełniając odpowiednie pola w oknie ClassWizarda możesz nawet filtrować wprowadzane wartości. Możesz ograniczyć do określonego zakresu wartość zmiennej całkowitej lub ograniczyć ilość znaków wprowadzanego ciągu.
Oprócz normalnych arkuszy właściwości, możesz dołączyć dodatkowe arkusze dodając je do sekcji BEGIN_PROPPAGEIDS pliku źródłowego. Istnieje kilka standardowych arkuszy właściwości dla elementów takich jak czcionki czy kolory, które możesz dodać bez żadnego wysiłku. W przypadku kolorów użyj CLSID_CColorPropPage, dla obrazków użyj CLSID_CPisturePropPage, zaś dla czcionek - CLSID_CFontPropPage.
Przykład dodania arkusza właściwości koloru znajdziesz w dalszej części rozdziału.
Analiza wygenerowanych plików
Gdy do rozpoczęcia projektu wykorzystujesz Control Wizarda, generuje on trzy ważne pliki CPP. Pierwszy z nich nosi tę samą nazwę co projekt; plik zawiera obiekt wyprowadzony z COleControIModule. Jest to podstawowa klasa reprezentująca kontrolkę ActiveX. Jeśli potrzebujesz jakiegokolwiek kodu, który ma być wykonywany podczas ładowania lub wyładowywania kontrolki, możesz dodać go właśnie w tym pliku.
Ten sam plik zawiera także funkcje zajmujące się samodzielną rejestracją. Zwykle nie ma potrzeby ich modyfikacji, gdyż dobrze wykonują swoje zadanie.
Najważniejszy plik generowany przez MFC ma taką nazwę jak nazwa projektu, lecz z dodanymi literami CTL (na przykład PROJCTL.CPP). Ten plik zawiera obiekt wyprowadzony z COleControl. Większość wprowadzanych zmian, zarówno ręcznych, jak i automatycznych, odnosi się właśnie do tego pliku. W nim znajduje się funkcja OnDraw, a także wszystkie funkcje zdarzeń, właściwości i metod kontrolki.
Ostatnim plikiem źródłowym tworzonym przez MFC jest plik o nazwie projektu, z dodanymi literami PPG (na przykład PROJPPG.CPP). W tym pliku znajdziesz obiekt reprezentujący główną stronę właściwości. Ten obiekt jest wyprowadzony z klasy podstawowej COlePropertyPage. Zwykle nie trzeba modyfikować tego pliku, chyba że poprzez ClassWizarda.
Testowanie i wykorzystanie kontrolki
Możesz osadzić kontrolkę MFC w dowolnym, odpowiednim pojemniku, jednak szczególnie wygodne jest użycie testowego pojemnika dostarczanego wraz z innymi narzędziami Visual C++. Ten pojemnik (rys. 8.9) pozwala na osadzenie dowolnej kontrolki. Możesz następnie testować jego właściwości, monitorować zdarzenia i ustawiać warunki, na przykład właściwości otoczenia.
Aby wstawić swoją kontrolkę do pojemnika, musisz uruchomić program TSTCON32.EXE (zwykle znajdziesz go w menu Tools Visuala). Gdy w menu Edit wybierzesz polecenie InsertControl, będziesz mógł używać poleceń w menu Edit, View i Options do testowania i monitorowania kontrolki. W szczególności, możesz użyć okna dziennika zdarzeń (Event Log; znajdziesz je w menu View) do monitorowania zdarzeń odpalanych przez kontrolkę.
Jeśli chcesz przetestować swoją kontrolkę w innych pojemnikach (na przykład w przeglądarce WWW), musisz wstawić kontrolkę przy użyciu metody właściwej dla danego pojemnika. W przypadku przeglądarki WWW musisz przejrzeć swój kod źródłowy aby poznać CLSID kontrolki, po czym napisać skrypt HTML, który będzie z niego korzystał. Możesz też wykorzystać jeden z programów do tworzenia skryptów HTML i automatycznie wstawić swoją kontrolkę z listy (podobnie jak w testowym pojemniku).
Prosta kontrolka
Rysunek 8.9 przedstawia bardzo prostą kontrolkę o nazwie Bull control. Ta uproszczona kontrolka przypomina nieco tarczę strzelniczą. Poprzez właściwości możesz sterować kolorami i rozmiarem koncentrycznych kół. Możesz także wykryć (monitorując zdarzenia), w którym momencie użytkownik kliknął na kontrolce.
Jak na kontrolkę, ta jest bardzo uproszczona. Zawiera trzy właściwości i jedno zdarzenie. Oprócz tego wykorzystuje właściwości otoczenia - kolor tła - w celu wypełnienia tła tarczy. Jedyna część, która wymagała jakiegokolwiek znaczącego kodu, to funkcja OnDraw (patrz listing 8.6).
Listing 8.6. Kontrolka Buli control
// BullCtl.cpp : Implementation of the CBullCtrl OLE control class.
#include "stdafx.h"
#include "bull.h"
#include "BullCtl.h"
#include "BullPpg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
IMPLEMENT_DYNCREATE(CBullCtrl, COleControl)
/////////////////////////////////////////////////////////////////////////////
// Message map
BEGIN_MESSAGE_MAP(CBullCtrl, COleControl)
//{{AFX_MSG_MAP(CBullCtrl)
ON_WM_LBUTTONDOWN()
//}}AFX_MSG_MAP
ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// Dispatch map
BEGIN_DISPATCH_MAP(CBullCtrl, COleControl)
//{{AFX_DISPATCH_MAP(CBullCtrl)
DISP_PROPERTY_NOTIFY(CBullCtrl, "Step", m_step, OnStepChanged, VT_I2)
DISP_STOCKPROP_FORECOLOR()
DISP_STOCKPROP_BACKCOLOR()
//}}AFX_DISPATCH_MAP
DISP_FUNCTION_ID(CBullCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()
/////////////////////////////////////////////////////////////////////////////
// Event map
BEGIN_EVENT_MAP(CBullCtrl, COleControl)
//{{AFX_EVENT_MAP(CBullCtrl)
EVENT_STOCK_CLICK()
//}}AFX_EVENT_MAP
END_EVENT_MAP()
/////////////////////////////////////////////////////////////////////////////
// Property pages
// TODO: Add more property pages as needed. Remember to increase the count!
BEGIN_PROPPAGEIDS(CBullCtrl, 2)
PROPPAGEID(CBullPropPage::guid)
PROPPAGEID(CLSID_CColorPropPage)
END_PROPPAGEIDS(CBullCtrl)
/////////////////////////////////////////////////////////////////////////////
// Initialize class factory and guid
IMPLEMENT_OLECREATE_EX(CBullCtrl, "BULL.BullCtrl.1", 0x59e8a903, 0xe0c6, 0x11cf, 0xa7, 0xb2, 0x44, 0x45, 0x53, 0x54, 0, 0)
/////////////////////////////////////////////////////////////////////////////
// Type library ID and version
IMPLEMENT_OLETYPELIB(CBullCtrl, _tlid, _wVerMajor, _wVerMinor)
/////////////////////////////////////////////////////////////////////////////
// Interface IDs
const IID BASED_CODE IID_DBull = { 0x59e8a901, 0xe0c6, 0x11cf, { 0xa7, 0xb2, 0x44, 0x45, 0x53, 0x54, 0, 0 } };
const IID BASED_CODE IID_DBullEvents = { 0x59e8a902, 0xe0c6, 0x11cf, { 0xa7, 0xb2, 0x44, 0x45, 0x53, 0x54, 0, 0 } };
/////////////////////////////////////////////////////////////////////////////
// Control type information
static const DWORD BASED_CODE _dwBullOleMisc =
OLEMISC_ACTIVATEWHENVISIBLE |
OLEMISC_SETCLIENTSITEFIRST |
OLEMISC_INSIDEOUT |
OLEMISC_CANTLINKINSIDE |
OLEMISC_RECOMPOSEONRESIZE;
IMPLEMENT_OLECTLTYPE(CBullCtrl, IDS_BULL, _dwBullOleMisc)
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl::CBullCtrlFactory::UpdateRegistry -
// Adds or removes system registry entries for CBullCtrl
BOOL CBullCtrl::CBullCtrlFactory::UpdateRegistry(BOOL bRegister)
{
// TODO: Verify that your control follows apartment-model threading rules.
// Refer to MFC TechNote 64 for more information.
// If your control does not conform to the apartment-model rules, then
// you must modify the code below, changing the 6th parameter from
// afxRegApartmentThreading to 0.
if (bRegister)
return AfxOleRegisterControlClass(
AfxGetInstanceHandle(),
m_clsid,
m_lpszProgID,
IDS_BULL,
IDB_BULL,
afxRegApartmentThreading,
_dwBullOleMisc,
_tlid,
_wVerMajor,
_wVerMinor);
else
return AfxOleUnregisterClass(m_clsid, m_lpszProgID);
}
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl::CBullCtrl - Constructor
CBullCtrl::CBullCtrl()
{
InitializeIIDs(&IID_DBull, &IID_DBullEvents);
// TODO: Initialize your control's instance data here.
}
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl::~CBullCtrl - Destructor
CBullCtrl::~CBullCtrl()
{
// TODO: Cleanup your control's instance data here.
}
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl::OnDraw - Drawing function
void CBullCtrl::OnDraw(
CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
COLORREF fore=TranslateColor(GetForeColor());
COLORREF back=TranslateColor(GetBackColor());
COLORREF backgrnd=TranslateColor(AmbientBackColor());
CBrush br1(fore);
CBrush br2(back);
CBrush *old;
CRect r=rcBounds;
int step=min(r.Width(),r.Height())/m_step;
pdc->SetBkColor(backgrnd);
pdc->ExtTextOut(0,0,ETO_OPAQUE,&r,"",NULL);
old=pdc->SelectObject(&br1);
pdc->Ellipse(&r);
pdc->SelectObject(&br2);
r.InflateRect(-step,-step);
pdc->Ellipse(&r);
pdc->SelectObject(&br1);
r.InflateRect(-step,-step);
pdc->Ellipse(&r);
pdc->SelectObject(old);
}
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl::DoPropExchange - Persistence support
void CBullCtrl::DoPropExchange(CPropExchange* pPX)
{
ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
COleControl::DoPropExchange(pPX);
PX_Short(pPX,"Step",m_step,8);
}
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl::OnResetState - Reset control to default state
void CBullCtrl::OnResetState()
{
COleControl::OnResetState(); // Resets defaults found in DoPropExchange
// TODO: Reset any other control state here.
}
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl::AboutBox - Display an "About" box to the user
void CBullCtrl::AboutBox()
{
CDialog dlgAbout(IDD_ABOUTBOX_BULL);
dlgAbout.DoModal();
}
/////////////////////////////////////////////////////////////////////////////
// CBullCtrl message handlers
void CBullCtrl::OnStepChanged()
{
InvalidateRect(NULL);
SetModifiedFlag();
}
void CBullCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
FireClick();
// actually, base class fires this event for you...
//COleControl::OnLButtonDown(nFlags, point);
}
Funkcja OnDraw musi respektować właściwości koloru występujące w kontrolce. Ponieważ kolor elementu (foreground) oraz kolor tła (background) to właściwości magazynowe, w celu ich pobrania kod wywołuje funkcje GetForeColor oraz GetBack-Color. Te funkcje zwracają wartość typu OLE_COLOR, podobnego, ale nie takiego samego typu, co COLORREF. Choć COLORREF składa się z 32 bitów, wykorzystywane są tylko 24 młodsze bity (po osiem bitów dla czerwieni, zieleni i błękitu). OLE_COLOR wykorzystuje najstarsze 8 bitów do określenia zawieranego koloru. Jeśli najstarszy bajt ma wartość zero, pozostałe 24 bity to COLORREF. W tym wypadku można przekazać wartość RGB jako OLE_COLOR bez żadnej konwersji. Jeśli jednak górne osiem bitów ma wartość, powiedzmy, 0x80, wtedy najmłodszy bajt zawiera indeks koloru systemowego (taki sam, jaki przekazujesz funkcji GetSystemColor). Istnieją jeszcze inne przypadki, ale nie musisz ich koniecznie znać. W celu otrzymania poprawnej wartości RGB możesz wywołać funkcję TranslateColor.
Kontrolka wyrysowuje obszar poza tarczą, używając koloru tła otoczenia. OnDraw uzyskuje go wywołując funkcję AmbientBackColor. Także ta funkcja zwraca wartość typu OLE_COLOR i wymaga zastosowania TranslateColor dla konwersji do wartości RGB.
Pozostały kod to zwykłe programowanie w MFC. Zmienna Step (krok) odpowiada właściwości kontrolki, definiującej rozmiar koncentrycznych okręgów. O ile właściwości magazynowe same o siebie dbają, w wielu przypadkach, aby zapewnić działanie swojej właściwości, będziesz musiał dopisać nieco kodu. Jeśli nie chcesz zachować trwałego stanu swojej właściwości, poza zdefiniowaniem jej w ClassWizardzie, nie musisz robić już nic poza tym. Jeśli jednak chcesz, by właściwość była trwała, do funkcji DoPropExchange musisz dopisać funkcję PX_ (patrz listing 8.6).
DoPropExchange służy do umieszczenia właściwości w trwałym miejscu przechowywania. Istnieje kilka funkcji PX_ dla różnych typów (patrz tabela 8.3). Musisz im dostarczyć kontekst (argument przekazany do DoPropExchange), zewnętrzną nazwę właściwości, odpowiadającą jej wewnętrzną zmienną oraz, opcjonalnie, domyślną wartość właściwości.
Tabela 8.3. Funkcje wymiany PX
_Funkcje
PX_Blob PX_Bool PX_Color PX_Currency PX_Double PX_Float PX_Font PX_Iunknown PX_Long PX_Picture PX_Short PX_String PX_Ulong PX_Ushort PX VBXFontConvert
Dlaczego właściwość Step powinna być trwała? Pojemnik, w którym zostanie użyta, może zechcieć zapisać kontrolkę i załadować ją kiedy indziej. Po załadowaniu kontrolka powinna wyglądać tak samo. Także, jeśli planujesz wykorzystanie kontrolki Buli w przeglądarce WWW, trwałe właściwości są używane do ładowania wartości określonych w pliku HTML. Przyjrzyjmy się poniższemu fragmentowi kodu HTML:
<OBJECT CLASSID=clsid:59e8a903-eOc6-llCF-A7B2-444553540000 WIDTH=100 HEIGHT=100>
Error! Object doesn't exists! <PARAM NAME="Step" VALUE=10> </OBJECT> end
Spróbuj usunąć funkcję PX_ i wykonać powyższy kod. Zauważysz, że kontrolka ignoruje wartość Step do momentu, kiedy odtworzysz funkcję PX_.
Inny fragment kodu związany z właściwością Step wiąże się z powiadamianiem o zmianie jej wartości. Gdy pojemnik zmieni wartość kroku, kontrolka powinna się przerysować. Taki jest cel istnienia funkcji OnStepChanged. Ta prosta funkcja składa się z dwóch linijek: InvalidateRect wymusza przerysowanie kontrolki, a SetModifiedFlag zaznacza kontrolkę jako “brudną" - tzn. kontrolka powinna zostać zapisana.
Kolejnym dobrym pomysłem, jaki należałoby zaimplementować, jest arkusz właściwości. MFC ułatwia także i to. Na początku listingu 8.6 występuje makro BEGIN_ PROPPAGEIDS. Ostatni argument tego makra to ilość stron właściwości zdefiniowanych dla kontrolki. Na początku ilość stron została zdefiniowana jako l, a narzędzia MFC stworzyły szkieletowe okno dialogowe strony arkusza właściwości. Możesz dostosować to okno (przy pomocy edytora zasobów) wstawiając pole edycji, tak by obsługiwało właściwość Step (rys. 8.10). Następnie użyj ClassWizarda, łącząc pole edycji z właściwością (służy do tego zakładka Member Variables). Pamiętaj, by w odpowiednim momencie podać nazwę właściwości ActiveX.
Właściwości koloru są nieco bardziej problematyczne. W jaki sposób dostarczyć wygodny interfejs doboru koloru? Cóż, nie musisz się tym martwić, gdyż MFC już taki interfejs posiada. Po prostu zmień ilość stron w makrze BEGIN_PROPPAGEIDS na dwie i do listy stron arkusza właściwości dodaj linię PROPPAGEID(CLSID_CColorPropPage). W efekcie otrzymasz okno doboru koloru, widoczne na rysunku 8.11.
Oprócz tego, MFC tworzy plik reprezentujący domyślną stronę arkusza właściwości (BULLPPG.CPP; patrz listing 8.7). Mamy w nim do czynienia ze względnie nieskomplikowaną stroną arkusza właściwości MFC, z pewnymi rozszerzeniami związanymi z ActiveX. Zwykle nie będziesz w tym pliku wprowadzał żadnych ręcznych zmian -zakładka Member Variables ClassWizarda w zupełności do tego wystarczy.
Listing 8.7. Klasa arkusza właściwości kontrolki.
// BullPpg.cpp : Implementation of the CBullPropPage property page class.
#include "stdafx.h"
#include "bull.h"
#include "BullPpg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
IMPLEMENT_DYNCREATE(CBullPropPage, COlePropertyPage)
/////////////////////////////////////////////////////////////////////////////
// Message map
BEGIN_MESSAGE_MAP(CBullPropPage, COlePropertyPage)
//{{AFX_MSG_MAP(CBullPropPage)
// NOTE - ClassWizard will add and remove message map entries
// DO NOT EDIT what you see in these blocks of generated code !
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// Initialize class factory and guid
IMPLEMENT_OLECREATE_EX(CBullPropPage, "BULL.BullPropPage.1",0x59e8a904, 0xe0c6, 0x11cf, 0xa7, 0xb2, 0x44, 0x45, 0x53, 0x54, 0, 0)
/////////////////////////////////////////////////////////////////////////////
// CBullPropPage::CBullPropPageFactory::UpdateRegistry -
// Adds or removes system registry entries for CBullPropPage
BOOL CBullPropPage::CBullPropPageFactory::UpdateRegistry(BOOL bRegister)
{
if (bRegister)
return AfxOleRegisterPropertyPageClass(AfxGetInstanceHandle(), m_clsid, IDS_BULL_PPG);
else
return AfxOleUnregisterClass(m_clsid, NULL);
}
/////////////////////////////////////////////////////////////////////////////
// CBullPropPage::CBullPropPage - Constructor
CBullPropPage::CBullPropPage() :
COlePropertyPage(IDD, IDS_BULL_PPG_CAPTION)
{
//{{AFX_DATA_INIT(CBullPropPage)
m_step = 0;
//}}AFX_DATA_INIT
}
/////////////////////////////////////////////////////////////////////////////
// CBullPropPage::DoDataExchange - Moves data between page and properties
void CBullPropPage::DoDataExchange(CDataExchange* pDX)
{
//{{AFX_DATA_MAP(CBullPropPage)
DDP_Text(pDX, IDC_EDIT1, m_step, _T("Step") );
DDX_Text(pDX, IDC_EDIT1, m_step);
DDV_MinMaxInt(pDX, m_step, 1, 10);
//}}AFX_DATA_MAP
DDP_PostProcessing(pDX);
}
/////////////////////////////////////////////////////////////////////////////
// CBullPropPage message handlers
Kolejny plik źródłowy generowany automatycznie przez MFC, reprezentuje kontrolkę jako taką (BULL.CPP; listing 8.8). Także i ten plik zwykle nie wymaga żadnych modyfikacji. Gdy do niego zajrzysz, znajdziesz wywołania służące do rejestracji DLL-a i inne elementy, które musiałbyś umieścić samodzielnie, gdybyś nie korzystał z MFC.
Listing 8.8. Bull.cpp
// bull.cpp : Implementation of CBullApp and DLL registration.
#include "stdafx.h"
#include "bull.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
CBullApp NEAR theApp;
const GUID CDECL BASED_CODE _tlid = { 0x59e8a900, 0xe0c6, 0x11cf, { 0xa7, 0xb2, 0x44, 0x45, 0x53, 0x54, 0, 0 } };
const WORD _wVerMajor = 1;
const WORD _wVerMinor = 0;
////////////////////////////////////////////////////////////////////////////
// CBullApp::InitInstance - DLL initialization
BOOL CBullApp::InitInstance()
{
BOOL bInit = COleControlModule::InitInstance();
if (bInit)
{
// TODO: Add your own module initialization code here.
}
return bInit;
}
////////////////////////////////////////////////////////////////////////////
// CBullApp::ExitInstance - DLL termination
int CBullApp::ExitInstance()
{
// TODO: Add your own module termination code here.
return COleControlModule::ExitInstance();
}
/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
AFX_MANAGE_STATE(_afxModuleAddrThis);
if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
return ResultFromScode(SELFREG_E_TYPELIB);
if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
return ResultFromScode(SELFREG_E_CLASS);
return NOERROR;
}
/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
AFX_MANAGE_STATE(_afxModuleAddrThis);
if (!AfxOleUnregisterTypeLib(_tlid))
return ResultFromScode(SELFREG_E_TYPELIB);
if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))
return ResultFromScode(SELFREG_E_CLASS);
return NOERROR;
}
Użycie kontrolek ActiveX
Zastosowanie kontrolki ActiveX w oknie dialogowym lub oknie CFormView jest dość proste. Oczywiście, musisz wbudować w projekt obsługę ActiveX (jeśli o tym zapomnisz podczas tworzenia projektu, możesz w metodzie Initlnstance dopisać wywołanie AfxEnableControlContainer, a w pliku STDAFX.H włączyć plik nagłówkowy AFXDISP.H).
Zakładając, że w Twoim systemie znajduje się kontrolka, którą chcesz zastosować, możesz ją wskazać wybierając polecenie Project | Add to Project | Components and Controls. Na marginesie, w poprzednich wersjach MFC nazywano to polecenie Component Gallery (galeria komponentów). Spowoduje to wstawienie jednej lub kilku klas C++ reprezentujących kontrolkę (oraz wszelkie pomocnicze obiekty, z których korzysta). W najprostszym przypadku wcale nie będziesz korzystał z tych klas bezpośrednio.
Jeśli używasz okna dialogowego (lub CFormView), możesz przenieść kontrolkę bezpośrednio z palety kontrolek, tak jak każdy inny standardowy element (na przykład przycisk). Gdy kontrolka znajdzie się już w dialogu, możesz użyć ClassWizarda do przechwycenia jej zdarzeń lub połączenia właściwości ze zmiennymi. Możesz także przypisać zmienną składową całej kontrolce, tak by móc odwoływać się bezpośrednio do jej właściwości i metod.
Przykładowy widok formularza, wykorzystujący kontrolkę ActiveX, znajdziesz na listingu 8.9 i na rysunku 8.12. Użyta kontrolka ActiveX to kontrolka kalendarza, pochodząca od Microsoftu. Szkieletowa klasa wygenerowana automatycznie przez Visual C++ została wyprowadzona z CWnd. Plik nagłówkowy zawiera wersje inline funkcji Create, które wywołują CreateControl zamiast standardowego kodu CWnd. Oprócz tego, plik CPP zawiera proste funkcje dla każdej właściwości i metody. Każdej metodzie odpowiada powiązana funkcja C++. Szkieletowa klasa posiada także funkcje do odczytu i ustawienia każdej z właściwości. Zwróć szczególną uwagę na funkcję OnClickCalendarl. Jest to nie tylko funkcja obsługująca zdarzenie kontrolki ActiveX, ale także odwołuje się do jej właściwości (ShowDays).
Listing 8.9. Wykorzystanie kontrolki ActiveX.
// axcalView.cpp : implementation of the CAxcalView class
//
#include "stdafx.h"
#include "axcal.h"
#include "axcalDoc.h"
#include "axcalView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CAxcalView
IMPLEMENT_DYNCREATE(CAxcalView, CFormView)
BEGIN_MESSAGE_MAP(CAxcalView, CFormView)
//{{AFX_MSG_MAP(CAxcalView)
// NOTE - the ClassWizard will add and remove mapping macros here.
// DO NOT EDIT what you see in these blocks of generated code!
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CFormView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CFormView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CFormView::OnFilePrintPreview)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CAxcalView construction/destruction
CAxcalView::CAxcalView()
: CFormView(CAxcalView::IDD)
{
//{{AFX_DATA_INIT(CAxcalView)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
// TODO: add construction code here
}
CAxcalView::~CAxcalView()
{
}
void CAxcalView::DoDataExchange(CDataExchange* pDX)
{
CFormView::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CAxcalView)
DDX_Control(pDX, IDC_CALENDAR1, m_calendar);
//}}AFX_DATA_MAP
}
BOOL CAxcalView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CFormView::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CAxcalView printing
BOOL CAxcalView::OnPreparePrinting(CPrintInfo* pInfo)
{
// default preparation
return DoPreparePrinting(pInfo);
}
void CAxcalView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add extra initialization before printing
}
void CAxcalView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add cleanup after printing
}
void CAxcalView::OnPrint(CDC* pDC, CPrintInfo*)
{
// TODO: add code to print the controls
}
/////////////////////////////////////////////////////////////////////////////
// CAxcalView diagnostics
#ifdef _DEBUG
void CAxcalView::AssertValid() const
{
CFormView::AssertValid();
}
void CAxcalView::Dump(CDumpContext& dc) const
{
CFormView::Dump(dc);
}
CAxcalDoc* CAxcalView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CAxcalDoc)));
return (CAxcalDoc*)m_pDocument;
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CAxcalView message handlers
BEGIN_EVENTSINK_MAP(CAxcalView, CFormView)
//{{AFX_EVENTSINK_MAP(CAxcalView)
ON_EVENT(CAxcalView, IDC_CALENDAR1, -600 /* Click */, OnClickCalendar1, VTS_NONE)
//}}AFX_EVENTSINK_MAP
END_EVENTSINK_MAP()
void CAxcalView::OnClickCalendar1()
{
m_calendar.SetShowDays(!m_calendar.GetShowDays());
}
Wykorzystanie kontrolki w zwykłym oknie CView jest tylko odrobinę bardziej skomplikowane. Tworząc kontrolkę w swoim kodzie użyj funkcji Create, tak jak w przypadku każdego innego okna. Oczywiście, w tym przypadku ClassWizard nie bardzo może Ci pomóc.
Pamiętaj także, że choć wywołanie funkcji Create kontrolki ActiveX wydaje się być takie samo jak w przypadku zwykłego okna CWnd, jednak zawarty w nim kod jest odmienny. W szczególności, kontrolka ActiveX obsługuje jedynie style WS_VISIBLE, WS_DISABLED, WS_BORDER, WS_GROUP oraz WSJTABSTOP.
Podsumowanie
Jeśli Microsoft będzie dążył ku swoim celom (a to zwykle robi), wszystko co robisz, pewnego dnia będzie korzystać z ActiveX. Wszystkie nowe API (takie jak MAPI czy nowa powłoka) korzystają z interfejsów ActiveX. Nie myśl o tym jak o czymś złym. ActiveX przenosi korzyści programowania obiektowego na poziom binarny - jeśli posiadasz narzędzia i umiejętności aby to wykorzystać.
Na szczęście MFC bardzo ułatwia wykorzystanie ActiveX. ClassWizard ukrywa w ten czy inny sposób większość związanych z tym szczegółów — oczywiście, za pewną cenę. Kontrolki ActiveX wykorzystujące MFC wymagają do pracy bibliotek DLL MFC (lub statycznych wersji bibliotek). Może to spowodować znaczny wzrost objętości plików i wydłużony czas ładowania, zwłaszcza w przypadku Internetu. Oczywiście, jeśli użytkownik już posiada odpowiednie DLL-e, wtedy przestaje to być problemem. Inne języki (na przykład Visual Basic) cierpią na podobne bolączki.
Do generowania niewielkich, efektywnych kontrolek ActiveX napisanych w C++ możesz użyć także ATL (Advanced Template Library), ale jest to bardzo trudne. Możesz także wyjść poza bibliotekę MFC, aby otrzymać podobną wydajność. Oczywiście, możesz użyć MFC razem z ATL, ale jaki to ma cel? Otrzymasz trudną do stworzenia kontrolkę ActiveX wykorzystującą bibliotekę DLL MFC.
Jeśli możesz założyć, że użytkownik posiada DLL-e biblioteki MFC (lub możesz je szybko dostarczyć), okazuje się, że MFC to bardzo szybki sposób tworzenia i wykorzystywania kontrolek ActiveX. Czy tak prosty jak Visual Basic? Nie, na pewno nie, ale z drugiej strony, MFC ma o wiele większe możliwości niż Visual Basic.
Choć ten rozdział pomógł Ci rozpocząć życie z ActiveX, powinieneś zdawać sobie sprawę, że ActiveX samo w sobie stanowi pole do rozległych studiów.
Praktyczny przewodnik ActiveX
Tworzenie obiektu MFC z interfejsem IDispatch
Interpretacja identyfikatorów CLSID, PROGID oraz pozycji Rejestru
Tworzenie kontrolek ActiveX
Debuggowanie kontrolek ActiveX
Umożliwianie projektantom VB lub stron WWW inicjalizacji twoich kontrolek ActiveX
Co to jest ATL?
Dodawanie arkuszy właściwości
Używanie kontrolek ActiveX
Obiekty ActiveX to obecnie rzecz najbardziej zbliżona do “programowania z klocków." Używając ActiveX możesz tworzyć elementy nadające się do wielokrotnego wykorzystania w różnych językach, włącznie z Visual C++, Visual Basic, Delphi, PowerBuilder i innymi. Możesz także wykorzystać te komponenty w swoich własnych programach.
Tworzenie obiektu MFC z interfejsem IDispatch
Wszystkie obiekty automatyzacji w MFC (tj. obiekty z interfejsem IDispatch) są wyprowadzone z klasy CCmdTarget (lub klasy, która została wyprowadzona z CCmd Target). Jedyne co musisz zrobić, to włączyć automatyzację podczas tworzenia klasy z pomocą ClassWizarda.
Przy tworzeniu właściwości i metod swojego obiektu możesz użyć powiązanych z ActiveX zakładek okna ClassWizarda. Pamiętaj, obiekty automatyzacji nie generują zdarzeń (obsługują je jedynie kontrolki ActiveX).
Interpretacja identyfikatorów CLSID, PROGID oraz pozycji Rejestru
ActiveX używa identyfikatora CLSID (128-bitowej liczby) dla jednoznacznej identyfikacji różnych elementów, łącznie z obiektami. CLSID to te długie liczby, na które często natrafiasz w Rejestrze, wyglądające mniej więcej tak:
{00000010-0000-0010-8000-OOAA006D2EA4}
Te liczby są generowane automatycznie przez kilka różnych narzędzi (łącznie z AppWizardem i ClassWizardem). Możesz także wygenerować je przy pomocy aplikacji UUIDGEN, programu linii poleceń dostarczanego w pakiecie Visual C++. Te narzędzia zawierają algorytmy, które sprawiają, że prawdopodobieństwo wygenerowania tego samego numeru jest bardzo znikome (pamiętaj, że do wyboru jest 2128 pozycji). Jeśli /Kiedykolwiek miałeś do czynienia z identyfikatorami UUID, widziałeś już także CLSID, ponieważ CLSID i UUID to jedno i to samo.
Choć MFC zwykle modyfikuje Rejestr za ciebie, dobrze jest wiedzieć choć trochę o tym, co wtedy robi. Wszystkie pozycje odnoszące się do ActiveX znajdują się w gałęzi HKEY_CLASSES_ROOT. W tej gałęzi znajdziesz podklucze z krótkimi nazwami klas ActiveX (czyli PROGID). Są to poręczne, choć niekoniecznie unikalne nazwy obiektów. Poniżej danego podklucza znajdziesz podklucz o nazwie CLSID. ten klucz zawiera rzeczywisty identyfikator klasy, odpowiadający identyfikatorowi PROGID.
Następnie możesz przejrzeć identyfikatory CLSID wewnątrz gałęzi HKEY_CLASSES_ ROOT\CLASSES. Pozycja odpowiadająca danemu identyfikatorowi CLSID zawiera bogactwo informacji o obiekcie ActiveX, łącznie z położeniem plików DLL i EXE obsługujących obiekt, identyfikatorem PROGID, numerem wersji oraz różnymi innymi danymi.
Tworzenie kontrolek ActiveX
Jeśli skorzystasz ze specjalnego kreatora, tworzenie kontrolek ActiveX w MFC będzie proste. Musisz odpowiedzieć na kilka prostych pytań, po czym kreator wygeneruje potrzebne pliki. W szczególności, generowany jest obiekt wyprowadzony z klasy COleControl. Cokolwiek narysujesz w metodzie OnDraw tego obiektu, zdecyduje o wyglądzie twojej kontrolki ActiveX.
Oczywiście, kontrolki ActiveX posiadają właściwości, metody i zdarzenia. Wszystkie możesz tworzyć przy pomocy ClassWizarda. ClassWizard umożliwia potraktowanie zwykłych funkcji w Twoim kodzie jako metod widocznych dla zewnętrznych programów. Gdy zewnętrzny program odczytuje lub ustawia właściwości, możesz zamapować te operacje na zmienne w swoim programie lub na parę specjalnych funkcji. Korzyść zużycia funkcji polega na możliwości dokonania konwersji lub zatwierdzenia danych, w momencie gdy zewnętrzny program zechce je odczytać lub zapisać. Zwracane wartości możesz także obliczać na bieżąco. Na koniec, dodanie zdarzenia o nazwie Synchronizuj (na przykład) spowoduje, że kreator stworzy funkcję o nazwie FireSynchronizuj. Możesz wywołać tę funkcję za każdym razem, gdy chcesz zgłosić to zdarzenie zewnętrznemu programowi.
Debuggowanie kontrolek ActiveX
Wygodnym sposobem debuggowania zwykłych kontrolek ActiveX jest użycie testowego pojemnika (TSTCON32). Ten pojemnik umożliwia wstawianie kontrolek, ustawianie właściwości, wywoływanie metod i prowadzenie dziennika zdarzeń. Możesz także manipulować właściwościami otoczenia oraz ładować i zapisywać właściwości do pliku.
Testowy pojemnik jest bardzo pomocny wtedy, gdy chcesz przetestować kontrolkę, ale nie chcesz dla niej pisać specjalnego pojemnika. Oczywiście, możesz także użyć standardowego debuggera Visual C++ aby ustawić punkty przerwań, przeglądać zmienne itd.
Umożliwianie projektantom VB lub stron WWW inicjalizacji twoich kontrolek ActiveX
Często zdarza się, że kontrolka ActiveX prawidłowo zachowuje się w testowym pojemniku podczas ręcznego ustawiania właściwości, ale nie chce działać w przeglądarce WWW, Visual Basicu lub innym pojemniku ogólnego przeznaczenia. Objawy polegają na tym, że właściwości ustawione podczas projektowania kontrolki (lub w pliku HTML) nie są ustawianie podczas działania programu. Nieodmienną przyczyną jest zaniechanie uczynienia właściwości trwałymi. Aby to osiągnąć, musisz wypełnić mapę właściwości kontrolki funkcjami PX_ (patrz tabela 8.3). Mapa właściwości kontrolki jest zawarta wewnątrz funkcji DoPropExchange.
Na czym polega problem? Programy takie jak Visual Basic umożliwiają programiście ustawienie właściwości obiektu. Jednak w czasie wykonania środowisko umieszcza wszystkie założone właściwości w strumieniu i ładuje je do obiektu. To oznacza, że jeśli Twój kod nie wymienia trwałych właściwości, nie może zainicjować ich poprawnymi wartościami. Dodaj funkcje PX_ i kontrolka powinna zacząć działać poprawnie.
Co to jest ATL?
ATL (Advanced Template Library) to zestaw wzorców C++ dostarczanych wraz z pakietem Visual C++. ATL dostarcza narzędzi pomocnych w tworzeniu interfejsów ActiveX od początku. Jeśli zbudujesz właściwe interfejsy, możesz powielić zachowanie dowolnego obiektu ActiveX, nawet rozbudowanej kontrolki. Zaletą ATL jest bardzo niewielki narzut własnego kodu, w związku z czym rozmiar kontrolki zależy głównie od rozmiaru twojego kodu.
Porównaj to z MFC, gdzie każda ze stworzonych kontrolek wymaga całej biblioteki MFC. Oczywiście, jeśli używasz biblioteki DLL i użytkownik już ma odpowiednie DLL-e, nie jest to duży problem. Ale przypuśćmy, że użytkownik musi ściągnąć z Internetu twoją kontrolkę i potężne DLL-e biblioteki MFC. Wtedy kontrolka staje się bardzo duża. Kontrolki ATL nie wymagają żadnych bibliotek (chyba że będziesz używał ich w swoim kodzie).
To oznacza, że w kontrolce ATL nie możesz użyć elementów MFC, chyba że zgodzisz się na konieczność dołączenia całej biblioteki MFC. Jest to oczywiście możliwe, ale raczej nie ma większego sensu. Jeśli już masz zamiar używać MFC, idź dalej i skorzystaj z uproszczonego modelu ActiveX udostępnianego przez MFC. Jeśli potrzebujesz niewielkiej kontrolki, będziesz musiał ją stworzyć korzystając wyłącznie z ATL i własnego kodu.
Dodawanie arkuszy właściwości
Jeśli wygenerowałeś kontrolkę ActiveX przy pomocy kreatora, otrzymałeś także domyślną stronę jej arkusza właściwości. Szablon arkusza znajdziesz w zasobach projektu; możesz go dostosować i wykorzystać ClassWizarda do połączenia elementów arkusza ze zmiennymi, podobnie jak w przypadku zwykłego dialogu.
Możesz także łatwo dodać standardowe strony wyboru koloru i czcionki; wystarczy zmienić ilość stron arkusza właściwości w makrze BEGIN_PROPPAGEIDS oraz dodać do listy stron linię PROPPAGEID(CLSID_CColorPropPage) w przypadku koloru, lub linię PROPPAGEID(CLSID_CFontPropPage) w przypadku czcionki.
Używanie kontrolek ActiveX
MFC pozwala na zaimportowanie kontrolki ActiveX poprzez wstawienie jej do projektu (użyj polecenia Project | Add to Project | Components and Controls). Gdy to uczynisz, do twojego programu zostaną dodane obiekty C++ reprezentujące kontrolkę oraz wszelkie wymagane przez nią obiekty. Te obiekty C++ bezpośrednio składają się na kontrolkę i możesz wstawiać je do szablonów okien dialogowych lub formularzy CFormView.
Szkieletowy obiekt tworzony przez MFC będzie zawierał funkcje odpowiadające każdej z metod. Oprócz tego otrzymasz po parze funkcji odczytujących i ustawiających każdą z właściwości. Możesz obsłużyć zdarzenia odpalane przez kontrolkę mapując je przy pomocy ClassWizarda.
Jeśli chcesz wykorzystać kontrolkę poza szablonem dialogu, po prostu wywołaj funkcję Create szkieletowej klasy, dokładnie tak samo jak zrobiłbyś w przypadku zwykłego okna CWnd. Pamiętaj jednak, że w odróżnieniu od CWnd, ta funkcja Create, w celu stworzenia kontrolki, wywołuje CreateControl.
Rozdział 9 MFC i Internet
Microsoft z wigorem przystąpił do wyścigu o Internet. Praktycznie każdy produkt oferowany przez tego producenta został zaprojektowany z myślą o wykorzystaniu Internetu, a MFC także nie jest wyjątkiem. Choć gniazda MFC i rozszerzenia ISAPI mają swoje zastosowania, do prostego przesyłania danych możesz preferować użycie ActiveX.
W kinie zawsze można łatwo odróżnić dobro od zła. Niestety, w prawdziwym życiu nie jest to takie łatwe. Alfred Nobel czuł się tak winien z powodu wynalezienia dynamitu, że aż ustanowił pokojową nagrodę Nobla. Obawiał się że jego wynalazek doprowadzi do zniszczenia świata. Patrząc wstecz, dostrzegamy, że dynamit w większości przypadków przyniósł więcej pożytku niż szkód. Tak jak w przypadku większości narzędzi, można go używać w dobrych i złych celach.
Istnieje wiele narzędzi, których można używać w dobrym lub złym celu. I tak, nóż ma mnóstwo użytecznych zastosowań, ale źle użyty może zranić lub nawet doprowadzić do tragedii. Telewizja, choć nie jest narzędziem w standardowym tego słowa znaczeniu, także może mieć dobre i złe zastosowania. Telewizja jest wspaniałym środkiem w przypadku edukacji i dostarczania informacji, ale może także być wykorzystana do szerzenia nieprawdy. Gdy telewizja była jeszcze w powijakach, ludzie nie chcieli się zgodzić co do jej ewentualnych skutków; niektórzy uważali, że świat zmieni się na lepszy, inni zaś byli pewni, że telewizja doprowadzi do upadku społeczeństwa. Telewizja zmieniła sposób naszego życia, ale na lepsze czy gorsze? Trudne pytanie, nieprawdaż? Poza tym, dawna telewizja tylko z grubsza przypominała dzisiejszą.
W ciągu kilku krótkich lat Internet zmienił oblicze komputerowego świata. Oczywiście, Sieć istniała już od jakiegoś czasu, ale dopiero ostatnio nastąpiła ogromna eksplozja jej popularności. I to jaka eksplozja! Nawet moja matka surfuje po Sieci. Gdy ogląda się dzisiejsze reklamy, można odnieść wrażenie, że każda firma, a nawet poszczególne produkty, posiadają swoje adresy URL. Z pewnością zastanawiasz się kto pędzi do swojego komputera, aby sprawdzić zawartość strony związanej z maszynką do golenia czy proszkiem do prania.
Niektórzy uważają, że Internet jest czymś złym, pełnym zboczeńców, pedofilów i morderców. Inni sądzą, że dzięki niemu powstanie nowa utopia. Osobiście sądzę że prawda jak zwykle leży pośrodku. Tak jak w przypadku narzędzi, ludzie mogą używać sieci do dobrych lub złych celów. Ogromna większość stron, które widziałem, nie wnosi czegoś szczególnie dobrego czy czegoś bardzo złego - nie jestem pewien co czuję, wiedząc o każdej płycie posiadanej przez kogoś zupełnie obcego. Z drugiej strony, istnieją witryny, które starają się pomóc ludziom w odnalezieniu zaginionych dzieci lub wspomóc ofiary raka. Istnieje także dużo stron, co do których większość osób zgodzi się, że nie posiadają żadnego przeznaczenia.
Czy go kochasz, czy nienawidzisz, istnienie Internetu nie podlega dyskusji. Tak jak wczesna telewizja różniła się od dzisiejszej, tak jutrzejszy Internet będzie różnił się od Internetu znanego dzisiaj. Jednak tak jak przetrwała telewizja (mimo wielu ponurych przewidywań), tak samo przetrwa Internet.
Przez jakiś czas wydawało się, że Microsoft nie jest zainteresowany Internetem, jednak gdy firma już się obudziła, wkroczyła weń z wielkim impetem. Teraz wszystko, co wychodzi z pracowni Microsoftu, w pewnym stopniu jest związane z Siecią - MFC także nie jest wyjątkiem. W najnowszych wersjach MFC znajduje się całkiem spora ilość narzędzi do obsługi Internetu.
Elementarz Internetu
Zanim zagłębimy się w wykorzystaniu Internetu z poziomu MFC, musimy dowiedzieć się co dzieje się pod maską. Choć większość osób wie, jak korzystać z Internetu, programiści muszą wiedzieć także jak on działa, a to już zupełnie coś innego. Na szczęście, opierając się na MFC nie musisz wiedzieć nawet drobnej części z tego, co musiałbyś wiedzieć programując bez tego wsparcia. Jednak i tak musisz choć trochę zdawać sobie sprawę z tego, jak ta cała sieć działa.
TCP/IP
Popularną analogią stosowaną do opisu oprogramowania jest stos warstw. Na górnej warstwie znajduje się użytkownik, a na dolnej - karta sieciowa. Pomiędzy nimi znajduje się wiele warstw wykonujących większość pracy. Korzyść płynąca z tego układu polega na tym, że każda warstwa musi umieć porozumieć się jedynie z warstwą położoną wyżej oraz warstwą położoną niżej. Dlatego warstwie użytkownika jest wszystko jedno, z jakiej karty sieciowej korzystasz.
TCP/IP (Transport Control Protocol / Internet Protocol) to dolne warstwy definiujące sposób porozumiewania się programów internetowych. IP definiuje bardzo niskopoziomowe protokoły adresowania (adres IP często wygląda jak rozdzielona kropkami liczba, na przykład 255.0.0.0). TCP obsługuje pewne konstrukcje wyższego poziomu, takie jak kontrola błędów.
Z punktu widzenia programisty MFC, nie musisz wiedzieć wiele o samym TCP/IP. Jeśli jednak korzystasz z gniazd, powinieneś poznać terminologię TCP/IP stosowaną w dokumentacji dotyczącej gniazd.
Gniazda
Gniazda (ang. socket) powstały w Berkeley jako część systemu operacyjnego Unix. Okazało się jednak, że działają one na tyle dobrze, że stały się pewnego rodzaju standardem. Windows obsługuje gniazda poprzez WinSock. WinSock skompiluje praktycznie każdy poprawnie napisany Unixowy kod dotyczący gniazd. Przez “poprawnie napisany" rozumiem kod, w którym nie zostały zastosowane tzw. “magiczne liczby." Ponieważ wielu programistów Unixa wie, że niedozwolone gniazdo ma numer O, stosują właśnie tę wartość. Zamiast tego powinni stosować wartość INVALID_SOCKET z pliku nagłówkowego (WINSOCK.H lub sysy/sockets.h). W Windows, niedozwolone gniazdo to -l, a nie 0.
Oprócz tych małych niespodzianek, Windows oferuje kilka funkcji obsługi gniazd; ich nazwy rozpoczynają się od liter WSA_. Te funkcje ułatwiają pisanie programów komunikacyjnych dla Windows, ale takie programy nie mogę być przenoszone na inne platformy.
Istnieje kilka rodzajów gniazd. Przy przekazywaniu i odbieraniu danych poprzez sieć, możesz korzystać z gniazd bez połączenia. Nie ma jednak żadnej pewności, że otrzymasz wszystkie wysyłane dane, lub że wszystkie wysyłane przez ciebie dane trafią tam, gdzie powinny. Nie możesz także zakładać, że dane dotrą w tej samej kolejności, w jakiej zostały wysłane. Takie założenie jest wygodne, jeśli sekwencjonowaniem danych, wykrywaniem i usuwaniem błędów zajmują się górne warstwy oprogramowania. Gniazda tego typu są czasem nazywane gniazdami datagramu.
Inne gniazda służą do tworzenia połączenia pomiędzy dwoma komputerami. Takie gniazda zapewniają poprawne dostarczanie danych (oczywiście, kosztem pewnego narzutu). Jeden z komputerów pełni rolę serwera i tworzy gniazdo o numerze portu, na który zgadzają się oba komputery. Popularne serwery (na przykład serwery WWW) korzystają z ogólnie znanych numerów portów, używanych powszechnie. Działanie serwera jest następujące:
• tworzy gniazdo z ogólnie znanym numerem portu,
• czeka na połączenie,
• gdy pojawi się żądanie połączenia, serwer akceptuje je tworząc inne gniazdo (z innym numerem portu) służące do obsługi połączenia.
Użycie innego gniazda pozwala na pozostawienie oryginalnego portu do obsługi kolejnych połączeń. Więcej informacji na ten temat znajdziesz przy okazji wykorzystania MFC do napisania serwera działającego w oparciu o gniazda, w dalszej części tego rozdziału.
Klienci tworzą gniazda z dowolnymi numerami portów. Następnie łączą się z serwerem wykorzystując jego adres i numer portu.
Programiści MFC niezbyt często bezpośrednio korzystają z gniazd, gdyż w bibliotece istnieją użyteczne klasy reprezentujące gniazda. Na najwyższym poziomie te obiekty wyglądają jak obiekty CArchive (te same, z których korzystałeś przy odczycie i zapisie plików).
Protokoły
Istnieje wiele protokołów definiujących standardowe sposoby wykonywania operacji zużyciem gniazda. Gniazda jako takie dostarczają jedynie sposobu na przekazywanie bajtów pomiędzy komputerami. Protokoły pozwalają na wykorzystanie tych bajtów do czegoś pożytecznego. Oto kilka głównych protokołów:
• FTP (File Transfer Protocol) - specjalny protokół zaprojektowany w celu przesyłania plików pomiędzy komputerami.
• Telnet - umożliwia użytkownikowi zdalne załogowanie się i wykorzystanie komputera ze swojego terminala.
• NNTP (Network News Transfer Protocol) - umożliwia skorzystanie z grup dyskusyjnych (na przykład grup dyskusyjnych USENET-u).
• POP3/1MAP - dwa protokoły używane do obsługi poczty elektronicznej. IM AP jest nowszy i ma większe możliwości, ale większość serwerów i klientów poczty w dalszym ciągu wykorzystuje jedynie POP3.
• HTTP (Hypertext Transfer Protocol) - protokół “napędzający" sieć WWW. HTTP umożliwia klientom pobieranie oraz przesyłanie danych do serwera HTTP. Ten protokół jest bardzo elastyczny i rozbudowy walny.
HTTP i URL-e
Ponieważ HTTP to główny element Sieci, powinieneś go dobrze zrozumieć, zanim przystąpisz do tworzenia jakiegokolwiek serwera sieciowego. Ten protokół powinni poznać także zaawansowani projektanci stron WWW.
Idea jest następująca: przeglądarka WWW klienta przy pomocy protokołu HTTP wysuwa swoje żądanie. To żądanie zawiera uniwersalny identyfikator zasobu (URL - Uniwersał Resource Locator), jednoznacznie identyfikujący komputer i dokument. Dokument może być plikiem tekstowym, plikiem binarnym lub stroną WWW (dokumentem HTML). Może także być jakimś programem, od którego klient oczekuje, że zostanie wykonany na serwerze.
Oprócz żądania, klient na kilka sposobów może przesyłać dane do serwera. Na przykład, gdy wypełniasz formularz sieciowy i klikasz na przycisku Wyślij, przesyłasz dane ze swojego komputera do serwera. Istnieją także inne sposoby, w jakie możesz przesłać dane.
Gdy klient żąda uruchomienia programu, zwykle jest to skrypt umieszczony na serwerze (program napisany w VBScript lub JavaScript) lub program CGI (Common Gateway Interface). Programy tego typu mogą przetwarzać dane pochodzące od klienta, tak jak normalne dane wejściowe. Dane wyjściowe programu stają się danymi przekazywanymi do klienta. Choć program CGI możesz napisać w prawie każdym języku, wiele osób używa języków skryptowych, takich jak Perl. Popularne są także C i C++.
URL stanowi klucz do zrozumienia protokołu HTTP. Oto pełny adres URL do mojej witryny WWW:
http: //www.al-williams.com:80/awc/index.html?MAINT=TRUEStID=X10SE
Prawdopodobnie nie przypomina ci spotykanych codziennie adresów URL. Jest tak dlatego, że umieściłem w nim kilka opcjonalnych części, które nie pojawiają się zbyt często. Przyjrzyjmy się każdemu z pól w przykładowym adresie:
• http: - określenie http: mówi, że tworzysz żądanie HTTP. Do innych dozwolonych protokołów należą FTP, TELNET lub FILE (lokalny plik).
• www.al-williams.com - To identyfikator mojego serwera, a konkretnie chodzi o maszynę o nazwie www, w domenie al-williams.com.
• 80 - Osiemdziesiątka po nazwie serwera określa port (numer gniazda) do wykorzystania. Jak się przekonasz, 80 to ogólnie znany numer gniazda, używany przez wszystkie zwykłe serwery HTTP. Z drugiej strony, niektóre serwisy WWW zawierają po kilka serwerów WWW na jednym komputerze; osiągają to przez przypisanie innego numeru portu każdemu z serwerów.
• awc - Jest to nazwa kartoteki, tak jak w przypadku zwykłej ścieżki do pliku. Zwróć uwagę, że ta kartoteka nie musi być podana względem głównej kartoteki komputera. Zamiast tego, serwer WWW zastępuje ją nazwą pewnej określonej kartoteki na dysku. W naszym przypadku, awc to w rzeczywistości kartoteka /users/home/alw/web.
• index.html - W tym przypadku, dokumentem, jaki chcemy załadować, jest in-dex.html.
• MAINT=TRUE&ID=X10SE - Te interesująco wyglądające przypisania ustawiają dwie zmienne, które serwer potrafi odczytać. Jeśli łańcuch żądania zawiera specjalne znaki (na przykład & lub spacja), muszą być one zakodowane przy pomocy znaku procentów i szesnastkowego odpowiednika znaku. Na przykład, jeśli chcesz ustawić zmienną GREETING na “Hello, Al", musisz użyć następującego łańcucha: GREETING=Hello%20Al:
Oczywiście, tak naprawdę potrzebujesz jedynie http://www.al-williams.com/awc/index. html. Z drugiej strony, opcjonalne elementy stają się bardziej ważne, w momencie gdy zechcesz tworzyć programy pobierające dane od klientów. Zwróć uwagę na fakt, że łańcuch żądania nie zawiera danych formularza. Przeglądarka wysyła te dane osobno, nie jako część łańcucha URL.
Innym ważnym elementem protokołu HTTP są nagłówki HTTP. Te nagłówki to zmienne, informujące komputer odbierający o żądaniu przychodzącym wraz z nagłówkiem. Nagłówki informują klienta o typie danych (HTML, GIF, JPEG itd.), dacie ostatniej modyfikacji dokumentu oraz długości danych dołączonego formularza.
ISAPI
Zamiast CG1, niektóre serwery (łącznie z IIS - Internet Information Server Microsoft-u) wolą, abyś umieszczał swoje programy w bibliotekach DLL spełniających specyfikację ISAPI. Jest to dużo efektywniejsze od pisania normalnych programów CGI. Programy ISAPI mogą filtrować dane w momencie wysyłania ich przez serwer, a także tworzyć dane przeznaczone do wysyłania. MFC zawiera pełną obsługę obu form ISAPI.
ActiveX i Java
Dwaj główni gracze Internetu, którym poświęca się najwięcej uwagi, to ActiveX oraz Java. Choć obie technologie często spełniają podobne funkcje, osiągają je w zasadniczo odmienny sposób.
Jak czytałeś w rozdziale 8, ActiveX jest standardem umożliwiającym dowolnemu językowi dostarczenie obiektów dla innych programów. Java to w istocie obiektowo zorientowany język programowania, oparty (w pewnym stopniu) o C++. Przeglądarka lub serwer WWW mogą dostarczyć obiekty pozwalające obiektom ActiveX na współpracę z nimi. Potrafią także zapewnić środowisko wykonania dla programów Java, dzięki czemu one również mogą współpracować z przeglądarkami lub serwerami.
Zarówno Java jak i ActiveX mają mocne i słabe punkty. Do obsługi Javy Microsoft stworzył pakiet Visual J++. Choć program Javy może użyć kodu stworzonego w Visual C++, jednak ginie przy tym dużo mocnych stron Javy.
Jak widzieliśmy w rozdziale 8, MFC zawiera pełną obsługę ActiveX, znacznie ułatwiającą programowanie obiektów ActiveX. Kontrolek ActiveX możesz używać wewnątrz Internet Explorera (i innych przeglądarek, jeśli masz właściwe oprogramowanie). Obiektów ActiveX możesz używać także razem z IIS 3.0 i nowszym. Możesz napisać obiekt ActiveX obsługujący bazę danych na serwerze i umożliwić użytkownikom przeglądanie jej rekordów wewnątrz przeglądarki WWW (w rzeczywistości, Microsoft stworzył już taki komponent działający z IIS).
Gniazda MFC
Najprostszą w użyciu klasą MFC, odnoszącą się do gniazd, jest klasa CSocket. Ta klasa ukrywa wszystkie zagmatwane szczegóły związane z wykorzystaniem gniazd. Nie musisz nic wiedzieć o kolejności bajtów w sieci, rozdzielczości nazw czy rodzinach adresów. Serwer i klient używają klasy CSocket w odmienny sposób (patrz tabela 9.1 i 9.2). Jeśli Twój program ma działać jako klient, musisz jedynie stworzyć obiekt CSocket, a następnie wywołać metodę Connect, podając nazwę serwera i numer portu.
Serwery są nieco bardziej skomplikowane. Najpierw tworzysz gniazdo z wybranym numerem portu. Następnie wywołujesz metodę Listen. Na koniec wywołujesz metodę Accept, oczekując na połączenie. Istnieje jednak pewien haczyk: w metodzie Accept przekazujesz niezainicjowany obiekt CSocket. Właśnie tego obiektu używasz do nawiązania konwersacji z klientem.
Tabela 9.1. Użycie klasy CSocket w przypadku klienta
Krok Kod Opis
1 CSocket socket; Utworzenie pustego, niezainicjowanego gniazda.
2 socket.Create(); Inicjowanie gniazda; użycie dowolnego portu.
3 socket.Connect("host",100); Połączenie z serwerem ,.host" poprzez port 100.
Tabela 9.2. Użycia klasy CSocket w przypadku serwera.
Krok Kod Opis
1 CSocket hostsocket; Utworzenie gniazda do nasłuchu
2 hostsocket.Create(100); Inicjowanie gniazda i użycie portu "100"
3 hostsocket.Listen(); Przygotowanie na połączenie
4 CSocket socket; Utworzenie gniazda do użycia przy połączeniu
5 hostsocket.Accept(socket); Obsługa połączenia przy pomocy nowego gniazda
Do czego potrzebne są dwa gniazda? Odpowiedź leży w sposobie działania większości serwerów. Przyjrzyjmy się serwerowi WWW. Zgodnie z konwencją, takie serwery nasłuchują poprzez port 80. W ten sposób, gdy klient chce się połączyć, wie, że musi używać portu 80. Ale co by się stało, gdyby naprawdę połączył się z portem 80? Pozostali klienci nie mogliby się połączyć do momentu odłączenia pierwszego klienta, co nie jest dobrym pomysłem. Tak więc serwery nasłuchują, używając ogólnie znanych numerów portów (takich jak 80). Gdy klient próbuje połączyć się z portem, serwer po prostu tworzy inny port o losowym numerze i przypisuje mu nowe gniazdo. W ten sposób ogólnie znane gniazda pozostają wolne dla dalszych połączeń.
W tym momencie możesz bezpośrednio użyć pary obiektów CSocket do przekazywania danych tam i z powrotem, pomiędzy dwoma komputerami (używając funkcji składowych Send i Receive). Takie gniazda są pewne. Zakładając że sieć działa, dane umieszczone w jednym gnieździe pojawią się w drugim gnieździe, poprawne i we właściwej kolejności. Możesz także tworzyć gniazda bez połączenia, ale wtedy nie możesz być pewien poprawności danych ani ich właściwej kolejności. Dlaczego miałbyś korzystać z gniazd, które nie są pewne? Być może piszesz program posiadający własny protokół zajmujący się obsługą błędów i kolejnością danych. W tym przypadku, powielanie działań na poziomie gniazd jest marnotrawstwem, i niepewne gniazda będą działały dużo efektywniej.
Choć w tej chwili możesz już bezpośrednio korzystać z gniazd, wszystko, do czego mogą posłużyć, to przekazywanie gołych danych. Co zrobić, gdy zechcesz przesłać obiekt C++ z jednego końca połączenia na drugi? MFC umożliwia serializację obiektów poprzez połączone gniazda. Tak jak w przypadku serializacji do pliku i z pliku, możesz zapisywać obiekty do sieci i odczytywać je z drugiej strony.
CSocket i obiekty CArchive
Sztuczka pozwalająca na przesyłanie obiektów poprzez sieć polega na powiązaniu obiektu CSocket z obiektem CSocketFile. Sam w sobie, ten obiekt nie jest zbyt użyteczny, możesz jednak połączyć go z obiektem CArchive i przy jego pomocy serializować dane. Dokładne kroki znajdziesz w tabeli 9.3.
Tabela 9.3. Łączenie obiektu CSocket z obiektem CArchiy
e1 CSocketFile file(&socket); Tworzy CSocketFile połączony z gniazdem.
2 CArchive input(&file,CArchive::load); Wiąże gniazdo z odczytywanym archiwum.
3 CArchive output(&file,CArchive::store); Wiąże gniazdo z zapisywanym archiwum.
Pamiętaj, obiekty CArchive mogą odczytywać lub zapisywać obiekty, ale nigdy w tym samym momencie. Zupełnie akceptowalne jest jednak stworzenie dwóch obiektów CArchive odwołujących się do tego samego gniazda. Możesz dzięki temu odczytywać obiekty z jednego archiwum i zapisywać je do drugiego.
I jeszcze jedno: w przypadku używania CArchive nie możesz korzystać z niepewnych połączeń. Obiekty CArchive traktują gniazda jak pliki, a pliki są pewne.
Przejdźmy głębiej: CAsyncSocket
Jeśli potrafisz oprogramować gniazdo, być może nie odpowiada ci, że MFC wykonuje za ciebie tak dużo pracy. Możesz wtedy użyć klasy CAsyncSocket (szczegóły znajdziesz w plikach pomocy MFC). Ta klasa (stanowiąca zresztą klasę podstawową klasy CSocket) umożliwia bezpośredni dostęp do gniazd. Jeśli jednak zdecydujesz się na skorzystanie z CAsyncSocket, będziesz musiał wykonać dużo więcej pracy. Na przykład, użycie CAsyncSocket wymaga ułożenia danych w sieciowej kolejności. Nie możesz także użyć takich gniazd z obiektami CArchive. W praktycznie każdym przypadku, zamiast CAsyncSocket użyjesz CSocket.
Znajdą się jednak przypadki, w których lepiej jest użyć CAsyncSocket. W szczególności, CSocket może czasem się zablokować i spowodować, że podczas oczekiwania na połączenie Twój program sprawia wrażenie jakby się zawiesił.
Blokujące wywołania
Jedna z największych różnic między odczytem danych z pliku a odczytem z gniazda polega na potencjalnym opóźnieniu. Gdy czytasz obiekt z pliku, to albo w nim jest, albo go nie ma. Jednak w przypadku gniazda nie możesz być pewien czy dane są dostępne. Jeśli spróbujesz odczytać obiekt, Twój program może wstrzymać działanie do momentu, w którym obiekt stanie się dostępny.
Jak temu zaradzić? Blokujące wywołania możesz umieścić w osobnym wątku. Możesz także, w niektórych przypadkach, użyć specjalnych wirtualnych funkcji.
Na przykład, gdy w gnieździe pojawią się dostępne dane, jest wywoływana funkcja OnReceive. Oczywiście, aby przesłonić tę funkcję, musisz wyprowadzić własną klasę z klasy CSocket. CAsyncSocket także zawiera przesłanialne funkcje dla połączeń, ale nie możesz użyć ich z CSocket. Połączenie CSocket albo się powiedzie, albo nie. Jeśli połączenie się nie powiedzie, do ciebie należy podjęcie kolejnej próby.
Przykład
Teraz, gdy znasz już klasę CSocket i potrafisz połączyć ją z CArchive, zastanówmy się jak można wykorzystać je w prawdziwym programie. Zdecydowałem się na przetestowanie swoich umiejętności pisząc prostą grę (rys. 9.1). W tej grze staje na przeciw siebie dwóch dowódców. W pierwszej fazie gry umieszcza się na planszy jednostki (powiedzmy czołgi bądź wyrzutnie rakiet). Gdy plansza zostanie wypełniona, komputer jednego z graczy zaczyna pełnić rolę serwera, z którym łączy się drugi gracz. (Aby to przetestować, możesz uruchomić obie strony na tym samym komputerze.) Następnie gracze kolejno bombarduj ą planszę używając myszki. Komputer zgłasza wszystkie pudła i trafienia.
Stworzenie klasy zawierającej informacje przesyłane przez sieć jest proste (patrz tabela 9.4). Dla tego prostego pakietu możesz łatwo napisać funkcje serializacji. Choć w tym przypadku nie jest to wymagane, klasy przeznaczone do serializacji możesz wyprowadzić z klasy CObject.
Tabela 9.4. Klas CPacket
cmd Liczba całkowita identyfikująca komendę (CMD_CLICK, CMD_REPLY, CMD_RESIGN)
x Współrzędna
X
(nie
używana z CMD_RESIGN)
y
Współrzędna Y (nie używana z CMD_RESIGN)
data Znak reprezentujący stan komórki o wsp. (x,y) (nie używane z CMD_CLICK ani CMD_RESIGN)
Serialize Funkcja zapisująca pakiety do Carchive
W przypadku naszej gry komunikacja przebiega zgodnie z określonym protokołem. Host (serwer) wysyła pakiet CMD_CLICK. Klient odpowiada pakietem CMD_REPLY. Następnie klient przesyła swój pakiet CMD_CLICK i odbiera pakiet CMD_REPLY. Zamiast polecenia CMD_CLICK program może wysłać polecenie CMD_RESIGN aby zakończyć grę.
Posiadanie pakietów oczywiście nie wystarczy, aby zagrać w grę. Potrzebny jest także interfejs użytkownika. Nie powinno to być specjalnym problemem, ale blokowane wywołania sieciowe mogą wymagać specjalnej uwagi.'
Podstawowy szkielet programu
Program sam w sobie to zwykły program MFC typu dokument/widok, korzystający z klasy CScrollView. Nie ma w nim niczego specjalnego. Większość pracy wykonuje obiekt dokumentu (patrz listing 9.1). Zawiera trójwymiarową tablicę (grid), która w rzeczywistości składa się z dwóch dwuwymiarowych tablic 10 x 10. Pierwsza z nich zawiera planszę lokalnego komputera. Druga tablica przechowuje to, co wiadomo o planszy przeciwnika.
Listing 9.1. Obiekt dokumentu gry Battlefield.
// Doc.cpp : implementation of the CBfieldDoc class
// Battlefield document -- w tym pliku dzieje się większość rzezcy
#include "stdafx.h"
#include "bfield.h"
#include "insert.h"
#include "bsocket.h"
#include "Doc.h"
#include "ConnDlg.h"
#include "HostDlg.h"
#include "packet.h"
#include "mainfrm.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
///////////////////////
// CBfieldDoc
IMPLEMENT_DYNCREATE(CBfieldDoc, CDocument)
BEGIN_MESSAGE_MAP(CBfieldDoc, CDocument)
//{{AFX_MSG_MAP(CBfieldDoc)
ON_COMMAND(IDM_CONNECT, OnConnect)
ON_COMMAND(IDM_HOST, OnHost)
ON_UPDATE_COMMAND_UI(IDM_CONNECT, OnUpdateConnect)
ON_UPDATE_COMMAND_UI(ID_TURN, OnUpdateTurn)
ON_UPDATE_COMMAND_UI(IDM_HOST, OnUpdateHost)
ON_COMMAND(IDM_RESIGN, OnResign)
ON_UPDATE_COMMAND_UI(IDM_RESIGN, OnUpdateResign)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
///////////////////////
// CBfieldDoc construction/destruction
CBfieldDoc::CBfieldDoc()
{
sockfile=NULL;
xmit=NULL;
rcv=NULL;
MyTurn=FALSE;
}
CBfieldDoc::~CBfieldDoc()
{
NetClose();
}
BOOL CBfieldDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
NetClose();
// opróżnij planszę
for (int i=0;i<2;i++)
for (int x=0;x<10;x++)
for (int y=0;y<10;y++)
grid[i][x][y]='.';
mode=0;
placed=0;
HitCt[0]=0;
HitCt[1]=0;
MyTurn=FALSE;
return TRUE;
}
///////////////////////
// Serializacja CBfieldDoc -- nie używane
void CBfieldDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
///////////////////////
// CBfieldDoc diagnostics
#ifdef _DEBUG
void CBfieldDoc::AssertValid() const
{
CDocument::AssertValid();
}
void CBfieldDoc::Dump(CDumpContext& dc) const
{
CDocument::Dump(dc);
}
#endif //_DEBUG
///////////////////////
// CBfieldDoc commands
// Odczytaj stan w oparciu o bieżący tryb
char CBfieldDoc::GetState(int x,int y)
{
return grid[mode][x][y];
}
// Obsługa kliknięć myszy z okna widoku
void CBfieldDoc::OnClick(int x,int y)
{
if (grid[mode][x][y]!='.'||x>9||y>9)
{
AfxMessageBox("Proszę kliknąć w wolnym polu");
return;
}
if (mode==0) // przygotowania
{
int q,step=1;
CInsert dlg;
if (placed==0x1F)
{
AfxMessageBox("Umieściłeś już wszystkie jednostki");
return;
}
// przygotowanie okna dialogowego rozmieszczania
dlg.m_placed=placed;
dlg.m_x=x;
dlg.m_y=y;
// wywołanie dialogu
int res=dlg.DoModal();
if (res==-1) return;
if (res=='.') return; // to się nie powinno zdarzyć
// sprawdzenie czy jednostki nie wychodzą poza krawędzie
if ((dlg.m_Dir==DIR_WEST && x-dlg.m_Len<-1)||
(dlg.m_Dir==DIR_EAST && x+dlg.m_Len>10)||
(dlg.m_Dir==DIR_NORTH && y-dlg.m_Len<-1)||
(dlg.m_Dir==DIR_SOUTH && y+dlg.m_Len>10))
{
AfxMessageBox(
"Jednostka musi się mieścić na planszy");
return;
}
// sprawdzenie czy jednostki nie zachodzą na siebie
if (dlg.m_Dir==DIR_WEST||dlg.m_Dir==DIR_NORTH) step=-1;
if (dlg.m_Dir==DIR_EAST||dlg.m_Dir==DIR_WEST)
{
for (q=0;q<dlg.m_Len;q++)
if (grid[0][x+step*q][y]!='.')
{
AfxMessageBox("Jednostka zachodzi na inną jednostkę!");
return;
}
for (q=0;q<dlg.m_Len;q++)
grid[0][x+step*q][y]=res;
}
else
{
for (q=0;q<dlg.m_Len;q++)
if (grid[0][x][y+step*q]!='.')
{
AfxMessageBox("Jednostka zachodzi na inną jednostkę!");
return;
}
for (q=0;q<dlg.m_Len;q++)
grid[0][x][y+step*q]=res;
}
// zaznacz jako umieszczona i odśwież widok
placed|=dlg.m_Piece;
UpdateAllViews(NULL);
}
else // poza trybem przygotowania -- wypuszczenie pocisku kliknięciem!
{
CPacket pkt;
if (!MyTurn) return; // bez oszustw
// budowanie wychodzącego pakietu
pkt.cmd=CMD_CLICK;
pkt.x=x;
pkt.y=y;
pkt.Serialize(*xmit);
xmit->Flush(); // upewnij się że został wysłany
// czekaj na odpowiedź -- może zostać zablokowany,
// ale zakładając że wszystko w porządku, to nie duży problem
pkt.Serialize(*rcv);
if (pkt.cmd!=CMD_REPLY)
{
AfxMessageBox("Zła odpowiedź");
return;
}
// aktualizowanie planszy
grid[1][pkt.x][pkt.y]=pkt.data;
// zlicznie trafień
if (pkt.data!='X') HitCt[0]++;
// sprawdzanie czy wygrana!
if (HitCt[0]==17)
{
AfxMessageBox("Wygrałeś!");
NetClose();
OnNewDocument();
UpdateAllViews(NULL);
}
UpdateAllViews(NULL);
MyTurn=FALSE;
}
}
int CBfieldDoc::GetMode(void)
{
return mode;
}
// Przejdź to aby połączyć się z hostem
// Host musi już być gotowy
void CBfieldDoc::OnConnect()
{
CConnDlg dlg;
if (dlg.DoModal()==IDCANCEL) return;
mode=-1;
UpdateStatus();
if (!socket.Create())
{
err(socket.GetLastError());
return;
}
if (!socket.Connect(dlg.m_Host,dlg.m_Port))
if (socket.GetLastError()!=WSAECONNREFUSED)
{
mode=0;
err(socket.GetLastError());
socket.Close();
return;
}
else
{
// Host jeszcze nie jest gotowy
mode=0;
AfxMessageBox("Host nie jest dostępny!");
socket.Close();
return;
}
sockfile=new CSocketFile(&socket);
xmit=new CArchive(sockfile,CArchive::store);
rcv=new CArchive(sockfile,CArchive::load);
mode=1;
UpdateStatus();
UpdateAllViews(NULL);
}
//Przejdź tu, aby przygotować hosta
void CBfieldDoc::OnHost()
{
CHostDlg dlg;
if (dlg.DoModal()==IDCANCEL) return;
mode=-1;
UpdateStatus();
if (!hostsocket.Create(dlg.m_Port))
{
err(GetLastError());
return;
}
if (!hostsocket.Listen())
{
err(GetLastError());
return;
}
// czekaj na połączenie lub błą -- blokuje
while (!hostsocket.Accept(socket) &&
!hostsocket.GetLastError());
if (hostsocket.GetLastError())
{
err(hostsocket.GetLastError());
return;
}
hostsocket.Close(); // bez następnych połączeń
sockfile=new CSocketFile(&socket);
xmit=new CArchive(sockfile,CArchive::store);
rcv=new CArchive(sockfile,CArchive::load);
mode=1;
UpdateStatus();
UpdateAllViews(NULL);
MyTurn=TRUE;
AfxMessageBox("Połączony: Twój ruch.");
}
// Włącz menu połączenia
void CBfieldDoc::OnUpdateConnect(CCmdUI* pCmdUI)
{
pCmdUI->Enable(mode==0&&placed==0x1F);
}
// Włącz menu hosta
void CBfieldDoc::OnUpdateHost(CCmdUI* pCmdUI)
{
pCmdUI->Enable(mode==0&&placed==0x1F);
}
// Zrezygnuj z gry
void CBfieldDoc::OnResign()
{
CPacket pkt;
if (AfxMessageBox(
"Czy naprawdę chcesz zrezygnować z gry?",MB_YESNO)==IDNO)
return;
pkt.cmd=CMD_RESIGN;
pkt.Serialize(*xmit);
NetClose();
OnNewDocument();
UpdateAllViews(NULL);
}
// Włącz rezygnację
void CBfieldDoc::OnUpdateResign(CCmdUI* pCmdUI)
{
pCmdUI->Enable(mode==1&&MyTurn);
}
// Procedura obsługi poważnych błędów
void CBfieldDoc::err(int ern)
{
CString s;
s.Format("%x-%x",ern,GetLastError());
AfxMessageBox(s);
}
// Zamknij połączenie sieciowe
void CBfieldDoc::NetClose()
{
if (xmit) delete xmit;
if (rcv) delete rcv;
if (sockfile) delete sockfile;
socket.Close();
sockfile=NULL;
xmit=NULL;
rcv=NULL;
mode=0;
placed=0;
}
void CBfieldDoc::OnCloseDocument()
{
NetClose();
CDocument::OnCloseDocument();
}
// CBattleSocket wywołuje to gdy dane są dostępne
void CBfieldDoc::GoAhead(void)
{
CPacket pkt;
CPacket outgoing;
UpdateStatus();
pkt.Serialize(*rcv); // pobierz pakiet
if (pkt.cmd==CMD_RESIGN) // czy wyjście?
{
AfxMessageBox("Twój przeciwnik zrezygnował");
NetClose();
OnNewDocument();
UpdateAllViews(NULL);
return;
}
if (pkt.cmd!=CMD_CLICK)
{
AfxMessageBox("Nieoczekiwany błąd");
return;
}
// przygotuj odpowiedź
outgoing.data=grid[0][pkt.x][pkt.y];
outgoing.cmd=CMD_REPLY;
outgoing.x=pkt.x;
outgoing.y=pkt.y;
if (outgoing.data=='.') // zamień nasze . na wychodzące X
outgoing.data='X';
else
HitCt[1]++; // Trafił nas!
// wyślij to
outgoing.Serialize(*xmit);
xmit->Flush();
// sprawdź straty
if (HitCt[1]==17)
{
AfxMessageBox("Przegrałeś!");
NetClose();
OnNewDocument();
UpdateAllViews(NULL);
}
MyTurn=TRUE;
CString msg;
if (outgoing.data!='X')
msg.Format("Przeciwnik trafiony: %c. ",outgoing.data);
msg=msg+"Twój ruch.";
AfxMessageBox(msg);
}
BOOL CBfieldDoc::GetTurn()
{
return MyTurn;
}
// Pomocnicza funkcja do aktualizacji paska stanu
void CBfieldDoc::UpdateStatus(void)
{
CMainFrame *fw=(CMainFrame *)AfxGetMainWnd();
fw->UpdateStatus();
}
// Będziemy gotowi gdy wszystkie jednostki będą rozstawione (0x1F)
BOOL CBfieldDoc::GetReady(void)
{
return placed==0x1F;
}
// Aktualizacja panelu paska stanu
void CBfieldDoc::OnUpdateTurn(CCmdUI* pCmdUI)
{
int mode=GetMode();
BOOL turn=GetTurn();
CString s;
if (!mode)
{
if (placed==0x1F)
s="Gotów do połączenia";
else
s="Przygotowywanie...";
}
else
{
if (mode==-1) s="Łączenie...";
if (turn)
s="Twój ruch";
else
s="Oczekiwanie...";
}
CString counts;
counts.Format("(%d/%d)",HitCt[0],HitCt[1]);
s+=counts;
pCmdUI->SetText(s);
pCmdUI->Enable(TRUE);
}
Jedyne funkcje w obiekcie widoku służą do wyrysowania planszy i obsługi kliknięć myszką. Funkcja OnDraw (patrz listing 9.2) pobiera z dokumentu zawartość każdej z komórek. W tej funkcji zawarta jest też logika decydująca o kolorze rysowanych obiektów.
listing 9.2. Obiekt widoku gry Battlefield.
// View.cpp : implementation of the CBfieldView class
//
#include "stdafx.h"
#include "bfield.h"
#include "bsocket.h"
#include "Doc.h"
#include "View.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
///////////////////////
// CBfieldView
IMPLEMENT_DYNCREATE(CBfieldView, CScrollView)
BEGIN_MESSAGE_MAP(CBfieldView, CScrollView)
//{{AFX_MSG_MAP(CBfieldView)
ON_WM_LBUTTONDOWN()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
///////////////////////
// CBfieldView construction/destruction
CBfieldView::CBfieldView()
{
// TODO: add construction code here
}
CBfieldView::~CBfieldView()
{
}
BOOL CBfieldView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CScrollView::PreCreateWindow(cs);
}
///////////////////////
// CBfieldView drawing
void CBfieldView::OnDraw(CDC* pDC)
{
CBfieldDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
TEXTMETRIC tm;
COLORREF clr,curcolor;
pDC->SelectStockObject(ANSI_FIXED_FONT);
pDC->GetTextMetrics(&tm);
pDC->SetBkColor(::GetSysColor(COLOR_WINDOW));
// na podstawie trybu zmień domyślny kolor (przygotowanie/bombardowanie)
clr=pDoc->GetMode()?RGB(0xFF,0,0):RGB(0,0x80,0);
for (int x=0;x<10;x++)
for (int y=0;y<10;y++)
{
char piece[2];
piece[1]='\0';
piece[0]=pDoc->GetState(x,y);
curcolor=clr;
// kolory użytkownika
if (piece[0]=='.') curcolor=RGB(0,0,0);
if (piece[0]=='X') curcolor=RGB(0,0,0xFF);
pDC->SetTextColor(curcolor);
pDC->TextOut(
x*tm.tmAveCharWidth*3+
tm.tmAveCharWidth,
y*tm.tmHeight*3+
tm.tmAveCharWidth,piece);
}
}
void CBfieldView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal;
// TODO: calculate the total size of this view
CDC *dc=GetDC();
TEXTMETRIC tm;
dc->SelectStockObject(ANSI_FIXED_FONT);
dc->GetTextMetrics(&tm);
// trzy znaki na pole plus znak ramki
sizeTotal.cx = 31*tm.tmAveCharWidth;
sizeTotal.cy = 31*tm.tmHeight;
SetScrollSizes(MM_TEXT, sizeTotal);
// Choć ten kod działa, ma problemy z nowymi stylami okien
// GetParentFrame()->RecalcLayout(TRUE);
// ResizeParentToFit();
}
///////////////////////
// CBfieldView diagnostics
#ifdef _DEBUG
void CBfieldView::AssertValid() const
{
CScrollView::AssertValid();
}
void CBfieldView::Dump(CDumpContext& dc) const
{
CScrollView::Dump(dc);
}
CBfieldDoc* CBfieldView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CBfieldDoc)));
return (CBfieldDoc*)m_pDocument;
}
#endif //_DEBUG
///////////////////////
// CBfieldView message handlers
void CBfieldView::OnLButtonDown(UINT nFlags, CPoint point)
{
CDC *dc;
CPoint scrollpos;
int x,y;
CBfieldDoc *doc=GetDocument();
// zamień na X,Y
dc=GetDC();
TEXTMETRIC tm;
dc->SelectStockObject(ANSI_FIXED_FONT);
dc->GetTextMetrics(&tm);
// Obliczenia dla przewijania
scrollpos=GetDeviceScrollPosition();
point+=scrollpos;
x=(point.x-tm.tmAveCharWidth)/(3*tm.tmAveCharWidth);
y=(point.y-tm.tmHeight)/(3*tm.tmHeight);
// przekazanie do dokumentu
doc->OnClick(x,y);
}
Procedura obsługi myszy przelicza współrzędne x i y w odniesieniu do pól planszy 10 x 10. Następnie przekazuje informacje bezpośrednio do dokumentu.
Obiekt dokumentu nie tylko śledzi zawartość planszy, ale zna także bieżący tryb. Tryb O to tryb, w którym gracz przygotowuje planszę. Gdy gra podejmuje próbę połączenia się z przeciwnikiem, program przechodzi do trybu —1. Gdy powiedzie się nawiązanie połączenia, gra jest w toku, a dokument zaznacza to przechodząc do trybu l.
W trybie O dokument śledzi, które jednostki znajdują się już na planszy. Służy to dwóm celom: po pierwsze, nie można dwa razy wstawić tej samej jednostki, a po drugie, nie można się połączyć przed umieszczeniem na planszy wszystkich jednostek.
Gdy jednostki znajdą się na swoich miejscach, możesz zdecydować się na pełnienie roli hosta (serwera) lub na podłączenie się do innego serwera. Pierwszym przypadkiem zajmuje się funkcja OnHost, drugim - funkcja OnConnect. Obie funkcje znajdziesz w listingu 9.1. Jeśli chcesz być hostem, musisz jedynie wybrać numer portu, którego chcesz użyć. Klient z kolei musi podać zarówno numer portu, jak i nazwę hosta. Jeśli chcesz uruchomić obie strony na tym samym komputerze, jako nazwy hosta użyj localhost (lub podaj adres TCP/IP 127.0.0.1).
Gdy host czeka na połączenie, aplikacja może sprawiać wrażenie zawieszonej. Winę za to ponosi kod, ponieważ MFC zgłasza kod błędu, jeśli wywołanie miałoby spowodować blokadę, a program cały czas podejmuje kolejne próby, aż do momentu, w którym wywołanie się powiedzie. Wprowadzenie mechanizmu czasu oczekiwania nie byłoby jednak zbyt skomplikowane. Trochę bardziej skomplikowane byłoby wprowadzenie zdarzenia pochodzącego od timera, które podejmowałoby próby nawiązania połączenia. Jeszcze bardziej skomplikowana mogłaby być obsługa sekwencji połączenia działająca w osobnym wątku. Jednak w przypadku tak prostej gry, zdecydowałem, że chwilowe wstrzymanie działania programu podczas oczekiwania na połączenie, nie jest zbyt dużym kłopotem.
Gdy rozpocznie się właściwa gra, obiekt dokumentu używa zmiennej MyTurn dla określenia, na kogo przypada dany ruch. Pierwszy ruch należy do hosta. Dopóki gracz nie kliknie na pustym polu, nic ciekawego się nie dzieje. Dopiero kliknięcie powoduje przesłanie pakietu do drugiego programu.
Pierwsza wersja stworzonego przeze mnie programu próbowała, w miarę potrzeby odczytywać dane z gniazda. Na nieszczęście, jeśli komputer akurat czekał na połączenie, program sprawiał wrażenie zawieszonego. Korzyścią takiego podejścia była możliwość wykorzystania standardowej klasy CSocket bez jakichkolwiek modyfikacji. Z drugiej strony, zablokowanie programu przez połowę gry było tak irytujące, że zdecydowałem się poprawić to przy pomocy własnej klasy obsługi gniazda.
Własna klasa obsługi gniazda
Aby móc przysłonić funkcję OnReceive, musiałem wyprowadzić własną klasę z klasy CSocket. Ta funkcja jest wywoływana przez obiekt CSocket, w momencie gdy dane stają się dostępne. Dzięki temu, bez opóźnienia ze strony kodu, możesz pobierać dane, mając pewność, że są dostępne i poprawne. ClassWizard nie pomoże ci w stworzeniu klasy pochodzącej od CSocket; musisz zrobić to sam starym sposobem. Funkcję On-Receive (jedyną ważną część klasy CBattleSocket) znajdziesz w listingu 9.3. Zwróć uwagę, że w klasie CBattleSocket starałem się unikać umieszczenia jakiegokolwiek kodu związanego z samą grą. Zamiast tego odszukuję bieżący dokument i wywołuję jego funkcję GoAhead. Właśnie ta funkcja odczytuje dane i przetwarza je.
Listing 9.3. CBattleSocket.
// BattleField Socket
// Proste przesłonięcie klasy CSocket
// Odebrane dane przekazuje funkcji doc->GoAhead
#include "stdafx.h"
#include "bfield.h"
#include "bsocket.h"
#include "mainfrm.h"
#include "doc.h"
void CBattleSocket::OnReceive(int n)
{
if (!n)
{
CMainFrame *frm=(CMainFrame *)
AfxGetMainWnd();
CBfieldDoc *doc=(CBfieldDoc *)
frm->GetActiveDocument();
doc->GoAhead();
}
}
Choć klasa CAsyncSocket udostępnia przesłanialne metody dla innych zdarzeń, CSocket nie oferuje takiego wsparcia. Oprócz tego, CSocket teoretycznie obsługuje komunikaty WM_PAINT, ale nie udało mi się potwierdzić, że to faktycznie działa. Kod źródłowy MFC zawiera kod, z którego jasno wynika, że funkcja jest przekonana, iż rzeczywiście coś robi (zajrzyj do funkcji OnMessagePending w pliku źródłowym MFC, SOCKCORE. CPP). Jeśli jednak wstawisz punkt przerwania na funkcji OnMessagePending, okaże się, że CSocket nigdy jej nie wywołuje.
Obiekt dokumentu posiada dwie zmienne składowe przeznaczone dla gniazd. Pierwsza z nich (socket) obsługuje aktywne połączenie i musi używać klasy CBattleSocket. Drugie gniazdo to hostsocket, a używane jest jedynie do nasłuchiwania żądań. Dzięki temu, można dla niego zastosować zwykłą klasę CSocket. Program nigdy nie wykorzystuje go do odczytu danych, nie ma więc potrzeby stosowania specjalnego kodu.
Inne zagadnienia
Jedyne pozostałe, interesujące zagadnienie, jest związane z wyświetlaniem komunikatów na pasku stanu. Zwykle nie jest to skomplikowane zadanie. Wstawiasz funkcję obsługi ON_UPDATE_COMMAND_UI do mapy komunikatów. W powiązanej z tym funkcji ustawiasz tekst w panelu paska stanu i uaktywniasz go. MFC obsługuje odpowiednie komunikaty tak jak komunikaty, pochodzące od menu, więc funkcję obsługi możesz wstawić do dowolnej mapy komunikatów.
ClassWizard nie stworzy funkcji obsługi dla paska stanu. Możesz go jednak nieco oszukać. Sztuczka polega na uświadomieniu sobie, że ClassWizard jest w stanie stworzyć funkcję obsługi ON_UPDATE_COMMAND_UI dla elementu menu. Stwórz funkcję obsługi dla elementu menu, który jeszcze jej nie ma - na przykład dla IDM_ABOUT. Pamiętaj o wybraniu klasy, która ma obsługiwać komunikat paska stanu. Gdy ClassWizard poprosi o potwierdzenie nazwy funkcji OnUpdateAbout, wybierz coś innego, co lepiej odda Twoje rzeczywiste intencje (na przykład OnUpdateStatus). Na koniec zmodyfikuj klasę zawierającą funkcję obsługi. W mapie komunikatów znajdziesz linię w takiej postaci:
ON_UPDATE_COMMAND_UI(IDM_ABOUT, OnUpdateStatus) W miejsce identyfikatora IDM_ABOUT wstaw identyfikator (ID) swojego paska stanu.
Jedyny problem pojawia się wtedy, gdy gniazdo zostaje zablokowane i zostają wstrzymane komunikaty aktualizacji. Jedno z możliwych rozwiązań polega na ręcznej aktualizacji paska wtedy, gdy spodziewamy się że gniazdo może zostać zablokowane. Tak właśnie robi nasza gra. Klasa głównej ramki zawiera prostą funkcję wymuszającą przemalowanie paska stanu. Jej kod jest następujący:
void CMainFrame::UpdateStatus(void) {
CWnd *sb=GetMessageBar();
sb->SendMessage(WM_IDLEUPDATECMDUI);
sb->UpdateWindow(); }
Kod lokalizuje obiekt CWnd paska stanu i wysyła mu komunikat WM_IDLE-UPDATECMDUI. Oprócz tego wywołuje UpdateWindow. Oczywiście, WM_IDLE-UPDATECMDUI to prywatny komunikat MFC; aby go użyć, trzeba więc dołączyć plik AFXPRIV.H.
Podsumowanie gniazd
Cóż więc można powiedzieć o gniazdach MFC? To zależy. Użycie klasy CSocket jest zdecydowanie prostsze od bezpośredniego wywoływania funkcji WINSOCK.API. W połączeniu z obiektami mechanizmu archiwów MFC, CSocket pozwala na pisanie nawet bardzo rozbudowanych programów sieciowych. Z drugiej strony, CSocket zawodzi, gdy chodzi o asynchroniczne powiadamianie o zdarzeniach. W takich przypadkach musisz skorzystać z klasy CAsyncSocket. Mówiąc szczerze, użycie CAsyncSocket jest nieco prostsze od użycia WINSOCK, ale różnica nie jest zbyt duża. Jeśli posiadasz już doświadczenie w obsłudze gniazd, możesz pominąć CAsyncSocket i programować gniazda bezpośrednio.
Oczywiście, wiele wad klasy CSocket znika w momencie zastosowania osobnych wątków (omówimy je w rozdziale 11). Wykonując w osobnym wątku operacje związane z obsługą gniazda, możesz uniknąć kłopotów związanych z odpytywaniem gniazda lub gubionymi zdarzeniami.
Protokoły wyższego poziomu
Windows udostępnia bibliotekę przeznaczoną do bezpośredniego łączenia się z Internetem, bez używania gniazda. Ta biblioteka nosi nazwę WinInet.
MFC oferuje dwie różne metody wykorzystania WinInet dla bezpośredniego dostępu do Internetu. Istnieje prosty model, przeznaczony do wykorzystania wtedy, gdy chcesz pobrać dane, oraz model bardziej skomplikowany, pozwalający na tworzenie bardziej wymyślnych klientów (na przykład, gdy chcesz wysłać formularz do serwera WWW). Oba modele zaczynają od klasy CInternetSession (patrz tabela 9.5). Ten obiekt reprezentuje Twoje połączenie z Internetem. Używając tego obiektu nie musisz przejmować się łączeniem z dostawcą usług, serwerami proxy ani żadnymi innymi obowiązkowymi szczegółami, wymaganymi do połączenia z Internetem. Cały proces przebiega zgodnie z normalnymi ustawieniami dokonywanymi przez użytkownika.
Tabela 9.5. ClntematSession
.Składowa Opis
QueryOption Zwraca bieżące opcje (takie jak działanie asynchroniczne, rozszerzone błędy, limity czasu i sterowanie podręcznym buforem).
SetOption Ustawia bieżące opcje.
OpenURL Otwiera URL do odczytu.
GetFTPConnection Otwiera połączenie FTP pozwalające na wyliczanie dostępnych plików, odczyt, zapis i wydawanie poleceń dla zdalnego hosta.
GetHTTPConnection Otwiera połączenie HTTP, pozwalające między innymi na przesyłanie danych i odczyt plików.
GetGopherConnection Otwiera połączenie Gopher.
EnableStatusCallback Włącza asynchroniczne powiadamianie.
Close Zamyka sesję.
OnStatusCallback Obsługuje asynchroniczne powiadamianie, jeśli zostało włączone.
W dokumentacji i plikach nagłówkowych występuje ServiceTypeFrom-Handle, choć nie występuje w kodzie źródłowym klasy CinternetSession. Użycie jej wywołuje błąd linkera.
Jeśli chcesz tylko pobrać dane z hosta, możesz wywołać metodę OpenURL obiektu CInternetSession. Metoda przetworzy podany adres URL i zwróci obiekt pliku.
Obiektem pliku zwykle będzie obiekt klasy CStdioFile lub klasy z niej wyprowadzonej. Gdy URL rozpoczyna się od file://, zwróconym obiektem będzie CStdioFile. Jeśli URL rozpoczyna się od http://, MFC zwraca obiekt CHttpFile; transfer FTP zwraca obiekt CInternetFile, a pliki Gophera używają klasy CGopherFile. Każda z tych klas jest podobna do CStdioFile (która z kolei pochodzi od klasy CFile), jednak każda posiada także własne, specjalne wywołania.
Bardziej wymyślny program może zechcieć współpracować z serwerem, zamiast jedynie pobierać od niego dane. Do obsługi takiego współdziałania wymagany jest specjalny obiekt połączenia. W przypadku HTTP takim obiektem jest CHttpConnection. Możesz go stworzyć, wywołując CInternetSession::GetHTTPConnection. W przypadku FTP i Gophera, możesz potem wywołać metodę OpenFile. W przypadku HTTP możesz wywołać OpenReąuest, a po nim SendRequest. W każdym przypadku otrzymasz obiekt wyprowadzony z klasy CStdioFile, tak jak opisano wcześniej. Różnica polega na możliwości manipulowania obiektem w celu współpracy z serwerem. Na przykład, obiekt CHttpFile (patrz tabela 9.6) osiągnięty w ten sposób może modyfikować nagłówki wysyłane do serwera (pomiędzy wywołaniami OpenReąuest a SendRequest wywołując metodę AddReąuestHeaders).
Tabela 9.6. CHttpFile.
Składowa Opis
AddRequestHeaders Dodaje nagłówki do wychodzącego żądania.
SendRequest Wysyła żądanie do serwera.
QueryInfo Odczytuje nagłówki z serwera.
QueryInfoStatusCode Odczytuje kod stanu z serwera.
GetVerb Zwraca metodę transmisji.
GetObject Zwraca nazwę obiektu.
GetFileURL Zwraca pełny URL pliku.
Close Zamyka plik.
Read Odczytuje dane.
ReadString Odczytuje cały łańcuch.
Write Zapisuje dane.
WriteString Zapisuje cały łańcuch.
Program LinkChecker
Jako przykład przeanalizujemy programowi sprawdzającemu zerwane łącza (listing 9.4). W przypadku jedynie sprawdzania łączy, wystarczy tylko odczytać plik HTTP oraz przeanalizować go pod kątem występujących w nim łączy HREF i SRC do innych plików. W praktyce nie robi się tego bez końca, ponieważ musiałbyś stale krążyć pomiędzy stronami WWW (tak jak programy typu Web spider) Zamiast tego, LINKCK analizuje jedynie początkowy plik.
Listing 9.4. Program LinkChecker.
// linkckView.cpp : implementation of the CLinkckView class
//
#include "stdafx.h"
#include "linkck.h"
#include <afxinet.h>
#include "urlinput.h"
#include "linkckDoc.h"
#include "linkckView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
///////////////////////
// CLinkckView
IMPLEMENT_DYNCREATE(CLinkckView, CFormView)
BEGIN_MESSAGE_MAP(CLinkckView, CFormView)
//{{AFX_MSG_MAP(CLinkckView)
ON_BN_CLICKED(IDC_SCAN, OnScan)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, OnFilePrintPreview)
ON_COMMAND(ID_FILE_SCAN, OnScan)
ON_LBN_DBLCLK(IDC_LB, OnScan)
ON_UPDATE_COMMAND_UI(ID_FILE_PRINT, OnUpdateFilePrint)
ON_UPDATE_COMMAND_UI(ID_FILE_PRINT_PREVIEW, OnUpdateFilePrint)
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CFormView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CFormView::OnFilePrint)
END_MESSAGE_MAP()
///////////////////////
// CLinkckView construction/destruction
void CLinkckView::OnFilePrintPreview()
{
AfxGetMainWnd()->ShowWindow(SW_MAXIMIZE);
CFormView::OnFilePrintPreview();
}
CLinkckView::CLinkckView()
: CFormView(CLinkckView::IDD)
{
//{{AFX_DATA_INIT(CLinkckView)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
PrintEnable=FALSE;
}
CLinkckView::~CLinkckView()
{
}
void CLinkckView::DoDataExchange(CDataExchange* pDX)
{
CFormView::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CLinkckView)
DDX_Control(pDX, IDC_LB, m_lb);
//}}AFX_DATA_MAP
}
BOOL CLinkckView::PreCreateWindow(CREATESTRUCT& cs)
{
return CFormView::PreCreateWindow(cs);
}
///////////////////////
// CLinkckView printing
BOOL CLinkckView::OnPreparePrinting(CPrintInfo* pInfo)
{
// default preparation
return DoPreparePrinting(pInfo);
}
void CLinkckView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add extra initialization before printing
}
void CLinkckView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add cleanup after printing
}
// Prosty kod do drukowania wyników na kartce
void CLinkckView::OnPrint(CDC* pDC, CPrintInfo*)
{
int i;
int y=0,texthi;
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
texthi=tm.tmHeight+tm.tmExternalLeading;
for (i=1;i<m_lb.GetCount();i++)
{
CString item;
m_lb.GetText(i,item);
pDC->TabbedTextOut(5,y,item,0,NULL,0);
y+=texthi;
}
}
///////////////////////
// CLinkckView diagnostics
#ifdef _DEBUG
void CLinkckView::AssertValid() const
{
CFormView::AssertValid();
}
void CLinkckView::Dump(CDumpContext& dc) const
{
CFormView::Dump(dc);
}
CLinkckDoc* CLinkckView::GetDocument()
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CLinkckDoc)));
return (CLinkckDoc*)m_pDocument;
}
#endif //_DEBUG
///////////////////////
// CLinkckView message handlers
// Przejrzenie interfejsu użytkownika
void CLinkckView::OnScan()
{
CString URL;
CString err;
int sel=m_lb.GetCurSel();
if (sel==LB_ERR||sel==0||m_lb.GetItemData(sel)==-1)
{
// obsługa po wybraniu złej pozycji z listy
CUrlInput indlg;
if (indlg.DoModal()==IDCANCEL) return;
URL=indlg.m_url;
}
else
{
m_lb.GetText(sel,URL);
}
// wyczyszczenie listy
m_lb.ResetContent();
m_lb.AddString("<<New URL>>");
// przejście do prawdziwej pracy
DoScan(URL);
// uaktywnienie pozycji menu
PrintEnable=TRUE;
}
// Prawdziwa praca zawarta jest w tej funkcji
void CLinkckView::DoScan(CString & URL, BOOL recurse)
{
CString line;
int item;
CInternetSession session("Commando Link Check V1.0");
CStdioFile *f;
try
{
f=session.OpenURL(URL);
}
catch (...)
{
f=NULL;
}
// jeśli nie przejdzie przez linker,
// użyj statycznej biblioteki MFC (patrz tekst)
if (f && f->IsKindOf(RUNTIME_CLASS(CHttpFile)))
// if (f && !strnicmp(URL,"http",4))
{
DWORD stat; // http status
CHttpFile *hf=(CHttpFile *)f;
hf->QueryInfoStatusCode(stat);
// poprawne są jedynie kody 200-299
if (stat<200||stat>299)
{
f->Close();
f=NULL;
}
}
if (!f) // jakiś problem
{
CString err;
err=URL;
err+="\tBłąd!";
item=m_lb.AddString(err);
m_lb.SetItemData(item,-1);
return;
}
// ruszaj!
item=m_lb.AddString(URL);
m_lb.SetItemData(item,0);
// jeśli recurse == true, then analizuj plik
while (recurse && f->ReadString(line))
{
int n=0;
char *p, *e;
p=(char *)(LPCSTR)line;
do {
p=strchr(p,'='); // szukaj '='
if (p) // wyszukiwanie wstecz dla HREF/SRC
{
if (toupper(p[-1])!='F' ||
toupper(p[-2])!='E' ||
toupper(p[-3])!='R' ||
toupper(p[-4])!='H')
if (toupper(p[-1])!='C' ||
toupper(p[-2])!='R' ||
toupper(p[-3])!='S')
{
p++;
continue;
}
if (p[1]=='"') // pomiń cudzysłowy
p++;
e=p+1+strcspn(p+1," \t\n>\"#");
*e='\0';
// pomiń puste URL-e (np. HREF=#anchor)
if (e!=p+1)
{
CString newURL;
ExpandURL(newURL,p+1,URL);
DoScan(newURL,FALSE);
}
*e=' ';
p=e+1;
}
} while (p && *p);
} // end while
f->Close();
}
void CLinkckView::OnInitialUpdate()
{
CFormView::OnInitialUpdate();
GetParentFrame()->RecalcLayout();
ResizeParentToFit(FALSE);
}
void CLinkckView::ExpandURL(CString &result,
CString source,CString current)
{
CString proto,host,path,file,chost,cpath,cproto;
// rozbij oba URL-e
ParseURL(source,&proto,&host,&path,&file);
ParseURL(current,&cproto,&chost,&cpath,NULL);
// skopiuj puste części z bieżącego URL-a
if (proto.IsEmpty()) proto=cproto;
if (host.IsEmpty()) host=chost;
if (path.IsEmpty())
path=cpath;
else if (path[0]!='/'&&path[0]!='\\'&&!cpath.IsEmpty())
path=cpath+"/"+path;
result=proto + host ;
// jeśli ścieżka jest pusta, uzupełnij o ścieżkę bieżącą
if (!path.IsEmpty())
{
if (path[0]!='/' && path[0]!='\\')
result+="/";
result+= path;
}
if (!file.IsEmpty()) result+="/" + file;
}
// Dostępny parser URL, AfxParseURL, jest zbyt leniwy aby
// odróżnić plik od kartoteki, więc zrobimy to sami (ach...)
void CLinkckView::ParseURL(CString url,CString *proto,
CString *host, CString *path,
CString *fileplus)
{
CString _proto, _host, _path, _fileplus;
int n;
n=url.Find("://");
if (n==-1) n=url.Find(":\\\\");
// pobierz protokół
if (n!=-1)
{
_proto=url.Left(n+3);
url=url.Right(url.GetLength()-(n+3));
n=url.FindOneOf("/\\");
if (n==-1)
{ // pobierz hosta
_host=url;
url="";
}
else
{
_host=url.Left(n);
url=url.Right(url.GetLength()-n);
}
}
// znajdź ścieżkę lub nazwę pliku
n=url.ReverseFind('/');
if (n==-1) n=url.ReverseFind('\\');
if (n!=-1)
{
_fileplus=url.Right(url.GetLength()-n-1);
_path=n==-1?url:url.Left(n);
if (!_path.IsEmpty()&&(_path[0]=='/'||
_path[0]=='\\'))
_path=_path.Right(_path.GetLength()-1);
}
else
{
_fileplus=url;
}
// Heurystyka dla specjalnych przypadków
// jeśli występuj nazwa hosta i pliku, ale nie występuje ścieżka, i jeśli nazwa pliku
// nie zawiera kropki, wtedy prawdopodobnie plik jest ścieżką
if (!_host.IsEmpty() && _path.IsEmpty() &&
!_fileplus.IsEmpty() && _fileplus.Find('.')==-1)
{
_path="/" + _fileplus;
_fileplus="";
}
if (proto) *proto=_proto;
if (host) *host=_host;
if (path) *path=_path;
if (fileplus) *fileplus=_fileplus;
}
void CLinkckView::OnEndPrintPreview(CDC* pDC, CPrintInfo* pInfo,
POINT point, CPreviewView* pView)
{
AfxGetMainWnd()->ShowWindow(SW_NORMAL);
CFormView::OnEndPrintPreview(pDC, pInfo, point, pView);
}
void CLinkckView::OnUpdateFilePrint(CCmdUI* pCmdUI)
{
pCmdUI->Enable(PrintEnable);
}
Analiza korzysta z kilku sztuczek i nie jest do końca stabilna. Program bierze pod uwagę każdą linię SRC= lub HREF=, nawet jeśli znajduje się w komentarzu. Oprócz tego, istnieje mnóstwo przypadków specjalnych, na przykład łącza względne, łącza z kotwicami i mi-riady, innych sposobów wyrażenia adresu URL. Próbowałem nawet brać pod uwagę wiodące ukośniki i odwrotne ukośniki oraz chociaż algorytm obsługuje wszystkie przypadki, jakie przyszły mi do głowy, jestem pewien że ktoś znajdzie URL-a, z którym program sobie nie poradzi.
Kluczowymi częściami parsem są funkcje ExpandURL oraz ParseURL. Funkcja ExpandURL używa dwóch URL-i: URL-a, który chcesz rozszerzyć oraz bieżącego URL-a. Następnie dwukrotnie wywołuje ParseURL do rozbicia URL-i na części składowe. Na koniec procedura uzupełnia URL-a wypełniając wszystkie brakujące elementy (jako wzorca używając oryginalnego URL-a). Te funkcje są funkcjami ogólnego przeznaczenia i możesz znaleźć dla nich zastosowanie w swoim własnym kodzie. MFC także udostępnia funkcję AfkParseURL, ale ponieważ format zwracanych przez nią danych mi nie odpowiadał, zdecydowałem się na napisanie własnej wersji.
Funkcja OnScan rozpoczyna działanie po kliknięciu na przycisku Skanuj, poleceniu w menu lub dwukrotnym kliknięciu na pozycji listy. Jeśli lista zawiera pozycję i jej dodatkowa dana wynosi zero, funkcja, jako URL, do skanowania wykorzystuje tę pozycję listy. W przeciwnym razie jest wyświetlane proste okno dialogowe z prośbą o podanie URL-a. Użytkownik musi wpisać kompletny adres URL.
Prawdziwa praca jest wykonywana w funkcji DoScan. Kod otwiera dany URL (używając OpenURL), sprawdza odpowiedź i, jeśli trzeba, analizuje plik szukając łączy. Zwróć uwagę na fakt, że kod zakłada, przeglądasz plik HTML. Jeśli wskazałbyś na przykład plik FTP, DoSkan także próbowałaby znaleźć łącza w tym pliku.
W związku z tym pojawia się inny interesujący problem. Chodzi o sprawdzenie, czy plik rzeczywiście istnieje. Załóżmy, że żądasz odczytu pliku na jakimś serwerze, ale plik nie istnieje. Możesz oczekiwać jakiegoś błędu czy wyjątku, prawda? Otóż nie. Funkcja OpenURL poda, że wszystko się powiodło, ale zawartością CHttpFHe będzie komunikat błędu pochodzący od serwera! Aby upewnić się co do rezultatu, musisz sprawdzić kod poworotu. Zwykłe kody zawierają się w przedziale od 200 do 299. Brakujący plik zwykle powoduje zwrócenie kodu 404.
To powinno być proste, prawda? CHttpFile posiada nawet metodę o nazwie Querylnfo-StatusCode zwracającą kod powrotu. Problem polega na tym, że OpenURL zwraca wskaźnik do obiektu CStdioFile, który może być (lub nie) wskaźnikiem do obiektu CHttpFile. W tym przypadku rzutowanie jest niebezpieczne, ponieważ nie masz żadnej pewności co do typu obiektu rzeczywiście zwracanego przez OpenURL.
To doskonały moment na użycie funkcji IsKindOf. Jako że klasa CFile jest wyprowadzona z klasy CObject, dlaczego nie miałbyś użyć IsKindOf do wykrycia CHttpFile? W teorii brzmi to dobrze, ale w praktyce okazuje się, że w dynamicznych bibliotekach MFC Microsoft zapomniał wyeksportować informacje o klasie CHttpFile. Jeśli więc napiszesz:
if(f && f->IsKindOf(RUNTIME_CLASS(CHttpFile)) . . .
kod nawet nie zostanie połączony z kodem MFC w postaci bibliotek DLL. Gdy przełączysz się na statyczną bibliotekę (w menu Project | Settings), wszystko zadziała poprawnie.
Zostawiłem kod taki jak jest, zakładając, że będziesz linkował go ze statyczną wersją biblioteki. Jeśli jednak się upierasz, możesz użyć kodu oznaczonego jako komentarz. Jedyny sposób uzyskania obiektu CHttpFile wymaga użycia URL-a rozpoczynającego się od “http" zapasowy kod wygląda mniej więcej tak:
if(f && !strnicmp(URL,"http",4))
W praktyce ta linia kodu daje taki sam efekt, jak położona powyżej linia używająca IsKindOf.
Poza tym program nie jest zbyt interesujący. DoScan dodaje do listy znalezione URL-e. Dobre pozycje są dopisywane bez zmian, a ich dodatkowa dana jest ustawiana na zero. Złe URL-e mają dołączony znak tabulacji i napis “Błąd!." Oprócz tego dodatkowa dana tych pozycji ma wartość -1. Dzięki temu, funkcja OnScan może wykryć błędne pozycje i zrezygnować z ich późniejszego skanowania.
Parser to mieszanina kodu CString i wywołań starych, dobrych funkcji operujących na łańcuchach C. Większość funkcji operujących obiektami CString odmawia działania na fragmentach łańcuchów, tak więc, w pewnych przypadkach, łatwiej jest użyć starych funkcji.
Inne pomysły
Jeśli chcesz wysyłać dane do serwera, napisać serwer FTP czy zrobić coś bardziej skomplikowanego niż prosty odczyt pliku, będziesz musiał skorzystać z bardziej wymyślnych zastosowań funkcji interfejsu Winlnet. Samo czytanie danych także może być użyteczne. Oto kilka pomysłów:
• Web spider budujący lokalną bazę wyszukiwania ulubionych słów kluczowych;
• Program monitorujący URL-e i zgłaszający ich zmiany;
• Dla ochrony mógłbyś napisać program, który okresowo sprawdzałby pliki FTP, obliczał sumy kontrolne i zgłaszał, czy w plikach nastąpiły nieautoryzowane zmiany.
Obsługa Interentu przy pomocy ActiveX
Inny sposób dostępu do Internetu polega na użyciu kontrolki ActiveX. Jeśli skłonisz się do takiego podejścia, masz do wyboru kilka opcji. Po pierwsze, Microsoft Internet Explorer to w rzeczywistości kontrolka ActiveX (Shell.Explorer w SHDOCVW.DLL). Jeśli sprawdzisz ją w przeglądarce obiektów lub zajrzysz do dokumentacji ActiveX, dowiesz się jak jej użyć, osadzając przeglądarkę WWW bezpośrednio w swojej aplikacji.
Oprócz tego, Microsoft oferuje inną, nadającą się do wykorzystania kontrolkę ActiveX o nazwie Microsoft Internet Transfer Control. Kontrolka potrafi odczytywać strony WWW (i ich nagłówki), wysyłać dane do serwerów WWW (podobnie jak formularze przekazujące dane) oraz dokonywać transakcji FTP. Gdy kontrolka wysyła dane, używa protokołu standardowego (HTTP) lub bezpiecznego (HTTPS).
Jeśli chcesz użyć tej kontrolki, musisz dodać ją do swojego projektu, podobnie jak każda inną kontrolkę ActiveX (znajdziesz ją w pliku MSINET.OCX). Jeśli chcesz zwyczajnie pobierać dane, przekonasz się, że ta kontrolka jest bardzo prosta w użyciu. Gdy spróbujesz przesyłać dane lub monitorować postęp przesyłu, także okaże się to proste, choć będzie wymagało nieco więcej dodatkowej pracy.
Kontrolka Transfer
Kontrolka Transfer oferuje bogactwo właściwości, kilka metod i tylko jedno zdarzenie. W wielu prostych zastosowaniach nie będziesz korzystał z tego zdarzenia i użyjesz tylko jednej lub dwóch kluczowych metod.
Składowe udostępniane przez kontrolkę zostały zebrane w tabeli 9.7. Jeśli chcesz jedynie odczytać URL, OpenURL jest wszystkim, czego w rzeczywistości potrzebujesz. Ta metoda, jako argumentu oczekuje adresu URL; możesz także'określić, czy zwracane dane mają być w postaci łańcucha, czy tablicy bajtów. Domyślnie, dane są zwracane jako łańcuch. Ta metoda wykonuje całą robotę.
Tabela 9.7. Składowe kontrolki Transfer.
Składowa Typ Opis
AccessType Właściwość Wskazuje, czy połączenie internetowe odbywa się bezpośrednio,
czy poprzez serwer proxy.
Document Właściwość Ustawia nazwę pliku, jaka ma zostać użyta w metodzie Execute.
hlnternet Właściwość Wewnętrzny uchwyt HINTERNET połączenia (używany jedynie
do bezpośredniego wywoływania funkcji WININET).
Password Właściwość Hasło logowania użytkownika, jeśli jest stosowane (patrz
UserName).
Protocol Właściwość Wybiera protokół HTTP, HTTPS lub FTP.
Proxy Właściwość Zwraca lub ustawia nazwę serwera proxy używanego do dostępu
do Internetu (patrz także AccessType).
RemoteHost Właściwość Wybiera zdalny serwer.
RemotePort Właściwość Wybiera port do połączenia na zdalnym serwerze.
RequestTimeout Właściwość Określa limit czasu oczekiwania (0 oznacza brak limitu).
ResponseCode Właściwość Kod błędu (gdy kod stanu to icError; patrz tabela 9.10).
Responselnfo Właściwość Opis błędu (gdy kod stanu to icError; patrz tabela 9. 10).
Stillexecuting Właściwość Znacznik typu boolean, który ma wartość TRUE gdy transmisja
jest w toku.
URL Właściwość Bieżący URL; zmiana tej właściwości może spowodować także
zmianę innych właściwości (takich jak Protocols, RemoteHost,
RemotePort i Document); zmiana innych właściwości może
wpłynąć także na URL.
UserName Właściwość Nazwa użytkownika, jeśli jest stosowana (patrz Password).
Cancel Metoda Anuluje bieżącą transmisję.
Execute Metoda Wykonuje polecenie FTP lub polecenie HTTP Get/Post.
GetChunk Metoda Pobiera dane, jeśli stanem jest icResponseReceived lub
icResponseCompleted (patrz tabela 9.10).
GetHeader Metoda Zwraca określony nagłówek lub wszystkie nagłówki.
OpenURL Metoda Zwraca kompletny dokument HTTP lub plik FTP.
StateChanged Zdarzenie Zgłasza zmianę w stanie transmisji (patrz tabela 9. 10).
Sporo części składowych z tabeli 9.7 nie jest używanych zbyt często. I tak, składowa hlnternet jest użyteczna tylko wtedy, gdy planujesz bezpośrednie wywoływanie funkcji Wini net. Także zwykle nie będziesz określał, czy kontrólka powinna połączyć się bezpośrednio czy przez serwer proxy. Zamiast tego zechcesz użyć raczej domyślnych ustawień dla danego systemu. Na szczęście, to jest dokładnie to, co robi kontrólka, jeśli nie zmienisz właściwości AccessType.
Pewną niespodzianką może być powiązanie niektórych innych właściwości z właściwością URL. W szczególności, właściwości Document, Protocol, RemoteHost oraz RemotePort zmieniają się, dopasowując do tego, co umieściłeś we właściwości URL. Konsekwentnie, gdy zmienisz którąś z tych właściwości, zmieni się także zawartość właściwości URL. Metody OpenURL oraz Execute także zmieniają te właściwości. W praktyce jednak, jeśli będziesz korzystał z metody OpenURL lub, być może, Execute, nie będziesz często zajmował się właściwościami.
Do nagłówków HTTP możesz dostać się używając metody GetHeader (typowe wartości nagłówków znajdziesz w tabel^.S). Jeśli ta metoda, jako argument otrzyma nazwę nagłówka, zwróci jego wartość. Jeśli nie przekażesz żadnego argumentu, metoda zwróci wszystkie dostępne nagłówki. Kod odpowiedzi HTTP możesz odczytać z właściwości ResponseCode (patrz tabele 9.7 i 9.9).
Tabela 9.8. Typowe nagłówki HTTP.
Nagłówek Opis
Datę Zwraca datę i czas transmisji dokumentu; format zwracanych danych to
Wednesday, 27-April-96 19:34:15 GMT.
MIME-version Zwraca wersję protokołu MIME (obecnie 1.00).
Server Zwraca nazwę serwera.
Content-length Zwraca długość danych w bajtach.
Content-type Zwraca typ zawartości MIME danych.
Last-modified Zwraca datę i czas ostatniej modyfikacji dokumentu; format zwracanych danych
to Wednesday, 27-April-96 19:34:15 GMT.
W przypadku HTTP i anonimowego FTP, nie musisz przejmować się właściwościami User i Password. Jeśli jednak łączysz się z hostem wymagającym logowania, możesz ich potrzebować. Gdy nie określisz nazwy użytkownika lub hasła, kontrólka użyje anonimowego FTP. Oczywiście, możesz także ustawić anonimowe FTP ustawiając właściwość UserName na anonumous i właściwość Password na adres e-mail użytkownika.
Jeśli chcesz wydawać polecenia FTP lub przesyłać dane do serwera WWW, będziesz musiał użyć metody Execute kontrolki. Możesz także użyć tej metody zamiast metody OpenURL. -Execute potrafi wszystko to co OpenURL, a także nieco więcej. Jednak Execute jest nieco bardziej skomplikowana w użyciu, powinieneś więc odwoływać się do niej tylko wtedy, gdy nie możesz użyć OpenURL.
Tabela 9.9 Kody odpowiedzi HTTP
Wartość Słowo kluczowe Opis
200 OK. Wszystko w porządku.
201 Created POST zakończony sukcesem.
202 Accepted Żądanie zaakceptowane, ale nie wykonane.
203 Partial Information Zwrócona informacja może nie być kompletna.
204 No Response Nie wyświetlaj żadnych danych, pozostań w
bieżącym dokumencie.
301 Moved Żądany dokument został przeniesiony.
302 Found Przeniesiony dokument został odszukany.
303 Method Spróbuj alternatywnego URL-a.
304 Not Modified Żądany dokument nie został zmodyfikowany.
400 B ad Request Niewłaściwe żądanie ze strony klienta.
401 Unauthorized Dokument jest chroniony.
402 Payment Required Klient wymaga nagłówka ChargeTo:
403 Forbidden Nikt nie ma dostępu do tego dokumentu.
404 Not Found Dokument nie został odnaleziony.
500 Internal Error Błąd serwera.
501 Not Implemented Serwer nie wykonuje danej czynności.
502 Service Overloaded Serwer zbyt zajęty, aby obsłużyć żądanie.
503 Gateway Timeout Gateway (na przykład skrypt CGI) nie
odpowiedział.
Jedyne zdarzenie kontrolki, StateChanged, przekazuje programowi kod wskazujący, że coś się stało (patrz tabela 9.10). Gdy używasz OpenURL, nie otrzymasz wielu zdarzeń, gdyż kontroika obsługuje je wewnętrznie. Jeśli jednak użyjesz Execute, otrzymasz tyle zdarzeń, że będziesz mógł śledzić każdy krok procesu transmisji. Wartość icResponse-Received wskazuje, że dane są dostępne i możesz odczytać je metodą GetChunk. Jeśli wolisz poczekać, aż całe dane staną się dostępne, możesz czekać na kod icResponse-Complete.
Większość pozostałych składowych jest albo oczywista, albo bardzo rzadko wykorzystywana. Jeśli rzeczywiście próbujesz napisać bardzo wymyślną aplikację, możesz kontrolować prawie wszystko. Jeśli sobie życzysz, możesz nawet pobrać wewnętrzny uchwyt reprezentujący połączenie z Internetem i wykorzystać go. W przeciwnym razie masz do czynienia ze zwykłą kontroika ActiveX (więcej informacji o ActiveX zawiera rozdział 8).
Tabela 9.10. Kody przekazywano przez zdarzeni* StatoChanged.
Wartość Wartość numeryczna Opis
icNone 0 Brak stanu do zgłoszenia.
icHostResolvingHost 1 Kontrolka sprawdza adres IP określonego serwera.
icHostResolved 2 Kontrolka odnalazła adres IP określonego serwera.
icConnecting 3 Kontrolka łączy się z serwerem.
icConnected 4 Kontrolka poprawnie połączyła się z serwerem.
icReąuesting 5 Kontrolka wysyła żądanie do serwera.
icRequestSent 6 Kontrolka poprawnie wysłała żądanie.
icReceivingResponse 7 Kontrolka odbiera odpowiedź od serwera.
icResponseRecei ved 8 Kontrolka poprawnie odebrała odpowiedź od serwera.
icDisconnecting 9 Kontrolka rozłącza się z serwerem.
icDisconnected 10 Kontrolka poprawnie rozłączyła się z serwerem.
icError 11 Nastąpił błąd podczas komunikacji z serwerem.
icResponseCompleted 12 Żądanie zostało wykonane i wszystkie dane zostały
pobrane.
Obsługa ISAPI
Jeśli chcesz tworzyć aktywne strony WWW, tradycyjnym rozwiązaniem jest napisanie programu CGI. Program CGI może przyjmować dane wejściowe (w postaci formularza lub URL-a) i wysyłać odpowiedź do przeglądarki WWW. Program CGI może przyjąć Twoje nazwisko i adres e-mail, poszukać Twojego konta w bazie danych i wyświetlić bieżącą wartość konta.
Jeśli używasz serwera IIS Microsoftu, możesz pisać programy CGI, ale poza nimi staje się dostępny jeszcze jeden sposób: ISAPI. DLL tworzący ISAPI aktualnie staje się częścią serwera i ogólnie jest bardziej efektywny od klasycznego programu CGI. DLL-e ISAPI zdarzało mi się pisać zarówno w C, jak i korzystając z MFC; żaden ze sposobów nie był bardzo trudny. Kiedyś kolega spytał mnie, czy mógłby napisać rozszerzenie ISAPI w Visual Basicu. Moja pierwsza odpowiedź brzmiała nie, ponieważ VB nie potrafi tworzyć tradycyjnych DLL-i (a jedynie DLL-e ActiveX). Odpaliłem wyszukiwarkę WWW, trochę się porozglądałem i okazało się, że Microsoft posiada przykład, OLEISAPI, pozwalający na tworzenie rozszerzeń ISAPI w VB. Sądząc jednak z ruchu w Sieci, nie działał on zbyt dobrze. Po poświęceniu dwóch dni na zmuszeniu go do działania, poddaliśmy się. Poza tym, OLEISAPI nie kapsułkował ISAPI w obiektowym rozumieniu tego słowa. Jedną z korzyści płynących z ActiveX jest programowanie zorientowane obiektowo, a przykładowe rozszerzenie nie pozwalało także na pełne wykorzystanie możliwości ISAPI. Kilka dni później ukończyłem CBISAPI, moduł ISAPI umożliwiający pisanie rozszerzeń ISAPI w postaci ActiveX. Choć przy tworzeniu go miałem na uwadze Visual Basic 5, możesz użyć go z dowolnym językiem Współpracującym z ActiveX. CBISAPI korzysta z MFC tworząc most pomiędzy IIS a obiektem ActiveX.
Unikanie ISAPI
Czasem lepiej jest całkowicie unikać ISAPI. Jeśli używasz IIS Microsoftu, dużo więcej osiągniesz korzystając z ASP (Active Server Pages) zamiast odwoływania się do CGI czy ISAPI.
Pliki ASP umożliwiają osadzenie funkcji YBScript lub JavaScript w pliku HTML, interpretowanym przez serwer i używanym do wygenerowania wysyłanego HTML-a. Łącząc ten skrypt z obiektami ActiveX możesz korzystać z baz danych, zapisywać preferencje użytkowników, przewijać ogłoszenia, a także dużo więcej.
Pliki ASP znajdują się daleko poza zakresem tej książki. Jeśli chcesz dowiedzieć się o nich czegoś więcej, zajrzyj do Active Server Pages Black Book, także wydanej przez The Coriolis Group.
Plan
Moja idea była prosta: napisać DLL rozszerzenia ISAPI, wywołujący serwer ActiveX. DLL przesyła do serwera obiekt ActiveX używany do odczytania informacji HTTO i manipulowania wyjściem HTTP (zwykle plikiem HTML). To jest nieco niezwykłe: DLL jest zarówno DLL-em ISAPI, jaki i serwerem ActiveX, który z kolei udostępnia obiekt innemu serwerowi ActiveX (rozszerzeniu ISAPI w VB). Mówiąc prawdę, rozszerzenie VB ISAPI nie musi w rzeczywistości być serwerem OLE, ale jest to jedyny rodzaj DLL, jakie można stworzyć w VB.
Tabela 9.11. zawiera składowe obiektu używanego przez serwer VB do współpracy z IIS. Podprogram VB może mieć dowolną nazwę, ale jako argument musi przyjmować obiekt:
Public Sub VBISAPI(Server as Object) End Sub
Obiektu (w tym przypadku servera) używa się w kodzie VB korzystając ze składowych z tabeli 9.11.
Połączenie obiektów
Choć przy pomocy VB5 można łatwo stworzyć DLL-a ActiveX, nie istnieje łatwy sposób stworzenia DLL-a ISAPI. Z tego powodu główny DLL ISAPI korzysta z MFC. (ISER.VER.CPP; patrz listing 9.5. Zwróć uwagę na to, że w związku z ograniczeniami długości strony, niektóre linie kodu rozpoczynające się od DISP_... są rozbite na dwie
Tabela 9.11 Składowe obiektu CBSIAPI
Składowa Typ Opis
RefVal Właściwość Ustawia zwracaną wartość ISAPI.
StatCode Właściwość Ustawia zwracany kod HTTP.
Method Właściwość (R/O) Metoda (na przykład “GET" lub “POST").
QueryString Właściwość (R/O) Cały łańcuch zapytania.
Pathlnfo Właściwość (R/O) Wirtualna ścieżka do skryptu.
PathTranslated Właściwość (R/O) Przetworzona ścieżka do skryptu.
Content Właściwość (R/O) Dane przesłane do skryptu.
ContentType Właściwość (R/O) Typ danych we właściwości Content.
ContentLength Właściwość (R/O) Długość danych we właściwości Content.
Write Metoda Zapisuje łańcuch do klienta (zakończony bajtem NULL).
WriteLine Metoda Zapisuje łańcuch zakończony kodem nowej linii (nie
<P>).
WriteByte Metoda Zapisuje pojedynczy bajt do klienta.
ServerVariable Metoda Zwraca standardową zmienną serwera CGI (patrz tabela
9.3).
ServerDone-Session Metoda Podczas asynchronicznej operacji wskazuje, że
rozszerzenie zakończyło pracę.
Redirect Metoda Kieruje przeglądarkę do innego URL.
SendUrl Metoda Wysyła alternatywny URL.
SendHeaders Metoda Wysyła nagłówki HTTP.
MapURL2Path Metoda Mapuje lokalny URL na pełną nazwę ścieżki.
linie, a w rzeczywistości powinny występować w jednej.) Choć MFC ma specjalne środki do tworzenia DLL-i ISAPI, serwer nie korzysta z nich, gdyż dokładają kolejną warstwę interfejsu. Zamiast tego, ten DLL to zwykły DLL MFC z poprawnymi punktami wejścia ISAPI. Kod DLL-a (CBISAPI.CPP i pliki nagłówkowe) znajdziesz na płytce CD-ROM dołączonej do książki.
Listing 9.5. ISERYER.CPP.
// IServer.cpp : implementation file
//
#include "stdafx.h"
#include "cbisapi.h"
#include "IServer.h"
#include <malloc.h>
#include <afxconv.h> // BSTR conversions
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CIsapiServer
IMPLEMENT_DYNCREATE(CIsapiServer, CCmdTarget)
CIsapiServer::CIsapiServer()
{
EnableAutomation();
}
CIsapiServer::~CIsapiServer()
{
}
void CIsapiServer::OnFinalRelease()
{
CCmdTarget::OnFinalRelease();
}
BEGIN_MESSAGE_MAP(CIsapiServer, CCmdTarget)
//{{AFX_MSG_MAP(CIsapiServer)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
BEGIN_DISPATCH_MAP(CIsapiServer, CCmdTarget)
//{{AFX_DISPATCH_MAP(CIsapiServer)
DISP_PROPERTY(CIsapiServer, "RetVal", m_retVal, VT_I4)
DISP_PROPERTY(CIsapiServer, "StatCode", m_statCode, VT_I4)
DISP_PROPERTY_EX(CIsapiServer, "Method", GetMethod, SetNotSupported, VT_BSTR)
DISP_PROPERTY_EX(CIsapiServer, "QueryString", GetQueryString, SetNotSupported, VT_BSTR)
DISP_PROPERTY_EX(CIsapiServer, "PathInfo", GetPathInfo, SetNotSupported, VT_BSTR)
DISP_PROPERTY_EX(CIsapiServer, "PathTranslated", GetPathTranslated, SetNotSupported, VT_BSTR)
DISP_PROPERTY_EX(CIsapiServer, "ContentLength", GetContentLength, SetNotSupported, VT_I4)
DISP_PROPERTY_EX(CIsapiServer, "Content", GetContent, SetNotSupported, VT_BSTR)
DISP_PROPERTY_EX(CIsapiServer, "ContentType", GetContentType, SetNotSupported, VT_BSTR)
DISP_FUNCTION(CIsapiServer, "Write", Write, VT_BOOL, VTS_VARIANT)
DISP_FUNCTION(CIsapiServer, "ServerVariable", ServerVariable, VT_BOOL, VTS_VARIANT VTS_PVARIANT)
DISP_FUNCTION(CIsapiServer, "WriteLine", WriteLine, VT_BOOL, VTS_VARIANT)
DISP_FUNCTION(CIsapiServer, "WriteByte", WriteByte, VT_BOOL, VTS_VARIANT)
DISP_FUNCTION(CIsapiServer, "ServerDoneSession", ServerDoneSession, VT_BOOL, VTS_NONE)
DISP_FUNCTION(CIsapiServer, "Redirect", Redirect, VT_BOOL, VTS_VARIANT)
DISP_FUNCTION(CIsapiServer, "SendURL", SendURL, VT_BOOL, VTS_VARIANT)
DISP_FUNCTION(CIsapiServer, "SendHeaders", SendHeaders, VT_BOOL, VTS_VARIANT VTS_VARIANT)
DISP_FUNCTION(CIsapiServer, "MapURL2Path", MapURL2Path, VT_BOOL, VTS_PVARIANT)
//}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()
// Note: we add support for IID_IIsapiServer to support typesafe binding
// from VBA. This IID must match the GUID that is attached to the
// dispinterface in the .ODL file.
// Not really used in any meaningful way, but the wiz puts it here
// {A3B7D305-647C-11D0-A7B2-444553540000}
static const IID IID_IIsapiServer =
{ 0xa3b7d305, 0x647c, 0x11d0, { 0xa7, 0xb2, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };
BEGIN_INTERFACE_MAP(CIsapiServer, CCmdTarget)
INTERFACE_PART(CIsapiServer, IID_IIsapiServer, Dispatch)
END_INTERFACE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CIsapiServer message handlers
// Write to client
BOOL CIsapiServer::Write(const VARIANT FAR& idata)
{
COleVariant data=idata;
USES_CONVERSION;
data.ChangeType(VT_BSTR); // Force to BSTR
if (data.vt!=VT_BSTR)
return FALSE;
char *s=W2A(data.bstrVal); // switch to ANSI
DWORD siz=strlen(s);
return ecb->WriteClient(ecb->ConnID,s,&siz,0); // out!
}
// This fetches a Server Variable into a VARIANT
// Be careful. Since the second argument is a variant
// by reference, the formal argument must really be
// a variant. In other words, NO:
// dim x as string
// server.ServerVariable "SCRIPT_NAME",x
// YES:
// dim x as variant
// server.ServerVariable "SCRIPT_NAME",x
// Probably should have been a function returning VARIANT, but then
// again...
BOOL CIsapiServer::ServerVariable(const VARIANT FAR& Variable,
VARIANT FAR* Result)
{
COleVariant var;
var=Variable;
var.ChangeType(VT_BSTR);
if (var.vt!=VT_BSTR) return FALSE;
USES_CONVERSION;
char *v=W2A(var.bstrVal);
CString res;
DWORD siz=1024;
BOOL rv;
rv=ecb->GetServerVariable(ecb->ConnID,v,(char *)res.GetBufferSetLength(siz),&siz);
res.ReleaseBuffer(siz-1);
VariantClear(Result);
Result->vt=VT_BSTR;
Result->bstrVal=res.AllocSysString();
return rv;
}
// R/O Property -- these all look the same
BSTR CIsapiServer::GetMethod()
{
CString strResult=ecb->lpszMethod;
BSTR rv;
rv=strResult.AllocSysString();
return rv;
}
// Another R/O Property
BSTR CIsapiServer::GetQueryString()
{
CString strResult=ecb->lpszQueryString;
BSTR rv;
rv=strResult.AllocSysString();
return rv;
}
// R/O Property
BSTR CIsapiServer::GetPathInfo()
{
CString strResult=ecb->lpszPathInfo;
BSTR rv;
rv=strResult.AllocSysString();
return rv;
}
// R/O Property
BSTR CIsapiServer::GetPathTranslated()
{
CString strResult=ecb->lpszPathTranslated;
BSTR rv;
rv=strResult.AllocSysString();
return rv;
}
// R/O Property
long CIsapiServer::GetContentLength()
{
return ecb->cbTotalBytes;
}
// R/O Property with a twist
// Apparently sometimes the server calls the
// extension without having all the content
// data (does this really happen?)
// This function reads it all so it is available
// BTW, the docs say that if the count is
// 0xFFFFFFFF then MORE than 4G of data
// is forthcoming and you should call ReadClient
// until it is empty
// NEWS BULLETIN: If you expect 4G or more in
// a request, don't use these functions!
BSTR CIsapiServer::GetContent()
{
CString strResult;
char *p=strResult.GetBufferSetLength(ecb->cbTotalBytes);
// put available bytes in CString
memcpy(p,ecb->lpbData,ecb->cbAvailable);
// Read excess
if (ecb->cbAvailable!=ecb->cbTotalBytes)
{
DWORD siz=ecb->cbTotalBytes-ecb->cbAvailable;
ecb->ReadClient(ecb->ConnID,p+ecb->cbAvailable,&siz);
}
strResult.ReleaseBuffer(ecb->cbTotalBytes);
BSTR rv;
rv=strResult.AllocSysString();
return rv;
}
// Another R/O
BSTR CIsapiServer::GetContentType()
{
CString strResult=ecb->lpszContentType;
BSTR rv;
rv=strResult.AllocSysString();
return rv;
}
// Simple Method to write a line
// Note that HTML doesn't care one
// whit about the \r\n -- it just
// makes the HTML source nicer
// Use <P> or <BR> to get a newline in HTML
BOOL CIsapiServer::WriteLine(const VARIANT FAR& idata)
{
BOOL rv=Write(idata);
DWORD siz=2;
if (rv) rv=ecb->WriteClient(ecb->ConnID,"\r\n",&siz,0);
return rv;
}
// Write a byte out
BOOL CIsapiServer::WriteByte(const VARIANT FAR& byte)
{
COleVariant num=byte;
num.ChangeType(VT_UI1);
if (num.vt!=VT_UI1)
return FALSE;
char s=num.bVal;
DWORD siz=1;
return ecb->WriteClient(ecb->ConnID,&s,&siz,0);
}
// Wrap ServerSupportFunction Done with Session
BOOL CIsapiServer::ServerDoneSession()
{
return ecb->ServerSupportFunction(ecb->ConnID,HSE_REQ_DONE_WITH_SESSION,NULL,NULL,NULL);
}
// Redirect to another URL (wrap ServerSupportFunction)
BOOL CIsapiServer::Redirect(const VARIANT FAR& url)
{
COleVariant var;
var=url;
var.ChangeType(VT_BSTR);
if (var.vt!=VT_BSTR) return FALSE;
USES_CONVERSION;
char *v=W2A(var.bstrVal);
DWORD siz=strlen(v);
return ecb->ServerSupportFunction(ecb->ConnID,HSE_REQ_SEND_URL_REDIRECT_RESP,v,&siz,NULL);
}
// Send alternate URL (wrap ServerSupportFunction)
BOOL CIsapiServer::SendURL(const VARIANT FAR& url)
{
COleVariant var;
var=url;
var.ChangeType(VT_BSTR);
if (var.vt!=VT_BSTR) return FALSE;
USES_CONVERSION;
char *v=W2A(var.bstrVal);
DWORD siz=strlen(v);
return ecb->ServerSupportFunction(ecb->ConnID,HSE_REQ_SEND_URL,v,&siz,NULL);
}
// Send headers (wrap ServerSupport Function)
BOOL CIsapiServer::SendHeaders(const VARIANT FAR& Status, const VARIANT FAR& Headers)
{
COleVariant var,var2;
var=Status;
var2=Headers;
var.ChangeType(VT_BSTR);
if (var.vt!=VT_BSTR) return FALSE;
var2.ChangeType(VT_BSTR);
if (var.vt!=VT_BSTR) return FALSE;
USES_CONVERSION;
char *status=W2A(var.bstrVal);
char *hdr=W2A(var.bstrVal);
return ecb->ServerSupportFunction(ecb->ConnID,HSE_REQ_SEND_RESPONSE_HEADER,status,NULL,(DWORD *)hdr);
}
// Map Virtual Path to Real Path (wrap ServerSupportFunction)
BOOL CIsapiServer::MapURL2Path(VARIANT FAR* urlpath)
{
BOOL rv;
COleVariant var,var2;
var=urlpath;
var.ChangeType(VT_BSTR);
if (var.vt!=VT_BSTR) return FALSE;
USES_CONVERSION;
char *varin=W2A(var.bstrVal);
DWORD siz=1024;
CString url(varin);
rv=ecb->ServerSupportFunction(ecb->ConnID,
HSE_REQ_MAP_URL_TO_PATH,
url.GetBufferSetLength(siz),&siz,NULL);
url.ReleaseBuffer(siz-1);
// set up return value
VariantClear(urlpath);
urlpath->vt=VT_BSTR;
urlpath->bstrVal=url.AllocSysString();
return rv;
}
Moje przykładowe rozszerzenie ISAPI korzysta z Visual Basica, ale może działać z praktycznie każdym językiem, potrafiącym stworzyć porównywalny serwer OLE. W tym rozdziale jednak trzymałem się nazwy serwer VB, aby odróżnić go od DLL rozszerzenia ISAPI dla C++. Jak napiszesz URL-a wywołującego Twój kod? Proces składa się z dwóch kroków. Najpierw ISAPI powinno wywołać DLL-a C++ (CBISAPI.DLL). Następnie umieszczasz nazwę serwera VB w łańcuchu zapytań URL-a (w części następującej po znaku zapytania). Na koniec dodajesz dwukropek i nazwę metody OLE, którą chcesz wywołać. Na przykład:
<A HREF=http://www.al-williams.com/awc/scripts/ cbisapi.dll7hilo.dll:Guess+newgame>
Kliknij aby rozpocząć </A>
Co to wszystko znaczy? Część przed znakiem zapytania uaktywnia DLL-a ISAPI. Gdy tworzysz serwer OLE w VB, nazwa serwera zawiera nazwę projektu, kropkę oraz nazwę modułu klasy zawierającej obiekt, z którym chcesz pracować. W tym przykładzie moduł o nazwie DLL zawiera cały kod projektu HILO (o którym opowiem w dalszej części rozdziału). Wewnątrz tego modułu występuje podprogram o nazwie Guess (zgadnij). Znak plus wyznacza koniec nazwy serwera OLE. Wszystko co znajduje się poza nim stanowi część łańcucha zapytań, który serwer przekazuje programowi. Zwróć uwagę na fakt, że serwer C++ nie zmienia łańcucha zapytań - to do Twojego kodu należy pominięcie pierwszej części, oddzielenie sekwencji wyjścia HTTP i przetworzenie łańcucha zapytań.
Pobieżne spojrzenie na ISAPI
Programy ISAPI nie są zbyt trudne do stworzenia przy pomocy C lub C++. Istnieją dwa rodzaje takich programów. DLL-e rozszerzeń (które poznasz w tym rozdziale) dynamicznie generują plik wyjściowy. Rozszerzenia drugiego rodzaju, filtry, mogą obsługiwać różnorodne żądania dotyczące danych. Więcej informacji o pisaniu filtrów znajdziesz w dokumentacji IIS i MFC.
MFC udostępnia ogólny mechanizm rozszerzeń ISAPI, obsługujący większość popularnych przypadków. Jednak w przypadku połączenia ActiveX z ISAPI, ten mechanizm nie na wiele się zda. W związku z tym, musiałem samodzielnie obsłużyć ISAPI, choć dla obsługi ActiveX wykorzystałem MFC. Później, w tym rozdziale, poznasz także bardziej tradycyjny program ISAPI stworzony przy pomocy MFC.
W przypadku tego typu DLL-a ISAPI potrzebujesz dwóch funkcji: GetExtensionVersion oraz HttpExtensionProc. Funkcja GetExtensłonVersion informuje serwer o wersji HS-a oczekiwanego przez Twój serwer oraz dostarcza łańcuch- z opisem. Możesz skopiować ten kod bezpośrednio z pliku pomocy - jest bardzo prosty, i jedyne co musisz zrobić, to zmodyfikować łańcuch tak, by odpowiadał Twojemu rozszerzeniu. Cała praca wykonywana jest dopiero w funkcji HttpExtensionProc. Funkcja otrzymuje pojedynczy argument, ale stanowi on wskaźnik do struktury EXTENSION_CONTROL_BLOCK (tabela 9.12) zawierającej pokaźną ilość danych. Porównaj tabele 9.11 i 9.12. Zauważysz, że pozycje tabeli 9.11 w dużym stopniu pokrywają się ze strukturą EXTENSION_ CONTROL_BLOCK, ale w sposób obiektowo zorientowany.
Gdy piszesz DLL rozszerzenia, musisz zachować ostrożność. Podobnie jak DLL-e używane przez program stają się częścią jego procesu, podobnie DLL-e rozszezreń stają się częścią HS-a. Jeśli Twój DLL się załamie, możesz załamać cały serwer. Jeśli zgłosisz wyjątek, serwer dyskretnie zamknie Twojego DLL-a i będzie kontynuował działanie. W CBISAPI przezornie kod VB wykonywany jest wewnątrz bloku try; program zgłasza wszystkie wyjątki wykryte w strumieniu HTML.
Tabela 9.12. Składowe EXTENSION_CONTROŁ_BLOCK.
Składowa Opis
cbSize Rozmiar struktury
dwVersion NumerWersjiStruktury
ConnID ID połączenia identyfikujący konkretne żądanie (przekazywany wielu funkcjom
IIS).
dwHttpStatusCode Kod błędu HTTP.
lpszLogData Dane dziennika specyficznego dla rozszerzenia.
lpszMethod Żądanie (na przykład “POST" lub “GET").
lpszQueryString Łańcuch zapytań.
lpszPathInfo Ścieżka do skryptu.
lpszPathTranslated Przetworzona ścieżka.
cbTotalBytes Całkowity rozmiar zawartości.
cbAvailable Rozmiar zawartości już odczytanej.
lpbData Zawartość (bajty cbAvailable).
lpszContentType Typ danych zawartości.
GetServerVariable Wskaźnik do funkcji używanej do odczytu zmiennych serwera (patrz tabela 9.3).
WriteClient Wskaźnik do funkcji używanej do zapisywania danych do klienta.
ReadClient Wskaźnik do funkcji używanej do pobierania pozostałych danych od klienta
(cbTotalBytes - cbAvailable).
ServerSupport Wskaźnik do funkcji używanej do wywoływania specjalnych funkcji
stosowanych do przełączania, przesyłania URL-i, mapowania wirtualnych
ścieżek i paru innych rzeczy.
Pisanie serwera HILO.DLL
VB5 bardzo ułatwia napisanie DLL-a serwera ActiveX. W początkowym oknie wybierz pozycję ActiveX DLL. Dzięki temu projekt zostanie prawidłowo przygotowany, i będziesz miał do dyspozycji moduł klasy, który będzie zawierał Twój kod oraz właściwości. Pamiętaj aby zmienić nazwę projektu i modułu klasy - na ich podstawie tworzona jest nazwa serwera.
W przypadku serwera CB1SAPI, musisz jedynie zdefiniować publiczny podprogram (Public Sub), wymagjący jako argumentu obiektu (Object). Moduł klasy HILO.DLL (listing 9.6) zawiera kilka prywatnych podprogramów do obsługi wewnętrznych procesów. Te podprogramy wspomagają jedynie podprogram Guess -jedyny wywoływany w kodzie HTML-a.
Listing 9.6. Moduł HILO.DLL Visual Basica.
YERSION 1.0 CLASS BEGIN
MultiUse = -l 'True END
Attribute VB_Name = "DLL" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = True Attribute VB_PredeclaredId = False Attribute VB_Exposed = True Option Explicit
Private Sub svrerr(server As Object, errstr As String) server.WriteLine "Błąd: " & errstr server.statcode = 400 server.retval = 4 End Sub
Private Sub Win(server As Object)
server .WriteLine "<HTMLxHEADxTITLE>I Win</TITLEx/HEADxBODY>"
server .WriteLine "Zgadłem! </BODYX/HTML>"
End Sub
Private Sub GuessAgain(server As Object, Hi As Long, Lo As Long)
Dim servername As Variant
Dim script As Variant
server .WriteLine "<HTMLxHEAD><TITLE>HiLo ! </TITLEx/HEADxBODY>"
server.WriteLine "Moja odpowiedź to " & CIntUHi + Lo) / 2) & "<P>"
server.ServerVariable "SERVER_NAME", servername
server.ServerVariable "SCRIPT_NAME", script
server.WriteLine "Czy liczba jest :<P>"
server.Write "<FORM ACTION=http://" & servername
server.Write "/" & script
server.WriteLine "7HILO.DLL:Guess+HI=" & Hi & "+L0=" & Lo & "
METHOD=POST>"
server.WriteLine "Wyższa <INPUT TYPE=RADIO NAME=ANSWER
VALUE=HI><P>"
server.WriteLine "Poprawna <INPUT TYPE=RADIO NAME=ANSWER
VALUE=OK><P>"
server.WriteLine "Niższa <INPUT TYPE=RftDIO NAME=ANSWER
VALUE=LO><P>"
server.WriteLine "<INPUT TYPE=SUBMIT>"
server .WriteLine "</FORM>" server .WriteLine "</BODY></HTML>" End Sub
vbTextCompare
)
Public
Sub Guess(server As Object) Dim Guess As Long Dim Hi As Long Dim Lo
As Long Dim pos As Long Dim ans As String
pos = InStr(l, server .QueryString, "H I f pos = O Then
svrerr server, "Can't f ind HI" Ex i t Sub End I f
Hi = Val (Mid(server .QueryString, pos + 3)) pos = InStrd, server .QueryString, "L0=", vbTextCompare) I f pos = O Then
svrerr server, "Can't f ind LO" Ex i t Sub End I f
Lo = Val (Mid(server .QueryString, pos + 3)) I f server .ContentLength = O Then GuessAgain server, Hi, Lo Else
Guess = (Hi + Lo) / 2
pos = InStrd, server .Content, "ANSWER=", vbTextCompare) I f pos = O Then
svrerr server, "Form error" Ex i t Sub End I f
ans = Mid(server .Content, pos +7, 2) If ans = "OK" Then Win server
If ans = "LO" Then GuessAgain server, Hi, Guess If ans = "HI" Then GuessAgain server, Guess, Lo
I f ans <> "OK" And ans <> "LO" And ans <> "HI" Then svrerr server, "Unknown Response: " & serwer .Content End I f End Sub
Choć ten serwer posiada tylko jeden publiczny punkt wejścia, nie ma powodu, dla którego nie mogłoby istnieć ich więcej. Możesz także dodać kolejne moduły klas. Umożliwi ci to pogrupowanie powiązanych ze sobą funkcji w jednej klasie, a powiązanych klas w jednym DLL-u.
Gotowy produkt widać na rysunku 9.2. Gra w rzeczywistości polega na prostym binarnym wyszukiwaniu. Prawidłowa odpowiedź zawsze zostanie znaleziona, w co najwyżej dziesięciu próbach. (Oczywiście, zawsze możesz oszukiwać.) Jeśli zawartość (dane wysłane przez formularz) jest pusta, program zakłada, że po prostu rozpoczynasz nową grę. Oczekuje, że łańcuch zapytań zawiera dwie zmienne: HI oraz LO (określające zakres liczb). Potem wywoływany jest podprogram GuessAgain generujący formularz, który zawierają aktualną odpowiedź i oferuje trzy przyciski radiowe, pozwalające na określenie czy liczba jest wyższa, niższa lub zgodna z odpowiedzią. Formularz przekazuje dane do tej samej metody Guess, poprzez CBSAPI. Program przygotowuje łańcuch zapytań, odzwierciedlając aktualną najwyższą i najniższą wartość.
W następnych wywołaniach zawartość będzie posiadała stan przycików formularza. Kod sprawdza, czy zawartość jest obecna, i przelicza górny oraz dolny limit. Następnie wywoływane jest GuessAgain w celu wygenerowania nowego formularza. Oczywiście, jeśli odpowiedź jest poprawna, kod nie generuje formularza - zamiast tego wywoływany jest podprogram Win generujący odpowiedni komunikat.
Listing 9.7 przedstawia plik HTML uruchamiający całą zabawę. Oczywiście, jeśli chcesz, możesz go rozbudować. Także formularz odpowiedzi z rysunku 9.2 może być zabawniejszy. Możesz umieścić w formularzu nieco JavaScript-u, co sprawi, że gdy klikniesz na przycisku, formularz zostanie automatycznie wysłany. Na razie jednak stworzony kod także działa i w zupełności wystarczy do zilustrowania pracy interfejsu ISAPI.
Listing 9.7. Strona WWW HILO.
<HTML>
<HEAD>
<TITLE>Play Hi-Lo!</TITLE>
</HEAD>
<BODY>
Pomyśl liczbę.<BR>
Wybierz liczbę pomiędzy l a 1024, a ja ją odgadnę.<BR>
Wybierz liczbę i
<A HREF=http://www/seripts/cbisapi.dl17HILO.DLL:GUESS+HI=1024+LO=1>
kliknij tu, aby rozpocząć</A>
</BODY>
</HTML>
Analiza DLL-a C++
DLL C++ to zwykły DLL OLE stworzony w MFC. Plik CBISAPf.CPP (zajrzyj do listingów na płytce) zawiera punkty wejścia wymagane dla ActiveX (wstawione przez AppWizarda MFC) oraz punkty wejścia ISAP1.
W standadrowych prototypach, w nagłówku HTTPEXT.H, funkcje zadeklarowane są jako C, więc kompilator C++ nie modyfikuje ich nazw. Z drugiej strony, ten sam nagłówek nie deklaruje tych funkcji jako eksportowalnych (używając _declspec(dllexport)). Z tego powodu musisz umieścić te funkcje w sekcji EXPORTS pliku DEF, tak aby IIS mógł je zlokalizować.
Funkcja HttpExtensionProc nie jest zbyt wymyślna. Analizuje łańcuch zapytań w celu wydzielenia nazwy serwera i metody. Ta nazwa musi być pierwszą rzeczą w łańcuchu zapytań. Analiza kończy się przy napotkaniu końca łańcucha zapytań lub przy natrafieniu na pierwszy znak plus. Jeśli pominiesz nazwę metody, CBISAPI spróbuje wywołać metodę ISAPI.
Zwróć uwagę, że w nazwie składowej użyto znaków UNICODE. Gdy przekazujesz nazwę, program używa funkcji A2W w celu konwersji łańcucha na UNICODE. W przeciwnym razie, używany jest literał L"ISAPI" (L oznacza stałą UNICODE). To jest pierwszy moment (i nie ostatni), w którym mogą pojawić się problemy. Oczywiście, IIS obsługuje HTTP. HTTP wykorzystuje znaki ANSI (zwykłe znaki, które znamy i lubimy). Jednak ActiveX do wszystkiego wykorzystuje łańcuchy UNICODE (znaki dwubajtowe). Teoretycznie Stany Zjednoczone przejdą na system metryczny - kiedyś. W tak zwanym “międzyczasie" musimy zdać się na konwersję.
Istnieje wiele sposobów konwersji znaków ANSI i UNICODE. Zdecydowałem się na użycie funkcji MFC z pliku AFXCONV.H. Więcej szczegółów na temat tych makr znajdziesz w Uwadze Technicznej dla MFC nr 59. Pamiętaj tylko, że w tej uwadze błędnie podano, iż do skorzystania z makr potrzeba nagłówka AFXPRIV.H; to wcześniej było prawdą, ale obecnie powinieneś użyć nagłówka AFXCONV.H.
Gdy kod wie już jaki obiekt ma stworzyć, do jego reprezentacji używa klasy Cdispatch-Driver z MFC. Wywołanie CreateDispatch powoduje utworzenie obiektu. Następnie, wywołanie GetIDsOfNames konwertuje nazwę składowej do odpowiedniego identyfikatora (DISPID - dispatch ID). Identyfikatory DISPID to kody funkcji, używane przez obiekty automatyzacji ActiveX do identyfikacji składowych (a także właściwości). Zaopatrzone w DISPID, wywołanie InvokeHelper, wywołuje kod Visual Basica. Poprzednie wywołanie GetIDispatch zwróciło wskaźnik, który jest potrzebny do przekazania do kodu VB, aby ten mógł odwołać się do obiektu serwera.
Zwróć uwagę na to, że CBISAPI chroni wywołanie InvokeHelper poleceniem try. Dzięki temu mamy pewność, że wszystkie wyjątki trafią do CBISAPI. CBISAPI zgłasza błędy drukując do strumienia HTML. To działa dobrze, dopóki rozszerzenie VB nie zaczyna zapisywać pewnych typów danych, niezgodnych z HTML.
Cała rzeczywista praca jest wykonywana w obiekcie serwera (patrz listing 9.5). Tę klasę można skonstruować łatwiej niż myślisz. Najpierw użyłem ClassWizarda do stworzenia nowej klasy wyprowadzonej z CCmdTarget. Klasa CCmdTarget stanowi klasę bazową wszystkich obiektów automatyzacji ActiveX w MFC. Następnie, na zakładce Automation, dodałem właściwości i metody. Jedyna trudna część1o pisanie kodu.
Z kolei jedyna nieco zagmatwana część kodu jest związana z konwersją BSTR (łańcuchów UNICODE ActiveX) i char* (łańcuchów ANSI serwera IIS). W kilku miejscach użyłem funkcji konwersji z MFC, o których wspominałem wcześniej. Jednak w kilku przypadkach, mając do dyspozycji dane w postaci CString z MFC, do stworzenia BTSR-a, zdecydowałem się użyć metody CString::AllocSysString(). I w tym momencie pojawił się mały kłopot.
BTSR-y to nie są łańcuchy C (ani C++). Mogą zawierać w sobie znaki '\0'. Aby mocje obsłużyć, BTSR zawiera licznik znaków. Zwykle taki łańcuch kończy się znakiem '\0' zgodnie z oczekiwaniami programistów C/C++ (i Windows API), ale kończący kod NULL zwykle nie stanowi części łańcucha. I tak, BTSR zawierający łańcuch "Coriolis\0" powinien mieć licznik znaków o wartości 8, chyba że masz zamiar dołączyć znak '\0' do łańcucha. Jednak rozmiar zwracany przez ISAPI uwzględnia znak '\0'. Komu to przeszkadza? Niestety, VB jest czuły na tym punkcie. Przyjrzyjmy się właściwości ServerVariable w obiekcie serwera CBISAPI. Co się stanie, jeśli stworzy BTSR-a zawierającego kod NULL? Spójrz na poniższy kod VB:
Dim x as Variant
server. ServerVariable "HOST_NAME",x
server.Write x
server. ServerVariable "SCRIPT_NAME",x
server.Write x
W powyższym fragmencie wszystko działa poprawnie i kończący kod NULL nie przeszkadza. Przyjrzyj się jednak poniższemu fragmentowi:
Dim x as Variant
Dim y as Variant
server. ServerVariable "HOST_NAME",x
server. ServerVariable "SCRIPT_NAME",y
server.Write x & y
Przypuśćmy, że wartością HOST_NAME jest www.al-williams.com\0, zaś wartością SCRIPT_NAME jest ztest.dll\0. VB posłusznie formuje łańcuch www.al-williams.com\ O ztest.dll\0, co powoduje, że kod C napędzający metodę Write zatrzymuje się na pierwszym znaku NULL.
Aby uniknąć tego problemu, nie zapomnij o odjęciu l od wartości zwracanej przez ISAPI. Alternatywnie, możesz także pozwolić, by CString przekalkulowało rozmiar. W każdym razie nie możesz pozwolić, by w rozmiarze był uwzględniany kończący bajt zero.
Instalacja i dystrybucja
DLL C++ musi stworzyć obiekt ActiveX. Musi to jednak uczynić na warunkach anonimowego użytkownika Internetu. Z tego powodu, domyślny użytkownik musi posiadać uprawnienia do tworzenia obiektów ActiveX. W przypadku NT 4.0, możesz ustawić to uruchamiając program DCOMCNFG (znajdujący się w folderze SYSTEM32). Kliknij na zakładce Default Security, i do sekcji Default Access Permissions oraz Default Launch Permissions dodaj IUSR_xxx (gdzie xxx to nazwa Twojego serwera. Gdy pierwszy raz klikniesz na przycisku Add danej sekcji, pojawią się jedynie nazwy grup. Aby wyświetlić poszczególnych użytkowników (łącznie z IUSR_xxx), kliknij na przycisku Show Users.
Oczywiście, wszystkie DLL-e wymagane przez części naszej układanki, także muszą być na serwerze. Jeśli zbudowałeś CBISAPI wykorzystujące DLL-e MFC, one także muszą być obecne (najlepiej w katalogu \WINNT\SYSTEM32). Część VB także wymaga swoich DLL-i czasu wykonania.
Jeszcze jedna rzecz, która powinna być oczywista: musisz zarejestrować swój serwer VB na komputerze będącym serwerem internetowym; w tym celu uruchom REGSVR32 dla DLL-i lub uruchom plik wykonywalny. Zarejestrowanie serwera na innym komputerze spowoduje jedynie modyfikację Rejestru tego komputera. Jeśli zapomnisz o rejestracji, otrzymasz wyjątek z kodem błędu REGDB_E_CLASSNOTREG (0x80040154).
Na marginesie, chociaż CBISAPI dostarcza obiekt ActiveX, nie wymaga on rejestracji. Dzieje się tak, ponieważ żaden inny program nigdy nie tworzy tego obiektu. CBISAPI tworzy go samodzielnie i przekazuje innym programom ActiveX. Oznacza, to że obiekt nie posiada informacji o typie. To powoduje, że VB nie sprawdza wywołań podczas kompilacji. Zamiast tego, wszelkie niezgodności typów i źle wpisane nazwy zostaną wykryte dopiero w czasie wykonania.
Debuggowanie rozszerzeń ISAPI
Debuggowanie rozszerzeń ISAPI to, w najlepszym razie, nieprzyjemna sprawa. Aby przetestować część w C++, możesz przygotować IIS do uruchomienia jako proces użytkownika, i uruchomić go pod debuggerem. Dokładne instrukcje na ten temat znajdziesz w Uwadze Technicznej MFC nr 63.
Debuggowanie części VB jest jeszcze gorsze. Najprostszym sposobem jest przyprawienie kodu VB wieloma tymczasowymi poleceniami WriteLine, aby wiedzieć co się dzieje w strumieniu HTML. Nie jest to idealne, ale działa. Na szczęście, CBISAPI samo zgłosi wszelkie wyjątki, więc musisz jedynie troszczyć się o błędy logiczne.
Inna niewygoda polega na tym, że zwykle trzeba zamknąć IIS, abyś mógł skopiować (lub przebudować) swoje pliki. Możesz ustawić klucz Rejestru HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SV C/Parameters/CacheExtensions na zero, co spowoduje, że IIS będzie szybciej zwalniał pliki. Oprócz tego, za każdym razem, gdy ponownie tworzysz serwer VB, musisz zarejestrować go na komputerze IIS.
Dalsze kierunki
Istnieje wiele ulepszeń, jakie możesz wprowadzić do DLL-a C++, CBISAPI. Jedną z oczekiwanych zmian byłaby automatyczna analiza łańcucha zapytań i treści. Mógłbyś stworzyć sparametryzowaną właściwość o nazwie ParsedQueryString. Mogłaby akceptować nazwę zmiennej zapytania i zwracać jej wartość, na przykład tak:
Dim s as String
s=server.ParsedQueryString("HI")
Oprócz tego, bawiłem się nieco dodaniem trybu debuggowania, który mógłbyś włączyć przy pomocy opcji w łańcuchu zapytań. Innym pomysłem byłoby stworzenie wersji CBISAPI.DLL dla debuggera (może CBISAPID.DLL). Ta wersja drukowałaby informacje debuggera do strumienia HTML. W związku z tym powstają dwa problemy. Po pierwsze, jeśli pisane przez ciebie rozszerzenie chce ustawiać nagłówki, musi to robić przed wysłaniem czegokolwiek (włącznie z informacjami debuggera). Po drugie, co zrobić, jeśli rozszerzenie zapisuje coś innego niż plik HTML (na przykład plik GIF)?
Na koniec, prawdopodobnie dobrym pomysłem byłoby ograniczenie zdalnemu użytkownikowi możliwości tworzenia serwerów ActiveX na Twoim komputerze. Oczywiście, ryzyko jest minimalne, ponieważ serwer ActiveX musiałby mieć punkt wejścia, który oczekiwałby pojedynczego obiektu. Nie byłoby jednak trudno sprawić, aby CBISAPI odczytywał plik konfiguracyjny na serwerze, definiujący nazwy symboliczne serwerów ActiveX. Jeśli w żądaniu zostałaby podana nazwa nie występująca na liście, CBISPAI odrzucałby polecenie.
Tradycyjny ISAPI MFC
Bardziej tradycyjne rozszerzenie ISAPI MFC znajdziesz na listingu 9.8. Możesz łatwo wygenerować program tego typu, używając kreatora MFC ISAPI Wizard (rys. 9.3). Ten kreator tworzy DLL-a zawierającego funkcje, które mogą być wywołane przez formularz lub dokument HTML.
MFC automatyzuje cały proces, do wydzielania zmiennych z danych używając mapy parsera. Ta mapa wygląda podobnie jak mapa komunikatów lub mapa danych, lecz ClassWizard jej nie obsługuje. W związku z tym, pozycje mapy parsera musisz dodawać samodzielnie.
Listing 9.8. Rozszerzenie ISAPI stworzone w oparciu o MFC
// ISAPIMFC.CPP - Implementation file for your Internet Server
// Example Form Extension
#include "stdafx.h"
#include "isapimfc.h"
///////////////////////////////////////////////////////////////////////
// The one and only CWinApp object
// NOTE: You may remove this object if you alter your project to no
// longer use MFC in a DLL.
CWinApp theApp;
///////////////////////////////////////////////////////////////////////
// command-parsing map
BEGIN_PARSE_MAP(CExtension, CHttpServer)
// TODO: insert your ON_PARSE_COMMAND() and
// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.
// For example:
ON_PARSE_COMMAND(Default, CExtension, ITS_EMPTY)
ON_PARSE_COMMAND(Greet,CExtension,ITS_PSTR)
ON_PARSE_COMMAND_PARAMS("name=~")
DEFAULT_PARSE_COMMAND(Default, CExtension)
END_PARSE_MAP(CExtension)
///////////////////////////////////////////////////////////////////////
// The one and only CExtension object
CExtension theExtension;
///////////////////////////////////////////////////////////////////////
// CExtension implementation
CExtension::CExtension()
{
}
CExtension::~CExtension()
{
}
BOOL CExtension::GetExtensionVersion(HSE_VERSION_INFO* pVer)
{
// Call default implementation for initialization
CHttpServer::GetExtensionVersion(pVer);
// Load description string
TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];
ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));
_tcscpy(pVer->lpszExtensionDesc, sz);
return TRUE;
}
///////////////////////////////////////////////////////////////////////
// CExtension command handlers
void CExtension::Default(CHttpServerContext* pCtxt)
{
StartContent(pCtxt);
WriteTitle(pCtxt);
*pCtxt << _T("This default message was produced by the Internet");
*pCtxt << _T(" Server DLL Wizard. Edit your CExtension::Default()");
*pCtxt << _T(" implementation to change it.\r\n");
EndContent(pCtxt);
}
// Do not edit the following lines, which are needed by ClassWizard.
#if 0
BEGIN_MESSAGE_MAP(CExtension, CHttpServer)
//{{AFX_MSG_MAP(CExtension)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
#endif // 0
///////////////////////////////////////////////////////////////////////
// If your extension will not use MFC, you'll need this code to make
// sure the extension objects can find the resource handle for the
// module. If you convert your extension to not be dependent on MFC,
// remove the comments arounn the following AfxGetResourceHandle()
// and DllMain() functions, as well as the g_hInstance global.
/****
static HINSTANCE g_hInstance;
HINSTANCE AFXISAPI AfxGetResourceHandle()
{
return g_hInstance;
}
BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason,LPVOID lpReserved)
{
if (ulReason == DLL_PROCESS_ATTACH)
{
g_hInstance = hInst;
}
return TRUE;
}
****/
void CExtension::Greet(CHttpServerContext * pCtxt, char *name)
{
StartContent(pCtxt);
WriteTitle(pCtxt);
if (*name=='~')
{
*pCtxt << _T("Hello <B>");
*pCtxt << name;
}
else
*pCtxt << _T("Powinieneś się przedstawić!");
EndContent(pCtxt);
}
Na szczęście, mapy parsera nie są skomplikowane. Możesz używać makr podanych w tabeli 9.13. Makro ON_PARSE_COMMAND informuje MFC, że istnieje funkcja, którą chcesz zamapować na określoną nazwę. W listingu 9.8 występują dwie takie pozycje: Default and Greet (pozdrowienie). Te funkcje nie zwracają wartości (a ściśle, zwracają typ void) i wymagają argumentów określonych w trzecim argumencie makra. Ten obiekt pozwala funkcjom na współpracę z serwerem.
Tabela 9.3 Makra mapy parser
a Makro Znaczenie
BEGIN_PARSE_MAP ON_PARSE_COMMAND ON_PARSE_COMMAND_PARAMS DEFAULT_PARSE_COMMAND
END PARSE_MAP
Rzpoczyna mapę parsera
Identyfikuje punkt wejścia do DLL-a
Parametry dla poprzedniego polecenia parsera Domyślny punkt wejścia do DLL-a
Koniec mapy
Jeśli funkcja nie wymaga żadnych dodatkowych argumentów (tzn. innych niż obiekt CHttpServerContext), dla takiej funkcji nie musisz stosować innych makr poza ON_PARSE_COMMAND. Jeśli funkcja wymaga podania argumentów, bezpośrednio po makrze ON_PARSE_COMMAND musi następować makro ON_PARSE_COM-MAND_PARAMS. To makro pozwala na określenie wartości formularza (lub łańcucha zapytania) odpowiadających argumentom funkcji. Możesz także podać ich domyślne wartości. Na przykład, nasz program w mapie parsera zawiera, między innymi, następujące linie:
ON_PARSE_COMMAND(Greet,CExtension,ITS_PSTR) ON_ PARSE_COMMAND_PARAMS("name=~")
Oznaczają one, że argument funkcji (w postaci łańcucha) powinien otrzymać wartość z pola name formularza. Jeśli to pole nie zawierałoby danych, MFC dostarczy znak tyldy (-).
Co zrobisz, jeśli twoja funkcja wymagałaby podania dwóch argumentów, powiedzmy FName i LName? Wtedy twoja mapa mogłaby wyglądać następująco:
ON_PARSE_COMMAND(Greet,CExtension,ITS_PSTR) ON_ PARSE_COMMAND_PARAMS("Fname=~ LName=~")
Wewnątrz swojej funkcji ISAPI możesz pisać do strumienia HTML używając operatora « obiektu CHttpServerContext. Możesz także wywoływać funkcje (tabela 9.14) tego obiektu w celu współpracy z serwerem.
Tabela 9.14. Składowe klasy CHttpServerContex
tSkładowa Definicja
M_pECB Extension Control Błock.
M_pStream Używany CHtmlStream.
GetServerVariable Zwraca zmienną serwera.
WriteClient Zapisuje dane do klienta.
ReadClient Pobiera dane od klienta.
ServerSupportFunction Wywołuje funkcje do (na przykład) przekierowywania,
mapowania ścieżek idt.
<< Zapisuje do wyjściowego strumienia HTML.
Gdy chcesz wywołać swoją funkcję, umieszczasz ją po łańcuchu zapytań w URL-u. Tak więc, aby wywołać Default, możesz użyć URL-a http://www.al-williams.com/awc/isapi/ isapimfc.dll?Default.
Ponieważ w mapie parsera dla funkcji Default występuje makro DEFAULT_ PARSE_COMMAND, MFC wywoła tę funkcję, nawet jeśli pominiesz łańcuch zapytań. Aby wywołać funkcję Guess dla formularza, napiszesz:
<FORM ACTION=http://www.al-williams.com/awc/isapi/isapimfc.dll?Guess METHOD=POST>
Powinieneś zawsze używać POST, ponieważ GET, w przypadku pewnych przeglądarek może powodować problemy. W łańcuchu zapytań może także nadać wartość zmiennej Name, na przykład:
<A HREF=http://www.al-williams.com/awc/isapi/isapimfc.dll?Guess name=none>
Pisanie programów ISAPI nie jest szczególnie trudne, z MFC, czy bez niego. Gdy przywykniesz do manipulowania mapami parsera, zobaczysz, że potrafisz bardzo szybko tworzyć rozszerzenia ISAPI.
Podsumowanie
Choć Microsoft spóźnił się nieco na starcie do wyścigu o Internet, z pewnościąjuż nadrobił straty. Wygląda na to, że obecnie każdy produkt Microsoftu w jakiś sposób współpracuje z Internetem. Wyjątkiem nie jest także MFC.
W pewnych przypadkach, gniazda MFC i rozszerzenia ISAPI są naprawdę użyteczne. W innych przypadkach można je spokojnie pominąć.
Użycie kontrolek ActiveX do transmisji danych jest rodzajem zadania, które naprawdę wykazuje prawdziwą potęgę technologii ActiveX.
Praktyczny przewodnik MFC i Internet
Używanie gniazd
Używanie gniazd jako strumieni
Użycie Winlnet w MFC
Kontrolka Internet Transfer
Pisanie filtrów i rozszerzeń ISAPI w MFC
Kiedy nie należy używać ISAPI
CBISAPI - Obiektowe podejście do ISAPI
Internet jest największym szałem, jaki komputerowy świat dotąd widział. Choć pisanie oprogramowania sieciowego jest zajęciem kilku specjalistów, coraz większa ilość programistów musi dołączać do swoich programów obsługę Internetu. Na szczęście, MFC ułatwia także programowanie dla Internetu.
Używanie gniazd
MFC obsługuje gniazda poprzez klasy CSocket oraz CAsyncSocket. Zwykle będziesz używał klasy CSocket, która zapewnia pewne połączenie pomiędzy dwoma komputerami. Zaróno serwer, jak i klient wymagają obiektu. Wymagane w obu przypadkach wywołania znajdziesz w tabelach 9.1 i 9.2.
Gdy nawiążesz połączenie, możesz się łatwo komunikować, używając metod Send oraz Receive. Możesz także przesyłać całe obiekty przy pomocy serializacji (zajrzyj do następnej sekcji).
Zwykle nie będziesz przesłaniał klas gniazda. Jedyny wyjątek ma miejsce wtedy, gdy chcesz użyć gniazda asynchronicznie. W takim wypadku musisz przesłonić funkcje powiadamiania (na przykład OnReceive). Odpowiedni przykład znajdziesz w listingu 9.3.
Używanie gniazd jako strumieni
Klasa CSocketFile umożliwia traktowanie gniazda, tak jakby stanowiło archiwum na dysku. Oczywiście, poprzez serializację, archiwum może obsługiwać całe obiekty MFC. Gdy konsrtruujesz obiekt CSocketFile, przekazujesz wskaźnik do istniejącego obiektu CSocket. Następnie używasz gniazda do stworzenia jednego lub kilku archiwów. Oto fragment kodu wydzielony z listingu 9. l:
sockfile=new CSocketFile(&socket);
xmit=new CArchive(sockfile,CArchive::storę);
rcv==new CArchive(sockfile,CArchive::load);
W tym przykładzie program tworzy dwa archiwa dla tego samego pliku, po jednym w każdym kierunku (przesyłanie i odbiór). Możesz ich używać w ten sam sposób, jak każdy inny obiekt CArchive (na przykład, przy pomocy metody Serialize lub operatora « obiektu).
Użycie WinInet w MFC
Winlnet to jedna z bibliotek w Windows ułatwiająca współpracę z Internetem. MFC umożliwa dostęp do Winlnet poprzez obiekt CInternetSession. Możesz użyć tego obiektu do otwarcia URL-a lub rozpoczęcia sesji HTTP, FTP lub Gopher (patrz tabela 9.5).
Najprostszym sposobem użycia obiektu jest wywołanie metody OpenURL, która zwraca obiekt wyprowadzony z klasy CStdioFile (lub, być może, obiekt klasy CStdioFile). Jeśli chcesz mieć większą kontrolę, możesz wywoływać także inne metody (patrz tabela 9.5), ale OpenURL powinna zaspokoić większość twoich potrzeb.
Kontrolka Internet Transfer
Kontrolka Internet Transfer to kontrolka ActiveX, pozwalająca Twoim programom na pobieranie danych z Internetu. Przekonasz się, że ten obiekt ActiveX nie oferuje wiele więcej niż standardowa biblioteka Winlnet. W celu dostępu do obiektu możesz w dalszym ciągu wywoływać OpenURL. Wszystkie wywołania znajdziesz w tabeli 9.7.
Zwróć uwagę, że jeśli chcesz przeanalizować i wyświetlić HTML-a, zamiast tej kontrolki dużo łatwiej będzie użyć obiekt ActiveX Internet Explorer (Shell.Explorer w SHDOCVW.DLL).
Pisanie filtrów i rozszerzeń ISAPI w MFC
Programy ISAPI umożliwiają rozszerzenie funkcji serwera IIS Microsoftu (Internet Information Server) oraz kilku innych serwerów WWW, bez konieczności pisania skryptów CGI. ISAPI posiada więcej możliwości i jest bardziej efektywne niż tradycyjne metody (takie jak CGI).
MFC zawiera kreatora pomagającego w pisaniu filtrów i rozszerzeń ISAPI (rysunek 9.3). Możesz potem wypełnić DLL-a funkcjami, które mogą być wywoływane przez strony WWW (łącznie z formularzami). Następnie musisz ręcznie skonstruować mapę parsera, w celu powiązania swoich funkcji z nazwami używanymi w kodzie HTML.
Twoje funkcje powinny zwracać typ void i wymagać przynajmniej jednego argumentu (obiektu CHttpServerContext). Ten obiekt kontekstu serwera umożliwia pobranie informacji o serwerze WWW, a także wydawanie mu poleceń. Przy pomocy tego obiektu możesz zapisywać dane do wyjściowego strumienia (- przykład znajdziesz w listingu 9.8). Możesz oczywiście zastosować także inne argumenty, których oczekujesz od kodu HTML.
Mapa parsera jest podobna do innych map występujących w MFC - także i tym razem mamy do czynienia z makrami (patrz tabela 9.13). Podczas generowania projektu kreator stworzy szkieletową mapę - powinieneś dodać do niej kolejne makra ON_PARSE_ COMMAND informujące MFC o funkcjach, które napisałeś oraz o tym, jak wielu argumentów wymagają. Jeśli funkcja oczekuje od HTML-a dodatkowych argumentów, bezpośrednio za tym makrem powinno wystąpić makro ON_PARSE_COMMAND_ PARAMS, opisujące argumenty i dostarczające domyślnych wartości. Oto przykład prostej mapy parsera:
BEGIN_PARSE_MAP(CExtension, CHttpServer)
ON_PARSE_COMMAND(Default, CExtension, ITS_EMPTY)
ON_PARSE_COMMAND(Greet, CExtension, ITS_PSTR)
ON_PARSE_COMMAND_PARAMS ( "name=~" )
DEFAULT_PARSE_COMMAND(Default, CExtension) END_PARSE_MAP(CExtension)
Pierwsze ON_PARSE_COMMAND definiuje funkcję Default, nie wymagającą żadnych argumentów. Ta funkcja występuje także w przypadku makra DEFAULT_PARSE_ COMMAND. To makro informuje MFC, że jeśli HTML nie dostarczy nazwy funkcji, powinna zostać użyta właśnie ta funkcja.
Drugie ON_PARSE_COMMAND definiuje funkcję (Greet), wymagającą pojedynczego argumentu w postaci łańcucha. Następna linia informuje MFC, że argument name HTML-a (z formularza lub łańcucha zapytań) zawiera wartość argumentu. Jeśli HTML nie dostarczy argumentu, MFC użyje znaku tyldy (~), gdyż właśnie ten znak jest użyty w makrze jako domyślna wartość. Kompletny przykład jest zawarty w listingu 9.8.
HTML zwykle przesyła argumenty przez pola w formularzu. Działanie Twojego formularza jest określone przy pomocy nazwy DLL-a, znaku zapytania oraz nazwy funkcji, którą chcesz wywołać. Oczywiście, serwer WWW posiadać kartotekę zawierającą potrzebny plik DLL-a.
Kiedy nie należy używać ISAPI
Warto zauważyć, że niektóre tradycyjne wykorzystania ISAPI są lepiej obsługiwane przez zawarty na serwerze skrypt wykorzystujący pliki ASP (Active Sewer Page). Te pliki umożliwiają wzajemne mieszanie HTML-a i funkcji napisanych w YBScript (języku podobnym do Visual Basica) i JavaScript (języka podobnego do Javy). Opis plików ASP wykracza poza zakres tej książki, ale jeśli Twój serwer może z nich korzystać, zanim pomyślisz o napisaniu programu CGI lub ISAPI, pomyśl właśnie o plikach ASP.
CBISAPI - Obiektowe podejście do ISAPI
W tym przykładzie znalazły się dwa programy związane z ISAPI. Listing 9.8 przedstawia tradycyjny program rozszerzenia ISAPI stworzony w MFC. Jednak program ISAPI znajdziesz także na listingu 9.5. Ten program to w rzeczywistości podejście do ISAPI z wykorzystaniem ActiveX. Jest w nim tworzony obiekt ActiveX wykonujący pracę, który z kolei jest udostępniany innemu obiektowi, pozwalającemu na współpracę z serwerem.
Jedną z korzyści takiego podejścia jest możliwość pisania rozszerzeń ISAPI w dowolnym języku mogącym skorzystać z ActiveX. Przykładowy program z listingu 9.6 używa do tego Visual Basica. Oczywiście, MFC także może tworzyć obiekty ActiveX.
Zauważysz, że ponieważ CBISAPI potrzebuje niskopoziomowej kontroli nad ISAPI, nie korzysta ze standardowego podejścia ISAPI MFC. Zamiast tego, współpracuje z ISAPI bezpośrednio. MFC jednak znacznie ułatwił pracę fragmentu kodu odnoszącego się do ActiveX.
Rozdział 10 MFC i bazy danych
Ponieważ ClassWizard potrafi połączyć pola bazy danych z polami widoku rekordów, napisanie bazy danych przy pomocy MFC jest dość łatwe. Bez względu na to, czy piszesz program przetwarzania wsadowego, czy program z interfejsem użytkownika, kod wygenerowany przez ClassWizarda może znacznie ułatwić Ci życie.
Wiele się zmieniło od czasu pojawienia się pierwszych komputerów osobistych. Choć w tamtych czasach zajmowałem się głównie sprzętem, wszyscy posiadający choćby niewielką wiedzę o komputerach traktowani byli jak kapłani, wykonujący rzeczy wykraczające daleko poza wyobrażenia zwykłych śmiertelników. Tęsknię za tymi dniami. Dzisiaj, gdy mówisz komuś, że programujesz komputery, zwykle słyszysz odpowiedź w rodzaju: “Och, moja dziesięcioletnia córka w zeszłym miesiącu uczyła się o tym w szkole." Na pewno, ale czy potrafi zastosować tablice relacji w zwykłym formularzu?
Jeszcze zanim komputery osobiste stały się popularne, przeciętny człowiek nieodmiennie wiązał komputery z bazami danych. Oczywiście w takim odbiorze wydatnie pomogły kino i telewizja. Nawet mój ulubiony serial, Star Trek, musiał mieć wszystkowiedzący komputer: “Komputer: zidentyfikuj nazwę RedJack." I oczywiście komputer znał odpowiedź. Robot z Lost in Space też posiadał podobne możliwości. Te komputery miały zainstalowane bazy danych zawierające wiedzę o wszystkim.
Rzeczywiste bazy danych zwykle skupiają się raczej na konkretnym temacie (najczęściej próbując odpowiedzieć na palące pytanie w rodzaju “Kto ma nasze pieniądze?"). Zadziwiające jest także, jak często programiści muszą zmagać się z bazami danych w najprzeróżniejszych formach i postaciach. Nawet jeśli twoje podstawowe zajęcie nie wiąże się z bazami danych, z pewnością zauważyłeś, że nie raz miałeś z nimi do czynienia.
Ponieważ występowanie baz danych jest tak powszechne, wiele środowisk programowych stara się jak najbardziej ułatwić ich używanie. Także MFC nie jest wyjątkiem. Jeśli potrafisz użyć okien dialogowych, będziesz w stanie skonstruować także rozbudowany program bazy danych napisany z pomocą MFC.
Na najwyższym poziomie pisanie programu bazy danych przebiega prawie tak samo, jak pisanie programu opartego na formularzach. Po prostu, podczas tworzenia projektu prosi się AppWizarda o połączenie programu z bazą danych.
AppWizard wykonuje praktycznie całą brudną robotę. Tworzy klasę specjalnego widoku oraz obiekt reprezentujący tzw. recordset (zestaw rekordów). Ten obiekt zawiera w sobie połączenie z bazą danych.
Oprócz tego, AppWizard tworzy pasek narzędzi umożliwiający poruszanie się po tabeli. Wszystko co musisz zrobić, to dodać pola do widoku (który jest bardzo podobny do CFormYiew) i połączyć je ze zmiennymi w rekordsecie (używając ClassWizarda). W najprostszym przypadku nie jest wymagane nic więcej (rys. 10.1).
To nie wymaga praktycznie żadnej pracy, ale nadaje się jedynie do najprostszych przypadków. TMa szczęście podstawowa architektura jest bardzo elastyczna. Możesz użyć rekordsetu w różnorodnych aplikacjach wymagających dostępu do bazy danych - nawet jeśli nie używasz w nich konwencjonalnych dokumentów i widoków.
Obiekt rekordsetu jest użyteczny w wielu sytuacjach, w których chcesz bezpośrednio manipulować danymi. Istnieją dwa różne typy rekordsetów, które możesz stworzyć. Jeden z nich służy do pracy ze źródłami danych ODBC (Open DataBase Connectivity). Oczywiście, wymaga to, aby dla twojej bazy były dostępne i zainstalowane sterowniki ODBC. Drugim rodzajem jest DAO (Data Access Object). DAO odwołuje się do baz danych Microsoft Access (i kilku innych formantów rozpoznawanych przez engine Microsoft Jet). W pewnych przypadkach użycie DAO zapewnia większą wydajność niż użycie ODBC. Z drugiej strony, ODBC ogólnie sprawuje się lepiej w transakcjach typu klient-serwer. DAO udostępnia więcej sposobów manipulowania strukturą tabel. DAO potrafi także odczytać dowolną bazę danych ODBC, ale użycie DAO ze sterownikami ODBC powoduje obniżenie wydajności (z powodu konieczności przejścia przez dwie warstwy bibliotek). W szczególności, DAO najlepiej współpracuje z plikami Microsoft Access.
Na szczęście, klasy ODBC i DAO są bardzo podobne. Ponieważ DAO wykonuje wszystko to co ODBC a także parę innych rzeczy, bardzo łatwo można przejść z ODBC do DAO. Przejście w drugą stronę może być nieco trudniejsze, zwłaszcza jeśli użyto specjalnych możliwości DAO.
Tabela 10.1. zawiera składowe rekordsetu ODBC (CRecordset). Podobne informacje odnoszące się do obiektu DAO (CDaoRecordset) znajdziesz w tabeli 10.2. Zwróć uwagę na to, że pomiędzy nimi nie ma zbyt wielkich różnic.
Tabela 10.1. Składowa rekordsetu ODBC
Składowa Opis
m_hstmt Uchwyt polecenia ODBC dla rekordsetu.
m_nFields Ilość składowych pól danych w rekordsecie.
m_nParams Ilość składowych parametrów danych w rekordsecie.
m_pDatabase Wskaźnik do obiektu CDatabase, poprzez który rekordset jest połączony ze
źródłem danych.
m_strFilter Klauzula SQL WHERE.
m_strSort Klauzula SQL ORDER BY.
Open Otwiera rekordset.
Close Zamyka rekordset i HSTMT ODBC.
CanAppend Zwraca wartość różną od zera, jeśli przy pomocy AddNew można dodać nowe
rekordy.
CanBookmark Zwraca wartość różną od zera, jeśli rekordset obsługuje zakładki.
CanRestart Zwraca wartość różną od zera, jeśli można wywołać Reąuery w celu ponownego
zapytania.
CanScroll Zwraca wartość różną od zera, jeśli można przewijać rekordy.
CanTransact Zwraca wartość różną od zera, jeśli źródło danych obsługuje transakcje.
CanUpdate Zwraca wartość różną od zera, jeśli rekordset może zostać zaktualizowany.
GetODBCFieldCount Zwraca ilość pól w rekordsecie.
GetRecordCount Zwraca ilość rekordów w rekordsecie.
GetStatus Zwraca stan rekordsetu (bieżący indeks rekordu oraz informuje czy dostępna jest
ostateczna liczba rekordów).
GetTableName Zwraca nazwę tabeli.
GetSQL Zwraca łańcuch SQL.
IsOpen Jeśli rekordset jest już otwarty, zwraca wartość różną od zera.
IsBOF Zwraca wartość różną od zera, jeśli wskaźnik rekordsetu znajduje się przed
pierwszym rekordem.
IsEOF Zwraca wartość różną od zera, jeśli wskaźnik rekordsetu znajduje się poza
ostatnim rekordem.
IsDeleted Zwraca wartość różną od zera, jeśli wskaźnik rekordsetu wskazuje na usunięty
rekord.
AddNew Przygotowuje do dodania nowego rekordu (wywołaj Update aby to dokończyć).
CancelUpdate Anuluje wszystkie zalegające aktualizacje (pochodzące z AddNew lub Update).
Delete Usuwa z rekordsetu bieżący rekord.
Edit Przygotowuje do zmian w bieżącym rekordzie (wywołaj Update, aby dokończyć
edycję).
Update Kończy operację AddNew lub Edit zapisując nowe lub zmodyfikowane dane w
źródle danych.
GetBookmark Przypisuje wartość zakładki rekordu do obiektu parametru
Move Przesuwa wskaźnik rekordsetu o określoną ilość rekordów względem bieżącego
rekordu.
MoveFirst Przenosi wskaźnik rekordsetu na pierwszy rekord w rekordsecie.
MoveLast Przenosi wskaźnik rekordsetu na ostatni rekord w rekordsecie.
MoveNext Przenosi wskaźnik rekordsetu do następnego rekordu.
MovePrev Przenosi wskaźnik rekordsetu do poprzedniego rekordu.
SetAbsolutePosition Przenosi wskaźnik rekordsetu do rekordu o określonym numerze.
SetBookmark Przenosi wskaźnik rekordsetu do rekordu określonego przez zakładkę.
Cancel Anuluje proces lub operację asynchronicznąz drugiego wątku.
FlushResultSet Zwraca wartość różną od zera, jeśli istnieje inny zestaw rezultatów (używane z
predefiniowanymi zapytaniami).
GetFieldValue Zwraca wartość pola w rekordsecie.
GetODBCFieldInfo Zwraca specyficzne informacje na temat pól w rekordsecie.
GetRowsetSize Zwraca ilość rekordów, jakie mają być odczytane pojedynczą operacją
GetRowsFetched Zwraca rzeczywistą ilość rekordów odczytanych pojedynczą operacją.
GetRowsStatus Zwraca stan wiersza po odczycie.
IsFieldDirty Zwraca wartość różną od zera, jeśli określone pole w bieżącym rekordzie zostało
zmienione.
IsFieldNull Zwraca wartość różną od zera, jeśli określone pole w bieżącym rekordzie ma
wartość Null (nie ma wartości).
IsFieldNullable Zwraca wartość różną od zera, jeśli określone pole w bieżącym rekordzie może
zostać ustawione na Null.
RefreshRowset Odświeża dane i stan określonego wiersza (wierszy).
Requery Ponownie wysyła zapytanie do rekordsetu w celu odświeżenia wybranych
rekordów.
SetFieldDirty Określone pole w bieżącym rekordzie oznacza jako zmienione.
SetFieldNull Określone pole w bieżącym rekordzie jest ustawiane na Null (czyli nie
zawierające wartości).
SetLockingMode Ustawia tryb blokowania na “optymistyczny" (domyślny) lub pesymistyczny (w
jaki sposób mają być blokowane zmiany w rekordach).
SetParamNull Ustawia określony parametr na Null.
SetRowsetCursorPosition Przenosi kursor do określonego wiersza w rekordsecie.
Check Wywoływane do sprawdzenia kodu zwracanego przez funkcję ODBC API.
CheckRowsetError Wywoływane do obsługi błędów powstałych podczas odczytu rekordu.
DoBulkFieldExchange Wywoływane do wymiany większej liczby danych pomiędzy źródłem a
rekordsetem.
DoFieldExchange Wywoływane do wymiany danych (w obu kierunkach) pomiędzy składowymi
pól danych w rekordsecie a odpowiednim rekordem w źródle danych.
GetDefaultConnect Wywoływane w celu pobrania domyślnego łańcucha połączenia.
GetDefaultSQL Wywoływane w celu pobrania domyślnego łańcucha SQL do wykonania.
OnSetOptions Wywoływane w celu ustawienia opcji określonego polecenia ODBC.
SetRowsetSize Określa ilość rekordów, jakie mają być pobrane podczas pojedynczego odczytu.
Tabela 10.2. Składowe rekordsetu DAO
Składowa Opis
m_bCheckCacheForDirtyFields Wskazuje, czy pola są automatycznie zaznaczane jako
zmienione
.m_pDAORecordset Wskaźnik do interfejsu DAO obiektu rekordsetu
m_nFields Ilość składowych pól danych w klasie rekordsetu oraz ilość kolumn zaznaczonych przez rekordset w źródle danych.
m_nParams Zawiera ilość składowych parametrów danych w klasie rekordsetu oraz
liczbę parametrów przekazanych z zapytaniem rekordsetu.
m_pDatabase Źródłowa baza danych dla tego zestawu wyników (wskaźnik do obiektu
CDaoDatabase).
m_strFilter Polecenie SQL WHERE.
m_strSort Polecenie SQL ORDER BY.
Close Zamyka rekordset.
Open Tworzy nowy rekordset.
CanAppend Zwraca wartość różną od zera, jeśli przy pomocy AddNew mogą być
dodawane nowe rekordy.
CanBookmark Zwraca wartość różną od zera, jeśli rekordset obsługuje zakładki.
CanRestart Zwraca wartość różną od zera, jeśli można wywołać Requery w celu
ponownego zapytania.
CanScroll Zwraca wartość różną od zera, jeśli można przewijać rekordy.
CanTransact Zwraca wartość różną od zera, jeśli źródło danych obsługuje transakcje.
CanUpdate Zwraca wartość różną od zera, jeśli rekordset może zostać zaktualizowany.
GetCurrentIndex Zawiera nazwę najczęściej używanego indeksu w indeksowanym rekordsecie typu tabela.
GetDateCreated Zwraca datę i czas stworzenia bazowej tablicy, na której opiera się obiekt CDaoRecord.
GetDateLastUpdated Zwraca datę i czas ostatnich zmian dokonanych w postaci bazowej tablicy, na której opiera się obiekt CDaoRecord.
GetEditMode Zwraca wartość wskazującą stan edycji bieżącego rekordu.
GetLastModifiedBookmark Używane do wyznaczenia ostatnio dodanego lub aktualizowanego
rekordu.
GetName Zawiera nazwę rekordsetu.
GetParamValue Zwraca bieżącą wartość określonego parametru przechowywanego w
powiązanym obiekcie DAOParameter.
GetRecordCount Zwraca ilość rekordów używanych w obiekcie rekordsetu.
GetSQL Zwraca łańcuch SQL użyty do zaznaczenia rekordów dla rekordsetu.
GetType Wywoływane do wyznaczenia typu rekordsetu: tabela, dynaści lub
snapshot.
GetValidationRule Zwraca regułę zatwierdzającą dane po wprowadzeniu do pola.
GetValidationText Zwraca tekst wyświetlany, jeśli dane nie mogą być zatwierdzone.
IsBOF Zwraca wartość różną od zera, jeśli wskaźnik rekordsetu znajduje się
przed pierwszym rekordem.
IsDeleted Zwraca wartość różną od zera, jeśli wskaźnik rekordsetu wskazuje na
usunięty rekord.
IsEOF Zwraca wartość różną od zera, jeśli wskaźnik rekordsetu znajduje się
poza ostatnim rekordem.
IsFieldDirty Zwraca wartość różną od zera, jeśli określone pole bieżącego rekordu
zostało zmienione.
IsFieldNull Zwraca wartość różną od zera, jeśli określone pole w bieżącym rekordzie
jest puste.
IsFieldNullable Zwraca wartość różną od zera, jeśli określone pole w bieżącym rekordzie
może być puste.
IsOpen Zwraca wartość różną od zera, jeśli wywołano Open.
SetCurrentIndex Wywoływane do ustawienia indeksu dla rekordsetu typu tabela.
SetParamValue Ustala wartość parametru przechowywanego w powiązanym obiekcie
DAOParameter.
SetParamValueNull Wartość określonego parametru jest ustawiana na Null.
AddNew Przygotowuje do dodania nowego rekordu (wywołaj Update aby to
dokończyć).
CancelUpdate Anuluje wszystkie zalegające aktualizacje (pochodzące z AddNew lub
Update).
Delete Usuwa z rekordsetu bieżący rekord.
Edit Przygotowuje do zmian w bieżącym rekordzie (wywołaj Update, aby
dokończyć edycję).
Update Kończy operację AddNew lub Edit zapisując nowe lub zmodyfikowane
dane w źródle danych.
Find Lokalizuje pierwsze, następne, poprzednie lub ostatnie wystąpienie
określonego łańcucha, spełniającego podane kryteria w rekordsecie typu
dynaset. Zlokalizowany rekord staje się rekordem bieżącym.
FindFirst Lokalizuje pierwszy rekord spełniający podane kryteria w rekordsecie
typu dynaset lub snapshot. Zlokalizowany rekord staje się rekordem
bieżącym.
FindLast Lokalizuje ostatni rekord spełniający podane kryteria w rekordsecie typu
dynaset lub snapshot. Zlokalizowany rekord staje się rekordem
bieżącym.
FindNext Lokalizuje następny rekord spełniający podane kryteria w rekordsecie
typu dynaset lub snapshot. Zlokalizowany rekord staje się rekordem
bieżącym.
FindPrev Lokalizuje poprzedni rekord spełniający podane kryteria w rekordsecie
typu dynaset lub snapshot. Zlokalizowany rekord staje się rekordem
bieżącym.
GetAbsolutePosition Zwraca numer rekordu bieżącego obiektu rekordsetu.
GetBookmark Zwraca wartość reprezentującą zakładkę rekordu.
GetPercentPosition Zwraca pozycję bieżącego rekordu jako procent ogólnej liczby rekordów.
Move Przesuwa wskaźnik rekordsetu o określoną ilość rekordów względem
bieżącego rekordu.
MoveFirst Przenosi wskaźnik rekordsetu na pierwszy rekord w rekordsecie.
MoveLast Przenosi wskaźnik rekordsetu na ostatni rekord w rekordsecie.
MoveNext Przenosi wskaźnik rekordsetu do następnego rekordu.
MovePrev Przenosi wskaźnik rekordsetu do poprzedniego rekordu.
Seek Wyszukuje w rekordsecie typu tabela rekord spełniający podane kryteria
dla bieżącego indeksu. Znaleziony rekord staje się rekordem bieżącym.
SetAbsolutePosition Przenosi wskaźnik rekordsetu do rekordu o określonym numerze.
SetBookmark Przenosi wskaźnik rekordsetu do rekordu określonego przez zakładkę.
SetPercentPosition Przenosi wskaźnik rekordsetu do pozycji określonej jako procent ogólnej
liczby rekordów w rekordsecie.
FillCache Wypełnia cały lub część lokalnego bufora obiektu rekordsetu
zawierającego dane ze źródła danych ODBC.
GetCacheSize Zwraca wartość określającą ilość rekordów w rekordsecie typu dynaset,
zawierającym dane buforowane lokalnie ze źródła danych ODBC.
GetCacheStart Zwraca wartość określającą zakładkę pierwszego rekordu w buforze.
GetFieldCount Zwraca wartość reprezentującą ilość pól w rekordsecie.
GetFieldInfo Zwraca specyficzne informacje na temat pól w rekordsecie.
GetFieldValue Zwraca wartość pola.
GetIndexCount Zwraca ilość indeksów w tabeli powiązanej z rekordsetem.
GetIndexInfo Zwraca różne informacje o indeksie.
GetLockingMode Zwraca wartość reprezentującą sposób blokowania pól obowiązujący
podczas edycji.
Requery Ponownie wysyła zapytanie do rekordsetu w celu odświeżenia
wybranych rekordów.
SetCacheSize Ustala wartość określającą ilość rekordów danych rekordsetu typu
dynaset mieszczących się w lokalnym buforze źródła danych ODBC.
SetCacheStart Ustala wartość określającą zakładkę pierwszego rekordu rekordsetu,
który ma znaleźć się w buforze.
SetFieldDirty Określone pole w bieżącym rekordzie oznacza jako zmienione.
SetFieldNull Określone pole w bieżącym rekordzie jest ustawiane na Null (czyli nie
zawiera wartości).
SetFieldValue Ustala wartość pola.
SetFieldValueNull Ustala wartość pola na Null.
SetLockingMode Ustala wartość określającą sposób blokowania pola podczas edycji.
DoFieldExchange Wywoływane do wymiany danych (w obu kierunkach) pomiędzy
składowymi pól danych w rekordsecie a odpowiednim rekordem w
źródle danych.
GetDefaultDBName Zwraca nazwę domyślnego źródła danych.
GetDefaultSQL Wywoływane w celu pobrania domyślnego łańcucha SQL do wykonania.
Przygotowanie bazy danych
Aby móc pracować z bazą danych, musisz przygotować źródło danych ODBC. Możesz to zrobić w Panelu sterowania (rys. 10.2). Przykładowy program w tej książce używa danych użytkownika o nazwie Books. Jeśli tworzysz bazę danych powiązaną z zestawem danych, lub otwierasz istniejącą bazę, program otworzy ją automatycznie podczas startu. Jeśli pole nazwy pliku pozostawisz puste, program na początku poprosi o podanie nazwy pliku bazy danych.
Szczegóły bazy danych
Aby stworzyć program bazy danych, musisz jedynie uruchomić AppWizarda. Wiele programów baz danych to programy SDI, gdyż zwykle pracuje się tylko z jednym rekordem na raz. Jeśli jednak tego chcesz, możesz wybrać aplikację MDI. W drugim kroku kreatora (rys. 10.3) wybierz opcję Database view without file support. W ten sposób kreator doda do projektu potrzebną obsługę plików. Jeśli chcesz, możesz dodać obsługę se-rializacji obiektów do plików (opcja Database view with file support) lub w swoich plikach umieścić tylko nagłówki, tak aby móc tworzyć i używać własnych obiektów baz danych (opcja Header files only). Domyślnie, rekordset pobiera całą tabelę, ale zwykle możesz zwrócić własne zapytanie w składowej GetDefaultSQL.
Klikając na przycisku Data Source musisz wskazać także źródło danych (rys. 10.4). W kolejnym oknie dialogowym możesz wskazać ODBC oraz odpowiednie źródło. Możesz także wybrać jeden z trzech sposobów, w jaki chcesz pracować z tabelą:
• Dynaset - Umożliwia dynamiczną pracę z rekordsetem, wspólnie z innymi użytkownikami;
• Snapshot - Umożliwia pracę z tabelą z chwili pobrania danych;
• Table - Praca bezpośrednio z tabelą zamiast pobranych danych.
Gdy kreator zakończy pracę, musisz jedynie umieścić elementy dialogowe w szablonie dialogu (podobnie jak w przypadku widoku formularza). Następnie, przy pomocy zakładki Member Yariable ClassWizarda, połącz te elementy z polami bazy danych (rys. 10.5).
Jeśli nie korzystasz z przeliczanych pól lub innych specjalnych właściwości, a chcesz jedynie przeglądać i edytować istniejące rekordy, wszystko jest już gotowe. To naprawdę jest aż tak proste.
Dodawanie innych elementów
Jednym z najczęstszych ulepszeń programów baz danych są przeliczane pola. I tak, baza danych sprzedaży może zawierać cenę, ilość oraz rabat. Jeśli chcesz obliczyć końcową cenę, musisz ją przeliczyć. Na szczęście, ponieważ widok rekordu jest bardzo podobny do widoku formularza, łatwo można to zrobić. Po prostu użyj ClassWizarda do przygotowania zmiennej składowej i dalej postępuj jak zwykle. Możesz wykonywać obliczenia na bieżąco, na przykład po zmianie rekordu (użyj zdarzenia OnMove widoku).
Dodawanie i usuwanie rekordów
Kreator nie generuje kodu do wstawiania i usuwania rekordów, ponieważ każdy program wykonuje te operacje inaczej. Można jednak łatwo samemu stworzyć odpowiednie funkcje. Aby usunąć bieżący rekord, wystarczy jedynie wywołać metodę Delete rekordsetu.
Wstawianie nowego rekordu zwykle przebiega w trzech krokach. Najpierw wywołujesz metodę AddNew recordsetu. Następnie ładujesz dane formularza do zmiennych rekordsetu przy pomocy metody UpdateData (podobnie jak W przypadku CFormYiew). Na koniec wywołujesz metodę Update kończąc transakcję. Możesz także wywołać CancelUpdate, anulując tym samym wszelkie zmiany.
Rezygnacja z użycia widoku
Jeśli to będzie potrzebne, możesz stworzyć program bazy danych bez widoku rekordów. Możesz napisać program automatycznie zmieniający wszystkie pola w bazie danych lub program jedynie generujący raporty.
W takich wypadkach zawsze możesz użyć rekordsetu bez widoku rekordów. Dostępne wywołania znajdziesz w tabeli 10.1 (ODBC) i w tabeli 10.2 (DAO).
Z tych tabel wynika, że obiekt rekordsetu to po prostu użyteczny interfejs do twojej bazy danych. DAO umożliwia nawet tworzenie tabel i manipulowanie ich strukturą. W przypadku ODBC, aby osiągnąć taką funkcjonalność, musisz wywoływać ODBC bezpośrednio.
Przykładowy program
Ponieważ programowanie baz danych w MFC jest takie proste, przykładowy program także będzie prosty (rys. 10.1). Powstał on jako nieco uzupełniony oryginalny program wygenerowany przez AppWizarda. Dodałem do niego jedynie kod pozwalający na usuwanie rekordów, a także przeliczane pole.
Rekordset znajdziesz w listingu 10.1. Jest dokładnie taki, jaki stworzył AppWizarda; bez żadnych modyfikacji. Listing 10.2 zawiera widok, który posiada kilka nowych elementów.
Listing 10.1. Przykładowy rekordset.
// dbdemoSet.cpp : implementation of the CDbdemoSet class
//
#include "stdafx.h"
#include "dbdemo.h"
#include "dbdemoSet.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CDbdemoSet implementation
IMPLEMENT_DYNAMIC(CDbdemoSet, CRecordset)
CDbdemoSet::CDbdemoSet(CDatabase* pdb)
: CRecordset(pdb)
{
//{{AFX_FIELD_INIT(CDbdemoSet)
m_Title = _T("");
m_ISBN = _T("");
m_Pages = 0;
m_warehouse = 0;
m_shelf = 0;
m_nFields = 5;
//}}AFX_FIELD_INIT
m_nDefaultType = snapshot;
}
CString CDbdemoSet::GetDefaultConnect()
{
return _T("ODBC;DSN=Books");
}
CString CDbdemoSet::GetDefaultSQL()
{
return _T("[Books]");
}
void CDbdemoSet::DoFieldExchange(CFieldExchange* pFX)
{
//{{AFX_FIELD_MAP(CDbdemoSet)
pFX->SetFieldType(CFieldExchange::outputColumn);
RFX_Text(pFX, _T("[Title]"), m_Title);
RFX_Text(pFX, _T("[ISBN]"), m_ISBN);
RFX_Int(pFX, _T("[Pages]"), m_Pages);
RFX_Int(pFX, _T("[InWhse]"), m_warehouse);
RFX_Int(pFX, _T("[OnShelf]"), m_shelf);
//}}AFX_FIELD_MAP
}
/////////////////////////////////////////////////////////////////////////////
// CDbdemoSet diagnostics
#ifdef _DEBUG
void CDbdemoSet::AssertValid() const
{
CRecordset::AssertValid();
}
void CDbdemoSet::Dump(CDumpContext& dc) const
{
CRecordset::Dump(dc);
}
#endif //_DEBUG
Analiza przykładu
Gdy otworzysz projekt przykładowego programu i przywołasz ClassWizarda, zauważysz, że powiązania zmiennych składowych z widokiem są nieco niezwykłe (rys. 10.5). Większość z nich ma na początku strzałkę. Dzieje się tak, ponieważ dane istnieją w obiekcie rekordsetu zamiast w widoku. Możesz także wybrać rekordset i zobaczyć, które kolumny bazy danych są powiązane ze zmiennymi w rekordsecie (rys. 10.6).
Rekordset posiada funkcję o nazwie GetDefaultConnect. W tym miejscu program pobiera łańcuch połączenia używany do otwarcia bazy danych. Tę funkcję tworzy AppWizard, więc zwracana wartość jest odpowiednia dla bazy danych wskazanej podczas tworzenia projektu. Możesz jednak zmienić tę funkcję zanim dostarczysz swój program lub gdy zmieni się baza danych. Możesz ją także zmienić, jeśli chcesz stworzyć interfejs użytkownika pozwalający na wybór bazy danych.
Kod wymagany do dodania rekordu (listing 10.2) jest ściśle uzależniony od tego, w jaki sposób chcesz zaimplementować tę operację. Przykładowy program używa pozycji menu w celu dodania pustego rekordu i wywołuje metodę UjJdate, w momencie gdy użytkownik przechodzi do innego rekordu. Niektóre bazy danych nie pokazują nowego rekordu natychmiast, więc program wywołuje metodę Reąuery. Jej efektem ubocznym jest odświeżenie widoku tak, by wyświetlał pierwszy rekord. Nie wszystkie bazy danych tego wymagają.
Możesz stworzyć kod wstawiający rekordy inaczej. Na przykład, możesz automatycznie dodawać rekord, w momencie gdy użytkownik przejdzie poza koniec bazy danych (używając zdarzenia OnMove).
W przykładowym programie zilustrowałem także wykorzystanie przeliczanego pola. Program oblicza ogólną ilość książek na podstawie dwóch różnych pól. Widok mapuje zwykłą zmienną (m_copies) na statyczny element kontrolny. Niestety, w przypadku statycznych elementów kontrolnych ClassWizard pozwala jedynie na mapowanie łańcuchów, więc dane muszą być w postaci takiego łańcucha. Jeśli chcesz zachować wyświetlanie zawartości jako liczby całkowitej, możesz wziąć pod uwagę pole edycji przeznaczone tylko do odczytu.
Problem z przeliczanym polem polega na wyznaczaniu momentu i miejsca jego aktualizacji. Dobrym miejscem jest funkcja DoDataExchange. MFC wywołuje ją aby upewnić się, że wszystkie elementy kontrolne są zaktualizowane, więc dane rekordsetu także w tym momencie powinny być poprawne. Pamiętaj by umieścić swój kod przed komentarzami ClassWizarda, tak aby dane, które umieszczasz w zmiennej, pojawiły się w elemencie kontrolnym. Widać to na listingu 10.2.
Istnieje wiele sposobów ulepszenia tej prostej aplikacji. Możesz automatycznie dodawać nowe rekordy na końcu bazy lub wracać do aktualnego miejsca po usunięciu bądź dodaniu rekordu. Jednak nawet ten prosty przykład jest użyteczny, gdyż pokazuje, że bez wsparcia ze strony MFC zbudowanie programu bazy danych byłoby dużo trudniejsze.
Podsumowanie
MFC ułatwia dostęp do baz danych dzięki obiektowi CRecordset. Nawet jeśli piszesz program przetwarzania wsadowego (tj. program bez interfejsu użytkownika), możesz bardzo skorzystać na użyciu MFC i klasy CRecordset (lub CDaoRecordset).
Jeśli potrzebujesz interfejsu użytkownika opartego na formularzu, szczególnie użyteczna staje się klasa CRecordYiew. Działa bardzo podobnie jak formularz, lecz ClassWizard potrafi połączyć pola bazy danych (z rekordsetu) bezpośrednio z elementami kontrolnymi widoku.
Podobnie jak w większości programów, możesz stworzyć program bazy danych tak trudny do napisania, jak tylko chcesz. Możesz dodawać dowolne “bajery" i “wodotryski." Jednak w ogromnej większości przypadków, jako podstawy i szkieletu użyjesz kodu wygenerowanego przez kreatora, z bardzo niewielkimi modyfikacjami z twojej strony.
Praktyczny przewodnik MFC i bazy danych
Tworzenie aplikacji bazy danych
Wybieranie ODBC lub DAO
Przygotowanie źródła danych
Łączenie pól bazy danych ze zmiennymi rekordsetu
Łączenie zmiennych rekordsetu z elementami kontrolnymi
Usuwanie rekordów
Dodawanie i aktualizowanie rekordów
Pola przeliczane
MFC bardzo ułatwia pisanie aplikacji baz danych. Gdy poznasz związaną z tym technologię, nie pozostanie już dużo więcej do zrobienia - większość pracy wykona za ciebie AppWizard!
Tworzenie aplikacji bazy danych
Aby zacząć tworzyć aplikację bazy danych, uruchom AppWizarda i w drugim kroku kreatora wybierz jedną z opcji baz danych. Jeśli chcesz wykonać wszystko samodzielnie (co może być dobrym pomysłem, jeśli chcesz przetwarzać dane wsadowo), możesz wybrać drugą opcję, wymuszając, aby AppWizard dodał do projektu jedynie nagłówki. (Pierwsza opcja -domyślna — wyłącza obsługę baz danych.)
Wybranie trzeciej lub czwartej opcji powoduje, że AppWizard tworzy rekordset oraz widok rekordów. Przeznaczeniem rekordsetu jest przechowanie wiersza z wynikowych rekordów lub tabeli bazy danych. Widok rekordu (podobny do widoku formularza) wyświetla wiersz i umożliwia jego edycję. Czwartą opcję wybierz wtedy, gdy chcesz aby AppWizard zajął się także obsługą plików bazy danych, ich otwieraniem, zapisywaniem itd. Wiele baz danych z tego nie korzysta.
Jeśli wybierzesz trzecią lub czwartą opcję, AppWizard stworzy także pasek narzędzi wraz z menu umożliwiającym nawigację po rekordsecie. W drugim oknie kreatora określa się także zestaw wykorzystywanych danych. Możesz wybrać zestaw danych ODBC lub możesz zdecydować się na użycie DAO (patrz następny temat). Możesz także zdecydować się na bezpośredni dostęp do tabeli (tylko DAO), użycie statycznego obrazu danych lub dynamicznie aktualizowanego zestawu rekordów (dynaset).
Wybieranie ODBC lub DAO
MFC może współpracować z bazami danych na dwa sposoby. Możesz użyć nieco starszych klas ODBC lub nowszych klas DAO. ODBC dobrze współpracuje z wszelkimi bazami danych posiadającymi sterowniki ODBC. DAO także współpracuje z takimi bazami, ale szczególnie dobrze radzi sobie z bazami Microsoft Access. DAO także umożliwia dużo łatwiejszą manipulację strukturą tabel.
Której klasy powinieneś użyć? Prawdopodobnie najlepszą decyzją będzie wybranie ODBC. Jeśli jednak używasz tylko Accessa, najlepsze będzie DAO. Jeśli chcesz podjąć się zadań w rodzaju tworzenia nowych tabel, zdecydowanie powinieneś wybrać DAO.
Przygotowanie źródła danych
Aby móc skorzystać z bazy danych, przy pomocy ikony ODBC w Panelu sterowania musisz przygotować źródło danych ODBC. Po prostu dodaj własną lub systemową bazę danych i postępuj zgodnie z instrukcjami. Nie musisz określać pliku zawierającego bazę danych. Jeśli pominiesz nazwę pliku, program poprosi o nią zaraz po uruchomieniu. Jeśli stworzysz nową bazę danych lub dołączysz zestaw danych do istniejącej bazy, program otworzy ją automatycznie, bez żadnej interwencji ze strony użytkownika.
Skąd program wie, który zestaw danych ma otworzyć? Obiekt rekordsetu posiada składową o nazwie GetDefaultConnect. Tę funkcję tworzy dla ciebie AppWizard; funkcja zwraca poprawny łańcuch połączenia odzwierciedlający wybrane przez ciebie źródło danych. Oczywiście, możesz ją zmienić aby umożliwić użytkownikowi wybranie innego źródła danych (lub w innym celu). Możesz także posiadać jedną wersję funkcji do debu-ggowania z użyciem lokalnej bazy, i inną wersję dla zdalnej bazy danych w rozprowadzanej wersji programu.
Łączenie pól bazy danych ze zmiennymi rekordsetu
ClassWizard dobrze radzi sobie także z obiektem rekordsetu. Jeśli w oknie dialogowym ClassWizarda wybierzesz obiekt rekordsetu i klikniesz na zakładce Member Yariables, ujrzysz listę wszystkich pól w bazie danych, a także zmienne rekordsetu, które je reprezentują. Ta lista zawiera jedynie pola znajdujące się w bazie danych, w momencie działania AppWizarda. Jeśli nie potrzebujesz wszystkich tych zmiennych, możesz usunąć niepotrzebne pozycje. Jeśli zmieniła się struktura bazy danych, możesz odświeżyć nazwy pól, używając specjalnego przycisku w tym oknie dialogowym.
Łączenie zmiennych rekordsetu z elementami kontrolnymi
ClassWizard wie, że widoki rekordów mogą zechcieć połączyć zmienne rekordsetu z elementami kontrolnymi formularza. W związku z tym, pracując ze zmiennymi składowymi widoku, możesz powiązać je ze zmiennymi rekordsetu. Choć możesz wpisywać je bezpośrednio, zwykle łatwiej jest rozwinąć listę obok nazwy zmiennej i wybrać odpowiednią pozycję.
Zwróć uwagę, że powiązanie pomiędzy zmiennymi rekordsetu a elementami kontrolnymi widoku jest bardzo podobne do połączenia pomiędzy zmiennymi okna dialogowego a jego polami. Aby przekazać dane w dowolnym kierunku, musisz wywołać metodę UpdatcData. Domyślne funkcje obsługi zwykle robią to w odpowiednim momencie, ale możesz zechcieć samodzielnie wywoływać tę metodę w miarę potrzeb, na przykład, gdy bezpośrednio manipulujesz bazą danych lub polami formularza.
Usuwanie rekordów
Usuwanie rekordów jest łatwe. Umieść rekordset na odpowiednim rekordzie, wywołaj metodę Delete i gotowe. Rekordset nie musi natychmiast usunąć rekordu, ale zaznaczy go jako usunięty. Możesz to sprawdzić wywołując metodę IsDeleted. Widok rekordów automatycznie wyświetla usunięty rekord przy pomocy napisu ,,-Deleted-" umieszczonego we wszystkich polach. Jeśli chcesz, aby rekordset nie zawierał rekordu, po jego usunięciu wywołaj metodę Reąuery.
Dodawanie i aktualizowanie rekordów
Jeśli chcesz zaktualizować rekord, po przejściu do tego rekordu wywołaj metodę Edit. Gdy korzystasz z widoku rekordów, dzieje się to automatycznie. Gdy zmienne rekordsetu będą już poprawne, wywołaj metodę Update. Nowe rekordy zostają dodane do bazy danych, ale mogą się nie pojawić do momentu wywołania metody Reąuery. Jeśli chcesz anulować aktualizację, po prostu wywołaj metodę CancelUpdate.
Pola przeliczane
Jeśli korzystasz z widoku rekordów, często zdarzy się, że zechcesz obliczyć wartości na podstawie zawartości innych pól. Wygodnym do tego miejscem jest funkcja DoData-Exchange. Ta funkcja wymienia dane z polami, jeśli więc została wywołana, wiadomo że coś się zmieniło i rekordset zawiera nowe wartości.
Oto przykład w którym jest dodawana zawartość dwóch pól w celu obliczenia wartości trzeciego pola (fragment przykładowego programu z tego rozdziału):
void CDbdemoView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX); // set copies up
m“copies.Format("%d",m_pSet->m_warehouse+m_pSet->m_shelf);
//{{AFX_DATA_MAP(CDbdemoView) DDX_Text(pDX, IDC_COPIES, m_copies);
Rozdział 11 Wielowątkowość
Choć MFC nie zapewnia w pełni bezpiecznej wielowątkowości, z jego pomocą możesz łatwo tworzyć i synchronizować wątki. Wielowątkowość, to dająca duże możliwości i bardzo wymyślna technika, umożliwiająca łatwe wykonywanie zadań w tle. Jeśli jesteś dobrze przygotowany i rozumiesz wielozadaniowość w Windows, nie powinieneś mieć większych kłopotów pracując z wątkami w MFC.
Czy kiedykolwiek widziałeś, w jakim pośpiechu lekarze przemieszczają się pomiędzy swoimi pokojami? Najpierw czekasz godzinę w poczekalni, po czym jesteś proszony do środka. Myślisz że już zostaniesz zbadany, ale nie, teraz też musisz czekać, tyle że w małym pomieszczeniu, w którym nie ma nawet kolorowych magazynów, jedynie stary egzemplarz People z naderwaną okładką.
Gdy w końcu znajdziesz cokolwiek interesującego do czytania, wpada lekarz, opukuje cię, szturcha, wypisuje świstek papieru, mamrocze kilka niezrozumiałych terminów i zaraz wypycha na zewnątrz. Wiem, że lekarze mają mnóstwo roboty, ale wolałbym żeby byli bardziej podobni do lekarzy z telewizji, gdyż można odnieść wrażenie że lekarze z ekranu zajmują się tylko jednym pacjentem naraz.
Prawdziwi znani mi lekarze zawsze mają drugą listę pacjentów, którymi zajmują się w tym samym czasie (chyba że są bardzo dobrymi i drogimi lekarzami). Podobnie jak większość ludzi, muszą żonglować wieloma rzeczami jednocześnie.
Z kolei komputery są doskonałe w żonglowaniu wieloma zadaniami. Choć może się zdawać że komputer wykonuje kilka programów jednocześnie, jest to tylko złudzenie. Wszystkie programy dzielą pomiędzy siebie dostępne procesory (a najczęściej tylko jeden procesor). Program przełącza programy tak szybko, że powstaje wrażenie, iż wszystkie aplikacje są wykonywane jednocześnie.
Starsze systemy operacyjne, takie jak Windows 3.1, nie chroniły programów przed wzajemnym współoddziaływaniem. Programy mogły się bez przeszkód komunikować ze sobą. Z drugiej strony, oznaczało to także, że system mógł się nagle i niespodziewanie załamać w wyniku naruszenia przez jeden program pamięci innego programu lub programów. Bardziej nowoczesne systemy, takie jak Windows NT czy Windows 95, starają się wzajemnie odizolować od siebie programy. W związku z tym system staje się bardziej stabilny, ale jeśli programy chcą się ze sobą komunikować, taka izolacja staje się przekleństwem.
Gdy chcesz napisać kilka fragmentów programu, które mają wykonywać się niezależnie, ale muszą się ze sobą komunikować, powinieneś je zaprojektować jako części tego samego programu. Każdy niezależnie działający fragment "kodu powinien zostać umieszczony w osobnym wątku.
Wątki kontra procesy
Wiele tradycyjnych systemów operacyjnych zarządza wieloma procesami. Proces, ogólnie rzecz ujmując, to egzemplarz programu. Podobnie jak wiele tradycyjnych systemów operacyjnych, Windows tworzy proces dla każdego wykonywanego programu. Jednak proces to nie jest to, co się wykonuje. Do procesu należy pamięć, otwarte pliki, otwarte gniazda oraz inne zasoby. Oprócz tego proces zawiera wątek. Wątek jest w istocie obrazem rejestrów procesora. W związku z tym wątek zawiera informacje o miejscu wykonywania kodu (licznik programu) oraz swoje lokalne zmienne (poprzez wskaźnik stosu i rejestry ogólnego przeznaczenia). Aby programy działały, Windows zarządza wątkami, nie procesami.
Dopóki każdy proces zawiera tylko jeden wątek, trudno jest dostrzec wagę tego stwierdzenia. Wszystko staje się bardziej interesujące, w momencie gdy główny wątek tworzy inne wątki. Możesz rozdzielić wykonywanie programu pomiędzy kilka różnych wątków. Oczywiście, każdy nowy wątek może tworzyć własne wątki. Wątki korzystają ze wspólnych zmiennych globalnych, ale posiadają własny punkt wykonywania i własne zmienne lokalne.
Problemy z wątkami
Procesy izoluje od siebie system operacyjny. Jednak wewnątrz każdego z procesów wątki mogą dowolnie ze sobą współpracować. Choć jest to bardzo użyteczna możliwość, bardzo łatwo może doprowadzić do załamania jakiegoś wątku przez inny wątek tego samego procesu. Nie pomoże przy tym żaden z mechanizmów ochrony procesów.
Inny problem powstaje w chwili, gdy wątki odwołują się do wspólnych zasobów. Przypuśćmy, że posiadasz dwa wątki, które chcą zapisać informacje do pliku dziennika na dysku. Nie możesz im pozwolić na jednoczesny zapis, gdyż tekst zostanie poszatkowa-ny. Musisz w jakiś sposób rozsądzić kolejność dostępu do wspólnego zasobu.
Pierwszym rozwiązaniem, o którym mogłeś pomyśleć, może być użycie globalnej zmiennej pełniącej rolę znacznika. Każdy z wątków testowałaby stan znacznika, sprawdzając czy jest ustawiony, czy nie. Jeśli znacznik nie byłby ustawiony, wątek by go ustawiał i rozpoczynał dostęp do pliku. W teorii brzmi to dość dobrze, ale w praktyce powoduje pewien problem. W przypadku jednoprocesorowego systemu Windows on może w dowolnym momencie wywłaszczyć działający wątek i umożliwić wykonanie innym wątkom. Wyobraź sobie, że pierwszy wątek odczytuje stan znacznika, po czym zostaje wywłaszczony. Gdy czeka na wznowienie wykonania, drugi wątek przejmuje sterowanie i także odczytuje znacznik. Odczytując, że znacznik nie jest ustawiony, drugi wątek ustawia go i rozpoczyna zapis do pliku. W tym momencie Windows wywłaszcza drugi wątek i przekazuje sterowanie pierwszemu. Ponieważ pierwszy wątek już wcześniej odczytał znacznik, uważa, że nie jest ustawiony (mimo że jest ustawiony, ale w momencie odczytu nie był), więc ustawia go i rozpoczyna zapis do pliku.
Rezultatem jest chaos. Nawet jeśli wątki są wykonywane na osobnych procesorach, ze względu na zależności czasowe może powstać ten sam problem. Aby temu zaradzić, musi istnieć sposób testowania stanu znacznika i ustawiania go tak, aby Windows nie mogło wywłaszczyć wątku w trakcie tej operacji. Na szczęście, zarówno Windows jak i MFC udostępniają taką możliwość.
Innym powszechnym problemem w wielozadaniowych i wielowątkowych systemach operacyjnych jest tzw. zakleszczenie. Przypuśćmy, że dwa wątki chcą otworzyć dwa pliki (powiedzmy A.DAT i B.DAT) do wyłącznego dostępu. Jeśli pierwszy wątek otworzy plik A.DAT, a drugi otworzy plik B.DAT, żaden z nie otrzyma obu potrzebnych zasobów. Jeśli oba wątki będą wciąż próbowały otworzyć potrzebny plik, a żaden nie zwolni już otwartego pliku, dojdzie do zakleszczenia wątków.
Opracowano wiele różnych sposobów zapobiegania zakleszczeniom. Gdyby w przypadku niemożności uzyskania wszystkich zasobów wątki zwalniały już posiadane zasoby, któryś z nich w końcu zdobyłby wszystkie potrzebne zasoby. Oczywiście, jeśli oba wątki otwierałyby pliki w tej samej kolejności (na przykład najpierw A.DAT, a potem B.DAT), problem by nie zaistniał. Rozwiązaniem mógłby być także znacznik dostępu (podobny do użytego do ochrony pliku dziennika z poprzedniego przykładu).
Żaden z opisanych problemów nie omija Windows. Więcej na temat problemów z wie-lowątkowością i wielozadaniowością możesz znaleźć w instrukcji każdego systemu operacyjnego. Ponieważ większość programów Windows nie korzysta z wielowątkowości (ani nawet z wielozadaniowości), niektórzy programiści nie przywykli do brania pod uwagę tych zagadnień.
Wątki i MFC
Do reprezentacji każdego wątku MFC używa obiektu CWinThread. Obiekt wyprowadzony z CWinThread jest stosowany do reprezentacji nawet głównego wątku twojego programu. Jeśli przyjrzysz się bliżej, przekonasz się, że CWinApp (klasa reprezentująca twój program) jest wyprowadzona z CWinThread.
Jeśli potrzebujesz wątku posiadającego własną pętlę komunikatów, także musisz wyprowadzić klasę z klasy CWinThread. Możesz przesłonić Initlnstance i Exitlnstance w celu samodzielnej obsługi inicjalizacji i zakończenia wątku. Jeśli nie potrzebujesz pętli komunikatów (gdy nie korzystasz z żadnych okien, z wyjątkiem, być może, modalnych okien dialogowych), możesz użyć klasy CWinThread takiej jaka jest. W każdym razie nie tworzysz obiektu CWinThread bezpośrednio; zamiast tego używasz funkcji AfxBeginThread.
Tworzenie roboczego wątku MFC
Najprostszym przykładem wątku jest wątek roboczy. Takie wątki wykonują niezależnie kod zawarty w zwykłych globalnych funkcjach (lub statycznych metodach składowych). W tym przypadku AfxBeginThread wymaga podania kilku argumentów:
• Nazwy funkcji zwracającej UINT i jako argument akceptującej LPYOID. Nawet jeśli nie interesuje cię zwracana wartość argumentu, musisz jednak napisać i udostępnić taką funkcję. Funkcja nie może być własną zmienną składową. Może być statyczną składową klasy lub możesz użyć funkcji globalnej.
• Argument w postaci wskaźnika do typu void, przekazywany do funkcji.
• Poziom priorytetu dla wątku.
• Rozmiar stosu dla wątku.
• Wartość zero, jeśli chcesz natychmiast uruchomić wątek lub stała CREATE_ SUSPENDED, aby rozpocząć wątek w stanie zawieszenia.
• Atrybuty ochrony dla wątku. Jest to użyteczne jedynie w przypadku Windows NT (Windows 95 nie stosuje mechanizmów ochrony). Domyślną wartościąjest NULL, czyli tę wartość zwykle zastosujesz.
Funkcja wymaga dostarczenia pierwszych dwóch parametrów; pozostałe cztery są opcjonalne.
Gdy wywołasz funkcję AfxBeginThread, otrzymasz wskaźnik do stworzonego przez nią obiektu CWinThread. Jak wkrótce zobaczysz, możesz użyć tego obiektu do manipulowania wątkiem.
Wątek zakończy działanie w momencie wyjścia z funkcji lub po wywołaniu funkcji AfxEndThread. Inne wątki mogą odczytać wartość powrotu (argument funkcji AfxEndThread), jeśli tylko wiedząjak.
Tworzenie wątku interfejsu użytkownika MFC
Funkcja AfxBeginThread występuje w dwóch odmianach. Pierwsza z nich tworzy wątek roboczy (o czym już wiesz). Druga wersja tworzy klasę wyprowadzoną z CWinThread. Ta wersja jest przydatna gdy chcesz stworzyć wątek posiadający interfejs użytkownika. Innymi słowy, jeśli twój wątek wymaga pętli komunikatów, musisz wyprowadzić nową klasę z CWinThread.
Argumenty tej wersji AfxBeginThread są podobne do argumentów wersji wątku roboczego. Jednak tym razem, zamiast nazwy funkcji i argumentu, podajesz klasę czasu wykonania własnego obiektu wątku (używając makra RUNTIME_CLASS). Pozostałe argumenty są takie same.
Dobrym miejscem na znalezienie przykładu wyprowadzania klasy z CWinThread jest kod źródłowy MFC. Przecież CWinApp jest właśnie wątkiem (pewnie, ten wątek jest pierwszy, ale w dalszym ciągu jest to jedynie wątek).
Aby stworzyć własne okno, musisz przynajmniej przesłonić Initlnstance. Nie zapomnij o przypisaniu zmiennej m_pMainWnd wskaźnika do głównego okna wątku. AppWizard zajmuje się tym automatycznie w przypadku tworzenia obiektu twojej aplikacji, ale gdy tworzysz własną klasę, musisz sam pamiętać o takich szczegółach.
Nową klasę możesz wyprowadzić przy pomocy ClassWizarda. AfxBeginThread zwraca wskaźnik do obiektu CWinThread, ale w rzeczywistości wskazywany obiekt należy do określonego przez ciebie typu. Jeśli chcesz odwoływać się do własnych składowych, musisz dokonać rzutowania wskaźnika do odpowiedniego typu.
Aby zakończyć wątek posiadający pętlę komunikatów, wywołaj PostQuitMessage. Argument tego wywołania stanie się wartością zwracaną przez wątek, którą to wartość mogą odczytać inne wątki.
Manipulowanie wątkami
CWinThread posiada kilka użytecznych składowych (patrz tabela 11.1). Domyślnie, znacznik m_bAutoDelete ma wartość TRUE. To oznacza, że gdy wątek kończy działanie, MFC automatycznie niszczy obiekt. Jest to wygodne wtedy, gdy tworzysz wątek aby niezależnie wykonał swoją pracę, jednak staje się problemem, gdy chcesz w jakiś sposób współpracować z obiektem.
Przypuśćmy że posiadasz wątek interfejsu użytkownika, wyprowadzony z CWinThread. Zadaniem nowego wątku jest odczytywanie poleceń z portu szeregowego. Istnieje wiele sposobów, w jakie odczytane polecenie może wpływać na resztę programu, jednak dla ilustracji przyjmijmy, że zdecydowałeś się umieścić w obiekcie wątku zmienną składową zawierającą łańcuch polecenia.
Zakładając że wszystko pójdzie dobrze, nie jest to zły pomysł. Główny program uruchamia wątek używając AfxBeginThread, rzutuje otrzymany wskaźnik do właściwego typu i odwołuje się do zmiennej zawierającej aktualne polecenie. Oprócz tego potrzebnych jest kilka innych zmiennych, na przykład znacznik określający, że polecenie jest poprawne, i funkcję składową zerującą polecenie oraz oczekującą na następne.
Problem pojawia się w momencie, gdy wątek nieoczekiwanie zakończy działanie.W tedy obiekt już nie istnieje i wskaźnik jest niewłaściwy. To powoduje duże kłopoty.
Odpowiedzią oczywiście będzie przypisanie składowej mJbAutoDelete wartości FALSE. Ale problem nie znika do końca. Co się stanie, jeśli wątek zakończy się prawie natychmiast? Innymi słowy, co się stanie, jeśli w momencie ustawienia znacznika, obiektu już nie ma? Nie możesz przecież założyć, że obiekt będzie dostępny przez jakikolwiek czas.
Tabela 11.1. Składowe klasy CWinThread.
Składowa Opis
CreateThread Tworzy wątek; zamiast tego użyj AfxCreateThread.
m_pMainWnd Główne okno wątku.
m_pActiveWnd Aktualnie aktywne okno.
m_bAutoDelete Jeśli TRUE, po zakończeniu wątku obiekt zostaje zniszczony; TRUE jest
wartością domyślną.
m_hThread Uchwyt wątku Windows (można także rzutować CWinThread na HTHREAD).
m_nThreadID Identyfikator wątku.
GetThreadPriority Zwraca poziom priorytetu wątku.
SetThreadPriority Ustawia poziom priorytetu wątku.
SuspendThread Zawiesza wątek.
ResumeThread Wznawia wątek (wywoływane raz na każde wywołanie SuspendThread).
PostThreadMessage Umieszcza komunikat w kolejce komunikatów wątku.
InitInstance Przesłoń, aby samemu zainicjować wątek.
Run Przesłoń, aby samemu obsłużyć pętlę komunikatów wątku.
PreTranslateMessage Filtruje komunikaty i przetwarza klawisze akceleratorów.
PumpMessage Niskopoziomowa pompa komunikatów.
OnIdle Wywoływane, gdy w kolejce nie ma oczekujących komunikatów.
IsIdleMessage Sprawdza, czy w kolejce są specjalne komunikaty MFC pracy jałowej.
ExitInstance Przesłoń w celu dostarczenia kodu wykonywanego na zakończenie wątku.
ProcessWndProcException Przetwarza nieobsłużone wyjątki.
ProcessMessageFilter Filtruje komunikaty.
GetMainWnd Zwraca wskaźnik do głównego okna wątku.
Rozwiązaniem jest stworzenie wątku w stanie wstrzymania. Pamiętasz znacznik CREATE_ SUSPENDED przekazywany do AfxCreateThread? Możesz przekazać ten znacznik, i MFC stworzy wątek w stanie uśpienia. Następnie możesz ustawić znacznik m_bAuto-Delete lub wykonać cokolwiek innego, po czym wywołać ResumeThread rozpoczynając wykonanie wątku.
Powinieneś wiedzieć, że wywołania SuspendThread i ResumeThread powinny się bilansować, tj. jeśli trzy razy wywołasz SuspendThread, wątek nie rozpocznie działania, dopóki trzy razy nie wywołasz ResumeThread.
Odczytywanie zwracanej wartości
Innym sposobem komunikowania się z innymi wątkami jest wartość zwracana na zakończenie działania wątku. Zwracaną wartość roboczego wątku możesz ustalić zwracając ją z funkcji wątku lub przekazując ją do AfxEndThread. Wartość zwracaną przez wątek interfejsu użytkownika możesz podać jako argument wywołania PostQuitMessage.
Jeśli chcesz odczytać wartość zwróconą przez inny wątek, twój program musi posiadać uchwyt tego wątku. Uchwyt jest dostępny w obiekcie CWinThread (jako składowa m_hThread). Następnie używasz tego uchwytu przy wywołaniu ::GetExitCodeThread. Ta funkcja jako pierwszego argumentu wymaga uchwytu wątku, a jako drugiego, wskaźnika do zmiennej typu DWORD, w której zostanie umieszczona zwrócona wartość. Jeśli wątek jest wciąż aktywny, w zmiennej DWORD umieszczana jest stała STILL_ACTIVE. W przeciwnym razie funkcja ustawia wartość DWORD zgodnie z wartością kodu zwróconego przez dany wątek.
Synchronizowanie wątków
Istnieje jeszcze jeden problem związany z komunikacją wątków. Ponieważ wszystkie wątki procesu mogą korzystać z tych samych zmiennych, mogą z tego wyniknąć różne konflikty.
Masz zmienną pełniącą rolę znacznika. Znacznik steruje dostępem do pliku, tak że wątki nie mogą do niego pisać jednocześnie. Teoretycznie, zanim jakiś wątek będzie mógł zapisać coś do pliku, musi upewnić się, że znacznik nie jest ustawiony, a następnie go ustawić.
Problem polega na tym, że wątki mogą działać równocześnie (a przynajmniej wygląda na to, że działają równocześnie). Powiedzmy, że jeden wątek odczytuje stan znacznika i zaraz po tym zostaje wywłaszczony przez Windows. Podczas gdy czeka na ponowne uruchomienie, inny wątek odczytuje stan znacznika, stwierdza, że nie jest ustawiony i ustawia go. Gdy pierwszy wątek odzyskuje sterowanie, także uważa, że znacznik jest czysty, więc ustawia go i rozpoczyna zapis do pliku. W tym momencie oba wątki uważają, że mają prawo zapisu do pliku.
Aby rozwiązać ten problem, potrzebujesz jakiegoś sposobu na sprawdzenie stanu i ustawienie znacznika w pojedynczej nieprzerywalnej operacji. Na szczęście, w Windows można to osiągnąć dwoma prostymi wywołaniami: Interlockedlncrement oraz Inter-lockedDecrement. Nie będziesz jednak z nich zbyt często korzystał, gdyż Windows udostępnia metodę wyższego poziomu, spełniającą to samo zadanie. W szczególności, Windows udostępnia obiekty synchronizacji, które wykorzystuje MFC, używając dla nich obiektów wyprowadzonych z klasy CSyncObject.
W tradycyjnym programowaniu Windows, obiekty synchronizacji były wykorzystywane bezpośrednio, w celu rozsądzenia, który wątek ma mieć dostęp do zasobów. Jednak w przypadku C++ lepiej jest zaprojektować klasy bezpieczne ze względu na wątki.
Ponownie odwołując się do przykładu, w którym dwa wątki chcą wspólnie korzystać z pliku, stwierdzam, że byłoby lepiej posiadać funkcję zawierającą metodę o nazwie WriteString (na przykład). Ta funkcja dokonywałaby zapisu do pliku, ale tylko po upewnieniu się, że w tym samym czasie żaden inny wątek niczego do niego nie zapisuje. Wtedy wątki nie musiałyby bezpośrednio zajmować się obiektami synchronizacji.
Obiekty synchronizacji mogą istnieć w jednym z dwóch stanów: włączony i wyłączony. Co to dokładnie znaczy, zależy od obiektu, ale ogólnie chodzi o dostępność danego zasobu. Gdy obiekt jest włączony, zasób jest dostępny. Gdy obiekt nie jest włączony, zasób nie jest dostępny. Wątki mogą sprawdzać stan obiektu. Jeśli obiekt nie jest włączony, wątek może zdecydować się na odczekanie pewnego czasu (lub czekać w nieskończoność), dopóki obiekt nie zostanie włączony.
Większość obiektów synchronizacji może posiadać nazwy, używane gdy chcesz wspólnie korzystać z obiektu w wielu wątkach różnych procesów. Oczywiście, możesz nazwać obiekt i używać tej nazwy wewnątrz pojedynczego procesu, ale w takim przypadku łatwiej jest użyć pojedynczej zmiennej, wspólnie wykorzystywanej przez wątki.
Jeśli spróbujesz stworzyć obiekt synchronizacji MFC używając nazwy będącej już w użyciu, otworzysz po prostu istniejący obiekt. Oczywiście, tworzenie obiektu bez nazwy zawsze powoduje skonstruowanie nowego obiektu.
Rodzaje obiektów synchronizacji
Windows udostępnia kilka różnych obiektów synchronizacji. Każdy z nich jest użyteczny w określonych warunkach:
• CEvent - Jest to znacznik, który możesz ustawiać i testować w bezpieczny (z punktu widzenia wątków) sposób. Znacznik może być różnego typu (typ jest określany w momencie tworzenia). Ręczny znacznik umożliwia dowolne ustawianie i zerowanie. Znacznik automatyczny powoduje wyzerowanie stanu, gdy jakiś wątek ogłosi jego włączenie.
• CMutex - Jest to specjalny rodzaj znacznika, przy którym w grę wchodzi przynależność zasobu. W danej chwili tylko jeden wątek może posiadać dany znacznik. Gdy wątek stwierdzi, że znacznik jest włączony, znacznik staje się niedostępny dla wszystkich innych wątków, jednak dla tego wątku znacznik w dalszym ciągu zgłasza się jako włączony. Dzięki temu wątek może przejąć znacznik wielokrotnie. Oznacza to także, że wątek musi zwolnić znacznik tyle razy, ile razy się do niego odwołał.
• CSemaphore - Semafor to wspólny licznik. Dopóki wartość licznika jest większa od zera, semafor jest w stanie włączenia. Za każdym razem, gdy wątek chce przejąć semafor, licznik zmniejsza się o jeden. Gdy wartość licznika dojdzie do zera, semafor staje się wyłączony do czasu, aż jeden lub kilka wątków zwolni, a wartość licznika wzrośnie. Semafory są użyteczne w gdy chcesz ograniczyć ilość wątków wykonujących coś jednocześnie (na przykład przy dostępie do bazy danych).
• CCriticalSection - Sekcja krytyczna jest bardzo podobna do obiektu CMutex, lecz nadaje się do użycia jedynie w przypadku wątków tegg samego procesu. Ten uproszczony znacznik przydaje się wtedy, gdy nie musisz komunikować się pomiędzy procesami, i potrzebujesz lepszej wydajności niż ofiaruje to znacznik typu CMutex.
Poza obiektami synchronizacji, MFC używa dwóch specjalnych obiektów, CSingleLock oraz CMultiLock, w celu sprawdzenia stanu obiektu synchronizacji. Te obiekty odpowiadają wywołaniom Windows API, WaitForSingleObject oraz WaitForMultiple-Objects. Możesz użyć jednego z tych obiektów blokowania do wyznaczenia czy obiekt synchronizacji jest włączony.
Dla przykładu, przyjrzyjmy się znacznikowi typu CMutex. W swom kodzie tworzysz obiekt CMutex oraz obiekt CSingleLock. Do konstruktora CSingleLock przekazujesz znacznik do obiektu CMutex. Gdy chcesz przejąć znacznik, wywołujesz metodę CSingleLock: :Lock. Gdy obiekt CSingleLock znajdzie się poza zakresem (lub zostanie inaczej zniszczony), automatycznie wywoła metodę CSingleLock: :Unlock, która zwalnia znacznik. Oczywiście, jeśli chcesz, możesz bezpośrednio wywołać metodę Unlock. CMultiLock działa podobnie, lecz do konstruktora nie jest przekazywany jeden obiekt, ale tablica wielu obiektów, a metoda Lock blokuje obiekt, który jest dostępny.
Alternatywy dla wątków
Choć wątki są bardzo przydatne przy przetwarzaniu w tle, nie stanowią jedynego rozwiązania. Jeśli masz do czynienia z operacjami, które r/adko ze sobą współpracują, możesz napisać je jako samodzielne programy. Windows udostępnia wiele sposobów komunikacji takich programów między sobą, choć nie są one tak wydajne jak wątki.
W pewnych przypadkach możesz wziąć pod uwagę przetwarzanie na jałowym biegu aplikacji. Pamiętaj że obiekt twojej aplikacji zawiera metodę o nazwie Onldle, wywoływaną przez MFC w momencie gdy w kolejce nie ma żadnych komunikatów dla wątku. Oczywiście, musisz uważać aby przy jałowym biegu aplikacji nie wykonywać zbyt dużo operacji. Na przykład niezbyt dobrym pomysłem byłoby oczekiwanie w Onldle na dane z portu szeregowego. Oprócz tego, bieg jałowy przydaje się w 16-bitowych Windows, które nie obsługują wątków.
Innym sposobem uniknięcia wielowątkowości jest zastosowanie nałożonych operacji wejścia/wyjścia. MFC nie obsługuje bezpośrednio takich operacji, poza tym w Windows 95 takie operacje są mocno ograniczone. Idea kryjąca się za nałożonymi operacjami wejścia/wyjścia polega na tym, że możesz zażądać odczytu lub zapisu do pliku (lub urządzenia), bez konieczności czekania na zakończenie wykonania tej operacji.
Aby to mogło działać, musisz użyć funkcji ::CreateFile z ustawionym znacznikiem FILE_FLAG_OVERLAPPED. Warto odnotować, że chociaż nazwą funkcji jest ::CreateFile, z jej pomocą można także otworzyć już istniejący plik. Lepszą nazwą tej funkcji byłoby ::CreateFileHandle, gdyż w jej wyniku zawsze otrzymujemy uchwyt pliku.
Gdy plik już zostanie otwarty dla nałożonych operacji wejścia/wyjścia, możesz stosować zwykłe wywołania Windows API (takie jak ::ReadFile czy ::WriteFile). Każda z tych funkcji otrzymuje wskaźnik do struktury OYERLAPPED; ta struktura zawiera offset w pliku i ewentualnie uchwyt zdarzenia (o którym za moment). Jeśli przekazywany wskaźnik ma wartość NULL, funkcja działa jak zwykle. Jeśli jednak przekażesz wskaźnik do struktury OYERLAPPED, może się zdarzyć jedna z dwóch rzeczy.
W pierwszym przypadku operacja wejścia/wyjścia zostanie zakończona natychmiast. Wtedy funkcja zwraca kod powodzenia i operacja zostaje zakończona. Jeśli jednak wywołanie spowoduje zablokowanie wątku (innymi słowy, oczekiwanie na operację wejścia/wyjścia), Windows zwróci kod błędu. Wywołanie funkcji ::GetLastError zwróci wartość ERROR_IO_PENDING. W rzeczywistości nie jest to błąd; oznacza jedynie, że operacja wejścia/wyjścia jest wykonywana asynchronicznie, umożliwiając wątkowi dalsze działanie.
Aby upewnić się, że operacja wejścia/wyjścia rzeczywiście została dokończona, masz do wyboru kilka możliwości. Po pierwsze, możesz wywołać ::GetOverlappedResult. Możesz zmieniać parametry tej funkcji tak, by sprawić, że wątek będzie w nieskończoność czekał na zakończenie operacji wejścia/wyjścia, lub jedynie sprawdzić czy operacja już została zakończona. Jeśli tylko sprawdzasz, ::GetOverlappedResuIt może zwrócić kod błędu, i kolejne wywołanie ::GetLastError zwróci wartość ERROR_IO_INCOMPLETE, co oznacza że za chwilę powinieneś jeszcze raz spróbować. Oczywiście, jeśli po ::ReadFile, ::WriteFile lub ::GetOverIappedResult ::GetLastError zwróci inny kod błędu, masz do czynienia z ewidentnym błędem wejścia/wyjścia.
Lepszym pomysłem jest określenie w strukturze OYERLAPPED obiektu synchronizacji zdarzenia. Wtedy Windows zgłosi ten wyjątek w momencie zakończenia operacji wejścia/ wyjścia. Możesz to sprawdzić na każdy ze zwykłych sposobów związanych z wyjątkami. Dla przykładu załóżmy, że chcesz odczytać dane z dwóch portów szeregowych i wyświetlić je w oknie. Twój program może rozpocząć dwa nałożone odczyty, po jednym dla każdego portu, i dla każdego z nich określić osobne zdarzenie. Możesz następnie użyć obiektu CMultiLock, oczekując aż jeden lub oba porty zgłoszą dostępność danych.
Innym sposobem wykorzystania nałożonych operacji wejścia/wyjścia jest użycie funkcji ::ReadFileEx i ::WriteFiIeEx (dostępnych jedynie w Windows NT). Te funkcje wymagają podania wskaźnika do struktury OYERLAPPED (który nie może mieć wartości NULL) oraz wskaźnika do funkcji. Gdy operacja wejścia/wyjścia zostanie zakończona, Windows wywoła tę funkcję w celu przetworzenia danych. To rozwiązanie jest idealne, w przypadku kiedy dane tajemniczo pojawiają się w buforze kołowym wykorzystywanym przez resztę aplikacji. Niestety, ten mechanizm nie działa w Windows 95. Ogólne rozwiązanie polega na wykonywaniu zwykłych operacji wejścia/wyjścia w osobnym wątku, wypełniającym także bufor kołowy. Które podejście jest lepsze? To zależy. Metoda wykorzystująca nałożone operacje wejścia/wyjścia jest prawdopodobnie nieco bardziej efektywna. Z drugiej strony, jeśli twój program ma działać w Windows 95, musisz użyć oddzielnego wątku.
Windows 95 nakłada jeszcze inne ograniczenia na nałożone operacje wejścia/wyjścia. Windows 95 może wykonywać takie oeracje jedynie na urządzeniach szeregowych i pewnych plikach otwartych przy pomocy ::DeviceIoControl (które otwiera urządzenia bezpośrednio).
Choć więc wątki mogą przydać się w wielu wypadkach, często istnieją inne, warte rozważenia, metody. Jednak wątki są bardzo wszechstronnym mechanizmem, więc z pewnością powinny być używane tam, gdzie się do tego nadają. Po prostu nie zapomnij, że zwykle jest więcej niż jeden sposób na osiągnięcie danego celu, i że zawsze powinieneś starannie rozważyć wszystkie możliwości.
Przykładowy program
Czy kiedykolwiek chciałeś wywołać interfejs użytkownika MFC z jakiegoś innego programu? Bardzo kuszące jest stworzenie DLL-a posiadającego pojedynczy punkt wejścia, który rozpoczynałby działanie całego programu MFC.
Istnieje kilka sposobów, w jakie możesz to osiągnąć, ale użycie wątku jest prostą i użyteczną metodą. Dwój DLL uruchomi nowy wątek, który będzie reprezentował zwykłą aplikację MFC.
Na rysunku 11.1 widzimy aplikację konsoli, przeliczającą długą kolumnę parzystych liczb. Ponieważ to może zabrać dłuższą chwilę, aplikacja umieszcza na ekranie okno dialogowe, umożliwiające użytkownikowi przerwanie operacji. Oczywiście, aplikacja konsoli nie może użyć modalnego okna dialogowego, ponieważ musi kontynuować przeliczanie. Z drugiej strony, ponieważ aplikacja konsoli nie posiada pętli komunikatów, nie może także użyć okna niemodalnego.
Rozwiązaniem jest zdefiniowanie DLL-a MFC obsługującego okno dialogowe. To stawia nam dwa wyzwania. Po pierwsze, aplikacja konsoli musi mieć jakiś sposób uruchomienia części MFC. Po drugie, aplikacja konsoli i okno dialogowe MFC muszą się w jakiś sposób komunikować. W szczególności, program konsoli musi wykryć fakt, że użytkownik chce przerwać operację, a okno dialogowe MFC musi się zamknąć, gdy program konsoli już go nie będzie potrzebował.
Stworzenie eksportowanej funkcji C nie jest zbyt trudne (więcej informacji na temat funkcji w DLL-ach znajdziesz w rozdziale 7). Ta funkcja może stworzyć klasę wyprowadzoną z CWinThread uruchamiającą okno dialogowe. Listing 11.1 zawiera główny kod DLL-a (łącznie z eksportowaną funkcją, AbortBox). Listing 11.2 zawiera kod wątku, zaś listing 11.3 zawiera obsługę okna dialogowego. Listing 11.4 ilustruje prosty program konsoli używający DLL-a MFC.
Listing 11.1. AbortDLL
// AbortThread.cpp : implementation file
//
#include "stdafx.h"
#include "abortdll.h"
#include "AbortThread.h"
#include "Abortdlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CAbortThread
IMPLEMENT_DYNCREATE(CAbortThread, CWinThread)
CAbortThread::CAbortThread()
{
}
CAbortThread::~CAbortThread()
{
}
BOOL CAbortThread::InitInstance()
{
CAbortDlg dlg;
m_pMainWnd=&dlg;
event=new CEvent(FALSE,TRUE,eventname);
dlg.m_text=text;
dlg.Thread=this;
dlg.DoModal();
delete event;
return FALSE;
}
int CAbortThread::ExitInstance()
{
// TODO: perform any per-thread cleanup here
return CWinThread::ExitInstance();
}
BEGIN_MESSAGE_MAP(CAbortThread, CWinThread)
//{{AFX_MSG_MAP(CAbortThread)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CAbortThread message handlers
Jeśli przyjrzysz się temu programowi, zdziwisz się, bo nie ma w nim nic niezwykłego. Dialog to zwykłe okno dialogowe. Klasa CAbortThread (stworzona przez ClassWizarda) jest bardzo zbliżona do zwykłej klasy CWinApp i służy tym samym celom. Kod DLL-a też niczym specjalnym się nie wyróżnia.
Jedyną ciekawszą częścią kodu AbortBox jest komunikacja pomiędzy głównym programem a wątkiem. Wywołujący program tworzy znacznik, wykorzystując dowolną nazwę. Ta nazwa jest przekazywana jako jeden z argumentów funkcji AbortBox. Wątek także korzysta z tego znacznika.
Znacznik służy dwóm celom. Po pierwsze, jeśli użytkownik przerwie wykonywanie programu, kod MFC włącza znacznik. Dzięki temu wątek wie, że powinien przerwać pracę. Po drugie, jeśli to oryginalny kod włączy znacznik, kod MFC wie, że operacja została skończona i powinien zamknąć swoje okno.
Listing 1 1 .3. Okno dialogowe.
// AbortDlg.cpp : implementation file
//
#include "stdafx.h"
#include "abortdll.h"
#include "AbortThread.h"
#include "AbortDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CAbortDlg dialog
CAbortDlg::CAbortDlg(CWnd* pParent /*=NULL*/)
: CDialog(CAbortDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CAbortDlg)
m_text = _T("");
//}}AFX_DATA_INIT
}
void CAbortDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CAbortDlg)
DDX_Text(pDX, IDC_TEXT, m_text);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CAbortDlg, CDialog)
//{{AFX_MSG_MAP(CAbortDlg)
ON_WM_TIMER()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CAbortDlg message handlers
void CAbortDlg::OnOK()
{
Thread->event->SetEvent();
CDialog::OnOK();
}
BOOL CAbortDlg::OnInitDialog()
{
CDialog::OnInitDialog();
SetTimer(1,500,NULL); // Timer co 1/2 sekundy
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
void CAbortDlg::OnTimer(UINT nIDEvent)
{
CSingleLock lock(Thread->event,FALSE);
if (lock.Lock(0))
{
CDialog::OnOK();
}
}
Pomijając znacznik i własny obiekt CWinThread, mógłby to być dowolny program MFC. Ponieważ AbortBox jest zwykłą eksportowaną funkcją, możesz wywołać ją z prawie każdego programu. Oczywiście, funkcja AborfBox jest bardzo prosta, ale możesz łatwo zastosować to rozwiązanie do wielu trudniejszych zadań.
Listing 11.4. Aplikacja konsoli do przetestowania DLL-a
ttinclude <stdio.h>
#include <windows.h>
extern "C" {
BOOL AbortBox(char *string,char *eventname); // DLL function }
void maint)
HANDLE event;
unsigned int ct=0;
event=CreateEvent(NULL,TRUE,FALSE,"CONABORTEYENT");
AbortBox("Kliknij na przycisku Przerwij aby zakończyć aplikację
konsoli.","CONABORTEVENT"); while (WaitForSingleObject(event,0)==WAIT“TIMEOUT&&ct<100000)
printf ("%d\n" , et) ,-ct+=2;
if (WaitForSingleObject(event,0)==WAIT_TIMEOUT)
SetEvent(event); // upewnij się, że okno Abort
// otrzyma wiadomość ::MessageBox(NULL,"Gotowe","Operacja zakończona",
mb“ok|mb_setforeground) ,-
// Bez komunikatu, jeśli przerwane po kliknięciu na przycisku CloseHandle(event);
Podsumowanie
Wielowątkowość jest bardzo użyteczną, choć nieco skomplikowaną techniką. Choć nie trzeba jej stosować do każdego pisanego programu, jeśli już jest potrzebna, mało co może ją zastąpić. Programy korzystające z wątków mogą łatwo wykonywać swoje zadania, przy okazji wykonując w tle operacje wejścia/wyjścia. Wątki bardzo często przydają się w programach symulacyjnych, w których każdy wątek reprezentuje symulowany obiekt.
Choć MFC znacznie ułatwia tworzenie i synchronizowanie wątków, w dalszym ciągu pozostaje parę problemów do rozwiązania. Jeśli chcesz, by więcej niż jeden wątek odwoływał się do tego samego obiektu, musisz samodzielnie rozstrzygnąć o kolejności dostępu. Co gorsza, obiekty zawierające mapy (na przykład obiekty CWnd) korzystają z lokalnych zmiennych wątku. To oznacza, że obiekty z mapami nie będą prawidłowo działać poza wątkiem, w którym zostały stworzone. Przygotuj się na tego typu problemy, gdy zaczniesz stosować wątki w swoich programach.
Praktyczny przewodnik Wielowątkowość
Tworzenie wątku roboczego
Tworzenie wątku interfejsu użytkownika
Zamykanie wątku
Sprawianie by okna pojawiały się na wierzchu
Sprawianie by okna komunikatów pojawiały się na wierzchu
Zapobieganie autodestrukcji wątków
Tworzenie wstrzymanego wątku
Odczytywanie zwracanej wartości
Rodzaje obiektów synchronizacji
Oczekiwanie na obiekt synchronizacji
Oczekiwanie na wielokrotny obiekt synchronizacji
Użycie metody Onldle
Wykorzystanie wątków z poziomu MFC nie jest trudne. Największa trudność polega na zaprojektowaniu spójnego programu korzystającego z kilku wątków. Uzbrojony w zasób wiedzy i arsenał obiektów synchronizacji Windows, możesz łatwo zaprząc wątki do użytecznej pracy.
Tworzenie wątku roboczego
Wątek MFC nie wymagający pętli komunikatów, nosi nazwę wątku roboczego. Wątek roboczy może dokonywać wszelkich operacji, a także może korzystać z modalnych okien komunikatów i modalnych okien dialogowych. Jednak bez pętli komunikatów nie da się stworzyć niemodalnego okna dialogowego lub standardowego okna.
Tworzenie wątków roboczych jest bardzo proste. Musisz jedynie napisać globalną funkcję lub statyczną metodę klasy, zwracającą wartość typu UFNT i akceptującą argument będący wskaźnikiem do typu void. Następnie powinieneś wywołać funkcję AfxBegin-Thread, przekazując jej nazwę swojej funkcji i argument, który chcesz jej przekazać. Możesz opcjonalnie określić poziom priorytetu, rozmiar stosu, tryb tworzenia wątku oraz atrybuty ochrony. Jeśli chcesz aby wątek od razu rozpoczął działanie, jako tryb tworzenia powinieneś przekazać wartość 0; jeśli wątek ma być zawieszony do momentu wywołania metody ResumeThread, jako tryb wywołania przekaż stałą CREATE_ SUSPENDED.
MFC stworzy obiekt CWinThread; wskaźnik do tego obiektu jest zwracany przez funkcję AfxBeginThread. Twoja robocza funkcja będzie wykonywana jako niezależny wątek. Będzie miała własne zmienne lokalne, ale ze zmiennych globalnych będzie korzystać wspólnie z innymi wątkami tego samego procesu1.
Tworzenie wątku interfejsu użytkownika
Jeśli twój wątek wymaga użycia pętli komunikatów (czyli korzysta ze standardowych okien lub niemodalnych okien dialogowych), musisz użyć ClassWizarda wyprowadzając nową klasę z klasy CWinThread. W tej klasie powinieneś przesłonić metodę Initlnstance, tworząc główne okno (m_pMainWnd) i wykonać pozostałą inicjalizację (tak jak zrobiłbyś w przypadku obiektu CWinApp).
Zamykanie wątku
Aby zakończyć wątek roboczy, możesz po prostu powrócić z roboczej funkcji lub wywołać funkcję AfxEndThread. AfxEndThread może się przydać jeśli chcesz zakończyć wątek po wywołaniu innych funkcji. W każdym przypadku możesz określić wartość zwracaną przez wątek, którą mogą odczytać inne wątki. Pamiętaj, że wywołanie AfxEndThread musi nastąpić wewnątrz wątku, który chcesz zakończyć.
Wątek interfejsu użytkownika możesz zakończyć wywołując PostQuitMessage. Argument tego wywołania staje się wartością zwracaną przez wątek, którą to wartość mogą odczytać inne działające wątki.
Sprawianie, by okna pojawiały się na wierzchu
Gdy pracujesz z wątkami, okazuje się że wątek obsługujący okno znajdujące się w ognisku wprowadzania procesu posiada specjalny status. Czasami takie wątki nazywa się wątkami pierwszoplanowymi. Gdy taki wątek tworzy okno, pojawia się ono na wierzchu, podczas gdy inne wątki tego samego procesu zwykle nie tworzą okien na wierzchu stosu. Zamiast tego, ich okna pojawiają się pod innymi oknami.
Jeśli tworzysz okno w MFC, MFC standardowo wywołuje SetForegroundWindow, aby mieć pewność, że okno pojawi się na wierzchu. Jeśli jednak samodzielnie tworzysz okno, musisz także samodzielnie wywołać SetForegroundWindow.
1 Jeśli chcesz korzystać w kilku wątkach ze zmiennych globalnych, zadeklaruj je jako volatile. W ten sposób kompilator zaniecha optymalizacji dostępu do tych zmiennych, dzięki czemu wątek zawsze będzie odczytywał aktualną wartość zmiennej. (Przyp. tłum.)
Sprawianie, by okna komunikatów pojawiały się na wierzchu
Umieszczenie okna komunikatu na wierzchu sprawia pewien problem. MFC zawsze próbuje umieścić swoje okna komunikatów na wierzchu, ale co zrobić ze zwykłymi oknami komunikatów, nie tworzonymi w MFC? Nie możesz wywołać SetForeground-Window, ponieważ w momencie, gdy działa okno komunikatu, wykonanie twojego kodu jest wstrzymane. Aby rozwiązać ten problem, Microsoft stworzył znacznik MB_SET-FOREGROUND. Jeśli przekażesz go do wywołania tworzącego okno komunikatu, zostanie wymuszone wyświetlenie tego okna na wierzchu (prawdopodobnie przez wewnętrzne wywołanie SetForegroundWindow).
Zapobieganie autodestrukcji wątków
Domyślnie, gdy wątek MFC kończy działanie, niszczy własny obiekt wątku. Dzięki temu mamy pewność, że wątek nie zajmuje już pamięci. Z drugiej strony, jeśli chcesz odczytać dane wątku, który już nie działa, możesz napotkać duże problemy.
Rozwiązanie jest proste. Upewnij się, że znacznik mJbAutoDelete w obiekcie wątku ma wartość FALSE. To zapobiega automatycznemu niszczeniu obiektu. Oczywiście, w takim przypadku musisz samodzielnie zwolnić pamięć, gdy już skończysz z obiektem.
Z ustawianiem tego znacznika związane jest pewne niebezpieczeństwo. Jeśli wątek prawie natychmiast zakończy działanie, może nie istnieć w momencie, gdy zechcesz ustawić ten znacznik. Rozwiązanie polega na utworzeniu wątku w stanie uśpienia, ustawieniu znacznika na FALSE, po czym rozpoczęciu wykonywania wątku. Szczegóły tej procedury znajdziesz w następnym temacie.
Tworzenie wstrzymanego wątku
Wartością trybu tworzenia, przekazywanego funkcji AfxBeginThread, zwykle jest zero. To powoduje, że tworzony wątek natychmiast rozpoczyna działanie. Istnieją jednak przypadki, gdy trzeba opóźnić rozpoczęcie pracy wątku. W takich razach, jako tryb tworzenia, powinieneś przekazać stałą CREATE_SUSPENDED.
Istnieje kilka powodów, dla których mógłbyś tworzyć wątki w stanie uśpienia. Najpopularniejszą przyczyną jest potrzeba początkowego ustawienia zmiennych składowych w obiekcie wątku (przykładem może być listing 11.2). Możesz także zechcieć uruchomić wątek nieco później, jeśli po jego zakończeniu chcesz odwoływać się do obiektu wątku (patrz poprzedni temat).
Aby uruchomić wątek stworzony w stanie uśpienia, wywołaj ResumeThread. W tym momencie wątek rozpoczyna wykonywanie wskazanej funkcji (wątki robocze) lub wywołuje odpowiednie funkcje obiektu wyprowadzonego z klasy CWinThread (wątki interfejsu użytkownika).
Wywołując SuspendThread, możesz w dowolnym momencie wstrzymać działanie wątku. Jeśli wywołasz SuspendThread kilka razy, aby przywrócić wątek do działania, musisz tyle samo razy wywołać ResumeThread.
Odczytywanie zwracanej wartości
Aby poznać wartość zwróconą przez wątek, będziesz potrzebował jego uchwytu (składowej m_hThread obiektu wątku). Musisz także mieć pewność, że w momencie odczytu tej składowej obiekt jeszcze istnieje.
Uzbrojony w uchwyt wątku możesz wywołać funkcję Windows API, ::GetExitCode-Thread, przekazując w wywołaniu wskaźnik do zmiennej typu DWORD. Do wskazanej zmiennej zostanie zapisana wartość zwrócona przez wątek (zwrócona przez roboczą funkcję lub przekazana do AfxEndThread czy PostQuitMessage) lub wartość STILL_ACTIVE, jeśli wątek wciąż działa. Czasem, informacja że wątek jeszcze działa, to wszystko co chcesz wiedzieć.
Rodzaje obiektów synchronizacji
Istnieje kilka dostarczanych przez Windows obiektów synchronizacji. Możesz ich użyć do skoordynowania kilku wątków, tak aby mogły ze sobą sprawnie współpracować. Większość obiektów synchronizacji (a właściwie wszystkie z wyjątkiem sekcji krytycznej) może działać z wątkami tego samego procesu lub różnych procesów. MFC posiada specjalne klasy (wyprowadzone z CSyncObject) ułatwiające korzystanie ze wszystkich typów obiektów synchronizacji.
Obiekt CEvent to nieco więcej niż znacznik wykorzystywany wspólnie przez kilka wątków. Jeśli znacznik jest ustawiony, obiekt synchronizacji jest włączony. W przeciwnym razie obiekt jest wyłączony. Możesz tworzyć ręczny obiekt, który wymaga jawnego ustawiania i zerowania znacznika, lub możesz tworzyć obiekt automatyczny. Obiekty automatyczne same się zerują, gdy któryś wątek stwierdza, że znacznik jest ustawiony, Istnieje także wywołanie (PulseEvent) pozwalające wątkom na oczekiwanie na ręczny obiekt, a następnie zerujące go.
Obiekt CMutex to znacznik określający posiadacza zasobu. Gdy wątek spostrzeże że znacznik jest włączony, wszystkie inne wątki będą postrzegać ten znacznik jako wyłączony, do momentu kiedy pierwszy wątek go zwolni.
Obiekt CSemaphore to licznik odwołań. Gdy określona liczba wątków zagarnie semafor (czyli zastanie go w stanie włączenia), semafor stanie się wyłączony. Semafory są użyteczne przy ograniczaniu operacji (na przykład zapytań bazy danych lub żądań sieciowych) do określonej liczby wątków jednocześnie.
Ostatnim typem obiektu synchronizacji jest CCriticalSection. Sekcja krytyczna jest podobna do obiektu typu CMutex, lecz nie jest aż tak elastyczna. Korzyścią jest lepsza wydajność sekcji krytycznej niż znacznika typu CMutex. Z drugiej strony, sekcja krytyczna spełnia zadanie jedynie w obrębie pojedynczego wątku - dwa wątki należące do różnych procesów nie mogą użyć sekcji krytycznej. Oczywiście, mogą skorzystać ze znacznika typu CMutex.
Jeśli używasz obiektów synchronizacji w wątkach pojedynczego procesu, nie musisz nadawać obiektom nazw. Jeśli jednak korzystasz z tych obiektów do synchronizacji wątków kilku procesów, nadanie nazw obiektom synchronizacji'jest najłatwiejszym rozwiązaniem. Wszystkie obiekty dzielą wspólną przestrzeń nazw (innymi słowy, nie możesz nadać tej samej nazwy obiektowi CMutex i CSemaphore). Wszystkie wątki używające tej samej nazwy dla obiektu, w rzeczywistości wspólnie korzystają z pojedynczego obiektu.
Żaden z obiektów synchronizacji nie jest zbyt trudny w użyciu. Często największym problemem jest wymyślenie którego z nich użyć i jak zaprojektować architekturę, najlepiej spełniającą stawiane przed programem zadania.
Oczekiwanie na obiekt synchronizacji
Obiekty synchronizacji mogą być w stanie włączenia lub wyłączenia. Do wyznaczenia stanu obiektu synchronizacji, programy MFC używaj ą obiektu CSingleLock.
Aby sprawdzić czy obiekt jest włączony, stwórz obiekt CSingleLock, przesyłając do konstruktora wskaźnik do obiektu, który cię interesuje. Następnie wywołaj metodę Lock sprawdzając czy obiekt jest włączony.
Wywołując Lock określasz, że chcesz czekać przez określony czas na włączenie obiektu, lub że chcesz czekać w nieskończoność. Możesz także wyznaczyć bieżący stan.
Jeśli metoda Lock stwierdzi, że obiekt CMutex lub CSemaphore jest włączony, dodatkowo zmieni stan tego obiektu. Aby odwołać tę zmianę, wywołaj metodę Unlock. Efektem jej wywołania jest zwolnienie znacznika lub zwiększenie licznika semafora. Podczas wykonywania destruktora obiektu CSingleLock, metoda Unlock wywoływana jest automatycznie. To oznacza, że często nie będziesz musiał wywoływać jej samodzielnie.
Oczekiwanie na wielokrotny obiekt synchronizacji
Od czasu do czasu możesz chcieć pracować z kilkoma obiektami synchronizacji. Możesz zechcieć odnotować fakt nastąpienia jednego z czterech zdarzeń, lub chcesz poczekać, aż jeden z trzech obiektów CMutex stanie się dostępny.
W takich przypadkach zamiast obiektu CSingleLock użyj po prostu CMultiLock. Jego działanie jest takie same, ale zamiast pojedynczego obiektu, do konstruktora jest przekazywana tablica obiektów.
Użycie metody Onldle
Czasem zdarza się, że chcesz całkowicie zrezygnować z wielowątkowości. Założony cel możesz uzyskać przesłaniając funkcję Onldle obiektu swojej aplikacji (użyj do tego ClassWizarda). MFC wywołuje tę funkcję wtedy, gdy aplikacja nie ma żadnych komunikatów do obsłużenia.
Oczywiście nie powinieneś podejmować się obliczeń zajmujących zbyt dużo czasu, lub wewnątrz Onldle oczekiwać na cokolwiek. Złym pomysłem byłoby odczytywanie w Onldle danych z portu szeregowego. To zadanie byłoby dobrym kandydatem na osobny wątek (lub skorzystanie z nałożonych operacji wejścia/wyjścia, wymyślnej techniki stosowanej głównie w Windows NT). Jednak w przypadku drukowania w tle, sprawdzania pisowni czy tym podobnych zadań, funkcja Onldle stanowi doskonałą alternatywę. Musisz jednak mieć możliwość podziału zadania na małe fragmenty, wykonywane kolejno w dostępnych momentach wolnego czasu.
Bardzo ważne jest abyś wywoływał metodę Onldle klasy bazowej. MFC wykorzystuje przetwarzanie jałowe do wielu różnych działań, nawet nie podejrzewasz do czego. W jałowym czasie uaktualniane są przyciski pasków narzędzi, oprócz tego, w wolnym czasie MFC usuwa tymczasowe obiekty. Jeśli zapomnisz wywołać klasę bazową, zaczną się dziać dziwne rzeczy. Nie zawsze oczywiste jest powiązanie przetwarzania czasu wolnego z niewłaściwym działaniem pasków narzędzi lub wyciekami pamięci w innych częściach programu.
Rozdział 12 Dalsze kierunki
Przyszłość jest jak czekoladka w bombonierce - nigdy nie wiesz, co w niej znajdziesz zanim jej nie ugryziesz, a wtedy, nawet jeśli Ci nie smakuje, jest już za późno. Jednak, jeśli pomyślisz nieco wcześniej, możesz wgryźć się w przyszłość zanim ona pogryzie Ciebie.
Czy masz dzieci? Jeśli tak, wiesz, że doświadczeń rodzicielstwa nie da się porównać z niczym innym. To ciężka próba. Najpierw dziewięć miesięcy. Duża radość trwa tylko na początku. Potem już nie jest tak zabawnie, ale w dalszym ciągu oczekiwanie na nadejście jest bardzo ekscytujące. Aż pewnego dnia wpadasz w prawdziwą panikę. Po jakimś czasie przyzwyczajasz się do niej i akceptujesz niewygody codziennego życia. Na koniec, po dużym wysiłku, masz dziecko. Ale to tylko początek, nieprawdaż?
Pisanie książki jest bardzo podobne do posiadania dzieci. Zabiera mniej więcej tyle samo czasu. Na samym początku także daje dużo radości, ale z czasem przechodzi się podobne etapy. Szczególnie ciężka jest praca pod koniec.
Wracając do dzieci, każdy rodzic potwierdzi, że narodziny to tylko początek. Musisz troszczyć się o pieluszki i odżywki. Martwisz się, że dziecko jest głodne. Martwisz się, że nie rośnie zbyt szybko. Martwisz się, że rośnie za szybko. W rodzicielstwie nie ma zbyt wiele logicznego myślenia.
Kluczowym zwrotem w ostatnim akapicie jest martwić się. Jeśli miałbym wybrać jeden zwrot opisujący rodzicielstwo, byłby to właśnie ten. Martwisz się o dzieci przez całe życie. Martwisz się, że nie są szczęśliwe. Martwisz się, że nie radzą sobie w szkole. Martwisz się, że nie uznają twoich wartości. Martwisz się, że martwisz się zbyt dużo. Czasem martwisz się dlatego, że nie martwisz się wystarczająco.
Większość młodych ludzi z dziećmi myśli, że w pewnej chwili zostanie przekroczona jakaś magiczna granica (zwykle 18 lub 21 lat), kiedy będą mogli przestać się martwić. Cóż, przeszedłem przez to (tylko jedno z trójki naszych dzieci pozostaje w domu) i mówię Ci, że zmartwieniom nie ma końca. Gdy dorastają, możesz martwić się jeszcze bardziej, ponieważ przez większość czasu nie wiesz gdzie są, ani co robią. Martwisz się, że nie idzie im w pracy. Martwisz się, że nie układa im się w małżeństwach. Martwisz się, martwisz się i martwisz się.
Istnieje wiele etapów, które przebywasz jako rodzic, a większość z nich wiąże się z zanikającą kontrolą i rosnącymi zmartwieniami. Pierwszy dzień w szkole to dla rodzica zawsze duże wydarzenie. Na myśl przychodzi także pierwsza całonocna prywatka. Przyglądanie się, jak twoje dziecko pierwszy raz samodzielnie wyjeżdża samochodem, jest szczególnie stresujące (zwłaszcza jeśli to twój samochód). Pomaganie dziecku w pakowaniu się przed wyjazdem na studia też zalicza się do takich momentów. Całe rodzicielstwo pełne jest takich sytuacji.
Cóż, pisanie książki wiąże się z niepokojem innego rodzaju. Gdy książka już powstanie, nie możesz wiele zmienić. Ale w dalszym ciągu się martwisz. Martwisz się, że księgarnie nie zechcą jej sprzedawać. Martwisz się, tym, że technologia zmieni się gwałtownie. Najbardziej jednak martwisz się tym, że czytelnicy uznają ją za bezużyteczną. Ale nie możesz z tym nic zrobić, podobnie jak w przypadku “dorosłego" dziecka.
Jasne, możesz odpowiadać na pytania czytelników. Korzystając z Sieci możesz nawet tworzyć i udostępniać aktualizacje do tekstu, o czym nie tak dawno jeszcze nikomu się nie śniło. Ale w większości przypadków książka stanowi samodzielną całość. Wiele wspaniałych książek nie spotkało się z uznaniem, gdyż zostały wydane w niewłaściwym czasie, lub z jakichś innych, pozornie nieważnych powodów. Niektóre słabe książki odniosły sukces, ponieważ znalazły się we właściwym miejscu we właściwym czasie. Jednak zwykle istnieje sprawiedliwość - dobre książki sprzedają się dobrze, złe książki sprzedają się źle.
A co z tą książką? Sam musisz o tym zdecydować, ja mogę się jedynie martwić. Ta książka zawiera prawie wszystko to, czego przez lata dowiedziałem się o MFC. Mam nadzieję że znajdziesz w niej coś użytecznego, nawet jeśli stanowi ona tylko inne spojrzenie na MFC.
Koniec drogi?
Podobnie jak posiadanie dzieci, programowanie jest zajęciem, które nigdy się nie kończy. Czasem ludzie przychodzą do mnie i pytają czy oni (lub ich dzieci) powinni zająć się komputerami. Zadaję im jedno pytanie: Czy lubisz czytać? Nie znalazłem żadnego innego pytania, lepiej oddalającego sukces w programowaniu.
Powód jest prosty. Programowanie stale ewoluuje. Nie możesz pójść do szkoły, nauczyć się wszystkiego co akurat chcesz wiedzieć, po czym stwierdzić: “Teraz już umiem. Nauczyłem się programować."
Za każdym razem, gdy przyzwyczajam się do świata, ktoś radykalnie wszystko zmienia. Przeniosłem się z dużych komputerów Univac na mikrokomputery Data General. Potem musiałem nauczyć się o mikroprocesorach. Następnie nauczyłem się kochać Unixa. Potem Windows (być może słowo “kochać" jest w tym przypadku trochę za mocne). Gdzieś pomiędzy porzuciłem C dla C++. Uczyłem się Visual Basica, Delphi i kilku innych języków. Przesiadłem się z SDK do MFC.
Co jutro przyciągnie moje zainteresowanie? Nie wiem. Jeśli bym wiedział, prawdopodobnie teraz pisałbym właśnie o tym. Wiele osób ekscytuje się Javą. Oczywiście, Java jest interesująca. Ale naprawdę interesujące są te zagadnienia, o'których jeszcze nawet nie wiemy. Oczywiście, to coś czego nie można przewidzieć. Jednak myślę że możemy spojrzeć na postęp technologii i na tej podstawie wysnuć kilka przypuszczeń co do przyszłości oprogramowania.
Rzeczy, które nadejdą
Pomyśl o innowacjach zmieniających nasze życie. Kto w 1910 roku mógł przewidzieć powstanie tranzystora czy komputera osobistego? Nawet jeśli przewidziałbyś ich istnienie, czy przewidziałbyś ich ogromny wpływ na nasze życie? Powszechnie znana (i prawdopodobnie prawdziwa) jest historia, według której IBM przewidywał, że światowy rynek zadowoli się kilkoma (sześcioma czy siedmioma) komputerami.
Większość zmian jest jednak bardziej ewolucyjna niż rewolucyjna. Weźmy pod uwagę bankomat. Jednym z największych problemów w podróżach była konieczność posiadania przy sobie odpowiedniej ilości pieniędzy. Szczególny kłopot pojawiał się wtedy, gdy podróż trwała dłużej niż zamierzałeś. Nikt nie przyjmował zwykłego czeku. W tamtych czasach najważniejszym powodem posiadania karty American Express była możliwość płacenia nią w większości hoteli. W biurach American Express mogłeś zamienić na gotówkę także większe kwoty.
Obecnie, w czasie bankomatów, kto by się tym przejmował? Już nawet nie wożę ze sobą czeków. Mogę pojechać prawie wszędzie, włożyć kartę do automatu i wyciągnąć pokaźną ilość gotówki. Któż mógłby przewidzieć taką prostą zmianę w życiu? Bankomat sam w sobie nie jest niczym rewolucyjnym. Stanowi jedynie rozwinięcie małego, taniego komputera i sieci telekomunikacyjnej. Z kolei te rzeczy wyewoluowały z innych technologii.
Układy scalone a pamięć rdzeniowa
Według mnie, największą rewolucją technologiczną naszego świata było powstanie układu scalonego. Mało kto tak naprawdę wie czym jest układ scalony, mimo że jego istnienie ma wpływ na życie każdego z nas.
Sam w sobie układ scalony nie jest niczym niezwykłym. Istnieje tylko kilka rzeczy, których wykonanie bez układów scalonych mogłoby sprawić poważne trudności (a i tak większość z nich da się jakoś wykonać w inny sposób). To co sprawia, że układy scalone są tak użyteczne, to łatwość i niskie koszty ich powielania.
Dawniej, gdy w komputerach stosowano pamięć rdzeniową, firmy komputerowe zatrudniały ludzi z małymi rękami do nawlekania na druty małych rdzeni ferrytowych. Na małym obszarze (może pięćdziesięciu centymetrów kwadratowych) upychano setki takich rdzeni. Był to szczególnie drogi sposób produkcji pamięci.
Obecnie załoga kilku techników wykonuje czynności bardzo podobne do wywoływania rolki filmu. Gdy skończą, otrzymują duży krzemowy krążek zawierający setki układów pamięci. Każdy z układów ma pojemność setki i tysiące razy większą od starych pamięci rdzeniowych.
Ponieważ tworzenie układów scalonych jest takie tanie, firmy mogą je tanio sprzedawać. Dzięki niskiej cenie mogą sprzedać ich dużo więcej. To oznacza, że koszt projektowania układu scalonego można rozłożyć na miliony lub nawet miliardy sprzedanych sztuk. Dzięki temu firmy mogą poświęcać ogromne nakłady na opracowywanie nowych i lepszych układów.
Inną interesującą cechą układów scalonych jest to, że firmy je produkujące starają się aby układy różnych firm były między sobą zgodne. Zwykle można bez problemu łączyć mikroprocesory Intela z pamięcią Hitachi i układami wejścia/wyjścia firmy Texas Instruments. Jeśli z jakiegoś powodu Hitachi nie ma na składzie potrzebnych układów, możesz udać się do NEC-a i zakupić podobną część. Układ NEC-a (lub nowszy układ Hitachi) może być nawet lepszy (ale w dalszym ciągu zgodny).
Wyobraź sobie jak mogłoby to wyglądać w przemyśle oprogramowania. Co by było, gdybyś mógł poświęcić trzy lata opracowując składnik oprogramowania, wiedząc, że mógłbyś go sprzedać w milionach egzemplarzy? A co by było, gdybyś mógł kupić składniki, którym poświęcono tak dużo czasu? Oczywiście, takie składniki musiałyby bez przeszkód współpracować z innymi składnikami stworzonymi przez kogoś innego. Przy takim nakładzie włożonej pracy mógłbyś oczekiwać, że składniki będą współdziałały bez żadnych problemów i będą bardzo wydajne. I oczywiście, że będą tanie.
Fantazje? Nie, już widzimy, że to się zaczyna. W ten czy inny sposób kontrolki VBX (Visual Basic Extension), kontrolki ActiveX (OCX) czy JavaBeans, stanowią szkielet takiego mechanizmu. Czy już istnieje? Nie. Obecne komponenty nie współpracują ze sobą zbyt dobrze. Oprócz tego nie posiadają standardowych interfejsów, koniecznych, aby różni producenci mogli tworzyć zgodne ze sobą składniki. Ale na razie to tylko początek.
Trudno sobie wyobrazić środowisko programowe przyszłości, nie będące niczym więcej niż pojemnikiem do umieszczania kontrolek. W rzeczywistości, Visual Basic, Delphi i Internet Explorer już tym są.
Jeśli jesteś podobny do mnie, przelękniesz się programowania typu wskaż i kliknij. Nie rób tego! Wszyscy możemy zaszyć się w swoich kątach i zacząć pisać komponenty używane przez wszystkich innych. Oprócz tego, ponieważ tak się prawie na pewno stanie, możesz na tym bardzo skorzystać.
Inne zasoby
Ponieważ nauka programowania jest środkiem, a nie celem, ważne jest, aby trzymać rękę na pulsie. Tradycyjne metody w dalszym ciągu się sprawdzają: czytanie książek, prenumerata magazynów, kursy i targi oprogramowania. Jednak obecnie podstawowym miejscem szybkiego uzyskiwania najświeższych informacji o programowaniu (i myślę, że także o wszystkim innym) jest Internet.
Oczywiście, z Internetem wiążą się dwa problemy. Po pierwsze, do przejrzenia jest mnóstwo informacji. Dopóki nie szukasz czegoś bardzo konkretnego, łatwo możesz zostać przywalony natłokiem informacji. Przeszukiwanie grup dyskusyjnych też jest niewdzięcznym zadaniem, a to z powodu narastającej ilość spamu. Po drugie, nie można wierzyć wszystkiemu, co się przeczyta w Sieci. Informacje w książkach i czasopismach zostały zweryfikowane przez co najmniej kilka osób. Wydawca musi mieć pewność, że są aktualne. Jasne, drukowane materiały także czasem zawierają błędy, ale przynajmniej jest pewność, że ktoś włożył pewien wysiłek, aby było ich jak najmniej. Wiele stron WWW jest przynajmniej tak dokładnych, jak większość drukowanych materiałów. Ale niektóre nie są. Pojedyncza osoba może opublikować stronę WWW równie łatwo jak duża grupa osób (czasem z pewnych względów jest jej nawet łatwiej). Czasem pomyłki są niezamierzone, lecz można spotkać się z celową dezinformacją.
Z drugiej strony, fakt że praktycznie każdy może opublikować stronę WWW, stanowi jeden z najmocniejszych punktów Internetu. Prasa drukarska Gutenberga udostępniła książki dla wszystkich i w ten sposób zapoczątkowała epokę Oświecenia. Możliwość publikowania stron WWW przez każdego zainteresowanego także jest początkiem nowej epoki, która jeszcze nie ma nazwy. Być może epoka Wolnej Informacji.
Jak więc możesz znaleźć to, czego potrzebujesz? Poniżej zamieściłem listę miejsc, od których możesz zacząć, zarówno drukowanych, jak i elektronicznych, w których możesz znaleźć dalsze informacje na temat MFC:
• www.al-williams.com - To moja witryna WWW, gdzie możesz znaleźć radę miesiąca (prawie zawsze na temat MFC), informacje o książkach (łącznie z tą), aktualizacje i mnóstwo innych rzeczy. Znajdziesz tam także informacje o tym, czym zajmuje się moja firma, łącznie z seminariami na temat MFC i innymi tematami dotyczącymi programowania.
• Visual Developer Magazine - Ten magazyn (należący do The Coriolis Group) w każdym wydaniu zawiera moją kolumnę “Windows Commando." Choć kolumna oficjalnie dotyczy programowania Windows w C/C++, prawdopodobnie 9 na 10 artykułów porusza zagadnienia MFC. Magazyn jest pełen innych kolumn i artykułów. Więcej na ten temat znajdziesz na stronie www.coriolis.com.
• www.program.com - To doskonałe miejsce do wyszukiwania innych stron dotyczących programowania. Ponieważ wiele stron nie jest ani poważnych, ani dokładnych, warto wiedzieć gdzie można zacząć szukać czegoś przydatnego.
• www.r2m.com/windev - Inne miejsce zawierające łącza do stron związanych /. programowaniem.
• www.microsoft.com - Chyba nie trzeba wspominać, że mnóstwo informacji dotyczących MFC znajduje się na własnych stronach Microsoftu.
• www.stars.com - To miejsce nie jest związane z MFC, ale jest bardzo użyteczne, jeśli zajmujesz się jakimkolwiek programowaniem dla Internetu.
• MFC FAQ - Scot Wingo prowadzi doskonałe (najczęściej zadawane pytania FAQ) o MFC. Najnowszą wersję możesz znaleźć wyszukując zwrot “MFC FAQ" w dowolnym serwerze przeszukiwania Sieci.
• MFC TechNotes - Choć to może być oczywiste, uwagi techniczne dotyczące MFC są bardzo pomocne. Często zawierają informacje związane z tym, dlaczego MFC działa tak jak działa. Możesz je znaleźć przeglądając pomoc pakietu, szukając zwrotu TechNotes lub Technical Notes (w zależności od wersji Visual C++, 7. której korzystasz).
• www.reference.com - Jednym ze sposobów uniknięcia bezcelowego szumu w grupach dyskusyjnych jest wyszukiwanie interesujących artykułów. Istnieje kilka nadających się do tego miejsc, ale najlepszym jest chyba www.refe-rence.com. Możesz w nim nie tylko przejrzeć grupy dyskusyjne, ale także zapamiętać wyszukiwanie, uruchomić je automatycznie w określonym czasie i zażądać przysłania wyników. To doskonały pomysł.
Istnieje mnóstwo materiałów o MFC, więc jestem pewien, że sam także możesz uzupełnić tę listę. Jeśli nie, przynajmniej będziesz mógł od czegoś zacząć. Jeśli wiesz o naprawdę interesującym miejscu, które pominąłem, daj mi znać, a może na swojej stronie umieszczę odpowiednie łącze.
Dodatek A: Procedura obsługi ikon powłoki
Jeden z moich najstarszych przyjaciół, Scott Anderson, nauczył mnie czegoś, czego nigdy nie zapomnę. Gdy chodziłem do ogólniaka, niezbyt dobrze radziłem sobie z projektowaniem. Nasz końcowy projekt polegał na wykreśleniu kompletnego zestawu planów domu zaprojektowanego według własnego pomysłu. Gdy nauczyciel zaczął opowiadać nam co mamy zrobić, Scott podniósł rękę i zapytał czy można dodać taras. Choć nauczyciel nieco się opierał, Scott wymusił na nim możliwość dodania tarasu i wielu innych rzeczy. Lista się wydłużała: jakuzi, atrium, świetliki, wiatrołapy. Reszta nas zaczęła się denerwować.
Po lekcji spytałem Scotta, dlaczego upierał się przy tych wszystkich dodatkach, przecież zadanie i tak już jest niełatwe. Scott odparł, że prawdopodobnie żadnej z tych rzeczy nie zastosuje, ale chciałby mieć taką możliwość jeśli zechce to zrobić.
Dla mnie to była rewelacja. Pamiętałem o tym gdy zabierałem się za projektowanie różnorodnego sprzętu i oprogramowania. Scott został dyrektorem handlowym wielkiego przedsiębiorstwa; mogłem się tego spodziewać, widząc jak przekonał naszego nauczyciela do wszystkich tych zwariowanych rzeczy.
Myślę że projektanci Windows 95 (a także NT 4.0) też kiedyś otrzymali taką lekcję. Pewnie dla tego umożliwili rozszerzanie funkcji powłoki poprzez Rejestr i obiekty OLE COM (ActiveX). Być może nigdy z tej możliwości nie skorzystasz, ale jeśli chcesz by powłoka bezpośrednio współpracowała z twoimi typami plików, istnieje możliwość napisania potrzebnego kodu.
W rozdziale 6 opisywałem kreatora AppWizard użytkownika służącego do tworzenia procedury obsługi ikon powłoki (jednego z typów rozszerzeń powłoki). Jeśli chcesz się dowiedzieć czegoś więcej o tym jak działa procedura obsługi ikon (i inne rozszerzenia powłoki), ten dodatek jest dla ciebie.
Rodzaje rozszerzeń powłoki
Istnieje kilka różnych typów rozszerzeń powłoki (patrz tabela A.l). Technicznie rzecz biorąc, rozszerzenie powłoki to serwer OLE implementujący obiekty używane przez powłokę. Mówiąc po ludzku, rozszerzenie powłoki to DLL z kilkoma specjalnymi funkcjami. Ten DLL udostępnia także konkretny zestaw funkcji wywoływanych przez powłokę w celu wykonania pewnych operacji.
MFC w pewnym stopniu wspiera obiekty COM (niestety, nie tak bardzo jak w przypadku innych obiektów ActiveX lub programów OLE typu klient i serwer). Jednak nawet niewielkie wsparcie jest lepsze od żadnego - szczególnie jeśli chodzi o ActiveX. Zanim jednak zaczniesz, powinieneś sobie zadać pytanie: Czy to rzeczywiście potrzebne?
Tabela A.l. Rozszerzenia powłoki.
Nazwa
Icon
Context menu
Property sheet
Copy hook
Drop target
Data object
Quick View
Briefcase reconcile
Opis
Dostarcza dynamiczne ikony dla pików danego typu.
Dodaje menu kontekstowe lub menu “przeciągnij i upuść" (wyświetlane w wyniku przeciągania prawym przyciskiem).
Dodaje arkusze właściwości tworzone przez użytkownika
Zarządza operacjami na plikach
Reaguje w momencie gdy jakiś obiekt jest upuszczany na inny obiekt.
Dostarcza obiekt danych dla operacji “przeciągnij i upuść" lub “kopiuj i wklej".
Przetwarza plik dla szybkiego podglądu.
Łączy w jeden plik kilka wersji dokumentu.
Interfejsy
IExtractIcon, IPersistFile
IContextMenu, IShellExtInit
IShellPropSheetExt, IShellExtlnit
ICopyHook
IDropTarget, IPersistFile
IDataObject, IPersistFile
IFileYiewer, IPersistFile
IReconcilableObject, IPersistFile
Kiedy nie należy używać rozszerzeń powłoki
Rozszerzenia powłoki dają ogromne możliwości i -jak się zapewne spodziewasz - są niełatwe do napisania. Często zdarza się, że ten sam efekt możesz osiągnąć bez konieczności tworzenia prawdziwego rozszerzenia. Przykładowy program (na podstawie którego powstał kreator AppWizard do tworzenia projektów rozszerzeń powłoki) udostępnia
osobne ikony dla plików specjalnego typu. Ikona pliku zmienia się w zależności od liczby bajtów zawartych w tym pliku. To dobry przykład, w którym użycie rozszerzenia powłoki staje się koniecznością. Jeśli twoim celem jest dostarczenie własnej ikony, która zawsze jest taka sama, zapomnij o rozszerzeniach powłoki. Musisz jedynie dodać do Rejestru klucz Defaultlcon dla danego typu pliku.
Parę słów o obiektach COM
Być może wiele słyszałeś o obiektach COM i OLE. Zwykle nie bardzo wiadomo o co chodzi, głównie dlatego, że dokumentacja OLE zawiera mnóstwo nowych terminów. Unikając żargonu, można powiedzieć: obiekt COM to tablica wskaźników do funkcji. To wszystko. Program udostępniający wskaźniki do funkcji (serwer) oraz program używający tych wskaźników (klient) muszą się zgadzać co do tego, jakie funkcje znajdują się na poszczególnych pozycjach tablicy. Muszą także zgadzać się co do argumentów tych funkcji, zwracanych wartości itd. Nie muszą zgadzać się co do nazwy funkcji ani co do użytego języka programowania.
Technicznie, tablica wskaźników do funkcji to interfejs. Niektóre obiekty COM udostępniają więcej niż jeden interfejs i w związku z tym zawierają więcej niż jedną tablicę wskaźników. Wszystkie interfejsy obiektów COM posiadają trzy pozycje, które zawsze muszą być takie same. Te trzy funkcje tworzą interfejs o nazwie lUnknown.
Tabela A.2. zawiera opis funkcji interfejsu lUnknown. Ponieważ te trzy pozycje występują na początku każdego interfejsu obiektów COM, możesz potraktować dowolny interfejs jako interfejs lUnknown. Następnie możesz użyć funkcji Querylnterface (patrz tabela A.2) w celu pobrania informacji o innych interfejsach udostępnianych przez obiekt.
Tabela A.2. Interfejs lUnknow
nFunkcja Opis
AddRef Zwiększa licznik odwołań do obiektu
Release Zmniejsza licznik odwołań do obiektu; gdy wartość licznika osiągnie zero,
system zwalnia obiekt z pamięci
Querylnterface Zwraca wskaźnik do podanego interfejsu
Choć system tego nie wymaga, wiele funkcji COM zwraca wartość HRESULT. Jest to 32-bitowe słowo podzielone na pola bitowe, zawierające informacje o tym, czy wywołanie funkcji zakończyło się sukcesem, czy porażką. W przypadku prostych programów COM zwykle możesz zwrócić wartości S_OK (poprawny powrót), E_NOTIMPL (nie zaimplementowane) lub E_FAIL (fiasko). Podczas sprawdzania kodu powrotu nie zakładaj, że wartość zero oznacza sukces - wartość HRESULT informująca o powodzeniu nie zawsze musi mieć wartość zero. Zamiast tego, do testowania wartości HRESULT użyj makra SUCCEEDED.
Gdy DLL udostępnia obiekt COM, oprócz tego udostępnia także tzw. fabryką klas. Fabryka klas to specjalny obiekt COM zajmujący się tworzeniem obiektów specyficznego typu. Obiekty i interfejsy posiadają swoje numery GUlD identyfikujące ich typ. Numer GUID jest wykorzystywany w wywołaniach fabryki klas, podczas używania Querylnterface oraz w Rejestrze systemowym do identyfikacji obiektu. Do generowania numerów GUID służy program UUIDGEN, ale MFC także może je tworzyć.
Wsparcie COM w MFC
Napisanie serwera COM nie jest zbyt trudne (w końcu to tylko DLL), ale trzeba pamiętać o mnóstwie szczegółów. Na szczęście, jeśli o to poprosisz, MFC wykona za ciebie większość pracy. Gdy jednak spróbujesz odszukać kreatora AppWizard dla obiektów COM, nie znajdziesz go na liście. Sztuczka polega na stworzeniu obiektu automatyzacji OLE (który jest specjalną formą obiektu COM) i wycięciu wszystkiego, co nie jest potrzebne.
Zanim jednak do tego przystąpisz, przyjrzyjmy się obsłudze OLE przez MFC, jaka będzie potrzebna dla twojego obiektu COM. MFC obsługuje obiekty z wieloma interfejsami w dość dziwny sposób. Jeśli masz wrażenie, że interfejsy COM wyglądają tak jak wirtualne tablice funkcji (VTBL) w C++, nie mylisz się. Do tworzenia interfejsów MFC wykorzystuje właśnie funkcje wirtualne; tablica VTBL pełni rolę interfejsu COM. Przy obsłudze wielu interfejsów MFC stosuje zagnieżdżone obiekty (standardowo, ich nazwy zaczynają się od X). Tak mogłaby wyglądać klasa dla obiektu OLE udostępniającego dwa interfejsy, II i 12:
class myobject: public CCmdTarget
class XII {
// funkcje interfejsu II // (włącznie z AddRef, Release i Querylnterf ace)
STDMETHODIMP fl(void); Class XI2
// funkcje interfejsu 12
STDMETHODIMP f2(void); }; // end of myobject
Wszystkie obiekty COM są wyprowadzane z klasy CCmdTarget (podstawowej klasy obiektów MFC posiadających mapy). Ta bazowa klasa dostarcza domyślnych implementacji funkcji AddRef, Release i Querylnterface. Dobra rzecz, ponieważ AddRef i Release są bardziej skomplikowane niż można by sądzić, gdyż muszą być napisane bezpiecznie ze względu na wątki. Oto przykład, jak mogłaby wyglądać funkcja AddRef interfejsu II:
STDMETHODIMP_(ULONG) myobject::XI1::AddRefO
METHOD_PROLOGUE(myobject,II) return pThis->ExternalAddRef();
Zwróć uwagę na użycie X w nazwie klasy (tak jak w XII) w deklaracji funkcji i brak użycia X w makrze METHOD_PROLOGUE (gdzie użyto po prostu II). To makro pozwala na dostęp do zewnętrznej klasy (myobject) przy pomocy predefmiowanego wskaźnika pThis.
Zagnieżdżonych klas nie pisze się bezpośrednio. Zamiast tego w pliku nagłówkowym używa się makr BEGIN_INTERFACE_PART oraz END_INTERFACE_PART.
Używając tych makr powinieneś pominąć X w nazwach zagnieżdżonych klas. Pomiędzy tymi dwoma makrami powinny znaleźć się nazwy funkcji udostępnianych przez interfejs (nazwy trójki funkcji interfejsu lUnknown - AddRef, Release i Querylnterface - pojawiają się automatycznie, gdy użyjesz tych makr). Definicja obiektu myobject mogłaby wyglądać następująco:
class myobject : public CCmdTarget
{
DECLARE_DYNCREATE(myobject,CCmdTarget); // fabryka klas
DECLARE_OLECREATE(myobject); myobject(); DECLARE_MESSAGE_MAP() BEGIN_INTERFACE_PART(myobject,II)
STDMETHOD (f 1) ( ) ,-END_INTERFACE_PART(II) BEGIN_INTERFACE_PART(myobject,12)
STDMETHOD (f2)(); END_INTERFACE_PART(12)
MFC wspiera OLE głównie przy pomocy makr (takich jak BEGIN_INTERFACE_ PART). Oto najważniejsze z nich:
• DECLARE_INTERFACE_MAP - To makro pojawia się w pliku nagłówkowym twojej klasy. Tworzy mapę interfejsu, która wyznacza zagnieżdżone klasy odnoszące się do konkretnego interfejsu COM.
• BEGIN_INTERFACE_MAP - To makro tworzy mapę interfejsu w pliku CPP klasy.
• INTERFACE_PART - Użyj tego makra po makrze BEGIN_INTERFACE_ MAP w celu zdefiniowania konkretnych pozycji mapy.
Makro wymaga podania trzech argumentów: nazwy głównej klasy, identyfikatora IID interfejsu oraz nazwy zagnieżdżonej klasy (bez początkowej litery X).
• END_INTERFACE_MAP - To makro kończy mapę interfejsu.
• DECLARE_OLECREATE - Użyj tego makra w swoim pliku nagłówkowym w celu zadeklarowania fabryki klas.
• IMPLEMENT_OŁECREATE - To makro dopełnia makro DECLARE_ OLECREATE, ale występuje w pliku CPP. Wymaga podania nazwy klasy, nazwy zewnętrznej oraz identyfikatora CLSID obiektu.
• STDMETHOD - Deklaruje funkcję interfejsu COM zwracającą wartość HRESULT.
• STDMETHODIMP - Używane do zaimplementowania funkcji COM zwracającej wartość HRESULT.
• STDMETHOD_ - Deklaruje funkcję interfejsu COM zwracającą wartość innego typu niż HRESULT.
• STDMETHODIMP_ - Używane do zaimplementowania funkcji COM zwracającej wartość innego typu niż HRESULT. Przykład użycia tego makra znajdziesz zaglądając do kodu funkcji AddRef w sekcji “Wsparcie COM w MFC."
• METHOD_PROLOGUE - To makro pojawia się w funkcjach składowych zagnieżdżonych klas implementujących każdy z interfejsów.
Tworzy zmienną, pThis, będącą wskaźnikiem do głównej klasy. Przykład użycia tego makra znajdziesz zaglądając do kodu funkcji AddRef w
sekcji “Wsparcie COM w MFC."
Obiekt COM krok po kroku
Aby MFC stworzyło obiekt COM, musisz stworzyć DLL-a automatyzacji, a następnie usunąć części związane z automatyzacją. Oto kroki jakie musisz podjąć, aby wymusić na MFC wygenerowanie serwera COM:
1. Przy pomocy AppWizarda stwórz DLL automatyzacji OLE (rys. A. l).
2. Przy pomocy ClassWizard-a wyprowadź z CCmdTarget nową klasę, korzystając z automatyzacji OLE (rys. A.2).
3. Usuń linie odnoszące się do mapy ekspedycji (dispatch map) nowej klasy.
4. Usuń wywołanie funkcji EnableAutomation w konstruktorze.
5. Z nagłówka i pliku CPP usuń funkcję OnFinalRelease.
6. Usuń makro BEGIN_INTERFACE_PART definiujące interfejs IDispatch. Zastąp je tyloma makrami BEGIN_INTERFACE_PART, ilu potrzebujesz do zadeklarowania wszystkich interfejsów, które chcesz udostępnić.
7. W plikach H i CPP wstaw makra DECLARE_OLECREATE oraz IMPLE-MENT_OLECREATE.
8. Włącz do pliku wszelkie potrzebne nagłówki (dla rozszerzenia powłoki będą potrzebne WINNETWK.H, SHLOBJ.H oraz WINNLS.H).
9. Napisz wymagane funkcje interfejsów (łącznie z funkcjami interfejsu lUnknown).
Gdy zajrzysz do tematów w plikach pomocy dotyczących funkcji interfejsu, zobaczysz, że każda funkcja posiada początkowy wskaźnik this. Gdy używasz C++, kompilator automatycznie dodaje ten parametr, więc nie dodawaj go samodzielnie.
Szczegóły procedury obsługi ikon
Teraz, gdy masz już pewne pojęcie o tworzeniu serwerów COM, dowiesz się do czego można je wykorzystać. Spójrz na tabelę A. l. Zwróć uwagę, że procedura obsługi ikon wymaga obiektu COM posiadającego dwa interfejsy: IPersistFile oraz IExtractIcon. Jeśli stworzysz serwer obsługujący obiekty z tymi interfejsami (a także poinformujesz o tym powłokę), możesz stworzyć kod dostarczający dynamiczne ikony dla plików określonego typu. Pamiętaj: jeśli potrzebujesz jedynie statycznej ikony, po prostu zmodyfikuj Rejestr. Procedura obsługi ikon najlepiej sprawdza się w przypadku ikon, które dynamicznie zmieniają swój wygląd w oparciu o pewne atrybuty pliku czy jakieś inne dynamicznie zmieniające się parametry. Użycie procedury obsługi plików byłoby najlepszym rozwiązaniem w przypadku:
• Plików księgowości, które mogłyby mieć różne ikony dla kont zbilansowanych, otwartych i zamkniętych.
• Ikon plików sieciowych, wyświetlanych na zielono w przypadku plików dostępnych i na czerwono, w przypadku plików niedostępnych.
• Zestawu plików bazy danych, w której ikona plfku odnoszącego się do bieżącego miesiąca byłaby zawsze wyświetlana inaczej niż ikony pozostałych plików; każdego miesiąca inny plik automatycznie otrzymywałby tę ikonę.
• Ikony wyświetlającej inicjały właściciela pliku.
Tabele A.3 i A.4 zawierają funkcje, które muszą być zaimplementowane w procedurze obsługi plików. Jak widać, większość z nich to tylko ewentualne punkty wejścia. Funkcja IPersistFile::Load przekazuje do twojego obiektu nazwę pliku w postaci UNICODE. Powłoka chce, aby obiekt dostarczył ikonę dla tego pliku. Zwykle zamienisz nazwę pliku na ANSI (chyba że już pracujesz z UNICODE) i przechowasz do późniejszego wykorzystania.
Tabela A.3. Interfejs IPersistFil
eFunkcja Opis
GetClassID Niewymagane;
zwróć E_NOTIMPL
IsDirty
Niewymagane; zwróć E_NOTIMPL
Load Powłoka przekazuje nazwę pliku do twojego obiektu; zamień ją z UNCODE na ANSI i
przechowaj do późniejszego wykorzystania.
Save Niewymagane; zwróć E_NOTIMPL
SaveCompleted Niewymagane; zwróć E_NOTIMPL
GetCurFile Niewymagane; zwróć E_NOTIMPL
Tabela A.4. Interfejs IExtractlco
nFunkcja Opis
GetlconLocation Zwraca
nazwę pliku zawierającego ikonę oraz indeks ikony pliku.
Extract
Zwraca uchwyt do ikony.
Wszystkie interfejsy zawierają także funkcje interfejsu IDnknown (patrz tabela A.2)
Dwie funkcje dostarczające ikony (IExtractIcon::GetIconLocation oraz IExtractIcon::Extract) są wymienne. Zwykle możesz zaimplementować tylko jedną z nich. GetlconLocation umożliwia zwrócenie nazwy pliku oraz numeru zawartej w nim ikony. Często do odczytania nazwy DLL-a wygodnie jest użyć funkcji GetModuleFileName. Możesz wtedy użyć ikon zawartych wewnątrz DLL-a. Możesz także użyć funkcji Extract zwracając uchwyt HICON ikony. Jest to użyteczne, gdy chcesz samodzielnie narysować ikonę (na przykład na podstawie listy obrazków). Do argumentów funkcji Extract należą nazwa pliku i numer ikony, ustawione w funkcji GetlconLocation (jeśli są używane).
Listingi A. l i A.2 zawierają główne części przykładowej procedury obsługi ikon. Ta procedura obsługi dostarcza dwóch ikon: dla plików o rozmiarze poniżej 1024 bajtów oraz dla plików większych rozmiarów. Obie ikony znajdują się w pliku DLL-a, więc procedura musi jedynie stwierdzić, której ikony należy użyć.
Samo dostarczenie procedury obsługi to jeszcze nie wszystko - musisz także poinformować o tym powłokę. Przypuśćmy że chcesz zastosować tę procedurę dla plików typu .VIZ. W takim przypadku potrzebne są następujące pozycje Rejestru:
[HKEY_CLASSES_ROOT\.VIZ]="VIZFile" [HKEY_CLASSES_ROOTWIZFile]="Visual Developer File"
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{twój CLSID}]=" YIZFile Icon Ext"
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\ {twój CLSID}\InProcServer32]="vizicon.dll" "ThreadingModel"="Apartment"
[HKEY_CLASSES_ROOT\VIZFile\shellex\IconHandler]="{twój CLSID}" [HKEY_CLASSES_ROOT\VIZFile\DefaultIcon]="%!"
l to wszystko z rozszerzeniami powłoki?
Większość rozszerzeń powłoki działa podobnie jak procedura obsługi ikon. Niektóre z rozszerzeń zamiast interfejsu IPersistFile używają interfejsu IShellExtInit, ale ta różnica jest trywialna. Dobra wiadomość jest taka, że jeśli wiesz jak stworzyć jedno rozszerzenie, reszta także będzie łatwa.
(W związku z ograniczoną szerokością strony, zwróć uwagę że na listingu A. l linie rozpoczynające się od IMPLEMENT_OLECREATE... zostały rozbite na dwa wiersze; w rzeczywistości powinny być wpisane w jednym wierszu.
Listing A.1. Główny plik procedury obsługi ikon
// IconHandler.cpp : implementation file
ttinclude
"stdafx.h"
#include
"Vizlcon.h"
ttinclude
ttinclude
łtinclude
łtinclude
winnetwk.h"
shlobj.h" winnls.h" IconHandler.h"
FILE
tłifdef
_DEBUG ttdefine new DEBUG_NEW
#undef THIS_FILE static char THIS_FILE[;
#endif
II CIconHandler
IMPLEMENT_DYNCREATE (CIconHandler , CCmdTarget)
CIconHandler : : CIconHandler ( )
CIconHandler : : -CIconHandler ( )
BEGIN_MESSAGE_MAP (CIconHandler , CCmdTarget)
//{{AFX_MSG_MAP (CIconHandler)
// NOTĘ - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP END_MESSAGE_MAP ( )
// Notę: This is the GUID used for your icon handler // {4E2A4CFA-5862-11D2-A6BB-00409514FD8B} static const IID IID_IIconHandler =
{ Ox4E2A4CFA, 0x5852, OxllD2,
{ OxA6,OxBB, 0x00, 0x40, 0x95, 0x14, OxFD,Ox8B } } ;
IMPLEMENT_OLECREATE ( CIconHandler, "AlIconHandler " , Ox4E2A4CFA, 0x5862, OxllD2 , OxA6, OxBB, 0x00, 0x40, 0x95, 0x14, OxFD, Ox8B) ;
BEGIN_INTERFACE_MAP ( CIconHandler , CCmdTarget )
INTERFACE_PART( CIconHandler, IID_IPersistFile, PersistFile) INTERFACE_PART ( CIconHandler, IID_IExtractIcon, Icon)
END_INTERFACE_MAP ( )
STDMETHODIMP_(ULONG) CIconHandler: : KPersistFile : :AddRef () {
METHOD_PROLOGUE (CIconHandler, PersistFile)
return pThis->ExternalAddRef ( ) ;
STDMETHODIMP_(ULONG) CIconHandler: :XPersistFile: :Release() {
METHOD_PROLOGUE (CIconHandler, PersistFile)
return pThis->ExternalRelease ( ) ,-
STDMETHODIMP CIconHandler: :XPersistFile: :QueryInterf ace (REFIID
iid,void FAR * FAR *pObj )
{
METHOD_PROLOGUE (CIconHandler, PersistFile)
return (HRESULT)pThis->ExternalQueryInterface (&iid,pObj ) ;
STDMETHODIMP CIconHandler::XPersistFile::IsDirty() {
return E_NOTIMPL;
STDMETHODIMP CIconHandler::XPersistFile::LoadlLPCOLESTR pszFileName,DWORD)
<
METHOD_PROLOGUE(CIconHandler, PersistFile) ; WideCharToMultiByte(CP_ACP,O,(LPCWSTR)pszFileName,-l,
pThis->m_szFile,sizeof(pThis->m_szFile),
NULL,NULL); return NOERROR;
STDMETHODIMP CIconHandler::XPersistFile::Save(LPCOLESTR,BOOL) return E_NOTIMPL;
STDMETHODIMP CIconHandler::XPersistFile::SaveCompleted(LPCOLESTR)
return E_NOTIMPL; }
STDMETHODIMP CIconHandler::XPersistFile::GetCurFile(LPOLESTR FAR *)
return E_NOTIMPL; }
STDMETHODIMP CIconHandler::XPersistFile::GetClassID(LPCLSID)
return E_NOTIMPL; }
STDMETHODIMP_(ULONG) CIconHandler::XIcon::AddRefO
METHOD_PROLOGUE(CIconHandler,Icon) return pThis->ExternalAddRef();
STDMETHODIMP_(ULONG) CIconHandler::XIcon::Release()
METHOD_PROLOGUE(CIconHandler, Icon) return pThis->ExternalRelease();
STDMETHODIMP CIconHandler::XIcon::Querylnterface(REFIID iid,void
FAR * FAR *pObj)
{
METHOD_PROLOGUE(CIconHandler,Icon)
return (HRESULT)pThis->ExternalQueryInterface(&iid,pObj);
STDMETHODIMP CIconHandler::XIcon::GetIconLocation(UINT uFlags,LPSTR szlconFile,
UINT cchMax,int *pilndex,
UINT *pwFlags) {
METHOD“PROLOGUE(CIconHandler,Icon);
// TODO: Supply icon flle & index here -- if you need the file name // use pThis->m_szFile OR return S_FALSE and supply an HICON below
: :GetModuleFileName (AfxGetInstanceHandle( ) , szlconFile, cchMax) ;
*pilndex=0;
*pwFlags | =GIL_PERINSTANCE; return S_OK; }
STDMETHODIMP CIconHandler : :X!con: : Extract (LPCSTR, UINT, HICON * , HICON
*,UINT)
{
METHOD_PROLOGUE( CIconHandler, Icon) ;
// TODO: Supply HICON here -- if you need the f ile name // use pThis->m_szFile OR return S_FALSE and provide an icon f ile // (see above)
return S_FALSE;
// CIconHandler message handlers
Listing A.2. Plik nagłówkowy.
// IconHandler .h : header f ile
I 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 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 1
II CIconHandler command target
class CIconHandler : public CCmdTarget {
DECLARE_DYNCREATE (CIconHandler ) DECLARE_OLECREATE( CIconHandler) CIconHandler O; // protected constructor used by
// dynamie creation // Attributes public :
char m_szFile[256] ;
// Operations public :
// Overrides
// ClassWizard generated virtual function overrides / / { {AFX_VIRTUAL (CIconHandler ) public : //}}AFX_VIRTUAL
// Implementation protected:
virtual -CIconHandler ( ) ;
// Generated message map functions
//{{AFX_MSG(CIconHandler)
// NOTĘ - the ClassWizard will add and remove member
// functions here.
//}}AFX_MSG
DECLARE_MESSAGE_MAP ( ) DECLARE_INTERFACE_MAP ( ) BEGIN_INTERFACE_PART(PersistFile, IPersistFile)
STDMETHOD (GetClassID)(LPCLSID);
STDMETHOD (IsDirty)();
STDMETHOD (Load)(LPCOLESTR,DWORD);
STDMETHOD (Save)(LPCOLESTR,BOOL);
STDMETHOD (SaveCompleted)(LPCOLESTR);
STDMETHOD (GetCurFile)(LPOLESTR
FAR *);
END_INTERFACE_PART(PersistFile)
BEGIN_INTERFACE_PART(Icon,IExtractIcon)
STDMETHOD (GetlconLocation)(UINT,LPSTR,UINT,int *,UINT *).
STDMETHOD (Extract)(LPCSTR,UINT,HICON *,HICON *,UINT); END_INTERFACE_PART(Icon)