Korzystanie z widoków


Wstęp

Niniejszy dokument opisuje zagadnienia związane z praktycznym wykorzystaniem widoków. Jego uzupełnieniem jest dokument Omówienie dostępnych klas widoków zawierający opis poszczególnych bibliotecznych klas widoków.

 

Dziedziczenie

Choć nic nie stoi na przeszkodzie aby bezpośrednio (tzn. bez wcześniejszego wyprowadzania nowych, pochodnych klas widoków) używać dostarczonych klas widoków to nie jest to zbyt praktyczne, gdyż autorzy MFC siłą rzeczy nie mogli uwzględnić wszystkich interakcji użytkownika z widokiem, tym bardziej że są one specyficzne dla każdej aplikacji. Dlatego w praktyce zawsze definiuje się własną klasę widoku wyprowadzając ją z jednej z klas bibliotecznych. Dzięki temu można zdefiniować własną mapę komunikatów i dostarczyć zbiór metod obsługujących wiadomości systemowych czyli tzw. handlerów, a także, przeciążając wirtualne metody klasy bazowej, dostosować zachowanie widoku do potrzeb aplikacji.

Generalnie w przypadku tworzenia nowych klas na bazie dostarczanych przez MFC wygodnie i praktycznie jest wykorzystać wbudowanego w Developer Studio (VC++ 4.x i 5.x) lub Visual Studio (VC++ 6.0) Class Wizarda, który zadba o wszystkie niezbędne elementy klasy potrzebne do jej wpasowania w środowisko biblioteki (ang. framework). Czasem jednak zachodzi potrzeba "ręcznego" dopasowania kodu do potrzeb (choćby dlatego, że nie wszystkie możliwości MFC można wydobyć za pomocą Class Wizarda), stąd warto wiedzieć co tak naprawdę robi Class Wizard.

Ponieważ widoki są częścią architektury dokument-widok należy zadbać aby nowa klasa widoku zawierała mechanizmy integrujące klasy widoków i dokumentów w MFC.  Aby spełnić ten warunek należy: dziedziczyć publicznie z klasy bazowej oraz zapewnić dynamiczne tworzenie obiektów nowej klasy widoku w trakcie wykonywania programu (ang. dynamic creation at run-time). Aby zaimplementować w klasie dynamiczne tworzenie należy wstawić makro DECLARE_DYNCREATE do definicji klasy (zazwyczaj w pliku nagłówkowym) jak na poniższym przykładzie:

class CMyView : public CScrollView

{

    DECLARE_DYNCREATE(CMyView)

    ...

};

Natomiast w pliku z implementacją klasy należy umieścić komplementarne makro IMPLEMENT_DYNCREATE, np.:

IMPLEMENT_DYNCREATE(CMyView, CScrollView)

Dzięki umieszczeniu powyższych makr możliwe będzie operowanie obiektami widoków za pośrednictwem klasy CRuntimeClass. Typowe jej zastosowanie to określenie czy obiekt należy do danej klasy lub klasy potomnej od podanej (metoda CRuntimeClass::IsKindOf) oraz, a może raczej przede wszystkim, dynamiczne tworzenie obiektów (za pomocą metody CRuntimeClass::CreateObject).

 

Zazwyczaj przeciążane metody

Bez wątpienia najważniejszym atutem dziedziczenia w programowaniu obiektowym jest możliwość przeciążania (ang. overloading) odziedziczonych metod. Zakładamy że czytelnik zna dobrze język C++ wobec czego nie będziemy się tutaj rozwodzić nad koncepcją przeciążania i metodami wirtualnymi. Skupimy się za to na najważniejszych metodach, które zazwyczaj należy przeciążyć przy implementacji nowej klasy widoku.

