Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   GUI-Design mit VCL / FireMonkey / Common Controls (https://www.delphipraxis.net/18-gui-design-mit-vcl-firemonkey-common-controls/)
-   -   Delphi "Bitte warten"-Formular korrekt anzeigen (https://www.delphipraxis.net/154000-bitte-warten-formular-korrekt-anzeigen.html)

TheMiller 24. Aug 2010 15:07

"Bitte warten"-Formular korrekt anzeigen
 
Hallo,

in meinem mittlerweile recht großen Programm gibt es verschiedene Aktionen, die länger dauern. Um dem Benutzer nun zu signalisieren, dass das Programm etwas arbeitet und nicht rumhängt (;)) habe ich ein Formular erstellt mit einer Progressbar, Maqueebar und einem Aktions-Label. Je nach Aktion wird die Marquee- oder Progressbar angezeigt.

Das Form wird dynamisch erstellt und freigegeben. Damit es sich aber neuzeichnet müsste ich es eigentlich in einem gesonderten Thread erstellen. Nun ist das aber wegen der Synchronisierung nicht so einfach. Wie muss ich die Sache angehen?

Kann ich in einem Thread das Formular erstellen lassen, wenn ich eine Methode synchronisiert aufrufe? Also, ist das der richtige Weg, damit das Formular "parallel" zu der Hintergrundaktion läuft?

Mein Code sieht - ohne Thread - bisher so aus (recht simpel)

Delphi-Quellcode:
var
  ...
  frmWait: TfrmWarten;
begin
  frmWait:=TfrmWarten.Create(nil);
  try
    frmWait.FormStyle:=fsStayOnTop;
    frmWait.ShowMarqueeBar(True);
    frmWait.Show;
    frmWait.Repaint;
    //Aktionen, die viel Zeit brauchen
    ...
    ...
    ...
  finally
    FreeAndNil(frmWait);
  end;
end;
Danke im Voraus

taveuni 24. Aug 2010 16:59

AW: "Bitte warten"-Formular korrekt anzeigen
 
Zitat:

Zitat von DJ-SPM (Beitrag 1044585)
Hallo,
gibt es verschiedene Aktionen, die länger dauern. Um dem Benutzer nun zu signalisieren, dass das Programm etwas arbeitet und nicht rumhängt (;))
habe ich ein Formular erstellt mit einer Progressbar, Maqueebar und einem Aktions-Label. Je nach Aktion wird die Marquee- oder Progressbar angezeigt.

Meiner Meinung nach wäre es sinnvoller diese länger dauernden Aktionen
in einen Thread zu schieben und den Benutzer weiter arbeiten zu lassen.
Als den Thread zu missbrauchen nichts zu tun und eine VCL Komponente zu aktualisieren :shock:

TheMiller 24. Aug 2010 17:03

AW: "Bitte warten"-Formular korrekt anzeigen
 
An sich richtig. Nur während dieser Aktionen soll der Benutzer nicht weiterarbeiten dürfen. Es geht um Dinge wie Backups/Sicherung der Datenbank, Hinzufügen von Dateianhängen in die Datenbank etc.

Bei diesen Aktionen habe ich Angst, dass etwas schief geht. Deswegen lieber dem Benutzer verweigern, etwas anderes nebenher mit der Datenbank zu machen. Oder :?:

Björn Ole 24. Aug 2010 18:06

AW: "Bitte warten"-Formular korrekt anzeigen
 
Zeig doch dein Warte-Formular mittels ShowModal an. Zusätzlich müsstest du nur noch das Schließen verhindern, solang der Thread läuft, z.B. im OnCloseQuery Event.

rollstuhlfahrer 24. Aug 2010 18:12

AW: "Bitte warten"-Formular korrekt anzeigen
 
oder kann man nicht das "Bitte Warten"-Fenster per nonVCL erzeugen und in einem Extra-Thread ausführen? - so kann die aktuelle Programmstruktur bestehen bleiben.

