Animowane powiadomienia Toast dla Delphi - może się komuś przyda

Animowane powiadomienia Toast dla Delphi - może się komuś przyda
FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
7

Cześć wszystkim!

Robie sobie w Delphi aplikację i potrzebowałem fajnych powiadomień
w stylu tych z Windowsa 10/11. Skoro już to napisałem, to może się
komuś przyda.

Co potrafi:
- Wyskakujące powiadomienia w różnych kolorach (sukces, błąd, info, ostrzeżenie)
- Animacje fade-in/fade-out
- Możesz ustawić pozycję (góra/dół, lewo/środek/prawo)
- Kolejkowanie powiadomień jak ich jest więcej
- Konfigurowalne kolory, czcionki, czas wyświetlania

Jak użyć:
ShowToast('Twoja wiadomość', ttSuccess);

Kodowane pod Embarcadero® Delphi 13. Moja wersja (Embarcadero® Delphi 13 Version 37.0.57242.3601 )

Jeśli znajdziesz bugi lub będziesz miał pomysły na ulepszenia - daj znać!

Pozdro!

Pisałem pod VCL (nie testowałem na FMX).

Kopiuj

{******************************************************************************
  Toast Notification Component for Delphi VCL
  
  Author: FPERSON
  
  Description:
    A modern, customizable toast notification system for VCL applications.
    Provides non-intrusive popup notifications with support for multiple styles,
    animations, and flexible positioning. Compatible with Windows applications
    using the VCL framework.
    
  Features:
    - Multiple notification types (Success, Error, Warning, Info)
    - Smooth fade-in/fade-out animations
    - Customizable colors, fonts, and durations
    - Multiple position options (Top/Bottom, Left/Center/Right)
    - Queue management for multiple notifications
    - Shadow effects and modern UI design
    
  Usage:
    ShowToast('Message', ttSuccess);
    
  Requirements:
    - Delphi VCL (Visual Component Library)
    - Windows OS
	
------------------------------------------------------------
	
PL

  Opis:
    Nowoczesny, konfigurowalny system powiadomień toast dla aplikacji VCL.
    Zapewnia dyskretne wyskakujące powiadomienia z obsługą wielu stylów,
    animacji i elastycznego pozycjonowania. Kompatybilny z aplikacjami
    Windows wykorzystującymi framework VCL.
    
  Funkcje:
    - Wiele typów powiadomień (Sukces, Błąd, Ostrzeżenie, Info)
    - Płynne animacje pojawiania i zanikania
    - Konfigurowalne kolory, czcionki i czas wyświetlania
    - Wiele opcji pozycjonowania (Góra/Dół, Lewo/Środek/Prawo)
    - Zarządzanie kolejką dla wielu powiadomień
    - Efekty cienia i nowoczesny design
    
  Użycie:
    ShowToast('Wiadomość', ttSuccess);
    
  Wymagania:
    - Delphi VCL (Visual Component Library)
    - System operacyjny Windows
	
******************************************************************************}

unit ToastNotification;

interface

uses
  Winapi.Windows, Winapi.Messages, Vcl.ExtCtrls, Vcl.Controls, Vcl.Forms,
  Vcl.Graphics, System.Classes, System.SysUtils, System.Generics.Collections;

type
  { TToastPosition - Określa gdzie na ekranie ma pojawić się toast }
  TToastPosition = (tpBottom, tpTop, tpCenter);

  { TToastSettings - Rekord z konfiguracją wyglądu i zachowania toastu
    Pozwala na pełną personalizację: kolory, rozmiar, czas trwania, prędkość animacji i pozycję }
  TToastSettings = record
    BackgroundColor: TColor;  // Kolor tła toastu
    TextColor: TColor;        // Kolor tekstu
    FontSize: Integer;        // Rozmiar czcionki
    FontStyle: TFontStyles;   // Styl czcionki (pogrubienie, kursywa, etc.)
    Width: Integer;           // Szerokość toastu w pikselach
    Height: Integer;          // Wysokość toastu w pikselach
    Duration: Integer;        // Czas wyświetlania w milisekundach (2000 = 2 sekundy)
    AnimationSpeed: Integer;  // Prędkość animacji (ms między klatkami animacji)
    Position: TToastPosition; // Pozycja na ekranie (dół, góra, środek)
  end;

  { TToast - Klasa zarządzająca wyświetlaniem animowanych powiadomień toast
    DZIAŁANIE:
    1. Toast pojawia się z animacją wjazdu (20 klatek)
    2. Pozostaje widoczny przez określony czas (Duration)
    3. Zjeżdża z animacją wyjazdu (20 klatek)
    4. Automatycznie się niszczy po zakończeniu

    STACK:
    - Tosty wyświetlają się jeden nad drugim bez nakładania
    - Każdy nowy toast pojawia się nad poprzednim (dla tpBottom) lub pod (dla tpTop)
    - Automatyczne przesuwanie gdy toast znika

    UŻYCIE:
    TToast.Show(Self, 'Wiadomość'); // Domyślne ustawienia
    TToast.Show(Self, 'Sukces!', TToast.SuccessSettings); // Predefiniowany styl

    FUNKCJE ANIMACJI:
    - Wjazd: Płynne pojawienie się z pozycji startowej do docelowej
    - Czekanie: Toast pozostaje nieruchomy przez czas Duration
    - Zjazd: Płynne zniknięcie w kierunku zależnym od pozycji }
  TToast = class
  private
    FPanel: TPanel;          // Panel wyświetlający toast
    FTimer: TTimer;          // Timer sterujący animacją
    FStep: Integer;          // Aktualny krok animacji (0..TotalSteps)
    FShouldFree: Boolean;    // Flaga wskazująca czy obiekt powinien się zwolnić
    FSettings: TToastSettings; // Ustawienia toastu
    FTargetY: Integer;       // Docelowa pozycja Y (po zakończeniu wjazdu)
    FStartY: Integer;        // Początkowa pozycja Y (przed animacją wjazdu)
    FOwner: TComponent;      // Zapamiętany owner dla stacku

    class var FActiveToasts: TObjectList<TToast>;  // Lista aktywnych toastów (stack)

    // Metody prywatne
    procedure OnTimer(Sender: TObject);       // Główna pętla animacji
    procedure AnimateEntrance(Parent: TWinControl); // Animacja wjazdu
    procedure AnimateExit(Parent: TWinControl);     // Animacja zjazdu
    procedure FreeResources;                  // Bezpieczne zwolnienie zasobów
    procedure CalculatePosition(Parent: TWinControl); // Oblicza pozycję toastu w stacku
    procedure FinishToast;                    // Kończy animację i zwalnia obiekt

    class procedure UpdateToastPositions;     // Aktualizuje pozycje wszystkich toastów w stacku

  public
    // Konstruktory
    constructor Create(AOwner: TComponent; const Msg: string); overload;
    constructor Create(AOwner: TComponent; const Msg: string;
      const Settings: TToastSettings); overload;
    destructor Destroy; override;

    // Metody statyczne do wyświetlania toastu
    class procedure Show(AOwner: TComponent; const Msg: string); overload;
    class procedure Show(AOwner: TComponent; const Msg: string;
      const Settings: TToastSettings); overload;

    // Predefiniowane ustawienia dla różnych typów komunikatów
    class function DefaultSettings: TToastSettings; // Domyślne (szary)
    class function SuccessSettings: TToastSettings; // Sukces (zielony)
    class function ErrorSettings: TToastSettings;   // Błąd (czerwony)
    class function WarningSettings: TToastSettings; // Ostrzeżenie (pomarańczowy)
    class function InfoSettings: TToastSettings;    // Informacja (niebieski)

    // Konstruktory/destruktory klasowe
    class constructor Create;
    class destructor Destroy;
  end;

var
  DefaultToastSettings: TToastSettings; // Globalne domyślne ustawienia

implementation

{ TToast }

// ============================================================================
// INICJALIZACJA KLASOWA
// ============================================================================

{ Inicjalizacja statycznych pól klasy }
class constructor TToast.Create;
begin
  FActiveToasts := TObjectList<TToast>.Create(False); // False = nie zwalnia automatycznie
end;

{ Czyszczenie statycznych pól klasy }
class destructor TToast.Destroy;
begin
  FActiveToasts.Free;
end;

// ============================================================================
// INICJALIZACJA DOMYŚLNYCH USTAWIEŃ
// ============================================================================

{ Inicjalizuje globalne domyślne ustawienia toastu.
  Te ustawienia są używane gdy wywołujemy TToast.Show bez podania Settings. }
