OwnerDraw
"Cóż to takiego?" - zapytasz. Posłużę się przykładem. Masz komponent ComboBox. Chcesz, aby na każdej pozycji po lewej stronie widniała ikonka. Właśnie, fajnie by to wyglądało, prawda? Nie jest to trudne. Polega to na ręcznym rysowaniu takiej ikonki w pozycji ComboBox'a używając klasy TCanvas.
Żeby móc rysować po komponencie ComboBox musisz po pierwsze zmienić właściwość Style na csOwnerDrawFixed. To najważniejsze, a często programistom zdarza się, że o tym zapominają, a później się dziwią, czemu program nie działa tak jak trzeba.
Taki sposób konturowania programu (ręczne rysowanie) jest dość skomplikowane, ale czasem się przydaje i satysfakcja jest duża, bo program bardzo fajnie wygląda.
Na formie umieść na początek komponent ImageList - to tutaj będą przechowywane ikony, które później znajdą się w kolejnych pozycjach komponentu ComboBox. A więc kliknij na ten komponent, umieść go na formie, a następnie jeżeli już to zrobiłeś kliknij na sam komponent, a otworzy się okienko dzięki któremu w prosty sposób (za pomocą przycisku Add) dodasz nowe ikony. Ja dodałem trzy.
Teraz zmień właściwosć Style na csOwnerDrawFixed. Już? Ok, teraz przechodzimy do pisania kodu..
W sekcji Implementation dodaj takie linie ( tablicę ):
Jak widzisz tutaj zadeklarowałem tablicę, która zawiera trzy elementy. To te elementy będą pozycjami dodanymi do kontrolki. Teraz wygeneruj procedurę OnCreate formy i wpisz taki kod:
W porządku, to nie było takie trudne. Po prostu dodaliśmy nowe pozycje do komponentu. Nic nadzwyczajnego. Teraz najważniejsza procedura. Komponent ComboBox posiada zdarzenie OnDrawItem - wygeneruj ją. Przyjrzyj się jak ma ona wyglądać:
Mało kodu, proste. cbMain to nazwa komponentu ComboBox. Dalej wywołujemy metodę Draw komponentu ImageList. Pierwszym parametrem jest miejsce docelowe, czyli gdzie umieścić ikonę? Będzie to płótno komponentu ComboBox. Kolejne dwa parametry to pozycja ikony w poziomie X oraz w pionie Y. Procedura OnDrawItem posiada zmienną Rect, która określa położenie danej pozycji w ComboBox (lewa, prawa). Tak więc kod Rect.Left + 2 jest rozumiany przez kompilator jako: "położenie: lewa strona pozycji ComboBox + 2 piksele". Ostatnim parametrem jest pozycja w ComboBox w której zostanie umieszczona ikona. Tak się dobrze składa, że w procedurze OnDrawItem jest parametr Index, który określa daną pozycję. Kolejna instrukcja to narysowanie obok tekstu.
Ok, to wszystko. Przeznacz, że nie było to tak trudne jak myślałeś? Cały kod programu wygląda tak:
Dalsze przykłady z trybem OwnerDraw
Dobrze, teraz inne równie fajne rzeczy. Ci, którzy mają Worda 2000 albo chociażby PaintShop Pro 7.0 wiedzą jak te programy fajnie wyświetlają czcionki. W liście rozwijalnej każda czcionka jest wyświetlona jej własnym krojem. Teraz my takie coś zrobimy w swoim programie. W OnCreate pisz linie, która spowoduje dodanie do ComboBox listy dostępnych czcionek:
A tak wygląda procedurę OnDrawItem dla tego komponentu:
W pierwszej linii następuje ustawienie kroju czcionki według nazwy pozycji, którą rozpatrujemy. W praktyce wygląda to tak: jeżeli w pozycji jest nazwa Arial to ta nazwa jest rysowana czcionką Arial. Itd., itp. Następnie za pomocą TextOut następuje narysowanie samego tekstu określonym krojem. I tak z każdą pozycją ComboBox'a. Jeżeli teraz uruchomisz program i rozwiniesz tę liste to faktycznie, program działa, ale po najechaniu kursorem na daną pozycje pojawia się obramowanie. Nie za fajnie to wygląda, prawda? Żeby się tego pozbyć na samym początku tej procedury dodaj linię:
Polecenie FillRect powoduje włączenie do regionu Rect dodanie lewych, górnych krawędzi Rect.
Teraz już wygląda dobrze. Zróbmy coś jeszcze. Po rozwinięciu pozycji wyświetli się lista kolorów, a po lewej stronie tzw. próbka, czyli kwadracik, który będzie namalowany tym właśnie kolorem, którego nazwa widnieje na liście. Z tym trochę trzeba pokombinować. Na początku należy stworzyć taką tablicę:
Na razie na naszej liście wystarczy, że będzie 6 pozycji. Ta tablica składa się z rekordu, który z kolei składa się z dwóch elementów typu ShortString ( opis koloru ) oraz TColor (sam kolor). Taką tablicę umieść w sekcji Implementation.
Następnie w OnCreate pętla, która doda do komponentu ColorBox (tak nazwałem TComboBox) kolejne pozycje oznaczające kolor:
Na samym początku ustawiany jest kolor rysowanego kwadratu. Jak widzisz kolor ten jest pobierany z tablicy na podstawie właściwości Index określającej daną pozycję ComboBox. Następnie po lewej stronie narysowany zostaje kwadracik o szerokości 20 pikseli. Kolejna linia ustawia tło na przeźroczyste gdyż będziemy nakładali tekst - nazwę koloru, także pobrany z tablicy Colors.
Postanowiłem połączyć oba źródła (wyświetlanie listy czcionek oraz kolorów) do zaprezentowania przykładowego programu. Wyświetlać on będzie podgląd czcionki, zresztą zobacz sam:
Trybu OwnerDraw możesz używać także na innych komponentach - np. ListBox, PopupMenu, MainMenu StringGrid. Jak widzisz ze znajomością "malowania" po komponentach można uzyskać fajne efekty. Wystarczy tylko znać klasę TCanvas.
Zaczynamy...
Żeby móc rysować po komponencie ComboBox musisz po pierwsze zmienić właściwość Style na csOwnerDrawFixed. To najważniejsze, a często programistom zdarza się, że o tym zapominają, a później się dziwią, czemu program nie działa tak jak trzeba.
Taki sposób konturowania programu (ręczne rysowanie) jest dość skomplikowane, ale czasem się przydaje i satysfakcja jest duża, bo program bardzo fajnie wygląda.
Na formie umieść na początek komponent ImageList - to tutaj będą przechowywane ikony, które później znajdą się w kolejnych pozycjach komponentu ComboBox. A więc kliknij na ten komponent, umieść go na formie, a następnie jeżeli już to zrobiłeś kliknij na sam komponent, a otworzy się okienko dzięki któremu w prosty sposób (za pomocą przycisku Add) dodasz nowe ikony. Ja dodałem trzy.
Teraz zmień właściwosć Style na csOwnerDrawFixed. Już? Ok, teraz przechodzimy do pisania kodu..
Trochę kodu?
W sekcji Implementation dodaj takie linie ( tablicę ):
{ pozycje, ktore pojawia sie w komponencie ComboBox } const Tx : array[0..2] of String[20] = ('Konfiguracja', 'Instalacja', 'Notatnik');
Jak widzisz tutaj zadeklarowałem tablicę, która zawiera trzy elementy. To te elementy będą pozycjami dodanymi do kontrolki. Teraz wygeneruj procedurę OnCreate formy i wpisz taki kod:
procedure TMainForm.FormCreate(Sender: TObject); var i : Integer; begin { podczas tworzenia programu dodaj nowe pozycje } for I := 0 to 2 do cbMain.Items.Add(Tx[i]); end;
W porządku, to nie było takie trudne. Po prostu dodaliśmy nowe pozycje do komponentu. Nic nadzwyczajnego. Teraz najważniejsza procedura. Komponent ComboBox posiada zdarzenie OnDrawItem - wygeneruj ją. Przyjrzyj się jak ma ona wyglądać:
procedure TMainForm.cbMainDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin with cbMain do begin { namalowanie obrazka w zaleznosci od pozycji } ImageList.Draw(cbMain.Canvas, Rect.Left + 2, Rect.Top + 2, Index); { obok obrazka dodaj odpowiedni tekst } Canvas.TextOut(Rect.Left + 22, Rect.Top + 2, Tx[Index]); end; end;
Mało kodu, proste. cbMain to nazwa komponentu ComboBox. Dalej wywołujemy metodę Draw komponentu ImageList. Pierwszym parametrem jest miejsce docelowe, czyli gdzie umieścić ikonę? Będzie to płótno komponentu ComboBox. Kolejne dwa parametry to pozycja ikony w poziomie X oraz w pionie Y. Procedura OnDrawItem posiada zmienną Rect, która określa położenie danej pozycji w ComboBox (lewa, prawa). Tak więc kod Rect.Left + 2 jest rozumiany przez kompilator jako: "położenie: lewa strona pozycji ComboBox + 2 piksele". Ostatnim parametrem jest pozycja w ComboBox w której zostanie umieszczona ikona. Tak się dobrze składa, że w procedurze OnDrawItem jest parametr Index, który określa daną pozycję. Kolejna instrukcja to narysowanie obok tekstu.
Ok, to wszystko. Przeznacz, że nie było to tak trudne jak myślałeś? Cały kod programu wygląda tak:
(************************************************) (* *) (* Owner Draw *) (* Copyright (c) 2001 by Adam Bopduch *) (* http://4programmers.net *) (* mailto:boduch@poland.com *) (* *) (************************************************) unit MainFrm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ImgList; type TMainForm = class(TForm) cbMain: TComboBox; ImageList: TImageList; Label1: TLabel; procedure cbMainDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } end; var MainForm: TMainForm; implementation {$R *.DFM} { pozycje, ktore pojawia sie w komponencie ComboBox } const Tx : array[0..2] of String[20] = ('Konfiguracja', 'Instalacja', 'Notatnik'); procedure TMainForm.cbMainDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin with cbMain do begin { namalowanie obrazka w zaleznosci od pozycji } ImageList.Draw(cbMain.Canvas, Rect.Left + 2, Rect.Top + 2, Index); { obok obrazka dodaj odpowiedni tekst } Canvas.TextOut(Rect.Left + 22, Rect.Top + 2, Tx[Index]); end; end; procedure TMainForm.FormCreate(Sender: TObject); var i : Integer; begin { podczas tworzenia programu dodaj nowe pozycje } for I := 0 to 2 do cbMain.Items.Add(Tx[i]); end; end.
Dalsze przykłady z trybem OwnerDraw
Dobrze, teraz inne równie fajne rzeczy. Ci, którzy mają Worda 2000 albo chociażby PaintShop Pro 7.0 wiedzą jak te programy fajnie wyświetlają czcionki. W liście rozwijalnej każda czcionka jest wyświetlona jej własnym krojem. Teraz my takie coś zrobimy w swoim programie. W OnCreate pisz linie, która spowoduje dodanie do ComboBox listy dostępnych czcionek:
ComboBox.Items.Assign(Screen.Fonts); // do komponentu dodaj spis czcionek
A tak wygląda procedurę OnDrawItem dla tego komponentu:
procedure TMainForm.ComboBoxDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin ComboBox.Canvas.Font.Name := ComboBox.Items[Index]; // odczytanie czcionki { nraysowanie tekstu ( nazwy czcionki :)) okreslonym krojem } ComboBox.Canvas.TextOut(Rect.Left + 2, Rect.Top + 2, ComboBox.Items[Index]); end;
W pierwszej linii następuje ustawienie kroju czcionki według nazwy pozycji, którą rozpatrujemy. W praktyce wygląda to tak: jeżeli w pozycji jest nazwa Arial to ta nazwa jest rysowana czcionką Arial. Itd., itp. Następnie za pomocą TextOut następuje narysowanie samego tekstu określonym krojem. I tak z każdą pozycją ComboBox'a. Jeżeli teraz uruchomisz program i rozwiniesz tę liste to faktycznie, program działa, ale po najechaniu kursorem na daną pozycje pojawia się obramowanie. Nie za fajnie to wygląda, prawda? Żeby się tego pozbyć na samym początku tej procedury dodaj linię:
ComboBox.Canvas.FillRect(Rect); // pobranie obszaru pozycji
Polecenie FillRect powoduje włączenie do regionu Rect dodanie lewych, górnych krawędzi Rect.
Teraz już wygląda dobrze. Zróbmy coś jeszcze. Po rozwinięciu pozycji wyświetli się lista kolorów, a po lewej stronie tzw. próbka, czyli kwadracik, który będzie namalowany tym właśnie kolorem, którego nazwa widnieje na liście. Z tym trochę trzeba pokombinować. Na początku należy stworzyć taką tablicę:
const Colors : array[0..5] of record Descritption: ShortString; Col: TColor; end = ((Descritption: 'Czarny'; Col: clBlack), (Descritption: 'Czerwony'; Col: clRed), (Descritption: 'Zielony'; Col: clGreen), (Descritption: 'Srebrny'; Col: clSilver), (Descritption: 'Błękitny'; Col: clAqua), (Descritption: 'Żółty'; Col : clYellow));
Na razie na naszej liście wystarczy, że będzie 6 pozycji. Ta tablica składa się z rekordu, który z kolei składa się z dwóch elementów typu ShortString ( opis koloru ) oraz TColor (sam kolor). Taką tablicę umieść w sekcji Implementation.
Następnie w OnCreate pętla, która doda do komponentu ColorBox (tak nazwałem TComboBox) kolejne pozycje oznaczające kolor:
{ do komponentu ColorBox dodaj opis koloru wyczytany z tablicy } for I := 0 to High(Colors) do ColorBox.Items.Add(Colors[i].Descritption); No i w końcu sama procedura OnDrawItem: procedure TMainForm.ColorBoxDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin with ColorBox.Canvas do begin FillRect(Rect); Brush.Color := Colors[Index].Col; // ustaw kolor rysowanego kwadrata { narysowanie kwadraru o okreslonej pozycji kwadratu } Rectangle(Rect.Left + 2, Rect.Top + 2, Rect.Left + 22, Rect.Bottom -2); Brush.Style := bsClear; // ustawienie tla na przezroczyste { napisanie tekstu obok kwadraru } TextOut(Rect.Left + 30, Rect.Top + 2, Colors[Index].Descritption); end; end;
Na samym początku ustawiany jest kolor rysowanego kwadratu. Jak widzisz kolor ten jest pobierany z tablicy na podstawie właściwości Index określającej daną pozycję ComboBox. Następnie po lewej stronie narysowany zostaje kwadracik o szerokości 20 pikseli. Kolejna linia ustawia tło na przeźroczyste gdyż będziemy nakładali tekst - nazwę koloru, także pobrany z tablicy Colors.
Postanowiłem połączyć oba źródła (wyświetlanie listy czcionek oraz kolorów) do zaprezentowania przykładowego programu. Wyświetlać on będzie podgląd czcionki, zresztą zobacz sam:
(****************************************************************) (* *) (* OnDrawItem *) (* Copyright (c) 2001 by Adam Boduch *) (* http://4programmers.net *) (* mailto:boduch@poland.com *) (* *) (****************************************************************) unit MainFrm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Menus; type TMainForm = class(TForm) ComboBox: TComboBox; ColorBox: TComboBox; TestLabel: TStaticText; btnSet: TButton; procedure FormCreate(Sender: TObject); procedure ComboBoxDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); procedure ColorBoxDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); procedure btnSetClick(Sender: TObject); end; var MainForm: TMainForm; implementation {$R *.DFM} { OPIS PROGRRAMU... Na formie umieszczone sa dwa komponentu typu ComboBox. Do nich dodam nazwe czcionek (w pierwszym), ktore sa dostepne w systemie, a w drugim komponencie lista paru kolorow wraz z opisem. Cala sztuka polega na tym, aby zarowno kroje czcionek jak i kolory byly odpowiednio przedstawione graficznie. BTW: wiecej o tym w artykule "OwnerDraw" na stronie 4programmers.net Na samym poczatku nalezy zadeklarowac tablice, ktora jest w rzeczywistosci rekordem zawierajacym opis koloru oraz jego odpowiednik w typie TColor ( np. clBlack ). Cale "serce" programu opiera sie na dwoch procedurach OnDrawItem. To w nich na Canvasie rysowane sa odpowiednie elementy.... } const Colors : array[0..5] of record Descritption: ShortString; Col: TColor; end = ((Descritption: 'Czarny'; Col: clBlack), (Descritption: 'Czerwony'; Col: clRed), (Descritption: 'Zielony'; Col: clGreen), (Descritption: 'Srebrny'; Col: clSilver), (Descritption: 'Błękitny'; Col: clAqua), (Descritption: 'Żółty'; Col : clYellow)); procedure TMainForm.FormCreate(Sender: TObject); var i : Integer; begin ComboBox.Items.Assign(Screen.Fonts); // do komponentu dodaj spis czcionek { do komponentu ColorBox dodaj opis koloru wyczytany z tablicy } for I := 0 to High(Colors) do ColorBox.Items.Add(Colors[i].Descritption); end; procedure TMainForm.ComboBoxDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin ComboBox.Canvas.FillRect(Rect); // pobranie obszaru pozycji ComboBox.Canvas.Font.Name := ComboBox.Items[Index]; // odczytanie czcionki { nraysowanie tekstu ( nazwy czcionki :)) okreslonym krojem } ComboBox.Canvas.TextOut(Rect.Left + 2, Rect.Top + 2, ComboBox.Items[Index]); end; procedure TMainForm.ColorBoxDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin with ColorBox.Canvas do begin FillRect(Rect); Brush.Color := Colors[Index].Col; // ustaw kolor rysowanego kwadrata { narysowanie kwadraru o okreslonej pozycji kwadratu } Rectangle(Rect.Left + 2, Rect.Top + 2, Rect.Left + 22, Rect.Bottom -2); Brush.Style := bsClear; // ustawienie tla na przezroczyste { napisanie tekstu obok kwadraru } TextOut(Rect.Left + 30, Rect.Top + 2, Colors[Index].Descritption); end; end; procedure TMainForm.btnSetClick(Sender: TObject); begin { procedura ustawia podlad na komponencie TestLabel } { na poczatek jest pobierana nazwa czcionki z komponentu ComboBox, a nastepnie odczytana pozycja w komponencie ColorBox i na podstawie pozycji wczytany kolor z tablicy Colors } TestLabel.Font.Name := ComboBox.Text; TestLabel.Font.Color := Colors[ColorBox.ItemIndex].Col; end; end.
Podsumowanie
Trybu OwnerDraw możesz używać także na innych komponentach - np. ListBox, PopupMenu, MainMenu StringGrid. Jak widzisz ze znajomością "malowania" po komponentach można uzyskać fajne efekty. Wystarczy tylko znać klasę TCanvas.
Geniusz :) Szukałem tego dwa lata ! (przesada...trzy lata) ;)