unit USlider;

interface

uses
  Winapi.Windows,
  System.SysUtils, System.Classes, System.Generics.Collections,
  Vcl.Forms, Vcl.Controls, Vcl.ExtCtrls, Vcl.Graphics, System.Types,
  System.Math, Winapi.GDIPAPI, Winapi.GDIPOBJ, Winapi.ActiveX;

type
  TFSlider = class(TForm)
    PaintBox1: TPaintBox;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure PaintBox1Paint(Sender: TObject);
    procedure PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure FormResize(Sender: TObject);
  private
    FGdiPlusToken: ULONG_PTR; // Token GDI+ pour initialisation/fermeture
    FBitmaps: TList<TGPBitmap>; // Liste des images chargées
    FDescriptions: TList<string>; // Liste des descriptions (une par image)
    FSelectedIndex: Integer; // Index de l’image actuellement affichée

    // Charge les images depuis les ressources
    procedure LoadImagesFromResources(const Names: array of string);
    // Dessine les pastilles de navigation
    procedure DrawDots(G: TGPGraphics; Count: Integer; Selected: Integer);
  end;

var
  FSlider: TFSlider;

implementation

{$R *.dfm}

procedure TFSlider.FormCreate(Sender: TObject);
var
  StartupInput: TGdiplusStartupInput;
  Status: TStatus;
begin
  // Paramètres d’initialisation GDI+
  StartupInput.GdiplusVersion := 1;
  StartupInput.DebugEventCallback := nil;
  StartupInput.SuppressBackgroundThread := False;
  StartupInput.SuppressExternalCodecs := False;

  // Démarrage de GDI+
  FGdiPlusToken := 0;
  Status := GdiplusStartup(FGdiPlusToken, @StartupInput, nil);
  if Status <> Ok then
    raise Exception.CreateFmt('GDI+ initialization failed (code %d)',
      [Integer(Status)]);

  // Création de la liste d’images
  FBitmaps := TList<TGPBitmap>.Create;
  // Création de la liste de descriptions
  FDescriptions := TList<string>.Create;

  // Chargement des images embarquées dans les ressources
  LoadImagesFromResources(['IMG1', 'IMG2', 'IMG3', 'IMG4']);

  FDescriptions.Add('Description de l''image 1 : ...');
  FDescriptions.Add('Description de l''image 2 : ...');
  FDescriptions.Add('Description de l''image 3 : ...');
  FDescriptions.Add('Description de l''image 4 : ...');

  // Première image sélectionnée
  FSelectedIndex := 0;
end;

procedure TFSlider.FormDestroy(Sender: TObject);
var
  B: TGPBitmap;
  S: string;
begin
  // Libération des bitmaps
  for B in FBitmaps do
    B.Free;
  FBitmaps.Free;

  // Libération des descriptions
  for S in FDescriptions do;
  FDescriptions.Free;

  // Fermeture GDI+
  if FGdiPlusToken <> 0 then
    GdiplusShutdown(FGdiPlusToken);
end;

function NormalizeBitmapTo32(BmpIn: TGPBitmap): TGPBitmap;
var
  W, H: Integer;
  BmpOut: TGPBitmap;
  G: TGPGraphics;
  SrcRect, DstRect: TGPRectF;
begin
  // Normalise un bitmap en 32 bits ARGB pour un rendu optimal
  Result := nil;
  if not Assigned(BmpIn) then
    Exit;

  W := BmpIn.GetWidth;
  H := BmpIn.GetHeight;

  // Création d’un bitmap 32 bits
  BmpOut := TGPBitmap.Create(W, H, PixelFormat32bppARGB);
  if BmpOut.GetLastStatus <> Ok then
  begin
    BmpOut.Free;
    Exit;
  end;

  // Copie bitmap en 32bpp
  G := TGPGraphics.Create(BmpOut);
  try
    G.SetSmoothingMode(SmoothingModeHighQuality);
    SrcRect.X := 0;
    SrcRect.Y := 0;
    SrcRect.Width := W;
    SrcRect.Height := H;
    DstRect := SrcRect;
    G.DrawImage(BmpIn, DstRect, SrcRect.X, SrcRect.Y, SrcRect.Width,
      SrcRect.Height, UnitPixel);
  finally
    G.Free;
  end;

  Result := BmpOut;