procedure InitializeDefaultSettings;
begin
  with DefaultToastSettings do
  begin
    BackgroundColor := $00333333; // Ciemny szary (RGB: 51, 51, 51)
    TextColor := clWhite;         // Biały tekst
    FontSize := 10;               // Rozmiar czcionki 10
    FontStyle := [fsBold];        // Pogrubiony tekst
    Width := 300;                 // Szerokość 300px
    Height := 45;                 // Wysokość 45px
    Duration := 2000;             // Czas wyświetlania: 2 sekundy
    AnimationSpeed := 15;         // Animacja: klatka co 15ms (~66 FPS)
    Position := tpBottom;         // Pozycja: na dole ekranu
  end;
end;

// ============================================================================
// PREDEFINIOWANE USTAWIENIA
// ============================================================================

{ Zwraca domyślne ustawienia toastu }
class function TToast.DefaultSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
end;

{ Ustawienia dla komunikatów sukcesu - zielone tło }
class function TToast.SuccessSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $004CAF50; // Zielony Material Design
  Result.TextColor := clWhite;
end;

{ Ustawienia dla komunikatów błędu - czerwone tło }
class function TToast.ErrorSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $00F44336; // Czerwony Material Design
  Result.TextColor := clWhite;
end;

{ Ustawienia dla komunikatów ostrzeżenia - pomarańczowe tło }
class function TToast.WarningSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $00FF9800; // Pomarańczowy Material Design
  Result.TextColor := clBlack;         // Czarny tekst dla lepszej czytelności
end;

{ Ustawienia dla komunikatów informacyjnych - niebieskie tło }
class function TToast.InfoSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $002196F3; // Niebieski Material Design
  Result.TextColor := clWhite;
end;

// ============================================================================
// PUBLICZNE METODY STATYCZNE
// ============================================================================

{ Wyświetla toast z domyślnymi ustawieniami.
  @param AOwner - Komponent właściciel (okno nadrzędne)
  @param Msg - Tekst do wyświetlenia }
class procedure TToast.Show(AOwner: TComponent; const Msg: string);
begin
  TToast.Create(AOwner, Msg);
end;

{ Wyświetla toast z niestandardowymi ustawieniami.
  @param AOwner - Komponent właściciel
  @param Msg - Tekst do wyświetlenia
  @param Settings - Niestandardowe ustawienia wyglądu i zachowania }
class procedure TToast.Show(AOwner: TComponent; const Msg: string;
  const Settings: TToastSettings);
begin
  TToast.Create(AOwner, Msg, Settings);
end;

// ============================================================================
// KONSTRUKTORY I DESTRUKTOR
// ============================================================================

{ Konstruktor z domyślnymi ustawieniami.
  Przekierowuje do konstruktora z niestandardowymi ustawieniami. }
constructor TToast.Create(AOwner: TComponent; const Msg: string);
begin
  Create(AOwner, Msg, DefaultToastSettings);
end;

{ Główny konstruktor tworzący i konfigurujący toast.
  KROKI TWORZENIA:
  1. Określa okno nadrzędne (ParentControl)
  2. Tworzy panel toastu z odpowiednimi właściwościami
  3. Dodaje toast do listy aktywnych (stack)
  4. Oblicza pozycję startową i docelową z uwzględnieniem innych toastów
  5. Uruchamia timer animacji }
constructor TToast.Create(AOwner: TComponent; const Msg: string;
  const Settings: TToastSettings);
var
  ParentControl: TWinControl;
begin
  inherited Create;

  FSettings := Settings;
  FShouldFree := False;
  FOwner := AOwner;

  // Określamy okno nadrzędne dla toastu
  if AOwner is TWinControl then
    ParentControl := TWinControl(AOwner)
  else
    ParentControl := Application.MainForm;

  // Tworzymy panel toastu
  FPanel := TPanel.Create(nil);
  try
    FPanel.Parent := ParentControl;

    // Konfiguracja wyglądu panelu
    FPanel.Caption := Msg;
    FPanel.Color := FSettings.BackgroundColor;
    FPanel.Font.Color := FSettings.TextColor;
    FPanel.Font.Size := FSettings.FontSize;
    FPanel.Font.Style := FSettings.FontStyle;
    FPanel.BevelOuter := bvNone;          // Bez obramowania
    FPanel.Width := FSettings.Width;
    FPanel.Height := FSettings.Height;
    FPanel.DoubleBuffered := True;        // Zapobiega migotaniu

    // Optymalizacje wydajnościowe
    FPanel.ParentBackground := False;
    FPanel.ParentDoubleBuffered := False;

    // WAŻNE: Dodaj toast do listy PRZED obliczeniem pozycji
    FActiveToasts.Add(Self);

    // Oblicz i ustaw pozycję (z uwzględnieniem stacku)
    CalculatePosition(ParentControl);

    // Pokazujemy i ustawiamy na wierzchu
    FPanel.Visible := True;
    FPanel.BringToFront;

    // Inicjalizacja animacji
    FStep := 0;
    FTimer := TTimer.Create(nil);
    FTimer.Interval := FSettings.AnimationSpeed;
    FTimer.OnTimer := OnTimer;
    FTimer.Enabled := True;
  except
    // W razie błędu bezpiecznie zwalniamy zasoby
    FActiveToasts.Remove(Self);
    FreeResources;
    raise;
  end;
end;

{ Destruktor - bezpiecznie zwalnia wszystkie zasoby }
destructor TToast.Destroy;
begin
  FreeResources;
  inherited;
end;

// ============================================================================
// METODY PRYWATNE - ZARZĄDZANIE STACKIEM
// ============================================================================

{ Aktualizuje pozycje wszystkich aktywnych toastów w stacku.
  Wywoływane gdy toast znika - pozostałe tosty płynnie zajmują jego miejsce }
class procedure TToast.UpdateToastPositions;
var
  I, Offset: Integer;
  Toast: TToast;
  ParentControl: TWinControl;
begin
  if FActiveToasts.Count = 0 then
    Exit;

  for I := 0 to FActiveToasts.Count - 1 do
  begin
    Toast := FActiveToasts[I];

    if not Assigned(Toast.FPanel) or not Assigned(Toast.FPanel.Parent) then
      Continue;

    ParentControl := TWinControl(Toast.FPanel.Parent);

    // Oblicz offset na podstawie wcześniejszych toastów
    Offset := 0;
    case Toast.FSettings.Position of
      tpBottom:
        begin
          // Dla każdego wcześniejszego toastu dodaj jego wysokość + odstęp
          var J: Integer;
          for J := 0 to I - 1 do
          begin
            if Assigned(FActiveToasts[J].FPanel) then
              Inc(Offset, FActiveToasts[J].FPanel.Height + 10);
          end;

          // Aktualizuj docelową pozycję (ale tylko jeśli toast już wjechał)
          if Toast.FStep > 20 then
          begin
            Toast.FTargetY := ParentControl.ClientHeight - Toast.FPanel.Height - 20 - Offset;
            Toast.FPanel.Top := Toast.FTargetY;
          end;
        end;

      tpTop:
        begin
          // Dla tpTop tosty układają się od góry w dół
          var J: Integer;
          for J := 0 to I - 1 do
          begin
            if Assigned(FActiveToasts[J].FPanel) then
              Inc(Offset, FActiveToasts[J].FPanel.Height + 10);
          end;

          if Toast.FStep > 20 then
          begin
            Toast.FTargetY := 20 + Offset;
            Toast.FPanel.Top := Toast.FTargetY;
          end;
        end;

      tpCenter:
        begin
          // Dla center każdy toast ma swoją pozycję z offsetem
          var J: Integer;
          for J := 0 to I - 1 do
          begin
            if Assigned(FActiveToasts[J].FPanel) then
              Inc(Offset, FActiveToasts[J].FPanel.Height + 10);
          end;

          if Toast.FStep > 20 then
          begin
            Toast.FTargetY := (ParentControl.ClientHeight - Toast.FPanel.Height) div 2 - Offset;
            Toast.FPanel.Top := Toast.FTargetY;
          end;
        end;
    end;
  end;
end;

// ============================================================================
// METODY PRYWATNE - LOGIKA ANIMACJI
// ============================================================================

{ Oblicza pozycję startową i docelową toastu w zależności od ustawień i stacku.
  Uwzględnia pozycję innych aktywnych toastów aby uniknąć nakładania }
procedure TToast.CalculatePosition(Parent: TWinControl);
var
  I, Offset: Integer;
