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í, Tvorba, Uložený vzor, Elsa, TV +, Jednoduchý trik, Komp, Region, Read, Okno, Zaoblený díl, Stejný vzor, Volaná zpráva, Button, True Color, Tvor, Komponenta, Message


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

Google dosáhl revolučního milníku v kvantové nadvládě. IBM ale nesouhlasí

Google dosáhl revolučního milníku v kvantové nadvládě. IBM ale nesouhlasí

** Google představil nový kvantový čip s 53 qubity ** Oznámil, že díky němu lidstvo poprvé dosáhlo kvantové nadvlády ** IBM toto tvrzení zlehčuje

Karel Javůrek | 15

Nejlepší notebooky do 20 000 Kč. Tipy, co se dnes vyplatí koupit

Nejlepší notebooky do 20 000 Kč. Tipy, co se dnes vyplatí koupit

** S cenou do 20 tisíc lze vybrat solidní notebook na práci i hry ** Přenosné notebooky nabídnou i kovová těla a rychlý hardware ** Na hraní se hodí více peněz, ale na použitelný základ dvacet tisíc stačí

Tomáš Holčík, David Polesný | 47

Už desítky let se pokoušíme odposlouchávat mozek. Rusům se podařil kousek, ze kterého vám spadne brada

Už desítky let se pokoušíme odposlouchávat mozek. Rusům se podařil kousek, ze kterého vám spadne brada

** K odposlechu mozků používáme EEG ** To má ale žalostné informační rozlišení ** Rusům pomohla počítačová neuronová síť

Jakub Čížek | 29

Antivir zdarma: 8 bezplatných řešení, která zatočí s havětí v počítači

Antivir zdarma: 8 bezplatných řešení, která zatočí s havětí v počítači

** Součástí Windows 10 je integrovaný antivirový program. Stačí to? ** Představíme vám sedm aplikací na boj proti virům a malwaru ** Všechny jsou k dispozici zdarma a některé ani nemusíte instalovat

Karel Kilián | 31


Aktuální číslo časopisu Computer

Megatest: 20 powerbank s USB-C

Test: mobily do 3 500 Kč

Radíme s výběrem routeru

Tipy na nejlepší vánoční dárky