Entwickler-Ecke

Sonstiges - ...eine Anwendung - gleichzeitig portabel UND installierbar


jaenicke - Di 30.12.08 01:37
Titel: ...eine Anwendung - gleichzeitig portabel UND installierbar
Hallo!

Es gibt ja immer wieder die Frage, wo und wie ich am besten die Einstellungen zu einer Anwendung speichere. Manche möchten eher eine portable Anwendung, andere lieber eine feste Installation. Ideal wäre es doch, wenn man beide Seiten gleichzeitig zufrieden stellen könnte, ohne dafür verschiedene Versionen des Programms erstellen zu müssen.

Genau das ist möglich, wenn man es richtig angeht. Wie das geht, werde ich hier ausführen und an Hand einer Demo zeigen.
  1. Die Überlegung dahinter - wie geht es am besten [http://www.delphi-library.de/viewtopic.php?p=541638#541638]
  2. Der Quelltext dazu konkret [http://www.delphi-library.de/viewtopic.php?p=541639#541639]
  3. SJConfigUtils - eine Unit, die die Arbeit teilweise abnimmt [http://www.delphi-library.de/viewtopic.php?p=541640#541640]
  4. Das Demoprojekt [http://www.delphi-library.de/viewtopic.php?p=541641#541641]
Zusätzlich gibt es in einem weiteren Beitrag noch eine Erklärung dazu, warum es überhaupt notwendig ist, das Anwendungsdatenverzeichnis zu benutzen und man nicht einfach immer das Verzeichnis für Einstellungen nutzen kann, in dem das Programm selbst liegt:
http://www.delphi-library.de/viewtopic.php?p=541636#541636


jaenicke - Di 30.12.08 01:38

1. Die Überlegung dahinter - wie geht es am besten

Es gibt mehrere Orte, an denen Einstellungen abgelegt werden können. Diese gelten teilweise für alle Benutzer des PCs oder werden bei einer Netzwerkanmeldung im Netzwerkprofil gespeichert, so dass auf allen PCs, auf denen sich der Benutzer anmeldet, diese Einstellungen vorhanden sind, und so weiter.

Ideal wäre also eine Lösung, die alle diese Möglichkeiten unter einen Hut bringt und dem Benutzer die Wahl lässt wo die Einstellungen liegen sollen.

Die Lösung ist, einfach in einer bestimmten Reihenfolge an den verschiedenen Orten nach den Einstellungen zu suchen (ich gebe hier die Standardpfade an, diese können auch anders lauten :!: ):
Mit dieser Reihenfolge ist gewährleistet, dass die Einstellungen des einzelnen Benutzers vor denen aller Benutzer berücksichtigt werden, und die lokalen für den PC vor denen des Benutzers auf allen PCs.

Zudem kann man eine portable Version vom USB-Stick mit deren Einstellungen starten ohne dass die Einstellungen einer installierten Version auf dem PC berücksichtigt oder verändert werden.


jaenicke - Di 30.12.08 01:38

2. Der Quelltext dazu konkret

Hiermit suche ich in FindSettingsFile im Verzeichnis der Exe und in den drei Anwendungsdatenverzeichnissen. In dem FormCreate rufe ich das zuerst auf und suche, wenn keine Datei gefunden wurde, in der Registry.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
// Es ist sinnvoll den Pfad an einer Stelle möglichst zusammenzusetzen,
// damit man nicht beim Lesen und Schreiben plötzlich einen Tippfehler
// drin hat und sich wundert warum es nicht geht.
function GetSettingsFileName(ARootDir: String): String;
begin
  Result := ARootDir + AppDataRootDir + AppDataProjectDir + '\MySettings.txt';
end;

// Sucht in den 4 Ordnern der Reihe nach und legt den gefundenen
// Dateinamen in AFileName. Der Rückgabewert gibt an, ob eine Datei
// gefunden wurde.
function FindSettingsFile(var AFileName: String): Boolean;
begin
  AFileName := ExtractFilePath(ParamStr(0)) + 'MySettings.txt';
  Result := FileExists(AFileName);
  if Result then
    Exit;
  AFileName := GetSettingsFileName(GetSpecialFolder(CSIDL_LOCAL_APPDATA));
  Result := FileExists(AFileName);
  if Result then
    Exit;
  AFileName := GetSettingsFileName(GetSpecialFolder(CSIDL_APPDATA));
  Result := FileExists(AFileName);
  if Result then
    Exit;
  AFileName := GetSettingsFileName(GetSpecialFolder(CSIDL_COMMON_APPDATA));
  Result := FileExists(AFileName);
  if Result then
    Exit;
  AFileName := ''// nix gefunden, also leeren String zurückgeben
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  Reg: TRegistry;
  Filename: String;
begin
  if FindSettingsFile(Filename) then
    // wenn eine Einstellungsdatei gefunden wurde....
    ...
  else
  begin
    // sonst in der Registry suchen...
    Reg := TRegistry.Create;
    try
      Reg.RootKey := HKEY_CURRENT_USER;
      if Reg.OpenKeyReadOnly('Software' + AppDataRootDir + AppDataProjectDir) then
        ...
      else
      begin
        Reg.RootKey := HKEY_LOCAL_MACHINE;
        if Reg.OpenKeyReadOnly('Software' + AppDataRootDir + AppDataProjectDir) then
          ...
      end;
    finally
      Reg.Free;
    end;
  end;
end;
Das lässt sich natürlich auch anders machen, aber dies sollte nur ein kleines Beispiel dazu sein.


jaenicke - Di 30.12.08 01:39

3. SJConfigUtils - eine Unit, die die Arbeit teilweise abnimmt

Diese Unit habe ich hier auch separat als Open Source vorgestellt:
http://www.delphi-forum.de/viewtopic.php?p=562996

Die Unit übernimmt die Verwaltung der Einstellungen, wo sie gespeichert werden und das Suchen nach Einstellungen beim Start. Aber natürlich kann die Unit die speziellen Einstellungen der Anwendung nicht kennen. Ich habe das so gelöst, dass es eine Klasse gibt, die die Verwaltung übernimmt und dem Programmierer dann praktisch sagt was er machen muss.

Dafür enthält die Klasse mehrere abstrakte Methoden, die implementiert werden müssen:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
    procedure LoadFromStream(Data: TStream); override;
    procedure SaveToXml(Ini: IXMLDocument); override;
    procedure LoadFromINI(Ini: TCustomIniFile); override;
    procedure LoadFromRegistry(Reg: TRegistry; ParentPath: String); override;

    procedure SaveToStream(Data: TStream); override;
    procedure LoadFromXml(Ini: IXMLDocument); override;
    procedure SaveToINI(Ini: TCustomIniFile); override;
    procedure SaveToRegistry(Reg: TRegistry; ParentPath: String); override;

    procedure RunConfigWizard; override;
    procedure ApplyStandardSettings; override;
    procedure GetProgramInfo(var Author, ProductName, ProductVersion: string); override;
Um diese zu implementieren, muss man nichts weiter wissen, alles nötige bekommt man geliefert.
Natürlich kann man per Compilerschalter auch einzelne dieser Möglichkeiten deaktivieren. Man muss also nicht z.B. INIs und die Registry unterstützen, nur die Unit tut es prinzipiell.

Durch die Unterstützung eines Streams als Ziel kann man beliebige Formate verwenden, von XML bis zu eigenen Speichermethoden.

Die Unit einzeln sowie eine genauere Erläuterung befinden sich in dem Vorstellungsthread in der Open Source Unit Sparte.
http://www.delphi-forum.de/viewtopic.php?p=562996

Ein Anwendungsbeispiel folgt in der Vorstellung des Demoprojektes [http://www.delphi-library.de/viewtopic.php?p=541641#541641] direkt anschließend.


jaenicke - Di 30.12.08 01:39

4. Das Demoprojekt

Beim Start muss lediglich die abgeleitete Klasse instantiiert werden und schon kann man die Einstellungen lesen:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
  TfrmMain = class(TForm)
  ...
  public
    { Public declarations }
    Config: TAppConfig;
  end;

...

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  Config := TAppConfig.Create;
  edtUserName.Text := Config.UserName;
  ...
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  Config.Free;
end;
Die Klasse TAppConfig wiederum sieht z.B. so aus, hier einmal nur für XML-Dateien:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
  TAppConfig = class(TAppConfigManager)
  private
  ...
  protected
  ...

implementation

procedure TAppConfig.RunConfigWizard;
begin
  frmConfigWizard := TfrmConfigWizard.Create(nil); // Wizard für die Einstellungen anzeigen
  frmConfigWizard.ShowModal(Self);
end;

procedure TAppConfig.ApplyStandardSettings;
begin
  fUserName := 'Standardname'
end;

procedure TAppConfig.GetProgramInfo(var Author, ProductName,
  ProductVersion: string);
begin
  Author := 'Sebastian Jänicke';
  ProductName := 'SJ Config Utils Demo';
  ProductVersion := '1.0';
end;

procedure TAppConfig.LoadFromXml(Ini: IXMLDocument);
begin
  if AccessManager.InitReadLocation('Userinfo', sIniSectionOpenError) then
  begin
    fUserName := AccessManager.ReadString('Username', sIniValueNotFound);
  end;
end;

procedure TAppConfig.SaveToXml(Ini: IXMLDocument);
begin
  if AccessManager.InitWriteLocation('Userinfo', sIniSectionOpenError) then
  begin
    AccessManager.WriteString('Username', fUserName);
  end;
end;
Das komplette Demoprojekt gibt es im Vorstellungsthread der Open Source Unit:
http://www.delphi-forum.de/viewtopic.php?p=562996


jaenicke - Mi 01.07.09 20:51

Abschließend bleibt zu sagen, dass die SJ Config Utils sicher nicht besonders umfangreich sind, die wirklich gleichen Aufgaben aber nach Möglichkeit abnehmen. Manche Features fehlen bewusst, weil es nicht so sinnvoll war, die einzubauen.

Der Sinn von diesem Thread ist auch vor allem zu zeigen wie man die verschiedenen Möglichkeiten der Speicherung (portabel, nicht portabel) sinnvoll kombinieren kann. Denn da gibt es eben viele, die portabel gut finden, und viele, die das nicht gut finden. Deshalb ist für viele Programme eine solche Kombilösung eine gute Alternative zu einer Festlegung auf eine bestimmte Speicherung.

Wenn jemand Änderungswünsche oder Ergänzungsvorschläge zu den SJ Config Utils hat, dann ist der Vorstellungsthread [http://www.delphi-forum.de/viewtopic.php?p=562996] der Unit der richtige Ort, unter anderem dafür habe ich die Threads überhaupt so getrennt.