begin
  // Pozycja pozioma - zawsze wyśrodkowana
  FPanel.Left := (Parent.ClientWidth - FPanel.Width) div 2;

  // Oblicz offset na podstawie innych aktywnych toastów
  Offset := 0;
  for I := 0 to FActiveToasts.Count - 1 do
  begin
    if FActiveToasts[I] <> Self then
    begin
      // Sprawdź czy toast ma ten sam FOwner i Position
      if (FActiveToasts[I].FOwner = FOwner) and
         (FActiveToasts[I].FSettings.Position = FSettings.Position) and
         Assigned(FActiveToasts[I].FPanel) then
      begin
        Inc(Offset, FActiveToasts[I].FPanel.Height + 10); // 10px odstępu między toastami
      end;
    end;
  end;

  // Pozycja pionowa zależna od ustawień i offsetu od innych toastów
  case FSettings.Position of
    tpBottom:
      begin
        FStartY := Parent.ClientHeight;                     // Start: poniżej ekranu
        FTargetY := Parent.ClientHeight - FPanel.Height - 20 - Offset; // Cel: 20px od dołu + offset
      end;
    tpTop:
      begin
        FStartY := -FPanel.Height;                         // Start: powyżej ekranu
        FTargetY := 20 + Offset;                           // Cel: 20px od góry + offset
      end;
    tpCenter:
      begin
        FStartY := Parent.ClientHeight;                    // Start: poniżej ekranu
        FTargetY := (Parent.ClientHeight - FPanel.Height) div 2 - Offset; // Cel: środek + offset
      end;
  end;

  // Ustaw początkową pozycję
  FPanel.Top := FStartY;
end;

{ Główna pętla animacji - wywoływana przez timer co AnimationSpeed ms.
  Fazy animacji:
  1. FStep 0-20: Animacja wjazdu (AnimateEntrance)
  2. FStep 21-(TotalSteps-20): Czekanie (toast nieruchomy)
  3. FStep (TotalSteps-19)-TotalSteps: Animacja zjazdu (AnimateExit) }
procedure TToast.OnTimer(Sender: TObject);
var
  P: TWinControl;
  TotalSteps: Integer;
begin
  if FShouldFree then
    Exit;

  // Bezpieczeństwo - sprawdzamy czy komponenty jeszcze istnieją
  if not Assigned(FPanel) or not Assigned(FPanel.Parent) then
  begin
    if Assigned(FTimer) then
      FTimer.Enabled := False;
    FreeResources;
    Exit;
  end;

  P := TWinControl(FPanel.Parent);
  Inc(FStep);

  // Oblicz całkowitą liczbę kroków animacji
  TotalSteps := (FSettings.Duration div FSettings.AnimationSpeed) + 40;

  // FAZA 1: ANIMACJA WJAZDU (pierwsze 20 kroków)
  if FStep <= 20 then
  begin
    AnimateEntrance(P);
  end
  // FAZA 2: CZEKANIE (toast widoczny bez ruchu)
  else if FStep <= TotalSteps - 20 then
  begin
    // Toast pozostaje w miejscu - nic nie robimy
  end
  // FAZA 3: ANIMACJA ZJAZDU (ostatnie 20 kroków)
  else
  begin
    AnimateExit(P);

    // Sprawdzamy czy toast zjechał poza ekran
    case FSettings.Position of
      tpBottom:
        if FPanel.Top >= P.ClientHeight then FinishToast;
      tpTop:
        if FPanel.Top <= -FPanel.Height then FinishToast;
      tpCenter:
        if FPanel.Top >= P.ClientHeight then FinishToast;
    end;
  end;
end;

{ Animacja wjazdu toastu na ekran.
  Używa interpolacji liniowej do płynnego przejścia z FStartY do FTargetY. }
procedure TToast.AnimateEntrance(Parent: TWinControl);
var
  Progress: Double;
begin
  // Progress rośnie od 0 do 1 w ciągu 20 kroków
  Progress := FStep / 20;

  // Interpolacja liniowa: pozycja = start + (cel - start) * progress
  FPanel.Top := Round(FStartY + (FTargetY - FStartY) * Progress);
end;

{ Animacja zjazdu toastu z ekranu.
  Określa kierunek zjazdu w zależności od pozycji toastu. }
procedure TToast.AnimateExit(Parent: TWinControl);
var
  ExitStep: Integer;
  Progress: Double;
  ExitStartY, ExitTargetY: Integer;
begin
  // Numer kroku w fazie zjazdu (0-20)
  ExitStep := FStep - (FSettings.Duration div FSettings.AnimationSpeed + 20);
  Progress := ExitStep / 20;

  // Określamy kierunek zjazdu
  case FSettings.Position of
    tpBottom:
      begin
        ExitStartY := FTargetY;            // Start zjazdu: pozycja docelowa
        ExitTargetY := Parent.ClientHeight; // Koniec zjazdu: poniżej ekranu
      end;
    tpTop:
      begin
        ExitStartY := FTargetY;            // Start zjazdu: pozycja docelowa
        ExitTargetY := -FPanel.Height;     // Koniec zjazdu: powyżej ekranu
      end;
    tpCenter:
      begin
        ExitStartY := FTargetY;            // Start zjazdu: pozycja docelowa
        ExitTargetY := Parent.ClientHeight; // Koniec zjazdu: poniżej ekranu
      end;
  else
    // Domyślnie: zjazd w dół
    ExitStartY := FTargetY;
    ExitTargetY := Parent.ClientHeight;
  end;

  // Interpolacja liniowa dla zjazdu
  FPanel.Top := Round(ExitStartY + (ExitTargetY - ExitStartY) * Progress);
end;

{ Kończy animację i inicjuje proces zwalniania obiektu.
  Usuwa toast ze stacku i aktualizuje pozycje pozostałych toastów }
procedure TToast.FinishToast;
begin
  if Assigned(FTimer) then
    FTimer.Enabled := False;

  FShouldFree := True;

  // Usuń ze stacku
  FActiveToasts.Remove(Self);

  // Aktualizuj pozycje pozostałych toastów
  UpdateToastPositions;

  // Bezpieczne zwolnienie przez kolejkę wiadomości
  PostMessage(Application.MainForm.Handle, WM_NULL, 0, 0);
  FreeResources;
end;

{ Bezpiecznie zwalnia wszystkie zasoby (timer i panel). }
procedure TToast.FreeResources;
begin
  // Zatrzymaj i zwolnij timer
  if Assigned(FTimer) then
  begin
    FTimer.Enabled := False;
    FreeAndNil(FTimer);
  end;

  // Ukryj i zwolnij panel
  if Assigned(FPanel) then
  begin
    FPanel.Visible := False;
    FreeAndNil(FPanel);
  end;
end;

// ============================================================================
// INICJALIZACJA GLOBALNA
// ============================================================================

initialization
  InitializeDefaultSettings;

end.


FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
2

Przykład użycia

Kopiuj

unit ToastExamples;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, ToastNotification;

type
  TMainForm = class(TForm)
    btnDone: TButton;
    lblTest: TLabel;
    btnError: TButton;
    btnWarning: TButton;
    btnInfo: TButton;
    btnCustom: TButton;
    btnMultiple: TButton;
    btnPositions: TButton;
    btnLargeToast: TButton;
    procedure btnDoneClick(Sender: TObject);
    procedure btnErrorClick(Sender: TObject);
    procedure btnWarningClick(Sender: TObject);
    procedure btnInfoClick(Sender: TObject);
    procedure btnCustomClick(Sender: TObject);
    procedure btnMultipleClick(Sender: TObject);
    procedure btnPositionsClick(Sender: TObject);
    procedure btnLargeToastClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

// ============================================================================
// Toast z niestandardowymi ustawieniami
// ============================================================================
procedure TMainForm.btnCustomClick(Sender: TObject);
begin
  // Tworzymy własne ustawienia od zera
  var CustomSettings := TToast.DefaultSettings; // Zaczynamy od domyślnych

  // Modyfikujemy to co chcemy
  CustomSettings.BackgroundColor := $00FF00FF; // Fioletowy (Magenta)
  CustomSettings.TextColor := clWhite;         // Bialy tekst
  CustomSettings.FontSize := 12;               // Większa czcionka
  CustomSettings.Width := 400;                 // Szerszy toast
  CustomSettings.Height := 60;                 // Wyższy toast
  CustomSettings.Duration := 5000;             // 5 sekund (zamiast 2)
  CustomSettings.Position := tpTop;            // Na górze ekranu

  TToast.Show(Self,
    'Toast z niestandardowymi ustawieniami!',
    CustomSettings
  );
end;

// ============================================================================
// Toast z komunikatem sukcesu (zielony)
// ============================================================================
procedure TMainForm.btnDoneClick(Sender: TObject);
begin
  TToast.Show(Self,
  'Operacja wykonana pomyślnie!',
  TToast.SuccessSettings
  );
