AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Tutorial: Threaded SplashScreen
Tutorial durchsuchen
Ansicht
Themen-Optionen

Tutorial: Threaded SplashScreen

Ein Tutorial von sakura · begonnen am 24. Dez 2011 · letzter Beitrag vom 13. Feb 2015
Antwort Antwort
Benutzerbild von sakura
sakura
Registriert seit: 10. Jun 2002
Liebe DP-Gemeinde,

schon lange war ich nicht mehr aktiv in der DP und die Gründe sind vielfältig. Aber dieses Jahr will ich Euch ein kleines Tutorial einstellen, zu einem Thema, welches immer wieder mal aufkommt.

Die Idee zu dieser Methode kam mir als ich die neuen MS Office™ Produkte sah. Während eine der Anwendungen startet, kann man den Start jederzeit ohne Probleme abbrechen. Das wird dadurch ermöglicht, dass der SplashScreen zu jederzeit interaktiv ist und nicht im Hauptthread der Anwendung läuft.

Außerdem war es mir wichtig, dass der SplashScreen sehr schnell auf dem Bildschirm erscheint. Das wird insbesondere dann zu einem Problem, wenn man viele und umfangreiche Packages in der Anwendung nutzt. Da diese Packages geladen werden, bevor auch nur die erste Zeile in der Anwendung ausgeführt wird, kann es schon mal ein paar Sekunden dauern, bevor die Anwendung ihren eigenen SplashScreen darstellen kann.

Problem: Eine, des Öfteren vorgeschlagene Methode ist der Umweg über eine separate Starter-Anwendung. Aber seit Windows 7 ist das auch kein besonders praktikabler Weg, da der Nutzer Anwendungen direkt in der Taskleiste anheften kann. Ja nach Moment des Anheftens, würde der Nutzer entweder die Starter-Anwendung anheften oder die eigentliche Anwendung. Wenn es die Starteranwendung ist, dann erscheint zwar immer der SplashScreen, aber die eigentliche Anwendung erhält zur Laufzeit ihr eigenes Icon in der Taskleiste. Wenn es die eigentliche Anwendung war, die der Nutzer angeheftet hat, dann erscheint kein SplashScreen beim Start.

Die Idee: Ein SplashScreen in einem eigenen Thread darstellen, welcher sofort zum Start der Anwendung ausgeführt wird.

Der Haken: Die VCL von Delphi ist nicht thread-sicher. Ein VCL Form kann nicht in einem separaten Thread dargestellt werden.

Die Lösung: Zwei Instanzen VCL der VCL in eigenen Anwendungen erstellen (siehe Problem), aber in eine Anwendung integrieren.

Wenn man sich über Lösung jetzt mal einen Moment und in Ruhe Gedanken macht, dann liegt die Lösung auf der Hand. Der SplashScreen wird in eine eigene Anwendung ausgelagert. Da wir den Ansatz einer Starter-Anwendung bereits abgelehnt haben, müssen wir eine dynamische Anwendung nutzen, eine DLL.

Delphi DLLs, wenn diese nicht(!) Packages nutzen und auch nicht als Package entwickelt wurden, verwalten die VCL komplett anonym zur ladenden Anwendung. Wenn wir unsere Anwendung also verpflichten eine DLL zu laden, welche die VCL enthält, dann erhalten wir zwei vollständig separate Instanzen der VCL. Somit können wir den SplashScreen in einem eigenen Thread darstellen.

Damit der SplashScreen möglichst frühzeitig dargestellt wird, müssen wir das Early-Binding von Delphi nutzen. Das Gute ist, dass Delphi-Anwendungen DLLs vor Packages laden, wenn wir das Early-Binding nutzen.

Gleich beim Laden der DLL starten wir einen neuen Thread, welcher den SplashScreen erstellt. Des Weiteren exportiert die DLL eine Method welche uns eine Schnittstelle auf den SplashScreen zurück liefert. Dieser implementiert ein eigenes Interface, im Beispiel ISplashScreen.
Delphi-Quellcode:
type
  // Interface für die Interaktion mit dem SplashScreen
  ISplashScreen = interface
    ['{0E3C4BEE-F2B3-48A9-A41C-337AD8FB52A4}']

    // Wenn es bei der Initialisierung zu Problemen kommt, dann kann der Abbruch
    // des Programmstarts an den SplashScreen mitgeteilt werden
    procedure CancelStart;
    // Liefert TRUE zurück, wenn der Programmstart (manuell oder programmatisch)
    // abgebrochen wurde, ansonsten FALSE.
    function IsStartCanceled: Boolean;
    // Schließt den SplashScreen und gibt alle Resourcen frei
    procedure CloseSplashScreen;

    // Erhöht den Schrittzählen im SplashScreen (TProgressBar) und gibt eine
    // Nachricht auf dem SplashScreen aus
    procedure NextStep(CurrentStep: PChar);

    // Blendet den SplashScreen kurzfristig aus
    procedure Show;
    // Zeigt den SplashScreen wieder an
    procedure Hide;
  end;
