LEKCJA 46: O PROGRAMACH OBIEKTOWO - ZDARZENIOWYCH. ________________________________________________________________ Po aplikacjach sekwencyjnych, proceduralno-zdarzeniowych, jedno- i dwupoziomowych, pora rozważyć dokładniej stosowanie technik obiektowych. ________________________________________________________________ Programy pracujące w środowisku Windows tworzone są w oparciu o tzw. model trójwarstwowy. Pierwsza warstwa to warstwa wizualizacji, druga - interfejs, a trzecia - to właściwa maszyneria programu. W tej lekcji zajmiemy się "anatomią" aplikacji wielowarstwowych a następnie sposobami wykorzystania bogatego instrumentarium oferowanego przez Borlanda wraz z kompilatorami BC++ 3+...4+. Biblioteka OWL w wersjach BORLAND C++ 3, 3.1, 4 i 4.5 zawiera definicje klas potrzebnych do tworzenia aplikacji dla Windows. Fundamentalne znaczenie dla większości typowych aplikacji mają następujące klasy: TModule (moduł - program lub biblioteka DLL) TApplication (program - aplikacja) TWindow (Okno) Rozpocznę od krótkiego opisu dwu podstawowych klas. KLASA TApplication. Tworząc obiekt klasy TNaszProgram będziemy wykorzystywać dziedziczenie od tej właśnie klasy bazowej: class TNaszProgram : public TApplication Podstawowym celem zastosowania tej właśnie klasy bazowej jest odziedziczenie gotowej funkcji - metody virtual InitMainWindow() (zainicjuj główne okno programu). Utworzenie obiektu klasy TNaszProgram następuje zwykle w czterech etapach: * Windows uruchamiają program wywołując główną funkcję WinMain() lub OwlMain() wchodzącą w skład każdej aplikacji. * Funkcja WinMain() tworzy przy pomocy operatora new nowy obiekt - aplikację. * Obiekt - aplikacja zaczyna funkcjonować. Konstruktor obiektu (własny, bądź odziedziczony po klasie TApplication) wywołuje funkcję - wirtualną metodę InitMainWindow(). * Funkcja przy pomocy operatora new tworzy obiekt - okno aplikacji. Wskaźnik do utworzonego obiektu zwraca funkcja GetApplication(). Dla zobrazowania mechanizmów poniżej przedstawiamy uproszczony "wyciąg" z dwu opisywanych klas. Nie jest to dokładna kopia kodu źródłowego Borlanda, lecz skrót tego kodu pozwalający na zrozumienie metod implementacji okienkowych mechanizmów wewnątrz klas biblioteki OWL i tym samym wewnątrz obiektów obiektowo - zdarzeniowych aplikacji. A oto najważniejsze elementy implementacji klasy TApplication: - Konstruktor obiektu "Aplikacja": TApplication::TApplication(const char far* name, HINSTANCE Instance, HINSTANCE prevInstance, const char far* CmdLine, int CmdShow, TModule*& gModule) { hPrevInstance = prevInstance; nCmdShow = CmdShow; MainWindow = 0; HAccTable = 0; //Accelerator Keys Table Handle BreakMessageLoop = FALSE; AddApplicationObject(this); //this to wskaźnik do własnego gModule = this; //obiektu, czyli do bież. aplikacji } Funkcja - metoda "Zainicjuj Instancję": void TApplication::InitInstance() { InitMainWindow(); if (MainWindow) { MainWindow->SetFlag(wfMainWindow); MainWindow->Create(); MainWindow->Show(nCmdShow); } Metoda "Zainicjuj główne okno aplikacji": void TApplication::InitMainWindow() { SetMainWindow(new TFrameWindow(0, GetName())); } Metoda Run() - "Uruchom program": int TApplication::Run() { int status; { if (!hPrevInstance) InitApplication(); InitInstance(); status = MessageLoop(); } A oto pętla pobierania komunikatów w uproszczeniu. "Pump" to po prostu "pompowanie" komunikatów (message) oczekujących (waiting) w kolejce. PeekMessage() to sprawdzenie, czy w kolejce oczekuje komunikat. PM_REMOWE to "brak komunikatu". BOOL TApplication::PumpWaitingMessages() { MSG msg; BOOL foundOne = FALSE; while (::PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { foundOne = TRUE; if (msg.message == WM_QUIT) { BreakMessageLoop = TRUE; MessageLoopResult = msg.wParam; ::PostQuitMessage(msg.wParam); break; } if (!ProcessAppMsg(msg)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } } return foundOne; } int TApplication::MessageLoop() { long idleCount = 0; MessageLoopResult = 0; while (!BreakMessageLoop) { TRY { if (!IdleAction(idleCount++)) ::WaitMessage(); if (PumpWaitingMessages()) idleCount = 0; } if (MessageLoopResult != 0) { ::PostQuitMessage(MessageLoopResult); break; } }) } BreakMessageLoop = FALSE; return MessageLoopResult; } else if (::IsWindowEnabled(wnd)) { *(info->Wnds++) = wnd; ::EnableWindow(wnd, FALSE); } } return TRUE; } KLASA TWindow. Klasa TWindow (Okno) zawiera implementację wielu przydatnych przy tworzeniu aplikacji "cegiełek". Poniżej przedstawiono fragment pliku źródłowego (patrz \SOURCE\OWL\WINDOW.CPP). Łatwo można rozpoznać pewne znane już elementy. ... extern LRESULT FAR PASCAL _export InitWndProc(HWND, UINT, WPARAM, LPARAM); ... struct TCurrentEvent //Struktura BieżąceZdarzenie { TWindow* win; //Wskażnik do okna UINT message; //Komunikat WPARAM wParam; LPARAM lParam; }; ... DEFINE_RESPONSE_TABLE(TWindow) //Makro: Zdefiniuj tablicę odpowiedzi na zdarzenia //EV_WM_SIZE - Zdarzenie (EVent)-nadszedł komunikat WM_SIZE ... EV_WM_SETCURSOR, EV_WM_SIZE, EV_WM_MOVE, EV_WM_PAINT, EV_WM_LBUTTONDOWN, EV_WM_KILLFOCUS, EV_WM_CREATE, EV_WM_CLOSE, EV_WM_DESTROY, EV_COMMAND(CM_EXIT, CmExit), ... END_RESPONSE_TABLE; Funkcje - metody obsługujące komunikaty zaimplementowane zostały wewnątrz klasy TWindow tak: TWindow::EvCreate(CREATESTRUCT far&) { SetupWindow(); return (int)DefaultProcessing(); } void TWindow::EvSize(UINT sizeType, TSize&) { if (Scroller && sizeType != SIZE_MINIMIZED) { Scroller->SetPageSize(); Scroller->SetSBarRange(); } } Metoda GetWindowClass() bardzo przypomina klasyczne zainicjowanie zanej już struktury WNDCLASS: void TWindow::GetWindowClass(WNDCLASS& wndClass) { wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = *GetModule(); wndClass.hIcon = 0; wndClass.hCursor = ::LoadCursor(0, IDC_ARROW); wndClass.hbrBackground = HBRUSH(COLOR_WINDOW + 1); wndClass.lpszMenuName = 0; wndClass.lpszClassName = GetClassName(); wndClass.style = CS_DBLCLKS; wndClass.lpfnWndProc = InitWndProc; } Skoro te wszystkie "klocki" zostały już zaimplementowane wewnątrz definicji klas, nasze programy powinny tylko umiejętnie z nich korzystać a teksty źródłowe programów powinny ulec skróceniu i uproszczeniu. STADIA TWORZENIA OBIEKTOWEJ APLIKACJI. Ponieważ znakomita większość dzisiejszych użytkowników pracuje z Windows 3.1, 3.11, i NT - zaczniemy tworzenie aplikacji od umieszczenia na początku informacji dla OWL, że nasz docelowy program ma być przeznaczony właśnie dla tego środowiska: #define WIN31 Jak już wiemy dzięki krótkiemu przeglądowi struktury bazowych klas przeprowadzonemu powyżej - funkcje API Windows są w istocie klasycznymi funkcjami posługującymi się mechanizmami języka C. C++ jest "pedantem typologicznym" i przeprowadza dodatkowe testowanie typów parametrów przekazywanych do funkcji (patrz "Technika programowania w C++"). Aby ułatwić współpracę, zwiększyć poziom bezpieczeństwa i "uregulować" potencjalne konflikty - dodamy do programu: #define STRICT Chcąc korzystać z biblioteki OWL wypada dołączyć właściwy plik nagłówkowy: #include Plik OWL.H zawiera już wewnątrz dołączony WINDOWS.H, który występował we wcześniejszych aplikacjach proceduralno - zdarzeniowych i jeszcze parę innych plików. Ponieważ chcemy skorzystać z gotowych zasobów - odziedziczymy pewne cechy po klasie bazowej TApplication. Zgodnie z zasadami programowania obiektowego chcąc utworzyć obiekt musimy najpierw zdefiniować klasę: class TOkno ... i wskazać po której klasie bazowej chcemy dziedziczyć: class TOkno : public TApplication { ... Konstruktor obiektu klasy TOkno powinien tylko przekazać parametry konstruktorowi klasy bazowej - i już. class TOkno : public TApplication { public: TOkno(LPSTR name, HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nShow) : TApplication(name, hInstance, hPrevInstance, lpCmdLine, nShow) { return; } virtual void InitMainWindow(); }; Umieściliśmy w definicji klasy jeszcze jedną funkcję inicjującą główne okno aplikacji. Możemy ją zdefiniować np. tak: void TOkno::InitMainWindow(void) { MainWindow = new (TWindow(0, "Napis - Tytul Okna")); } Działanie funkcji polega na utworzeniu nowego obiektu (operator new) klasy bazowej TWindow. Główne okno stanie się zatem obiektem klasy TWindow (Niektóre specyficzne aplikacje posługują się okienkiem dialogowym jako głównym oknem programu. W takiej sytuacji dziedziczenie powinno następować po klasie TDialog). Konstruktorowi tego obiektu przekazujemy jako parametr napis, który zostanie umieszczony w nagłówku głównego okna aplikacji. Pierwszy argument (tu ZERO) to wskażnik do macieżystego okna, ponieważ w bardziej złożonych aplikacjach występują okna macieżyste (parent) i okna potomne (child). Okno macieżyste to zwykle obiekt klasy "główne okno" a okno potomne to najczęściej okienko dialogowe, bądź okienko komunikatów. W tym przypadku wpisujemy zero, ponieważ program nie posiada w tym stadium wcześniejszego okna macieżystego. Pozostało nam jeszcze dodać funkcję WinMain() i pierwszy program obiektowy w wersji "Maszyna do robienia nic" jest gotów. Listing . Obiektowa "Maszyna do robienia nic" ________________________________________________________________ #define STRICT #define WIN31 #include class TOkno : public TApplication { public: TOkno(LPSTR AName, HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) : TApplication(AName, hInstance, hPrevInstance, lpCmdLine, nCmdShow) {}; void InitMainWindow(){MainWindow = new TWindow(NULL, Name);}; }; int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { TOkno OBIEKT("Windows - Program PW1", hInstance, hPrevInstance, lpCmdLine, nCmdShow); OBIEKT.Run(); return 0; } ________________________________________________________________ Wykonanie takiej aplikacji przebiega następująco. Windows wywołują główną funkcję WinMain(), która przekazuje swoje parametry do konstruktora klasy TOkno::TOkno(). Konstruktor przekazuje parametry do konstruktora klasy bazowej TApplication(). Po skonstruowaniu obiektu w pamięci funkcja wywołuje odziedziczoną metodę Run(). Funkcja Run() wywołuje metody InitApplication() (zainicjuj aplikację) i InitInstance() (zainicjuj dane wystąpienie programu). Metoda InitInstance() wywołuje funkcję InitMainWindow(), która buduje główne okno aplikacji na ekranie. Po pojawieniu się okna rozpoczyna działanie pętla pobierania komunikatów (message loop). Pętla komunikatów działa aż do otrzymania komunikatu WM_QUIT. Rozbudujmy aplikację o okienko komunikatów. Zastosujemy do tego funkcję MessageBox(). Funkcja zostanie użyta nie jako metoda (składnik obiektu), lecz jako "wolny strzelec" (stand alone function). Listing B. Maszyna rozszerzona o okienka komunikatów. ________________________________________________________________ #define WIN31 #define STRICT #include class TOkno : public TApplication { public: TOkno(LPSTR Nazwa, HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) : TApplication(Nazwa, hInstance, hPrevInstance, lpCmdLine, nCmdShow) {}; void InitMainWindow(){MainWindow = new TWindow(NULL, "Okno PW2" );}; }; int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { TOkno OBIEKT("Okno PW2", hInstance, hPrevInstance, lpCmdLine, nCmdShow); LPSTR p1 = "Jesli wybierzesz [Anuluj]\n- aplikacja nie ruszy!"; LPSTR p2 = "START"; if (MessageBox(NULL, p1, p2, MB_OKCANCEL) == IDCANCEL) MessageBox(NULL, "I juz..." , "KONIEC" , MB_OK); else OBIEKT.Run(); return 0; } ________________________________________________________________ Uwagi techniczne. Ścieżki do katalogów: ..\INCLUDE;..\CLASSLIB\INCLUDE;..\OWL\INCLUDE; ..\LIB;..\CLASSLIB\LIB;..\OWL\LIB; Konsolidacja: Options | Linker | Settings: Windows EXE (typ aplikacji) Options | Linker | Libraries: - Container class Libraries: Static (bibl. klas CLASSLIB) - OWL: Static (bibl. OWL statycze .LIB) - Standard Run-time Lib: Static (bibl. uruchomieniowe .LIB) (.) None - oznacza żadne (nie zostaną dołączone); (.) Static - oznacza statyczne .LIB (.) Dinamic - oznacza dynamiczne .DLL ________________________________________________________________ JAK ROZBUDOWYWAĆ OBIEKTOWE APLIKACJE? Mimo całego uroku obiektowych aplikacji pojawia się tu wszakże drobny problem. Skoro komunikacja klawiatura/myszka -> program -> ekran nie odbywa się wprost, lecz przy pomocy wymiany danych pomiędzy obiektami różnych warstw - w jaki sposób (w którym miejscu programu) umieścić "zwyczajne" funkcje i procedury i jak zorganizować wymianę informacji. "Zwyczajne" funkcje będą przecież wchodzić w skład roboczych części naszych programów (Engine). Rozważmy to na przykładzie aplikacji reagującej na naciśnięcie klawisza myszki. Najbardziej istotny - "newralgiczny" punkt programu został zaznaczony w tekście "<-- TU". Od Windows przejmiemy obsługę komunikatów WM_LBUTTONDOWN, WM_RBUTTONDOWN. Aby wiedzieć, w którym miejscu ekranu jest kursor myszki, wykorzystamy informacje przenoszone przez parametr lParam. Rozpoczniemy tworzenie programu od zdefiniowania klasy. #define WIN31 #define STRICT #include #include #include class TNAplikacja : public TApplication { public: TNAplikacja(LPSTR AName, HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) : TApplication(AName, hInstance, hPrevInstance, lpCmdLine, nCmdShow) {}; virtual void InitMainWindow(); }; Wykorzystamy okienko komunikatu do świadomego zakończenia pracy aplikacji. Klasa TApplication jest wyposażona w metodę CanClose() (czy można zamknąć?) służącą do zamykania głównego okna aplikacji. Metoda została zaimplementowana tak: BOOL TApplication::CanClose() { if (MainWindow) return (MainWindow->CanClose()); else return (TRUE); } Będzie nam więc potrzebna własna wersja metody CanClose() i wskaźnik do obiektu MainWindow. Wskaźnik (typu far utworzony przez składowe makro _FAR) wygenerujemy przy pomocy makra _CLASSDEF(nazwa_klasy): _CLASSDEF(TGOkno) Implementujemy teraz klasę główne okno aplikacji. Jako klasę bazową stosujemy TWindow. class TGOkno : public TWindow { public: TGOkno(PTWindowsObject AParent, LPSTR ATitle) : TWindow(AParent, ATitle) {}; Konstruktor tradycyjnie wykorzystujemy do przekazania parametrów do konstruktora klasy bazowej. PTWindowsObject AParent to wskażnik (PoinTer) do obiektu "okno" a ATitle to string - tytuł. Obsługa komunikatów kierowanych do tego okna może być realizowana przy pomocy metod zaimplementowanych jako elemeny składowe klasy Główne Okno - TGOkno. Program graficzny powinien reagować raczej na myszkę niż na klawiaturę. Windows rozpoznają zdarzenia związane z myszką i generują komunikaty o tych zdarzeniach. Zdarzenia myszki (mouse events). ________________________________________________________________ Komunikat Zdarzenie ________________________________________________________________ WM_MOUSEMOWE - przesunięto myszkę (wewnątrz obszaru roboczego - inside the client area - ICA) WM_LBUTTONDOWN - naciśnięto LEWY klawisz myszki (ICA) WM_LBUTTONDBLCLK - naciśnięto dwukrotnie LEWY klaw. (ICA) WM_LBUTTONUP - puszczono LEWY klawisz (ICA) WM_RBUTTONDOWN - naciśnięto PRAWY klawisz myszki (ICA) WM_RBUTTONDBLCLK - naciśnięto dwukrotnie PRAWY klaw. (ICA) WM_RBUTTONUP - puszczono PRAWY klawisz (ICA) WM_MBUTTONDOWN - naciśnięto ŚRODK. klawisz myszki (ICA) WM_MBUTTONDBLCLK - naciśnięto dwukrotnie ŚROD. klaw. (ICA) WM_MBUTTONUP - puszczono ŚRODKOWY klawisz (ICA) WM_NCMOUSEMOVE - ruch myszki poza client area (NCA) WM_NLBUTTONDOWN - naciśnięto LEWY klawisz myszki poza obszarem roboczym - non-client area (NCA) WM_NCLBUTTONDBLCLK - naciśnięto dwukrotnie LEWY klaw. (NCA) WM_NCLBUTTONUP - puszczono LEWY klawisz (NCA) WM_NCRBUTTONDOWN - naciśnięto PRAWY klawisz myszki (NCA) WM_NCRBUTTONDBLCLK - naciśnięto dwukrotnie PRAWY klaw. (NCA) WM_NCRBUTTONUP - puszczono PRAWY klawisz (NCA) WM_NCMBUTTONDOWN - naciśnięto ŚR. klawisz myszki (NCA) WM_NCMBUTTONDBLCLK - naciśnięto dwukrotnie ŚRODK. klaw. (NCA) WM_LBUTTONUP - puszczono ŚRODKOWY klawisz (NCA) ________________________________________________________________ Następna tabelka zawiera (znacznie skromniejszy) zestaw komunikatów generowanych pod wpływem zdarzeń związanych z klawiaturą. Choćby z wizualnego porównaia wielkości tych tabel wyrażnie widać, że Windows znacznie bardziej "lubią" współpracę z myszką. Komunikaty Windows w odpowiedzi na zdarzenia związane z klawiaturą. _______________________________________________________________ Komunikat Zdarzenie _______________________________________________________________ WM_KEYDOWN Naciśnięto (jakiś) klawisz. WM_KEYUP Puszczono klawisz. WM_SYSKEYDOWN Naciśnięto klawisz "systemowy". WM_SYSKEYUP Puszczono klawisz "systemowy". WM_CHAR Kod ASCII klawisza. ________________________________________________________________ Klawisz systemowy to np. [Alt]+[Esc], [Alt]+[F4] itp. ________________________________________________________________ Komunikaty Windows możemy wykorzystać w programie. ... BOOL CanClose(); void WMLButtonDown(RTMessage Msg)= [WM_FIRST + WM_LBUTTONDOWN]; void WMRButtonDown(RTMessage Msg)= [WM_FIRST + WM_RBUTTONDOWN]; }; Nasze Główne_Okno potrafi obsługiwać następujące zdarzenia: * Funkcja CanClose() zwróciła wynik TRUE/FALSE, * Naciśnięto lewy/prawy klawisz myszki. Komunikat Msg zadeklarowany jako zmienna typu RTMessage jest w klasie macieżystej TWindow wykorzystywany tak: _CLASSDEF(TWindow) class _EXPORT TWindow : public TWindowsObject { ... protected: virtual LPSTR GetClassName() { return "OWLWindow"; } virtual void GetWindowClass(WNDCLASS _FAR & AWndClass); virtual void SetupWindow(); virtual void WMCreate(RTMessage Msg) = [WM_FIRST + WM_CREATE]; virtual void WMMDIActivate(RTMessage Msg) = [WM_FIRST + WM_MDIACTIVATE]; ... virtual void WMSize(RTMessage Msg) = [WM_FIRST + WM_SIZE]; virtual void WMMove(RTMessage Msg) = [WM_FIRST + WM_MOVE]; virtual void WMLButtonDown(RTMessage Msg) = [WM_FIRST + WM_LBUTTONDOWN]; Zwróć uwagę na notację. Zamiast WM_CREATE pojawiło się [WM_FIRST + WM_CREATE]. Komunikat WM_FIRST jest predefiniowany w OWLDEF.H i musi wystąpić w obiektowych aplikacjach w dowolnej klasie okienkowej, bądź sterującej (window class/controll class), która winna odpowiadać na określony komunikat. Oto fragment pliku OWLDEF.H zawierający definicje stałych tej grupy: #define WM_FIRST 0x0000 /* 0x0000- 0x7FFF window messages */ #define WM_INTERNAL 0x7F00 /* 0x7F00- 0x7FFF reserved for internal use */ #define ID_FIRST 0x8000 /* 0x8000- 0x8FFF child id messages */ #define NF_FIRST 0x9000 /* 0x9000- 0x9FFF notification messages */ #define CM_FIRST 0xA000 /* 0xA000- 0xFFFF command messages */ #define WM_RESERVED WM_INTERNAL - WM_FIRST #define ID_RESERVED ID_INTERNAL - ID_FIRST #define ID_FIRSTMDICHILD ID_RESERVED + 1 #define ID_MDICLIENT ID_RESERVED + 2 #define CM_RESERVED CM_INTERNAL - CM_FIRST W tym momencie zwróćmy jeszcze uwagę, że funkcje z grupy MessageHandlers są typu void i zwykle są metodami wirtualnymi - przeznaczonymi "z definicji" do nadpisywania przez programistów w klasach potomnych. Wszystkie te metody mają zawsze jedyny argument - referencję do struktury TMessage zdefiniowanej następująco: struct TMessage { HWND Receiver; //Identyfikator okna - odbiorcy WORD Message; //sam komunikat union { WORD WParam; //Parametr WParam stowarzyszony z //komunikatem; ALBO (dlatego unia!) struct tagWP { BYTE Lo; BYTE Hi; } WP; union { DWORD lParam; struct tagLP { WORD Lo; WORD Hi; } LP; }; long Result; }; Po tych wyjaśnieniach możemy zaimplementować poszczególne funkcje. void TAplikacja::InitMainWindow() { MainWindow = new (0, Name); } Jeśli wybrano klawisz [Yes] funkcja zwróci IDYES. Jeśli funkcja zwróciła IDYES - operator porównania zwróci TRUE (prawda) i ta też wartość zostanie zwrócona przez metodę CanClose: BOOL TMyWindow::CanClose() { return (MessageBox(HWindow, "Wychodzimy?", "Koniec", MB_YESNO | MB_ICONQUESTION) == IDYES); } Stosunkowo najciekawsza kombinacja odbywa się wewnątrz handlera komunikatu WM_LBUTTONDOWN. Ze struktury komunikatów pobierana jest zawartość młodszego słowa parametru lParam - Msg.LP.Lo i starszego słowa Msg.LP.Hi. Są to względne współrzędne graficzne kursora myszki (względem narożnika okna) w momencie naciśnięcia lewego klawisza myszki. Funkcja sprintf() zapisuje je w postaci dwu liczb dziesiętnych %d, %d do bufora znakowego char string[20]. Funkcja GetDC() (Get Device Context) określa kontekst urządzenia (warstwa sterownika urządzenia) i dalej obiekt może już stosując funkcję kontekstową "czuć się" niezależny od sprzętu. Dane te w postaci znakowej są pobierane przez funkcję kontekstową OutText() jako string a równocześnie pobierane są w formie liczbowej: Msg.LP.Hi. Msg.LP.Lo, aby wyznaczyć współrzędne tekstu na ekranie. Funkcja strlen() oblicza długość łańcucha znakowego - i to już ostatni potrzebny nam parametr. void TMyWindow::WMLButtonDown(RTMessage Msg) { HDC DC; char string[20]; sprintf(string, "(%d, %d)", Msg.LP.Lo, Msg.LP.Hi); <-- TU DC = GetDC(HWindow); TextOut(DC, Msg.LP.Lo, Msg.LP.Hi, string, strlen(string)); /* Można zwolnić kontekst */ ReleaseDC(HWindow, DC); } Ewentualna metoda unieważniająca prostokąt (invalid rectangle) i kasująca w ten sposób zawartość okna w odpowiedzi na WM_RBUTTONDOWN może zostać zaimplementowana np. tak: void TMyWindow::WMRButtonDown(RTMessage) { InvalidateRect(HWindow, 0, 1); } Główny program to już tylko wywołanie metody Run() wobec obiektu. int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { TNAplikacja OBIEKT("Wspolrzedne w oknie", hInstance, hPrevInstance, lpCmdLine, nCmdShow); OBIEKT.Run(); return (OBIEKT.Status); } Wyświetlanie współrzędnych jakkolwiek wartościowe z dydaktycznego punktu widzenia jest mało interesujące. Pokusimy się o obiektową aplikację umożliwiającą odręczne rysowanie w oknie (freehand drawing). [!!!]UWAGA ________________________________________________________________ Pakiety Borland C++ 3..4.5 zawierają wiele gotowych "klocków" do wykorzystania. Oto przykład wykorzystania w pliku zasobów .RC standardowego okienka wejściowego (Input Dialog Box) i standardowego okienka typu Plik (File Dialog Box): #include #include rcinclude INPUTDIA.DLG rcinclude FILEDIAL.DLG ROZKAZY MENU LOADONCALL MOVEABLE PURE DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New" CM_FILENEW MENUITEM "&Open" CM_FILEOPEN MENUITEM "&Save" CM_FILESAVE END END Takie menu można zastosować w programie obiektowym umieszcając je w konstruktorze i dokonując nadpisania metody AssignMenu() (przypisz menu): TGOkno::TGOkno(PTWindowsObject AParent, LPSTR ATitle) : TWindow(AParent, ATitle) { AssignMenu("ROZKAZY"); ... } [S] rcinclude - dołącz zasoby LOADONCALL - załaduj po wywołaniu owlrc - zasoby biblioteki klas OWL Gotowe "klocki" można wykorzystać nawet wtedy, gdy nie pasują w 100%. Inne niż typowe odpowiedzi na wybór rozkazu implementujemy w programie głównym poprzez nadpisanie wirtualnej metody virtual void CMFileOpen(RTMessage msg) = [CM_FIRST + CM_FILEOPEN] TGOkno GOkno; void TGOkno::CMFileOpen(RTMessage) { ... obsługa zdarzenia ... } ________________________________________________________________ [Z] ________________________________________________________________ 1. Przeanalizuj gotowe zasoby dołączone do Twojej wersji Borland C++. 2. Uruchom kilka projektów "firmowych" dołączonych w katalogu \EXAMPLES. Zwróć szczególną uwagę na projekty STEPS (kolejne kroki w tworzeniu aplikacji obiektowej). ________________________________________________________________