end;
// ============================================================================
// Toast z komunikatem błędu (czerwony)
// ============================================================================
procedure TMainForm.btnErrorClick(Sender: TObject);
begin
  TToast.Show(Self,
    'Wystąpił błąd podczas wykonywania operacji!',
    TToast.ErrorSettings
  );
end;

// ============================================================================
// Toast informacyjny (niebieski)
// ============================================================================
procedure TMainForm.btnInfoClick(Sender: TObject);
begin
  TToast.Show(Self,
    'To jest informacja dla użytkownika.',
    TToast.InfoSettings
  );
end;

// ============================================================================
// Ustawiamy dłuższy czas dla Toast'a
// ===========================================================================
procedure TMainForm.btnLargeToastClick(Sender: TObject);
begin
  var Settings := TToast.InfoSettings;
  Settings.Duration := 8000; // Ostrzeżenie pokazujemy dłużej (8 sekundy)

  TToast.Show(Self,
    'Toast z 8 sekundowym wyświetleniem',
    Settings
  );
end;


// ============================================================================
// Wiele toastów jednocześnie (stack)
// ============================================================================
procedure TMainForm.btnMultipleClick(Sender: TObject);
begin
  // Tosty pojawią się jeden nad drugim
  TToast.Show(Self, 'Pierwszy toast', TToast.InfoSettings);
  TToast.Show(Self, 'Drugi toast', TToast.SuccessSettings);
  TToast.Show(Self, 'Trzeci toast', TToast.WarningSettings);
end;

// ============================================================================
// Różne pozycje toastów
// ============================================================================
procedure TMainForm.btnPositionsClick(Sender: TObject);
var
  TopSettings, CenterSettings, BottomSettings: TToastSettings;
begin
  // Toast na górze
  TopSettings := TToast.InfoSettings;
  TopSettings.Position := tpTop;
  TToast.Show(Self, 'Toast na górze ekranu', TopSettings);

  // Toast w środku
  CenterSettings := TToast.WarningSettings;
  CenterSettings.Position := tpCenter;
  TToast.Show(Self, 'Toast w środku ekranu', CenterSettings);

  // Toast na dole (domyślna pozycja)
  BottomSettings := TToast.SuccessSettings;
  BottomSettings.Position := tpBottom;
  TToast.Show(Self, 'Toast na dole ekranu', BottomSettings);
end;

// ============================================================================
// Toast z komunikatem błędu (czerwony)
// ============================================================================
procedure TMainForm.btnWarningClick(Sender: TObject);
begin
  TToast.Show(Self,
    'Uwaga! To jest ostrzeżenie.',
    TToast.WarningSettings
  );
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Self.Position := poScreenCenter;
  Self.Color := clWhite;
end;

end.

I1
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 205
0
first_person napisał(a):

Author: FPERSON

Author: FPERSON in deep collaboration with AI :)

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
2

@itou123
Spokojnie, nadal potrafię pisać kod bez wsparcia robotów.
Z drugiej strony jako narzędzie "rarytas". Podpowie, sprawdzi błędy, ale to ja decyduję 😄

Co do kodu mam kilka pomysłow na ulepszenie:

Zaokrąglone rogi + cienie (Modern UI)
Używając GDI+ lub Skia4Delphi do zaokrąglonych rogów
Efekt rozmycia/cienia pod toastem
Ikony z Segoe MDL2 Assets lub Material Icons

Transparentne formularze (Glassmorphism)
TForm zamiast TPanel z AlphaBlend i AlphaBlendValue
Efekt przezroczystości/blur
Bardziej nowoczesny wygląd jak w Windows 11

Dodatkowo
Ikony statusu (opcjonalnie z domyślną wartościa)
Lepsze animacje (ease-in-out)
Opcjonalny progress bar
Obsługa kliknięcia

I1
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 205
0
first_person napisał(a):

Spokojnie, nadal potrafię pisać kod bez wsparcia robotów.
Z drugiej strony jako narzędzie "rarytas". Podpowie, sprawdzi błędy, ale to ja decyduję 😄

Chodzi, że kod jest bardzo rozwlekły przez sposób formatowania i pisania komentarzy przez AI, a te rzeczy + to, ze bardzo widzać udział AI (czyli w domyśle człowiek mało czasu poświęcił) zniechęcają do poświęcenia swojego czasu by to np. przeanalizować.
AI może pisać fajny kod i używanie AI samo w sobie nie jest złe, ale trzeba go wspomóc np. plikiem projektu z wytycznymi dot. pisania kodu i ciągle te wytyczne aktualizować, aż osiągnie zadowalający efekt.

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
0

@itou123
Wiem, ale komentarze dodaję świadomie. Mam z tyłu głowy, że kod mogę wrzucić publicznie, więc chcę, żeby był czytelny także dla innych. Część z nich to też notatki dla mnie na przyszłość.
Format można zawsze dopracować, ale zależy mi bardziej na przejrzystości niż minimalizmie. Gdybym pisał kod wyłącznie dla siebie, pewnie wyglądałby zupełnie inaczej, znacznie oszczędniej w komentarzach i bardziej "technicznie".

PS. Nowe wersje Delphi wprowadzają sporo ciekawych usprawnień.
Testuję je sobie i widać, że projekt się rozwija.

Takie nowinki jak ta robią robotę.

Kopiuj

// Inline Variable Declarations (Variable Line)
// Możliwość deklarowania zmiennych bezpośrednio w miejscu ich użycia via C++ czy Python
procedure Example;
begin
  ShowMessage('Start');
  
  var MyNumber := 42;     // deklaracja w środku
  ShowMessage(MyNumber.ToString);
  
  var MyText := 'Hello';  // kolejna deklaracja dalej
  ShowMessage(MyText);
  
  var Result := MyNumber * 2;   // Bezpośrednio przed użyciem:
  ShowMessage('Wynik: ' + Result.ToString);
end;

procedure Example2;
begin
  if SomeCondition then
  begin
    var TempValue := GetSomeValue();
    ShowMessage(TempValue);
    // TempValue istnieje tylko w tym bloku if
  end;
  
  for var I := 1 to 10 do
  begin
    var Square := I * I;
    ShowMessage(Square.ToString);
  end;
end;

// Type Inference (wnioskowanie typów)
// Kompilator sam określa typ na podstawie przypisanej wartości:
var 
  Count := 10;           // Integer
  Price := 19.99;        // Extended/Double
  Name := 'Jan';         // String
  IsActive := True;      // Boolean

// Managed Records (rekordy zarządzane)
// Używamy managed records z konstruktorami i metodami
type
  TPerson = record
    Name: string;
    Age: Integer;
    class operator Initialize(out Dest: TPerson);
    constructor Create(AName: string; AAge: Integer);
  end;

var Person := TPerson.Create('Anna', 25);

// Custom Managed Records
// Kontrolujemy inicjalizację i finalizację
type
  TResource = record
  private
    FData: string;
  public
    class operator Initialize(out Dest: TResource);
    class operator Finalize(var Dest: TResource);
  end;

// Mi się to podoba, kod wygląda czytelniej, zmienne są deklarowane blisko miejsca użycia.

I1
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 205
0
first_person napisał(a):

@itou123
Wiem, ale komentarze dodaję świadomie. Mam z tyłu głowy, że kod mogę wrzucić publicznie, więc chcę, żeby był czytelny także dla innych.

Polecam poniższą pozycję, autor promuje podejście m.in. aby pisać kod używająć nazw tak zrozumiałych i intencjonalnych, aby minimalizować potrzebę komentarzy:
https://helion.pl/ksiazki/czysty-kod-podrecznik-dobrego-programisty-robert-c-martin,czykvv.htm#format/d
Też jestem programistą Delphi i pomogła mi ta książka. Kiedyś mój kod wyglądał bardzo podobnie do Twojego. To podejście pomaga, dzięki temu funkcje mieszczą się na ekranie i widzisz w pełni co robią (to też jedna z zasad czystego kodu, taki podział funkcji by mieściły się na ekranie).
Krótwszy wpis o tym: https://en.wikipedia.org/wiki/Self-documenting_code

I samo pisanie begin w tej samej linii co if/for, jak w innych językach - "if (x == 15) { " też zaoszczędzi miejsca.

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
0

@itou123 Kolego, rozumiem Twoją perspektywę. Dla mnie najważniejsze jest, żeby kod był "poprawny" i zrozumiały. Komentarze traktuję jako dodatek.. nie przeszkadzają tym, którzy ich nie potrzebują, a pomagają tym, którzy chcą szybciej wejść w kontekst. Uwierz mi, do tego Toasta (ToastNotificatio.pas) dodałem je świadomie, bo wrzuciłem tutaj post edukacyjnie.

Gdybym pisał wyłącznie dla siebie, wyglądałby inaczej i pewnie byłby w całości po angielsku :)