Bernhard

shmia 24. Aug 2010 18:15

AW: "Bitte warten"-Formular korrekt anzeigen
 
Mach's doch so:
Delphi-Quellcode:
var
  ...
  frmWait: TfrmWarten;
begin
  frmWait:=TfrmWarten.Create(nil);
  try
    //frmWait.FormStyle:=fsStayOnTop; // lieber nicht-das ist zu "agressiv" für den Benutzer
    frmWait.ShowMarqueeBar(True);
    frmWait.Show;
    //Aktionen, die viel Zeit brauchen
    frmWait.AddMessage('Datenbank wird überprüft...');
    ... // arbeiten
    frmWait.AddMessage('Index neu anlegen...');
    ...
    frmWait.AddMessage('Fertig');
    frmWait.Finish;
  finally
    frmWait.Free;
  end;
end;
Die Methode AddMessage() ruft intern immer auch noch BringToFront auf, damit das "Warten"-Formular nicht permanent hinter dem Hautpformular verschwindet.

Die Methode Finish sieht so aus:
Delphi-Quellcode:
procedure TfrmWarten.Finish;
begin
   PlaySound('RingIn', 0, SND_ALIAS);
   Cursor := crDefault;
   Hide;
   BtnClose.Visible := True; // Schliesen-Button einblenden
   ShowModal;
end;
Somit muss der Benutzer das Fenster zum Schluss schliesen.
Grund: er soll die Gelegenheit haben die Meldungen zu lesen.

Satty67 24. Aug 2010 19:47

AW: "Bitte warten"-Formular korrekt anzeigen
 
Ich weis nicht ob das evtl. etwas "schmutzig" ist...

...das aufgerufene modale fenster hat eine property für eine "Arbeits-Methode", vergleichbar mit einem Event-Handler.

Das "Bitte warten" Fenster mit Progressbar ist wie folgt ausgestattet:
Delphi-Quellcode:
type
  TWorkerMethode = procedure (Sender : TObject; AProgressBar : TProgressBar) of object;

  TFormWorkProgress = class(TForm)
    ProgressBar1: TProgressBar;
    Timer1: TTimer;
    procedure Timer1Timer(Sender: TObject);
  private
    { Private-Deklarationen }
    FWorkerMethode : TWorkerMethode;
  public
    { Public-Deklarationen }
    property WorkerMethode : TWorkerMethode read FWorkerMethode write FWorkerMethode;
  end;

var
  FormWorkProgress: TFormWorkProgress;

implementation

{$R *.dfm}

procedure TFormWorkProgress.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  if assigned(FWorkerMethode) then
    FWorkerMethode(self, ProgressBar1);
end;
Das Hauptformular mit der Arbeits-Procedure so:
Delphi-Quellcode:
procedure TForm1.Button1Click(Sender: TObject);
var
  FormWorkProgress : TFormWorkProgress;
begin
  FormWorkProgress := TFormWorkProgress.Create(Application);
  try
     FormWorkProgress.WorkerMethode := Worker;
     FormWorkProgress.ShowModal;
  finally
    FormWorkProgress.Free;
  end;
end;

procedure TForm1.Worker(Sender : TObject; AProgressBar : TProgressBar);
var
  i : Integer;
begin
  for i := 1 to 100 do
  begin
    AProgressBar.Position := i;
    Sleep(100);
    Application.ProcessMessages;
  end;
  (Sender as TForm).Close;
end;
Nur ganz grob... etwas Feintunig ist da noch nötig... das modale Fenster ist etwas blockiert. Evtl. das Close aus dem Timer raus (kann ja der Worker aufrufen, er hat ja den Sender)

€: Etwas angepasst, jetzt stellt sich die App nicht mehr Tod.

Luckie 24. Aug 2010 22:11

AW: "Bitte warten"-Formular korrekt anzeigen
 