OnDraw Metoda OnDraw jest wywoływana przez metodę OnPaint w odpowiedzi na wiadomość systemową WM_PAINT. Zadaniem OnDraw jest odświeżanie zawartości widoku czyli de facto wizualizacja dokumentu. W przypadku gdy klasą bazową jest klasa abstrakcyjna, np. CView lub CScrollView, przeciążenie tej metody jest konieczne, chyba że nowa klasa widoku nie jest przeznaczona do bezpośredniego użycia a jedynie ma być klasą bazową. Z kolei w przypadku widoków bazujących na kontrolkach, pochodnych od klasy CCtrlView takich jak CEditView, CRichEditView, CListView, CTreeView oraz widoków pochodnych od CFormView przeciążanie OnDraw nie jest konieczne a czasami wręcz bezcelowe gdyż np. we wszystkich potomnych klasach klasy CCtrlView metoda OnDraw nigdy nie jest wywoływana (sic!). Oto przykład metody OnDraw, której działanie polega na zamalowywaniu losowo dobranym kolorem obszaru całego wymagającego przerysowania:
void CMyView::OnDraw(CDC *pDC)

{

    CRect clipRect;

    pDC->GetClipBox(&clipRect);

    pDC->FillSolidRect(&clipRect, RGB(rand()%256, rand()%256,

                       rand()%256));

}
OnPrepareDC Przeciążanie tej metody będzie prawie zawsze konieczne jeżeli nowa klasa jest wyprowadzana z klasy CView. Metoda OnPrepareDC jest wywoływana w metodzie OnPaint przed wywołaniem metody OnDraw, a także przed wywołanie metody OnPrint dla każdej strony, która ma zostać wydrukowana. Jej zadaniem jest ustawienie właściwych atrybutów kontekstu urządzenia (ang. device context) przed rozpoczęciem pracy z kontekstem. Przykładowo klasa CScrollView na podstawie położenia suwaków ustawia współrzędne tzw. viewportu czyli aktualnie widocznego obszaru wirtualnego okna (wymiary okna wirtualnego są większe niż wymiary widoku - patrz też: Konwersja współrzędnych). Należy pamiętać aby zaraz na początku przeciążonej metody wywołać jej wersję z klasy bazowej.
OnInitialUpdate Metoda ta jest wywoływana zaraz po podłączeniu widoku do dokumentu jeszcze zanim widok stanie się widoczny na ekranie. Jej zadaniem jest jednorazowa, wstępna inicjalizacja widoku. Wewnątrz metody OnInitialUpdate nie powinno się tworzyć i dodawać do widoku kontrolek, do tego celu należy przeciążyć metodę OnCreate. Przykładowo w klasach potomnych od CScrollView, jeżeli dokument ma stałe wymiary, można wywołując metodę SetScrollSizes ustalić wymiary okna wirtualnego, tryb mapowania współrzędnych oraz wymiary logicznych stron i wierszy dokumentu. Innym przykładem niech będzie klasa pochodna od CTreeView: metoda OnInitialUpdate może wstępnie wypełnić drzewko elementami.
OnUpdate Metoda ta jest wywoływana po każdej zmianie dokumentu przez metodę CDocument::UpdateAllViews. Domyślna wersja OnUpdate deleguje cały obszar widoku do przerysowania (dokonuje tzw. dewalidacji). Ponieważ w przekazanych do metody argumentach można umieścić mniej lub bardziej szczegółowe informacje o zmianach w dokumencie, przeciążenie OnUpdate pozwala zoptymalizować dewalidację, poprzez minimalizację obszaru wymagającego uaktualnienia (przerysowania). Efektem optymalizacji jest przyspieszenie operacji graficznych gdyż nie trzeba każdorazowo przerysowywać całego obszaru widoku. Więcej szczegółów o aktualizacji widoków znajdziesz w paragrafie Synchronizacja widoków. Oto dosyć typowy przykład szkieletu kodu metody OnUpdate:
void CMyView::OnUpdate(CView *pSender, LPARAM lHint, CObject *pHint)