Ważne że działa :)

Miłego dnia.

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12279
2

Wypowiem się na temat komentarzy, bo od kilku lat mam z nimi dość dużo do czynienia. 😉

Komentarze są dobre, jeśli są one tworzone w stricte określony i kontrolowany sposób (nie na pałę). Każdy kod powinien je posiadać, a szczególnie ten, nad którym pracuje więcej niż jedna osoba i/lub jest publiczny. Są one dokumentacją, która jest zawsze w zasięgu wzroku — nie wymaga przełączania się pomiędzy oknami, a tym bardziej korzystania z zewnętrznych aplikacji.

Oprócz bycia dokumentacją, poprawnie stworzone komentarze są detektorem dryfu semantycznego, czyli mechanizmem pozwalającym wykryć rozbieżność pomiędzy intencją a implementacją — należy je traktować jako dodatkowe symbole debugowania. Dzięki nim nie musisz analizować kodu, aby wiedzieć co robi. Jeśli komentarz nie opisuje tego co robi kod, to znalazłeś potencjalne miejsce istnienia bugów — bez dotykania debuggera.

Po trzecie, komentarze są pomocą dla wszystkich — dla autora, który z biegiem czasu zapomni o intencjach oraz dla innych, którzy nie pisali danego kodu i intencji nigdy nie znali. Komentarze potrafią w znaczący sposób ułatwić i przyspieszyć pracę z kodem, dlatego że znacznie łatwiej jest przeczytać zdanie niż kilka linijek kodu. Możesz mieć wspaniałe, opisowe identyfikatory oraz perfekcyjnie sformatowany kod, ale nadal nie będziesz wiedział dlaczego dana instrukcja (czy ich zestaw) są wykonywane w danym kontekście — komentarz ci to powie, bez żadnego wysiłku.

Dla przykładu, zobacz na poniższy kod. Posiada sensowne, opisowe identyfikatory oraz jest poprawnie sformatowany:

Kopiuj
NodeHead := @Segment^.Data;
NodeTail := Pointer(NodeHead) + (AList^.NodeNumSegment - 1) * AList^.SizeNode;

while NodeHead < NodeTail do
begin
  NodeHead^.Segment := Segment;
  NodeHead^.Next    := Pointer(NodeHead) + AList^.SizeNode;

  Pointer(NodeHead) += AList^.SizeNode;
end;

Czy jesteś w stanie mi powiedzieć co ten kod robi? Jak długo potrwa jego przeanalizowanie, biorąc pod uwagę to, że zawiera kilka zmiennych, pętlę, arytmetykę na pointerach oraz przypisania? I finalnie, jaką masz pewność, że wnioski do jakich dojdziesz po kilku minutach są poprawne (czyli zrozumiałeś co ten kod faktycznie robi)? Niby kilka linijek kodu, ale zrozumienie jego działania wymaga wysiłku.

A ile zajmie ci zrozumienie tego snippetu, jeśli dodam do niego komentarze?

Kopiuj
// Get a pointer to the first and last node in the segment data block.
NodeHead := @Segment^.Data;
NodeTail := Pointer(NodeHead) + (AList^.NodeNumSegment - 1) * AList^.SizeNode;

// For each node in the node's data memory block, initialize a pointer to the segment it belongs to, as well as a link to
// the next node in that segment. In this way, a singly-linked list of all segment nodes is created, available in the bank.
while NodeHead < NodeTail do
begin
  NodeHead^.Segment := Segment;
  NodeHead^.Next    := Pointer(NodeHead) + AList^.SizeNode;

  Pointer(NodeHead) += AList^.SizeNode;
end;

Odpowiedź: tyle ile potrwa przeczytanie dwóch zdań w języku angielskim — zakładam, że umiesz płynnie czytać ze zrozumieniem, więc nie powinno to zająć więcej niż 10-15 sekund.

Kod ten pochodzi z modułu zawierającego implementację listy wiązanej używającej segmentów danych, w których fizycznie osadzone są węzły — tutaj pełen kod modułu, a tutaj fragment, który pokazałem wyżej. Możesz też zapoznać się z pozostałym kodem i spróbować zrozumieć jak cała lista działa — znajdziesz tam pełen zestaw komentarzy z opisem intencji oraz będących typową dokumentacją (np. te poprzedzające definicje każdej z funkcji).

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
0

@flowCRANE Jak profesionalnie pisać komentarze. Dla przykładu dam w moim ulubionym C++
Skoro to polskie forum, komentarze dam po polsku.
Standardowo używam angielskiego.

Kopiuj
/**
 * @file UserManager.h
 * @brief Zarządzanie kontami użytkowników w systemie
 * @author Jan Kowalski
 * @date 2026-02-11
 * @version 1.0
 */

#ifndef USER_MANAGER_H
#define USER_MANAGER_H

#include <string>
#include <memory>

/**
 * @class User
 * @brief Reprezentuje pojedynczego użytkownika w systemie
 * 
 * Klasa zawiera podstawowe informacje o użytkowniku oraz
 * metody do zarządzania jego kontem.
 */
class User {
private:
    int userId_;           ///< Unikalny identyfikator użytkownika
    std::string username_; ///< Nazwa użytkownika (3-50 znaków)
    bool isActive_;        ///< Status aktywności konta

public:
    /**
     * @brief Konstruktor tworzący nowego użytkownika
     * @param username Nazwa użytkownika (musi mieć 3-50 znaków)
     * @throw std::invalid_argument gdy nazwa jest nieprawidłowa
     */
    explicit User(const std::string& username);
    
    /**
     * @brief Aktywuje konto użytkownika i wysyła email powitalny
     * @return true jeśli aktywacja się powiodła, false w przeciwnym razie
     * @note Metoda loguje wszystkie próby aktywacji
     */
    bool activate();
    
    // Gettery
    int getUserId() const { return userId_; }
    const std::string& getUsername() const { return username_; }
    bool isActive() const { return isActive_; }
};

// Implementacja

User::User(const std::string& username) 
    : userId_(0), isActive_(false) {
    
    // Walidacja długości nazwy użytkownika
    if (username.length() < 3 || username.length() > 50) {
        throw std::invalid_argument("Nazwa musi mieć 3-50 znaków");
    }
    
    username_ = username;
}

bool User::activate() {
    // TODO: Dodać logowanie do pliku audytu
    // FIXME: Obsłużyć przypadek gdy email nie może być wysłany
    
    try {
        isActive_ = true;
        bool emailSent = sendWelcomeEmail(username_);
        
        /* HACK: Tymczasowe rozwiązanie
         * Należy przepisać po wdrożeniu nowego email API
         * planowane na Q2 2027
         */
        if (!emailSent) {
            logWarning("Email nie został wysłany do: " + username_);
        }
        
        return true;
        
    } catch (const std::exception& e) {
        // Wycofanie zmian w przypadku błędu
        isActive_ = false;
        throw;
    }
}

#endif // USER_MANAGER_H

Zasada zawodowca

Co komentować:

Nagłówki plików/modułów z metadanymi
Publiczne API (klasy, metody, funkcje)
Złożoną logikę (biznesową)
Nieoczywiste decyzje projektowe
TODO, FIXME, HACK z kontekstem

Czego unikać:

Komentowania oczywistości: i++; // zwiększ i 😄
Zdezaktualizowanych komentarzy
Komentowania złego kodu zamiast go poprawić
Nadmiernego komentowania prostego kodu (chyba że piszesz kod edukacyjnie)

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12279
0

Nie jestem zwolennikiem pisania komentarzy w formie meta-języka do generowania zewnętrznego systemu pomocy. Komentarze to z natury metadane, więc umieszczanie znaczników w komentarzach, czyli metadanych w metadanych, to obfuskacja, a nie pomoc. Komentarze z zasady mają służyć programistom (czyli tym, którzy pracują z kodem), a nie zewnętrznym narzędziom. Jeśli generator dokumentacji nie potrafi parsować kodu, który nie jest wypchany po brzegi specjalnymi tokenami, to jest bezwartościowy.


Ciekawy jest ten moduł — z jednej strony jego zawartość tłumaczy, że jest menedżerem użytkowników, a z drugiej strony, zawiera implementację klasy pojedynczego użytkownika (ani śladu jakiegokolwiek zarządzania). Po drugie, jest świetnym przykładem pokazującym jak nielogicznym i wypaczonym jest współczesny paradygmat obiektowy, promowany przez rzeczonych ”zawodowców”.

