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
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.