Omówienie dostępnych klas widoków
Wstęp
Dokument opisuje poszczególne biblioteczne klasy widoków, z których programista może wyprowadzać własne klasy widoków. Przed rozpoczęciem lektury dokumentu zalecane jest zapoznanie się z zawartością dokumentu Korzystanie z widoków.
CView
Jest to abstrakcyjna klasa ogólnego przeznaczenia. Nie ma zaimplementowanych żadnych specyficznych właściwości ułatwiających pracę programiście. Dziedziczenie nowych widoków z tej klasy jest mało praktyczne. Jeżeli aplikacja wymaga zaimplementowania całkowicie specyficznego widoku, nie będącego rozszerzeniem istniejących typów widoków znacznie wygodniej jest skorzystać z klasy CScrollView. Ze względu, że jest to klasa abstrakcyjna może być tylko stosowana jako bazowa dla nowych klas widoków.
CScrollView
Bardzo praktyczna abstrakcyjna klasa dająca nieograniczone możliwości prezentacji danych. Pozwala na pracę z tzw. wirtualnym oknem czyli obszarem nie ograniczonym wymiarami widoku. Wymiary obszaru roboczego (okna wirtualnego) można ustawiać za pomocą metody SetScrollSizes. Typowo dokonuje się tego z wnętrza metod OnInitalUpdate oraz OnUpdate. Wbudowane w klasę handlery obsługują scrolling a programiście pozostaje tylko zapewnić właściwe przerysowywanie "odsłoniętych" fragmentów obszaru roboczego. Podobnie jak klasa CView również może być używana tylko jako klasa bazowa.
CEditView
Jest widok bazujący na kontrolce typu EDIT posiadający funkcjonalność bardzo prostego edytora tekstu. Posiada zaimplementowane handlery obsługujące funkcje wycinania (ang. cut), kopiowania (ang. copy) i wklejania (ang. paste) tekstów przez Schowek, a także obsługę wydruku oraz wyszukiwanie i podmienianie fragmentów tekstu. Możliwości formatowania tekstu są ograniczone do wyrównywania jego całości do lewego lub prawego marginesu, centrowania oraz do jednego dla całego dokumentu kroju czcionki. Wadą tego widoku jest to, że nie implementuje on prawdziwego WYSIWYG (ang. what you see is what you get - to co widać na ekranie tak samo będzie wyglądać na wydruku) oraz ograniczona długość tekstu, który można edytować do 64KB pod Windows 95/98 (pod Windows NT długość tekstu jest "ograniczona" do 4GB).
Z klasy CEditView można korzystać bezpośrednio bez konieczności wyprowadzania z niej klas pochodnych, co z resztą zazwyczaj nie jest konieczne. Ponieważ w klasie CEditView metoda OnDraw nigdy nie jest wywoływana ingerencja w sposób wyświetlania tekstu, choć możliwa, nie jest wskazana gdyż wymaga przechwycenia komunikatu WM_PAINT i przeciążenia metody OnPaint. Jeżeli już koniecznie trzeba wzbogacić standardową procedurę rysującą o dodatkowe elementy można to zrobić w następujący sposób (choć nie jest to zbyt elegancki sposób to bodajże jedyny całkowicie "legalny"):
void CMyEditView::OnPaint() { // najpierw standardowa obsługa WM_PAINT Default(); // teraz możemy malować co dusza zapragnie CClientDC dc(this); CRect rect; GetClientRect(&rect); CPen pen(PS_SOLID, 4, RGB(0xFF, 0x00, 0x00)); int iOldROP2 = dc.SetROP2(R2_NOTXORPEN); CPen *pOld = dc.SelectObject(&pen); dc.MoveTo(rect.TopLeft()); dc.LineTo(rect.BottomRight()); dc.SelectObject(pOld); dc.SetROP2(iOldROP2); }Uważny czytelnik może w tej chwili zapytać dlaczego w powyższym przykładzie wołana jest metoda Default zamiast CEditView::OnPaint? I dlaczego korzystamy z obiektu kontekstu klasy CClientDC zamiast CPaintDC?
Odpowiedź na pierwsze pytanie jest prosta: ponieważ metoda CEditView::OnPaint nic nie robi - cała obsługa ekranu w kontrolce EDIT jest "zaszyta" w systemie Windows. Metoda Default odwołuje się do standardowej procedury obsługi okna typu EDIT nie będącej przecież częścią MFC. Cała procedura jest dosyć skomplikowana i jest jednym z aspektów subklasowania (ang. subclassing) systemowych procedur obsługi okien przez MFC.
Odpowiedź na drugie pytanie jest nieco bardziej złożona: wywołana za pośrednictwem Default procedura obsługi WM_PAINT odwołuje się do funkcji BeginPaint i EndPaint i podobnie też czyni klasa CPaintDC (odpowiednio w konstruktorze i destruktorze). Między odwołaniami do tych funkcji można operować (rysować) na niezwalidowanym obszarze do przerysowania. Po wywołaniu EndPaint nie ma już czego odświeżać gdyż cały obszar widoku jest już zwalidowany. Tak więc konstruowanie obiektu klasy CPaintDC po odwołaniu do Default nie ma sensu. Z kolei gdybyśmy utworzyli ten obiekt przed odwołaniem do Default uniemożliwilibyśmy poprawne działanie standardowej procedury okna.
CRichEditView
Jest to kolejna klasa działająca w oparciu o systemową kontrolkę. Tym razem jest to kontrolka typu RICHEDIT będąca znacznie bardziej rozbudowaną wersją kontrolki EDIT. Pozwala na dowolne formatowanie tekstu w tym używanie różnych czcionek a także wstawianie obiektów OLE do tekstu (dokumentu). Dołączana do Windows aplikacja WordPad została napisana z wykorzystaniem obiektów klasy CRichEditView. W architekturze dokument-widok klasa ta powinna współpracować z dokumentami dedykowanej klasy CRichEditDoc! Klasa również może być wykorzystywana bezpośrednio bez konieczności wyprowadzania klasy potomnej. Ze względu na bogate możliwości formatowania tekstu nie powinno być konieczności uciekania się do tricków z przerysowywaniem podobnych do przedstawionego w omówieniu klasy CEditView. Teksty są przechowywane w powszechnie akceptowanym przez procedury tekstu formacie RTF (ang. Rich Text Format).
CListView
Jest to klasa widoku bazująca na kontrolce typu list view służącej do wyświetlania kolekcji elementów, z których każdy posiada swoją ikonę oraz etykietę. Określenie "lista" w nazwie klasy bynajmniej nie oznacza, że elementy są ułożone jak w klasycznej liście jeden pod drugim. Kontrolka, a zatem i widok, typu list view potrafi wyświetlać elementy na cztery różne sposoby (w nawiasach podano nazwę stylu, którego ustawienie powoduje aktywację danego sposobu aranżacji):
Icon view
(LVS_ICON)Każdy element jest przedstawiany jako pełnowymiarowa ikona (32 x 32 piksele) z tekstową etykietą poniżej. Użytkownik może przeciągać (ang. drag) elementy do dowolnego miejsca w obrębie widoku. Small icon view
(LVS_SMALLICON)Każdy element jest przedstawiany jako mała ikonka (16 x 16 pikseli) z tekstową etykietą po prawej stronie. Użytkownik również może przeciągać elementy. List view
(LVS_LIST)Każdy element jest reprezentowany przez małą ikonkę z tekstową etykietą po prawej stronie. Elementy są poukładane w kolumny i nie mogą być przeciągane. Report view
(LVS_REPORT)Każdy element jest wyświetlany w osobnym wierszu zawierającym także dodatkowe informacje pogrupowane w kolumnach. Najbardziej skrajna na lewo, pierwsza kolumna zawiera małą ikonkę z etykietą. Kolejne kolumny zawierają subelementy wyspecyfikowane przez aplikację. Wykorzystywany przez widok nagłówek tabeli (klasa CHeaderCtrl) implementuje te kolumny. Aplikowanie styli aranżacji
Sposób rozmieszczania (aranżacji) elementów można zmieniać w dowolnym momencie poprzez zmianę stylu. Jeżeli założymy, że mamy klasę CMyView wyprowadzoną z klasy CListView to metoda realizująca zmianę stylu może mieć następującą postać:
// SetView - ustawia styl aranżacji widoku // dwView - nowy styl void CMyView::SetView(DWORD dwView) { // odczytanie aktualnego stylu DWORD dwStyle = GetWindowLong(GWL_STYLE); // dokonujemy zmiany jeżeli nowy styl // jest inny niż aktualny if ((dwStyle & LVS_TYPEMASK) != dwView) SetWindowLong(GWL_STYLE, (dwStyle & ~LVS_TYPEMASK) | dwView); }Jeżeli nie planujemy zmiany stylu w trakcie działania aplikacji możemy znacznie prościej ustawić styl widoku w konstruktorze, np.:
CMyView::CMyView() { m_dwDefaultStyle & ~LVS_TYPEMASK; m_dwDefaultStyle |= LVS_REPORT; }Stosowanie ikonek
Jeżeli zastosujemy aranżację typu Icon view lub Small icon view to konieczne jest zbudowanie i przekazanie do widoku (kontrolki) list view listy ikon, które mogą być przyporządkowywane elementom. W przypadku aranżacji List view oraz Report view tworzenie listy ikon nie jest konieczne gdyż w tych trybach widok (kontrolka) może operować jedynie na samych etykietach. Niemniej jednak i tutaj warto zdefiniować listę ikon dzięki czemu aplikacja zyska na estetyce i profesjonaliźmie wykonania. Listę ikon enkapsuluje klasa CImageList. Oto najprostszy sposób stworzenia i zainicjalizowania takiej listy małymi ikonami załadowanymi z zasobów:
// stworzenie obiektu listy CImageList images; if(!m_Images.Create(::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON), ILC_COLOR | ILC_MASK, 6, 6)) { TRACE0("Nie można stworzyć listy ikon!\n"); return; } // dodanie ikonek images.Add(AfxGetApp()->LoadIcon(IDI_NETWORK)); images.Add(AfxGetApp()->LoadIcon(IDI_PROVIDER)); images.Add(AfxGetApp()->LoadIcon(IDI_WORKGROUP)); images.Add(AfxGetApp()->LoadIcon(IDI_SERVER)); images.Add(AfxGetApp()->LoadIcon(IDI_DISK)); images.Add(AfxGetApp()->LoadIcon(IDI_PRINTER))W aplikacjach korzystających z architektury dokument-widok powyższy fragment kodu najlepiej ulokować w konstruktorze widoku (oczywiście obiekt klasy CImageList powinien wtedy być składową klasy widoku a nie zmienną lokalną!). Jest to podyktowane tym, że w przypadku aplikacji z interfejsem SDI widok nie jest tworzony od nowa z nowym dokumentem (zachodzi tzw. reusing widoku i dokumentu!) zaś metoda OnInitialUpdate jest wołana po raz drugi (z punktu widzenia obiektu widoku). Warto nadmienić, że do listy ikon można dodawać nie tylko ikony ale także bitmapy (które uzupełnione o maski - czyli monochomatyczne bitmapy definiujące transparentne piksele bitmapy z obrazkiem - zachowują się dokładnie tak samo jak ikony).
Listę ikon można przydzielić do widoku (kontrolki) listowego za pomocą metody CListCtrl::SetImageList. Przykładowo w metodzie OnInitalUpdate naszego hipotetycznego widoku CMyView mogłoby to wyglądać następująco:
GetListCtrl().SetImageList(m_largeIcons, LVSIL_NORMAL); GetListCtrl().SetImageList(m_smallIcons, LVSIL_SMALL); GetListCtrl().SetImageList(m_stateIcons, LVSIL_STATE);Z powyższego przykładu widać, że do jednego widoku (kontrolki) można przydzielić trzy różne listy:
- listę dużych ikon dla aranżacji Icon view - pierwszy wiersz przykładu
- listę małych ikon dla aranżacji Small icon view, List view, oraz Report view - drugi wiersz przykładu
- listę ikon stanu elementów - trzeci wiersz przykładu.
Jak przypisywać konkretne ikony poszczególnym elementom widoku czytelnik, może dowiedzieć się z lektury poniższego paragrafu.
Dodawanie kolumn, elementów i subelementów
W widokach wyprowadzonych z klasy CListView programista nie musi zaprzątać sobie głowy wizualizacją danych - robi to za niego widok (kontrolka) - a jedynie musi zadbać o dostarczenie i pogrupowanie danych do widoku.
Jeżeli nie planuje się korzystania z aranżacji typu Report view można w ogóle nie zaprzątać sobie głowy definiowaniem kolumn ani dodawaniem subelementów do elementów. Pozostaje zatem wstawianie (CListCtrl::InsertItem), usuwanie (CListCtrl::DeleteItem) oraz modyfikowanie (CListCtrl::SetItem) elementów do widoku. Oczywiście można wykonywać znacznie więcej operacji na kontrolkach typu list view lecz tutaj zajmiemy się tymi podstawowymi. Oto przykład wykorzystania tych metod jaki mógłby mieć miejsce w naszej hipotetycznej klasie CMyView:
// dodanie nowego elementu void CMyView::OnNewElement() { CNewElementDlg dialog(this); if(dialog.DoModal() == IDOK) { GetListCtrl().InsertItem(GetListCtrl().GetItemCount(), dialog.Label, dialog.image); } } // usunięcie zaznaczonych elementów void CMyView::OnDeleteSelected() { CListCtrl &list = GetListCtrl(); POSITION pos = list.GetFirstSelectedItemPosition(); while(pos) list.DeleteItem(list.GetNextSelectedItem(pos)); }W przypadku aranżacji typu Report view przed dodawaniem elementów należy zdefiniować kolumny organizujące elementy i ich subelementy. Ponownie najdogodniejszym miejscem do tego celu będzie metoda OnInitialUpdate:
#define FIRST_COL 0 #define SECOND_COL 1 #define THIRD_COL 2 #define FIRST_SUB 0 #define SECOND_SUB 1 ... void CMyView::OnInitialUpdate() { CListCtrl &list = GetListCtrl(); list.InsertColumn(FIRST_COL, _T("Etykieta"), LVCFMT_LEFT, 200); list.InsertColumn(SECOND_COL, _T("Subelement 1"), LVCFMT_CENTER, 200, FIRST_SUB); list.InsertColumn(THIRD_COL, _T("Subelement 2"), LVCFMT_CENTER, 200, SECOND_SUB); ... }Elementy listy w trybie Report view dodajemy jak poprzednio za pomocą metody CListCtrl::InsertItem zaś subelementy, jako atrybuty elementów, dodajemy (i modyfikujemy) za pomocą metody CListCtrl::SetItemText (która jest szczególnym aspektem bardziej ogólnej metody CListCtrl::SetItem). Oto jak można to zrobić (załóżmy że poniższy kod wstawiamy przed "..." z przykładu powyżej):
int nItem; nItem = list.InsertItem(list.GetItemCount(), _T("Element 1")); list.SetItemText(nItem, FIRST_SUB, _T("Subelement 1 elementu 1")); list.SetItemText(nItem, SECOND_SUB, _T("Subelement 2 elementu 1"));
CTreeView
Jest to kolejny przykład klasy widoku powstałej w oparciu o istniejącą kontrolkę systemową. Widoki wyprowadzone z klasy CTreeView znakomicie nadają się do prezentacji danych shierarchizowanych drzewiasto (stąd właśnie nazwa klasy gdyż angielskie słowo tree oznacza właśnie drzewo). Choć teoretycznie jest możliwe bezpośrednie wykorzystanie tej klasy przez aplikację to jednak w praktycznych zastosowaniach, aby skorzystać z możliwości oferowanych przez klasę CTreeView, zawsze zachodzi konieczność wyprowadzenia nowej klasy.
Z punktu widzenia programisty obsługa widoku CTreeView jest bardzo podobna do obsługi widoku CListView. Każdy element drzewa może (ale nie musi) posiadać ikonkę przed tekstową etykietą. Stąd przed przystąpieniem do pracy z widokiem CTreeView również może zajść konieczność przygotowania listy ikon. Wstępne wypełnienie drzewka elementami (na podstawie danych zawartych w dokumencie) należy wykonywać w metodzie OnInitalUpdate. Poniższy przykład demonstruje jak można wypełniać drzewko elementami (dla czytelności przykładu drzewo wypełniane jest elementami statycznymi):
void CMyTreeView::OnInitialUpdate() { CTreeCtrl &tree = GetTreeCtrl(); // dodawanie "gałązek" HTREEITEM hItem = tree.InsertItem(_T("Poziom 1, Element 1")); tree.InsertItem(_T("Poziom 2, Element 1"), hItem); hItem = tree.InsertItem(_T("Poziom 2, Element 2"), hItem); tree.InsertItem(_T("Poziom 3, Element 1"), hItem); hItem = tree.InsertItem(_T("Poziom 1, Element 2")); }
CFormView
Jest to klasa bazowa dla wszystkich widoków przedstawiających dane w postaci form. Generalnie widoki klasy CFormView to po prostu widoki z kontrolkami, będące niejako hybrydą okna dialogowego z widokiem klasy CScrollView (która, nota bene, jest klasą bazową klasy CFormView). Widoki-formy bazują na dialogach zdefiniowanych w zasobach (ang. dialog-template resource).
W odróżnieniu do wcześniej omawianych klas, klasa CFormView nie posiada swoich dodatkowych, dedykowanych metod. Posiada tylko specjalną, jednoargumentową wersję konstruktora, akceptującego ID zasobów przechowujących wzorzec dialogu, który posłuży jako forma.