Tvorba komponent pro C++ Builder - Image CheckBox

V tomto článku si vytvoříme komponentu check-box, která bude zobrazovat vlastní obrázky znázorňující jednotlivé stavy zaškrtnutí. Tyto obrázky budou definovány v přidruženém ImageListu, a check-box bude mít jako property indexy položek v image-listu pro jednotlivé stavy: tedy nezaškrtnuto, zaškrtnuto a disabled.
Jak na to?

Komponentu (pojmenovanou TRPJImgCheckBox) si odvodíme od TWinControl. Nejprve vypublikujeme bežné property zděděné od TWinControl tak, že je jednoduše opíšeme do sekce _published:

__published:
__property Caption;
__property Color;
__property Font;
__property OnClick;
__property OnContextPopup;
__property OnKeyDown;
__property OnKeyUp;
__property OnKeyPress;
__property OnMouseDown;
__property OnMouseMove;
__property OnMouseUp;
__property OnEnter;
__property OnExit;

Dále si přidáme nové property takové, jaké vidíte na výpisu z deklarace třídy:

__property TImageList* Images  = { read=FImages, write=SetImages };
__property bool Flat = { read=FFlat, write=SetFlat, default=true };
__property TAlign TextAlign  = { read=FTextAlign, write=SetTextAlign };
__property int IndexON  = { read=FIndexON, write=SetIndexON, default = 0 };
__property int IndexOFF  = { read=FIndexOFF, write=SetIndexOFF, default = 1 };
__property int IndexDisabled  = { read=FIndexDisabled, write=SetIndexDisabled,
default = 2};
__property bool Checked  = { read=FChecked, write=SetChecked, default=false };

Význam jednotlivých nových property je asi zřejmý z jejích názvů, postupně se k nim dostaneme tak jak budeme vytvářet komponentu.

Nejprve si jako obvykle v konstruktoru nastavíme nějaké „rozumné“ výchozí hodnoty některých property:

__fastcall TRPJImgCheckBox::TRPJImgCheckBox(TComponent* Owner)
: TWinControl(Owner)
{
Width = 97;
Height = 17;
Flat = true;
IndexON = 0;
IndexOFF = 1;
IndexDisabled = 0;
Images = NULL;
Checked = false;
}

Nyní musíme přepsat funkci CreateParams, ve které vytvoříme vlastní check-box a nastavíme jeho vlastnosti. Check-box jako takový není samostatný prvek Windows, ale ve skutečnosti je to buton příslušného typu, konkrétně s vlastností BS_CHECKBOX. Dále musíme nastavit vlastnost BS_OWNERDRAW, tedy uživatelsky kreslený buton, neboť jej budeme vykreslovat „ručně“:

void __fastcall TRPJImgCheckBox::CreateParams(Controls::TCreateParams& Params)
{
TWinControl::CreateParams(Params);
CreateSubClass(Params, "BUTTON");
Params.Style = Params.Style | BS_CHECKBOX | WS_TABSTOP;
if ( FImages != NULL )
Params.Style |= BS_OWNERDRAW;
if ( !FFlat ) Params.ExStyle = Params.ExStyle | WS_EX_CLIENTEDGE;
Params.Width = Width;
Params.Height = Height;
}

Dále nesmíme zapomenout na funkci Notification, neboť máme jako property (Images) nastavenu jinou komponentu a musíme tedy reagovat na její případné odstranění. V opačném případě by totiž při odstranění komponenty, která je stále napojena na naši komponentu došlo k výjimce programu. Přepišme tedy virtuální funkci Notification asi takto:

void __fastcall TRPJImgCheckBox::Notification(Classes::TComponent* AComponent,
Classes::TOperation Operation)
{
TWinControl::Notification(AComponent, Operation);
if ( Operation == opRemove )
{
if ( AComponent == Images )
Images = NULL;
}
}

Nyní si vytvořme funkci, která bude „hlídat“ a případně upozorní programátora-návrháře, že nastavené indexy položek v přidruženém image-listu jsou mimo rozsah, tedy není dostatečný počet položek v image-listu.

bool __fastcall TRPJImgCheckBox::ValidIndexes()
{
if ( FImages == NULL ) return true;
if ( FImages->Count <= FIndexON )
{
if ( ComponentState.Contains(csDesigning) )
ErrorMessage("IndexON mimo rozsah ImageListu");
return false;
}
if ( FImages->Count <= FIndexOFF )
{
if ( ComponentState.Contains(csDesigning) )
ErrorMessage("IndexOFF mimo rozsah ImageListu");
return false;
}
if ( FImages->Count <= FIndexDisabled )
{
if ( ComponentState.Contains(csDesigning) )
ErrorMessage("IndexDisabled mimo rozsah ImageListu");
return false;
}
return true;
}

„Pomocná“ funkce ErrorMessage je velice jednoduchá a vypadá následovně:

void __fastcall TRPJImgCheckBox::ErrorMessage(LPCTSTR lpText)
{
MessageBox(NULL, lpText, "RPJImgCheckBox - CHYBA !",
MB_ICONERROR | MB_TASKMODAL);
}

Nyní již přistupme k vlastnímu uživatelskému kreslení check-boxu. Jak již víme z minulých částí tohoto seriálu, musíme reagovat na zprávu WM_DRAWITEM, resp. V systému VCL na zprávu CN_DRAWITEM, kterou dostane přímo komponenta. Vytvoříme si tedy její handler například pomocí makra mapy zpráv (BEGIN_MESSAGE_MAP – END_MESSAGE_MAP). V této zprávě je pro nás důležitý parametr lParam, což je adresa struktury DRAWITEMSTRUCT:

typedef struct tagDRAWITEMSTRUCT {
  UINT      CtlType;
  UINT      CtlID;
  UINT      itemID;
  UINT      itemAction;
  UINT      itemState;
  HWND    hwndItem;
  HDC      hDC;
  RECT      rcItem;
  ULONG_PTR itemData;
} DRAWITEMSTRUCT;

V této struktuře máme všechny potřebné informace, které potřebujeme pro vykreslení příslušného uživatelsky kresleného prvku, v našem případě check-boxu. Podívejte se tedy, jak vypadá naše kreslící funkce:

void __fastcall TRPJImgCheckBox::OnCN_DRAWITEM(TMessage& Msg)
{
LPDRAWITEMSTRUCT lpdis;
lpdis = (LPDRAWITEMSTRUCT)Msg.LParam;
RECT rect = ClientRect;
int x, y;
switch ( FTextAlign )
{
case alRight:
x = ClientWidth - Images->Width - 1;
y = (ClientHeight - Images->Height) / 2;
break;
default:
x = 1;
y = (ClientHeight - Images->Height) / 2;
break;
}
if ( !Enabled )
ImageList_Draw((HIMAGELIST)Images->Handle,
      IndexDisabled, lpdis->hDC, x, y, ILD_TRANSPARENT);
else
{
if ( Checked )
ImageList_Draw((HIMAGELIST)Images->Handle, IndexON, lpdis->hDC,
x, y, ILD_TRANSPARENT);
else
ImageList_Draw((HIMAGELIST)Images->Handle, IndexOFF, lpdis->hDC,
x, y, ILD_TRANSPARENT);
}
SetBkMode(lpdis->hDC, TRANSPARENT);
switch ( FTextAlign )
{
case alRight:
rect.right -= (Images->Width + 4);
DrawText(lpdis->hDC, Caption.c_str(), strlen(Caption.c_str()), &rect,
DT_SINGLELINE | DT_VCENTER | DT_RIGHT);
break;
default:
rect.left += (Images->Width + 4);
DrawText(lpdis->hDC, Caption.c_str(), strlen(Caption.c_str()), &rect,
DT_SINGLELINE | DT_VCENTER | DT_LEFT);
break;
}
}

Nyní musíme ještě zajistit, aby programátor byl informován o změně stavu (zaškrtnutí) našeho check-boxu. Již máme pro tento účel vytvořenu property Checked. Musíme ale zajistit její řádné „updatování“. Jak na to? Check-box, jako každý buton posílá svému vlastníku při kliknutí na něj notifikační zprávu BN_CLICKED formou zprávy WM_COMMAND. Jak již víme, zpráva WM_COMMAND, stejně jako několik dalších je v systému VCL posílána také komponentě ve formě zprávy CN_COMMAND se stejnými parametry jako „původní“ zpráva WM_COMMAND. Notifikační kód (v tomto případě BN_CLICKED) je v horním WORDU parametru wParam zprávy WM_COMMAND. Vytvořme si proto následující handler zprávy CN_COMMAND:

void __fastcall TRPJImgCheckBox::OnCN_COMMAND(TMessage& Msg)
{
TWinControl::Dispatch(&Msg);
if ( Msg.WParamHi == BN_CLICKED )
{
Checked = !Checked;
}
}

Jak vidíte při kliknutí na check-box jednoduše překlopíme hodnotu property Checked.

Na závěr si ještě do komponenty přidejme „public“ funkci umožňující nastavit pozadí check-boxu přiřazením bitmapy, která bude tvořit vzorek pozadí:

void __fastcall TRPJImgCheckBox::SetBackBitmap(Graphics::TBitmap* ptBitmap)
{
if ( (ptBitmap == NULL) || (ptBitmap->Empty) )
{
Brush->Bitmap = NULL;
Brush->Color = Color;
}
else
{
Brush->Bitmap = ptBitmap;
}
Invalidate();
}

Jak je vidět tato funkce prostě přiřadí zadanou bitmapu jako vlastnost (property) Bitmap štětce (Brush) komponenty.

Mohli bychom samozřejmě tuto bitmapu na pozadí realizovat jako property, podobně jako jsme to udělali v tomto seriálu již několikrát (například pozadí položek vlastního popup-menu). V tomto případě jsem však šel cestou úspornějšího kódu. Mnoho „programátorů“ by totiž v případě pozadí jako property šlo cestou nejmenšího odporu a nastavili by i v případě většího počtu (graficky) stejných check-boxů v ObjectInspectoru jako property nějakou (stejnou) bitmapu. To by vedlo k tomu, že by ve zdrojích programu byly data této bitmapy zcela zbytečně několikrát opakována. Naším způsobem se snažíme „donutit“ programátora-designéra aby vytvořil dynamicky jednu bitmapu, kterou pak někde na začátku programu přiřadí všem check-boxům. Ukázkový screen-shot, obsahující 4 check-boxy se stejnou bitmapou na pozadí je realizován takto:

__fastcall TForm2::TForm2(TComponent* Owner)
: TForm(Owner)
{
m_CheckBack = new Graphics::TBitmap();
m_CheckBack->LoadFromFile("d:\\data\\grafika\\pozadí\\BD2150.bmp");
RPJImgCheckBox1->SetBackBitmap(m_CheckBack);
RPJImgCheckBox2->SetBackBitmap(m_CheckBack);
RPJImgCheckBox3->SetBackBitmap(m_CheckBack);
RPJImgCheckBox4->SetBackBitmap(m_CheckBack);
}

Samozřejmě že bitmapu by bylo možné (a v praxi lepší) uložit do zdrojů (resources) a načítat jí metodou LoadFromResourceID nebo LoadFromResourceName.

Zde si můžete stáhnout zdrojový kód komponenty img_check_box.zip

Diskuze (1) Další článek: Chyba v MP3 přehrávači iPod

Témata článku: , , , , , , , , , , , , , , , , , ,