Mowa tutaj standardowo o mieszaniu danych z logiką oraz o odwróconym kierunku kontroli stanu. W twoim przykładzie, obiekt reprezentujący użytkownika ma władzę, której mieć nie powinien — to nie użytkownik powinien decydować o swojej aktywacji, ani też zajmować się wysyłaniem e-maili. Nie powinien też rzucać wyjątków, a już tym bardziej, że funkcja jego aktywacji (bądź co bądź błędnie będąca częścią użytkownika) zwraca wartość logiczną informującą o tym czy udało się aktywować czy nie.

Obiekt powinien służyć jedynie do przechowywania danych określających stan jednostki (tu: użytkownika), natomiast kontrola tych danych powinna pochodzić z zewnątrz. Aktywacją użytkownika nie powinna być metoda będąca częścią klasy użytkownika, a zewnętrzna funkcja, przyjmująca w parametrze obiekt użytkownika, na którym mają zostać wykonane operacje.


No ale cóż — w OOP wszystko jest na opak, do tego okraszone kupą boilerplate'u. Dlatego zrezygnowałem z OOP i wróciłem do paradygmatu czysto strukturalno-proceduralnego, bo on nie wymaga stawiania wszystkiego na głowie i tworzenia potworków, w którym nie wiadomo co ma władzę i co kontroluje co. 😉

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
1

Po optymalziacji, poprawkach...

Auto-szerokość - dopasowuje się do tekstu (było: 300px fixed → teraz: 300-600px)
WordWrap - długie teksty zawijają się w 2-3 linie (było: 1 linia obcięta)
Dynamiczna wysokość - toast rośnie dla wieloliniowych tekstów (było: 45px fixed)
Auto-czas trwania - krótki tekst 2s, długi 6s (było: zawsze 2s)
Kolory niezależne od motywu - StyleElements := [] (było: kolory zmieniały się z motywem)
Współdzielony bitmap - jedna instancja dla wszystkich (było: tworzenie przy każdym toaście)
Brak migotania - Label.Parent ustawiane na końcu (zabezpiecznie)
Constans zamiast magic numbers - łatwa konfiguracja

Kopiuj
// ToastNotification.pas
// Fixed and enhanced version of a toast notification system for Delphi VCL applications.
// CODE BY FPERSON (2026-02-14)

unit ToastNotification;

interface

uses
  Winapi.Windows, Winapi.Messages, Vcl.ExtCtrls, Vcl.Controls, Vcl.Forms,
  Vcl.Graphics, System.Classes, System.SysUtils, System.Math,
  Vcl.StdCtrls, System.Generics.Collections;

type
  TToastPosition = (tpBottom, tpTop, tpCenter);

 //Settings for a single toast
  TToastSettings = record
    BackgroundColor: TColor;
    TextColor: TColor;
    FontSize: Integer;
    FontStyle: TFontStyles;
    Width: Integer;
    Height: Integer;
    Duration: Integer;
    AnimationSpeed: Integer;
    Position: TToastPosition;
  end;

  TToast = class
  private
    FPanel: TPanel;
    FTimer: TTimer;
    FStep: Integer;
    FShouldFree: Boolean;
    FSettings: TToastSettings;
    FTargetY: Integer;
    FStartY: Integer;
    FOwner: TComponent;

    class var FActiveToasts: TObjectList<TToast>;
    class var FMeasureBitmap: TBitmap;

    procedure OnTimer(Sender: TObject);
    procedure AnimateEntrance(Parent: TWinControl);
    procedure AnimateExit(Parent: TWinControl);
    procedure FreeResources;
    procedure CalculatePosition(Parent: TWinControl);
    procedure FinishToast;

    class function CalculateStackOffset(Index: Integer; Owner: TComponent;
      Pos: TToastPosition): Integer;
    class procedure UpdateToastPositions;

  public
    constructor Create(AOwner: TComponent; const Msg: string); overload;
    constructor Create(AOwner: TComponent; const Msg: string;
      const Settings: TToastSettings); overload;
    destructor Destroy; override;

    class procedure Show(AOwner: TComponent; const Msg: string); overload;
    class procedure Show(AOwner: TComponent; const Msg: string;
      const Settings: TToastSettings); overload;

    class function DefaultSettings: TToastSettings;
    class function SuccessSettings: TToastSettings;
    class function ErrorSettings: TToastSettings;
    class function WarningSettings: TToastSettings;
    class function InfoSettings: TToastSettings;

    class constructor Create;
    class destructor Destroy;
  end;

var
  DefaultToastSettings: TToastSettings;

implementation

// constants for animation and layout
const
  TOAST_ANIMATION_STEPS = 20;
  TOAST_SPACING = 10;
  TOAST_MARGIN_VERTICAL = 20;
  TOAST_TEXT_PADDING = 40;
  TOAST_MAX_WIDTH = 600;
  TOAST_MIN_DURATION = 2000;
  TOAST_MAX_DURATION = 6000;
  TOAST_MS_PER_CHAR = 50;

{ TToast }

class constructor TToast.Create;
begin
  FActiveToasts := TObjectList<TToast>.Create(False);
  FMeasureBitmap := TBitmap.Create;
end;

class destructor TToast.Destroy;
begin
  FActiveToasts.Free;
  FMeasureBitmap.Free;
end;

procedure InitializeDefaultSettings;
begin
  with DefaultToastSettings do
  begin
    BackgroundColor := $00333333;
    TextColor := clWhite;
    FontSize := 10;
    FontStyle := [fsBold];
    Width := 300;
    Height := 45;
    Duration := 2000;
    AnimationSpeed := 15;
    Position := tpBottom;
  end;
end;

class function TToast.DefaultSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
end;

class function TToast.SuccessSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $004CAF50;
  Result.TextColor := clWhite;
end;

class function TToast.ErrorSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $00F44336;
  Result.TextColor := clWhite;
end;

class function TToast.WarningSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $00FF9800;
  Result.TextColor := clBlack;
end;

class function TToast.InfoSettings: TToastSettings;
begin
  Result := DefaultToastSettings;
  Result.BackgroundColor := $002196F3;
  Result.TextColor := clWhite;
end;

class procedure TToast.Show(AOwner: TComponent; const Msg: string);
begin
  TToast.Create(AOwner, Msg);
end;

class procedure TToast.Show(AOwner: TComponent; const Msg: string;
  const Settings: TToastSettings);
begin
  TToast.Create(AOwner, Msg, Settings);
end;

constructor TToast.Create(AOwner: TComponent; const Msg: string);
begin
  Create(AOwner, Msg, DefaultToastSettings);
end;

constructor TToast.Create(AOwner: TComponent; const Msg: string;
  const Settings: TToastSettings);
var
  ParentControl: TWinControl;
begin
  inherited Create;

  FSettings := Settings;
  FShouldFree := False;
  FOwner := AOwner;

  if AOwner is TWinControl then
    ParentControl := TWinControl(AOwner)
  else
    ParentControl := Application.MainForm;

  FPanel := TPanel.Create(nil);
  try
    FPanel.Parent := ParentControl;
    FPanel.Caption := '';
    FPanel.Color := FSettings.BackgroundColor;
    FPanel.Font.Color := FSettings.TextColor;
    FPanel.Font.Size := FSettings.FontSize;
    FPanel.Font.Style := FSettings.FontStyle;
    FPanel.BevelOuter := bvNone;
    FPanel.StyleElements := [];

    // custom width based on text
    FMeasureBitmap.Canvas.Font.Assign(FPanel.Font);
    var TextWidth := FMeasureBitmap.Canvas.TextWidth(Msg) + TOAST_TEXT_PADDING;
    FPanel.Width := Max(FSettings.Width, Min(TOAST_MAX_WIDTH, TextWidth));

    FPanel.Height := FSettings.Height;

    //  create label for text
    var Lbl := TLabel.Create(FPanel);
    Lbl.Align := alClient;
    Lbl.Alignment := taCenter;
    Lbl.Layout := tlCenter;
    Lbl.WordWrap := True;
    Lbl.AutoSize := False;
    Lbl.Caption := Msg;
    Lbl.Font.Assign(FPanel.Font);
    Lbl.Transparent := True;
    Lbl.StyleElements := [];

    // calculate needed height for text
    FMeasureBitmap.Canvas.Font.Assign(Lbl.Font);
    var TextRect := Rect(0, 0, FPanel.Width - TOAST_TEXT_PADDING, 0);
    DrawText(FMeasureBitmap.Canvas.Handle, PChar(Msg), Length(Msg), TextRect,
             DT_CALCRECT or DT_WORDBREAK or DT_CENTER);
    var NeededHeight := TextRect.Height + 20;
    if NeededHeight > FSettings.Height then
      FPanel.Height := NeededHeight;

    Lbl.Parent := FPanel;

    // Adjust duration based on text length
    var CharCount := Length(Msg);
    FSettings.Duration := Max(TOAST_MIN_DURATION,
      Min(TOAST_MAX_DURATION, CharCount * TOAST_MS_PER_CHAR));

    FPanel.DoubleBuffered := True;
    FPanel.ParentBackground := False;
    FPanel.ParentDoubleBuffered := False;

    FActiveToasts.Add(Self);
    CalculatePosition(ParentControl);

    FPanel.Visible := True;
    FPanel.BringToFront;

    FStep := 0;
    FTimer := TTimer.Create(nil);
    FTimer.Interval := FSettings.AnimationSpeed;
    FTimer.OnTimer := OnTimer;
    FTimer.Enabled := True;
  except
    FActiveToasts.Remove(Self);
    FreeResources;
    raise;
  end;
