Tvorba komponent pro C++ Builder - vlastní Popup-menu

V tomto článku si ukážeme vytvoření komponenty odvozené od TPopupMenu, představující tedy plovoucí nabídku.
V tomto článku si ukážeme vytvoření komponenty odvozené od TPopupMenu, představující tedy plovoucí nabídku, která bude umět:
  • V levé (nebo pravé) části zobrazovat svislou bitmapu přesahující přes více položek menu, podobně jako v nabídce <start>
  • Zvolit bitmapu jako podklad pro pozadí plochy menu
  • Zvolit jinou bitmapu sloužící jako pozadí právě vybrané položky
  • Nezávisle zvolit písmo (font) pro „normální“ a vybranou položku
  • U každé položky zobrazovat obrázky (z image-listu) s možností zarovnání vpravo nebo vlevo

Jak na to?

Vytvoříme si komponentu (nazval jsem ji TRPJPopupMenu) odvozenou od „standardní“ komponenty TPopupMenu.

V našem případě se bude jednat o tzv. uživatelsky kreslené menu. Obecně to znamená, že musíme zachytit a „obsloužit“ alespoň 2 zprávy: WM_MEASUREITEM a WM_DRAWITEM. Tyto zprávy jsou v „klasickém“ programu posílány rodičovskému oknu příslušného menu. Systém VCL nám však situaci usnadňuje. V případě komponenty odvozené od TMenu máme k dispozici pro třídu TMenuItem (což jsou položky tvořící menu) události:

OnMeasureItem:

typedef void __fastcall (__closure *TMenuMeasureItemEvent)(System::TObject* Sender, Graphics::TCanvas* ACanvas, int &Width, int &Height);
__property TMenuMeasureItemEvent OnMeasureItem = {read=FOnMeasureItem, write=FOnMeasureItem};

OnDrawItem:

typedef void __fastcall (__closure *TMenuDrawItemEvent)(System::TObject* Sender, Graphics::TCanvas* ACanvas, const Windows::TRect &ARect, bool Selected);
__property TMenuDrawItemEvent OnDrawItem = {read=FOnDrawItem, write=FOnDrawItem};

OnAdvancedDrawItem:

typedef void __fastcall (__closure *TAdvancedMenuDrawItemEvent)(System::TObject* Sender, Graphics::TCanvas* ACanvas, const Windows::TRect &ARect, TOwnerDrawState State);
__property TAdvancedMenuDrawItemEvent OnAdvancedDrawItem = {read=FOnAdvancedDrawItem, write=FOnAdvancedDrawItem};

Tyto události nám zapouzdřují handlery výše zmíněných zpráv. Pomocí těchto událostí bychom mohli menu uživatelsky kreslit v příslušném programu bez použití komponenty (což je způsob, který by zkušenější programátor měl preferovat!). My však máme za cíl vytvořit komponentu „pro každého“, takže musíme veškerý sofistikovanější kód umístit přímo do komponenty.

Jedním možným řešením je vytvořit si vlastní funkce typově odpovídající výše zmíněným událostem a ve vhodném okamžiku je napojit na tyto události. Takovým vhodným okamžikem může být volání funkce Loaded. Podívejme se, jak bude vypadat tato přepsaná funkce v naší komponentě:

void __fastcall TRPJPopupMenu::Loaded()
{
  TPopupMenu::Loaded();
  m_MenuHeight = 0;
  for ( int i = 0; i < Items->Count; i++ )
  {
    if ( Items->Items[i]->OnMeasureItem == NULL )
      Items->Items[i]->OnMeasureItem = RPJMeasureItem;
    if ( Items->Items[i]->OnDrawItem == NULL )
      if ( Items->Items[i]->OnAdvancedDrawItem == NULL )
        Items->Items[i]->OnAdvancedDrawItem = RPJAdvancedDrawItem;
    if ( Items->Items[i]->Caption == "-" )
      m_MenuHeight += FSeparatorHeight;
    else
      m_MenuHeight += FItemsHeight;
  }
}

Nyní ale trochu od začátku. Do komponenty si přidáme následující property:

__published:
  __property Graphics::TBitmap* Background
      = { read=FBackground, write=SetBackground };
  __property Graphics::TBitmap* BackgroundSelected
      = { read=FBackgroundSelected, write=SetBackgroundSelected };
  __property Graphics::TBitmap* Bitmap
      = { read=FBitmap, write=SetBitmap };
  __property eBitmapAlignment BitmapAlignment
      = { read=FBitmapAlignment, write=SetBitmapAlignment };
  __property TColor Color
      = { read=FColor, write=SetColor };
  __property TColor ColorSelected
      = { read=FColorSelected, write=SetColorSelected };
  __property int CustomSlot
      = { read=FCustomSlot, write=SetCustomSlot, default=2 };
  __property TFont* Font
      = { read=FFont, write=SetFont };
  __property TFont* FontSelected
      = { read=FFontSelected, write=SetFontSelected };
  __property eImagesAlignment ImagesAlignment
      = { read=FImagesAlignment, write=SetImagesAlignment };
  __property int ItemsHeight
      = { read=FItemsHeight, write=SetItemsHeight, default=32 };
  __property int ItemsWidth
      = { read=FItemsWidth, write=SetItemsWidth, default=175 };
  __property eTextAlignment TextAlignment
      = { read=FTextAlignment, write=SetTextAlignment };
  __property int SeparatorHeight
      = { read=FSeparatorHeight, write=SetSeparatorHeight, default=5 };

Bitmap – bude bitmapa zobrazovaná na okraji přes všechny položky.

BitmapAlignment – určení, ke kterému okraji má být „zarovnána“ pruhová bitmapa.

Color, ColorSelected – barva pozadí položek, resp. barva pozadí vybrané položky

Background – bitmapa pozadí položek

BackgroundSelected – bitmapa pozadí vybrané položky

CustomSlot – „přídavná“ mezera mezi položkami

Font – písmo položek menu

FontSelected – písmo vybrané položky menu

ImagesAlignment – zarovnání obrázků zobrazovaných u jednotlivých položek menu

ItemsHeight – výška jedné položky

ItemsWidht – šířka položky menu

TextAlignment – zarovnání textů položek menu

SeparatorHeight – výška položky typu „oddělovač“

V konstruktoru si připravíme pomocné proměnné pro uložení property, od kterých je třeba vytvořit instance, a tamtéž nastavíme „nějak defaultně“ výchozí hodnoty některých property:

__fastcall TRPJPopupMenu::TRPJPopupMenu(TComponent* Owner)
  : TPopupMenu(Owner)
{
  OwnerDraw = true;
  ItemsHeight = 32;
  ItemsWidth = 175;
  CustomSlot = 2;
  SeparatorHeight = 5;
  BitmapAlignment = baLeftDown;
  Color = (TColor)GetSysColor(COLOR_WINDOW);
  ColorSelected = (TColor)GetSysColor(COLOR_HIGHLIGHT);
  FBitmap = new Graphics::TBitmap();
  FBackground = new Graphics::TBitmap();
  FBackgroundSelected = new Graphics::TBitmap();
  FFont = new TFont;
  FFontSelected = new TFont;
  TForm* fOwner = (TForm>Owner;
  FFont->Assign(fOwner->Font);
  FFontSelected->Assign(fOwner->Font);
  FFontSelected->Color =(TColor)GetSysColor(COLOR_HIGHLIGHTTEXT);
}

__fastcall TRPJPopupMenu::~TRPJPopupMenu()
{
  delete FBitmap;
  delete FBackground;
  delete FBackgroundSelected;
  delete FFont;
  delete FFontSelected;
}

Již jsme si pověděli o zprávě WM_MEASUREITEM. V této zprávě musíme systému říci, jakou výšku (popř. délku) mají mít položky našeho menu. V systému VCL to provedeme v události OnMeasureItem, na kterou jsme si napojili naši vlastní funkci. Ta vypadá následovně:

void __fastcall TRPJPopupMenu::RPJMeasureItem(TObject *Sender, TCanvas *ACanvas,
      int &Width, int &Height)
{
  TMenuItem*  CurrentItem;
  CurrentItem = (TMenuItem>Sender;
  Width = FItemsWidth;
  if ( CurrentItem->Caption ==  "-" )
    Height = FSeparatorHeight;
  else
    Height = FItemsHeight;
}

Jedná se tedy obecně o to, naplnit parametry Width a Height požadovanými hodnotami.

Nyní je před námi „pouze“ vykreslení položky menu jako reakce na zprávu WM_DRAWITEM, tedy v našem případě opět ve vlastní funkci, kterou máme navázanou na událost OnAdvancedDrawItem. V této funkci (resp. události stejného typu) máme k disposici kontext zařízení (jako TCanvas) a souřadnice (jako TRect) aktuálně kreslené položky s tím, že nejsme při kreslení vázáni tímto obdélníkem, ale můžeme při kreslení libovolné položky kreslit do celého kontextu zařízení. Kreslící funkce, tentokrát poněkud rozsáhlejší vzhledem k nutnosti výpočtů polohy v závislosti na zarovnáních, vypadá takto:

void __fastcall TRPJPopupMenu::RPJAdvancedDrawItem(TObject *Sender,
      TCanvas *ACanvas, const TRect &ARect, TOwnerDrawState State)
{
  HBRUSH hbrush;
  int xPos, yPos; // pozice bitmapy

  TMenuItem*  CurrentItem;
  CurrentItem = (TMenuItem>Sender;

  RECT rect = ARect;
  if ( FBitmap->Empty )
    goto FILLBACK;
  switch ( FBitmapAlignment )
  {
    case baLeftDown:
      xPos = 0;
      yPos = ARect.Top;
      rect.left += FBitmap->Width;
      break;
    case baRightDown:
      xPos = ARect.Right - FBitmap->Width;
      yPos = ARect.Top;
      rect.right -= FBitmap->Width;
      break;
    case baTopLeft:
      xPos = 0;
      yPos = ARect.Top;
      rect.left += FBitmap->Width;
      break;
    case baTopRight:
      xPos = ARect.Right - FBitmap->Width;
      yPos = ARect.Top;
      rect.right -= FBitmap->Width;
      break;
  }
  switch ( FBitmapAlignment )
  {
    case baLeftDown:
    case baRightDown:
      BitBlt(ACanvas->Handle,
        xPos, yPos,
        FBitmap->Width,
        FItemsHeight,
        FBitmap->Canvas->Handle,
        0,
        ARect.Top - (m_MenuHeight - FBitmap->Height),
        SRCCOPY);
      break;
    case baTopLeft:
    case baTopRight:
      BitBlt(ACanvas->Handle,
        xPos, yPos,
        FBitmap->Width,
        FItemsHeight,
        FBitmap->Canvas->Handle,
        0,
        ARect.Top,
        SRCCOPY);
      break;
    default:
      BitBlt(ACanvas->Handle,
        xPos,  yPos,
        FBitmap->Width,
        FItemsHeight,
        FBitmap->Canvas->Handle,
        0,
        ARect.Top - (m_MenuHeight - FBitmap->Height),
    SRCCOPY);
      break;
  }
  // odtud používat rect
FILLBACK:
  if ( lstrcmp(CurrentItem->Caption.c_str(), "-") == 0 )
  { // jde o separátor
    MoveToEx(ACanvas->Handle, rect.left,
      rect.top + (FSeparatorHeight / 2),
      NULL);
    LineTo(ACanvas->Handle, rect.right - 2,
      rect.top + (FSeparatorHeight / 2) );
    return;
  }
  if ( State.Contains(odSelected) )
  {
    if ( FBackgroundSelected->Empty )
      hbrush = CreateSolidBrush(FColorSelected);
    else
      hbrush = CreatePatternBrush(FBackgroundSelected->Handle);
  }
  else
  {
    if ( FBackground->Empty )
      hbrush = CreateSolidBrush(FColor);
    else
      hbrush = CreatePatternBrush(FBackground->Handle);
  }
  FillRect(ACanvas->Handle, &rect, hbrush);
  DeleteObject(hbrush);

DRAWIMAGE:
  if ( Images == NULL )
    goto DRAWTEXT;
  if ( CurrentItem->ImageIndex < 0 )
    goto DRAWTEXT;
  switch ( FImagesAlignment )
  {
    case ialLeft:
      xPos = rect.left + FCustomSlot;
      yPos = rect.top +
      (rect.bottom - rect.top - Images->Height)/2;
      rect.left += Images->Width + (2*FCustomSlot);
      break;
    case ialRight:
      xPos = rect.right - Images->Width - FCustomSlot;
      yPos = rect.top +
      (rect.bottom - rect.top - Images->Height)/2;
      rect.right -= (Images->Width + (2*FCustomSlot));
      break;
    case ialCenter:
      xPos = rect.left +
        ((rect.right - rect.left - Images->Width) / 2);
      yPos = rect.top +
      (rect.bottom - rect.top - Images->Height)/2;
      break;
  }
  ImageList_Draw((HIMAGELIST)Images->Handle,
    CurrentItem->ImageIndex,
    ACanvas->Handle,
    xPos,
    yPos,
    ILD_TRANSPARENT);

DRAWTEXT:
  SetBkMode(ACanvas->Handle, TRANSPARENT);
  if ( State.Contains(odSelected) )
  {
    SelectObject(ACanvas->Handle, FFontSelected->Handle);
    SetTextColor(ACanvas->Handle, FFontSelected->Color);
  }
  else
  {
    SelectObject(ACanvas->Handle, FFont->Handle);
    SetTextColor(ACanvas->Handle, FFont->Color);
  }
  switch ( FTextAlignment )
  {
    case talLeft:
      rect.left += FCustomSlot;
      DrawText(ACanvas->Handle,
        CurrentItem->Caption.c_str(),
        CurrentItem->Caption.Length(),
        &rect, DT_SINGLELINE | DT_VCENTER);
      break;
    case talCenter:
      DrawText(ACanvas->Handle,
        CurrentItem->Caption.c_str(),
        CurrentItem->Caption.Length(),
        &rect, DT_SINGLELINE | DT_VCENTER | DT_CENTER);
      break;
    case talRight:
      rect.right -= FCustomSlot;
      DrawText(ACanvas->Handle,
        CurrentItem->Caption.c_str(),
        CurrentItem->Caption.Length(),
        &rect, DT_SINGLELINE | DT_VCENTER | DT_RIGHT);
      break;
  }
}

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

Váš názor Další článek: Jak se zbavit červa BadTrans?

Témata článku: Software, Windows, Programování, Komp, FSE, Images, Canvas, Elsa, Menu, Read, Break, VLA, Tvorba, FCU, Case, Tvor, Položka, POP, TV +, Komponenta


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

Air Bank, Fio banka a MONETA zakládají alianci pro bankovní identitu
Jakub Čížek
BankaČeskoeGovernment
Uživatelé hlásí problémy s jednou z listopadových záplat pro Windows 10
Karel Kilián
Windows UpdateAktualizaceWindows 10
Co je to UWB? Nová technologie zastoupí Wi-Fi, Bluetooth i NFC a slibuje velké věci

Co je to UWB? Nová technologie zastoupí Wi-Fi, Bluetooth i NFC a slibuje velké věci

** V nových mobilech se začíná objevovat tajemná zkratka UWB ** Jde o další technologii, jak navzájem propojit různá zařízení ** Oproti Wi-Fi a Bluetooth má řadu výhod

Lukáš Václavík | 36

Lukáš Václavík
UWBIoTTechnologie
Jak se šíří Covid v Česku: Čerstvá data, semafor PES, mapy okresů a obcí. Každý den aktualizované grafy

Jak se šíří Covid v Česku: Čerstvá data, semafor PES, mapy okresů a obcí. Každý den aktualizované grafy

** Vývoj COVID-19 v Česku: nakažení, úmrtí, testovaní, hospitalizovaní ** Mapa podle okresů, přehled podle věku, situace v Evropě i ve světě ** Každý den aktualizované grafy a mapy

Marek Lutonský | 172

Marek Lutonský
COVID-19Koronavirus
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

Aktuální číslo časopisu Computer

Jak prodloužit výdrž notebooku

Velké testy: gamepady a inkoustové tiskárny

Důkladný test Sony Playstation 5