Hier noch mal zwei Links mit Hintergrundinformationen:
The correct order for disabling and enabling windows
Be careful that your splash screen doesn't squander the foreground love

hoika 25. Aug 2010 06:52

AW: "Bitte warten"-Formular korrekt anzeigen
 
Hallo,

siehe auch

http://www.delphipraxis.net/79498-ic...showmodal.html

Thread #7


Heiko

TheMiller 25. Aug 2010 10:05

AW: "Bitte warten"-Formular korrekt anzeigen
 
Vielen Dank für die vielen Antworten. Leider komme ich heute und morgen vllt auch nicht dazu, diese auszuprobieren. Ich melde mich wieder, wenn ich weiterarbeiten konnte.

Danke sehr!

TheMiller 26. Aug 2010 00:33

AW: "Bitte warten"-Formular korrekt anzeigen
 
Hallo,

auch auf die Gefahr hin, dass ich mich jetzt fürchterlich blamiere, stelle ich diesen Quelltext online. Es ist verdammt spät und ich habe kaum noch Auffassungsgabe.

Also, mein Ziel ist es, einen "Bitte-Warten"-Dialog schnell und einfach zu erstellen, auf dem permanent eine Marquee-Bar oder Statusbar zu sehen ist und ein entsprechendes Aktions-Label. Alle drei Komponenten sollen regelmäßig aktualisiert werden, während im Hauptprogramm Berechnungen durchgeführt werden.

Also habe ich ein vorgebasteltes Formular, einen Thread und eine "Wrapper-Klasse". Das Formular wird nicht automatisch erstellt. Das Dialog-Fenster wird komplett über die Wraptter-Klasse gesteuert. Es wird durch sie ein Thread erzeugt, der das Formular erzeugt. Der Thread wird mit einer while-Schleife am Leben gehalten, bis in der Wrapper-Klasse "Finished:=True" gesetzt ist.

Problem ist, dass die Komponenten nicht aktualisiert werden. Erst, wenn alle Berchnungen fertig sind, werden die Komponenten neu gezeichnet und die Anwendung friert während der Berechnungen ein.

Hier der Quelltext, extra schön kommentiert.

Anwendung des Warten-Dialogs in der Hauptanwendung
Delphi-Quellcode:
procedure TForm1.Button2Click(Sender: TObject);
var
  w: TWaitForm;
  i,j: Integer;
begin
  w:=TWaitForm.Create;
  w.CreateWaitForm;
  for i:=0 to 100000 do
  begin
    randomize();
    j:=random(9999);
    w.AddMessage(IntToStr(j));
  end;
  w.Finished:=True;
  //w.DestroyWaitForm;
