TDirectory.Copy + ProgressBar

di il
10 risposte

TDirectory.Copy + ProgressBar

Ciao,

devo copiare una cartella in rete, un processo che, secondo la dimensione della cartella, può freezare l'UI e lascia l'utente senza informazioni.

Per tener informato l'utente sul progresso del processo pensavo di utilizzare una ProgressBar.
Eseguendo il TDirectory.Copy in un TThread.CreateAnonymousThread risolve il problema del freez, ma senza iterazione non so come aggiornare mediante TThread.Synchronize la ProgressBar.

In questo momento mi viene solo in mente sostituire il TDirectory.Copy con un Array := TDirectory.GetFiles(Path, '.', TSearchOption.soAllDirectories) in modo di poter eseguire la copia di ciascun file in un loop che aggiorna la ProgressBar.

È questa la strada giusta da percorrere per realizzare quanto descritto sopra o c'è una soluzione migliore e più semplice utilizzando TDirectory.Copy?

Ale

10 Risposte

  • Re: TDirectory.Copy + ProgressBar

    al.delphi ha scritto:


    Per tener informato l'utente sul progresso del processo pensavo di utilizzare una ProgressBar.
    Per poter utilizzare una TProgressBar, a meno che tu non voglia impiegarla nella sua modalità "indefinita" (ossia farla illuminare ma senza un preciso inizio/fine e una indicazione precisa del progresso), devi eseguire la copia della directory in modo da ottenere informazioni sul processo in corso, cosa che - a meno che non mi sbagli - TDirectory.Copy (stando alla documentazione) non ti consente di fare: quando invochi il metodo, l'operazione di copia ha inizio e il metodo termina quando la copia è finita.

    al.delphi ha scritto:


    Eseguendo il TDirectory.Copy in un TThread.CreateAnonymousThread risolve il problema del freez, ma senza iterazione non so come aggiornare mediante TThread.Synchronize la ProgressBar.
    E' esattamente questo il problema, come descritto all'inizio.

    al.delphi ha scritto:


    In questo momento mi viene solo in mente sostituire il TDirectory.Copy con un Array := TDirectory.GetFiles(Path, '.', TSearchOption.soAllDirectories) in modo di poter eseguire la copia di ciascun file in un loop che aggiorna la ProgressBar.
    L'operazione è più complessa di quel che sembra: bisognerebbe recuperare anticipatamente l'elenco delle directory e dei file da copiare nel loro complesso, ivi compresa magari la dimensione dei file stessi, ed eseguire la creazione delle directory e la copia dei singoli file uno alla volta, calcolando l'avanzamento del progresso e aggiornando di conseguenza il valore della TProgressBar.

    Bisogna scrivere un po' di codice...

    al.delphi ha scritto:


    È questa la strada giusta da percorrere per realizzare quanto descritto sopra o c'è una soluzione migliore e più semplice utilizzando TDirectory.Copy?
    Quel metodo non offre particolari soluzioni. La copia dei file più interattiva che si possa fare, al netto di scriversela autonomamente, è quella di usare direttamente la funzione della shell, la SHFileOperation (vedi documentazione), che dovrebbe anche consentire la definizione di un callback da invocare per monitorare il progresso di copia delle cartelle e dei file nella locazione proposta.

    Ciao!
  • Re: TDirectory.Copy + ProgressBar

    Grazie, Marco. Avevo intravisto discussioni riguardo SHFileOperation. Approfondisco allora la lettura e provo.

    Ale
  • Re: TDirectory.Copy + ProgressBar

    Ho provato il seguente codice per eliminare una cartella "ProjectPath" (+ sottocartelle) e copiare il contenuto della cartella "pBackupPath". I processi si trovano all'interno di due TThread in modo di evitare il freeze dell'UI.

    Risultato: L'eliminazione di files e cartelle dal ProjectPath (fase 1) funziona correttamente, anche la ricreazione della struttura delle cartelle da backup (fase 2). Ma quando viene eseguita la copia dei files da backup (fase 3), il processo termina al 98% a causa dell'eliminazione dell'ultima cartella principale creata e quindi fallendo nel copiare gli ultimi files.

    Come è possibile che l'ultima cartella viene eliminata in fase 3 se il codice non prevede l'eliminazione di cartelle?

    Al
    //fase 1 - elimina ProjectPath
      Thread := TThread.CreateAnonymousThread(procedure
      var
        _fPrj: TStringDynArray;
        i: Integer;
      begin
        SetLength(_fPrj, 0);                                                           //array con files ProjectPath da eliminare
        _fPrj := TDirectory.GetFiles(ProjectPath, '*.*', TSearchOption.soAllDirectories);
        pgbProc.Max := Length(_fPrj);
        for i := 0 to Length(_fPrj) - 1 do                                         
          begin
            TFile.Delete(_fPrj[i]);
            TThread.Synchronize(nil,
            procedure
            begin
              pgbProc.Position := i + 1;
              lblProgress.Caption := IntToStr(Round((i + 1) / Length(_fPrj) * 100)) + '%';
              lblProc.Caption := 'Deleting file ' + IntToStr(i + 1) + ' of ' + IntToStr(Length(_fPrj));
            end);
          end;
      end);
      try
        Thread.FreeOnTerminate := False;
        H := Thread.Handle;
        Thread.Start;
        while MsgWaitForMultipleObjects(1, H, False, INFINITE, QS_ALLINPUT) = (WAIT_OBJECT_0+1) do
          Application.ProcessMessages;
      finally
        Thread.Free;
        TDirectory.Delete(ProjectPath, True);                             //azzera ProjectPath
      end;
      
      //fase 2 - ricrea struttura cartelle ProjectPath da backup
      Thread := TThread.CreateAnonymousThread(procedure
      var
        _pPrj, _fBu, _fRel: TStringDynArray;
        i: Integer;
        fPrj: String;
      begin
        SetLength(_pPrj, 0);                                                       //array con cartelle backup
        _pPrj := TDirectory.GetDirectories(pBackupPath, '*', TSearchOption.soAllDirectories);
        for i := 0 to Length(_pPrj) - 1 do
          begin
            Delete(_pPrj[i], 1, Length(BackupPath));                     //array: sostituisci src path con dst path
            TDirectory.CreateDirectory(ProjectRoot + _pPrj[i]);
          end;
     
      //fase 3 - copia files da backup nel ProjectPath   
        SetLength(_fBu, 0);                                                         //array: backup files
        _fBu := TDirectory.GetFiles(pBackupPath, '*.*', TSearchOption.soAllDirectories);
        pgbProc.Position := 0;
        pgbProc.Max := Length(_fBu);
        SetLength(_fRel, 0);
        _fRel := TDirectory.GetFiles(pBackupPath, '*.*', TSearchOption.soAllDirectories);
        for i := 0 to Length(_fBu) - 1 do
          begin
            Delete(_fRel[i], 1, Length(BackupPath));                      //array: sostituisci src path con dst path
            fPrj := ProjectRoot + _fRel[i];
            TFile.Copy(_fBu[i], fPrj);
            TThread.Synchronize(nil,
            procedure
            begin
              pgbProc.Position := i + 1;
              lblProgress.Caption := IntToStr(Round((i + 1) / Length(_fBu) * 100)) + '%';
              lblProc.Caption := 'Recovering file ' + IntToStr(i + 1) + ' of ' + IntToStr(Length(_fBu));
            end);
          end;
      end);
      try
        Thread.FreeOnTerminate := False;
        H := Thread.Handle;
        Thread.Start;
        while MsgWaitForMultipleObjects(1, H, False, INFINITE, QS_ALLINPUT) = (WAIT_OBJECT_0+1) do
          Application.ProcessMessages;
      finally
        Thread.Free;
      end;
  • Re: TDirectory.Copy + ProgressBar

    al.delphi ha scritto:


    Ho provato il seguente codice per eliminare una cartella "ProjectPath" (+ sottocartelle) e copiare il contenuto della cartella "pBackupPath". I processi si trovano all'interno di due TThread in modo di evitare il freeze dell'UI.
    Innanzitutto, perché fare due thread quando se ne potrebbe utilizzare uno unico?

    Come osservazione generale, faccio molto fatica ad analizzare il codice, perché vi sono dei Thread anonimi creati e messi in Wait sul loro handle, con dei TThread.Synchronize() al loro interno e delle Application.ProcessMessages() che mi sembrano superflue (forse è meglio inserire degli Sleep() per attendere in un ciclo?)...

    Inoltre, attenzione perché (a meno che io non abbia letto male) vi sono ancora delle istruzioni che agiscono sui controlli VCL all'interno dei thread, come in questo blocco dove vengono assegnati valori alle proprietà della TProgressBar (precisamente Position e Max) in un thread anonimo che non è sincronizzato:
    
      //fase 3 - copia files da backup nel ProjectPath   
        SetLength(_fBu, 0);                                                         //array: backup files
        _fBu := TDirectory.GetFiles(pBackupPath, '*.*', TSearchOption.soAllDirectories);
        pgbProc.Position := 0;
        pgbProc.Max := Length(_fBu);
    
    In breve, procederei in questo modo:
  • Re: TDirectory.Copy + ProgressBar

    Grazie della tua risposta, Marco.

    Ammetto che il contenuto del TThread relativo a Handle e ProcessMessages è frutto di CTRL+C/V e non ho idea dell'impatto. Non solo per quantità di post tu sei utente senior e io utente junior. : )

    Nel codice postato ci sono due TThread perché a opera compiuta vorrei chiamare combinazioni di varie routine per la creazione, il ripristino, la soprascrittura e l'eliminazione del backup. Per il momento posso unirli e certamente usare TTask.

    Approfondirò anche sull'utilizzo del timer che per mancanza di know how lo uso poco niente.

    Ale
  • Re: TDirectory.Copy + ProgressBar

    Curioso, anche con TTask e togliendo qualsiasi riferimento all'UI l'errore del 98% persiste.
    Riassumo i valori del mio test: TDirectory.CreateDirectory crea correttamente 64 cartelle (con 5 cartelle principali). Poi, in qualche modo, eseguendo TFile.Copy la 5° cartella principale viene eliminata impedendo così la copia degli ultimi 2% dei files.

    Di seguito il codice ridotto all'osso:
    procedure TfrmProcDlg.FormActivate(Sender: TObject);
    begin
      TaskRestore := TTask.Create(procedure
      begin
        Restore;
      end);
      TaskRestore.Start;
    end;
    
    procedure TfrmProcDlg.Restore;
    var
      _fPrj, _pPrj, _fBu, _fRel: TStringDynArray;
      i: Integer;
    begin
      SetLength(_fPrj, 0);
      _fPrj := TDirectory.GetFiles(ProjectPath, '*.*', TSearchOption.soAllDirectories);
      for i := 0 to Length(_fPrj) - 1 do                                  
        TFile.Delete(_fPrj[i]);
      TDirectory.Delete(ProjectPath, True);            
    
      SetLength(_pPrj, 0);                             
      _pPrj := TDirectory.GetDirectories(pBackupPath, '*', TSearchOption.soAllDirectories);
      for i := 0 to Length(_pPrj) - 1 do
        begin
          Delete(_pPrj[i], 1, Length(BackupPath));        
          TDirectory.CreateDirectory(ProjectRoot + _pPrj[i]);
        end;
    
      SetLength(_fBu, 0);                         
      _fBu := TDirectory.GetFiles(pBackupPath, '*.*', TSearchOption.soAllDirectories);
      SetLength(_fRel, 0);
      _fRel := TDirectory.GetFiles(pBackupPath, '*.*', TSearchOption.soAllDirectories);
      for i := 0 to Length(_fBu) - 1 do
        begin
          Delete(_fRel[i], 1, Length(BackupPath));              
          _fRel[i] := ProjectRoot + _fRel[i];
          TFile.Copy(_fBu[i], _fRel[i]);
        end;
    end;
  • Re: TDirectory.Copy + ProgressBar

    Altra curiosità: La 5° cartella principale, colei che viene eliminata durante TFile.Copy, si chiama "Upload". Può il suo nome essere la causa del malfunzionamento creando un conflitto con TFile.Copy?

    Chiedo perché rinominandola in "PincoPallo" l'app arriva al 100%. : )

    Se si, qualcuno ha un'idea come posso gestire il nome di cartella "Upload" in modo che funzioni con TFile.Copy?

    Ale
  • Re: TDirectory.Copy + ProgressBar

    al.delphi ha scritto:


    Curioso, anche con TTask e togliendo qualsiasi riferimento all'UI l'errore del 98% persiste.
    Ipotizzo che sia un errore logico.

    al.delphi ha scritto:


    Di seguito il codice ridotto all'osso: [...]
      
      _pPrj := TDirectory.GetDirectories(pBackupPath, '*', TSearchOption.soAllDirectories);
          Delete(_pPrj[i], 1, Length(BackupPath));        
    
    Analizzando il codice, vedo apparire un BackupPath e un pBackupPath: che differenza c'è tra i due?
    Perché ci sono due variabili quasi omonime?

    al.delphi ha scritto:


    TDirectory.CreateDirectory(ProjectRoot + _pPrj[i]);
    Non si combinano i percorsi usando l'operatore "+" come per le stringhe tradizionali: per ogni evenienza, meglio usare .

    al.delphi ha scritto:


    
      SetLength(_fBu, 0);                         
      _fBu := TDirectory.GetFiles(pBackupPath, '*.*', TSearchOption.soAllDirectories);
      SetLength(_fRel, 0);
      _fRel := TDirectory.GetFiles(pBackupPath, '*.*', TSearchOption.soAllDirectories);
      for i := 0 to Length(_fBu) - 1 do
        begin
          Delete(_fRel[i], 1, Length(BackupPath));              
          _fRel[i] := ProjectRoot + _fRel[i];
          TFile.Copy(_fBu[i], _fRel[i]);
        end;
    end;
    In questo pezzo di codice, non mi è chiaro perché tu vada ad acquisire due volte l'elenco dei file, con il pericolo di ottenerlo differente tra le istruzioni se qualcuno sta copiando o spostando un file in quel momento; essendo poi che il percorso di destinazione è determinabile da quello in arrivo (vale lo stesso anche nel tuo codice), basta acquisire il percorso del file originale e valorizzare una variabile con l'equivalente percorso di destinazione partendo dal primo, e poi effettuare la TFile.Copy().

    Per dirla meglio, se determini il percorso di destinazione partendo da "fRel" quando questo array contiene gli stessi valori di "fBu", tanto vale partire dal valore originale di "fBu" e determinare direttamente da questo il percorso del file di destinazione, poiché usare l'altra variabile non cambia nulla trattandosi del medesimo valore, e ti basta una variabile stringa per salvare il percorso di destinazione.

    Esemplificando velocemente senza cambiare troppo il codice originale:
    
      for i := 0 to Length(_fBu) - 1 do
        begin
          SourcePath := _fBu[i];
          DestPath := SourcePath;
          Delete(DestPath, 1, Length(BackupPath));
          DestPath := ProjectRoot + DestPath;
          TFile.Copy(SourcePath, DestPath);
        end;
    
    Mi viene il dubbio di come le directory vengano create prima di copiare i file, e perché questa operazione sia effettivamente necessaria, ma secondo me nel codice persistono dei bug e degli aspetti da approfondire che si potrebbero risolvere facendo un po' di refactoring (es. creando funzioni per i nomi dei file e usando le routine della RTL di gestione dei percorsi per semplificare il lavoro delle stringhe).

    Ciao!
  • Re: TDirectory.Copy + ProgressBar

    Ciao Marco,

    pur non avendo mai risolto veramente la questione, ho trovato una soluzione funzionante nell'inversione delle cartelle da processare. Quindi, dopo aver cancellato FolderA e poi FolderB, ricreando e popolando FolderB prima di FolderA il codice funziona.

    Ale
  • Re: TDirectory.Copy + ProgressBar

    al.delphi ha scritto:


    pur non avendo mai risolto veramente la questione, ho trovato una soluzione funzionante nell'inversione delle cartelle da processare. Quindi, dopo aver cancellato FolderA e poi FolderB, ricreando e popolando FolderB prima di FolderA il codice funziona.
    L'importante è risolvere.
Devi accedere o registrarti per scrivere nel forum
10 risposte