Aplikacja: typowe funkcje, które należy zaimplementować
Wstęp
W tej części opracowania omówimy kolejne etapy programowania aplikacji, tzn. podstawowe obiekty, które każda aplikacja musi zawierać. Skupimy się przede wszystkim na metodach, które należy przeciążyć. Zawartość tego rozdziału uzupełnia rozdział Korzystanie z widoków. Poniższy opis zawiera omówienie i przykłady, które odnoszą się do aplikacji korzystających zarówno z interfejsu SDI jak również MDI. Jako dodatek opisujemy tzw. idle processing czyli przetwarzanie danych w "wolnych chwilach" przez program.
CWinApp: Obiekt aplikacji
Obiekt aplikacji jest podstawowym elementem projektu. Bez niego nasza aplikacja nie będzie w ogóle funkcjonować. W projekcie musi występować dokładnie jeden obiekt klasy aplikacji, który zostanie aktywizowany przez niejawną funkcję WinMain. Fakt, że w projektach wykorzystujących MFC nie ma funkcji WinMain zazwyczaj jest bardzo deprymujący dla początkujących programistów przyzwyczajonych do pisania w czystym C++ lub C.
Samą klasę aplikacji wyprowadza się zawsze z bibliotecznej klasy CWinApp. Jeżeli przy tworzeniu projektu nie skorzystało się z Kreatora Aplikacji (AppWizard) należy to zrobić samodzielnie.
class CMyApp : public CWinApp { //{{AFX_MSG(CMyApp) //}}AFX_MSG //{{AFX_VIRTUAL(CMyApp) public: virtual BOOL InitInstance(); //}}AFX_VIRTUAL public: CMyApp() {} ~CMyApp() {} DECLARE_MESSAGE_MAP() };Trochę "dziwne" komentarze w powyższym przykładzie zawierają informacje potrzebne Kreatorowi Klas (ClassWizard) do zarządzania klasą i nie będą tutaj omawiane. Zazwyczaj są wstawiane automatycznie i nie trzeba sobie nimi zaprzątać uwagi - najlepiej pozostawić je tak jak są.
W klasach wyprowadzanych z klasy CWinApp w przeważającej liczbie przypadków wystarcza przeciążenie metody InitInstance. Jest jeszcze kilka istotnych metod, których przeciążenie może być użyteczne. W poniższej tabelce zamieszczono te najważniejsze:
InitInstace Windows umożliwia uruchomienie kilku kopii (instancji) aplikacji równocześnie. Inicjalizacja aplikacji jest podzielona na dwie części: jednorazową inicjalizację wykonywaną przy pierwszym uruchomieniu aplikacji oraz inicjalizację instancyjną dokonywaną przy każdorazowym uruchamianiu kolejnych kopii. Metoda InitInstance odpowiada za inicjalizację instancyjną. Jej zadaniem jest inicjalizacja aplikacji. Typowo jest to dodanie do aplikacji jednego (interfejs SDI) lub więcej (interfejs MDI) tzw. document templates, ewentualne przetworzenie argumentów podanych z linii poleceń. ExitInstance Stosunkowo rzadko przeciążana metoda, jest na tyle istotna, że warto ją tutaj wymienić. Wołana kiedy instancja aplikacji kończy działanie. Domyślna wersja tej metody zapisuje opcje aplikacji do pliku INI (lub rejestrów). OnIdle Jest to metoda wołana w "wolnym czasie" tj. kiedy kolejka przybywających do aplikacji wiadomości systemowych jest pusta (tzn. nie ma żadnych wiadomości do obsłużenia). Więcej informacji o tej metodzie czytelnik znajdzie w paragrafie Dodatek: Idle processing. OnDDECommand Metoda jest wywoływana kiedy do aplikacji przybędzie komunikat systemowy związany z DDE (ang. Dynamic Data Exchange). Domyślna wersja tej metody (z klasy bazowej) obsługuje polecenie otwierania dokumentów i powinna być wywołana na początku przeciążonej wersji metody w klasie pochodnej. InitInstance
Jest to jedyna metoda klasy CWinApp, którą zawsze trzeba przeciążać w klasach pochodnych dlatego omówimy ją szerzej. Zadaniem metody InitInstance jest zainicjalizowanie środowiska MFC (ang. framework) do pracy. Przeważnie sprowadza się to do skojarzenia razem elementów architektury dokument-widok, czyli widoków i dokumentów, oraz okien ramowych i szablonów zawartych w zasobach. Oto przykład kodu metody w wersjach SDI i MDI:
// wersja SDI BOOL CMyApp::InitInstance() { // załadowanie opcji z INI LoadStdProfileSettings(); // zarejestrowanie JEDNEGO dokumentu CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CMyDoc), RUNTIME_CLASS(CMainFrame), RUNTIME_CLASS(CMyView)); AddDocTemplate(pDocTemplate); // otwarcie okna przez stworzenie // nowego dokumentu OnFileNew(); // przetworzenie argumentów // z linii poleceń if (m_lpCmdLine[0] != '\0') { // TODO: add command line processing here } return TRUE; } //wersja MDI BOOL CMyApp::InitInstance() { // załadowanie opcji z INI LoadStdProfileSettings(); // otwarcie głównego okna ramowego CMainFrame *pWnd = new CMainFrame; pWnd->LoadFrame(IDR_MAINFRAME); pWnd->ShowWindow(m_nCmdShow); m_pMainWnd = pWnd; // zarejestrowanie dokumentu 1 CMultiDocTemplate *pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_MYDOC1, RUNTIME_CLASS(CMyDoc1), RUNTIME_CLASS(CMDIChildWnd), RUNTIME_CLASS(CMyView1)); AddDocTemplate(pDocTemplate); // zarejestrowanie dokumentu 2 pDocTemplate = new CMultiDocTemplate( IDR_MYDOC2, RUNTIME_CLASS(CMyDoc2), RUNTIME_CLASS(CMDIChildWnd), RUNTIME_CLASS(CMyView2)); AddDocTemplate(pDocTemplate); // nowy dokument OnFileNew(); return TRUE; }Jak widać z powyższych przykładów, procedura postępowania w przypadku interfejsów SDI i MDI jest zbliżona. Najistotniejsze różnice to:
Element SDI MDI Główne okno ramowe
(klasa CMainFrame)Wyprowadzone z CFrameWnd, otwierane razem z dokumentem przez OnFileNew. Wyprowadzona z CMDIFrameWnd, otwierane bezpośrednio przez LoadFrame. Document template,
(szablon dokumentu)Pojedynczy obiekt klasy CSingleDocTemplate. Jeden lub więcej obiektów klasy CMultiDocTemplate. Okno ramowe widoku Jest zarazem głównym oknem ramowym programu. Użyta bezpośrednio klasa CMDIChildWnd lub klasa pochodna.
CFrameWnd: Główne okno ramowe
Główne okno ramowe jest głównym oknem aplikacji. W interfejsie SDI jest ono zarazem oknem ramowym widoku. W interfejsie MDI jest ono kontenerem całej rzeszy potomnych okien ramowych, z których każde zawiera swoje widoki. Mówiąc obrazowo: główne okno ramowe reprezentuje przestrzeń roboczą (ang. workspace) aplikacji.
Okna ramowe są realizowane przez dwie klasy: podstawową CFrameWnd i pochodną od niej CMDIFrameWnd. Ta pierwsza jest wykorzystywana w aplikacjach korzystających z interfejsu SDI, druga w korzystających z interfejsu MDI. W obydwu przypadkach jest możliwe bezpośrednie, tzn. bez konieczności wyprowadzania klas pochodnych, użycie klasy. W praktyce przeważnie wyprowadza się nowe klasy aby dodać dodatkowe elementy, np. pasek statusowy (ang. status bar) czy paski narzędzi (ang. toolbars), do okna ramowego. Do wyprowadzania nowych klas okien ramowych jest najpraktyczniej skorzystać z pomocy Kreatora Klas (ClassWizard). W przypadku okna ramowego przeciąża się najczęściej dwie metody:
OnCreate Metodę tę posiadają wszystkie klasy pochodne od CWnd (czyli wszystkie klasy reprezentujące okna). Metoda jest handlerem wiadomości systemowej WM_CREATE, która jest przysyłana do okna bezpośrednio po jego utworzeniu ale zanim okno stanie się widoczne. Zadaniem OnCreate jest dodanie do okna dodatkowych elementów, najczęściej są to rozmaite kontrolki, paski statusowe i paski narzędziowe. OnClientCreate Metoda ta jest wywoływana przez metodę OnCreate podczas tworzenia obszaru klienta okna. Domyślna wersja tej metody tworzy obiekt widoku na podstawie informacji otrzymanych w argumencie. Metodę OnClientCreate przeciąża się przeważnie kiedy zachodzi potrzeba podzielenia obszaru roboczego okna ramowego na panele, w których np. można umieścić różne widoki w ten sposób prezentując dane dokumentu równocześnie na kilka sposobów. Oto przykład kodu metody OnCreate, w którym do okna ramowego dodawany jest pasek statusowy oraz dokowalny pasek narzędziowy. Zakładamy że w definicji klasy (w pliku nagłówkowym) znajdują się składowe m_wndStatusBar i m_wndToolBar odpowiednio typów CStatusBar i CToolBar:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { static const UINT indicators[2] = {ID_SEPARATOR, ID_STATUS}; if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; // dodanie paska statusowego if(!m_wndStatusBar.Create(this)) { TRACE0("Nie udało się stworzyć Status Bara!\n"); return -1; } if(!m_wndStatusBar.SetIndicators(indicators, 2)) { TRACE0("Nie udało się zainicjalizować paska statusowego!\n"); return -1; } m_wndStatusBar.SetPaneInfo(1, ID_STATUS, SBPS_NORMAL, 100); if (!m_wndToolBar.Create(this) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Nie udało się stworzyć paska narzędzi!\n"); return -1; // fail to create } // dodanie paska narzędziowego m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC); m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); return 0; }A teraz znacznie ciekawszy przykład kodu przeciążonej metody OnCreateClient. Panele są tworzone w oparciu o klasę CSplitterWnd, dlatego w definicji klasy okna ramowego (w MDI nie musi to być główne okno ramowe) musi się znaleźć składowa klasy CSplitterWnd. W poniższym przykładzie składowa ta ma nazwę m_wndSplitter.
BOOL CMyFrameWnd::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) { // stworzenie statycznego dwupanelowego splittera if(!m_wndSplitter.CreateStatic(this, 1, 2)) { TRACE0("Nie udało się stworzyć splittera.\n"); return FALSE; } // standardowy widok z template dokumentu w lewym panelu // panel ma mieć szerokość 3/4 szerokości okna ramowego if(!m_wndSplitter.CreateView(0, 0, pContext->m_pNewViewClass, CSize(lpcs->cx*3/4, lpcs->cy), pContext)) { TRACE0("Nie udało się stworzyć widoku w pierwszym panelu.\n"); return FALSE; } // dodatkowy widok w prawym panelu (1/4 szerokości okna ramowego) if(!m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CSecondView), CSize(lpcs->cx/4, lpcs->cy), pContext)) { TRACE0("Nie udało się stworzyć widoku w drugim panelu.\n"); return FALSE; } // dodanie dodatkowego widoku z drugiego panelu do dokumentu pContext->m_pCurrentDoc->AddView(m_wndSplitter.GetPane(0, 1)); // pomyślne zakończenie całej tej karkołomnej operacji return TRUE; }Powyższe przykłady przeciążonych metod są aplikowalne zarówno dla klas okien ramowych wyprowadzanych z klas CFrameWnd i CMDIFrameWnd ale również dla tych, które wyprowadzono z klasy CMDIChildWnd. Warto również wiedzieć, że większość z pokazanych tu elementów można dodawać z poziomu Galerii Komponentów (Component Gallery) czyli specjalizowanego kreatora, który upraszcza dodawanie standardowych komponentów okien takich jak paski statusowe czy paski narzędzi. Jedyne co można zarzucić Galerii Komponentów to generowanie czasem trochę nadmiarowego kodu.
CDocument: Dokumenty
W dualnej strukturze aplikacji dokumenty reprezentują tzw. model czyli część aplikacji związaną z przechowywaniem i przetwarzaniem informacji (więcej informacji na ten temat czytelnik znajdzie w sekcji Architektura dokument-widok). Klasa CDocument jest bazą dla wszystkich klas dokumentów wyprowadzanych przez programistę i posiada zaimplementowane takie operacje jak:
- tworzenie nowego dokumentu
- odczyt dokumentu z pamięci masowej (tzw. deserializacja)
- zapis dokumentu do pamięci masowej (tzw. serializacja)
Aplikacja może wykorzystywać więcej niż jeden dokument; np. aplikacja może operować na arkuszach kalkulacyjnych oraz tekstach. Każdy typ dokumentu jest skojarzony z szablonem, który specyfikuje zasoby (takie jak: menu, ikona czy tablica akceleratorów) wykorzystywane przez dokument danego typu.
W praktycznych zastosowaniach przeciąża się głównie opisane poniżej metody, chociaż oczywiście zbiór przeciążalnych metod klasy CDocument zawiera znacznie więcej metod.
DeleteContents Metoda ta jest wywoływana przez środowisko MFC (ang. framework) w celu skasowania danych dokumentu bez konieczności usuwania samego obiektu dokumentu. Ma to szczególne znaczenie dla aplikacji SDI, które wielokrotnie wykorzystują obiekt dokumentu (tzw. reusing). Metoda może mieć też zastosowanie w hadlerze opcji Kasuj wszystko (ang. Clear all) o standardowym identyfikatorze ID_EDIT_CLEAR_ALL z menu Edycja (ang. Edit). OnNewDocument Metoda jest handlerem obsługującym opcję Nowy (ang. New) o standardowym identyfikatorze ID_FILE_NEW z menu Plik (ang. File). Jej zadaniem jest dokonywanie inicjalizacji dokumentu (np. w dokumentach tekstowych: wybranie pewnego domyślnego formatowania). Domyślna wersja tej metody wywołuje metodę DeleteContents. Serialize Metoda odpowiedzialna za zapis (serializację) i odczyt (deserializację) dokumentu. Jest wywoływana przez standardowe wersje metod OnOpenDocument i OnSaveDocument. Zagadnienie serializacji dokładnie omawia rozdział Zapis i odczyt danych.
CView: Widoki
Widoki są obiektami odpowiedzialnymi za prezentację danych zawartych w dokumencie. Ponieważ zagadnieniu pracy z widokami jest niezwykle rozległe jest ono omówione w osobnym rozdziale: Korzystanie z widoków. Zaś typowe metody, które należy lub warto zaimplementować (przeciążyć) są opisane w podrozdziale Zazwyczaj przeciążane metody.
Dodatek: Idle processing
Pod pojęciem Idle processing należy rozumieć wykonywanie kodu w chwilach bezczynności aplikacji tj. wtedy gdy kolejka komunikatów systemowych, oczekujących na obsłużenie, jest pusta. MFC implementuje idle processing za pomocą metody CWinApp::OnIdle o następującej składni:
virtual BOOL OnIdle( LONG lCount );Metoda powinna zwrócić wartość zerową (FALSE) jeżeli zdążyła wykonać wszelkie zaplanowane operacje i nie potrzebuje więcej czasu na ich dokończenie. Jeżeli zwrócona zostanie wartość niezerowa (TRUE) to oznacza to, że metoda nie zdążyła wykonać zaplanowanych operacji i na ich dokończenie będzie potrzebować jeszcze trochę czasu procesora.
Otrzymywany argument przyjmuje bieżącą wartość licznika wywołań OnIdle. Licznik ten jest inkrementowany za każdym wywołaniem OnIdle i może posłużyć do określenia długości czasu bezczynności aplikacji. Znacznie częściej wykorzystuje się go do spriorytetyzowania zadań, które mają być zrealizowane w czasie bezczynności aplikacji. Licznik jest zerowany każdorazowo kiedy pojawi się wiadomość systemowa do obsłużenia przez aplikację.
Poniżej przedstawiono ogólny schemat działania idle processing w MFC:
- Jeżeli w pętli przyjmującej i przetwarzającej wiadomości systemowe MFC stwierdzi, że w kolejce nie ma żadnych wiadomości do obsłużenia, zostanie wywołana metoda OnIdle z zerową wartością licznika wywołań.
- Metoda OnIdle wykonuje zaplanowane operacje i zwraca niezerową wartość sygnalizującą, że OnIdle powinna być wywołana ponownie w celu dokończenia niedokończonych operacji względnie wykonania kolejnych.
- W pętli komunikatów następuje sprawdzenie kolejki zadań. Jeżeli kolejka nadal jest pusta ponownie jest wywoływana metoda OnIdle z zawartością zinkrementowanego licznika wywołań jako argumentem.
- W przypadku gdy metoda OnIdle zakończy wszystkie zaplanowane operacje zwraca wartość zerową. Spowoduje to, że metoda nie zostanie wywołana do czasu obsłużenia kolejnych wiadomości systemowych i opróżnienia kolejki.
Uwaga 1: Ze względu na to, że dopóki metoda OnIdle nie zakończy działania nie mogą być obsługiwane wiadomości systemowe, które nadeszły w trakcie jej wykonywania, zadania wykonywane przez OnIdle nie mogą być czasochłonne. Zlekceważenie tego warunku może spowodować, że aplikacja będzie reagować na akcje użytkownika ospale lub wcale!
Uwaga 2: Standardowa wersja metody OnIdle wykonuje bardzo ważne, dla środowiska MFC, (framework) zadania takie jak: aktualizacja elementów interfejsu użytkownika (menu, pasków narzędzi, pasków statusowych etc.) oraz porządkowanie wewnętrznych struktur danych (np. tablic asocjacyjnych). Z tego względu należy zawsze wywoływać bazową wersję metody, i dopiero kiedy zwróci ona wartość zerową można rozpocząć własne operacje.
W celu lepszego zrozumienia opisanego problemu proponujemy analizę dwóch poniższych przykładów. Pierwszy przykład pokazuje jak wykorzystać argument lCount w celu spriorytetyzowania zadań. Pierwsze zadanie ma wysoki priorytet i powinno być wykonywane kiedy tylko jest to możliwe. Drugie zadanie jest mniej ważne i powinno być wykonywane przy dłuższym braku aktywności ze strony użytkownika aplikacji. Proszę zwrócić uwagę na wywołanie wersji metody OnIdle z klasy bazowej.
BOOL CMyApp::OnIdle(LONG lCount) { BOOL bMore = CWinApp::OnIdle(lCount); if (lCount == 0) { TRACE("Aplikacja bezczynna przez krótki okres czasu.\n"); bMore = TRUE; } else if (lCount == 10) { TRACE("Aplikacja bezczynna przez dłuższy okres czasu.\n"); bMore = TRUE; } else if (lCount == 100) { TRACE("Aplikacja bezczynna przez dosyć długi okres czasu.\n"); bMore = TRUE; } else if (lCount == 1000) { TRACE("Aplikacja bezczynna przez długi okres czasu.\n"); // bMore nie jest tym razem ustawiane na TRUE gdyż // nie potrzeba już więcej czasu na dokończenie zadań // UWAGA!!! bMore nie jest nigdzie ustawiane na FALSE // ze względu na to, że CWinApp::OnIdle może potrzebować // więcej czasu na dokończenie swoich zadań! } // zwracamy TRUE tak długo dopóki są jeszcze // jakieś zadania do wykonania return bMore; }Drugi przykład pokazuje jak inaczej można zarządzać zadaniami o różnych priorytetach, a także jak powinno się odwoływać do bazowej wersji OnIdle (proszę porównać z pierwszym przykładem!).
// W tym przykładzie są cztery zadania do wykonania w czasie // bezczynności: // Zadanie1 zawsze ma szansę być wykonane gdy tylko kolejka zadań // do obsłużenia się opróżni a środowisko (framework) wykona już // swoje zadania (dla lCount równego 0 i 1). // Zadanie2 może być wykonane dopiero po wykonaniu Zadania1 i nie // przybyła w tym czasie żadna wiadomość systemowa do obsłużenia. // Zadanie3 i Zadanie4 mogą być wykonane dopiero po wykonaniu Zadania1 // i Zadania2 oraz gdy nadal nie przybyła żadna wiadomość systemowa. // Jeżeli tylko Zadanie3 otrzyma szansę wykonania to Zadanie4 zostanieBOOL CMyApp::OnIdle(LONG lCount) { // W tym przykładzie pozwolimy wpierw aby CWinApp::OnIdle wykonała // swoje zadania, dopiero potem możemy spróbować wykonać nasze zadania. // W ten sposób powinno się postępować w większości przypadków. if (CWinApp::OnIdle(lCount)) return TRUE; // Bazowa wersja OnIdle (czyli CWinApp::OnIdle) ma zarezerwowane // wartości 0 i 1 argumentu lCount do wykonywania swoich operacji. // Jeżeli chcielibyśmy wykonywać nasze zadania na tym samym poziomie // musielibyśmy zastąpić dyrektywę "if" bezpośrednim odwołaniem do // CWinApp::OnIdle; po czym moglibyśmy dodać przypadki 0 i 1 do // polecenia "switch". // Przestudiowanie implementacji klasy bazowej na pewno pomoże // w zrozumieniu i poznaniu w jaki sposób przebiega idle processing // środowisku MFC (framework). switch (lCount) { case 2: Zadanie1(); return TRUE; // dajemy szansę na uruchomienie następnym // razem Zadaniu2 case 3: Zadanie2(); return TRUE; // dajemy szansę na uruchomienie następnym // razem Zadaniu3 i Zadaniu4 case 4: Zadanie3(); Zadanie4(); return FALSE;// wszystkie zadania wykonane - nie potrzeba // nam więcej czasu } return FALSE; }