end;
Bevor der Quelltext der Unit kommt, eine kurze Erklärung, wie was abläuft:
  1. Wrapper-Objekt wird erstellt (TWaitForm.Create)
  2. CreateWaitForm (Thread wird erstellt => Execute erstellt Form via Synchronize über Wrapper-Fkt "ThreadCreateWaiForm)
  3. Execute zeichnet in Schleife Form über Synchronize neu, bis Wrapper-Finished=True
  4. MainVCL-Aufruf: Wrapper.AddMessage ruft Thread.SetMessage auf, dieser setzt Variable für Aktionslabel und ruft Wrapper-ThreadAddMessage via Synchronize auf

Nun endlich der Code, bei dem durch Test noch veraltete Codefragmente bestehen könnten

Delphi-Quellcode:
type
  //Das ist das vorgefertigte Formular
  TfrmWarten = class(TForm)
    GroupBox1: TGroupBox;
    Label1: TLabel;
    lblAktion: TLabel;
    Label3: TLabel;
    StatusBar: TProgressBar;
    lblPercent: TLabel;
    MarqueeBar: TUbuntuProgress;
  private
  public

  end;

  //Eine Forward-Deklaration
  TWaitThread = class;

  //Das ist die Wrapper-Klasse, die einfach nur die Handhabung
  //im Hauptquelltext erleichtern soll
  TWaitForm = class(TObject)
  private
    WartenForm: TfrmWarten;
    wt: TWaitThread;
    fFinished: Boolean;
  public
    procedure CreateWaitForm;
    procedure DestroyWaitForm;
    procedure AddMessage(Value: String);

    //Prozeduren, die vom Thread per Synchronize aufgerufen werden
    procedure ThreadCreateWaitForm;
    procedure ThreadShowWaitForm;
    procedure ThreadAddMessage;

    property Finished: Boolean read fFinished write fFinished;
  end;

  //Das ist der Thread, der für die Aktualisierung der Komponenten
  //zuständig ist.
  TWaitThread = class(TThread)
  private
    fCreated: Boolean;
    fwf: TWaitForm;
    ffrmWarten: TFrmWarten;
    fMsg: String;
    procedure ShowWaitForm;
  public
    procedure SetMessage(Value: String);
    property FormCreated: Boolean read fCreated write fCreated;
    property WaitFormWrapper: TWaitForm read fwf write fwf;
    property Msg: String read fMsg write fMsg;
  protected
    procedure Execute; override;
  end;

var
  frmWarten: TfrmWarten;

implementation

{$R *.dfm}

{************** TWaitThread ***************}

procedure TWaitThread.Execute;
begin
  inherited;
  Synchronize(WaitFormWrapper.ThreadCreateWaitForm);
  Synchronize(WaitFormWrapper.ThreadShowWaitForm);
  while not (WaitFormWrapper.Finished) do
  begin
    //Application.ProcessMessages;
    Synchronize(WaitFormWrapper.ThreadShowWaitForm);
  end;
end;

procedure TWaitThread.ShowWaitForm;
begin
  Synchronize(WaitFormWrapper.ThreadShowWaitForm);
end;

procedure TWaitThread.SetMessage(Value: string);
begin
  Msg:=Value;
  Synchronize(WaitFormWrapper.ThreadAddMessage);
end;

{************** TWaitForm ***************}

procedure TWaitForm.CreateWaitForm;
begin
  wt:=TWaitThread.Create(True);
  wt.FormCreated:=False;
  wt.WaitFormWrapper:=Self;
  wt.FreeOnTerminate:=False;

  //Im Execute des Threads wird das Fenster erstellt und angezeigt
  wt.Resume;
  while not (wt.FormCreated) do
    Application.ProcessMessages;
end;

procedure TWaitForm.ThreadCreateWaitForm;
begin
  WartenForm:=TFrmWarten.Create(nil);
  wt.FormCreated:=True;
end;

procedure TWaitForm.DestroyWaitForm;
begin
  FreeAndNil(WartenForm);
  wt.Free;
end;

procedure TWaitForm.AddMessage(Value: String);
begin
  wt.SetMessage(Value);
end;

procedure TWaitForm.ThreadShowWaitForm;
begin
  if not (WartenForm.Visible) then
    WartenForm.Show;
  WartenForm.lblAktion.Caption:=IntToStr(random(999));
  WartenForm.lblAktion.Repaint;
end;

procedure TWaitForm.ThreadAddMessage();
begin
  WartenForm.lblAktion.Caption:=wt.Msg;
end;

end.
Wie gesagt, auch wenn ich mich jetzt blamiere. Vielleicht habe ich einen gewaltigen Denkfehler oder doch nur eine Kleinigkeit vergessen. Aber ich finde den Wald vor lauter Bäumen nicht...

Bitte helft mir und erklärt mir ggf. meinen Gedankenfehler.

Danke und gute Nacht. Ich hau ich jetzt endlich hin ;)

Björn Ole 26. Aug 2010 02:59

AW: "Bitte warten"-Formular korrekt anzeigen
 
Liste der Anhänge anzeigen (Anzahl: 1)
Hab jetzt noch nicht genau den Aufbau deiner Idee geblickt, aber ich häng dir mal eine kleine Demo an.

