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
...
...