Tvorba komponent pro C++ Builder: ImageListBox

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:

Klepněte pro větší obrázek

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.

Váš názor Další článek: Podpora PHP do FrontPage

Témata článku: Software, Microsoft, Windows, Programování, Draw, Komp, TV +, Praktická realizace, Images, Komponenta, Položka, Vybrané okno, Read, Elsa, Tvor, Tvorba


Určitě si přečtěte

Dostali jste nový počítač? Tohle s ním udělejte, než ho začnete používat

Dostali jste nový počítač? Tohle s ním udělejte, než ho začnete používat

** Každý nový počítač si zaslouží počáteční péči ** Odinstalujte bloatware a nezapomeňte na vhodné nastavení ** Poradíme, jak se o počítač s Windows 10 postarat

David Polesný, Stanislav Janů | 71

David PolesnýStanislav Janů
PočítačeNotebooky
Uživatelé hlásí problémy s jednou z listopadových záplat pro Windows 10
Karel Kilián
Windows UpdateAktualizaceWindows 10
Micro:bit V2: Tuto destičku plnou čipů dokáže naprogramovat i vaše babička

Micro:bit V2: Tuto destičku plnou čipů dokáže naprogramovat i vaše babička

** Chcete se teď hned naučit programovat čipy? ** Nechcete nic instalovat a číst zdlouhavé manuály? ** Naprogramujeme si Micro:bit, který zahraje Tichou noc

Jakub Čížek | 33

Jakub Čížek
Pojďme programovat elektronikuProgramování pro děti
Vybíráme nejlepší monitory: Od úplně levných až po displeje na rozmazlování očí

Vybíráme nejlepší monitory: Od úplně levných až po displeje na rozmazlování očí

** Vybrali jsme nejlepší monitory na práci i pořádné hraní ** Nejlevnější monitor s kvalitním panelem nestojí ani tři tisíce ** Rozlišení 4K a větší obrazovka už není nedostupný luxus

David Polesný | 30

David Polesný
Monitory
Cableporn: Podívejte se na úžasná díla umělců z podnikových serveroven

Cableporn: Podívejte se na úžasná díla umělců z podnikových serveroven

** Uspořádání kabelů můžete vnímat i jako podivný druh umění ** To nejkrásnější se skrývá v datacentrech a serverovnách ** Podívejte se na skutečné „cableporn“ z optiky i kroucené dvojlinky

Vojtěch Malý | 52

Vojtěch Malý
DatacentraServery
Messenger a Instagram přicházejí v Evropě o funkce. Kvůli nové směrnici o soukromí
Vladislav Kluska
EvropaInstagramFacebook Messenger
10 míst na mapách Googlu, která nesmíte vidět. Nahradily je čtverečky

10 míst na mapách Googlu, která nesmíte vidět. Nahradily je čtverečky

** Deset míst, které nesmíte vidět ve webových mapách ** Jsou to letiště, základny i elektrárny ** Nejvíce míst tají Francie

Jakub Čížek | 21

Jakub Čížek
Mapy GoogleMapy

Aktuální číslo časopisu Computer

Jak prodloužit výdrž notebooku

Velké testy: gamepady a inkoustové tiskárny

Důkladný test Sony Playstation 5