end;

destructor TToast.Destroy;
begin
  FreeResources;
  inherited;
end;

// Helper function to calculate vertical offset for stacking toasts
class function TToast.CalculateStackOffset(Index: Integer; Owner: TComponent;
  Pos: TToastPosition): Integer;
var
  J: Integer;
begin
  Result := 0;
  for J := 0 to Index - 1 do
    if Assigned(FActiveToasts[J].FPanel) and
       (FActiveToasts[J].FOwner = Owner) and
       (FActiveToasts[J].FSettings.Position = Pos) then
      Inc(Result, FActiveToasts[J].FPanel.Height + TOAST_SPACING);
end;

class procedure TToast.UpdateToastPositions;
var
  I, Offset: Integer;
  Toast: TToast;
  ParentControl: TWinControl;
begin
  if FActiveToasts.Count = 0 then
    Exit;

  for I := 0 to FActiveToasts.Count - 1 do
  begin
    Toast := FActiveToasts[I];
    if not Assigned(Toast.FPanel) or not Assigned(Toast.FPanel.Parent) then
      Continue;

    ParentControl := TWinControl(Toast.FPanel.Parent);

    if Toast.FStep <= TOAST_ANIMATION_STEPS then
      Continue; // no need to adjust position during entrance animation

    Offset := CalculateStackOffset(I, Toast.FOwner, Toast.FSettings.Position);

    case Toast.FSettings.Position of
      tpBottom:
        Toast.FTargetY := ParentControl.ClientHeight - Toast.FPanel.Height -
          TOAST_MARGIN_VERTICAL - Offset;
      tpTop:
        Toast.FTargetY := TOAST_MARGIN_VERTICAL + Offset;
      tpCenter:
        Toast.FTargetY := (ParentControl.ClientHeight - Toast.FPanel.Height) div 2 - Offset;
    end;

    Toast.FPanel.Top := Toast.FTargetY;
  end;
end;

procedure TToast.CalculatePosition(Parent: TWinControl);
var
  Offset: Integer;
begin
  FPanel.Left := (Parent.ClientWidth - FPanel.Width) div 2;

  Offset := 0;
  for var I := 0 to FActiveToasts.Count - 1 do
  begin
    if (FActiveToasts[I] <> Self) and
       (FActiveToasts[I].FOwner = FOwner) and
       (FActiveToasts[I].FSettings.Position = FSettings.Position) and
       Assigned(FActiveToasts[I].FPanel) then
      Inc(Offset, FActiveToasts[I].FPanel.Height + TOAST_SPACING);
  end;

  case FSettings.Position of
    tpBottom:
      begin
        FStartY := Parent.ClientHeight;
        FTargetY := Parent.ClientHeight - FPanel.Height - TOAST_MARGIN_VERTICAL - Offset;
      end;
    tpTop:
      begin
        FStartY := -FPanel.Height;
        FTargetY := TOAST_MARGIN_VERTICAL + Offset;
      end;
    tpCenter:
      begin
        FStartY := Parent.ClientHeight;
        FTargetY := (Parent.ClientHeight - FPanel.Height) div 2 - Offset;
      end;
  end;

  FPanel.Top := FStartY;
end;

procedure TToast.OnTimer(Sender: TObject);
var
  TotalSteps: Integer;
begin
  if FShouldFree or not Assigned(FPanel) or not Assigned(FPanel.Parent) then
  begin
    if Assigned(FTimer) then
      FTimer.Enabled := False;
    FreeResources;
    Exit;
  end;

  Inc(FStep);
  TotalSteps := (FSettings.Duration div FSettings.AnimationSpeed) +
    (TOAST_ANIMATION_STEPS * 2);

  if FStep <= TOAST_ANIMATION_STEPS then
    AnimateEntrance(TWinControl(FPanel.Parent))
  else if FStep <= TotalSteps - TOAST_ANIMATION_STEPS then
    // waiting period - do nothing
  else
  begin
    AnimateExit(TWinControl(FPanel.Parent));

    case FSettings.Position of
      tpBottom:
        if FPanel.Top >= TWinControl(FPanel.Parent).ClientHeight then
          FinishToast;
      tpTop:
        if FPanel.Top <= -FPanel.Height then
          FinishToast;
      tpCenter:
        if FPanel.Top >= TWinControl(FPanel.Parent).ClientHeight then
          FinishToast;
    end;
  end;
end;

procedure TToast.AnimateEntrance(Parent: TWinControl);
begin
  var Progress := FStep / TOAST_ANIMATION_STEPS;
  FPanel.Top := Round(FStartY + (FTargetY - FStartY) * Progress);
end;

procedure TToast.AnimateExit(Parent: TWinControl);
var
  ExitStep: Integer;
  ExitTargetY: Integer;
begin
  ExitStep := FStep - (FSettings.Duration div FSettings.AnimationSpeed +
    TOAST_ANIMATION_STEPS);
  var Progress := ExitStep / TOAST_ANIMATION_STEPS;

  if FSettings.Position = tpTop then
    ExitTargetY := -FPanel.Height
  else
    ExitTargetY := Parent.ClientHeight;

  FPanel.Top := Round(FTargetY + (ExitTargetY - FTargetY) * Progress);
end;

procedure TToast.FinishToast;
begin
  if Assigned(FTimer) then
    FTimer.Enabled := False;

  FShouldFree := True;
  FActiveToasts.Remove(Self);
  UpdateToastPositions;

  if Assigned(Application) and Assigned(Application.MainForm) then
    PostMessage(Application.MainForm.Handle, WM_NULL, 0, 0);
  FreeResources;
end;

procedure TToast.FreeResources;
begin
  if Assigned(FTimer) then
  begin
    FTimer.Enabled := False;
    FreeAndNil(FTimer);
  end;

  if Assigned(FPanel) then
  begin
    FPanel.Visible := False;
    FreeAndNil(FPanel);
  end;
end;

initialization
  // Set default settings on initialization
  InitializeDefaultSettings;

end.


Marius.Maximus
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 2225
0

@first_person dla mnie podstawowym mankamentem jest umieszczanie kodu w wątku i coś mi tu pachnie że kolejne wersje to kolejne wpisy co wydaj mi się jeszcze większym problemem.
Lepiej wypchnąć na GIT-a i mieć to po ludzku

A co do samego projektu to na filmiku wygląda to ciekawie i ta muzyczka która tak buduje napięcie !!!

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12279
0

@Marius.Maximus timer nie jest osobnym wątkiem — o ile dobrze pamiętam, każdy jego tick to komunikat WM_TIMER, który trafia do procedury obsługi komunikatów okna, a więc działa w ramach głównego wątku. To jest dobre rozwiązanie, dlatego że w zdarzeniu timera można bezpiecznie modyfikować stan kontrolek, bez jakiejkolwiek synchronizacji, właśnie ze względu na działanie w ramach głównego wątku.

Tak więc tutaj użycie timera jest jak najbardziej sensowne — to domyślne rozwiązanie dla animowanego UI.


Edit: PS: plus jest też taki, że timer nie przestaje działać nawet gdy pojawi się okno modalne. Dzięki temu, jeśli mamy animowane UI, wyświetlenie modalnego okna nie blokuje timerów w oknie, które traci fokus, więc te animacje mogą się dokończyć spokojnie. Przynajmniej tak to działa w LCL.

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
0

Przykład użycia w kodzie, APKA - File Hash Tool
https://xfperson.blogspot.com/2026/02/file-hash-tool-multilingua.html

Zmień np język aplikacji i mamy fajny animowany toast.

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
0

@flowCRANE Ogólnie, pisałem ten kod stricte pod VCL i nie bez powodu. Sporo znajomych i nie tylko wciąż koduje właśnie w VCL, więc chciałem żeby był od razu użyteczny dla jak największej liczby osób.

