OwnerDraw

Adam Boduch

"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.

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:[email protected]              *)
(*                                              *)
(************************************************)

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:[email protected]                        *)
(*                                                              *)
(****************************************************************)

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.

7 komentarzy

Geniusz :) Szukałem tego dwa lata ! (przesada...trzy lata) ;)

Fajny artykuł, przydał się:)

Copyright (c) 2001 by Adam Bopduch

;)

Tylko że nadal mi coś nie wychodzi...

Przyda sie :) Coś bardzo umilające widok :)

Racja, przyda się :)