|
XP style icons met TImageList en TBitmap
Erik Stok (aka Baldo)
Tijdens een klein onderhoud met Richard op de NLDelphi chat kwam
het uiterlijk van applicaties ter sprake. Richard had zijn applicatie
met een XP manifest en mooie xp style icons een prachtig uiterlijk
gegeven. Richard demonstreerde hoe eenvoudig dit gedaan kon worden
met een (Delphi 7) xp manifest en de juiste icons (internet).
Tijdens mijn zoektocht op het internet naar icons voor mijn eigen
applicaties kwam ik diverse mooie sets tegen, maar ik stuitte telkens
weer op het probleem dat deze libraries vaak in een bestandsformaat
worden aangeleverd waarbij vloeiende randen en schaduweffecten worden
gebruikt met behulp van zogenaamd alpha blending. En zowel TImageList
als TBitmap, die we denk ik bijna allemaal gebruiken voor onze applicaties,
ondersteunen deze faciliteit niet.
De vraag is dus: hoe kan ik TImageList, TBitmap of de controls
die ze gebruiken zo manipuleren dat alpha blending toch gebruikt
wordt? Want dat mooie effect van die vloeiende randen en schaduw
willen we natuurlijk ook allemaal wel...
Icons
Om te beginnen is er een icon library nodig waarmee gewerkt gaat
worden. Het bedrijf waar ik werk vindt de icons van Incors
een mooie set. Gelukkig hebben ze ook een gratis
set icons te downloaden waarvan in dit artikel gebruikt gemaakt
kan worden.
Er zijn natuurlijk ook icon libraries die reeds in niet alpha-blending
formaat worden geleverd, bijvoorbeeld die van Glyfx,
maar die hebben niet die vloeiende randen en schaduweffecten die
ik zo graag in mijn icons wil hebben.
De verschillen tussen beide oplossingen zijn trouwens goed zichtbaar
in onderstaand voorbeeld (en dat is dan nog zonder schaduw):
 |
 |