end;

procedure TFSlider.LoadImagesFromResources(const Names: array of string);
var
  I: Integer;
  ResName: string;
  Rs: TResourceStream;
  Ms: TMemoryStream;
  Stm: IStream;
  Bmp, BmpNorm: TGPBitmap;
begin
  // Charge chaque image depuis les ressources RCDATA
  for I := Low(Names) to High(Names) do
  begin
    ResName := Names[I];
    Rs := nil;
    Ms := nil;
    try
      // Lecture de la ressource brute
      Rs := TResourceStream.Create(HInstance, ResName, RT_RCDATA);
      Ms := TMemoryStream.Create;
      Ms.CopyFrom(Rs, 0);
      Ms.Position := 0;

      // Conversion en IStream pour GDI+
      Stm := TStreamAdapter.Create(Ms, soReference);

      // Création du bitmap GDI+
      Bmp := TGPBitmap.Create(Stm);
      if (not Assigned(Bmp)) or (Bmp.GetLastStatus <> Ok) then
      begin
        Bmp.Free;
        Bmp := nil;
      end;

      // Normalisation en 32 bits
      if Assigned(Bmp) then
      begin
        BmpNorm := NormalizeBitmapTo32(Bmp);
        if Assigned(BmpNorm) then
        begin
          Bmp.Free;
          FBitmaps.Add(BmpNorm);
        end
        else
          FBitmaps.Add(Bmp);
      end;

    finally
      Rs.Free;
      Ms.Free;
    end;
  end;
end;

procedure TFSlider.PaintBox1Paint(Sender: TObject);
var
  G: TGPGraphics;
  N: Integer;
  cX, cY, scale, W, H: Single;
  Bmp: TGPBitmap;
  Dest: TGPRectF;
  SelIndex: Integer;
  AvailW, AvailH, ImgW, ImgH, ScaleW, ScaleH: Single;
  MaxScale, MinScale: Single;
  // variables pour la zone de description
  OverlayRect: TGPRectF;
  OverlayBrush: TGPSolidBrush;
  TextBrush: TGPSolidBrush;
  Font: TGPFont;
  Format: TGPStringFormat;
  DescText: string;
  FontSize: Single;
begin
  // Nombre d’images
  N := FBitmaps.Count;
  if N = 0 then
    Exit;

  // Contexte graphique GDI+
  G := TGPGraphics.Create(PaintBox1.Canvas.Handle);
  try
    G.SetSmoothingMode(SmoothingModeHighQuality);
    G.Clear(MakeColor(255, 255, 255, 255)); // Fond blanc

    // Centre du PaintBox
    cX := PaintBox1.Width * 0.5;
    cY := PaintBox1.Height * 0.5;

    // Limites de zoom
    MaxScale := 2.0;
    MinScale := 0.3;

    // Sélection sécurisée
    SelIndex := EnsureRange(FSelectedIndex, 0, N - 1);
    Bmp := FBitmaps[SelIndex];

    if Assigned(Bmp) and (Bmp.GetLastStatus = Ok) then
    begin
      // Zone disponible pour l’image
      AvailW := PaintBox1.Width * 0.85;
      AvailH := PaintBox1.Height * 0.75;

      ImgW := Bmp.GetWidth;
      ImgH := Bmp.GetHeight;

      // Calcul du meilleur scale
      ScaleW := AvailW / ImgW;
      ScaleH := AvailH / ImgH;
      scale := Min(ScaleW, ScaleH);
      scale := EnsureRange(scale, MinScale, MaxScale);

      // Dimensions finales
      W := ImgW * scale;
      H := ImgH * scale;

      // Positionnement centré
      Dest.X := cX - W * 0.5;
      Dest.Y := cY - H * 0.5 - 10;
      Dest.Width := W;
      Dest.Height := H;

      // Dessin de l’image
      G.DrawImage(Bmp, Dest);

      // Dessin de la zone de description semi-transparente en haut de l'image
      // Récupère la description correspondante si disponible
      if (SelIndex >= 0) and (SelIndex < FDescriptions.Count) then
        DescText := FDescriptions[SelIndex]
      else
        DescText := '';

      if DescText <> '' then
      begin
        // Taille de police adaptative
        FontSize := Max(10, Round(14 * (PaintBox1.Width / 800)));

        // Rectangle overlay : positionné en haut du Dest
        OverlayRect.X := Dest.X;
        OverlayRect.Y := Dest.Y;
        OverlayRect.Width := Dest.Width;
        // hauteur max 18% de l'image ou 60px
        OverlayRect.Height := Min(Dest.Height * 0.18, 60);

        // Brush semi-transparent (alpha 50 / 255)
        OverlayBrush := TGPSolidBrush.Create(MakeColor(50, 0, 0, 0));
        try
          G.FillRectangle(OverlayBrush, OverlayRect);
        finally
          OverlayBrush.Free;
        end;

        // Préparer le texte (occupant tout l'OverlayRect)
        TextBrush := TGPSolidBrush.Create(MakeColor(255, 255, 255, 255));
        // texte blanc opaque
        try
          Font := TGPFont.Create('Segoe UI', FontSize, FontStyleRegular,
            UnitPixel);
          try
            Format := TGPStringFormat.Create;
            try // alignement vertical : centré
              Format.SetAlignment(StringAlignmentCenter);
              Format.SetLineAlignment(StringAlignmentCenter);

              // Dessine le texte occupant tout l'OverlayRect (pas d'inset)
              G.DrawString(DescText, -1, Font, OverlayRect, Format, TextBrush);
            finally
              Format.Free;
            end;
          finally
            Font.Free;
          end;
        finally
          TextBrush.Free;
        end;
      end;
    end;

    // Dessin des pastilles
    DrawDots(G, N, SelIndex);

  finally
    G.Free;
  end;
