Molti programmi gestionali hanno bisogno di eseguire operazioni ricorrenti — un invio email, un backup, una sincronizzazione — a un orario prestabilito, magari escludendo alcuni giorni festivi. Costruire questa logica come componente Delphi, incapsulato in un TThread anonimo, permette di riutilizzarla ovunque senza duplicare codice di temporizzazione.
Un componente basato su thread anonimo
Il cuore del componente è un record con i dati del task (descrizione, orario, giorni da escludere, nome di una DLL da eseguire) e una classe TComponent che avvia un thread con TThread.CreateAnonymousThread:
type
TDatiTask = record
Descrizione: string;
OrarioEsecuzione: TTime;
GiorniEsclusione: string; // Es: 'D:25/12;D:NATALE;D:PASQUA'
NomeDLL: string;
end;
TTaskSchedulerThread = class(TComponent)
private
FThread: TThread;
FDatiTask: TDatiTask;
FProssimaEsecuzione: TDateTime;
FAttivo: Boolean;
procedure Esegui;
procedure AggiornaProssimaEsecuzione;
function DeveEseguireOggi: Boolean;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Avvia;
procedure Ferma;
property DatiTask: TDatiTask read FDatiTask write FDatiTask;
end;
implementation
procedure TTaskSchedulerThread.Avvia;
begin
if not FAttivo then
begin
FAttivo := True;
AggiornaProssimaEsecuzione;
FThread := TThread.CreateAnonymousThread(Esegui);
FThread.FreeOnTerminate := False;
FThread.Start;
end;
end;
procedure TTaskSchedulerThread.Ferma;
begin
if FAttivo then
begin
FAttivo := False;
FThread.Terminate;
FThread.WaitFor;
FreeAndNil(FThread);
end;
end;
procedure TTaskSchedulerThread.Esegui;
begin
while FAttivo do
begin
if (Now >= FProssimaEsecuzione) and DeveEseguireOggi then
begin
// ... esecuzione del task ...
AggiornaProssimaEsecuzione;
end;
Sleep(1000);
end;
end;
Il ciclo dentro Esegui gira ogni secondo su un thread separato dalla UI: è un approccio semplice, meno preciso di un timer di sistema ma sufficiente per attività con cadenza giornaliera, e facile da fermare in modo pulito con Terminate + WaitFor nel distruttore.
Escludere le festività, calcolo della Pasqua incluso
Un dettaglio interessante è la gestione delle esclusioni: oltre alle date fisse, il task può essere marcato per saltare le festività "mobili", calcolando la Pasqua con un classico algoritmo (Gauss):
function DataPasqua(AAnno: Integer): TDateTime;
var
G, C, H, I, J, L: Integer;
Mese, Giorno: Word;
begin
G := AAnno mod 19;
C := AAnno div 100;
H := (C - C div 4 - (8 * C + 13) div 25 + 19 * G + 15) mod 30;
I := H - (H div 28) * (1 - (H div 28) * (29 div (H + 1)) * ((21 - G) div 11));
J := (AAnno + AAnno div 4 + I + 2 - C + C div 4) mod 7;
L := I - J;
Mese := 3 + (L + 40) div 44;
Giorno := L + 28 - 31 * (Mese div 4);
Result := EncodeDate(AAnno, Mese, Giorno);
end;
Con la data di Pasqua calcolata, la stringa di esclusione può contenere alias simbolici come PASQUA, PASQUETTA, NATALE o FERRAGOSTO, oltre a date fisse in formato gg/mm: un piccolo motore di regole di calendario che evita di dover aggiornare a mano l'elenco delle festività ogni anno.
Eseguire codice esterno da una DLL
Il task vero e proprio, nell'esempio originale, viene eseguito caricando dinamicamente una DLL e richiamando una funzione con una firma concordata:
type
TFunzioneEsegui = function(Messaggio, Nome: PChar): Boolean; stdcall;
procedure TTaskSchedulerThread.EseguiTask;
var
HandleDLL: THandle;
FunzioneEsegui: TFunzioneEsegui;
begin
HandleDLL := LoadLibrary(PChar(FDatiTask.NomeDLL));
if HandleDLL = 0 then
Exit; // Log dell'errore di caricamento
try
@FunzioneEsegui := GetProcAddress(HandleDLL, 'Esegui');
if Assigned(FunzioneEsegui) then
FunzioneEsegui(PChar('{}'), PChar('Scheduler'));
finally
FreeLibrary(HandleDLL);
end;
end;
Questo pattern a plugin permette di aggiungere nuovi task senza ricompilare l'eseguibile principale: basta distribuire una nuova DLL con la funzione Esegui esportata. L'unica accortezza, dato che il codice gira su un thread separato, è aggiornare log o interfaccia sempre tramite TThread.Queue o TThread.Synchronize, mai toccando direttamente i controlli VCL dal thread secondario.