| Uitvergroting van een icon met
alpha blended randen. |
Uitvergroting van een icon zonder
alpha blended randen. |
Png images
De Incors icons worden geleverd in Png formaat, een formaat dat
een alpha channel ondersteunt en dus alpha blending aankan. Delphi
ondersteunt standaard geen Png, dus na de icons volgt de zoektocht
naar een library met Png support code. Google levert met de zoekterm
TPngImage direct resultaat: pngdelphi.sourceforge.net
is een gratis Png implementatie voor Delphi.
Omdat deze library zonder package wordt geleverd zal ik voor het
gemak deze library insluiten in het component package met het eindresultaat
van dit artikel.
TBitmap en TSpeedbutton
Het eerste waarmee ik aan de slag wil is een TSpeedButton. De glyph
property van dit component ondersteunt alleen maar TBitmap. Transparency
wordt bereikt door naar de kleur van de pixel linksonder in de bitmap
te kijken en alle pixels in die kleur te vervangen door de kleur
van de button achtergrond.
TBitmap aanpassen om alpha blending te bereiken haalt weinig uit,
want het bepalen van de transparantie wordt niet in de bitmap zelf
geïmplementeerd. De aanpassing zal dus in TSpeedButton zelf
gedaan moeten worden.
Een makkelijke oplossing is de bitmap niet transparant te maken,
maar de kleur van een knop, en daarop een alpha blended image te
tekenen. Dit zal niet werken in alle gevallen van transparantie
van de speedbutton, maar die voor mij uitzonderlijke gevallen laat
ik voor wat ze zijn.
TSpeedButton uitbreiden met een PngImage property is eenvoudig:
voeg een published property toe van het type TPngObject (zo heeft
de maker van de library het type helaas genoemd), maak een private
variabele aan om het image te bevatten en schrijf Get en Set procedures
om de property te benaderen.
De truuk van de oplossing zit in het omzetten van het Png image
naar een bitmap. Daarvoor maak ik een bitmap aan van 2x de breedte
en 1x de hoogte van het Png image. Er is dan ruimte voor een enabled
en een disabled versie van het Png image.
Daarna vul ik de bitmap met de kleur van een knop, teken het Png
image er alpha blended op en teken een disabled versie van het Png
image er alpha blended op. Deze bitmap wordt dan als Glyph
van de speedbutton gezet.
procedure TPngSpeedButton.CreatePngGlyph; var Bmp : TBitmap; Png : TPNGObject; begin // Maak images voor transport Bmp := TBitmap.Create; Png := TPNGObject.Create;
try
// Zety bitmap 2x de breedte van de png Bmp.Width := FPngImage.Width * 2; Bmp.Height := FPngImage.Height;
// Maak bitmap kleur van een knop Bmp.Canvas.Brush.Color := clBtnFace; Bmp.Canvas.FillRect(Rect(0, 0, Bmp.Width, Bmp.Height));
// Teken enabled png FPngImage.Draw(Bmp.Canvas, Rect(0, 0,
FPngImage.Width, FPngImage.Height));
// Maak disabled png Png.Assign(FPngImage); MakeImageHalfTransparent(Png);
// Teken disabled png Png.Draw(Bmp.Canvas, Rect(Png.Width, 0,
Bmp.Width, Png.Height));
// Assign 2 image glyph Glyph.Assign(Bmp); NumGlyphs := 2;
finally Png.Free; Bmp.Free; end;
end;
MakeImageHalfTransparent is een functie die, zoals de naam
al zegt, een Png half transparant maakt.
Een probleem bij de gekozen oplossing is echter wel dat de glyph
gedurende designtime ook wordt aangemaakt. Het Png image staat dan
3 keer in de dfm: 1 keer onder de PngImage property, 1 keer
als de linkerhelf van de Glyph bitmap en 1 keer als de rechterhelft
van de Glyph bitmap. En die laatste twee zijn niet nodig,
want tijdens runtime zal de Glyph toch opnieuw bepaald moeten
worden want clBtnFace kan per configuratie verschillen. Door code
toe te voegen aan de setter van de PngImage property
procedure TPngSpeedButton.SetPngImage(const Value: TPngObject);
begin // Zet png image FPngImage.Assign(Value);
// Forceer paint tijdens designtime, maar laat glyph niet gezet worden zodat // deze niet in de dfm geschreven wordt. In rutime, reset de glyph. if csDesigning in ComponentState then Paint else CreatePngGlyph; end;
aan de Paint method
procedure TPngSpeedButton.Paint; var PaintRect : TRect; dx : Integer; dy : Integer; Png : TPNGObject; begin inherited;
// In designtime, paint png in op een standaard manier. Niet fancy, gewoon // het image tonen if csDesigning in ComponentState then begin
Png := TPNGObject.Create;
try
Png.Assign(FPngImage);
if not Enabled then MakeImageHalfTransparent(Png);
dx := (ClientRect.Right - ClientRect.Left) - FPngImage.Width; dy := (ClientRect.Bottom - ClientRect.Top) - FPngImage.Height;
PaintRect := Rect(dx div 2, dy div 2, (dx div 2) + FPngImage.Width, (dy div 2) + FPngImage.Height);
Canvas.Brush.Color := clBtnFace; Canvas.FillRect(PaintRect);
Png.Draw(Canvas, PaintRect);
finally Png.Free; end;
end;
end;
en aan de Loaded method
procedure TPngSpeedButton.Loaded; begin inherited;
// Maak glyph during runtime if not (csDesigning in ComponentState) then CreatePngGlyph; end;
kan ervoor worden gezorgd dat de Glyph alleen tijdens runtime
bepaald wordt, terwijl tijdens designtime het Png image toch zichtbaar
is.
Het resultaat is bereikt: de Glyph wordt getekend met alpha
blending en de Png wordt maar 1 keer in de dfm opgeslagen.
TImagelist, TMainMenu en TToolbar
De truuk die bij TSpeedButton uitgehaald kan worden is niet haalbaar
bij images in TMainMenu en TToolbar. Deze halen hun images immers
uit een TImageList.
Gelukkig gebruiken TMainMenu en TToolbar een standaard method van
een TImageList om hun images te tekenen. Daar ligt dan ook de sleutel
tot de oplossing. Ik ga deze method van TImageList override en daar
de Png tekenen.
Een probleem is echter wel: waar kan ik de Pngs opslaan en
hoe zorg ik er voor dat bijvoorbeeld de Count van de ImageList toch
klopt met die van de opslag?
TPngImageCollection
Daarom heb ik de TPngImageCollection gemaakt. TPngImageCollection
is een component dat een collection van TPngImages bevat.
TPngImageCollection bevat zoals gezegd een collection van TPngImages,
dus een collection class TPngImageCollectionItems en een collection
item class TPngImageCollectionItem liggen voor de hand.
TPngImageCollectionItem heeft een property PngImage, waarin
op dezelfde manier als bij TPngSpeedButton het image wordt opgeslagen.
TPngImageList
Ook heb ik een afgeleide van TImageList gemaakt, TPngImageList,
die naar dit component verwijst. Door in deze imagelist ook interne
images aan te maken op basis van de inhoud van de png image collection,
lopen de Count en alle andere properties van de imagelist
synchroon met die van de png image collection.
Een TImageList uitrusten met een referentie property naar een TPngImageCollection
is eenvoudig: voeg een published property PngImageCollection
toe van het type TPngImageCollection, schrijf hiervoor de Get en
Set methods en klaar.
Voor het overzetten van de images uit de collection naar images
in de list is de method CopyPngs gemaakt.
{ Procedure : TPngImageList.CopyPngs Auteur : Erik Stok Doel : Kopieer png's van de PngImageCollection zodat alle properties goed blijven werken. Deze images worden nooit in de dfm opgeslagen. } procedure TPngImageList.CopyPngs(ImageList: TCustomImageList); var i : Integer; Bmp : TBitmap; Png : TPngObject; begin // Maak eerst de list schoon ImageList.Clear;
// Kopieer alleen als er images zijn if Assigned(FPngImageCollection) then begin
// Maak transfer images Png := TPngObject.Create; Bmp := TBitmap.Create;
try // Stel bitmap in Bmp.Width := ImageList.Width; Bmp.Height := ImageList.Height;
// Vul bitmap altijd met button color Bmp.Canvas.Brush.Color := clBtnFace;
// Loop door alle png images for i := 0 to FPngImageCollection.Items.Count - 1 do begin // Wis bitmap Bmp.Canvas.FillRect(Rect(0, 0, Bmp.Width, Bmp.Height));
// Dupliceer png Png.Assign(TPngImageCollectionItem(
FPngImageCollection.Items.Items[i]).PngImage);
if Assigned(Png) and (Png.Width > 0) then begin
// Indien niet enabled, maak de png dan semi-transparant if not FEnabledImages then MakeImageHalfTransparent(Png);
// Teken png op bitmap Png.Draw(Bmp.Canvas, Rect(0, 0, Bmp.Width, Bmp.Height));
end;
// Voor aan de list toe ImageList.Add(Bmp, nil); end;
finally // Ruim op Bmp.Free; Png.Free; end;
end;
end;
Deze wordt natuurlijk in de Set method van de PngImageCollection
property aangeroepen, en bij iedere update van de PngImageCollection.
Dat laatste is een minder eenvoudig principe. Er moet vanuit een
collection item een update signaal naar de collection worden gestuurd.
Dit gebeurt in de Set method van de PngImage property:
procedure TPngImageCollectionItem.SetPngImage(const Value: TPngObject); begin // Zet image FPngImage.Assign(Value);
// Forceer changed Changed(False); end;
Zodra de collection een wijziging doorkrijgt of zelf wijzigt (er
wordt een item toegevoegd of verwijderd), dan moet de PngImageCollection
waar de collection bij hoort een signaal krijgen. Dit gebeurt in
de Update method:
procedure TPngImageCollectionItems.Update(Item: TCollectionItem); begin inherited;
// Update owner indien nodig if Assigned(FPngImageCollection) then FPngImageCollection.Update; end;
Zodra de collection wordt aangemaakt zet de PngImageCollection
zich als owner van de lijst, zodat deze de update signalen kan ontvangen.
Bij het updaten van de imagelists die aan de PngImageCollection
gekoppeld zijn ligt het allemaal wat complexer. Er kunnen namelijk
meerdere PngImageLists naar dezelfde PngImageCollection refereren.
Daarom wordt in de PngImageCollection een lijst bijgehouden van
alle refererende PngImageLists. Zodra een PngImageList refereert
naar een PngImageCollection roept deze de AddList method
aan. Zodra de referentie ophoudt te bestaan roept de PngImageList
de RemoveList method aan.
procedure TPngImageList.SetPngImageCollection(const Value: TPngImageCollection); begin // Unregister bij vorige collection if Assigned(FPngImageCollection) then FPngImageCollection.RemoveList(Self);
// Stel collection in FPngImageCollection := Value;
// Zet deze lijst als update listener if Assigned(FPngImageCollection) then FPngImageCollection.AddList(Self);
// Refresh images in imagelist CopyPngs(Self); end;
{ Procedure : TPngImageCollection.AddList Auteur : Erik Stok Doel : Voeg list als listener toe } procedure TPngImageCollection.AddList(List: TPngImageList); begin if FPngImageLists.IndexOf(List) = -1 then FPngImageLists.Add(List); end;
{ Procedure : TPngImageCollection.RemoveList Auteur : Erik Stok Purpose : Wis list uit listener lijst } procedure TPngImageCollection.RemoveList(List: TPngImageList); var i : Integer; begin i := FPngImageLists.IndexOf(List); if i <> -1 then FPngImageLists.Delete(i); end;
Op deze manier krijgen alle PngImageLists die refereren naar een
een PngImageCollection een update signaal zodra er iets wijzigt.
De referentie naar de png images is geregeld, nu kan het tekenen
gebeuren. Een TImageList kent de DoDraw method waarin het
tekeken van het image gebeurt. Door die aan te passen naar een routine
die een soortgelijke actie als TPngSpeedButton.CreatePngGlyph
uithaalt kan het image worden getekend met alpha blending:
procedure TPngImageList.DoDraw(Index: Integer; Canvas: TCanvas;
X, Y: Integer; Style: Cardinal;
Enabled: Boolean);
var PngImage : TPngObject; PaintRect : TRect; begin if Assigned(FPngImageCollection) then begin // Bepaal rect PaintRect := Rect(x, y, x + Width, y + Height);
// Teken background Canvas.Brush.Style := bsClear; Canvas.FillRect(PaintRect);
// Teken transparante png daarover PngImage := TPngObject.Create;
try PngImage.Assign(
TPngImageCollectionItem(FPngImageCollection.Items.Items[Index]).PngImage);
if not Enabled then MakeImageHalfTransparent(PngImage);
PngImage.Draw(Canvas, PaintRect); finally PngImage.Free; end;
end;
end;
TToolbar
Voor TMainMenu werkt nu alles naar behoren, maar voor TToolbar
is er nog een klein probleem. Hoewel TToolbar gebruik maakt van
een routine die uiteindelijk DoDraw aanroept, wordt voor
de disabled images gebruik gemaakt van de standaard disabled image
routine van TImageList. Deze routine bakt helemaal niets van een
image met alpha blending en bovendien wil ik net als bij de TPngSpeedButton
een half transparant image.
TToolbar heeft echter wel een DisabledImages property waarmee
je naar een imagelist kunt verwijzen waaruit de disabled images
worden gehaald. Door deze te laten verwijzen naar een TPngImageList
die al zijn images disabled tekent is het probleem opgelost.
Om gebruik te kunnen maken van dezelfde PngImageCollection heb
ik aan de PngImageList een EnabledImages property toegevoegd.
Indien deze op true wordt gezet worden de images uit de PngImageCollection
enabled in de list weergegeven, als deze op false staat worden ze
dus disabled weergegeven. Dit is terug te vinden in de CopyPngs
routine van TPngImageList:
[...]
// Indien niet enabled, maak de png dan semi-transparant
if not FEnabledImages then
MakeImageHalfTransparent(Png);
[...]
Door een tweede TPngImageList aan te maken met de property EnabledImages
op false, is een TPngImageList aangemaakt die geschikt is als DisabledImages
van TToolbar.
Designtime
Alle componenten werken nu netjes, alleen moet er designtime nog
het een en ander geregeld worden om te voorkomen dat er zaken ontsporen
door het onjuist invullen van properties door de programmeur. Voor
de TPngSpeedButton heb ik de Glyph en de NumGlyphs
properties verborgen want deze worden voortaan door de button zelf
bepaald. Helaas is een geen TCustomSpeedButton waarmee dit netjes
opgelost kan worden.
Voor de TPngImageList heb ik gezorgd dat de component editor van
TImageList onderdrukt wordt want de inhoud van de imagelist wordt
geregeld door het component zelf.
Deze properties worden allemaal nog wel door het Delphi component
streaming mechanisme meegenomen, maar er zijn geen faciliteiten
om deze gemakkelijk, buiten directe invoer in de dfm, te manipuleren.
Code
De code die bij dit artikel hoort
is een zip bestand met daarin twee mappen: PngComponents en PngComponents
demo.
In de eerste map zit een projectgroup met daarin het runtime en
designtime package van de componenten (en de pngimage code). Build
beide packages en install het designtime package. Daarna zullen
onder de tab Png in het componenent pallet de drie nieuwe componenten
te vinden zijn.
In de tweede map zit een klein demo project waarin te zien is wat
het verschil is tussen de standaard TSpeedButton en de TPngSpeedbutton,
hoe je de componenten aan elkaar knoopt en hoe images eruit zijn
in enabled en disabled toestand.
De code is geschreven voor Delphi versie 7, maar zou met wat kleine
aanpassingen ook moeten werken voor Delphi 6 en 5. De code is nog
niet helemaal perfect, maar het is zeker een goede aanzet om XP
style icons in je eigen applicaties in te bouwen...
Conclusie
Hoewel Delphi standaard geen ondersteuning biedt voor XP style
icons kan er door de flexibele opzet en goed toegankelijke broncode
toch nog het een en ander bereikt worden om je applicatie die XP
look-and-feel te geven. Met deze code als basis kan voor verschillende
image formats die alpha blending ondersteunen een implementatie
worden gemaakt voor de gangbare GUI componenten met images.
|