{ :: X2CLGraphicList contains a container component for TGraphic :: descendants and a replacement for TImageList. :: :: Many thanks to Erik Stok. While I had the idea to create these components, :: he created TPngImageList and worked out many of the problems I thought we :: would face. His original (Dutch) article can be found at: :: http://www.erikstok.net/delphi/artikelen/xpicons.html :: :: Last changed: $Date$ :: Revision: $Rev$ :: Author: $Author$ } unit X2CLGraphicList; interface uses Windows, Classes, Controls, Graphics; type // Forward declarations TX2GraphicList = class; TX2GraphicContainer = class; { :$ Holds a single graphic. } TX2GraphicCollectionItem = class(TCollectionItem, IChangeNotifier) private FPicture: TPicture; procedure SetPicture(const Value: TPicture); protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef(): Integer; stdcall; function _Release(): Integer; stdcall; procedure NotifierChanged(); procedure IChangeNotifier.Changed = NotifierChanged; public constructor Create(Collection: TCollection); override; destructor Destroy(); override; published property Picture: TPicture read FPicture write SetPicture; end; { :$ Holds a collection of graphics. } TX2GraphicCollection = class(TCollection) private FContainer: TX2GraphicContainer; function GetItem(Index: Integer): TX2GraphicCollectionItem; procedure SetItem(Index: Integer; Value: TX2GraphicCollectionItem); protected procedure Notify(Item: TCollectionItem; Action: TCollectionNotification); override; procedure Update(Item: TCollectionItem); override; public constructor Create(const AContainer: TX2GraphicContainer); function Add(): TX2GraphicCollectionItem; property Items[Index: Integer]: TX2GraphicCollectionItem read GetItem write SetItem; default; end; { :$ Container object for graphics. :: TX2GraphicContainer holds all the original graphic data. Link a container :: to a TX2GraphicList to provide the graphics for various components. } TX2GraphicContainer = class(TComponent) private FGraphics: TX2GraphicCollection; FLists: TList; procedure SetGraphics(const Value: TX2GraphicCollection); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure Notify(Item: TCollectionItem; Action: TCollectionNotification); virtual; procedure Update(Item: TCollectionItem); virtual; procedure RegisterList(const AList: TX2GraphicList); procedure UnregisterList(const AList: TX2GraphicList); public constructor Create(AOwner: TComponent); override; destructor Destroy(); override; published property Graphics: TX2GraphicCollection read FGraphics write SetGraphics; end; { :$ Defines the various modes for drawing a larger image. } TX2GLStretchMode = (smCrop, smStretch); { :$ ImageList replacement for graphics. } TX2GraphicList = class(TImageList) private FBackground: TColor; FContainer: TX2GraphicContainer; FStretchMode: TX2GLStretchMode; procedure SetBackground(const Value: TColor); procedure SetContainer(const Value: TX2GraphicContainer); procedure SetStretchMode(const Value: TX2GLStretchMode); protected procedure DefineProperties(Filer: TFiler); override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure CreateImage(const AIndex: Integer; var AImage, AMask: TBitmap); virtual; procedure AddImage(const AIndex: Integer); virtual; procedure UpdateImage(const AIndex: Integer); virtual; procedure DeleteImage(const AIndex: Integer); virtual; procedure RebuildImages(); virtual; public constructor Create(AOwner: TComponent); override; destructor Destroy(); override; procedure Loaded(); override; published property Background: TColor read FBackground write SetBackground default clBtnFace; property Container: TX2GraphicContainer read FContainer write SetContainer; property StretchMode: TX2GLStretchMode read FStretchMode write SetStretchMode default smCrop; end; implementation uses Dialogs, ImgList, SysUtils; type PClass = ^TClass; PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[Word] of TRGBTriple; {=============== TX2GraphicCollectionItem Initialization ========================================} constructor TX2GraphicCollectionItem.Create; begin inherited; FPicture := TPicture.Create(); FPicture.PictureAdapter := Self; end; destructor TX2GraphicCollectionItem.Destroy; begin FreeAndNil(FPicture); inherited; end; function TX2GraphicCollectionItem.QueryInterface; begin if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE; end; function TX2GraphicCollectionItem._AddRef; begin Result := -1; end; function TX2GraphicCollectionItem._Release; begin Result := -1; end; procedure TX2GraphicCollectionItem.NotifierChanged; begin Changed(False); end; procedure TX2GraphicCollectionItem.SetPicture; begin FPicture.Assign(Value); end; {=================== TX2GraphicCollection Item Management ========================================} constructor TX2GraphicCollection.Create; begin inherited Create(TX2GraphicCollectionItem); FContainer := AContainer; end; function TX2GraphicCollection.Add; begin Result := TX2GraphicCollectionItem(inherited Add()); end; procedure TX2GraphicCollection.Notify; begin inherited; if Assigned(FContainer) then FContainer.Notify(Item, Action); end; procedure TX2GraphicCollection.Update; begin inherited; if Assigned(FContainer) then FContainer.Update(Item); end; function TX2GraphicCollection.GetItem; begin Result := TX2GraphicCollectionItem(inherited GetItem(Index)); end; procedure TX2GraphicCollection.SetItem; begin inherited SetItem(Index, Value); end; {==================== TX2GraphicContainer Initialization ========================================} constructor TX2GraphicContainer.Create; begin inherited; FGraphics := TX2GraphicCollection.Create(Self); FLists := TList.Create(); end; destructor TX2GraphicContainer.Destroy; begin FreeAndNil(FGraphics); FreeAndNil(FLists); inherited; end; procedure TX2GraphicContainer.Notification; begin if not Assigned(FLists) then exit; if Operation = opRemove then FLists.Remove(AComponent) else // In design-time, if a TX2GraphicList is added and it doesn't yet have // a container, assign ourselves to it for lazy programmers (such as me :)) if (Operation = opInsert) and (csDesigning in ComponentState) and (AComponent is TX2GraphicList) and (not Assigned(TX2GraphicList(AComponent).Container)) then TX2GraphicList(AComponent).Container := Self; inherited; end; procedure TX2GraphicContainer.Notify; var iList: Integer; begin case Action of cnAdded: for iList := FLists.Count - 1 downto 0 do TX2GraphicList(FLists[iList]).AddImage(Item.Index); cnDeleting: for iList := FLists.Count - 1 downto 0 do TX2GraphicList(FLists[iList]).DeleteImage(Item.Index); end; end; procedure TX2GraphicContainer.Update; var iList: Integer; begin if Assigned(Item) then for iList := FLists.Count - 1 downto 0 do TX2GraphicList(FLists[iList]).UpdateImage(Item.Index) else for iList := FLists.Count - 1 downto 0 do TX2GraphicList(FLists[iList]).RebuildImages(); end; procedure TX2GraphicContainer.RegisterList; begin if FLists.IndexOf(AList) = -1 then FLists.Add(AList); end; procedure TX2GraphicContainer.UnregisterList; begin FLists.Remove(AList); end; procedure TX2GraphicContainer.SetGraphics; begin FGraphics.Assign(Value); end; {========================= TX2GraphicList Initialization ========================================} constructor TX2GraphicList.Create; begin inherited; FBackground := clBtnFace; FStretchMode := smCrop; end; procedure TX2GraphicList.Loaded; begin inherited; RebuildImages(); end; destructor TX2GraphicList.Destroy; begin SetContainer(nil); inherited; end; {========================= TX2GraphicList Graphics ========================================} procedure TX2GraphicList.CreateImage; function DrawGraphic(const ADest: TCanvas; const AIndex: Integer): Boolean; var rDest: TRect; begin Result := False; if not Assigned(FContainer.Graphics[AIndex].Picture) then exit; with FContainer.Graphics[AIndex].Picture do begin if (FStretchMode = smCrop) or ((Width <= Self.Width) and (Height <= Self.Height)) then ADest.Draw(0, 0, Graphic) else begin rDest := Rect(0, 0, Width, Height); if rDest.Right > Self.Width then rDest.Right := Self.Width; if rDest.Bottom > Self.Height then rDest.Bottom := Self.Height; ADest.StretchDraw(rDest, Graphic); end; end; Result := True; end; function RGBTriple(const AColor: TColor): TRGBTriple; var cColor: Cardinal; begin cColor := ColorToRGB(AColor); with Result do begin rgbtBlue := GetBValue(cColor); rgbtGreen := GetGValue(cColor); rgbtRed := GetRValue(cColor); end; end; function SameColor(const AColor1, AColor2: TRGBTriple): Boolean; begin Result := CompareMem(@AColor1, @AColor2, SizeOf(TRGBTriple)); end; var bmpCompare: TBitmap; bOk: Boolean; cImage: TRGBTriple; cMask: TRGBTriple; iBit: Integer; iPosition: Integer; iX: Integer; iY: Integer; pCompare: PRGBTripleArray; pImage: PRGBTripleArray; pMask: PByteArray; begin // Technique used here: draw the image twice, once on the background color, // once on black. Loop through the two images, check if a pixel is the // background color on one image and black on the other; if so then it's // fully transparent. This doesn't eliminate all problems with alpha images, // but it's the best option (at least for pre-XP systems). // // Note that components using ImageList.Draw will have full alpha support, // this routine only ensures compatibility with ImageList_Draw components. // TMenu is among the first, TToolbar and similar are amongst the latter. with AImage do begin Width := Self.Width; Height := Self.Height; PixelFormat := pf24bit; with Canvas do begin Brush.Color := FBackground; FillRect(Rect(0, 0, Width, Height)); bOk := DrawGraphic(Canvas, AIndex); end; end; with AMask do begin Width := Self.Width; Height := Self.Height; PixelFormat := pf1bit; with Canvas do begin Brush.Color := clBlack; FillRect(Rect(0, 0, Width, Height)); end; end; // No point in looping through the // images if they're blank anyways... if not bOk then exit; bmpCompare := TBitmap.Create(); try with bmpCompare do begin Width := Self.Width; Height := Self.Height; PixelFormat := pf24bit; with Canvas do begin Brush.Color := clBlack; FillRect(Rect(0, 0, Width, Height)); DrawGraphic(Canvas, AIndex); end; end; cImage := RGBTriple(FBackground); cMask := RGBTriple(clBlack); for iY := 0 to AImage.Height - 1 do begin pImage := AImage.ScanLine[iY]; pCompare := bmpCompare.ScanLine[iY]; pMask := AMask.ScanLine[iY]; iPosition := 0; iBit := 128; for iX := 0 to AImage.Width - 1 do begin if iBit = 128 then pMask^[iPosition] := 0; if SameColor(pImage^[iX], cImage) and SameColor(pCompare^[iX], cMask) then begin // Transparent pixel FillChar(pImage^[iX], SizeOf(TRGBTriple), $00); pMask^[iPosition] := pMask^[iPosition] or iBit; end; iBit := iBit shr 1; if iBit < 1 then begin iBit := 128; Inc(iPosition); end; end; end; finally FreeAndNil(bmpCompare); end; end; procedure TX2GraphicList.AddImage; var bmpImage: TBitmap; bmpMask: TBitmap; begin if csLoading in ComponentState then exit; bmpImage := TBitmap.Create(); bmpMask := TBitmap.Create(); try CreateImage(AIndex, bmpImage, bmpMask); Assert(AIndex <= Self.Count, 'AAAH! Images out of sync! *panics*'); if AIndex = Self.Count then Add(bmpImage, bmpMask) else Insert(AIndex, bmpImage, bmpMask); finally FreeAndNil(bmpMask); FreeAndNil(bmpImage); end; end; procedure TX2GraphicList.UpdateImage; var bmpImage: TBitmap; bmpMask: TBitmap; begin if csLoading in ComponentState then exit; bmpImage := TBitmap.Create(); bmpMask := TBitmap.Create(); try CreateImage(AIndex, bmpImage, bmpMask); Replace(AIndex, bmpImage, bmpMask); finally FreeAndNil(bmpMask); FreeAndNil(bmpImage); end; end; procedure TX2GraphicList.DeleteImage; begin Delete(AIndex); end; procedure TX2GraphicList.RebuildImages; var iIndex: Integer; begin if (csLoading in ComponentState) or (Width = 0) or (Height = 0) then exit; Clear(); if not Assigned(FContainer) then exit; for iIndex := 0 to FContainer.Graphics.Count - 1 do AddImage(iIndex); end; {========================= TX2GraphicList Properties ========================================} procedure TX2GraphicList.DefineProperties; var pType: TClass; begin // TCustomImageList defines the Bitmap property, we don't want that // (since the ImageList will be generated from a GraphicContainer). // Erik's solution was to override Read/WriteData, but in Delphi 6 those // aren't virtual yet. Instead we skip TCustomImageList's DefineProperties. // // The trick here is to modify the ClassType so the VMT of descendants // (include ourself!) is ignored and only TComponent.DefineProperties // is called... pType := Self.ClassType; PClass(Self)^ := TComponent; try DefineProperties(Filer); finally PClass(Self)^ := pType; end; end; procedure TX2GraphicList.Notification; begin if (Operation = opRemove) and (AComponent = FContainer) then FContainer := nil; inherited; end; procedure TX2GraphicList.SetBackground; begin FBackground := Value; RebuildImages(); end; procedure TX2GraphicList.SetContainer; begin if Assigned(FContainer) then FContainer.UnregisterList(Self); FContainer := Value; if Assigned(FContainer) then FContainer.RegisterList(Self); RebuildImages(); end; procedure TX2GraphicList.SetStretchMode; begin FStretchMode := Value; RebuildImages(); end; end.