Wenn wir die Schnittstelle auf den SplashScreen ermittelt haben, dann können wir jederzeit auf diesen zugreifen und den Nutzer über den Stand des Ladevorganges informieren. Dabei muss beachtet werden, dass der Zugriff auf die VCL des SplashScreens nicht direkt durch die Anwendung erfolgt.

Hinweis: Das automatische Referenzcounting ist für VCL-Formulare nicht implementiert! Wir müssen uns also selbst um die Freigabe des SplashScreens und seiner Resourcen kümmern.

Die Lösung ist recht simpel. Wenn wir zum Beispiel neuen Text ausgeben wollen, dann müssen wir eine Variabel setzen, welche den neuen Text speichert. Das Form des SplashScreens überprüft regelmäßig (z.B.: mittels TTimer), ob es neue Anweisungen gibt und führt diese dann aus (z.B.: neuen Text darstellen).

Kurz bevor die Anwendung gestartet wird (Application.Run in der DPR-Datei), müssen wir nochmals überprüfen, ob der Start evtl. abgebrochen wurde. Ist dieses der Fall, müssen wir noch das Hauptformular der Anwendung wieder freigeben, damit dieses nicht mehr angezeigt wird und die Anwendung sich selbst und ordentlich beendet.

Nachfolgend die Implementation der wichtigsten Elemente der Anwendung. Im Anhang findet Ihr das komplette Projekt (getestet Delphi 2010, Delphi XE2).

Der Projekt Quelltext
Delphi-Quellcode:
function GetSplashScreen(FinalStepCount: Integer): ISplashScreen; external 'OurSplash.dll';

var
  SplashScreen
    : ISplashScreen;
begin
  // ReportMemoryLeaksOnShutdown := True;

  SplashScreen := GetSplashScreen(1005);

  Application.Initialize;
  Application.MainFormOnTaskbar := True;

  // langwierige Initialisierungen...

  if not SplashScreen.IsStartCanceled then
  begin
    Application.CreateForm(TfrmHello, frmHello);

    // noch mehr langwierige Initialisierungen...

    if SplashScreen.IsStartCanceled then
      frmHello.Free;
  end;

  SplashScreen.CloseSplashScreen;
  Application.Run;
end.
Auszüge aus der SplashScreen Form Unit
Delphi-Quellcode:
type
  TThreadedSplashForm = class(TThread)
  protected
    procedure Execute; override;
  end;

{ TThreadedSplashForm }

procedure TThreadedSplashForm.Execute;
begin
  TfrmSplash.frmSplash := TfrmSplash.Create();
  TfrmSplash.frmSplash.ShowModal;
  FreeOnTerminate := True;
end;

{ TfrmSplash }

procedure TfrmSplash.CancelStart;
begin
  FCriticalSection.Acquire;
  try
    FDoCancel := True;
  finally
    FCriticalSection.Release;
  end;
end;

procedure TfrmSplash.CloseSplashScreen;
begin
  FCriticalSection.Acquire;
  try
    FDoClose := True;
  finally
    FCriticalSection.Release;
  end;
end;

constructor TfrmSplash.Create();
begin
  inherited Create(nil);

  Position := poScreenCenter;

  FDoCancel := False;
  FIsCanceled := False;
  FDoInit := False;
  FDoClose := False;
  FDoHide := False;
  FDoShow := False;
  FMessage := 'Bibliotheken und Packages werden geladen';
  FStepIncrement := 0;
  FCriticalSection := TCriticalSection.Create;
end;

destructor TfrmSplash.Destroy;
begin
  FCriticalSection.Free;

  inherited Destroy;
end;

procedure TfrmSplash.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  CanClose := FDoClose;
  if not CanClose then
    CancelStart;
end;

procedure TfrmSplash.FormKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = Char(VK_ESCAPE) then
    CancelStart;
end;

class function TfrmSplash.GetSplashScreen(FinalStepCount: Integer): TfrmSplash;
begin
  if not FSplashReturned then
  begin
    FSplashReturned := True;
    while frmSplash = nil do
      Sleep(50);
    frmSplash.DoInit(FinalStepCount);
  end;

  Result := frmSplash;
end;

function TfrmSplash.IsStartCanceled: Boolean;
begin
  Result := FIsCanceled;
end;

procedure TfrmSplash.NextStep(CurrentStep: PChar);
begin
  FCriticalSection.Acquire;
  try
    if FIsCanceled then
      Exit;

    FMessage := CurrentStep;
    Inc(FStepIncrement);
  finally
    FCriticalSection.Release;
  end;
end;

procedure TfrmSplash.tmrUpdateScreenTimer(Sender: TObject);
begin
  if not (FDoInit or FDoCancel or FDoClose or (FStepIncrement <> 0)) then
    Exit;

  tmrUpdateScreen.Enabled := False;
  try
    FCriticalSection.Acquire;
    try
      DoCheckInit;
      DoCheckCancel;
      DoCheckProgress;
      DoHideSplash;
      DoShowSplash;
    finally
      FCriticalSection.Release;
    end;
    Application.ProcessMessages;
  finally
    tmrUpdateScreen.Enabled := not FDoClose;
  end;

  DoCheckClose;
