V tomto pokračování si vytvoříme komponentu odvozenou od TListBox, která bude umožňovat i méně zkušenému programátorovi formou vizuálního návrhu nastavit řadu položek.
V tomto pokračování si vytvoříme komponentu odvozenou od TListBox, která bude umožňovat i méně zkušenému programátorovi formou vizuálního návrhu nastavit:
- Barvu pozadí a barvu vybrané položky
- Bitmapu tvořící pozadí a jinou bitmapu tvořící pozadí vybrané položky
- Font vybrané položky
- ImageList určující obrázky zobrazované u jednotlivých položek
- „Výšku mezery“ mezi jednotlivými položkami (obrázky) list-boxu
Výsledek vidíte na následujícím obrázku:
Výsledkem není samozřejmě nic světoborného, ale cílem bude naučit se, jak používat uživatelsky kreslené prvky Windows, tedy především poznat zprávy WM_MEASUREITEM a WM_DRAWITEM a jejich správnou „obsluhu“.
Vytvořme si tedy komponentu TRPJImageListBox odvozenou od TListBox. Přidáme si nové property:
__published:
__property TImageList* Images = { read=FImages, write=SetImages };
__property Graphics::TBitmap* Background = { read=FBackground, write=SetBackground };
__property Graphics::TBitmap* BackgroundSelected =
{ read=FBackgroundSelected, write=SetBackgroundSelected };
__property int ImagesGap =
{ read=FImagesGap, write=SetImagesGap, default=0 };
__property TColor ColorSelected =
{ read=FColorSelected, write=SetColorSelected };
__property TFont* FontSelected = { read=FFontSelected, write=SetFontSelected };
K jejich významu jen pár poznámek: image-list (představovaný komponentou TImageList) Images bude obsahovat grafiku zobrazující se u jednotlivých položek list-boxu s odpovídajícími si indexy položek. Výchozí pozadí list-boxu je tvořeno property Color, zděděnou do TListBox. Pokud určíme platnou bitmapu (jako třída VCL Graphics::Bitmap) Background, bude tato tvořit pozadí položek list-boxu. Barva pozadí vybraných položek bude podobně určena novou property ColorSelected a podobně budeme moci určit bitmapu pozadí vybraných položek pomocí nové property BackgroundSelected. Výška položky list-boxu bude určovat výška image-listu „Images“ plus hodnota určená property ImagesGap, kterou budeme moci určit „dodatečnou mezeru“ mezi položkami.
Ve vlastní implementaci nesmíme zapomenout, jak už jsme si ukázali v předchozích částech seriálu, vytvořit instance „úložných proměnných“ našich nových property. Konstruktor a destruktor může tedy vypadat následovně:
__fastcall TRPJImageListBox::TRPJImageListBox(TComponent* Owner)
: TListBox(Owner)
{
FBackground = new Graphics::TBitmap();
FBackgroundSelected = new Graphics::TBitmap();
FFontSelected = new TFont();
FFontSelected->Assign(Font);
ColorSelected = TColor(GetSysColor(COLOR_HIGHLIGHT));
}
__fastcall TRPJImageListBox::~TRPJImageListBox()
{
delete FBackground;
delete FBackgroundSelected;
delete FFontSelected;
}
Dále musíme, jak jsem již také upozorňoval v minulých dílech, upravit „wizardem“ vygenerované funkce Setxxxxx (pokud je přidáváte z ClassExploreru „vizuálně“) u property FontSelected, Backgroud a BackgroudSelected, kde místo přiřazení ukazatelů rovnítkem musíme použít členské funkce Assign, tedy např. na případě bitmapy pozadí takto:
void __fastcall TRPJImageListBox::SetBackground(Graphics::TBitmap* value)
{
if (FBackground != value)
{
FBackground->Assign(value);
Repaint();
}
}
Trochu teorie o uživatelsky kreslených prvcích
Nyní si řekněme nejprve trochu teorie o uživatelsky kreslených prvcích Windows. Touto terminologií nyní myslím prvky, které posílají svému rodičovskému oknu (mimo jiné) zprávu
WM_DRAWITEM, kterou ho informují (resp. operační systém ho informuje) o požadavku na uživatelské překreslení prvku. Informace potřebné pro kreslící funkci jsou – jak za chvíli uvidíme –v parametrech zprávy. Takovým prvkem tedy může být například buton, list-box, bombo-box, list-view apod. Pokud má být některý z prvků uživatelsky kreslený, musí mít ve stylu okna uvedenou příslušnou vlastnost. V případě list-boxu je to příznak
LBS_OWNERDRAWFIXED nebo
LBS_OVNERDRAWVARIABLE. Tuto vlastnost je obecně možné nastavit při vytvoření okna, popřípadě „za běhu“ pomocí funkce SetWindowLongPtr. V našem konkrétním případě, tj. komponenty odvozené od TListBox, se tím nemusíme zabývat, protože předek naší komponenty má již publikovanou příslušnou property (Style), pomocí níž můžeme tuto vlastnost nastavit „vizuálně“. Podrobněji se nastavením owner-draw stylu budeme zabývat v některém příštím pokračování, kde si ukážeme vytvoření uživatelsky kreslené komponenty (třeba butonu) „od základu“ , tedy od TWinControl. Nastavení owner-draw tedy necháme na programátorovi-designérovi a vrátíme se ke zprávám posílaným uživatelsky kreslenými prvky. Jak jsem již řekl, v „klasickém“ programu zprávy WM_DRAWITEM a WM_MEASUREITEM, které nás budou zajímat, dostane rodičovské okno prvku a „kreslící“ rutiny se pak řeší v proceduře okna (resp. se z ní volají) tohoto rodičovského okna, kterým je v případě programu používajícím VCL formulář TForm. Systém zpráv VCL nám však dává možnost zpracovávat i tyto zprávy přímo v handlenu zpráv komponenty díky tomu, že uvedené zprávy a několik dalších (např. WM_COMMNAND) jsou posílány zpět komponentě s naprosto stejnými daty (tedy parametry wParam a lParam), pouze s tím rozdílem, že identifikátor zprávy začíná na CN_xxxxxx. Osobně nevím, proč vytvářet další identifikátory, když jednodušší je poslat zpět zcela totožnou zprávu (tj. WM_xxxx), jako tomu je v systému zpráv MFC (tedy knihovny od Microsoft používané ve Visual C++). Navíc vzhledem k úrovni dokumentace tyto zprávy zůstávají často programátorům úspěšně skryty. Jejich seznam nejlépe naleznete, když si otevřete soubor controls.hpp.
Praktická realizace uživatelského kreslení
Co tedy musíme udělat v kódu komponenty? Vytvoříme si nějakým způsobem handler zpráv CN_DRAWITEM a CN_MEASUREITEM. Já jsem konkrétně použil makra mapy zpráv, která jsou v deklaraci třídy:
protected:
BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER(CN_DRAWITEM, TMessage, OnWM_DRAWITEM)
VCL_MESSAGE_HANDLER(CN_MEASUREITEM, TMessage, OnWM_MEASUREITEM)
END_MESSAGE_MAP(TListBox)
Vlastní implementace je pak ve zdrojovém souboru cpp. Nejdříve k té jednodušší zprávě, tedy CN_MESSUREITEM. Tuto zprávu nám pošle systém a chce po nás, abychom mu v jejím zpracování oznámili rozměry uživatelsky kresleného prvku nebo jeho položky, jako je tomu v případě list-boxu, kde mu musíme říci výšku položky. Tuto výšku položky zadáme jako prvek itemHeight struktury
typedef struct tagMEASUREITEMSTRUCT {
UINT CtlType;
UINT CtlID;
UINT itemID;
UINT itemWidth;
UINT itemHeight;
ULONG_PTR itemData;
} MEASUREITEMSTRUCT;
jejíž adresu dostaneme v parametru lParam zprávy WM_MEASUREITEM (resp. CN_MEASUREITEM). Pokud se ptáte na smysl ostatních prvků, tak například v případě list-boxu s proměnnou výškou položek je tato zpráva posílána pro každou právě přidanou položku a my máme možnost určit individuálně její výšku. A právě prvek itemID nám určí, o kterou položku (její index) se právě jedná. Prvky CtlType a CtlID jsou zde nutné při „klasickém“ způsobu zachytávání této zprávy (WM_MEASUREITEM), kterou – jak jsme si řekli – dostává rodičovské okno prvku, a v případě, že toto okno obsahuje více uživatelsky kreslených prvků, musí mít možnost rozlišit, od kterého prvku daná zpráva pochází. Kód handleru zprávy CN_MEASUREITEM tedy může vypadat takto:
void __fastcall TRPJImageListBox::OnWM_MEASUREITEM(TMessage& msg)
{
TListBox::Dispatch(&msg);
LPMEASUREITEMSTRUCT lpmis;
lpmis = (LPMEASUREITEMSTRUCT)msg.LParam;
if ( FImages != NULL )
lpmis->itemHeight = FImages->Height + FImagesGap;
else
lpmis->itemHeight = 20;
}
Nyní ke zprávě CN_DRAWITEM. Její parametr lParam je adresou struktury
typedef struct tagDRAWITEMSTRUCT {
UINT CtlType;
UINT CtlID;
UINT itemID;
UINT itemAction;
UINT itemState;
HWND hwndItem;
HDC hDC;
RECT rcItem;
ULONG_PTR itemData;
} DRAWITEMSTRUCT;
Tuto strukturu nám naplní systém vším potřebným k vykreslení dané položky. V našem případě nás budou zajímat prvky:
- itemState – určuje stav položky. Úplný popis možných hodnot naleznete v dokumentaci, pro nás bude významné testování na příznak ODS_SELETED, určující v případě list-boxu, zda je položka vybraná.
- hDC – kontext zařízení (HDC), na které budeme položku kreslit. Jej třeba si pouze uvědomit, že i při kreslení jedné položky máme možnost kreslit do celého list-boxu, jde tedy o kontext zařízení celého list-boxu. Pro určení souřadnic aktuálně kreslené položky nám slouží prvek
- rcItem – obdélník určující souřadnice právě kreslení položky.
- itemID – identifikátor položky, v případě list-boxu index položky.
Když tedy použijeme takto získané údaje v kombinaci s hodnotami zadanými prostřednictvím property, můžeme vytvořit funkci realizující kreslení položky list-boxu jako handler zprávy CN_DRAWITEM takto:
void __fastcall TRPJImageListBox::OnWM_DRAWITEM(TMessage& msg)
{
HBRUSH hbrush;
LPDRAWITEMSTRUCT lpdis;
lpdis = (LPDRAWITEMSTRUCT)msg.LParam;
if ( lpdis->itemState & ODS_SELECTED )
{
if ( FBackgroundSelected->Empty )
hbrush = CreateSolidBrush((COLORREF)PrepareTColor(FColorSelected));
else
hbrush = CreatePatternBrush(FBackgroundSelected->Handle);
}
else
{
if ( FBackground->Empty )
hbrush = CreateSolidBrush(PrepareTColor(Color));
else
hbrush = CreatePatternBrush(FBackground->Handle);
}
FillRect(lpdis->hDC, &lpdis->rcItem, (HBRUSH)(hbrush));
if ( FImages != NULL )
if ( FImages->Count > lpdis->itemID )
{
ImageList_Draw((HIMAGELIST)Images->Handle, lpdis->itemID,
lpdis->hDC,
lpdis->rcItem.left + ImagesGap/2,
lpdis->rcItem.top + ImagesGap/2, ILD_TRANSPARENT);
}
lpdis->rcItem.left += (FImages->Width + (2*FImagesGap) + 2);
SetBkMode(lpdis->hDC, TRANSPARENT);
if ( lpdis->itemState & ODS_SELECTED )
{
SelectObject(lpdis->hDC, FFontSelected->Handle);
SetTextColor(lpdis->hDC, FFontSelected->Color);
}
else
{
SelectObject(lpdis->hDC, Font->Handle);
SetTextColor(lpdis->hDC, Font->Color);
}
DrawText(lpdis->hDC,
Items->Strings[lpdis->itemID].c_str(),
strlen(Items->Strings[lpdis->itemID].c_str()), &lpdis->rcItem, DT_SINGLELINE|DT_VCENTER);
DeleteObject(hbrush);
}
Použitá funkce
TColor __fastcall PrepareTColor(TColor color)
{
TColor res;
if ( HIBYTE(HIWORD(color)) != 0)
{
res = (TColor)(DWORD)MAKELONG(LOWORD((DWORD)color),
MAKEWORD(LOBYTE(HIWORD((DWORD)color)), 0));
return (TColor)(GetSysColor((int)res));
}
else
return color;
}
slouží k vynulování nejvyššího BYTE z hodnoty typu TColor, který může obsahovat hodnotu určující použití palety, a v případě, že potřebujeme hodnoty barvy ve tvaru COLORREF, jako je tomu v případě použité funkce CreateSolidBrush, musí mít vždy hodnotu 0.
Zdrojový kód komponenty si můžete stáhnout image_list_box.zip.