Ausschnitt:
Delphi-Quellcode:
procedure TfrmMain.Button1Click(Sender: TObject);
var
  frmWait: TfrmWait;
begin
  // Wartefenster erstellen
  frmWait := TfrmWait.Create(nil);
  // Thread starten
  TWorkerThread.Create(frmWait).OnTerminate := OnWorkerThreadTerminate;
  // Wartefenster anzeigen
  frmWait.ShowModal;
end;
Und im Thread, der die Aufgaben erledigt, dann so:
Delphi-Quellcode:
procedure TWorkerThread.Execute;
begin
  // setzt mit Synchronize das Label im Wartefenster
  SetAction('Wichtige Berechnung');
  Sleep(2000);
end;
Sobald der Thread terminiert, wird das Wartefenster geschlossen.

Ich hoffe mal, das ist in etwa das, was du brauchst. :stupid:

TheMiller 26. Aug 2010 09:18

AW: "Bitte warten"-Formular korrekt anzeigen
 
Danke für die Antwort.

Der Sinn meiner Idee - ob das klapp oder nicht sei dahingestellt - war, eine einfache Klasse zu haben, die für mich den Thread und das WaitForm erstellt und auch wieder freigibt. So muss ich mich jedesmal selbst um den Thread etc. kümmern.

Der Unterschied unserer Versionen scheint zu sein, dass in deiner die Berechnungen im Thread durchgeführt werden.
Bei mir werden die Berechnungen im MainThread durchgeführt und existierten, bevor mir die Idee mit dem "Bitte-Warten"-Fenster kam. D.h, mein Thread soll so lange das Form anzeigen und aktualisieren, bis die Wrapper-Klasse den Status "Finished" erreicht hat.

Aber da liegt glaub ich das Problem. Während der MainThread die Berechnung durchführt, ist er blockiert und kann auch nicht das Warten-Fenster neuzeichnen, richtig?

Das würde auch bedeuten, dass ich alle Aktionen, die das Fenster benutzen sollen, in einen Thread verschoben werden müssen und ich so das Programm komplett umstellen darf... auch richtg?

Danke

blauweiss 26. Aug 2010 10:42

AW: "Bitte warten"-Formular korrekt anzeigen
 
Hallo DJ-SPM,

das hat jetzt nichts mit deinem eigentlich Problem zu tun, nur ein Hinweis:
Randomize soll in einem Programm nur 1-mal aufgerufen werden.
Du rufst es x-mal in einer Schleife auf.....
Delphi-Quellcode:
  for i:=0 to 100000 do
  begin
    randomize();
Gruss,
blauweiss

TheMiller 26. Aug 2010 11:46

AW: "Bitte warten"-Formular korrekt anzeigen
 
Ja ich weiß ;)

Das war nur, weil die Schleife sowie nur den Testzwecken dient und ich, als ich es gemerkt habe, keine Lust mehr hatte, es umzuändern (da eh nur Test) ;)

Aber trotzdem danke für den Hinweis

Björn Ole 26. Aug 2010 12:34

AW: "Bitte warten"-Formular korrekt anzeigen
 
Zitat:

Zitat von DJ-SPM (Beitrag 1045147)
Während der MainThread die Berechnung durchführt, ist er blockiert und kann auch nicht das Warten-Fenster neuzeichnen, richtig?

Genau. Grundsätzlich sollte gelten, dass der Main Thread für die Oberfläche zuständig ist und alle aufwendigeren Berechnungen in einen weiteren Thread ausgelagert und Status-Meldung o.ä. synchronisiert an den Main-Thread geschickt werden.

Zitat:

Zitat von DJ-SPM (Beitrag 1045147)
Der Unterschied...

...ist mir auch erst nach meiner Antwort aufgefallen, hätte ich früher sehen müssen. :oops:

