{*****************************************************************}
{ This is a component for placing icons in the notification area  }
{ of the Windows taskbar (aka. the traybar).                      }
{                                                                 }
{ It is an expanded version of my CoolTrayIcon component, which   }
{ you will need to make this work. The expanded features allow    }
{ you to easily draw text in the tray icon.                       }
{                                                                 }
{ The component is freeware. Feel free to use and improve it.     }
{ I would be pleased to hear what you think.                      }
{                                                                 }
{ Troels Jakobsen - troels.jakobsen@gmail.com                     }
{ Copyright (c) 2006                                              }
{                                                                 }
{ Portions by Jouni Airaksinen - mintus@codefield.com             }
{*****************************************************************}

unit TextTrayIcon;

interface

uses
  CoolTrayIcon, Windows, Graphics, Classes, Controls;

type
  TOffsetOptions = class(TPersistent)
  private
    FOffsetX,
    FOffsetY,
    FLineDistance: Integer;
    FOnChange: TNotifyEvent;           // Procedure var.
    procedure SetOffsetX(Value: Integer);
    procedure SetOffsetY(Value: Integer);
    procedure SetLineDistance(Value: Integer);
  protected
    procedure Changed; dynamic;
  published
    property OffsetX: Integer read FOffsetX write SetOffsetX;
    property OffsetY: Integer read FOffsetY write SetOffsetY;
    property LineDistance: Integer read FLineDistance write SetLineDistance;
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
  end;

  TTextTrayIcon = class(TCoolTrayIcon)
  private
    FFont: TFont;
    FColor: TColor;
    FInvertTextColor: TColor;
    FBorder: Boolean;
    FBorderColor: TColor;
    FText: String;
    FTextBitmap: TBitmap;
    FOffsetOptions: TOffsetOptions;
    FBackgroundIcon: TIcon;
    procedure FontChanged(Sender: TObject);
    procedure SplitText(const Strings: TList);
    procedure OffsetOptionsChanged(OffsetOptions: TObject);
    procedure SetBackgroundIcon(Value: TIcon);
  protected
    procedure Loaded; override;
    function LoadDefaultIcon: Boolean; override;
    function LoadDefaultBackgroundIcon: Boolean; virtual;
    procedure Paint; virtual;
    procedure SetText(Value: String);
    procedure SetTextBitmap(Value: TBitmap);
    procedure SetFont(Value: TFont);
    procedure SetColor(Value: TColor);
    procedure SetBorder(Value: Boolean);
    procedure SetBorderColor(Value: TColor);
    procedure SetOffsetOptions(Value: TOffsetOptions);
    function TransparentBitmapToIcon(const Bitmap: TBitmap; const Icon: TIcon;
      MaskColor: TColor): Boolean;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Draw;
  published
    property BackgroundIcon: TIcon read FBackgroundIcon write SetBackgroundIcon;
    property Text: String read FText write SetText;
    property Font: TFont read FFont write SetFont;
    property Color: TColor read FColor write SetColor default clBtnFace;
    property Border: Boolean read FBorder write SetBorder;
    property BorderColor: TColor read FBorderColor write SetBorderColor
      default clBlack;
    property Options: TOffsetOptions read FOffsetOptions write SetOffsetOptions;
  end;


implementation

uses
  SysUtils;

{------------------- TOffsetOptions -------------------}

procedure TOffsetOptions.Changed;
begin
  if Assigned(FOnChange) then FOnChange(Self);
end;


procedure TOffsetOptions.SetOffsetX(Value: Integer);
begin
  if Value <> FOffsetX then
  begin
    FOffsetX := Value;
    Changed;
  end;
end;


procedure TOffsetOptions.SetOffsetY(Value: Integer);
begin
  if Value <> FOffsetY then
  begin
    FOffsetY := Value;
    Changed;
  end;
end;


procedure TOffsetOptions.SetLineDistance(Value: Integer);
begin
  if Value <> FLineDistance then
  begin
    FLineDistance := Value;
    Changed;
  end;
end;

{------------------- TTextTrayIcon --------------------}