{

    switch(lHint)

    {

        case HINT_UPDATE_OBJECT_A:    // zdefiniowane przez programistę

            DoSomething((CMyObjectA*) pHint);

            break;

        case HINT_UPDATE_OBJECT_B:    // zdefiniowane przez programistę

            DoSomething((CMyObjectB*) pHint);

            break;

        default:

            Invalidate();

     }

}

Powyższa lista siłą rzeczy nie uwzględnia wszystkich możliwych do przeciążenia metod klasy CView i jej pochodnych. Wymieniliśmy jedynie te najważniejsze, które pojawiają się praktycznie w każdej aplikacji.

 

Konwersja współrzędnych

W metodach widoku bardzo często zachodzi konieczność narysowania czegoś w widoku lub określenia na podstawie otrzymanych współrzędnych myszy, w który fragment dokumentu kliknięto (wciąż pamiętamy tym, że klikamy jedynie w graficzną reprezentację dokumentu na ekranie zaś dopiero odpowiedni fragment kodu odnajduje skojarzone z tą reprezentacją dane). Praktycznie zawsze, niezależnie od tego którą klasę widoku użyto jako bazową, zachodzi konieczność konwersji logicznych współrzędnych dokumentu na współrzędne urządzenia (karty graficznej lub drukarki) i na odwrót. Są dwie przyczyny, z których powodu konwersja jest konieczna. Po pierwsze: fizyczne wymiary widoku są ograniczone wymiarami macierzystego okna ramowego, które z kolei jest ograniczone wymiarami ekranu, zaś dokument może być wizualizowany po kawałku a wtedy widok niejako "przesuwa się" nad dokumentem co pokazuje poniższy rysunek.

doc-view.gif (2982 bytes)

Po drugie: współrzędne elementów dokumentu niekoniecznie muszą być wyrażone w pikselach ale na przykład w milimetrach lub punktach drukarskich. W takiej sytuacji należy zadbać aby wymiary elementów pozostały zachowane na ekranie (lub na wydruku).
W systemie Windows wszelkie operacje graficzne, a więc i konwersja, odbywają się poprzez (a może raczej "na") konteksty urządzeń. W MFC konteksty są enkapsulowane przez klasę CDC i pochodne. Do zamiany współrzędnych urządzenia (ang. Device Points) na współrzędne logiczne dokumentu (ang. Logical Points) służy metoda CDC::DPtoLP zaś konwersji w drugą stronę dokonuje metoda CDC::LPtoDP:

// zamiana współrzędnych zawartych w strukturze RECT lub klasie CRect

pDC->DPtoLP(pRect);

// zamiana współrzędnych zawartych w strukture POINT lub klasie CPoint

pDC->LPtoDP(pPoint);

Dla zapewnienia poprawności konwersji należy wcześniej ustawić atrybuty kontekstu takie jak tryb mapowania oraz offset i wymiary tzw. viewportu. W przypadku widoków kontekst jest przygotowywany do użycia przez wywołanie metody OnPrepareDC:

CClientDC dc(this);

OnPrepareDC(&dc);

W przypadku metody OnDraw wołanie OnPrepareDC jest zbyteczne gdyż jest ona wywoływana w OnPaint przed wywołaniem OnDraw, która otrzymuje wskaźnik do już przygotowanego kontekstu urządzenia.
A oto przykład konwersji współrzędnych zastosowany w metodzie obsługującej wciśnięcie lewego przycisku myszy nad obszarem widoku:

void CMyView::OnLButtonDown(UINT nFlags, CPoint point)

{

    CPoint logical = point;

    CClientDC dc(this);

    OnPrepareDC(&dc);

    dc.DPtoLP(&logical); // oryginalne współrzędne jeszcze będą potrzebne!



    // ...



    CView::OnLButtonDown(nFlags, point); // wywołanie wersji z klasy bazowej

}

Ponieważ konteksty urządzeń są częścią API Windows a MFC jedynie enkapsuluje funkcje operujące na kontekstach to po szczegółowy opis odsyłamy czytelnika do specjalistycznych publikacji oraz pomocy on-line Developer Studio (Visual Studio).

 


Strona główna