Als Alternative zur Auslagerung der Berechnungen in einen neuen Thread könntest du, wie rollstuhlfahrer hier früher schon erwähnt hat, in einem Thread das Warte Fenster nonVCL erstellen. So könntest du dann z.B. mit PostThreadMessage den Warte-Fenster-Thread über Updates benachrichtigen, während der Main Thread am werkeln ist. Schätze ab, was dir mehr Arbeit macht. :wink:

TheMiller 26. Aug 2010 22:38

AW: "Bitte warten"-Formular korrekt anzeigen
 
Liste der Anhänge anzeigen (Anzahl: 1)
Hallo,

ich habe nun also in den sauren Apfel gebissen und die Aufgaben in einen Thread ausgelagert (bzw. das werde ich alles nach und nach machen).

Mein Warten-Formular und der Thread dazu steht und funktioniert. Meine Wrapper-Klasse wollte ich dennoch behalten. So ergibt sich der unten aufgeführte Code.

Ich habe also eine Wrapper-Klasse, die die Warten-Form anzeigt und den Thread startet. Zuvor wird der Wrapper-Klasse gesagt, welche Aktion der Thread zu erledigen hat. Der Thread startet und führt die für die Aktion hinterlegte Berechnung durch. Danach gibt sich die Klasse über den Thread wieder frei.

Wer's haben mag, darf es gerne nutzen. Schaut euch bitte meinen Code an und sagt, was positiv und was negativ ist. Danke..! (Achso: Bisher gab's keine Probleme bez. VCL, AVs etc.)

Anwendung der Wrapper-Klasse (Darstellen eines Warten-Bildschirms)
Delphi-Quellcode:
procedure TForm1.Button2Click(Sender: TObject);
var
  wu: TWaitUtils;
begin
  //Erstellt ein WaitUtil-Objekt, zeigt das Fenster an
  wu:=TWaitUtils.Create;

  //Erstellt einen Thread, sagt ihm was zu tun ist und lässt den Dingen seinen lauf
  wu.InitWaitThread(taStoreAttachment);

  //Freigeben nicht nötig, geschieht automatisch (laut FastMM4 alles OK).
end;
Und hier das, was wie im Hintergrund passiert (Auszug)

Delphi-Quellcode:
type
  TThreadActions = (taStoreAttachment, taLoadAttachment);

{************** TWaitThread ***************}
procedure TWaitThread.Execute;
begin
  case (Action) of

    taStoreAttachment:
    begin
      StoreAttachment;
    end;

    taLoadAttachment:
    begin

    end;
   
  end;
end;

{************** TWaitUtils ***************}
constructor TWaitUtils.Create;
begin
  InUse:=False;
  WartenForm:=TfrmWarten.Create(nil);
  Thread:=TWaitThread.Create(True);
  Thread.WaitFormular:=WartenForm;
  Thread.FreeOnTerminate:=True;
  Thread.WaitUtilObject:=Self;
  WorkerThread:=Thread;
  WorkerThread.OnTerminate:=ThreadTerminate;
end;

procedure TWaitUtils.InitWaitThread(ta: TThreadActions);
begin
  InUse:=True;
  ShowWaitForm;

  //Welche Aktion soll der Thread ausüben?
  Thread.Action:=ta;

  //Thread ins Rollen bringen
  Thread.Resume;
end;

procedure TWaitUtils.ThreadTerminate(Sender: TObject);
begin
  InUse:=False;
  with (Sender as TWaitThread).WaitFormular do
  begin
    Close;
  end;

  //Das Warten-Formular freigeben
  DestroyWaitForm;
 
  //Das WaitUtilObjekt wieder freigeben
  Thread.WaitUtilObject.Free;
end;
Ich häng's hier nochmal an, vielleicht mögt ihr es nutzen oder mal "Korrekturlesen"

Danke!


Alle Zeitangaben in WEZ +1. Es ist jetzt 23:39 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz