Tvorba komponent pro C++ Builder ImageButton

20. listopadu 2001
Bleem skončil SDÍLET NA FACEBOOKU TWEETNOUT
V tomto článku si vytvoříme tlačítko podobné TBitButtonu, tedy zobrazující vybranou bitmapu ale mající několik vlastností navíc.
Bude umožňovat:
  • Definovat různé bitmapy (jako Graphics::Tbitmap> reprezentující 3 stavy: normální, mající fokus a stlačený.
  • Budeme moci definovat masku (černobílou bitmapu), která bude sloužit jako vzor regionu okna tlačítka. Region okna znamená, že ty části neobdélníkového okna, které jsou mimo region, nepřijímají ani zprávy myši, a jsou tedy „imunní“ vůči kliknutí.
  • Volitelně umožníme uživateli za běhu programu přesouvat tlačítko po formuláři (stlačení klávesy <Ctrl> a tažením myší.
Klepněte pro větší obrázek

Trocha teorie

Nejprve trochu teorie o regionech okna. Regiony jsou grafické objekty GDI, které mají svůj handle (typu HRGN). Definují určitou oblast (jak již název napovídá), která může být obdélníková, eliptická, tvořená obdélníkem se zaoblenými rohy nebo obecně polygonální. Pro vytváření regionů slouží několik GDI API funkcí, jako CreateRectRgn, CreateEllipticRgn, CreatePolygonRgn a další. Jejich podrobný popis naleznete v dokumentaci (nejlépe MSDN). Dále se zmíním o funkci CombineRgn:

int CombineRgn(
  HRGN hrgnDest,      // handle “cílového” regionu
  HRGN hrgnSrc1,      // handle “zdrojového” regionu
  HRGN hrgnSrc2,      // handle “zdrojového” regionu
  int fnCombineMode  // způsob kombinace
);

která umí vytvořit kombinaci dvou zdrojových regionů. Jak uvidíte, tuto funkci také v naším komponentě využijeme.

Každému oknu Windows lze přiřadit existující region funkcí SetWindowRgn:

int SetWindowRgn(
  HWND hWnd,    // handle okna
  HRGN hRgn,      // handle regionu
  BOOL bRedraw  // volba překreslení
);

Pokud má okno přiřazeno region, který se liší od obdélníku tvořícího oblast okna, pak ty části tohoto „opsaného obdélníku“, které jsou mimo oblast regionu, se navenek tváří, jako kdyby neměly s oknem nic společného. Znamená to, že do nich nelze kreslit (v handleru WM_PAINT), jsou průhledné i „průklepné“, tj. nereagují na zprávy myši. V případě našeho tlačítka to bude znamenat, že na myš bude reagovat například pouze nepravidelná oblast obrázku (bitmapy) zobrazeného na tlačítku.

Jak na to?

Vytvoříme si komponentu odvozenou od TWinControl. Přidáme si (již dříve naučeným způsobem) tyto property:

__published
//..
//….. zděděné propety
//…
// nové property
__property Graphics::TBitmap* Bitmap  =
{ read=FBitmap, write=SetBitmap };
__property Graphics::TBitmap* BitmapMask =
{ read=FBitmapMask, write=SetBitmapMask };
__property Graphics::TBitmap* BitmapFocus =
{ read=FBitmapFocus, write=SetBitmapFocus };
__property Graphics::TBitmap* BitmapClick =
{ read=FBitmapClick, write=SetBitmapClick };
__property bool UserCanDrag  =
{ read=FUserCanDrag, write=FUserCanDrag, default=false };
__property bool AutoResize =
{ read=FAutoResize, write=SetAutoResize, default = true };
__property bool UseRegion =
{ read=FUseRegion, write=SetUseRegion, default = true };

Jejich význam je asi zřejmý z názvů a předešlého úvodu. AutoResize bude znamenat, že rozměry tlačítka (tedy Width a Height) se automaticky nastaví při změně bitmapy (podle jejích rozměrů.

V konstruktoru nesmíme zapomenout vytvořit instance tříd pro uložení příslušných property a v destruktoru je opět uvolnit. Současně si nastavíme výchozí hodnoty našich property.

__fastcall TRPJImageButton::TRPJImageButton(TComponent* Owner)
: TWinControl(Owner)
{
Width = 75;
Height = 25;
Cursor = crHandPoint;
UserCanDrag = false;
AutoResize = true;
UseRegion = true;
FBitmap = new Graphics::TBitmap;
FBitmapMask = new Graphics::TBitmap;
FBitmapFocus = new Graphics::TBitmap;
FBitmapClick = new Graphics::TBitmap;
}

__fastcall TRPJImageButton::~TRPJImageButton()
{
delete Bitmap;
delete BitmapMask;
delete BitmapFocus;
delete BitmapClick;
}

Ještě znovu připomenu nutnost opravit vygenerované Setxxxxx funkce (pokud pro vytvoření property použijete ClassExplorer) našich „bitmapových“ property, kde místo přiřazení ukazatele rovnítkem použijeme metodu Assign, jak vidíte na příkladě:

void __fastcall TRPJImageButton::SetBitmap(Graphics::TBitmap* value)
{
if (FBitmap != value)
  {
FBitmap->Assign(value);
if ( FAutoResize )
MakeAutoResize();
RecreateWnd();
}
}

Zde použitá funkce MakeAutoResize je velmi jednoduchá:

void __fastcall TRPJImageButton::MakeAutoResize()
{
if ( FBitmap->Empty )
return;
Width = FBitmap->Width;
Height = FBitmap->Height;
}

Dále musíme přepsat funkce CreateParams a CreateWindowHandle, o nichž již víme více z předchozích dílů tohoto seriálu, uvedu proto jen jejich konkrétní implementaci:

//////////////////////
// Přepsané virtuální funkce

void __fastcall TRPJImageButton::CreateParams(Controls::TCreateParams& Params)
{
TWinControl::CreateParams(Params);
CreateSubClass(Params, "BUTTON");
Params.Style = Params.Style | BS_PUSHBUTTON | WS_TABSTOP;
Params.Style = Params.Style | BS_OWNERDRAW;
Params.Width = Width;
Params.Height = Height;
}

void __fastcall TRPJImageButton::CreateWindowHandle(const TCreateParams &Params)
{
TWinControl::CreateWindowHandle(Params);
if ( FUseRegion )
CreateButtonRgn();
}

Nyní se podívejme, jak vypadá (zde volaná) funkce, která vytvoří zmíněný region tlačítka z bitmapy FbitmapMask, a to tak, že části bitmapy mající bílou barvu nebudou do regionu zahrnuty.

void __fastcall TRPJImageButton::CreateButtonRgn()
{
if ( FBitmapMask->Empty )
if ( FBitmap->Empty )
return;
DeleteObject(m_hrgn);
HRGN hrgn;
COLORREF crColor;
COLORREF crPixel;
INT ConsecPix;
INT x, y;
HDC hdc;
BITMAP bitmap;
HBITMAP m_hbitmapMask;
if ( FBitmapMask->Empty )
m_hbitmapMask = FBitmap->Handle;
else
m_hbitmapMask = FBitmapMask->Handle;
hdc = CreateCompatibleDC(::GetDC(::GetDesktopWindow()));
GetObject(m_hbitmapMask, sizeof(BITMAP), &bitmap);
SelectObject(hdc, m_hbitmapMask);
m_hrgn = CreateRectRgn(0, 0, bitmap.bmWidth, bitmap.bmHeight);
for ( y = 0; y < bitmap.bmHeight; y++ )
{
crColor = GetPixel(hdc, 0, y);
ConsecPix = 1;
for ( x = 0; x < bitmap.bmWidth; x++ )
{
crPixel = GetPixel(hdc, x, y);
if ( crColor == crPixel )
ConsecPix++;
else
{
if ( crColor == 0x00FFFFFF )
{
hrgn = CreateRectRgn(x - ConsecPix, y, x, y + 1);
CombineRgn(m_hrgn, m_hrgn, hrgn, RGN_DIFF);
DeleteObject(hrgn);
}
crColor = crPixel;
ConsecPix = 1;
}
}
if ( (crColor == 0x00FFFFFF) && (ConsecPix > 0) )
{
hrgn = CreateRectRgn(x-ConsecPix, y, x, y+1);
CombineRgn(m_hrgn, m_hrgn, hrgn, RGN_DIFF);
DeleteObject(hrgn);
}
}
if ( m_hrgn != NULL )
{
SetWindowRgn(Handle, m_hrgn, TRUE);
SetWindowLongPtr(Handle, GWL_STYLE,
GetWindowLongPtr(Handle, GWL_STYLE) | WS_CLIPSIBLINGS);
}
DeleteObject(hdc);
}

Pokud je region úspěšně vytvořen, hned ho přiřadíme našemu buttonu.

Nyní ještě musíme zachytit a zpracovat dvě zprávy Windows, které náš button dostane:

  • CN_DRAWITEM (což je zpráva WM_DRAWITEM, kterou nám posílá VCL zpět buttonu
  • WM_NCHITTEST – zpráva o události myši, ve které realizujeme možnost tažení buttonu po formuláři za běhu programu.
Toto zachycení provedeme nejlépe v přepsané funkci zapouzdřující proceduru okna (mohli bychom použít i makra mapy zpráv):

/////////////////////////////////////////////////////////////////////////////
// procedura okna

void __fastcall TRPJImageButton::WndProc(Messages::TMessage &Message)
{
switch ( Message.Msg )
{
case WM_NCHITTEST:
if ( FUserCanDrag )
{
TWinControl::WndProc(Message);
if ( (Message.Result == HTCLIENT) &&
( GetAsyncKeyState(MK_LBUTTON) < 0 ) &&
( GetAsyncKeyState(VK_CONTROL) < 0 ) )
Message.Result = HTCAPTION;
return; // již nesmíme znovu volat proceduru okna!
}
break;
case CN_DRAWITEM:
MyDrawItem((LPDRAWITEMSTRUCT)Message.LParam);
break;
}
TWinControl::WndProc(Message);
}

Pro možnost tažení použijeme jednoduchý trik, spočívající v tom, že když procedura okna vrátí jako odpověď na zprávu WM_NCHITTEST hodnotu HTCAPTION, systém tím přesvědčíme, že myš je v titulkovém pruhu okna, za který lze u „běžných“ oken okno přemístit tažením. Současně musíme samozřejmě otestovat stlačení zvolené modifikační klávesy (v našem případě <Ctrl>).

Nakonec si ukážeme to základní, tedy uživatelské kreslení buttonu, jako reakci na zprávu WM_DRAWITEM, kterou nám systém zpráv VCL pošle ve formě zprávy CN_DRAWITEM. Ta má však samozřejmě zcela totožné hodnoty parametrů jako původní zpráva WM_DRAWITEM, kterou standardně dostane pouze rodičovské okno buttonu, tedy v našem případě příslušný formulář.

void __fastcall TRPJImageButton::MyDrawItem(LPDRAWITEMSTRUCT lpdis)
{
HDC hdcBmp;
hdcBmp = CreateCompatibleDC(lpdis->hDC);
if ( hdcBmp == NULL ) return;
if ( lpdis->itemState & ODS_FOCUS ) // button má fokus
{
if ( lpdis->itemState & ODS_SELECTED ) // button je stlačen
{
if ( FBitmapClick->Empty )
SelectObject(hdcBmp, FBitmap->Handle);
else
SelectObject(hdcBmp, FBitmapClick->Handle);
}
else
{
if ( FBitmapFocus->Empty )
SelectObject(hdcBmp, FBitmap->Handle);
else
SelectObject(hdcBmp, FBitmapFocus->Handle);
}
}
else
SelectObject(hdcBmp, FBitmap->Handle);
BitBlt(lpdis->hDC,
lpdis->rcItem.left,
lpdis->rcItem.top,
lpdis->rcItem.right - lpdis->rcItem.left,
lpdis->rcItem.bottom - lpdis->rcItem.top,
hdcBmp, 0, 0, SRCCOPY);
DeleteDC(hdcBmp);
}

Na závěr ještě doporučení týkající se optimalizace. Je jasné, že takto vykreslovaná okna, navíc s regiony, které jsou složitější, jsou náročná na systémové zdroje a výkon počítače. I přesto bychom se měli snažit v tomto rámci o možnou optimalizaci. Například pokud budeme mít na formuláři více buttonů, které budou mít stejné bitmapy, je třeba si uvědomit, že pokud přiřadíte stejnou bitmapu (ze souboru) v době návrhu pomocí ObjectInspectora, je to sice pohodlné, ale data této bitmapy, jejichž velikost nemusí být v případě větších true-color bitmap zanedbatelná, se ve zdrojích programu (tedy v .exe souboru) budou vyskytovat opakovaně a zbytečně zvětšovat jeho velikost. Možným řešením je v designu tyto bitmapy nastavit pouze u jednoho z buttonů a u ostatních ji pak přiřadit za běhu (nejlépe v konstruktoru formuláře). Jako příklad uvedu výpis konstruktoru formuláře, jehož screen-shot vidíte na začátku článku:

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
  RPJImageButton2->BitmapMask->Assign(RPJImageButton1->BitmapMask);
  RPJImageButton2->Bitmap->Assign(RPJImageButton1->Bitmap);
  RPJImageButton2->BitmapFocus->Assign(RPJImageButton1->BitmapFocus);
  RPJImageButton2->BitmapClick->Assign(RPJImageButton1->BitmapClick);
  RPJImageButton2->Width = RPJImageButton1->Width;
  RPJImageButton2->Height = RPJImageButton1->Height;
  RPJImageButton2->Top = RPJImageButton1->Top;
}

Zde si můžete stáhnout zdrojový kód komponenty s přibaleným .dcr souborem pro vytvoření vlastní „ikonky“ na paletě komponent: image_button.zip

Váš názor Další článek: Bleem skončil

Témata článku: Software, Windows, Programování, Jednoduchý trik, Okno, Zaoblený díl, TV +, Read, Message, Button, Elsa, Region, Tvorba, Stejný vzor, Komp, Komponenta, Uložený vzor, True Color, Volaná zpráva, Tvor


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

Na měsíc jsem opustil Google a potupně se zase vrátil zpět (komentář)

Na měsíc jsem opustil Google a potupně se zase vrátil zpět (komentář)

** Zkusil jsem se zbavit závislosti na vyhledávači od Googlu ** Jako alternativy posloužily Bing, Seznam a DuckDuckGo ** Mají své silné stránky, ale i nepřekonatelná negativa

Lukáš Václavík | 60

Co přijde po Netflixu a Amazonu? Tohle jsou streamovací služby, na které v Česku čekáme

Co přijde po Netflixu a Amazonu? Tohle jsou streamovací služby, na které v Česku čekáme

** Rozhodujete se mezi Netflixem a HBO Go? Věřte, bude hůř ** Na trhu je mnohem víc ambicióznějších streamovacích služeb ** Některé by mohly do ČR zamířit ještě letos

Lukáš Václavík | 45


Aktuální číslo časopisu Computer

Megatest: nejlepší notebooky do 20 000 Kč

Test 8 levných IP kamer

Jak vybrat bezdrátová sluchátka

Testujeme Android 11