end;

initialization
  TThreadedSplashForm.Create(False);

end.
Wie schon geschrieben, das vollständige und lauffähige Projekt ist mit allen Sourcen im Anhang

sakura
......
Angehängte Dateien
Dateityp: zip DP X-MAS.zip (300,5 KB, 301x aufgerufen)
Ich bin nicht zurück, ich tue nur so
 
Benutzerbild von Helmi
Helmi

 
Delphi XE2 Professional
 
#2
  Alt 24. Dez 2011, 15:11
Hallo Sakura,

dein Tutorial klingt sehr interessant - nur leider kann ich es mit meinem (historischen) Delphi 7 nicht kompilieren.
Wäre es möglich, dass du es für ältere Delphi-Versionen abänderst, bzw. für ältere Versionen eine eigene Version
zimmerst?

Schon mal Danke im Voraus!
  Mit Zitat antworten Zitat
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#3
  Alt 24. Dez 2011, 15:37
Nicht wirklich, ich habe nichts älteres als Delphi 2010 mehr installiert.

An und für sich ist aber eigentlich nicht viel enthalten, was Probleme bereiten könnte. Evtl. mal das JPG aus dem SplashScreen-Form entfernen. Der Rest ist eigentlich Standard seit Delphi 4 oder so.

......
Daniel W.

Geändert von sakura (24. Dez 2011 um 15:37 Uhr) Grund: naja
  Mit Zitat antworten Zitat
Benutzerbild von jaenicke
jaenicke

 
Delphi 11 Alexandria
 
#4
  Alt 24. Dez 2011, 16:01
dein Tutorial klingt sehr interessant - nur leider kann ich es mit meinem (historischen) Delphi 7 nicht kompilieren.
Viel zu ändern ist da doch nicht. In der Hauptanwendung ReportMemoryLeaksOnShutdown und Application.MainFormOnTaskbar raus, in der DLL aus den Klassenvariablen globale Variablen machen, das war es schon fast.

Dann gibt es TProgressBar.State noch nicht, das heißt du müsstest dir etwas anderes für einen Fehlerstatus einfallen lassen oder das über Windows Messages direkt in dem Progress Bar Control von Windows umschalten.

Wo etwas zu ändern ist, sagt dir ja der Compiler...
Sebastian Jänicke
  Mit Zitat antworten Zitat
neo4a

 
Delphi XE2 Architect
 
#5
  Alt 27. Dez 2011, 11:47
Was für eine bemerkenswerte, aufwändige Lösung für eine eigentlich simple Anforderung:

- DLL mit Early Binding
- Shared Interfaces
- und threaded Form mit Critical Section Isolierung

Und obwohl man in diesem Beispiel eine DLL erhält, die genauso groß ist wie die eigentliche App, ist dieser Ansatz auch sehr gut für Non-Blocking-GUIs (z.B. eine Druck-Ausgabe) geeignet, zumal sich das gemeinsame Interface kinderleicht erweitern lässt.
Andreas
  Mit Zitat antworten Zitat
FAM

 
Delphi XE Enterprise
 
#6
  Alt 13. Feb 2015, 10:14
hallo zusammen,

das beispielprojekt kann ich ganz normal ausführuen, nur wenn ich es in mein eigenens projekt kopieren möchte (OurSplash.dll + Anpassung Main.dpr Datei) startet die anwendung komplett nicht.

Muss ich irgendwas noch an den Build-Konfigurationen einstellen?

Wenn ich in dem von mir erstellen Projekt die rechte maustaste auf die OurSplash.dll mache (bei mir heißt diese SplashScreen.dll) bekomme ich folgenden fehler

error01.png

Geändert von FAM (13. Feb 2015 um 10:20 Uhr)
  Mit Zitat antworten Zitat
Daniel

 
Delphi 10.4 Sydney
 
#7
  Alt 13. Feb 2015, 10:18
Was passiert denn, wenn Du die Anwendung startest?
Und wenn Du sie im Debugger ausführst, wie weit kommt sie denn?
Daniel R. Wolf
  Mit Zitat antworten Zitat
FAM

 
Delphi XE Enterprise
 
#8
  Alt 13. Feb 2015, 10:24
Was passiert denn, wenn Du die Anwendung startest?
Und wenn Du sie im Debugger ausführst, wie weit kommt sie denn?
Habe ein Breakpoint gesetzt, passiert aber so mal garnichts

error02.jpg

ich vermute es liegt an der Instanziierung von

SplashScreen := GetSplashScreen(1); die warnung im Screenshot bitte ignorieren, das habe ich gefixed

komisch ist nur das garkein fehler kommt...

Geändert von FAM (13. Feb 2015 um 10:32 Uhr)
  Mit Zitat antworten Zitat
FAM

 
Delphi XE Enterprise
 
#9
  Alt 13. Feb 2015, 10:44
Fehler gefunden, es lag tatsächlich an den Build-Konfiguration - das Ausgabeverzeichnis musste noch auf den aktuellen Main(App) Output gesetzt werden.
  Mit Zitat antworten Zitat
Antwort Antwort

 

Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 08:51 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