constructor TTextTrayIcon.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FBackgroundIcon := TIcon.Create;
  FTextBitmap := TBitmap.Create;
  FFont := TFont.Create;
  FFont.OnChange := FontChanged;
  FColor := clBtnFace;
  FBorderColor := clBlack;
  FOffsetOptions := TOffsetOptions.Create;
  FOffsetOptions.OnChange := OffsetOptionsChanged;

  { Assign a default bg. icon if BackgroundIcon property is empty.
    This will assign a bg. icon to the component when it is created for
    the very first time. When the user assigns another icon it will not
    be overwritten next time the project loads.
    This is similar to the default Icon in parent class CoolTrayIcon. }
  { On second thought: do we really want a default bg. icon? Probably not.
    For this reason the class method LoadDefaultBackgroundIcon will
    return false. }
  if (csDesigning in ComponentState) then
    if FBackgroundIcon.Handle = 0 then
      if LoadDefaultBackgroundIcon then
      begin
        FBackgroundIcon.Handle := LoadIcon(0, IDI_WINLOGO);
        Draw;
      end;
end;


destructor TTextTrayIcon.Destroy;
begin
  try
    FFont.Free;
    FTextBitmap.Free;
    FOffsetOptions.Free;
    try
      if FBackgroundIcon <> nil then
        FBackgroundIcon.Free;
    except
      on Exception do
        // Do nothing; the background icon seems to be invalid
    end;
  finally
    inherited Destroy;
  end;
end;


procedure TTextTrayIcon.Loaded;
begin
  inherited Loaded;          // Always call inherited Loaded first
  // No extra handling needed
end;


function TTextTrayIcon.LoadDefaultIcon: Boolean;
{ We don't want a default icon, so we override this method inherited
  from CoolTrayIcon. }
begin
  Result := False;           // No thanks, no default icon
end;


function TTextTrayIcon.LoadDefaultBackgroundIcon: Boolean;
{ This method is called to determine whether to assign a default bg. icon
  to the component. Descendant classes can override the method to change
  this behavior. }
begin
  Result := False;           // No thanks, no default bg. icon
end;


procedure TTextTrayIcon.FontChanged(Sender: TObject);
{ This method is invoked when user assigns to Font (but not when Font is set
  directly to another TFont var.) }
begin
  Draw;
end;


procedure TTextTrayIcon.SetText(Value: String);
begin
  FText := Value;
  Draw;
end;


procedure TTextTrayIcon.SetTextBitmap(Value: TBitmap);
begin
  FTextBitmap := Value;      // Assign?
  Draw;
end;


procedure TTextTrayIcon.SetFont(Value: TFont);
begin
  FFont.Assign(Value);
  Draw;
end;


procedure TTextTrayIcon.SetColor(Value: TColor);
begin
  FColor := Value;
  Draw;
end;


procedure TTextTrayIcon.SetBorder(Value: Boolean);
begin
  FBorder := Value;
  Draw;
end;


procedure TTextTrayIcon.SetBorderColor(Value: TColor);
begin
  FBorderColor := Value;
  Draw;
end;


procedure TTextTrayIcon.SetOffsetOptions(Value: TOffsetOptions);
{ This method will only be invoked if the user creates a new
  TOffsetOptions object. User will probably just set the values
  of the existing TOffsetOptions object. }
begin
  FOffsetOptions.Assign(Value);
  Draw;
end;


procedure TTextTrayIcon.OffsetOptionsChanged(OffsetOptions: TObject);
{ This method will be invoked when the user changes the values of the
  existing TOffsetOptions object. }
begin
  Draw;
end;


procedure TTextTrayIcon.SetBackgroundIcon(Value: TIcon);
begin
  FBackgroundIcon.Assign(Value);
  Draw;
end;


procedure TTextTrayIcon.Draw;
var
  Ico: TIcon;
  rc: Boolean;
begin
  CycleIcons := False;       // We cannot cycle and draw at the same time
  Paint;                     // Render FTextBitmap
  Ico := TIcon.Create;
  if (Assigned(FBackgroundIcon)) and not (FBackgroundIcon.Empty) then
    // Draw text transparently on background icon
    rc := TransparentBitmapToIcon(FTextBitmap, Ico, FColor)
  else
  begin
    // Just draw text; no background icon
    if FColor <> clNone then
      FInvertTextColor := clNone;
    rc := BitmapToIcon(FTextBitmap, Ico, FInvertTextColor);
  end;

  if rc then
  begin
    Icon.Assign(Ico);
//    Refresh;                 // Always refresh after icon assignment
    Ico.Free;
  end;
end;


function TTextTrayIcon.TransparentBitmapToIcon(const Bitmap: TBitmap;
  const Icon: TIcon; MaskColor: TColor): Boolean;
{ Render an icon from a 16x16 bitmap. Return false if error.
  MaskColor is a color that will be rendered transparently. Use clNone for
  no transparency. }
var
  BitmapImageList: TImageList;
  Bmp: TBitmap;
  FInvertColor: TColor;