end;

procedure TFSlider.DrawDots(G: TGPGraphics; Count: Integer; Selected: Integer);
var
  I: Integer;
  TotalW, StartX, DotX, DotY, R, Gap: Single;
  Brush: TGPSolidBrush;
  ColorSelected, ColorNormal: TGPColor;
  Rect: TGPRectF;
begin
  // Paramètres des pastilles
  R := 10;
  Gap := 16;

  // Largeur totale de la ligne de pastilles
  TotalW := Count * (2 * R) + (Count - 1) * Gap;

  // Position centrée
  StartX := (PaintBox1.Width - TotalW) * 0.5;
  DotY := PaintBox1.Height - 40;

  // Couleurs
  ColorSelected := MakeColor(255, 0, 122, 204);
  ColorNormal := MakeColor(200, 200, 200, 200);

  // Dessin de chaque pastille
  for I := 0 to Count - 1 do
  begin
    DotX := StartX + I * (2 * R + Gap);

    if I = Selected then
      Brush := TGPSolidBrush.Create(ColorSelected)
    else
      Brush := TGPSolidBrush.Create(ColorNormal);

    try
      Rect.X := DotX;
      Rect.Y := DotY;
      Rect.Width := 2 * R;
      Rect.Height := 2 * R;
      G.FillEllipse(Brush, Rect);
    finally
      Brush.Free;
    end;
  end;
end;

procedure TFSlider.PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  I: Integer;
  R, Gap, TotalW, StartX, DotX, DotY: Single;
begin
  // Détection du clic sur une pastille
  R := 10;
  Gap := 16;
  TotalW := FBitmaps.Count * (2 * R) + (FBitmaps.Count - 1) * Gap;
  StartX := (PaintBox1.Width - TotalW) * 0.5;
  DotY := PaintBox1.Height - 40;

  for I := 0 to FBitmaps.Count - 1 do
  begin
    DotX := StartX + I * (2 * R + Gap);

    // Test de collision avec la pastille
    if (X >= Round(DotX)) and (X <= Round(DotX + 2 * R)) and
      (Abs(Y - DotY) <= Round(2 * R)) then
    begin
      FSelectedIndex := I;
      PaintBox1.Invalidate; // Redessine
      Exit;
    end;
  end;
end;

procedure TFSlider.FormResize(Sender: TObject);
begin
  // Redessine lors du redimensionnement
  PaintBox1.Invalidate;
end;

end.