Ale przyznam szczerze.... gdybym robił to samo w FMX, połowa tej roboty by zniknęła sama z siebie. Cały ten ręczny licznik kroków, interpolacja w timerze, pilnowanie czy panel już wyjechał poza ekran... w FMX to dosłownie kilka linijek z TFloatAnimation, albo nawet jedno wywołanie TAnimator.AnimateFloat i gotowe. FMX ma wbudowany render loop i system animacji, który robi całą tę matematykę za ciebie, łącznie z easingiem, callbackiem po zakończeniu animacji i obsługą wielu platform.

Tak więc timer tutaj jest jak najbardziej sensowny i działa świetnie, ale to też trochę urok VCL: robisz więcej rzeczy ręcznie, za to masz pełną kontrolę nad tym co się dzieje. W FMX oddajesz trochę tej kontroli frameworkowi i po prostu... działa 😉

A tak przy okazji,ciekawostka dla tych którzy nie wiedzieli: TAnimator.AnimateFloat istnieje w Delphi już od FMX XE5, czyli od 2013 roku. Przez lata mnóstwo osób pisało ręcznie timery do animacji w FMX, nie mając pojęcia, że framework ma to wbudowane od dawna. Dokumentacja Delphi potrafi być bezlitosna w chowaniu takich smaczków.

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12279
2

Ja tam sobie cenię własną implementację od cudzych, szczególnie tych silących się na bycie uniwersalnymi — moja implementacja zawiera dokładnie to czego potrzebuję i działa tak jak oczekuję, a cudza ma tony abstrakcji i ograniczenia wynikające z bycia przeznaczoną dla każdego i do wszystkiego. A jak wiadomo, to co jest do wszystkiego, jest do niczego.

Całe LCL to góry abstrakcji, gigantyczna i niemiłosiernie skomplikowana implementacja, przez co napisanie podstawowej kontrolki wymaga siłowania się z tą biblioteką. Robię sobie właśnie kilka kontrolek na potrzeby IDE dla mojej gry (głównie dla toolbarów, takich jakie ma w Visual Studio) i ciągle muszę się kopać z koniem, bo podstawowe rzeczy jak choćby własny Caption nie działają prawidłowo w połączeniu z designerem, albo LCL zapomina wysłać komunikat CM_MOUSELEAVE po zakończeniu mouse capture. Albo — jak zwykle — nie mogę sobie danego elementu kontrolki namalować po swojemu, bo nie ma do tego zdarzenia.

Tego typu biblioteki to tony obiektowych abstrakcji, kupa bloatu (masa zbędnych danych i metod), do tego spaghetti (nie wiadomo co ma wpływ na co), minimalna kontrola nad wyglądem kontrolek (większość narzuca widgetset/OS) oraz często fatalna wydajność, powodująca albo miganie interfejsu (bo brak podwójnego buforowania), albo glitchowanie interfejsu (jak w przypadku splitterów).

Zobacz jak wspaniale działa splitter w oknie Inspektora Obiektów (IDE zbudowane z optymalizacjami -O3):

screenshot-20260218172925.gif

Tak bym podsumował LCL i de facto całego Lazarusa.


Mam już po dziurki w nosie zarówno współczesnych, gównianych języków obiektowych, jak i tych ”mądrych” bibliotek komponentów — coraz częściej mam ochotę wrócić do czystego Win32 API.

Najpewniej kolejnym dużym projektem (po obecnej grze i IDE dla niej) będzie własny język programowania, malutki i proceduralny. Następnie leciutki widgetset i biblioteka własnych kontrolek, dające pełną kontrolę nad wyglądem każdego elementu aplikacji (dekoracji okna, jego klienta, kontrolek i ich pełnej zawartości). A na koniec wystrugam sobie całe IDE, tak abym mógł wygodnie pracować z tym językiem. I wtedy będę mógł raz na zawsze pożegnać Lazarusa i Pascala, bo coraz bardziej mnie zawodzą i irytują.

FP
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 47
0

@flowCRANE Rozumiem ten punkt widzenia i sam nieraz czułem podobną frustrację, ale mam wrażenie, że to trochę błąd dostrzegania wad biblioteki tam, gdzie problem leży gdzie indziej.

Owszem, LCL ma swoje bolączki i CM_MOUSELEAVE to klasyk, ale "tony abstrakcji" to też powód, dla którego nie musisz pisać od zera obsługi klawiatury, focusu, accessibility, skalowania DPI, wysokiego kontrastu i dziesiątek innych rzeczy, o których przy własnej implementacji po prostu zapomnisz i odkryjesz je dopiero gdy ktoś zgłosi buga rok po premierze.

Co do własnego języka, własnego widgesetu i własnego IDE, brzmi jak marzenie każdego programisty, który ma dość cudzych narzędzi. Tylko że to projekt na dekadę, nie na rok. Ci którzy to zrobili i zrobili dobrze, to są zespoły, nie pojedyncze osoby. Zobacz ile lat zajęło Jetbrainom dojście do sensownego działania własnego renderowania UI w Fleet, mając przy tym setki inżynierów i nieograniczony budżet.

Nie mówię że to zły pomysł, robienie rzeczy od zera uczy niesamowicie dużo. Ale "pełna kontrola" ma swoją cenę i często okazuje się, że te wszystkie abstrakcje których nienawidzisz, rozwiązują problemy które jeszcze przed tobą.

Własna implementacja ma sens, tylko pytanie kiedy.

Jeśli piszesz kontrolkę do własnego projektu, którą użyjesz w jednym miejscu i dokładnie wiesz czego potrzebujesz - jak najbardziej. Pisz od zera, będzie prosta, szybka i bez zbędnego balastu. To właśnie zrobiłem z tym toastem i nie żałuję.

Ale jeśli ta implementacja ma działać na różnych maszynach, z różnymi ustawieniami systemowymi, DPI, motywami, czytnikami ekranu, może kiedyś na innym systemie, wtedy abstrakcje biblioteki przestaną być wrogą i staną się tarczą. Bo ktoś te problemy już rozgryzł, opisał bugi, naprawił edge case'y i zapomniał o tym pięć lat temu. 🙃

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12279
2
first_person napisał(a):

[…] ale mam wrażenie, że to trochę błąd dostrzegania wad biblioteki tam, gdzie problem leży gdzie indziej.

No nie, problem leży dokładnie tam, gdzie napisałem — kupa abstrakcji, bloat i często tragiczna wydajność. To jest problem, który mi przeszkadza i utrudnia pracę z tą biblioteką.

Co do własnego języka, własnego widgesetu i własnego IDE, brzmi jak marzenie każdego programisty, który ma dość cudzych narzędzi. Tylko że to projekt na dekadę, nie na rok.

Nie pisałem, że chcę to zrobić w rok. To i tak tylko wstępne plany, które pojawiają się w głowie za każdym razem, gdy kopię się z LCL, albo kiedy mi się Lazarus wykrzacza.

Nie mówię że to zły pomysł, robienie rzeczy od zera uczy niesamowicie dużo. Ale "pełna kontrola" ma swoją cenę i często okazuje się, że te wszystkie abstrakcje których nienawidzisz, rozwiązują problemy które jeszcze przed tobą.

To co mam na myśli pisząc „pełna kontrola”, to brak implementowania tego, co ostatecznie implementować zechce użytkownik (np. renderowanie zawartości okna i kontrolek), a więc o wiele mniej roboty. No i brak implementowania debilnych, pseudo-inteligentnych rozwiązań, które mają myśleć za programistę, a więc jeszcze mniej roboty.

Taki mini zestaw kontrolek, które niczego nie narzucają, a jedynie przechowują informacje o swoim stanie, to co najwyżej kilkanaście tysięcy linijek kodu. Odebrać komunikaty z systemu, przepakować je do własnych zdarzeń (tak jak to robi np. SDL), przesłać do callbacku i to wszystko — renderujesz samodzielnie, a więc kontrolujesz każdy piksel okna i kontrolek. Takie maleństwo to co najwyżej 10-20 kLoc, core plus zestaw podstawowych kontrolek (labelki, przyciski, menu, edity, listboxy, drzewka itd.).

Zresztą już taki system kontrolek robiłem, a dokładniej dla swojej gry — core ma tylko 2,273 linie kodu. Co prawda jest specyficzny dla mojej gry, ale obsługuje boksy z kontrolkami (animowanymi), fokus i jego przenoszenie pomiędzy kontrolkami, tryb pisania (pełne wsparcie UTF-8), sterowanie myszą, klawiaturą i gamepadami, pozwala obsługiwać kilka boksów jednocześnie przez kilku graczy. Teraz jedyne co pozostało to stworzyć docelowe kontrolki na bazie podstawowego typu.

Własna implementacja ma sens, tylko pytanie kiedy.

Na emeryturze, no bo kiedy? 😉

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.