begin
  BitmapImageList := TImageList.CreateSize(16, 16);
  try
    Result := False;
    BitmapImageList.AddIcon(FBackgroundIcon);
    Bmp := TBitmap.Create;

    if (FColor = clNone) or (FColor = FFont.Color) then
      FInvertColor := ColorToRGB(FFont.Color) xor $00FFFFFF
    else
      FInvertColor := MaskColor;

    Bmp.Canvas.Brush.Color := FInvertColor;
    BitmapImageList.GetBitmap(0, Bmp);
    Bitmap.Transparent := True;
    Bitmap.TransParentColor := FInvertTextColor;
    Bmp.Canvas.Draw(0, 0, Bitmap);

    BitmapImageList.AddMasked(Bmp, FInvertColor);
    BitmapImageList.GetIcon(1, Icon);
    Bmp.Free;
    Result := True;
  finally
    BitmapImageList.Free;
  end;
end;


procedure TTextTrayIcon.Paint;
var
  Bitmap: TBitmap;
  Left, Top, LinesTop, LineHeight: Integer;
  Substr: PChar;
  Strings: TList;
  I: Integer;
begin
  Bitmap := TBitmap.Create;
  try
    Bitmap.Width := 16;
    Bitmap.Height := 16;
//    Bitmap.Canvas.TextFlags := 2;         // ETO_OPAQUE

    // Render background rectangle
    if (FColor = clNone) or (FColor = FFont.Color) then
      FInvertTextColor := ColorToRGB(FFont.Color) xor $00FFFFFF
    else
      FInvertTextColor := FColor;
    Bitmap.Canvas.Brush.Color := FInvertTextColor;
    Bitmap.Canvas.FillRect(Rect(0, 0, 16, 16));

    // Render text; check for line breaks
    Bitmap.Canvas.Font.Assign(FFont);
    Substr := StrPos(PChar(FText), #13);
    if Substr = nil then
    begin
      // No line breaks
      Left := (15 - Bitmap.Canvas.TextWidth(FText)) div 2;
      if FOffsetOptions <> nil then
        Left := Left + FOffsetOptions.OffsetX;
      Top := (15 - Bitmap.Canvas.TextHeight(FText)) div 2;
      if FOffsetOptions <> nil then
        Top := Top + FOffsetOptions.OffsetY;
      Bitmap.Canvas.TextOut(Left, Top, FText);
    end
    else
    begin
      // Line breaks
      Strings := TList.Create;
      SplitText(Strings);
      LineHeight := Bitmap.Canvas.TextHeight(Substr);
      if FOffsetOptions <> nil then
        LineHeight := LineHeight + FOffsetOptions.LineDistance;
      LinesTop := (15 - (LineHeight * Strings.Count)) div 2;
      if FOffsetOptions <> nil then
        LinesTop := LinesTop + FOffsetOptions.OffsetY;
      for I := 0 to Strings.Count -1 do
      begin
        Substr := Strings[I];
        Left := (15 - Bitmap.Canvas.TextWidth(Substr)) div 2;
        if FOffsetOptions <> nil then
          Left := Left + FOffsetOptions.OffsetX;
        Top := LinesTop + (LineHeight * I);
        Bitmap.Canvas.TextOut(Left, Top, Substr);
      end;
      for I := 0 to Strings.Count -1 do
        StrDispose(PChar(Strings[I]));
      Strings.Free;
    end;

    // Render border
    if FBorder then
    begin
      Bitmap.Canvas.Brush.Color := FBorderColor;
      Bitmap.Canvas.FrameRect(Rect(0, 0, 16, 16));
    end;

    // Assign the final bitmap
    FTextBitmap.Assign(Bitmap);

  finally
    Bitmap.Free;
  end;
end;


procedure TTextTrayIcon.SplitText(const Strings: TList);

  function PeekedString(S: String): String;
  var
    P: Integer;
  begin
    P := Pos(#13, S);
    if P = 0 then
      Result := S
    else
      Result := Copy(S, 1, P-1);
  end;

var
  Substr: String;
  P: Integer;
  S: PChar;
begin
  Strings.Clear;
  Substr := FText;
  repeat
    P := Pos(#13, Substr);
    if P = 0 then
    begin
      S := StrNew(PChar(Substr));
      Strings.Add(S);
    end
    else
    begin
      S := StrNew(PChar(PeekedString(Substr)));
      Strings.Add(S);
      Delete(Substr, 1, P);
    end;
  until P = 0;
end;

end.

